rsm-roguchi commited on
Commit
7b68ff7
·
1 Parent(s): 3f587d8

update blogs

Browse files
Files changed (5) hide show
  1. Dockerfile +18 -19
  2. pyproject.toml +2 -0
  3. server/blog.py +210 -118
  4. ui/blog.py +5 -3
  5. uv.lock +83 -0
Dockerfile CHANGED
@@ -1,30 +1,29 @@
1
- # Dockerfile for Weather Assistant Demo
2
- # This file configures how the application will run on Hugging Face Spaces
3
 
4
- # Start with Python 3.12 as our base image
5
- # This gives us a clean Python environment to work with
6
- FROM python:3.12
 
 
 
 
 
7
 
8
  # Install UV package manager
9
- # UV is a fast, reliable Python package installer written in Rust
10
- # It's much faster than pip for installing dependencies
11
  RUN pip install uv
12
 
13
-
14
- # Copy all files from our project into the container
15
- # This includes app.py, pyproject.toml, and other necessary files
16
  COPY . .
17
 
18
- # Use UV to install all dependencies specified in pyproject.toml
19
- # This ensures we have the same packages as in local development
20
  RUN uv sync
21
 
22
- # Configure the container to listen on port 7860
23
- # This is the default port that Hugging Face Spaces expects
 
 
24
  EXPOSE 7860
25
 
26
- # Command to run when the container starts
27
- # This launches the Shiny app with specific host and port settings
28
- # - host 0.0.0.0 allows connections from outside the container
29
- # - port 7860 matches our EXPOSE setting above
30
- CMD ["/.venv/bin/shiny", "run", "app.py", "--host", "0.0.0.0", "--port", "7860"]
 
1
+ # Start with Python 3.12 base image
2
+ FROM python:3.12-slim
3
 
