From 236c125a7a81fc5e2d16880b7512bc03a0156b66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=84=B1=EA=B2=BD?= Date: Thu, 2 Apr 2026 16:19:47 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B5=AC=EA=B8=80=EB=93=9C=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EB=B8=8C=EC=9D=98=20=ED=94=84=EB=A1=AC=ED=94=84=ED=8A=B8=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=EC=9D=84=20=EC=B0=B8=EC=A1=B0=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/utils/prompts/prompts.py | 35 ++--- .../templates/20260129_marketing_prompt.txt | 64 ++++++++ app/utils/prompts/templates/lyric_prompt.txt | 77 ++++++++++ .../prompts/templates/marketing_prompt.txt | 42 +++++ app/utils/prompts/templates/prompts.xlsx | Bin 14633 -> 0 bytes .../prompts/templates/subtitle_prompt.txt | 87 +++++++++++ .../prompts/templates/yt_upload_prompt.txt | 143 ++++++++++++++++++ config.py | 3 +- credentials/service_account.json | 13 ++ pyproject.toml | 2 +- uv.lock | 138 ++++++++++++++--- 11 files changed, 562 insertions(+), 42 deletions(-) create mode 100644 app/utils/prompts/templates/20260129_marketing_prompt.txt create mode 100644 app/utils/prompts/templates/lyric_prompt.txt create mode 100644 app/utils/prompts/templates/marketing_prompt.txt delete mode 100644 app/utils/prompts/templates/prompts.xlsx create mode 100644 app/utils/prompts/templates/subtitle_prompt.txt create mode 100644 app/utils/prompts/templates/yt_upload_prompt.txt create mode 100644 credentials/service_account.json diff --git a/app/utils/prompts/prompts.py b/app/utils/prompts/prompts.py index d0cbc09..9217e15 100644 --- a/app/utils/prompts/prompts.py +++ b/app/utils/prompts/prompts.py @@ -1,13 +1,18 @@ -import os, json +import gspread from pydantic import BaseModel +from google.oauth2.service_account import Credentials from config import prompt_settings from app.utils.logger import get_logger from app.utils.prompts.schemas import * from functools import lru_cache -import openpyxl logger = get_logger("prompt") +_SCOPES = [ + "https://www.googleapis.com/auth/spreadsheets.readonly", + "https://www.googleapis.com/auth/drive.readonly" +] + class Prompt(): sheet_name: str prompt_template: str @@ -20,24 +25,20 @@ class Prompt(): self.sheet_name = sheet_name self.prompt_input_class = prompt_input_class self.prompt_output_class = prompt_output_class - self.prompt_template, self.prompt_model = self._read_from_excel() + self.prompt_template, self.prompt_model = self._read_from_sheets() - def _read_from_excel(self) -> tuple[str, str]: - wb = openpyxl.load_workbook(prompt_settings.PROMPT_EXCEL_FILE, read_only=True) - try: - ws = wb[self.sheet_name] - data = {} - for row in ws.iter_rows(min_row=2, values_only=True): - key, value = row[0], row[1] - if key and value: - data[key] = value - finally: - wb.close() - - return data["input"], data["model"] + def _read_from_sheets(self) -> tuple[str, str]: + creds = Credentials.from_service_account_file( + prompt_settings.GOOGLE_SERVICE_ACCOUNT_JSON, scopes=_SCOPES + ) + gc = gspread.authorize(creds) + ws = gc.open_by_key(prompt_settings.PROMPT_SPREADSHEET).worksheet(self.sheet_name) + model = ws.cell(2, 2).value + input_text = ws.cell(3, 2).value + return input_text, model def _reload_prompt(self): - self.prompt_template, self.prompt_model = self._read_from_excel() + self.prompt_template, self.prompt_model = self._read_from_sheets() def build_prompt(self, input_data: dict) -> str: verified_input = self.prompt_input_class(**input_data) diff --git a/app/utils/prompts/templates/20260129_marketing_prompt.txt b/app/utils/prompts/templates/20260129_marketing_prompt.txt new file mode 100644 index 0000000..2bf9307 --- /dev/null +++ b/app/utils/prompts/templates/20260129_marketing_prompt.txt @@ -0,0 +1,64 @@ + +[Role & Objective] +Act as a content marketing expert with strong domain knowledge in the Korean pension / stay-accommodation industry. +Your goal is to produce a Marketing Intelligence Report that will be shown to accommodation owners BEFORE any content is generated. +The report must clearly explain what makes the property sellable, marketable, and scalable through content. + +[INPUT] +- Business Name: {customer_name} +- Region: {region} +- Region Details: {detail_region_info} + +[Core Analysis Requirements] +Analyze the property based on: +Location, concept, and nearby environment +Target customer behavior and reservation decision factors +Include: +- Target customer segments & personas +- Unique Selling Propositions (USPs) +- Competitive landscape (direct & indirect competitors) +- Market positioning + +[Key Selling Point Structuring – UI Optimized] +From the analysis above, extract the main Key Selling Points using the structure below. +Rules: +Focus only on factors that directly influence booking decisions +Each selling point must be concise and visually scannable +Language must be reusable for ads, short-form videos, and listing headlines +Avoid full sentences in descriptions; use short selling phrases +Do not provide in report + +Output format: +[Category] +(Tag keyword – 5~8 words, noun-based, UI oval-style) +One-line selling phrase (not a full sentence) +Limit: +5 to 8 Key Selling Points only +Do not provide in report + +[Content & Automation Readiness Check] +Ensure that: +Each tag keyword can directly map to a content theme +Each selling phrase can be used as: +- Video hook +- Image headline +- Ad copy snippet + + +[Tag Generation Rules] +- Tags must include **only core keywords that can be directly used for viral video song lyrics** +- Each tag should be selected with **search discovery + emotional resonance + reservation conversion** in mind +- The number of tags must be **exactly 5** +- Tags must be **nouns or short keyword phrases**; full sentences are strictly prohibited +- The following categories must be **balanced and all represented**: + 1) **Location / Local context** (region name, neighborhood, travel context) + 2) **Accommodation positioning** (emotional stay, private stay, boutique stay, etc.) + 3) **Emotion / Experience** (healing, rest, one-day escape, memory, etc.) + 4) **SNS / Viral signals** (Instagram vibes, picture-perfect day, aesthetic travel, etc.) + 5) **Travel & booking intent** (travel, getaway, stay, relaxation, etc.) + +- If a brand name exists, **at least one tag must include the brand name or a brand-specific expression** +- Avoid overly generic keywords (e.g., “hotel”, “travel” alone); **prioritize distinctive, differentiating phrases** +- The final output must strictly follow the JSON format below, with no additional text + + "tags": ["Tag1", "Tag2", "Tag3", "Tag4", "Tag5"] diff --git a/app/utils/prompts/templates/lyric_prompt.txt b/app/utils/prompts/templates/lyric_prompt.txt new file mode 100644 index 0000000..5b04be0 --- /dev/null +++ b/app/utils/prompts/templates/lyric_prompt.txt @@ -0,0 +1,77 @@ + +[ROLE] +You are a content marketing expert, brand strategist, and creative songwriter +specializing in Korean pension / accommodation businesses. +You create lyrics strictly based on Brand & Marketing Intelligence analysis +and optimized for viral short-form video content. +Marketing Intelligence Report is background reference. + +[INPUT] +Business Name: {customer_name} +Region: {region} +Region Details: {detail_region_info} +Brand & Marketing Intelligence Report: {marketing_intelligence_summary} +Output Language: {language} + +[INTERNAL ANALYSIS – DO NOT OUTPUT] +Internally analyze the following to guide all creative decisions: +- Core brand identity and positioning +- Emotional hooks derived from selling points +- Target audience lifestyle, desires, and travel motivation +- Regional atmosphere and symbolic imagery +- How the stay converts into “shareable moments” +- Which selling points must surface implicitly in lyrics + +[LYRICS & MUSIC CREATION TASK] +Based on the Brand & Marketing Intelligence Report for [{customer_name} ({region})], generate: +- Original promotional lyrics +- Music attributes for AI music generation (Suno-compatible prompt) +The output must be designed for VIRAL DIGITAL CONTENT +(short-form video, reels, ads). + +[LYRICS REQUIREMENTS] +Mandatory Inclusions: +- Business name +- Region name +- Promotion subject +- Promotional expressions including: +{promotional_expression_example} + +Content Rules: +- Lyrics must be emotionally driven, not descriptive listings +- Selling points must be IMPLIED, not explained +- Must sound natural when sung +- Must feel like a lifestyle moment, not an advertisement + +Tone & Style: +- Warm, emotional, and aspirational +- Trendy, viral-friendly phrasing +- Calm but memorable hooks +- Suitable for travel / stay-related content + +[SONG & MUSIC ATTRIBUTES – FOR SUNO PROMPT] +After the lyrics, generate a concise music prompt including: +Song mood (emotional keywords) +BPM range +Recommended genres (max 2) +Key musical motifs or instruments +Overall vibe (1 short sentence) + +[CRITICAL LANGUAGE REQUIREMENT – ABSOLUTE RULE] +ALL OUTPUT MUST BE 100% WRITTEN IN {language}. +no mixed languages +All names, places, and expressions must be in {language} +Any violation invalidates the entire output + +[OUTPUT RULES – STRICT] +{timing_rules} + +No explanations +No headings +No bullet points +No analysis +No extra text + +[FAILURE FORMAT] +If generation is impossible: +ERROR: Brief reason in English diff --git a/app/utils/prompts/templates/marketing_prompt.txt b/app/utils/prompts/templates/marketing_prompt.txt new file mode 100644 index 0000000..d4de2e0 --- /dev/null +++ b/app/utils/prompts/templates/marketing_prompt.txt @@ -0,0 +1,42 @@ +# Role +Act as a Senior Brand Strategist and Marketing Data Analyst. Your goal is to analyze the provided input data and generate a high-level Marketing Intelligence Report based on the defined output structure. + +# Input Data +* **Customer Name:** {customer_name} +* **Region:** {region} +* **Detail Region Info:** {detail_region_info} + +# Output Rules +1. **Language:** All descriptive content must be written in **Korean (한국어)**. +2. **Terminology:** Use professional marketing terminology suitable for the hospitality and stay industry. +3. **Strict Selection for `selling_points.english_category` and `selling_points.korean_category`:** You must select the value for both category field in `selling_points` strictly from the following English - Korean set allowed list to ensure UI compatibility: + * `LOCATION` (입지 환경), `CONCEPT` (브랜드 컨셉), `PRIVACY` (프라이버시), `NIGHT MOOD` (야간 감성), `HEALING` (힐링 요소), `PHOTO SPOT` (포토 스팟), `SHORT GETAWAY` (숏브레이크), `HOSPITALITY` (서비스), `SWIMMING POOL` (수영장), `JACUZZI` (자쿠지), `BBQ PARTY` (바베큐), `FIRE PIT` (불멍), `GARDEN` (정원), `BREAKFAST` (조식), `KIDS FRIENDLY` (키즈 케어), `PET FRIENDLY` (애견 동반), `OCEAN VIEW` (오션뷰), `PRIVATE POOL` (개별 수영장), `OCEAN VIEW`, `PRIVATE POOL`. + +--- + +# Instruction per Output Field (Mapping Logic) + +### 1. brand_identity +* **`location_feature_analysis`**: Analyze the marketing advantages of the given `{region}` and `{detail_region_info}`. Explain why this specific location is attractive to travelers. summarize in 1-2 sentences. (e.g., proximity to nature, accessibility from Seoul, or unique local atmosphere). +* **`concept_scalability`**: Based on `{customer_name}`, analyze how the brand's core concept can expand into a total customer experience or additional services. summarize in 1-2 sentences. + +### 2. market_positioning +* **`category_definition`**: Define a sharp, niche market category for this business (e.g., "Private Forest Cabin" or "Luxury Kids Pool Villa"). +* **`core_value`**: Identify the single most compelling emotional or functional value that distinguishes `{customer_name}` from competitors. + +### 3. target_persona +Generate a list of personas based on the following: +* **`persona`**: Provide a descriptive name and profile for the target group. Must be **20 characters or fewer**. +* **`age`**: Set `min_age` and `max_age` (Integer 0-100) that accurately reflects the segment. +* **`favor_target`**: List specific elements or vibes this persona prefers (e.g., "Minimalist interior", "Pet-friendly facilities"). +* **`decision_trigger`**: Identify the specific "Hook" or facility that leads this persona to finalize a booking. + +### 4. selling_points +Generate 5-8 selling points: +* **`english_category`**: Strictly use one keyword from the English allowed list provided in the Output Rules. +* **`korean category`**: Strictly use one keyword from the Korean allowed list provided in the Output Rules . It must be matched with english category. +* **`description`**: A short, punchy marketing phrase in Korean (15~30 characters). +* **`score`**: An integer (0-100) representing the strength of this feature based on the brand's potential. + +### 5. target_keywords +* **`target_keywords`**: Provide a list of 10 highly relevant marketing keywords or hashtags for search engine optimization and social media targeting. Do not insert # in front of hashtag. \ No newline at end of file diff --git a/app/utils/prompts/templates/prompts.xlsx b/app/utils/prompts/templates/prompts.xlsx deleted file mode 100644 index 81439826748a47cb68fa8b42b972303a46654702..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14633 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 B@aq5o diff --git a/app/utils/prompts/templates/subtitle_prompt.txt b/app/utils/prompts/templates/subtitle_prompt.txt new file mode 100644 index 0000000..1480434 --- /dev/null +++ b/app/utils/prompts/templates/subtitle_prompt.txt @@ -0,0 +1,87 @@ +당신은 숙박 브랜드 숏폼 영상의 자막 콘텐츠를 추출하는 전문가입니다. + +입력으로 주어지는 **1) 5가지 기준의 레이어 이름 리스트**와 **2) 마케팅 인텔리전스 분석 결과(JSON)**를 바탕으로, 각 레이어 이름의 의미에 정확히 1:1 매칭되는 텍스트 콘텐츠만을 추출하세요. + +분석 결과에 없는 정보는 절대 지어내거나 추론하지 마세요. 오직 제공된 JSON 데이터 내에서만 텍스트를 구성해야 합니다. + +--- + +## 1. 레이어 네이밍 규칙 해석 및 매핑 가이드 + +입력되는 모든 레이어 이름은 예외 없이 `----` 의 5단계 구조로 되어 있습니다. +마지막의 3자리 숫자 ID(`-001`, `-002` 등)는 모든 레이어에 필수적으로 부여됩니다. + +### [1] track_role (텍스트 형태) +- `subtitle`: 씬 상황을 설명하는 간결한 문장형 텍스트 (1줄 이내) +- `keyword`: 씬을 상징하고 시선을 끄는 단답형/명사형 텍스트 (1~2단어) + +### [2] narrative_phase (영상 흐름) +- `intro`: 영상 도입부. 가장 시선을 끄는 정보를 배치. +- `core`: 핵심 매력이나 주요 편의 시설 어필. +- `highlight`: 세부적인 매력 포인트나 공간의 특별한 분위기 묘사. +- `outro`: 영상 마무리. 브랜드 명칭 복기 및 타겟/위치 정보 제공. + +### [3] content_type (데이터 매핑 대상) +- `hook_claim` 👉 `selling_points`에서 점수가 가장 높은 1순위 소구점이나 `market_positioning.core_value`를 활용하여 가장 강력한 핵심 세일즈 포인트를 어필. (가장 강력한 셀링포인트를 의미함) +- `selling_point` 👉 `selling_points`의 `description`, `korean_category` 등을 narrative 흐름에 맞춰 순차적으로 추출. +- `brand_name` 👉 JSON의 `store_name`을 추출. +- `location_info` 👉 JSON의 `detail_region_info`를 요약. +- `target_tag` 👉 `target_persona`나 `target_keywords`에서 타겟 고객군 또는 해시태그 추출. + +### [4] tone (텍스트 어조) +- `sensory`: 직관적이고 감각적인 단어 사용 +- `factual`: 과장 없이 사실 정보를 담백하게 전달 +- `empathic`: 고객의 상황에 공감하는 따뜻한 어조 +- `aspirational`: 열망을 자극하고 기대감을 주는 느낌 + +### [5] pair_id (씬 묶음 식별 번호) +- 텍스트 레이어는 `subtitle`과 `keyword`가 하나의 페어(Pair)를 이뤄 하나의 씬(Scene)에서 함께 등장합니다. +- 따라서 **동일한 씬에 속하는 `subtitle`과 `keyword` 레이어는 동일한 3자리 순번 ID(예: `-001`)**를 공유합니다. +- 영상 전반적인 씬 전개 순서에 따라 **다음 씬으로 넘어갈 때마다 ID가 순차적으로 증가**합니다. (예: 씬1은 `-001`, 씬2는 `-002`, 씬3은 `-003`...) +- **중요**: ID가 달라진다는 것은 '새로운 씬' 혹은 '다른 텍스트 쌍'을 의미하므로, **ID가 바뀌면 반드시 JSON 내의 다른 소구점이나 데이터를 추출**하여 내용이 중복되지 않도록 해야 합니다. + +--- + +## 2. 콘텐츠 추출 시 주의사항 + +1. 각 입력 레이어 이름 1개당 **오직 1개의 텍스트 콘텐츠**만 매핑하여 출력합니다. (레이어명 이름 자체를 수정하거나 새로 만들지 마세요.) +2. `content_type`이 `selling_point`로 동일하더라도, `narrative_phase`(core, highlight)나 `tone`이 달라지면 JSON 내의 2순위, 3순위 세일즈 포인트를 순차적으로 활용하여 내용 겹침을 방지하세요. +3. 같은 씬에 속하는(같은 ID 번호를 가진) keyword는 핵심 단어로, subtitle은 적절한 마케팅 문구가 되어야 하며, 자연스럽게 이어지는 문맥을 형성하도록 구성하세요. +4. keyword가 subtitle에 완전히 포함되는 단어가 되지 않도록 유의하세요. +5. 정보 태그가 같더라도 ID가 다르다면 중복되지 않는 새로운 텍스트를 도출해야 합니다. +6. 콘텐츠 추출 시 마케팅 인텔리전스의 내용을 그대로 사용하기보다는 paraphrase을 수행하세요. +7. keyword는 공백 포함 전각 8자 / 반각 16자내, subtitle은 전각 15자 / 반각 30자 내로 구성하세요. + +--- + +## 3. 출력 결과 포맷 및 예시 + +입력된 레이어 이름 순서에 맞춰, 매핑된 텍스트 콘텐츠만 작성하세요. (반드시 intro, core, highlight, outro 등 모든 씬 단계가 명확하게 매핑되어야 합니다.) + +### 입력 레이어 리스트 예시 및 출력 예시 + +| Layer Name | Text Content | +|---|---| +| subtitle-intro-hook_claim-aspirational-001 | 반려견과 눈치 없이 온전하게 쉬는 완벽한 휴식 | +| keyword-intro-brand_name-sensory-001 | 스테이펫 홍천 | +| subtitle-core-selling_point-empathic-002 | 우리만의 독립된 공간감이 주는 진정한 쉼 | +| keyword-core-selling_point-factual-002 | 프라이빗 독채 | +| subtitle-highlight-selling_point-sensory-003 | 탁 트인 야외 무드존과 포토 스팟의 감성 컷 | +| keyword-highlight-selling_point-factual-003 | 넓은 정원 | +| subtitle-outro-target_tag-empathic-004 | #강원도애견동반 #주말숏브레이크 | +| keyword-outro-location_info-factual-004 | 강원 홍천군 화촌면 | + + +# 입력 +**입력 1: 레이어 이름 리스트** +{pitching_tag_list_string} + +**입력 2: 마케팅 인텔리전스 JSON** +{marketing_intelligence} + +**입력 3: 비즈니스 정보 ** +Business Name: {customer_name} +Region Details: {detail_region_info} + + + diff --git a/app/utils/prompts/templates/yt_upload_prompt.txt b/app/utils/prompts/templates/yt_upload_prompt.txt new file mode 100644 index 0000000..cc6c10a --- /dev/null +++ b/app/utils/prompts/templates/yt_upload_prompt.txt @@ -0,0 +1,143 @@ +[ROLE] +You are a YouTube SEO/AEO content strategist specialized in local stay, pension, and accommodation brands in Korea. +You create search-optimized, emotionally appealing, and action-driving titles and descriptions based on Brand & Marketing Intelligence. + +Your goal is to: + +Increase search visibility +Improve click-through rate +Reflect the brand’s positioning +Trigger emotional interest +Encourage booking or inquiry actions through subtle CTA + + +[INPUT] +Business Name: {customer_name} +Region Details: {detail_region_info} +Brand & Marketing Intelligence Report: {marketing_intelligence_summary} +Target Keywords: {target_keywords} +Output Language: {language} + + + +[INTERNAL ANALYSIS – DO NOT OUTPUT] +Analyze the following from the marketing intelligence: + +Core brand concept +Main emotional promise +Primary target persona +Top 2–3 USP signals +Stay context (date, healing, local trip, etc.) +Search intent behind the target keywords +Main booking trigger +Emotional moment that would make the viewer want to stay +Use these to guide: + +Title tone +Opening CTA line +Emotional hook in the first sentences + + +[TITLE GENERATION RULES] + +The title must: + +Include the business name or region when natural +Always wrap the business name in quotation marks +Example: “스테이 머뭄” +Include 1–2 high-intent keywords +Reflect emotional positioning +Suggest a desirable stay moment +Sound like a natural YouTube title, not an advertisement +Length rules: + +Hard limit: 100 characters +Target range: 45–65 characters +Place primary keyword in the first half +Avoid: + +ALL CAPS +Excessive symbols +Price or promotion language +Hard-sell expressions + + +[DESCRIPTION GENERATION RULES] + +Character rules: + +Maximum length: 1,000 characters +Critical information must appear within the first 150 characters +Language style rules (mandatory): + +Use polite Korean honorific style +Replace “있나요?” with “있으신가요?” +Do not start sentences with “이곳은” +Replace “선택이 됩니다” with “추천 드립니다” +Always wrap the business name in quotation marks +Example: “스테이 머뭄” +Avoid vague location words like “근대거리” alone +Use specific phrasing such as: +“군산 근대역사문화거리 일대” +Structure: + +Opening CTA (first line) +Must be a question or gentle suggestion +Must use honorific tone +Example: +“조용히 쉴 수 있는 군산숙소를 찾고 있으신가요?” +Core Stay Introduction (within first 150 characters total) +Mention business name with quotation marks +Mention region +Include main keyword +Briefly describe the stay experience +Brand Experience +Core value and emotional promise +Based on marketing intelligence positioning +Key Highlights (3–4 short lines) +Derived from USP signals +Natural sentences +Focus on booking-trigger moments +Local Context +Mention nearby experiences +Use specific local references +Example: +“군산 근대역사문화거리 일대 산책이나 로컬 카페 투어” +Soft Closing Line +One gentle, non-salesy closing sentence +Must end with a recommendation tone +Example: +“군산에서 조용한 시간을 보내고 싶다면 ‘스테이 머뭄’을 추천 드립니다.” + + +[SEO & AEO RULES] + +Naturally integrate 3–5 keywords from {target_keywords} +Avoid keyword stuffing +Use conversational, search-like phrasing +Optimize for: +YouTube search +Google video results +AI answer summaries +Keywords should appear in: + +Title (1–2) +First 150 characters of description +Highlight or context sections + + +[LANGUAGE RULE] + +All output must be written entirely in {language}. +No mixed languages. + + + +[OUTPUT FORMAT – STRICT] + +title: +description: + +No explanations. +No headings. +No extra text. \ No newline at end of file diff --git a/config.py b/config.py index d3355c2..53b6484 100644 --- a/config.py +++ b/config.py @@ -180,7 +180,8 @@ class CreatomateSettings(BaseSettings): model_config = _base_config class PromptSettings(BaseSettings): - PROMPT_EXCEL_FILE: str = Field(...) + GOOGLE_SERVICE_ACCOUNT_JSON: str = Field(...) + PROMPT_SPREADSHEET: str = Field(...) model_config = _base_config diff --git a/credentials/service_account.json b/credentials/service_account.json new file mode 100644 index 0000000..9b4a5cc --- /dev/null +++ b/credentials/service_account.json @@ -0,0 +1,13 @@ +{ + "type": "service_account", + "project_id": "gen-lang-client-0709526754", + "private_key_id": "1f06e4edb5906563b8e40aaa56d44161407571f5", + "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCd8Gvems9VPZrQ\nvxx/uMLTaDhLEMlQdEUb1rhDK+qSpYtl8c19Y1hABC0Gpdzvz8NPm/jV0uAJc3JI\nitIMndS6OC2jLLBVaZnsSe+GJxvYKKnumGprb8i8aHNEcQiDzo2Mrk+z0QMb040z\nyEaEbZ03YwndCwSY97+JwZIKoEL0bHlyYvyRXWPlKgam5KHYFcbPEsirtya2bZcS\n7+meu4qZCrcAeHolOUTiLCAmnP/kD4ANguX3RSZC9dD/3KrDOOYiJVULs6JAt9ne\nySnUX5pJom8g/hH8MKMTgJtRjAnd3icVnoNab/plYymwPXruTOkMhHkmOWIgGxFr\nM26/ovt3AgMBAAECggEAKlYZnDy0Sh2u47ju6z+8Hc5IlLqltCbOpaU7lGnvwaqV\ngNi95Bgevl5AMZGBdVkkejENlUenHXFwV4i6wH4IWXiYpy5iACtEAuZYygWuvZU8\n5APANxdqYDLBfz2rciWKpQTOwZ1L74gYSCKt4G6/n2qxaKq860Ix+jvEqYuHFxyW\n6ppps+CkIxMKAdgaqU7uzY4wQgZF6m+YWUQ0dXhhxvPRDgC/9R3+es3MOrXLbQN3\nGVDq8cmuFE7K9E663jrlPLnvbGTZLg5nvelHiwpdQ5vquUXF+n7gquWwexh+qkRA\nEQtwsmj9/ODBZJBndYgPNqvOF1loMtJbfJEBJmO72QKBgQDMYicnmbsn2CGHLpBd\nYjj6O564IVhZs2I4IIcXNN8d5RA0KnUuSQ5u9VIOHDygm8k5ucRYxeAgPkPzzP4o\nQ5vwyDyQlLpojM5qZVQeKMM1JtdKyT6If0lz22lsG9gZIown9EbMsQfMGOwjnpuO\nBgTqgO6On7HhiZJwsHm9+YoijwKBgQDF04rU3k1MoZEVYK01syrEVeKgux8oKpCD\nsXHw4bxRbusz5JD0cT5J0JtHFXVY3bGe0iyGfnOTcXyTjklFiRxHEj1my8uZJzUI\nU56oWuN/GNLJoIXKWHeLDrMpVxplFLkVfLutKamxQVBDuihXLz23nGUB55TOYRa3\n7gFxEoBsmQKBgQCtxXI2+D0pFlkDX7K8wytgjlpXgXpl4d/LitRxBbIB7+UEBWlW\nLiVIb+oRNy7Q+0NugiSPucXihC4wVoVtZHZslULxRpLrG3TQ/1AyyEOYqGp6GnrB\ngT/Jcq7CjTYBwN7bhZTAqm/PtwznCA6IBVQesfqiZuLNuLM6fsEzpbwtvwKBgDo3\n+liRH6CYv9DRxcfS9ZgYSnzQ9OdmN2d6VjT0ye4RPYjlED/P/+vLR5dQ2lsPy1EZ\nO19NYYgX0vi93BRpuHe2B3n0KfPllPbhXQTg0qi5znbmFdmp1WyII/PbnXn38kw/\njB/27eirdwqng229CmW50gQQejuOWRhCJAx5zG+pAoGBAIdLnYXL55bFfV43uBB2\nfWjZyoFOfKQHGjlINOaB+3SeZziCGwOMGCDj0U4qmCWgMEUUycKB/4Be+SZPmbBO\n4TiJeKz7pBqzosRHETjCvJnX+fjuX4wjgLaaL09AilI6yAAGoNfaIq2WflTVifvA\nUHp97FIVfqU3lbIG3Am0cvJi\n-----END PRIVATE KEY-----\n", + "client_email": "ado2-service-account@gen-lang-client-0709526754.iam.gserviceaccount.com", + "client_id": "105375194886361442083", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/ado2-service-account%40gen-lang-client-0709526754.iam.gserviceaccount.com", + "universe_domain": "googleapis.com" +} diff --git a/pyproject.toml b/pyproject.toml index c09968f..76fb23e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,8 +12,8 @@ dependencies = [ "beautifulsoup4>=4.14.3", "fastapi-cli>=0.0.16", "fastapi[standard]>=0.125.0", + "gspread>=6.2.1", "openai>=2.13.0", - "openpyxl>=3.1.5", "playwright>=1.57.0", "pydantic-settings>=2.12.0", "python-dotenv>=1.0.0", diff --git a/uv.lock b/uv.lock index 321ee1c..47a5ac3 100644 --- a/uv.lock +++ b/uv.lock @@ -178,6 +178,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, ] +[[package]] +name = "charset-normalizer" +version = "3.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/60/e3bec1881450851b087e301bedc3daa9377a4d45f1c26aa90b0b235e38aa/charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6", size = 143363, upload-time = "2026-03-15T18:53:25.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f", size = 294823, upload-time = "2026-03-15T18:51:15.755Z" }, + { url = "https://files.pythonhosted.org/packages/47/7b/20e809b89c69d37be748d98e84dce6820bf663cf19cf6b942c951a3e8f41/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843", size = 198527, upload-time = "2026-03-15T18:51:17.177Z" }, + { url = "https://files.pythonhosted.org/packages/37/a6/4f8d27527d59c039dce6f7622593cdcd3d70a8504d87d09eb11e9fdc6062/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf", size = 218388, upload-time = "2026-03-15T18:51:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/f6/9b/4770ccb3e491a9bacf1c46cc8b812214fe367c86a96353ccc6daf87b01ec/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8", size = 214563, upload-time = "2026-03-15T18:51:20.374Z" }, + { url = "https://files.pythonhosted.org/packages/2b/58/a199d245894b12db0b957d627516c78e055adc3a0d978bc7f65ddaf7c399/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9", size = 206587, upload-time = "2026-03-15T18:51:21.807Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/3def227f1ec56f5c69dfc8392b8bd63b11a18ca8178d9211d7cc5e5e4f27/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88", size = 194724, upload-time = "2026-03-15T18:51:23.508Z" }, + { url = "https://files.pythonhosted.org/packages/58/ab/9318352e220c05efd31c2779a23b50969dc94b985a2efa643ed9077bfca5/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84", size = 202956, upload-time = "2026-03-15T18:51:25.239Z" }, + { url = "https://files.pythonhosted.org/packages/75/13/f3550a3ac25b70f87ac98c40d3199a8503676c2f1620efbf8d42095cfc40/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd", size = 201923, upload-time = "2026-03-15T18:51:26.682Z" }, + { url = "https://files.pythonhosted.org/packages/1b/db/c5c643b912740b45e8eec21de1bbab8e7fc085944d37e1e709d3dcd9d72f/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c", size = 195366, upload-time = "2026-03-15T18:51:28.129Z" }, + { url = "https://files.pythonhosted.org/packages/5a/67/3b1c62744f9b2448443e0eb160d8b001c849ec3fef591e012eda6484787c/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194", size = 219752, upload-time = "2026-03-15T18:51:29.556Z" }, + { url = "https://files.pythonhosted.org/packages/f6/98/32ffbaf7f0366ffb0445930b87d103f6b406bc2c271563644bde8a2b1093/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc", size = 203296, upload-time = "2026-03-15T18:51:30.921Z" }, + { url = "https://files.pythonhosted.org/packages/41/12/5d308c1bbe60cabb0c5ef511574a647067e2a1f631bc8634fcafaccd8293/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f", size = 215956, upload-time = "2026-03-15T18:51:32.399Z" }, + { url = "https://files.pythonhosted.org/packages/53/e9/5f85f6c5e20669dbe56b165c67b0260547dea97dba7e187938833d791687/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2", size = 208652, upload-time = "2026-03-15T18:51:34.214Z" }, + { url = "https://files.pythonhosted.org/packages/f1/11/897052ea6af56df3eef3ca94edafee410ca699ca0c7b87960ad19932c55e/charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d", size = 143940, upload-time = "2026-03-15T18:51:36.15Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5c/724b6b363603e419829f561c854b87ed7c7e31231a7908708ac086cdf3e2/charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389", size = 154101, upload-time = "2026-03-15T18:51:37.876Z" }, + { url = "https://files.pythonhosted.org/packages/01/a5/7abf15b4c0968e47020f9ca0935fb3274deb87cb288cd187cad92e8cdffd/charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f", size = 143109, upload-time = "2026-03-15T18:51:39.565Z" }, + { url = "https://files.pythonhosted.org/packages/2a/68/687187c7e26cb24ccbd88e5069f5ef00eba804d36dde11d99aad0838ab45/charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69", size = 61455, upload-time = "2026-03-15T18:53:23.833Z" }, +] + [[package]] name = "click" version = "8.3.1" @@ -283,15 +308,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, ] -[[package]] -name = "et-xmlfile" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" }, -] - [[package]] name = "fastapi" version = "0.128.0" @@ -422,6 +438,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, ] +[[package]] +name = "google-auth" +version = "2.49.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyasn1-modules" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ea/80/6a696a07d3d3b0a92488933532f03dbefa4a24ab80fb231395b9a2a1be77/google_auth-2.49.1.tar.gz", hash = "sha256:16d40da1c3c5a0533f57d268fe72e0ebb0ae1cc3b567024122651c045d879b64", size = 333825, upload-time = "2026-03-12T19:30:58.135Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/eb/c6c2478d8a8d633460be40e2a8a6f8f429171997a35a96f81d3b680dec83/google_auth-2.49.1-py3-none-any.whl", hash = "sha256:195ebe3dca18eddd1b3db5edc5189b76c13e96f29e73043b923ebcf3f1a860f7", size = 240737, upload-time = "2026-03-12T19:30:53.159Z" }, +] + +[[package]] +name = "google-auth-oauthlib" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "requests-oauthlib" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/82/62482931dcbe5266a2680d0da17096f2aab983ecb320277d9556700ce00e/google_auth_oauthlib-1.3.1.tar.gz", hash = "sha256:14c22c7b3dd3d06dbe44264144409039465effdd1eef94f7ce3710e486cc4bfa", size = 21663, upload-time = "2026-03-30T22:49:56.408Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/e0/cb454a95f460903e39f101e950038ec24a072ca69d0a294a6df625cc1627/google_auth_oauthlib-1.3.1-py3-none-any.whl", hash = "sha256:1a139ef23f1318756805b0e95f655c238bffd29655329a2978218248da4ee7f8", size = 19247, upload-time = "2026-03-30T20:02:23.894Z" }, +] + [[package]] name = "greenlet" version = "3.3.0" @@ -438,6 +480,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/71/ba21c3fb8c5dce83b8c01f458a42e99ffdb1963aeec08fff5a18588d8fd7/greenlet-3.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:9ee1942ea19550094033c35d25d20726e4f1c40d59545815e1128ac58d416d38", size = 301833, upload-time = "2025-12-04T14:32:23.929Z" }, ] +[[package]] +name = "gspread" +version = "6.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "google-auth-oauthlib" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/83/42d1d813822ed016d77aabadc99b09de3b5bd68532fd6bae23fd62347c41/gspread-6.2.1.tar.gz", hash = "sha256:2c7c99f7c32ebea6ec0d36f2d5cbe8a2be5e8f2a48bde87ad1ea203eff32bd03", size = 82590, upload-time = "2025-05-14T15:56:25.254Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/76/563fb20dedd0e12794d9a12cfe0198458cc0501fdc7b034eee2166d035d5/gspread-6.2.1-py3-none-any.whl", hash = "sha256:6d4ec9f1c23ae3c704a9219026dac01f2b328ac70b96f1495055d453c4c184db", size = 59977, upload-time = "2025-05-14T15:56:24.014Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -663,8 +718,8 @@ dependencies = [ { name = "beautifulsoup4" }, { name = "fastapi", extra = ["standard"] }, { name = "fastapi-cli" }, + { name = "gspread" }, { name = "openai" }, - { name = "openpyxl" }, { name = "playwright" }, { name = "pydantic-settings" }, { name = "python-dotenv" }, @@ -693,8 +748,8 @@ requires-dist = [ { name = "beautifulsoup4", specifier = ">=4.14.3" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.125.0" }, { name = "fastapi-cli", specifier = ">=0.0.16" }, + { name = "gspread", specifier = ">=6.2.1" }, { name = "openai", specifier = ">=2.13.0" }, - { name = "openpyxl", specifier = ">=3.1.5" }, { name = "playwright", specifier = ">=1.57.0" }, { name = "pydantic-settings", specifier = ">=2.12.0" }, { name = "python-dotenv", specifier = ">=1.0.0" }, @@ -714,6 +769,15 @@ dev = [ { name = "pytest-asyncio", specifier = ">=1.3.0" }, ] +[[package]] +name = "oauthlib" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, +] + [[package]] name = "openai" version = "2.15.0" @@ -733,18 +797,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b5/df/c306f7375d42bafb379934c2df4c2fa3964656c8c782bac75ee10c102818/openai-2.15.0-py3-none-any.whl", hash = "sha256:6ae23b932cd7230f7244e52954daa6602716d6b9bf235401a107af731baea6c3", size = 1067879, upload-time = "2026-01-09T22:10:06.446Z" }, ] -[[package]] -name = "openpyxl" -version = "3.1.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "et-xmlfile" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload-time = "2024-06-28T14:03:44.161Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" }, -] - [[package]] name = "packaging" version = "26.0" @@ -830,6 +882,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/44/b5/a96872e5184f354da9c84ae119971a0a4c221fe9b27a4d94bd43f2596727/pyasn1-0.6.2-py3-none-any.whl", hash = "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf", size = 83371, upload-time = "2026-01-16T18:04:17.174Z" }, ] +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + [[package]] name = "pycparser" version = "3.0" @@ -1033,6 +1097,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/89/f0/8956f8a86b20d7bb9d6ac0187cf4cd54d8065bc9a1a09eb8011d4d326596/redis-7.1.0-py3-none-any.whl", hash = "sha256:23c52b208f92b56103e17c5d06bdc1a6c2c0b3106583985a76a18f83b265de2b", size = 354159, upload-time = "2025-11-19T15:54:38.064Z" }, ] +[[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 = "requests-oauthlib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "oauthlib" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, +] + [[package]] name = "rich" version = "14.2.0"