4
+ # Install system dependencies required by Playwright + Chromium
5
+ RUN apt-get update && apt-get install -y \
6
+ wget curl gnupg ca-certificates fonts-liberation libappindicator3-1 \
7
+ libasound2 libatk-bridge2.0-0 libatk1.0-0 libcups2 \
8
+ libdbus-1-3 libgdk-pixbuf2.0-0 libnspr4 libnss3 libx11-xcb1 \
9
+ libxcomposite1 libxdamage1 libxrandr2 xdg-utils libgbm1 libxshmfence1 \
10
+ libxss1 libxtst6 libdrm2 libgtk-3-0 \
11
+ && apt-get clean && rm -rf /var/lib/apt/lists/*
12
 
13
  # Install UV package manager
 
 
14
  RUN pip install uv
15
 
16
+ # Copy project files
 
 
17
  COPY . .
18
 
19
+ # Install Python dependencies
 
20
  RUN uv sync
21
 
22
+ # Install Playwright and Chromium
23
+ RUN playwright install --with-deps
24
+
25
+ # Expose Shiny port
26
  EXPOSE 7860
27
 
28
+ # Run the Shiny app
29
+ CMD ["/.venv/bin/shiny", "run", "app.py", "--host", "0.0.0.0", "--port", "7860"]
 
 
 
pyproject.toml CHANGED
@@ -22,4 +22,6 @@ dependencies = [
22
  "pytrends>=4.9.2",
23
  "requests>=2.32.4",
24
  "tweepy>=4.16.0",
 
 
25
  ]
 
22
  "pytrends>=4.9.2",
23
  "requests>=2.32.4",
24
  "tweepy>=4.16.0",
25
+ "bs4>=0.0.2",
26
+ "playwright>=1.53.0",
27
  ]
server/blog.py CHANGED
@@ -1,115 +1,195 @@
1
  from shiny import reactive, render, ui
2
- import sys, os
3
- from dotenv import load_dotenv
4
- import requests
5
- import warnings
6
  from pytrends.request import TrendReq
 
 
 
 
 
 
 
7
 
 
8
  load_dotenv()
9
- warnings.filterwarnings("ignore", category=FutureWarning)
10
 
11
  SHOPIFY_STORE = "ultima-supply.myshopify.com"
12
  SHOPIFY_TOKEN = os.getenv("SHOPIFY_TOKEN")
13
  SHOPIFY_API_VERSION = "2024-04"
14
  BLOG_ID = "73667707064"
15
- BRAVE_API_KEY = os.getenv("BRAVE_API_KEY")
16
 
17
- sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "code")))
18
- from llm_connect import get_response # assumed to exist
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
- # === UTILS ===
 
 
 
 
 
21
 
22
- def get_brave_snippets(query, num_results=20):
23
- if not BRAVE_API_KEY:
24
- print("[ERROR] Missing BRAVE_API_KEY in environment.")
25
- return []
 
 
 
 
 
 
 
26
 
27
- query = query.strip()
28
- if not query or len(query) < 3:
29
- print("[ERROR] Invalid or empty Brave search query.")
30
- return []
 
 
 
 
 
 
31
 
32
- url = "https://api.search.brave.com/res/v1/web/search"
33
- headers = {
34
- "Accept": "application/json",
35
- "X-Subscription-Token": BRAVE_API_KEY
36
- }
37
- params = {
38
- "q": query,
39
- "count": min(num_results, 30),
40
- "search_lang": "en"
41
- }
42
 
43
- try:
44
- response = requests.get(url, headers=headers, params=params)
45
- if response.status_code == 422:
46
- print(f"[ERROR] Brave rejected the query: {query}")
47
- print(f"[DETAIL] Response: {response.text}")
48
- return []
49
-
50
- response.raise_for_status()
51
- data = response.json()
52
- return [
53
- r.get("description", "")
54
- for r in data.get("web", {}).get("results", [])
55
- if "description" in r
56
- ]
57
  except Exception as e:
58
- print(f"[ERROR] Brave search failed: {e}")
59
- return []
60
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
 
62
- def get_keywords_and_research(topic: str, top_n=5, llm_n=25):
63
- research_snippets = get_brave_snippets(topic)
64
- research_context = "\n".join(research_snippets)
 
 
 
 
 
65
 
66
- pytrend_keywords = []
67
- try:
68
- pytrends = TrendReq(
69
- hl="en-US",
70
- tz=360,
71
- proxies=[],
72
- retries=5,
73
- backoff_factor=0.5,
74
- requests_args={"verify": False},
75
- timeout=(10, 30)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  )
77
 
78
- pytrends.build_payload([topic], geo="US", timeframe="today 1-m")
79
- related = pytrends.related_queries()
80
 
81
- top_df = related.get(topic, {}).get("top")
82
- if top_df is not None and not top_df.empty:
83
- pytrend_keywords = top_df["query"].head(top_n).tolist()
84
- else:
85
- print("[INFO] PyTrends related_queries() returned empty.")
86
- except Exception as e:
87
- print(f"[WARN] PyTrends failed: {e}")
 
 
 
88
 
89
- # Generate LLM keywords
90
- prompt = (
91
- f"You are an SEO assistant. Given the blog topic: '{topic}',\n"
92
- f"and this research context:\n{research_context[:1000]}\n\n"
93
- f"List {llm_n} high-impact, niche-relevant SEO keywords. Respond with a comma-separated list only."
94
- )
 
95
 
96
- llm_keywords_raw = get_response(
97
- input=prompt,
98
- template=lambda x: x,
99
- llm="gemini",
100
- md=False,
101
- temperature=0.7,
102
- max_tokens=1000
103
- )
104
 
105
- llm_keywords = [kw.strip() for kw in llm_keywords_raw.split(",") if kw.strip()]
106
 
107
- # Combine + deduplicate
108
- all_keywords = list(dict.fromkeys(pytrend_keywords + llm_keywords))[:30]
109
 
110
- return all_keywords, research_context
111
 
112
 
 
113
  def publish_blog_post(title: str, html_body: str, blog_id: str = BLOG_ID):
114
  url = f"https://{SHOPIFY_STORE}/admin/api/{SHOPIFY_API_VERSION}/blogs/{blog_id}/articles.json"
115
  headers = {
@@ -129,60 +209,64 @@ def publish_blog_post(title: str, html_body: str, blog_id: str = BLOG_ID):
129
  else:
130
  return False, response.text
131
 
132
- # === SERVER ===
133
-
134
  def server(input, output, session):
135
  related_keywords = reactive.Value([])
136
- generated_blog = reactive.Value(("", "")) # (seo_title, html_content)
137
 
138
  @output
139
  @render.ui
140
  @reactive.event(input.generate_btn)
141
- def blog_result():
142
- topic = input.topic()
143
- if not topic:
144
- return ui.HTML("<p><strong>⚠️ Please enter a topic before generating.</strong></p>")
145
 
146
- keywords, research = get_keywords_and_research(topic)
147
  related_keywords.set(keywords)
148
  keyword_str = ", ".join(keywords)
149
 
150
- # Generate blog content
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  prompt = (
152
- f"You are a blog writer for a hobby brand called 'Ultima Supply'.\n"
153
- f"Write an SEO-optimized blog post using these keywords: {keyword_str}.\n"
154
- f"Use <h1>, <h2>, and <p> tags appropriately.\n\n"
155
- f"Base your blog on the following research:\n{research}\n\n"
156
- f"Topic: {topic}\n"
157
- f"Make sure to be very detailed, and have enough content to capture search."
 
 
 
 
158
  )
159
 
160
  blog_html = get_response(
161
  input=prompt,
162
- template=lambda x: x,
163
  llm="gemini",
164
  md=False,
165
- temperature=0.8,
166
  max_tokens=5000
167
  )
168
 
169
- # Generate SEO-optimized title
170
- title_prompt = (
171
- f"You are an SEO expert helping a blog called 'Ultima Supply'.\n"
172
- f"Given the following topic: {topic}\n"
173
- f"And these SEO keywords: {keyword_str}\n"
174
- f"Generate a short, engaging, SEO-optimized blog title (max 70 characters).\n"
175
- f"Return only the title."
176
- )
177
 
178
- seo_title = get_response(
179
- input=title_prompt,
180
- template=lambda x: x.strip().replace('"', ''),
181
- llm="gemini",
182
- md=False,
183
- temperature=0.5,
184
- max_tokens=20
185
- )
186
 
187
  generated_blog.set((seo_title, blog_html))
188
 
@@ -192,10 +276,18 @@ def server(input, output, session):
192
  )
193
 
194
  @output
195
- @render.text
196
  def keywords_used():
197
  kws = related_keywords()
198
- return f"SEO Keywords Injected: {', '.join(kws)}" if kws else "No SEO keywords retrieved yet."
 
 
 
 
 
 
 
 
199
 
200
  @reactive.effect
201
  @reactive.event(input.post_btn)
 
1
  from shiny import reactive, render, ui
2
+ import os, sys
3
+ from bs4 import BeautifulSoup
 
 
4
  from pytrends.request import TrendReq
5
+ from playwright.async_api import async_playwright
6
+ import requests
7
+ import re, ast
8
+ import time
9
+
10
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "code")))
11
+ from llm_connect import get_response
12
 
13
+ from dotenv import load_dotenv
14
  load_dotenv()
 
15
 
16
  SHOPIFY_STORE = "ultima-supply.myshopify.com"
17
  SHOPIFY_TOKEN = os.getenv("SHOPIFY_TOKEN")
18
  SHOPIFY_API_VERSION = "2024-04"
19
  BLOG_ID = "73667707064"
 
20
 
21
+ # === Async JS-rendered scraping ===
22
+ async def scrape_div_content_from_url(url: str) -> str:
23
+ try:
24
+ async with async_playwright() as p:
25
+ browser = await p.chromium.launch(headless=True)
26
+ page = await browser.new_page()
27
+ await page.goto(url, wait_until="networkidle")
28
+ html = await page.content()
29
+ await browser.close()
30
+
31
+ soup = BeautifulSoup(html, "html.parser")
32
+ divs = soup.find_all("div", class_="article-body")
33
+ if not divs:
34
+ print("[WARN] No <div class='article-body'> found.")
35
+ return ""
36
+
37
+ texts = [div.get_text(separator=" ", strip=True) for div in divs]
38
+ return "\n\n".join(texts)
39
+ except Exception as e:
40
+ print(f"[ERROR] Failed to render or scrape: {e}")
41
+ return ""
42
 
43
+ # === Async keyword + scrape + fallback logic ===
44
+ async def get_keywords_and_content(url: str, top_n=5, llm_n=25):
45
+ scraped_text = await scrape_div_content_from_url(url)
46
+ if not scraped_text:
47
+ print("[ERROR] No scraped content. Cannot proceed.")
48
+ return [], ""
49
 
50
+ # === Step 1: Extract condensed topic keywords ===
51
+ try:
52
+ condensed_prompt = (
53
+ "From the content below, extract 5 to 7 mid-specific Google search phrases that reflect real user intent. "
54
+ "They should describe product types, use cases, or collector topics — not brand names alone. "
55
+ "Avoid single-word topics and overly broad terms like 'pokemon'. Each phrase should be 2–5 words, lowercase, and ASCII only.\n\n"
56
+ "You MUST return ONLY a valid Python list of strings. Do not use bullet points, newlines, or any explanation. "
57
+ "Your response must look exactly like this format:\n"
58
+ "['phrase one', 'phrase two', 'phrase three']\n\n"
59
+ f"Content:\n{scraped_text}"
60
+ )
61
 
62
+ condensed_topic_raw = get_response(
63
+ input=condensed_prompt,
64
+ template=lambda x: x.strip(),
65
+ llm="gemini",
66
+ md=False,
67
+ temperature=0.6,
68
+ max_tokens=100,
69
+ model_name="gemini-2.0-flash-lite"
70
+ )
71
+ print(condensed_topic_raw)
72
 
73
+ match = re.search(r"\[.*?\]", condensed_topic_raw, re.DOTALL)
74
+ condensed_topic = ast.literal_eval(match.group(0)) if match else []
75
+ if not condensed_topic:
76
+ condensed_topic = ["trading cards"]
 
 
 
 
 
 
77
 
78
+ print(f"[INFO] Condensed topic keywords: {condensed_topic}")
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  except Exception as e:
80
+ print(f"[WARN] Could not infer topics: {e}")
81
+ condensed_topic = ["trading cards"]
82
 
83
+ # === Step 2: Pull suggestions from PyTrends ===
84
+ all_suggestions = set()
85
+ try:
86
+ pytrends = TrendReq(hl="en-US", tz=360, timeout=10)
87
+ for topic in condensed_topic:
88
+ time.sleep(5)
89
+ suggestions = pytrends.suggestions(keyword=topic)
90
+ if suggestions:
91
+ titles = [s["title"] for s in suggestions]
92
+ all_suggestions.update(titles)
93
+ print(f"[INFO] Suggestions for '{topic}': {titles[:3]}")
94
+ except Exception as e:
95
+ print(f"[WARN] PyTrends suggestions failed: {e}")
96
+
97
+ all_suggestions = list(all_suggestions)
98
+
99
+ # === Step 3: Let Gemini filter suggestions for relevance ===
100
+ filtered_keywords = []
101
+ if all_suggestions:
102
+ filter_prompt = (
103
+ f"The following article was scraped:\n\n{scraped_text[:1500]}\n\n"
104
+ f"Here is a list of keyword suggestions:\n{all_suggestions}\n\n"
105
+ "Return only the keywords that are clearly relevant to the article topic. "
106
+ "Return a valid Python list of strings only. No explanation, bullets, or formatting."
107
+ )
108
 
109
+ raw_filtered = get_response(
110
+ input=filter_prompt,
111
+ template=lambda x: x.strip(),
112
+ llm="gemini",
113
+ md=False,
114
+ temperature=0.3,
115
+ max_tokens=200
116
+ )
117
 
118
+ match = re.search(r"\[.*?\]", raw_filtered)
119
+ if match:
120
+ try:
121
+ filtered_keywords = ast.literal_eval(match.group(0))
122
+ except:
123
+ filtered_keywords = []
124
+
125
+ # === Step 4: Fallback to Gemini keyword generation if needed ===
126
+ if not filtered_keywords:
127
+ fallback_prompt = (
128
+ f"You are an SEO expert. Generate {llm_n} niche-relevant SEO keywords "
129
+ f"based on this content:\n\n{scraped_text}\n\n"
130
+ "Return a comma-separated list of lowercase 2–5 word search phrases. No formatting."
131
+ )
132
+ fallback_keywords_raw = get_response(
133
+ input=fallback_prompt,
134
+ template=lambda x: x.strip(),
135
+ llm="gemini",
136
+ md=False,
137
+ temperature=0.7,
138
+ max_tokens=400
139
+ )
140
+ filtered_keywords = [kw.strip() for kw in fallback_keywords_raw.split(",") if kw.strip()]
141
+ print(f"[INFO] Fallback keywords used: {filtered_keywords[:top_n]}")
142
+
143
+ # === Step 5: Enforce minimum of 30 keywords ===
144
+ combined_keywords = list(dict.fromkeys(filtered_keywords)) # remove duplicates
145
+ if len(combined_keywords) < 30:
146
+ needed = 30 - len(combined_keywords)
147
+ print(f"[INFO] Need {needed} more keywords to reach 30. Using Gemini to pad.")
148
+
149
+ pad_prompt = (
150
+ f"The following article content is missing SEO keyword coverage:\n\n"
151
+ f"{scraped_text}\n\n"
152
+ f"Generate exactly {needed} additional SEO keyword phrases. "
153
+ "Each keyword must be:\n"
154
+ "- 2 to 5 words long\n"
155
+ "- lowercase only\n"
156
+ "- written in ASCII (no symbols or accents)\n"
157
+ "- clearly relevant to the article\n"
158
+ "- not overlapping with any common generic terms like 'pokemon'\n\n"
159
+ "You MUST return a valid Python list of strings. DO NOT include any explanation, extra text, markdown, or formatting.\n"
160
+ "Format example:\n"
161
+ "['keyword one', 'keyword two', 'keyword three']"
162
  )
163
 
 
 
164
 
165
+ pad_raw = get_response(
166
+ input=pad_prompt,
167
+ template=lambda x: x.strip(),
168
+ llm="gemini",
169
+ md=False,
170
+ temperature=0.7,
171
+ max_tokens=200
172
+ )
173
+
174
+ pad_keywords = []
175
 
176
+ pad_match = re.search(r"\[[^\]]+\]", pad_raw) # greedy, non-linebreaking
177
+ if pad_match:
178
+ try:
179
+ pad_keywords = ast.literal_eval(pad_match.group(0))
180
+ except Exception as e:
181
+ print(f"[WARN] ast.literal_eval failed: {e}")
182
+ pad_keywords = []
183
 
184
+ combined_keywords = list(dict.fromkeys(combined_keywords + pad_keywords))
185
+ print(f"[INFO] Padded {len(pad_keywords)} keywords:", pad_keywords)
 
 
 
 
 
 
186
 
 
187
 
188
+ return combined_keywords[:30], scraped_text
 
189
 
 
190
 
191
 
192
+ # === Shopify publisher ===
193
  def publish_blog_post(title: str, html_body: str, blog_id: str = BLOG_ID):
194
  url = f"https://{SHOPIFY_STORE}/admin/api/{SHOPIFY_API_VERSION}/blogs/{blog_id}/articles.json"
195
  headers = {
 
209
  else:
210
  return False, response.text
211
 
212
+ # === SHINY SERVER ===
 
213
  def server(input, output, session):
214
  related_keywords = reactive.Value([])
215
+ generated_blog = reactive.Value(("", "")) # (title, html_content)
216
 
217
  @output
218
  @render.ui
219
  @reactive.event(input.generate_btn)
220
+ async def blog_result():
221
+ url = input.url()
222
+ if not url:
223
+ return ui.HTML("<p><strong>⚠️ Please enter a URL.</strong></p>")
224
 
225
+ keywords, scraped = await get_keywords_and_content(url)
226
  related_keywords.set(keywords)
227
  keyword_str = ", ".join(keywords)
228
 
229
+ # Title generation from scraped text
230
+ infer_topic_prompt = (
231
+ f"Based on the following article content:\n\n{scraped[:2000]}\n\n"
232
+ f"Return a short, descriptive blog post title (max 70 characters)."
233
+ f"Return ONLY the TITLE"
234
+ )
235
+ seo_title = get_response(
236
+ input=infer_topic_prompt,
237
+ template=lambda x: x.strip().replace('"', ''),
238
+ llm="gemini",
239
+ md=False,
240
+ temperature=0.5,
241
+ max_tokens=20
242
+ )
243
+
244
+ # Blog generation with injected SEO
245
  prompt = (
246
+ f"You are a content writer for a collectibles brand called 'Ultima Supply'.\n"
247
+ f"Given the following scraped content:\n\n{scraped}\n\n"
248
+ f"Adapt this into an engaging, original, and heavily detailed SEO-optimized blog post.\n"
249
+ f"Inject the following SEO keywords naturally and organically throughout the content:\n{keyword_str}\n\n"
250
+ f"Use proper HTML structure: <h1> for the title, <h2> for section headers, and <p> for all paragraphs.\n"
251
+ f"Do NOT include any markdown, code blocks, or triple backticks. Do NOT use ```html or any formatting fences.\n"
252
+ f"Just return the raw HTML.\n\n"
253
+ f"DO NOT include any hyperlinks or images inside the body of the blog post.\n"
254
+ f"At the very end, add a single call-to-action in a new <p> tag:\n"
255
+ f"Visit <a href='https://ultima-supply.myshopify.com'>Ultima Supply</a> to explore more collectibles."
256
  )
257
 
258
  blog_html = get_response(
259
  input=prompt,
260
+ template=lambda x: x.strip(),
261
  llm="gemini",
262
  md=False,
263
+ temperature=0.9,
264
  max_tokens=5000
265
  )
266
 
267
+ blog_html = re.sub(r"```[a-zA-Z]*\n?", "", blog_html).strip()
268
+ blog_html = blog_html.replace("```", "").strip()
 
 
 
 
 
 
269
 
 
 
 
 
 
 
 
 
270
 
271
  generated_blog.set((seo_title, blog_html))
272
 
 
276
  )
277
 
278
  @output
279
+ @render.ui
280
  def keywords_used():
281
  kws = related_keywords()
282
+ if not kws:
283
+ return ui.HTML("<p><strong>No SEO keywords retrieved yet.</strong></p>")
284
+
285
+ return ui.HTML(
286
+ f"<p><strong>✅ SEO Keywords Injected ({len(kws)}):</strong></p><ul>"
287
+ + "".join(f"<li>{kw}</li>" for kw in kws) +
288
+ "</ul>"
289
+ )
290
+
291
 
292
  @reactive.effect
293
  @reactive.event(input.post_btn)
ui/blog.py CHANGED
@@ -2,8 +2,10 @@ from shiny import ui
2
 
3
  ui = ui.nav_panel(
4
  "Blog Writer",
5
- ui.input_text("topic", "Enter Blog Topic", placeholder="e.g. Best One Piece Cards in 2025"),
6
-
 
 
7
  ui.div(
8
  ui.input_action_button("generate_btn", "Generate Blog"),
9
  ui.input_action_button("post_btn", "Post to Shopify", class_="ms-3"),
@@ -11,5 +13,5 @@ ui = ui.nav_panel(
11
  ),
12
 
13
  ui.output_ui("blog_result"),
14
- ui.output_text('keywords_used')
15
  )
 
2
 
3
  ui = ui.nav_panel(
4
  "Blog Writer",
5
+
6
+ # Only need URL input now
7
+ ui.input_text("url", "Enter Article URL", placeholder="e.g. https://example.com/some-article"),
8
+
9
  ui.div(
10
  ui.input_action_button("generate_btn", "Generate Blog"),
11
  ui.input_action_button("post_btn", "Post to Shopify", class_="ms-3"),
 
13
  ),
14
 
15
  ui.output_ui("blog_result"),
16
+ ui.output_ui("keywords_used")
17
  )
uv.lock CHANGED
@@ -173,6 +173,19 @@ wheels = [
173
  { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 },
174
  ]
175
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
  [[package]]
177
  name = "black"
178
  version = "25.1.0"
@@ -225,6 +238,18 @@ wheels = [
225
  { url = "https://files.pythonhosted.org/packages/01/b6/dcd0fd188cc28d772e0df23a31ce50af4d358ef31bfee969dc5a033482a5/botocore-1.39.0-py3-none-any.whl", hash = "sha256:d8e72850d3450aeca355b654efb32c8370bf824c1945a61cad2395dc2688581e", size = 13753356 },
226
  ]
227
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  [[package]]
229
  name = "cachetools"
230
  version = "5.5.2"
@@ -1532,10 +1557,12 @@ wheels = [
1532
  { url = "https://files.pythonhosted.org/packages/9a/55/2cb24ea48aa30c99f805921c1c7860c1f45c0e811e44ee4e6a155668de06/lxml-6.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:219e0431ea8006e15005767f0351e3f7f9143e793e58519dc97fe9e07fae5563", size = 4952289 },
1533
  { url = "https://files.pythonhosted.org/packages/31/c0/b25d9528df296b9a3306ba21ff982fc5b698c45ab78b94d18c2d6ae71fd9/lxml-6.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bd5913b4972681ffc9718bc2d4c53cde39ef81415e1671ff93e9aa30b46595e7", size = 5111310 },
1534
  { url = "https://files.pythonhosted.org/packages/e9/af/681a8b3e4f668bea6e6514cbcb297beb6de2b641e70f09d3d78655f4f44c/lxml-6.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:390240baeb9f415a82eefc2e13285016f9c8b5ad71ec80574ae8fa9605093cd7", size = 5025457 },
 
1535
  { url = "https://files.pythonhosted.org/packages/69/f8/693b1a10a891197143c0673fcce5b75fc69132afa81a36e4568c12c8faba/lxml-6.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ca50bd612438258a91b5b3788c6621c1f05c8c478e7951899f492be42defc0da", size = 5257565 },
1536
  { url = "https://files.pythonhosted.org/packages/a8/96/e08ff98f2c6426c98c8964513c5dab8d6eb81dadcd0af6f0c538ada78d33/lxml-6.0.0-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:c24b8efd9c0f62bad0439283c2c795ef916c5a6b75f03c17799775c7ae3c0c9e", size = 4713390 },
1537
  { url = "https://files.pythonhosted.org/packages/a8/83/6184aba6cc94d7413959f6f8f54807dc318fdcd4985c347fe3ea6937f772/lxml-6.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:afd27d8629ae94c5d863e32ab0e1d5590371d296b87dae0a751fb22bf3685741", size = 5066103 },
1538
  { url = "https://files.pythonhosted.org/packages/ee/01/8bf1f4035852d0ff2e36a4d9aacdbcc57e93a6cd35a54e05fa984cdf73ab/lxml-6.0.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:54c4855eabd9fc29707d30141be99e5cd1102e7d2258d2892314cf4c110726c3", size = 4791428 },
 
1539
  { url = "https://files.pythonhosted.org/packages/5c/f7/5495829a864bc5f8b0798d2b52a807c89966523140f3d6fa3a58ab6720ea/lxml-6.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36531f81c8214e293097cd2b7873f178997dae33d3667caaae8bdfb9666b76c0", size = 5281290 },
1540
  { url = "https://files.pythonhosted.org/packages/79/56/6b8edb79d9ed294ccc4e881f4db1023af56ba451909b9ce79f2a2cd7c532/lxml-6.0.0-cp312-cp312-win32.whl", hash = "sha256:690b20e3388a7ec98e899fd54c924e50ba6693874aa65ef9cb53de7f7de9d64a", size = 3613495 },
1541
  { url = "https://files.pythonhosted.org/packages/0b/1e/cc32034b40ad6af80b6fd9b66301fc0f180f300002e5c3eb5a6110a93317/lxml-6.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:310b719b695b3dd442cdfbbe64936b2f2e231bb91d998e99e6f0daf991a3eba3", size = 4014711 },
@@ -1546,10 +1573,12 @@ wheels = [
1546
  { url = "https://files.pythonhosted.org/packages/52/46/3572761efc1bd45fcafb44a63b3b0feeb5b3f0066886821e94b0254f9253/lxml-6.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d18a25b19ca7307045581b18b3ec9ead2b1db5ccd8719c291f0cd0a5cec6cb81", size = 4947559 },
1547
  { url = "https://files.pythonhosted.org/packages/94/8a/5e40de920e67c4f2eef9151097deb9b52d86c95762d8ee238134aff2125d/lxml-6.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d4f0c66df4386b75d2ab1e20a489f30dc7fd9a06a896d64980541506086be1f1", size = 5102143 },
1548
  { url = "https://files.pythonhosted.org/packages/7c/4b/20555bdd75d57945bdabfbc45fdb1a36a1a0ff9eae4653e951b2b79c9209/lxml-6.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f4b481b6cc3a897adb4279216695150bbe7a44c03daba3c894f49d2037e0a24", size = 5021931 },
 
1549
  { url = "https://files.pythonhosted.org/packages/d4/dd/39c8507c16db6031f8c1ddf70ed95dbb0a6d466a40002a3522c128aba472/lxml-6.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae06fbab4f1bb7db4f7c8ca9897dc8db4447d1a2b9bee78474ad403437bcc29", size = 5247467 },
1550
  { url = "https://files.pythonhosted.org/packages/4d/56/732d49def0631ad633844cfb2664563c830173a98d5efd9b172e89a4800d/lxml-6.0.0-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:1fa377b827ca2023244a06554c6e7dc6828a10aaf74ca41965c5d8a4925aebb4", size = 4720601 },
1551
  { url = "https://files.pythonhosted.org/packages/8f/7f/6b956fab95fa73462bca25d1ea7fc8274ddf68fb8e60b78d56c03b65278e/lxml-6.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1676b56d48048a62ef77a250428d1f31f610763636e0784ba67a9740823988ca", size = 5060227 },
1552
  { url = "https://files.pythonhosted.org/packages/97/06/e851ac2924447e8b15a294855caf3d543424364a143c001014d22c8ca94c/lxml-6.0.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:0e32698462aacc5c1cf6bdfebc9c781821b7e74c79f13e5ffc8bfe27c42b1abf", size = 4790637 },
 
1553
  { url = "https://files.pythonhosted.org/packages/52/03/0e764ce00b95e008d76b99d432f1807f3574fb2945b496a17807a1645dbd/lxml-6.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7488a43033c958637b1a08cddc9188eb06d3ad36582cebc7d4815980b47e27ef", size = 5272430 },
1554
  { url = "https://files.pythonhosted.org/packages/5f/01/d48cc141bc47bc1644d20fe97bbd5e8afb30415ec94f146f2f76d0d9d098/lxml-6.0.0-cp313-cp313-win32.whl", hash = "sha256:5fcd7d3b1d8ecb91445bd71b9c88bdbeae528fefee4f379895becfc72298d181", size = 3612896 },
1555
  { url = "https://files.pythonhosted.org/packages/f4/87/6456b9541d186ee7d4cb53bf1b9a0d7f3b1068532676940fdd594ac90865/lxml-6.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:2f34687222b78fff795feeb799a7d44eca2477c3d9d3a46ce17d51a4f383e32e", size = 4013132 },
@@ -1974,6 +2003,8 @@ sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3
1974
  wheels = [
1975
  { url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800 },
1976
  { url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296 },
 
 
1977
  { url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787 },
1978
  { url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236 },
1979
  { url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950 },
@@ -1986,6 +2017,8 @@ wheels = [
1986
  { url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443 },
1987
  { url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474 },
1988
  { url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038 },
 
 
1989
  { url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503 },
1990
  { url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574 },
1991
  { url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060 },
@@ -1995,6 +2028,8 @@ wheels = [
1995
  { url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055 },
1996
  { url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110 },
1997
  { url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547 },
 
 
1998
  { url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001 },
1999
  { url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814 },
2000
  { url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124 },
@@ -2004,6 +2039,8 @@ wheels = [
2004
  { url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803 },
2005
  { url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520 },
2006
  { url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116 },
 
 
2007
  { url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336 },
2008
  { url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699 },
2009
  { url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789 },
@@ -2013,6 +2050,8 @@ wheels = [
2013
  { url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385 },
2014
  { url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129 },
2015
  { url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580 },
 
 
2016
  { url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888 },
2017
  { url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330 },
2018
  { url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089 },
@@ -2031,6 +2070,25 @@ wheels = [
2031
  { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567 },
2032
  ]
2033
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2034
  [[package]]
2035
  name = "polars"
2036
  version = "1.31.0"
@@ -2412,6 +2470,18 @@ wheels = [
2412
  { url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235 },
2413
  ]
2414
 
 
 
 
 
 
 
 
 
 
 
 
 
2415
  [[package]]
2416
  name = "pygments"
2417
  version = "2.19.2"
@@ -2955,6 +3025,15 @@ wheels = [
2955
  { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
2956
  ]
2957
 
 
 
 
 
 
 
 
 
 
2958
  [[package]]
2959
  name = "sqlalchemy"
2960
  version = "2.0.41"
@@ -3241,6 +3320,7 @@ version = "0.1.0"
3241
  source = { virtual = "." }
3242
  dependencies = [
3243
  { name = "anthropic" },
 
3244
  { name = "google-generativeai" },
3245
  { name = "ipywidgets" },
3246
  { name = "langchain" },
@@ -3250,6 +3330,7 @@ dependencies = [
3250
  { name = "langchain-google-genai" },
3251
  { name = "langchain-openai" },
3252
  { name = "openai" },
 
3253
  { name = "pydantic" },
3254
  { name = "pydantic-ai" },
3255
  { name = "pyrsm" },
@@ -3263,6 +3344,7 @@ dependencies = [
3263
  [package.metadata]
3264
  requires-dist = [
3265
  { name = "anthropic", specifier = ">=0.55.0" },
 
3266
  { name = "google-generativeai", specifier = ">=0.8.5" },
3267
  { name = "ipywidgets", specifier = ">=8.1.7" },
3268
  { name = "langchain", specifier = ">=0.3.26" },
@@ -3271,6 +3353,7 @@ requires-dist = [
3271
  { name = "langchain-google-genai", specifier = ">=2.0.10" },
3272
  { name = "langchain-openai", specifier = ">=0.3.27" },
3273
  { name = "openai", specifier = ">=1.93.0" },
 
3274
  { name = "pydantic", specifier = ">=2.11.7" },
3275
  { name = "pydantic-ai", specifier = ">=0.3.5" },
3276
  { name = "pyrsm", specifier = ">=1.6.0" },
 
173
  { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 },
174
  ]
175
 
176
+ [[package]]
177
+ name = "beautifulsoup4"
178
+ version = "4.13.4"
179
+ source = { registry = "https://pypi.org/simple" }
180
+ dependencies = [
181
+ { name = "soupsieve" },
182
+ { name = "typing-extensions" },
183
+ ]
184
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067 }
185
+ wheels = [
186
+ { url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285 },
187
+ ]
188
+
189
  [[package]]
190
  name = "black"
191
  version = "25.1.0"
 
238
  { url = "https://files.pythonhosted.org/packages/01/b6/dcd0fd188cc28d772e0df23a31ce50af4d358ef31bfee969dc5a033482a5/botocore-1.39.0-py3-none-any.whl", hash = "sha256:d8e72850d3450aeca355b654efb32c8370bf824c1945a61cad2395dc2688581e", size = 13753356 },
239
  ]
240
 
241
+ [[package]]
242
+ name = "bs4"
243
+ version = "0.0.2"
244
+ source = { registry = "https://pypi.org/simple" }
245
+ dependencies = [
246
+ { name = "beautifulsoup4" },
247
+ ]
248
+ sdist = { url = "https://files.pythonhosted.org/packages/c9/aa/4acaf814ff901145da37332e05bb510452ebed97bc9602695059dd46ef39/bs4-0.0.2.tar.gz", hash = "sha256:a48685c58f50fe127722417bae83fe6badf500d54b55f7e39ffe43b798653925", size = 698 }
249
+ wheels = [
250
+ { url = "https://files.pythonhosted.org/packages/51/bb/bf7aab772a159614954d84aa832c129624ba6c32faa559dfb200a534e50b/bs4-0.0.2-py2.py3-none-any.whl", hash = "sha256:abf8742c0805ef7f662dce4b51cca104cffe52b835238afc169142ab9b3fbccc", size = 1189 },
251
+ ]
252
+
253
  [[package]]
254
  name = "cachetools"
255
  version = "5.5.2"
 
1557
  { url = "https://files.pythonhosted.org/packages/9a/55/2cb24ea48aa30c99f805921c1c7860c1f45c0e811e44ee4e6a155668de06/lxml-6.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:219e0431ea8006e15005767f0351e3f7f9143e793e58519dc97fe9e07fae5563", size = 4952289 },
1558
  { url = "https://files.pythonhosted.org/packages/31/c0/b25d9528df296b9a3306ba21ff982fc5b698c45ab78b94d18c2d6ae71fd9/lxml-6.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bd5913b4972681ffc9718bc2d4c53cde39ef81415e1671ff93e9aa30b46595e7", size = 5111310 },
1559
  { url = "https://files.pythonhosted.org/packages/e9/af/681a8b3e4f668bea6e6514cbcb297beb6de2b641e70f09d3d78655f4f44c/lxml-6.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:390240baeb9f415a82eefc2e13285016f9c8b5ad71ec80574ae8fa9605093cd7", size = 5025457 },
1560
+ { url = "https://files.pythonhosted.org/packages/99/b6/3a7971aa05b7be7dfebc7ab57262ec527775c2c3c5b2f43675cac0458cad/lxml-6.0.0-cp312-cp312-manylinux_2_27_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d6e200909a119626744dd81bae409fc44134389e03fbf1d68ed2a55a2fb10991", size = 5657016 },
1561
  { url = "https://files.pythonhosted.org/packages/69/f8/693b1a10a891197143c0673fcce5b75fc69132afa81a36e4568c12c8faba/lxml-6.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ca50bd612438258a91b5b3788c6621c1f05c8c478e7951899f492be42defc0da", size = 5257565 },
1562
  { url = "https://files.pythonhosted.org/packages/a8/96/e08ff98f2c6426c98c8964513c5dab8d6eb81dadcd0af6f0c538ada78d33/lxml-6.0.0-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:c24b8efd9c0f62bad0439283c2c795ef916c5a6b75f03c17799775c7ae3c0c9e", size = 4713390 },
1563
  { url = "https://files.pythonhosted.org/packages/a8/83/6184aba6cc94d7413959f6f8f54807dc318fdcd4985c347fe3ea6937f772/lxml-6.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:afd27d8629ae94c5d863e32ab0e1d5590371d296b87dae0a751fb22bf3685741", size = 5066103 },
1564
  { url = "https://files.pythonhosted.org/packages/ee/01/8bf1f4035852d0ff2e36a4d9aacdbcc57e93a6cd35a54e05fa984cdf73ab/lxml-6.0.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:54c4855eabd9fc29707d30141be99e5cd1102e7d2258d2892314cf4c110726c3", size = 4791428 },
1565
+ { url = "https://files.pythonhosted.org/packages/29/31/c0267d03b16954a85ed6b065116b621d37f559553d9339c7dcc4943a76f1/lxml-6.0.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c907516d49f77f6cd8ead1322198bdfd902003c3c330c77a1c5f3cc32a0e4d16", size = 5678523 },
1566
  { url = "https://files.pythonhosted.org/packages/5c/f7/5495829a864bc5f8b0798d2b52a807c89966523140f3d6fa3a58ab6720ea/lxml-6.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36531f81c8214e293097cd2b7873f178997dae33d3667caaae8bdfb9666b76c0", size = 5281290 },
1567
  { url = "https://files.pythonhosted.org/packages/79/56/6b8edb79d9ed294ccc4e881f4db1023af56ba451909b9ce79f2a2cd7c532/lxml-6.0.0-cp312-cp312-win32.whl", hash = "sha256:690b20e3388a7ec98e899fd54c924e50ba6693874aa65ef9cb53de7f7de9d64a", size = 3613495 },
1568
  { url = "https://files.pythonhosted.org/packages/0b/1e/cc32034b40ad6af80b6fd9b66301fc0f180f300002e5c3eb5a6110a93317/lxml-6.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:310b719b695b3dd442cdfbbe64936b2f2e231bb91d998e99e6f0daf991a3eba3", size = 4014711 },
 
1573
  { url = "https://files.pythonhosted.org/packages/52/46/3572761efc1bd45fcafb44a63b3b0feeb5b3f0066886821e94b0254f9253/lxml-6.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d18a25b19ca7307045581b18b3ec9ead2b1db5ccd8719c291f0cd0a5cec6cb81", size = 4947559 },
1574
  { url = "https://files.pythonhosted.org/packages/94/8a/5e40de920e67c4f2eef9151097deb9b52d86c95762d8ee238134aff2125d/lxml-6.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d4f0c66df4386b75d2ab1e20a489f30dc7fd9a06a896d64980541506086be1f1", size = 5102143 },
1575
  { url = "https://files.pythonhosted.org/packages/7c/4b/20555bdd75d57945bdabfbc45fdb1a36a1a0ff9eae4653e951b2b79c9209/lxml-6.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f4b481b6cc3a897adb4279216695150bbe7a44c03daba3c894f49d2037e0a24", size = 5021931 },
1576
+ { url = "https://files.pythonhosted.org/packages/b6/6e/cf03b412f3763d4ca23b25e70c96a74cfece64cec3addf1c4ec639586b13/lxml-6.0.0-cp313-cp313-manylinux_2_27_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a78d6c9168f5bcb20971bf3329c2b83078611fbe1f807baadc64afc70523b3a", size = 5645469 },
1577
  { url = "https://files.pythonhosted.org/packages/d4/dd/39c8507c16db6031f8c1ddf70ed95dbb0a6d466a40002a3522c128aba472/lxml-6.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae06fbab4f1bb7db4f7c8ca9897dc8db4447d1a2b9bee78474ad403437bcc29", size = 5247467 },
1578
  { url = "https://files.pythonhosted.org/packages/4d/56/732d49def0631ad633844cfb2664563c830173a98d5efd9b172e89a4800d/lxml-6.0.0-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:1fa377b827ca2023244a06554c6e7dc6828a10aaf74ca41965c5d8a4925aebb4", size = 4720601 },
1579
  { url = "https://files.pythonhosted.org/packages/8f/7f/6b956fab95fa73462bca25d1ea7fc8274ddf68fb8e60b78d56c03b65278e/lxml-6.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1676b56d48048a62ef77a250428d1f31f610763636e0784ba67a9740823988ca", size = 5060227 },
1580
  { url = "https://files.pythonhosted.org/packages/97/06/e851ac2924447e8b15a294855caf3d543424364a143c001014d22c8ca94c/lxml-6.0.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:0e32698462aacc5c1cf6bdfebc9c781821b7e74c79f13e5ffc8bfe27c42b1abf", size = 4790637 },
1581
+ { url = "https://files.pythonhosted.org/packages/06/d4/fd216f3cd6625022c25b336c7570d11f4a43adbaf0a56106d3d496f727a7/lxml-6.0.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4d6036c3a296707357efb375cfc24bb64cd955b9ec731abf11ebb1e40063949f", size = 5662049 },
1582
  { url = "https://files.pythonhosted.org/packages/52/03/0e764ce00b95e008d76b99d432f1807f3574fb2945b496a17807a1645dbd/lxml-6.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7488a43033c958637b1a08cddc9188eb06d3ad36582cebc7d4815980b47e27ef", size = 5272430 },
1583
  { url = "https://files.pythonhosted.org/packages/5f/01/d48cc141bc47bc1644d20fe97bbd5e8afb30415ec94f146f2f76d0d9d098/lxml-6.0.0-cp313-cp313-win32.whl", hash = "sha256:5fcd7d3b1d8ecb91445bd71b9c88bdbeae528fefee4f379895becfc72298d181", size = 3612896 },
1584
  { url = "https://files.pythonhosted.org/packages/f4/87/6456b9541d186ee7d4cb53bf1b9a0d7f3b1068532676940fdd594ac90865/lxml-6.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:2f34687222b78fff795feeb799a7d44eca2477c3d9d3a46ce17d51a4f383e32e", size = 4013132 },
 
2003
  wheels = [
2004
  { url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800 },
2005
  { url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296 },
2006
+ { url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726 },
2007
+ { url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652 },
2008
  { url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787 },
2009
  { url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236 },
2010
  { url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950 },
 
2017
  { url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443 },
2018
  { url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474 },
2019
  { url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038 },
2020
+ { url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407 },
2021
+ { url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094 },
2022
  { url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503 },
2023
  { url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574 },
2024
  { url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060 },
 
2028
  { url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055 },
2029
  { url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110 },
2030
  { url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547 },
2031
+ { url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554 },
2032
+ { url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132 },
2033
  { url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001 },
2034
  { url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814 },
2035
  { url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124 },
 
2039
  { url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803 },
2040
  { url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520 },
2041
  { url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116 },
2042
+ { url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597 },
2043
+ { url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246 },
2044
  { url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336 },
2045
  { url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699 },
2046
  { url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789 },
 
2050
  { url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385 },
2051
  { url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129 },
2052
  { url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580 },
2053
+ { url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860 },
2054
+ { url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694 },
2055
  { url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888 },
2056
  { url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330 },
2057
  { url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089 },
 
2070
  { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567 },
2071
  ]
2072
 
2073
+ [[package]]
2074
+ name = "playwright"
2075
+ version = "1.53.0"
2076
+ source = { registry = "https://pypi.org/simple" }
2077
+ dependencies = [
2078
+ { name = "greenlet" },
2079
+ { name = "pyee" },
2080
+ ]
2081
+ wheels = [
2082
+ { url = "https://files.pythonhosted.org/packages/f5/e2/2f107be74419280749723bd1197c99351f4b8a0a25e974b9764affb940b2/playwright-1.53.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:48a1a15ce810f0ffe512b6050de9871ea193b41dd3cc1bbed87b8431012419ba", size = 40392498 },
2083
+ { url = "https://files.pythonhosted.org/packages/ac/d5/e8c57a4f6fd46059fb2d51da2d22b47afc886b42400f06b742cd4a9ba131/playwright-1.53.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a701f9498a5b87e3f929ec01cea3109fbde75821b19c7ba4bba54f6127b94f76", size = 38647035 },
2084
+ { url = "https://files.pythonhosted.org/packages/4d/f3/da18cd7c22398531316e58fd131243fd9156fe7765aae239ae542a5d07d2/playwright-1.53.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:f765498341c4037b4c01e742ae32dd335622f249488ccd77ca32d301d7c82c61", size = 40392502 },
2085
+ { url = "https://files.pythonhosted.org/packages/92/32/5d871c3753fbee5113eefc511b9e44c0006a27f2301b4c6bffa4346fbd94/playwright-1.53.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:db19cb5b58f3b15cad3e2419f4910c053e889202fc202461ee183f1530d1db60", size = 45848364 },
2086
+ { url = "https://files.pythonhosted.org/packages/dc/6b/9942f86661ff41332f9299db4950623123e60ca71e4fb6e6942fc0212624/playwright-1.53.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9276c9c935fc062f51f4f5107e56420afd6d9a524348dc437793dc2e34c742e3", size = 45235174 },
2087
+ { url = "https://files.pythonhosted.org/packages/51/63/28b3f2d36e6a95e88f033d2aa7af06083f6f4aa0d9764759d96033cd053e/playwright-1.53.0-py3-none-win32.whl", hash = "sha256:36eedec101724ff5a000cddab87dd9a72a39f9b3e65a687169c465484e667c06", size = 35415131 },
2088
+ { url = "https://files.pythonhosted.org/packages/a9/b5/4ca25974a90d16cfd4a9a953ee5a666cf484a0bdacb4eed484e5cab49e66/playwright-1.53.0-py3-none-win_amd64.whl", hash = "sha256:d68975807a0fd997433537f1dcf2893cda95884a39dc23c6f591b8d5f691e9e8", size = 35415138 },
2089
+ { url = "https://files.pythonhosted.org/packages/9a/81/b42ff2116df5d07ccad2dc4eeb20af92c975a1fbc7cd3ed37b678468b813/playwright-1.53.0-py3-none-win_arm64.whl", hash = "sha256:fcfd481f76568d7b011571160e801b47034edd9e2383c43d83a5fb3f35c67885", size = 31188568 },
2090
+ ]
2091
+
2092
  [[package]]
2093
  name = "polars"
2094
  version = "1.31.0"
 
2470
  { url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235 },
2471
  ]
2472
 
2473
+ [[package]]
2474
+ name = "pyee"
2475
+ version = "13.0.0"
2476
+ source = { registry = "https://pypi.org/simple" }
2477
+ dependencies = [
2478
+ { name = "typing-extensions" },
2479
+ ]
2480
+ sdist = { url = "https://files.pythonhosted.org/packages/95/03/1fd98d5841cd7964a27d729ccf2199602fe05eb7a405c1462eb7277945ed/pyee-13.0.0.tar.gz", hash = "sha256:b391e3c5a434d1f5118a25615001dbc8f669cf410ab67d04c4d4e07c55481c37", size = 31250 }
2481
+ wheels = [
2482
+ { url = "https://files.pythonhosted.org/packages/9b/4d/b9add7c84060d4c1906abe9a7e5359f2a60f7a9a4f67268b2766673427d8/pyee-13.0.0-py3-none-any.whl", hash = "sha256:48195a3cddb3b1515ce0695ed76036b5ccc2ef3a9f963ff9f77aec0139845498", size = 15730 },
2483
+ ]
2484
+
2485
  [[package]]
2486
  name = "pygments"
2487
  version = "2.19.2"
 
3025
  { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
3026
  ]
3027
 
3028
+ [[package]]
3029
+ name = "soupsieve"
3030
+ version = "2.7"
3031
+ source = { registry = "https://pypi.org/simple" }
3032
+ sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418 }
3033
+ wheels = [
3034
+ { url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677 },
3035
+ ]
3036
+
3037
  [[package]]
3038
  name = "sqlalchemy"
3039
  version = "2.0.41"
 
3320
  source = { virtual = "." }
3321
  dependencies = [
3322
  { name = "anthropic" },
3323
+ { name = "bs4" },
3324
  { name = "google-generativeai" },
3325
  { name = "ipywidgets" },
3326
  { name = "langchain" },
 
3330
  { name = "langchain-google-genai" },
3331
  { name = "langchain-openai" },
3332
  { name = "openai" },
3333
+ { name = "playwright" },
3334
  { name = "pydantic" },
3335
  { name = "pydantic-ai" },
3336
  { name = "pyrsm" },
 
3344
  [package.metadata]
3345
  requires-dist = [
3346
  { name = "anthropic", specifier = ">=0.55.0" },
3347
+ { name = "bs4", specifier = ">=0.0.2" },
3348
  { name = "google-generativeai", specifier = ">=0.8.5" },
3349
  { name = "ipywidgets", specifier = ">=8.1.7" },
3350
  { name = "langchain", specifier = ">=0.3.26" },
 
3353
  { name = "langchain-google-genai", specifier = ">=2.0.10" },
3354
  { name = "langchain-openai", specifier = ">=0.3.27" },
3355
  { name = "openai", specifier = ">=1.93.0" },
3356
+ { name = "playwright", specifier = ">=1.53.0" },
3357
  { name = "pydantic", specifier = ">=2.11.7" },
3358
  { name = "pydantic-ai", specifier = ">=0.3.5" },
3359
  { name = "pyrsm", specifier = ">=1.6.0" },