Compare commits
14 Commits
5f83ecd7ee
...
view_manag
| Author | SHA1 | Date | |
|---|---|---|---|
| a9f6323715 | |||
| c0f24715f4 | |||
| 7fe9dfe6f6 | |||
| 112755224f | |||
| 4778d2043b | |||
| 10d5bdc518 | |||
| 5ecf58ba6a | |||
| 5002528eae | |||
| 4826c4a744 | |||
| 56e1edb0db | |||
| 45bc817428 | |||
| a1d9a19d04 | |||
|
|
c5fa92c4ec | ||
|
|
a92c5b699f |
@@ -1,2 +1,4 @@
|
||||
Uv managed python project that grabs data from the meta api and saves it in a timescaledb database.
|
||||
|
||||
Always use uv run to execute python related stuff
|
||||
|
||||
|
||||
22
Dockerfile
22
Dockerfile
@@ -1,5 +1,5 @@
|
||||
# Dockerfile for Meta API Grabber
|
||||
# Production-ready container for scheduled data collection
|
||||
# Dockerfile for View Manager
|
||||
# Production-ready container for database view management
|
||||
|
||||
FROM python:3.13-slim
|
||||
|
||||
@@ -15,26 +15,20 @@ RUN apt-get update && apt-get install -y \
|
||||
RUN pip install --no-cache-dir uv
|
||||
|
||||
# Copy project files
|
||||
COPY pyproject.toml uv.lock README.md ./
|
||||
COPY pyproject.toml README.md ./
|
||||
COPY src/ ./src/
|
||||
COPY metadata.yaml ./metadata.yaml
|
||||
|
||||
# Install Python dependencies using uv
|
||||
RUN uv pip install --system -e .
|
||||
|
||||
# Copy environment file template (will be overridden by volume mount)
|
||||
# This is just for documentation - actual .env should be mounted
|
||||
COPY .env.example .env.example
|
||||
|
||||
# Create directory for token metadata
|
||||
RUN mkdir -p /app/data
|
||||
|
||||
# Set environment variables
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV PYTHONPATH=/app
|
||||
|
||||
# Health check - verify the script can at least import
|
||||
# Health check - verify database connection
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
||||
CMD python -c "from src.meta_api_grabber.scheduled_grabber import ScheduledInsightsGrabber; print('OK')" || exit 1
|
||||
CMD python -c "import asyncio; from meta_api_grabber.database import TimescaleDBClient; asyncio.run(TimescaleDBClient().connect())" || exit 1
|
||||
|
||||
# Run the scheduled grabber
|
||||
CMD ["python", "-m", "src.meta_api_grabber.scheduled_grabber"]
|
||||
# Run the view manager setup
|
||||
CMD ["uv", "run", "view-manager-setup"]
|
||||
|
||||
@@ -1,210 +0,0 @@
|
||||
# Meta API Rate Limiter Enhancements
|
||||
|
||||
## Summary
|
||||
|
||||
Enhanced the rate limiter in [rate_limiter.py](src/meta_api_grabber/rate_limiter.py) to monitor **all** Meta API rate limit headers as documented in the [official Meta documentation](https://developers.facebook.com/docs/graph-api/overview/rate-limiting).
|
||||
|
||||
## New Headers Monitored
|
||||
|
||||
### 1. **X-App-Usage** (Platform Rate Limits)
|
||||
Tracks application-level rate limits across all users.
|
||||
|
||||
**Fields:**
|
||||
- `call_count`: Percentage of calls made (0-100)
|
||||
- `total_cputime`: Percentage of CPU time used (0-100)
|
||||
- `total_time`: Percentage of total time used (0-100)
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"call_count": 28,
|
||||
"total_time": 25,
|
||||
"total_cputime": 25
|
||||
}
|
||||
```
|
||||
|
||||
### 2. **X-Ad-Account-Usage** (Ad Account Specific)
|
||||
Tracks rate limits for specific ad accounts. **Stored per account ID** to support multiple accounts.
|
||||
|
||||
**Fields:**
|
||||
- `acc_id_util_pct`: Percentage of ad account usage (0-100)
|
||||
- `reset_time_duration`: Time in seconds until rate limit resets
|
||||
- `ads_api_access_tier`: Access tier (e.g., "standard_access", "development_access")
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"acc_id_util_pct": 9.67,
|
||||
"reset_time_duration": 100,
|
||||
"ads_api_access_tier": "standard_access"
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** Metrics are tracked separately for each ad account in a dictionary keyed by account ID (e.g., `act_123456789`).
|
||||
|
||||
### 3. **X-Business-Use-Case-Usage** (Business Use Case Limits)
|
||||
Tracks rate limits per business use case (ads_insights, ads_management, etc.).
|
||||
|
||||
**Fields:**
|
||||
- `business_id`: Business object ID
|
||||
- `type`: Type of BUC (ads_insights, ads_management, custom_audience, etc.)
|
||||
- `call_count`: Percentage of calls made (0-100)
|
||||
- `total_cputime`: Percentage of CPU time (0-100)
|
||||
- `total_time`: Percentage of total time (0-100)
|
||||
- `estimated_time_to_regain_access`: Time in minutes until access is restored
|
||||
- `ads_api_access_tier`: Access tier
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"66782684": [{
|
||||
"type": "ads_management",
|
||||
"call_count": 95,
|
||||
"total_cputime": 20,
|
||||
"total_time": 20,
|
||||
"estimated_time_to_regain_access": 0,
|
||||
"ads_api_access_tier": "development_access"
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
### 4. **x-fb-ads-insights-throttle** (Legacy)
|
||||
Original header still supported for backward compatibility.
|
||||
|
||||
**Fields:**
|
||||
- `app_id_util_pct`: App usage percentage
|
||||
- `acc_id_util_pct`: Account usage percentage
|
||||
|
||||
## Key Enhancements
|
||||
|
||||
### 1. Intelligent Throttling
|
||||
The rate limiter now uses `estimated_time_to_regain_access` and `reset_time_duration` to calculate optimal delays:
|
||||
|
||||
```python
|
||||
# If we have estimated_time_to_regain_access from BUC header
|
||||
if self.estimated_time_to_regain_access > 0:
|
||||
delay = self.estimated_time_to_regain_access * 60 # Convert minutes to seconds
|
||||
|
||||
# If we have reset_time_duration from Ad Account header
|
||||
elif self.reset_time_duration > 0:
|
||||
delay = self.reset_time_duration * 0.5 # Use fraction as safety margin
|
||||
```
|
||||
|
||||
### 2. Comprehensive Error Code Detection
|
||||
Expanded error code detection to include all Meta rate limit error codes:
|
||||
|
||||
- **4**: App rate limit
|
||||
- **17**: User rate limit
|
||||
- **32**: Pages rate limit
|
||||
- **613**: Custom rate limit
|
||||
- **80000-80014**: Business Use Case rate limits (Ads Insights, Ads Management, Custom Audience, Instagram, LeadGen, Messenger, Pages, WhatsApp, Catalog)
|
||||
|
||||
### 3. Debug Logging
|
||||
All headers are now logged in DEBUG mode with detailed parsing information:
|
||||
|
||||
```python
|
||||
logger.debug(f"X-App-Usage header: {header_value}")
|
||||
logger.debug(f"Parsed X-App-Usage: {result}")
|
||||
```
|
||||
|
||||
### 4. Enhanced Statistics
|
||||
The `get_stats()` and `print_stats()` methods now display comprehensive metrics from all headers:
|
||||
|
||||
```
|
||||
======================================================================
|
||||
RATE LIMITER STATISTICS
|
||||
======================================================================
|
||||
Total Requests: 0
|
||||
Throttled Requests: 0
|
||||
Rate Limit Errors: 0
|
||||
|
||||
X-App-Usage (Platform Rate Limits):
|
||||
Call Count: 95.0%
|
||||
Total CPU Time: 90.0%
|
||||
Total Time: 88.0%
|
||||
|
||||
X-Ad-Account-Usage:
|
||||
Account Usage: 97.5%
|
||||
Reset Time Duration: 300s
|
||||
API Access Tier: standard_access
|
||||
|
||||
X-Business-Use-Case-Usage:
|
||||
Type: ads_insights
|
||||
Call Count: 98.0%
|
||||
Total CPU Time: 95.0%
|
||||
Total Time: 92.0%
|
||||
Est. Time to Regain: 15 min
|
||||
|
||||
Legacy (x-fb-ads-insights-throttle):
|
||||
App Usage: 93.0%
|
||||
Account Usage: 96.0%
|
||||
|
||||
Max Usage Across All Metrics: 98.0%
|
||||
Currently Throttled: True
|
||||
======================================================================
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Enable Debug Logging
|
||||
To see all header parsing in debug mode:
|
||||
|
||||
```python
|
||||
import logging
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
```
|
||||
|
||||
### Updating Usage with Account ID
|
||||
When calling `update_usage()`, you can optionally provide an account ID to track per-account metrics:
|
||||
|
||||
```python
|
||||
# Option 1: Provide account_id explicitly
|
||||
limiter.update_usage(response, account_id='act_123456789')
|
||||
|
||||
# Option 2: Let the limiter try to extract it from the response
|
||||
limiter.update_usage(response) # Will attempt to extract account_id
|
||||
```
|
||||
|
||||
### Access New Metrics
|
||||
All metrics are available through the `get_stats()` method:
|
||||
|
||||
```python
|
||||
stats = limiter.get_stats()
|
||||
|
||||
print(f"App call count: {stats['app_call_count']}%")
|
||||
print(f"Regain access in: {stats['estimated_time_to_regain_access']} min")
|
||||
|
||||
# Per-account metrics
|
||||
for account_id, usage in stats['ad_account_usage'].items():
|
||||
print(f"Account {account_id}:")
|
||||
print(f" Usage: {usage['acc_id_util_pct']}%")
|
||||
print(f" Reset in: {usage['reset_time_duration']}s")
|
||||
print(f" API tier: {usage['ads_api_access_tier']}")
|
||||
|
||||
# Business use case details
|
||||
for buc in stats['buc_usage']:
|
||||
print(f"BUC {buc['type']}: {buc['call_count']}%")
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Run the test script to see the rate limiter in action:
|
||||
|
||||
```bash
|
||||
uv run python test_rate_limiter.py
|
||||
```
|
||||
|
||||
This will demonstrate:
|
||||
- Parsing all four header types
|
||||
- Intelligent throttling based on usage
|
||||
- Comprehensive statistics display
|
||||
- Debug logging output
|
||||
|
||||
## References
|
||||
|
||||
- [Meta Graph API Rate Limiting](https://developers.facebook.com/docs/graph-api/overview/rate-limiting)
|
||||
- [Meta Marketing API Best Practices](https://developers.facebook.com/docs/marketing-api/insights/best-practices/)
|
||||
88
VIEWS_SUMMARY.md
Normal file
88
VIEWS_SUMMARY.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# Database Views Summary
|
||||
|
||||
## Overview
|
||||
Updated the public schema with new views to make advertising analytics data from Meta, Google, and combined sources more accessible.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Adapted `explore_schemas.py` Script
|
||||
- Modified to save schema exploration output to `schema_info.txt`
|
||||
- Provides persistent reference for table structures across meta, google, and alpinebits schemas
|
||||
- Usage: `uv run explore_schemas.py`
|
||||
|
||||
### 2. New Views Added
|
||||
|
||||
#### **account_insights_by_country** ✓
|
||||
- Aggregates Meta campaign insights by country to account level
|
||||
- **Columns**: time, account_id, country, impressions, clicks, spend, link_click, landing_page_view, lead
|
||||
- **Purpose**: Analyze Meta advertising performance by geographic region
|
||||
- **Source**: meta.custom_campaign_country
|
||||
|
||||
#### **g_campaign_insights** ✓
|
||||
- Google campaign-level insights with calculated performance metrics
|
||||
- **Columns**: time, campaign_id, campaign_name, clicks, impressions, interactions, cost_micros, cost, conversions, all_conversions, conversions_value, ctr, cpm, cpc, cost_per_conversion
|
||||
- **Purpose**: Unified Google campaign performance view with key metrics (CTR, CPM, CPC, Cost per Conversion)
|
||||
- **Source**: google.campaign_metrics
|
||||
|
||||
#### **unified_account_insights_by_device** ✓
|
||||
- Combines Meta and Google account insights broken down by device type
|
||||
- **Columns**: time, google_account_id, meta_account_id, device, google_impressions, meta_impressions, total_impressions, google_clicks, meta_clicks, total_clicks, google_cost, meta_spend, total_spend, meta_link_clicks, meta_leads
|
||||
- **Purpose**: Compare Meta vs Google performance by device (DESKTOP, MOBILE, TABLET)
|
||||
- **Requires**: account_metadata table for linking Meta and Google accounts
|
||||
|
||||
#### **unified_account_insights_by_gender** ✓
|
||||
- Meta audience insights broken down by gender
|
||||
- **Columns**: time, meta_account_id, gender, impressions, clicks, spend, link_clicks, leads
|
||||
- **Purpose**: Analyze Meta advertising performance by audience gender demographics
|
||||
- **Source**: meta account_insights_by_gender
|
||||
|
||||
### 3. Existing Views (Not Modified)
|
||||
The following views were already present and working correctly:
|
||||
- **campaign_insights** - Base Meta campaign insights
|
||||
- **campaign_insights_by_gender/age/device/country** - Meta campaign breakdowns
|
||||
- **account_insights** - Aggregated Meta account insights
|
||||
- **account_insights_by_gender/age/device/gender_and_age** - Meta account breakdowns
|
||||
- **ads_insights** (materialized) - Individual ad performance
|
||||
- **adset_insights** - Ad set level insights
|
||||
- **g_account_insights** - Google account-level insights
|
||||
- **g_account_insights_device** - Google account insights by device
|
||||
- **unified_account_insights** - Combined Meta+Google account-level view
|
||||
|
||||
## Testing
|
||||
All views have been tested and verified to:
|
||||
- Execute without errors
|
||||
- Return valid data with correct column structures
|
||||
- Support LIMIT queries for performance verification
|
||||
|
||||
## Usage Examples
|
||||
|
||||
```sql
|
||||
-- View Meta performance by country
|
||||
SELECT time, country, SUM(impressions), SUM(spend), SUM(lead)
|
||||
FROM account_insights_by_country
|
||||
WHERE account_id = '1416908162571377'
|
||||
GROUP BY time, country;
|
||||
|
||||
-- View Google campaign metrics
|
||||
SELECT time, campaign_name, clicks, impressions, cpc, cpm
|
||||
FROM g_campaign_insights
|
||||
WHERE ctr > 2.0
|
||||
ORDER BY time DESC
|
||||
LIMIT 10;
|
||||
|
||||
-- Compare device performance across Meta and Google
|
||||
SELECT time, device, total_impressions, total_clicks, total_spend
|
||||
FROM unified_account_insights_by_device
|
||||
ORDER BY time DESC;
|
||||
|
||||
-- Analyze Meta audience by gender
|
||||
SELECT time, gender, impressions, clicks, spend, leads
|
||||
FROM unified_account_insights_by_gender
|
||||
WHERE time > CURRENT_DATE - INTERVAL '30 days'
|
||||
ORDER BY time, gender;
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
- Set up automated refreshes for materialized views if needed
|
||||
- Create additional unified views for campaign-level comparisons
|
||||
- Consider indexing frequently queried views for performance optimization
|
||||
@@ -19,22 +19,22 @@ services:
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
|
||||
# Meta API Grabber - Scheduled data collection
|
||||
meta-grabber:
|
||||
# View Manager - Setup database schema and views
|
||||
view-manager:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: meta_api_grabber
|
||||
container_name: view_manager
|
||||
environment:
|
||||
# Database connection (connects to timescaledb service)
|
||||
DATABASE_URL: postgresql://meta_user:meta_password@timescaledb:5432/meta_insights
|
||||
env_file:
|
||||
- .env # Must contain META_ACCESS_TOKEN, META_APP_ID, META_APP_SECRET
|
||||
- .env # Optional: for any additional configuration
|
||||
volumes:
|
||||
# Mount .env for token updates (auto-refresh will update the file)
|
||||
- ./.env:/app/.env
|
||||
# Mount token metadata file (preserves token refresh state across restarts)
|
||||
- ./.meta_token.json:/app/.meta_token.json
|
||||
# Mount metadata.yaml for account configuration
|
||||
- ./metadata.yaml:/app/metadata.yaml:ro
|
||||
# Optional: Mount custom db_schema.sql if you want to edit it externally
|
||||
# - ./src/meta_api_grabber/db_schema.sql:/app/src/meta_api_grabber/db_schema.sql:ro
|
||||
depends_on:
|
||||
timescaledb:
|
||||
condition: service_healthy
|
||||
|
||||
110
explore_schemas.py
Normal file
110
explore_schemas.py
Normal file
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script to explore available tables and their structures in meta, google, and alpinebits schemas.
|
||||
Saves output to schema_info.txt file.
|
||||
"""
|
||||
import os
|
||||
import asyncio
|
||||
import asyncpg
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
|
||||
DB_URL = os.getenv("DATABASE_URL")
|
||||
OUTPUT_FILE = "schema_info.txt"
|
||||
|
||||
async def get_connection():
|
||||
"""Create database connection"""
|
||||
return await asyncpg.connect(DB_URL)
|
||||
|
||||
async def get_schema_info(schema_name, output_file):
|
||||
"""Get all tables and their columns for a schema"""
|
||||
conn = await get_connection()
|
||||
|
||||
header = f"\n{'='*80}\nSCHEMA: {schema_name}\n{'='*80}\n"
|
||||
print(header, end="")
|
||||
output_file.write(header)
|
||||
|
||||
# Get all tables in schema
|
||||
query = """
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = $1
|
||||
ORDER BY table_name
|
||||
"""
|
||||
tables = await conn.fetch(query, schema_name)
|
||||
|
||||
if not tables:
|
||||
msg = f"No tables found in schema '{schema_name}'\n"
|
||||
print(msg, end="")
|
||||
output_file.write(msg)
|
||||
await conn.close()
|
||||
return
|
||||
|
||||
for row in tables:
|
||||
table_name = row['table_name']
|
||||
table_header = f"\nTable: {table_name}\n" + "-" * 80 + "\n"
|
||||
print(table_header, end="")
|
||||
output_file.write(table_header)
|
||||
|
||||
# Get columns for this table
|
||||
col_query = """
|
||||
SELECT column_name, data_type, is_nullable
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = $1 AND table_name = $2
|
||||
ORDER BY ordinal_position
|
||||
"""
|
||||
columns = await conn.fetch(col_query, schema_name, table_name)
|
||||
|
||||
for col in columns:
|
||||
null_str = "NULL" if col['is_nullable'] == "YES" else "NOT NULL"
|
||||
col_line = f" - {col['column_name']}: {col['data_type']} ({null_str})\n"
|
||||
print(col_line, end="")
|
||||
output_file.write(col_line)
|
||||
|
||||
# Get row count
|
||||
try:
|
||||
count_query = f"SELECT COUNT(*) FROM {schema_name}.{table_name}"
|
||||
count = await conn.fetchval(count_query)
|
||||
count_line = f" Rows: {count}\n"
|
||||
print(count_line, end="")
|
||||
output_file.write(count_line)
|
||||
except Exception as e:
|
||||
count_line = f" Rows: (unable to count)\n"
|
||||
print(count_line, end="")
|
||||
output_file.write(count_line)
|
||||
|
||||
# Show sample data
|
||||
try:
|
||||
sample_query = f"SELECT * FROM {schema_name}.{table_name} LIMIT 1"
|
||||
sample = await conn.fetchrow(sample_query)
|
||||
if sample:
|
||||
sample_line = f" Sample: {dict(sample)}\n"
|
||||
print(sample_line, end="")
|
||||
output_file.write(sample_line)
|
||||
except Exception as e:
|
||||
sample_line = f" Sample: (unable to fetch)\n"
|
||||
print(sample_line, end="")
|
||||
output_file.write(sample_line)
|
||||
|
||||
await conn.close()
|
||||
|
||||
async def main():
|
||||
schemas = ["meta", "google", "alpinebits"]
|
||||
|
||||
with open(OUTPUT_FILE, "w") as output_file:
|
||||
for schema in schemas:
|
||||
try:
|
||||
await get_schema_info(schema, output_file)
|
||||
except Exception as e:
|
||||
error_msg = f"\nError accessing schema '{schema}': {e}\n"
|
||||
print(error_msg, end="")
|
||||
output_file.write(error_msg)
|
||||
|
||||
completion_msg = f"\n{'='*80}\nExploration complete! Output saved to {OUTPUT_FILE}\n"
|
||||
print(completion_msg, end="")
|
||||
output_file.write(completion_msg)
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
24
metadata.yaml
Normal file
24
metadata.yaml
Normal file
@@ -0,0 +1,24 @@
|
||||
# Metadata configuration for account mapping
|
||||
# This file stores important metadata that links different accounts and identifiers
|
||||
|
||||
accounts:
|
||||
- label: "Hotel Bemelmans Post"
|
||||
meta_account_id: "238334370765317"
|
||||
google_account_id: "7581209925"
|
||||
alpinebits_hotel_code: "39054_001"
|
||||
|
||||
- label: "Jagdhof-Kaltern"
|
||||
#meta_account_id: "act_987654321"
|
||||
google_account_id: "1951919786"
|
||||
alpinebits_hotel_code: "39052_001"
|
||||
|
||||
- label: "Residence Erika"
|
||||
google_account_id: "6604634947"
|
||||
alpinebits_hotel_code: "39040_001"
|
||||
|
||||
|
||||
# Add more accounts as needed
|
||||
# - label: "Your Account Name"
|
||||
# meta_account_id: "act_xxxxx"
|
||||
# google_account_id: "xxx-xxx-xxxx"
|
||||
# alpinebits_hotel_code: "HOTELxxx"
|
||||
@@ -1,18 +1,13 @@
|
||||
[project]
|
||||
name = "meta-api-grabber"
|
||||
version = "0.1.0"
|
||||
description = "Meta Marketing API data grabber with TimescaleDB storage"
|
||||
description = "View manager for TimescaleDB with metadata handling"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"aiohttp>=3.13.1",
|
||||
"alembic>=1.17.0",
|
||||
"asyncpg>=0.30.0",
|
||||
"facebook-business>=23.0.3",
|
||||
"google-ads>=28.3.0",
|
||||
"python-dotenv>=1.1.1",
|
||||
"requests-oauthlib>=2.0.0",
|
||||
"sqlalchemy[asyncio]>=2.0.44",
|
||||
"pyyaml>=6.0.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
@@ -22,13 +17,7 @@ test = [
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
meta-auth = "meta_api_grabber.auth:main"
|
||||
meta-scheduled = "meta_api_grabber.scheduled_grabber:main"
|
||||
meta-insights = "meta_api_grabber.insights_grabber:main"
|
||||
meta-test-accounts = "meta_api_grabber.test_ad_accounts:main"
|
||||
meta-test-leads = "meta_api_grabber.test_page_leads:main"
|
||||
meta-token = "meta_api_grabber.token_manager:main"
|
||||
google-ads-test = "meta_api_grabber.test_google_ads_accounts:main"
|
||||
view-manager-setup = "meta_api_grabber.setup_and_wait:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
|
||||
551
schema_info.txt
Normal file
551
schema_info.txt
Normal file
@@ -0,0 +1,551 @@
|
||||
|
||||
================================================================================
|
||||
SCHEMA: meta
|
||||
================================================================================
|
||||
|
||||
Table: ad_sets
|
||||
--------------------------------------------------------------------------------
|
||||
- id: character varying (NULL)
|
||||
- name: character varying (NULL)
|
||||
- adlabels: jsonb (NULL)
|
||||
- bid_info: jsonb (NULL)
|
||||
- account_id: character varying (NULL)
|
||||
- bid_amount: numeric (NULL)
|
||||
- start_time: timestamp with time zone (NULL)
|
||||
- campaign_id: character varying (NULL)
|
||||
- created_time: timestamp with time zone (NULL)
|
||||
- daily_budget: numeric (NULL)
|
||||
- updated_time: timestamp with time zone (NULL)
|
||||
- bid_constraints: jsonb (NULL)
|
||||
- lifetime_budget: numeric (NULL)
|
||||
- promoted_object: jsonb (NULL)
|
||||
- _airbyte_raw_id: character varying (NOT NULL)
|
||||
- _airbyte_extracted_at: timestamp with time zone (NOT NULL)
|
||||
- _airbyte_generation_id: bigint (NULL)
|
||||
- _airbyte_meta: jsonb (NOT NULL)
|
||||
Rows: 47
|
||||
Sample: {'id': '120207428712700382', 'name': 'Neue Anzeigengruppe für Leads', 'adlabels': None, 'bid_info': None, 'account_id': '1416908162571377', 'bid_amount': None, 'start_time': datetime.datetime(2024, 3, 15, 8, 50, 7, tzinfo=datetime.timezone.utc), 'campaign_id': '120207428712650382', 'created_time': datetime.datetime(2024, 3, 15, 8, 50, 7, tzinfo=datetime.timezone.utc), 'daily_budget': Decimal('1000.000000000'), 'updated_time': datetime.datetime(2025, 1, 4, 6, 42, tzinfo=datetime.timezone.utc), 'bid_constraints': None, 'lifetime_budget': Decimal('0E-9'), 'promoted_object': '{"page_id": "106160371809313"}', '_airbyte_raw_id': 'dccb9c4b-7fa8-4e98-9249-7a359dd86521', '_airbyte_extracted_at': datetime.datetime(2025, 11, 13, 16, 6, 6, 112000, tzinfo=datetime.timezone.utc), '_airbyte_generation_id': 2, '_airbyte_meta': '{"changes": [], "sync_id": 42}'}
|
||||
|
||||
Table: ads
|
||||
--------------------------------------------------------------------------------
|
||||
- id: character varying (NULL)
|
||||
- name: character varying (NULL)
|
||||
- status: character varying (NULL)
|
||||
- adlabels: jsonb (NULL)
|
||||
- adset_id: character varying (NULL)
|
||||
- bid_info: jsonb (NULL)
|
||||
- bid_type: character varying (NULL)
|
||||
- creative: jsonb (NULL)
|
||||
- account_id: character varying (NULL)
|
||||
- bid_amount: bigint (NULL)
|
||||
- campaign_id: character varying (NULL)
|
||||
- created_time: timestamp with time zone (NULL)
|
||||
- source_ad_id: character varying (NULL)
|
||||
- updated_time: timestamp with time zone (NULL)
|
||||
- tracking_specs: jsonb (NULL)
|
||||
- recommendations: jsonb (NULL)
|
||||
- conversion_specs: jsonb (NULL)
|
||||
- effective_status: character varying (NULL)
|
||||
- last_updated_by_app_id: character varying (NULL)
|
||||
- _airbyte_raw_id: character varying (NOT NULL)
|
||||
- _airbyte_extracted_at: timestamp with time zone (NOT NULL)
|
||||
- _airbyte_generation_id: bigint (NULL)
|
||||
- _airbyte_meta: jsonb (NOT NULL)
|
||||
Rows: 163
|
||||
Sample: {'id': '120236221098270382', 'name': 'Vertriebsmitarbeiter_14.11.2025_Grafik_1', 'status': 'ACTIVE', 'adlabels': None, 'adset_id': '120236221098250382', 'bid_info': None, 'bid_type': 'ABSOLUTE_OCPM', 'creative': '{"id": "2042451856579224"}', 'account_id': '1416908162571377', 'bid_amount': None, 'campaign_id': '120219679742650382', 'created_time': datetime.datetime(2025, 11, 14, 9, 30, 29, tzinfo=datetime.timezone.utc), 'source_ad_id': '0', 'updated_time': datetime.datetime(2025, 11, 14, 9, 45, 27, tzinfo=datetime.timezone.utc), 'tracking_specs': '[{"fb_pixel": ["1165111234632775"], "action.type": ["offsite_conversion"]}, {"action.type": ["onsite_conversion"], "conversion_id": ["9883202595118306"]}, {"action.type": ["onsite_conversion"]}, {"page": ["106160371809313"], "post": ["860615083140579"], "action.type": ["post_engagement"]}, {"page": ["106160371809313"], "post": ["860615083140579"], "action.type": ["post_interaction_gross"]}, {"post": ["860615083140579"], "post.wall": ["106160371809313"], "action.type": ["link_click"]}]', 'recommendations': None, 'conversion_specs': '[{"action.type": ["offsite_conversion"], "conversion_id": ["25218548091171302"]}]', 'effective_status': 'ACTIVE', 'last_updated_by_app_id': '119211728144504', '_airbyte_raw_id': '826ad572-f17f-4f94-a682-fe633da5092d', '_airbyte_extracted_at': datetime.datetime(2025, 11, 14, 16, 6, 20, 979000, tzinfo=datetime.timezone.utc), '_airbyte_generation_id': 1, '_airbyte_meta': '{"changes": [], "sync_id": 44}'}
|
||||
|
||||
Table: ads_insights
|
||||
--------------------------------------------------------------------------------
|
||||
- cpc: numeric (NULL)
|
||||
- cpm: numeric (NULL)
|
||||
- cpp: numeric (NULL)
|
||||
- ctr: numeric (NULL)
|
||||
- ad_id: character varying (NULL)
|
||||
- reach: bigint (NULL)
|
||||
- spend: numeric (NULL)
|
||||
- clicks: bigint (NULL)
|
||||
- actions: jsonb (NULL)
|
||||
- ad_name: character varying (NULL)
|
||||
- adset_id: character varying (NULL)
|
||||
- date_stop: date (NULL)
|
||||
- frequency: numeric (NULL)
|
||||
- objective: character varying (NULL)
|
||||
- account_id: character varying (NULL)
|
||||
- adset_name: character varying (NULL)
|
||||
- date_start: date (NULL)
|
||||
- campaign_id: character varying (NULL)
|
||||
- conversions: jsonb (NULL)
|
||||
- impressions: bigint (NULL)
|
||||
- account_name: character varying (NULL)
|
||||
- created_time: date (NULL)
|
||||
- action_values: jsonb (NULL)
|
||||
- account_currency: character varying (NULL)
|
||||
- ad_click_actions: jsonb (NULL)
|
||||
- conversion_values: jsonb (NULL)
|
||||
- optimization_goal: character varying (NULL)
|
||||
- cost_per_conversion: jsonb (NULL)
|
||||
- ad_impression_actions: jsonb (NULL)
|
||||
- _airbyte_raw_id: character varying (NOT NULL)
|
||||
- _airbyte_extracted_at: timestamp with time zone (NOT NULL)
|
||||
- _airbyte_generation_id: bigint (NULL)
|
||||
- _airbyte_meta: jsonb (NOT NULL)
|
||||
Rows: 6654
|
||||
Sample: {'cpc': Decimal('1.190000000'), 'cpm': Decimal('10.967742000'), 'cpp': Decimal('13.678161000'), 'ctr': Decimal('0.921659000'), 'ad_id': '120226947319320382', 'reach': 174, 'spend': Decimal('2.380000000'), 'clicks': 2, 'actions': '[{"value": 15, "1d_view": 15, "7d_view": 15, "28d_view": 15, "action_type": "page_engagement", "action_target_id": "0", "action_destination": "unknown"}, {"value": 3, "1d_view": 3, "7d_view": 3, "28d_view": 3, "action_type": "page_engagement", "action_target_id": "1240005757657489", "action_destination": "Content Creator (m/w/d) gesucht – Jetzt bewerben!"}, {"value": 15, "1d_view": 15, "7d_view": 15, "28d_view": 15, "action_type": "post_engagement", "action_target_id": "0", "action_destination": "unknown"}, {"value": 3, "1d_view": 3, "7d_view": 3, "28d_view": 3, "action_type": "post_engagement", "action_target_id": "1240005757657489", "action_destination": "Content Creator (m/w/d) gesucht – Jetzt bewerben!"}, {"value": 15, "1d_view": 15, "7d_view": 15, "28d_view": 15, "action_type": "video_view", "action_target_id": "0", "action_destination": "unknown"}, {"value": 3, "1d_view": 3, "7d_view": 3, "28d_view": 3, "action_type": "video_view", "action_target_id": "1240005757657489", "action_destination": "Content Creator (m/w/d) gesucht – Jetzt bewerben!"}]', 'ad_name': 'Content_Creator_11.06.25', 'adset_id': '120226947319300382', 'date_stop': datetime.date(2025, 11, 14), 'frequency': Decimal('1.247126000'), 'objective': 'OUTCOME_LEADS', 'account_id': '1416908162571377', 'adset_name': 'Content Creator Bewerber 25_InstantForm_DE', 'date_start': datetime.date(2025, 11, 14), 'campaign_id': '120219679742650382', 'conversions': None, 'impressions': 217, 'account_name': '99tales', 'created_time': datetime.date(2025, 6, 11), 'action_values': None, 'account_currency': 'EUR', 'ad_click_actions': None, 'conversion_values': None, 'optimization_goal': 'LEAD_GENERATION', 'cost_per_conversion': None, 'ad_impression_actions': None, '_airbyte_raw_id': '1603d437-0177-4fb7-9f25-0a1f2e91a548', '_airbyte_extracted_at': datetime.datetime(2025, 11, 14, 16, 8, 38, 501000, tzinfo=datetime.timezone.utc), '_airbyte_generation_id': 3, '_airbyte_meta': '{"changes": [], "sync_id": 44}'}
|
||||
|
||||
Table: ads_insights_age_and_gender
|
||||
--------------------------------------------------------------------------------
|
||||
- age: character varying (NULL)
|
||||
- cpc: numeric (NULL)
|
||||
- cpm: numeric (NULL)
|
||||
- cpp: numeric (NULL)
|
||||
- ad_id: character varying (NULL)
|
||||
- clicks: bigint (NULL)
|
||||
- gender: character varying (NULL)
|
||||
- actions: jsonb (NULL)
|
||||
- adset_id: character varying (NULL)
|
||||
- account_id: character varying (NULL)
|
||||
- date_start: date (NULL)
|
||||
- campaign_id: character varying (NULL)
|
||||
- conversions: jsonb (NULL)
|
||||
- account_name: character varying (NULL)
|
||||
- created_time: date (NULL)
|
||||
- _airbyte_raw_id: character varying (NOT NULL)
|
||||
- _airbyte_extracted_at: timestamp with time zone (NOT NULL)
|
||||
- _airbyte_generation_id: bigint (NULL)
|
||||
- _airbyte_meta: jsonb (NOT NULL)
|
||||
Rows: 5962
|
||||
Sample: {'age': '45-54', 'cpc': None, 'cpm': Decimal('3.750000000'), 'cpp': Decimal('4.285714000'), 'ad_id': '120236395246040196', 'clicks': 0, 'gender': 'male', 'actions': None, 'adset_id': '120223804684640196', 'account_id': '238334370765317', 'date_start': datetime.date(2025, 10, 5), 'campaign_id': '120223804684630196', 'conversions': None, 'account_name': 'Hotel Bemelmans Post', 'created_time': datetime.date(2025, 8, 26), '_airbyte_raw_id': 'fc0e034f-5938-4fe7-9364-d36743807245', '_airbyte_extracted_at': datetime.datetime(2025, 11, 3, 15, 47, 3, 587000, tzinfo=datetime.timezone.utc), '_airbyte_generation_id': 0, '_airbyte_meta': '{"changes": [], "sync_id": 1}'}
|
||||
|
||||
Table: ads_insights_country
|
||||
--------------------------------------------------------------------------------
|
||||
- cpc: numeric (NULL)
|
||||
- cpm: numeric (NULL)
|
||||
- cpp: numeric (NULL)
|
||||
- ctr: numeric (NULL)
|
||||
- ad_id: character varying (NULL)
|
||||
- clicks: bigint (NULL)
|
||||
- country: character varying (NULL)
|
||||
- objective: character varying (NULL)
|
||||
- account_id: character varying (NULL)
|
||||
- date_start: date (NULL)
|
||||
- conversions: jsonb (NULL)
|
||||
- impressions: bigint (NULL)
|
||||
- created_time: date (NULL)
|
||||
- account_currency: character varying (NULL)
|
||||
- _airbyte_raw_id: character varying (NOT NULL)
|
||||
- _airbyte_extracted_at: timestamp with time zone (NOT NULL)
|
||||
- _airbyte_generation_id: bigint (NULL)
|
||||
- _airbyte_meta: jsonb (NOT NULL)
|
||||
Rows: 1701
|
||||
Sample: {'cpc': Decimal('0.459545000'), 'cpm': Decimal('12.086073000'), 'cpp': Decimal('15.033457000'), 'ctr': Decimal('2.630006000'), 'ad_id': '120224889911030196', 'clicks': 44, 'country': 'IT', 'objective': 'OUTCOME_LEADS', 'account_id': '238334370765317', 'date_start': datetime.date(2025, 10, 5), 'conversions': None, 'impressions': 1673, 'created_time': datetime.date(2025, 4, 10), 'account_currency': 'EUR', '_airbyte_raw_id': '37a9805d-66c5-4288-ac51-04ec995a1ba6', '_airbyte_extracted_at': datetime.datetime(2025, 11, 3, 15, 51, 24, 448000, tzinfo=datetime.timezone.utc), '_airbyte_generation_id': 0, '_airbyte_meta': '{"changes": [], "sync_id": 1}'}
|
||||
|
||||
Table: campaigns
|
||||
--------------------------------------------------------------------------------
|
||||
- id: character varying (NULL)
|
||||
- name: character varying (NULL)
|
||||
- status: character varying (NULL)
|
||||
- adlabels: jsonb (NULL)
|
||||
- objective: character varying (NULL)
|
||||
- spend_cap: numeric (NULL)
|
||||
- stop_time: timestamp with time zone (NULL)
|
||||
- account_id: character varying (NULL)
|
||||
- start_time: timestamp with time zone (NULL)
|
||||
- buying_type: character varying (NULL)
|
||||
- issues_info: jsonb (NULL)
|
||||
- bid_strategy: character varying (NULL)
|
||||
- created_time: timestamp with time zone (NULL)
|
||||
- daily_budget: numeric (NULL)
|
||||
- updated_time: timestamp with time zone (NULL)
|
||||
- lifetime_budget: numeric (NULL)
|
||||
- budget_remaining: numeric (NULL)
|
||||
- effective_status: character varying (NULL)
|
||||
- boosted_object_id: character varying (NULL)
|
||||
- configured_status: character varying (NULL)
|
||||
- source_campaign_id: numeric (NULL)
|
||||
- special_ad_category: character varying (NULL)
|
||||
- smart_promotion_type: character varying (NULL)
|
||||
- budget_rebalance_flag: boolean (NULL)
|
||||
- special_ad_category_country: jsonb (NULL)
|
||||
- _airbyte_raw_id: character varying (NOT NULL)
|
||||
- _airbyte_extracted_at: timestamp with time zone (NOT NULL)
|
||||
- _airbyte_generation_id: bigint (NULL)
|
||||
- _airbyte_meta: jsonb (NOT NULL)
|
||||
Rows: 25
|
||||
Sample: {'id': '120207428712650382', 'name': 'Bewerber_99tales', 'status': 'PAUSED', 'adlabels': None, 'objective': 'OUTCOME_LEADS', 'spend_cap': None, 'stop_time': None, 'account_id': '1416908162571377', 'start_time': datetime.datetime(2024, 3, 15, 8, 50, 7, tzinfo=datetime.timezone.utc), 'buying_type': 'AUCTION', 'issues_info': None, 'bid_strategy': None, 'created_time': datetime.datetime(2024, 3, 15, 8, 50, 6, tzinfo=datetime.timezone.utc), 'daily_budget': None, 'updated_time': datetime.datetime(2025, 10, 10, 5, 42, 18, tzinfo=datetime.timezone.utc), 'lifetime_budget': None, 'budget_remaining': Decimal('0E-9'), 'effective_status': 'PAUSED', 'boosted_object_id': None, 'configured_status': 'PAUSED', 'source_campaign_id': Decimal('0E-9'), 'special_ad_category': 'EMPLOYMENT', 'smart_promotion_type': 'GUIDED_CREATION', 'budget_rebalance_flag': False, 'special_ad_category_country': '["IT"]', '_airbyte_raw_id': 'd94ca411-6b5a-4382-871c-681de8f35440', '_airbyte_extracted_at': datetime.datetime(2025, 11, 13, 17, 24, 46, 639000, tzinfo=datetime.timezone.utc), '_airbyte_generation_id': 12, '_airbyte_meta': '{"changes": [], "sync_id": 42}'}
|
||||
|
||||
Table: custom_campaign_country
|
||||
--------------------------------------------------------------------------------
|
||||
- cpc: numeric (NULL)
|
||||
- cpm: numeric (NULL)
|
||||
- cpp: numeric (NULL)
|
||||
- ctr: numeric (NULL)
|
||||
- reach: bigint (NULL)
|
||||
- spend: numeric (NULL)
|
||||
- clicks: bigint (NULL)
|
||||
- actions: jsonb (NULL)
|
||||
- country: character varying (NULL)
|
||||
- date_stop: date (NULL)
|
||||
- frequency: numeric (NULL)
|
||||
- objective: character varying (NULL)
|
||||
- account_id: character varying (NULL)
|
||||
- date_start: date (NULL)
|
||||
- campaign_id: character varying (NULL)
|
||||
- impressions: bigint (NULL)
|
||||
- created_time: date (NULL)
|
||||
- social_spend: numeric (NULL)
|
||||
- _airbyte_raw_id: character varying (NOT NULL)
|
||||
- _airbyte_extracted_at: timestamp with time zone (NOT NULL)
|
||||
- _airbyte_generation_id: bigint (NULL)
|
||||
- _airbyte_meta: jsonb (NOT NULL)
|
||||
Rows: 2038
|
||||
Sample: {'cpc': Decimal('0.908257000'), 'cpm': Decimal('7.444729000'), 'cpp': Decimal('9.872357000'), 'ctr': Decimal('0.819672000'), 'reach': 92, 'spend': Decimal('0.908257000'), 'clicks': 1, 'actions': '[{"value": 18, "1d_view": 17, "7d_view": 17, "1d_click": 1, "28d_view": 17, "7d_click": 1, "28d_click": 1, "action_type": "page_engagement"}, {"value": 18, "1d_view": 17, "7d_view": 17, "1d_click": 1, "28d_view": 17, "7d_click": 1, "28d_click": 1, "action_type": "post_engagement"}, {"value": 1, "1d_click": 1, "7d_click": 1, "28d_click": 1, "action_type": "link_click"}, {"value": 17, "1d_view": 17, "7d_view": 17, "28d_view": 17, "action_type": "video_view"}]', 'country': 'AT', 'date_stop': datetime.date(2025, 11, 14), 'frequency': Decimal('1.326087000'), 'objective': 'OUTCOME_LEADS', 'account_id': '1416908162571377', 'date_start': datetime.date(2025, 11, 14), 'campaign_id': '120235573403300382', 'impressions': 122, 'created_time': datetime.date(2025, 10, 31), 'social_spend': None, '_airbyte_raw_id': '31ca2e7a-6757-4c8e-96e8-6e3768c86315', '_airbyte_extracted_at': datetime.datetime(2025, 11, 14, 16, 14, 21, 341000, tzinfo=datetime.timezone.utc), '_airbyte_generation_id': 1, '_airbyte_meta': '{"changes": [], "sync_id": 44}'}
|
||||
|
||||
Table: custom_campaign_device
|
||||
--------------------------------------------------------------------------------
|
||||
- ad_id: character varying (NULL)
|
||||
- reach: bigint (NULL)
|
||||
- spend: numeric (NULL)
|
||||
- clicks: bigint (NULL)
|
||||
- actions: jsonb (NULL)
|
||||
- date_stop: date (NULL)
|
||||
- frequency: numeric (NULL)
|
||||
- account_id: character varying (NULL)
|
||||
- date_start: date (NULL)
|
||||
- campaign_id: character varying (NULL)
|
||||
- impressions: bigint (NULL)
|
||||
- campaign_name: character varying (NULL)
|
||||
- device_platform: character varying (NULL)
|
||||
- _airbyte_raw_id: character varying (NOT NULL)
|
||||
- _airbyte_extracted_at: timestamp with time zone (NOT NULL)
|
||||
- _airbyte_generation_id: bigint (NULL)
|
||||
- _airbyte_meta: jsonb (NOT NULL)
|
||||
Rows: 1291
|
||||
Sample: {'ad_id': None, 'reach': 5, 'spend': Decimal('0.111560000'), 'clicks': 0, 'actions': None, 'date_stop': datetime.date(2025, 11, 14), 'frequency': Decimal('1.000000000'), 'account_id': '1416908162571377', 'date_start': datetime.date(2025, 11, 14), 'campaign_id': '120235573403300382', 'impressions': 5, 'campaign_name': 'MairLorenz_Cold_Traffic_Conversions_InstantForm', 'device_platform': 'desktop', '_airbyte_raw_id': 'a9231688-5cde-4f02-a84d-99759e7ea3d6', '_airbyte_extracted_at': datetime.datetime(2025, 11, 14, 16, 18, 56, 210000, tzinfo=datetime.timezone.utc), '_airbyte_generation_id': 0, '_airbyte_meta': '{"changes": [], "sync_id": 44}'}
|
||||
|
||||
Table: custom_campaign_gender
|
||||
--------------------------------------------------------------------------------
|
||||
- age: character varying (NULL)
|
||||
- cpc: numeric (NULL)
|
||||
- cpm: numeric (NULL)
|
||||
- cpp: numeric (NULL)
|
||||
- ctr: numeric (NULL)
|
||||
- reach: bigint (NULL)
|
||||
- spend: numeric (NULL)
|
||||
- clicks: bigint (NULL)
|
||||
- gender: character varying (NULL)
|
||||
- actions: jsonb (NULL)
|
||||
- date_stop: date (NULL)
|
||||
- frequency: numeric (NULL)
|
||||
- account_id: character varying (NULL)
|
||||
- date_start: date (NULL)
|
||||
- campaign_id: character varying (NULL)
|
||||
- conversions: jsonb (NULL)
|
||||
- impressions: bigint (NULL)
|
||||
- created_time: date (NULL)
|
||||
- updated_time: date (NULL)
|
||||
- campaign_name: character varying (NULL)
|
||||
- purchase_roas: jsonb (NULL)
|
||||
- account_currency: character varying (NULL)
|
||||
- _airbyte_raw_id: character varying (NOT NULL)
|
||||
- _airbyte_extracted_at: timestamp with time zone (NOT NULL)
|
||||
- _airbyte_generation_id: bigint (NULL)
|
||||
- _airbyte_meta: jsonb (NOT NULL)
|
||||
Rows: 8554
|
||||
Sample: {'age': '18-24', 'cpc': None, 'cpm': Decimal('12.060708000'), 'cpp': Decimal('13.916202000'), 'ctr': Decimal('0E-9'), 'reach': 13, 'spend': Decimal('0.180911000'), 'clicks': 0, 'gender': 'female', 'actions': '[{"value": 3, "1d_view": 3, "7d_view": 3, "28d_view": 3, "action_type": "page_engagement"}, {"value": 3, "1d_view": 3, "7d_view": 3, "28d_view": 3, "action_type": "post_engagement"}, {"value": 3, "1d_view": 3, "7d_view": 3, "28d_view": 3, "action_type": "video_view"}]', 'date_stop': datetime.date(2025, 11, 14), 'frequency': Decimal('1.153846000'), 'account_id': '1416908162571377', 'date_start': datetime.date(2025, 11, 14), 'campaign_id': '120218699205090382', 'conversions': None, 'impressions': 15, 'created_time': datetime.date(2025, 3, 15), 'updated_time': datetime.date(2025, 10, 10), 'campaign_name': '15_03_25_Adverto_Hotelmarketing', 'purchase_roas': None, 'account_currency': 'EUR', '_airbyte_raw_id': '4642fee0-6c01-4297-b4fd-48acddf44062', '_airbyte_extracted_at': datetime.datetime(2025, 11, 14, 16, 16, 22, 159000, tzinfo=datetime.timezone.utc), '_airbyte_generation_id': 3, '_airbyte_meta': '{"changes": [], "sync_id": 44}'}
|
||||
|
||||
Table: custom_campaign_gender_ab_soft_reset
|
||||
--------------------------------------------------------------------------------
|
||||
- age: character varying (NULL)
|
||||
- cpc: numeric (NULL)
|
||||
- cpm: numeric (NULL)
|
||||
- cpp: numeric (NULL)
|
||||
- ctr: numeric (NULL)
|
||||
- reach: bigint (NULL)
|
||||
- spend: numeric (NULL)
|
||||
- clicks: bigint (NULL)
|
||||
- gender: character varying (NULL)
|
||||
- actions: jsonb (NULL)
|
||||
- date_stop: date (NULL)
|
||||
- frequency: numeric (NULL)
|
||||
- account_id: character varying (NULL)
|
||||
- date_start: date (NULL)
|
||||
- campaign_id: character varying (NULL)
|
||||
- conversions: jsonb (NULL)
|
||||
- impressions: bigint (NULL)
|
||||
- created_time: date (NULL)
|
||||
- updated_time: date (NULL)
|
||||
- campaign_name: character varying (NULL)
|
||||
- purchase_roas: jsonb (NULL)
|
||||
- account_currency: character varying (NULL)
|
||||
- _airbyte_raw_id: character varying (NOT NULL)
|
||||
- _airbyte_extracted_at: timestamp with time zone (NOT NULL)
|
||||
- _airbyte_generation_id: bigint (NULL)
|
||||
- _airbyte_meta: jsonb (NOT NULL)
|
||||
Rows: 1105
|
||||
Sample: {'age': None, 'cpc': Decimal('1.044085000'), 'cpm': Decimal('22.273805000'), 'cpp': Decimal('24.281038000'), 'ctr': Decimal('2.133333000'), 'reach': 344, 'spend': Decimal('8.352677000'), 'clicks': 8, 'gender': 'female', 'actions': '[{"value": 7, "1d_click": 7, "7d_click": 7, "28d_click": 7, "action_type": "page_engagement"}, {"value": 2, "1d_click": 2, "7d_click": 2, "28d_click": 2, "action_type": "landing_page_view"}, {"value": 2, "1d_click": 2, "7d_click": 2, "28d_click": 2, "action_type": "omni_landing_page_view"}, {"value": 7, "1d_click": 7, "7d_click": 7, "28d_click": 7, "action_type": "post_engagement"}, {"value": 7, "1d_click": 7, "7d_click": 7, "28d_click": 7, "action_type": "link_click"}]', 'date_stop': datetime.date(2025, 11, 11), 'frequency': Decimal('1.090116000'), 'account_id': '238334370765317', 'date_start': datetime.date(2025, 11, 11), 'campaign_id': '120223804684630196', 'conversions': None, 'impressions': 375, 'created_time': datetime.date(2025, 4, 4), 'updated_time': datetime.date(2025, 5, 23), 'campaign_name': 'Conversions_Hotel_Bemelmans_DEU', 'purchase_roas': None, 'account_currency': 'EUR', '_airbyte_raw_id': '37862ffc-80d4-4c32-bb17-85656877ab99', '_airbyte_extracted_at': datetime.datetime(2025, 11, 11, 16, 27, 40, 468000, tzinfo=datetime.timezone.utc), '_airbyte_generation_id': 1, '_airbyte_meta': '{"changes": [], "sync_id": 29}'}
|
||||
|
||||
Table: custom_conversions
|
||||
--------------------------------------------------------------------------------
|
||||
- id: character varying (NULL)
|
||||
- name: character varying (NULL)
|
||||
- rule: character varying (NULL)
|
||||
- business: character varying (NULL)
|
||||
- account_id: character varying (NULL)
|
||||
- description: character varying (NULL)
|
||||
- is_archived: boolean (NULL)
|
||||
- data_sources: jsonb (NULL)
|
||||
- creation_time: timestamp with time zone (NULL)
|
||||
- is_unavailable: boolean (NULL)
|
||||
- retention_days: numeric (NULL)
|
||||
- last_fired_time: timestamp with time zone (NULL)
|
||||
- first_fired_time: timestamp with time zone (NULL)
|
||||
- custom_event_type: character varying (NULL)
|
||||
- event_source_type: character varying (NULL)
|
||||
- default_conversion_value: numeric (NULL)
|
||||
- offline_conversion_data_set: character varying (NULL)
|
||||
- _airbyte_raw_id: character varying (NOT NULL)
|
||||
- _airbyte_extracted_at: timestamp with time zone (NOT NULL)
|
||||
- _airbyte_generation_id: bigint (NULL)
|
||||
- _airbyte_meta: jsonb (NOT NULL)
|
||||
Rows: 12
|
||||
Sample: {'id': '1049186899714335', 'name': 'Purchase', 'rule': '{"and":[{"event":{"eq":"PageView"}},{"or":[{"Event Parameters":{"eq":"purchase"}}]}]}', 'business': None, 'account_id': '277560745', 'description': None, 'is_archived': True, 'data_sources': '[{"id": "7753250311370403", "name": "Spitalerhof Facebook Pixel", "source_type": "PIXEL"}]', 'creation_time': datetime.datetime(2024, 1, 25, 10, 21, 3, tzinfo=datetime.timezone.utc), 'is_unavailable': False, 'retention_days': Decimal('0E-9'), 'last_fired_time': None, 'first_fired_time': None, 'custom_event_type': 'OTHER', 'event_source_type': 'pixel', 'default_conversion_value': Decimal('0E-9'), 'offline_conversion_data_set': None, '_airbyte_raw_id': 'e9adb670-8ebb-441e-b3dc-5163ac5922ec', '_airbyte_extracted_at': datetime.datetime(2025, 11, 14, 16, 9, 25, 869000, tzinfo=datetime.timezone.utc), '_airbyte_generation_id': 41, '_airbyte_meta': '{"changes": [], "sync_id": 44}'}
|
||||
|
||||
Table: customcampaign_insights
|
||||
--------------------------------------------------------------------------------
|
||||
- cpc: numeric (NULL)
|
||||
- cpm: numeric (NULL)
|
||||
- cpp: numeric (NULL)
|
||||
- ctr: numeric (NULL)
|
||||
- ad_id: character varying (NULL)
|
||||
- reach: bigint (NULL)
|
||||
- spend: numeric (NULL)
|
||||
- clicks: bigint (NULL)
|
||||
- actions: jsonb (NULL)
|
||||
- date_stop: date (NULL)
|
||||
- frequency: numeric (NULL)
|
||||
- objective: character varying (NULL)
|
||||
- account_id: character varying (NULL)
|
||||
- date_start: date (NULL)
|
||||
- campaign_id: character varying (NULL)
|
||||
- impressions: bigint (NULL)
|
||||
- created_time: date (NULL)
|
||||
- campaign_name: character varying (NULL)
|
||||
- purchase_roas: jsonb (NULL)
|
||||
- optimization_goal: character varying (NULL)
|
||||
- _airbyte_raw_id: character varying (NOT NULL)
|
||||
- _airbyte_extracted_at: timestamp with time zone (NOT NULL)
|
||||
- _airbyte_generation_id: bigint (NULL)
|
||||
- _airbyte_meta: jsonb (NOT NULL)
|
||||
Rows: 2328
|
||||
Sample: {'cpc': Decimal('0.565714000'), 'cpm': Decimal('13.846154000'), 'cpp': Decimal('17.217391000'), 'ctr': Decimal('2.447552000'), 'ad_id': None, 'reach': 690, 'spend': Decimal('11.880000000'), 'clicks': 21, 'actions': '[{"value": 3, "1d_click": 3, "7d_click": 3, "28d_click": 3, "action_type": "offsite_complete_registration_add_meta_leads"}, {"value": 174, "1d_view": 161, "7d_view": 161, "1d_click": 13, "28d_view": 161, "7d_click": 13, "28d_click": 13, "action_type": "page_engagement"}, {"value": 174, "1d_view": 161, "7d_view": 161, "1d_click": 13, "28d_view": 161, "7d_click": 13, "28d_click": 13, "action_type": "post_engagement"}, {"value": 3, "1d_click": 3, "7d_click": 3, "28d_click": 3, "action_type": "offsite_content_view_add_meta_leads"}, {"value": 3, "1d_click": 3, "7d_click": 3, "28d_click": 3, "action_type": "lead"}, {"value": 3, "1d_click": 3, "7d_click": 3, "28d_click": 3, "action_type": "offsite_search_add_meta_leads"}, {"value": 11, "1d_click": 11, "7d_click": 11, "28d_click": 11, "action_type": "link_click"}, {"value": 3, "1d_click": 3, "7d_click": 3, "28d_click": 3, "action_type": "onsite_conversion.lead_grouped"}, {"value": 163, "1d_view": 161, "7d_view": 161, "1d_click": 2, "28d_view": 161, "7d_click": 2, "28d_click": 2, "action_type": "video_view"}]', 'date_stop': datetime.date(2025, 11, 14), 'frequency': Decimal('1.243478000'), 'objective': 'OUTCOME_LEADS', 'account_id': '1416908162571377', 'date_start': datetime.date(2025, 11, 14), 'campaign_id': '120218699205090382', 'impressions': 858, 'created_time': datetime.date(2025, 3, 15), 'campaign_name': '15_03_25_Adverto_Hotelmarketing', 'purchase_roas': None, 'optimization_goal': 'Unknown Optimization Goal', '_airbyte_raw_id': '4ad10a16-dd49-4599-bd7c-65a349edbd4c', '_airbyte_extracted_at': datetime.datetime(2025, 11, 14, 16, 11, 34, 572000, tzinfo=datetime.timezone.utc), '_airbyte_generation_id': 4, '_airbyte_meta': '{"changes": [], "sync_id": 44}'}
|
||||
|
||||
================================================================================
|
||||
SCHEMA: google
|
||||
================================================================================
|
||||
|
||||
Table: account_performance_report
|
||||
--------------------------------------------------------------------------------
|
||||
- customer_id: bigint (NULL)
|
||||
- metrics_ctr: numeric (NULL)
|
||||
- segments_date: date (NULL)
|
||||
- metrics_clicks: bigint (NULL)
|
||||
- segments_device: character varying (NULL)
|
||||
- customer_manager: boolean (NULL)
|
||||
- customer_time_zone: character varying (NULL)
|
||||
- metrics_average_cpc: numeric (NULL)
|
||||
- metrics_average_cpe: numeric (NULL)
|
||||
- metrics_average_cpm: numeric (NULL)
|
||||
- metrics_average_cpv: numeric (NULL)
|
||||
- metrics_conversions: numeric (NULL)
|
||||
- metrics_cost_micros: bigint (NULL)
|
||||
- metrics_engagements: bigint (NULL)
|
||||
- metrics_impressions: bigint (NULL)
|
||||
- metrics_video_views: bigint (NULL)
|
||||
- metrics_average_cost: numeric (NULL)
|
||||
- metrics_interactions: bigint (NULL)
|
||||
- segments_day_of_week: character varying (NULL)
|
||||
- customer_test_account: boolean (NULL)
|
||||
- customer_currency_code: character varying (NULL)
|
||||
- metrics_active_view_cpm: numeric (NULL)
|
||||
- metrics_active_view_ctr: numeric (NULL)
|
||||
- metrics_all_conversions: numeric (NULL)
|
||||
- metrics_engagement_rate: numeric (NULL)
|
||||
- metrics_video_view_rate: numeric (NULL)
|
||||
- metrics_interaction_rate: numeric (NULL)
|
||||
- segments_ad_network_type: character varying (NULL)
|
||||
- customer_descriptive_name: character varying (NULL)
|
||||
- metrics_conversions_value: numeric (NULL)
|
||||
- metrics_cost_per_conversion: numeric (NULL)
|
||||
- metrics_value_per_conversion: numeric (NULL)
|
||||
- customer_auto_tagging_enabled: boolean (NULL)
|
||||
- metrics_all_conversions_value: numeric (NULL)
|
||||
- metrics_active_view_impressions: bigint (NULL)
|
||||
- metrics_active_view_viewability: numeric (NULL)
|
||||
- metrics_interaction_event_types: jsonb (NULL)
|
||||
- metrics_search_impression_share: numeric (NULL)
|
||||
- metrics_content_impression_share: numeric (NULL)
|
||||
- metrics_cost_per_all_conversions: numeric (NULL)
|
||||
- metrics_cross_device_conversions: numeric (NULL)
|
||||
- metrics_view_through_conversions: bigint (NULL)
|
||||
- metrics_active_view_measurability: numeric (NULL)
|
||||
- metrics_value_per_all_conversions: numeric (NULL)
|
||||
- metrics_search_rank_lost_impression_share: numeric (NULL)
|
||||
- metrics_active_view_measurable_cost_micros: bigint (NULL)
|
||||
- metrics_active_view_measurable_impressions: bigint (NULL)
|
||||
- metrics_content_rank_lost_impression_share: numeric (NULL)
|
||||
- metrics_conversions_from_interactions_rate: numeric (NULL)
|
||||
- metrics_search_budget_lost_impression_share: numeric (NULL)
|
||||
- metrics_search_exact_match_impression_share: numeric (NULL)
|
||||
- metrics_content_budget_lost_impression_share: numeric (NULL)
|
||||
- metrics_all_conversions_from_interactions_rate: numeric (NULL)
|
||||
- _airbyte_raw_id: character varying (NOT NULL)
|
||||
- _airbyte_extracted_at: timestamp with time zone (NOT NULL)
|
||||
- _airbyte_generation_id: bigint (NULL)
|
||||
- _airbyte_meta: jsonb (NOT NULL)
|
||||
Rows: 543
|
||||
Sample: {'customer_id': 7581209925, 'metrics_ctr': Decimal('0.083333333'), 'segments_date': datetime.date(2025, 11, 14), 'metrics_clicks': 1, 'segments_device': 'DESKTOP', 'customer_manager': False, 'customer_time_zone': 'Europe/Rome', 'metrics_average_cpc': Decimal('10000.000000000'), 'metrics_average_cpe': None, 'metrics_average_cpm': Decimal('833333.333333333'), 'metrics_average_cpv': None, 'metrics_conversions': Decimal('0E-9'), 'metrics_cost_micros': 10000, 'metrics_engagements': 0, 'metrics_impressions': 12, 'metrics_video_views': 0, 'metrics_average_cost': Decimal('10000.000000000'), 'metrics_interactions': 1, 'segments_day_of_week': 'FRIDAY', 'customer_test_account': False, 'customer_currency_code': 'EUR', 'metrics_active_view_cpm': None, 'metrics_active_view_ctr': None, 'metrics_all_conversions': Decimal('0E-9'), 'metrics_engagement_rate': None, 'metrics_video_view_rate': None, 'metrics_interaction_rate': Decimal('0.083333333'), 'segments_ad_network_type': 'MIXED', 'customer_descriptive_name': 'Hotel Bemelmans Post', 'metrics_conversions_value': Decimal('0E-9'), 'metrics_cost_per_conversion': None, 'metrics_value_per_conversion': None, 'customer_auto_tagging_enabled': True, 'metrics_all_conversions_value': Decimal('0E-9'), 'metrics_active_view_impressions': 0, 'metrics_active_view_viewability': None, 'metrics_interaction_event_types': '["2"]', 'metrics_search_impression_share': None, 'metrics_content_impression_share': None, 'metrics_cost_per_all_conversions': None, 'metrics_cross_device_conversions': Decimal('0E-9'), 'metrics_view_through_conversions': 0, 'metrics_active_view_measurability': Decimal('0E-9'), 'metrics_value_per_all_conversions': None, 'metrics_search_rank_lost_impression_share': None, 'metrics_active_view_measurable_cost_micros': 0, 'metrics_active_view_measurable_impressions': 0, 'metrics_content_rank_lost_impression_share': None, 'metrics_conversions_from_interactions_rate': Decimal('0E-9'), 'metrics_search_budget_lost_impression_share': None, 'metrics_search_exact_match_impression_share': None, 'metrics_content_budget_lost_impression_share': None, 'metrics_all_conversions_from_interactions_rate': Decimal('0E-9'), '_airbyte_raw_id': '2ed0b149-adef-4b9d-a7f8-2276ec48d9ee', '_airbyte_extracted_at': datetime.datetime(2025, 11, 14, 19, 8, 43, 233000, tzinfo=datetime.timezone.utc), '_airbyte_generation_id': 1, '_airbyte_meta': '{"changes": [], "sync_id": 45}'}
|
||||
|
||||
Table: campaign_metrics
|
||||
--------------------------------------------------------------------------------
|
||||
- campaign_id: bigint (NULL)
|
||||
- campaign_name: character varying (NULL)
|
||||
- segments_date: date (NULL)
|
||||
- metrics_clicks: bigint (NULL)
|
||||
- metrics_orders: numeric (NULL)
|
||||
- campaign_status: character varying (NULL)
|
||||
- campaign_start_date: character varying (NULL)
|
||||
- metrics_conversions: numeric (NULL)
|
||||
- metrics_cost_micros: bigint (NULL)
|
||||
- metrics_impressions: bigint (NULL)
|
||||
- metrics_interactions: bigint (NULL)
|
||||
- metrics_all_conversions: numeric (NULL)
|
||||
- campaign_campaign_budget: character varying (NULL)
|
||||
- metrics_conversions_value: numeric (NULL)
|
||||
- campaign_optimization_score: numeric (NULL)
|
||||
- _airbyte_raw_id: character varying (NOT NULL)
|
||||
- _airbyte_extracted_at: timestamp with time zone (NOT NULL)
|
||||
- _airbyte_generation_id: bigint (NULL)
|
||||
- _airbyte_meta: jsonb (NOT NULL)
|
||||
Rows: 510
|
||||
Sample: {'campaign_id': 183901432, 'campaign_name': 'WW_DE_Brand', 'segments_date': datetime.date(2025, 10, 27), 'metrics_clicks': 8, 'metrics_orders': Decimal('0E-9'), 'campaign_status': 'ENABLED', 'campaign_start_date': '2015-01-25', 'metrics_conversions': Decimal('0E-9'), 'metrics_cost_micros': 9660000, 'metrics_impressions': 60, 'metrics_interactions': 8, 'metrics_all_conversions': Decimal('0E-9'), 'campaign_campaign_budget': 'customers/7581209925/campaignBudgets/173136112', 'metrics_conversions_value': Decimal('0E-9'), 'campaign_optimization_score': Decimal('0.474560674'), '_airbyte_raw_id': 'b992b3dd-07cb-4ef6-ad60-0a27454de75c', '_airbyte_extracted_at': datetime.datetime(2025, 11, 11, 10, 30, 54, 31000, tzinfo=datetime.timezone.utc), '_airbyte_generation_id': 1, '_airbyte_meta': '{"changes": [], "sync_id": 26}'}
|
||||
|
||||
================================================================================
|
||||
SCHEMA: alpinebits
|
||||
================================================================================
|
||||
|
||||
Table: acked_requests
|
||||
--------------------------------------------------------------------------------
|
||||
- id: integer (NOT NULL)
|
||||
- client_id: character varying (NULL)
|
||||
- unique_id: character varying (NULL)
|
||||
- timestamp: timestamp with time zone (NULL)
|
||||
- username: character varying (NULL)
|
||||
Rows: 814
|
||||
Sample: {'id': 1, 'client_id': 'ASA HOTEL 25.10 @', 'unique_id': '666247dc-9d5a-4eb7-87a7-677bf646', 'timestamp': datetime.datetime(2025, 10, 8, 8, 55, 44, 149632, tzinfo=datetime.timezone.utc), 'username': None}
|
||||
|
||||
Table: conversions
|
||||
--------------------------------------------------------------------------------
|
||||
- id: integer (NOT NULL)
|
||||
- reservation_id: integer (NULL)
|
||||
- customer_id: integer (NULL)
|
||||
- hashed_customer_id: integer (NULL)
|
||||
- hotel_id: character varying (NULL)
|
||||
- pms_reservation_id: character varying (NULL)
|
||||
- reservation_number: character varying (NULL)
|
||||
- reservation_date: date (NULL)
|
||||
- creation_time: timestamp with time zone (NULL)
|
||||
- reservation_type: character varying (NULL)
|
||||
- booking_channel: character varying (NULL)
|
||||
- advertising_medium: character varying (NULL)
|
||||
- advertising_partner: character varying (NULL)
|
||||
- advertising_campagne: character varying (NULL)
|
||||
- arrival_date: date (NULL)
|
||||
- departure_date: date (NULL)
|
||||
- room_status: character varying (NULL)
|
||||
- room_type: character varying (NULL)
|
||||
- room_number: character varying (NULL)
|
||||
- num_adults: integer (NULL)
|
||||
- rate_plan_code: character varying (NULL)
|
||||
- sale_date: date (NULL)
|
||||
- revenue_total: character varying (NULL)
|
||||
- revenue_logis: character varying (NULL)
|
||||
- revenue_board: character varying (NULL)
|
||||
- revenue_fb: character varying (NULL)
|
||||
- revenue_spa: character varying (NULL)
|
||||
- revenue_other: character varying (NULL)
|
||||
- created_at: timestamp with time zone (NULL)
|
||||
Rows: 0
|
||||
|
||||
Table: customers
|
||||
--------------------------------------------------------------------------------
|
||||
- id: integer (NOT NULL)
|
||||
- given_name: character varying (NULL)
|
||||
- contact_id: character varying (NULL)
|
||||
- surname: character varying (NULL)
|
||||
- name_prefix: character varying (NULL)
|
||||
- email_address: character varying (NULL)
|
||||
- phone: character varying (NULL)
|
||||
- email_newsletter: boolean (NULL)
|
||||
- address_line: character varying (NULL)
|
||||
- city_name: character varying (NULL)
|
||||
- postal_code: character varying (NULL)
|
||||
- country_code: character varying (NULL)
|
||||
- gender: character varying (NULL)
|
||||
- birth_date: character varying (NULL)
|
||||
- language: character varying (NULL)
|
||||
- address_catalog: boolean (NULL)
|
||||
- name_title: character varying (NULL)
|
||||
Rows: 487
|
||||
Sample: {'id': 1, 'given_name': 'Adriana', 'contact_id': 'd2e59109-a911-4f8f-8339-d9b119a29e56', 'surname': 'Balasa', 'name_prefix': 'Familie', 'email_address': 'adriana.balasa@yahoo.com', 'phone': '+4917645952405', 'email_newsletter': False, 'address_line': None, 'city_name': None, 'postal_code': None, 'country_code': None, 'gender': None, 'birth_date': None, 'language': 'de', 'address_catalog': False, 'name_title': None}
|
||||
|
||||
Table: hashed_customers
|
||||
--------------------------------------------------------------------------------
|
||||
- id: integer (NOT NULL)
|
||||
- customer_id: integer (NOT NULL)
|
||||
- contact_id: character varying (NULL)
|
||||
- hashed_email: character varying (NULL)
|
||||
- hashed_phone: character varying (NULL)
|
||||
- hashed_given_name: character varying (NULL)
|
||||
- hashed_surname: character varying (NULL)
|
||||
- hashed_city: character varying (NULL)
|
||||
- hashed_postal_code: character varying (NULL)
|
||||
- hashed_country_code: character varying (NULL)
|
||||
- hashed_gender: character varying (NULL)
|
||||
- hashed_birth_date: character varying (NULL)
|
||||
- created_at: timestamp with time zone (NULL)
|
||||
Rows: 487
|
||||
Sample: {'id': 387, 'customer_id': 196, 'contact_id': 'e7718c11-b5b1-43bb-a74e-4c0a2edf7938', 'hashed_email': 'b56472d4360348ef2aa9df7479d9adcb031aa83291c9b6d93eaa9c1b38ad9522', 'hashed_phone': '29cbb04171b6cf2ad9f49a01904d94bf1e4e5d5f41460b5e085b9d832376918a', 'hashed_given_name': 'c49b876c05fd376922883ceaf5b7e7871ae4ae9f24e79958f3fc70efe114627d', 'hashed_surname': 'fb18245487ca9ab219a007c65da495254f9cf8b3a89a2341e1d9a9b2a1e26ba4', 'hashed_city': None, 'hashed_postal_code': None, 'hashed_country_code': None, 'hashed_gender': None, 'hashed_birth_date': None, 'created_at': datetime.datetime(2025, 10, 20, 15, 51, 14, 783241, tzinfo=datetime.timezone.utc)}
|
||||
|
||||
Table: reservations
|
||||
--------------------------------------------------------------------------------
|
||||
- id: integer (NOT NULL)
|
||||
- customer_id: integer (NULL)
|
||||
- unique_id: character varying (NULL)
|
||||
- md5_unique_id: character varying (NULL)
|
||||
- start_date: date (NULL)
|
||||
- end_date: date (NULL)
|
||||
- num_adults: integer (NULL)
|
||||
- num_children: integer (NULL)
|
||||
- children_ages: character varying (NULL)
|
||||
- offer: character varying (NULL)
|
||||
- created_at: timestamp with time zone (NULL)
|
||||
- utm_source: character varying (NULL)
|
||||
- utm_medium: character varying (NULL)
|
||||
- utm_campaign: character varying (NULL)
|
||||
- utm_term: character varying (NULL)
|
||||
- utm_content: character varying (NULL)
|
||||
- user_comment: character varying (NULL)
|
||||
- fbclid: character varying (NULL)
|
||||
- gclid: character varying (NULL)
|
||||
- hotel_code: character varying (NULL)
|
||||
- hotel_name: character varying (NULL)
|
||||
- room_type_code: character varying (NULL)
|
||||
- room_classification_code: character varying (NULL)
|
||||
- room_type: character varying (NULL)
|
||||
- meta_account_id: character varying (NULL)
|
||||
- google_account_id: character varying (NULL)
|
||||
Rows: 517
|
||||
Sample: {'id': 1, 'customer_id': 1, 'unique_id': '5abe9219-37f6-415f-97ff-96846313f16c', 'md5_unique_id': '161e4c2752efd919e8b03a1178bfe61c', 'start_date': datetime.date(2025, 12, 26), 'end_date': datetime.date(2025, 9, 28), 'num_adults': 2, 'num_children': 2, 'children_ages': '9,16', 'offer': 'Zimmer: Doppelzimmer', 'created_at': datetime.datetime(2025, 10, 6, 13, 55, 18, 195026, tzinfo=datetime.timezone.utc), 'utm_source': 'fb', 'utm_medium': 'Facebook_Mobile_Reels', 'utm_campaign': 'Conversions_Hotel_Bemelmans_DEU', 'utm_term': 'Cold_Traffic_Conversions_Hotel_Bemelmans_DEU', 'utm_content': 'Grafik_1_Advent am Ritten', 'user_comment': '', 'fbclid': 'IwZXh0bgNhZW0BMABhZGlkAasrrpKSEUQBHrQyRa1AdOw96ax7dXZoqFypxoThi7Rudg8daveq68g7gpYOI2uO2GEjlkFh_aem_K79wHg9HhLRag_qyO_ibzQ', 'gclid': '', 'hotel_code': '123', 'hotel_name': 'Frangart Inn', 'room_type_code': None, 'room_classification_code': None, 'room_type': None, 'meta_account_id': None, 'google_account_id': None}
|
||||
|
||||
================================================================================
|
||||
Exploration complete! Output saved to schema_info.txt
|
||||
@@ -1,291 +0,0 @@
|
||||
"""
|
||||
OAuth2 authentication module for Meta/Facebook API.
|
||||
|
||||
Handles:
|
||||
- Initial OAuth2 flow for short-lived tokens
|
||||
- Exchange short-lived for long-lived tokens (60 days)
|
||||
- Automatic token refresh
|
||||
- Token persistence
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
|
||||
import requests
|
||||
from dotenv import load_dotenv
|
||||
from requests_oauthlib import OAuth2Session
|
||||
from requests_oauthlib.compliance_fixes import facebook_compliance_fix
|
||||
|
||||
|
||||
class MetaOAuth2:
|
||||
"""Handle OAuth2 authentication flow for Meta/Facebook API."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
app_id: Optional[str] = None,
|
||||
app_secret: Optional[str] = None,
|
||||
redirect_uri: str = "https://localhost/",
|
||||
api_version: str = "v18.0",
|
||||
):
|
||||
"""
|
||||
Initialize OAuth2 handler.
|
||||
|
||||
Args:
|
||||
app_id: Facebook App ID (or set META_APP_ID env var)
|
||||
app_secret: Facebook App Secret (or set META_APP_SECRET env var)
|
||||
redirect_uri: OAuth redirect URI
|
||||
api_version: Facebook API version
|
||||
"""
|
||||
load_dotenv()
|
||||
|
||||
self.app_id = app_id or os.getenv("META_APP_ID")
|
||||
self.app_secret = app_secret or os.getenv("META_APP_SECRET")
|
||||
self.redirect_uri = redirect_uri
|
||||
self.api_version = api_version
|
||||
|
||||
if not self.app_id or not self.app_secret:
|
||||
raise ValueError(
|
||||
"App ID and App Secret are required. "
|
||||
"Provide them as arguments or set META_APP_ID and META_APP_SECRET env vars."
|
||||
)
|
||||
|
||||
self.authorization_base_url = f"https://www.facebook.com/{api_version}/dialog/oauth"
|
||||
self.token_url = f"https://graph.facebook.com/{api_version}/oauth/access_token"
|
||||
|
||||
# Scopes needed for ads insights
|
||||
self.scopes = ["ads_management", "ads_read"]
|
||||
|
||||
def get_authorization_url(self) -> tuple[str, str]:
|
||||
"""
|
||||
Get the authorization URL for the user to visit.
|
||||
|
||||
Returns:
|
||||
Tuple of (authorization_url, state)
|
||||
"""
|
||||
facebook = OAuth2Session(
|
||||
self.app_id,
|
||||
redirect_uri=self.redirect_uri,
|
||||
scope=self.scopes,
|
||||
)
|
||||
facebook = facebook_compliance_fix(facebook)
|
||||
|
||||
authorization_url, state = facebook.authorization_url(self.authorization_base_url)
|
||||
return authorization_url, state
|
||||
|
||||
def fetch_token(self, authorization_response: str) -> Dict[str, str]:
|
||||
"""
|
||||
Exchange the authorization code for an access token.
|
||||
|
||||
Args:
|
||||
authorization_response: The full redirect URL after authorization
|
||||
|
||||
Returns:
|
||||
Token dictionary containing access_token and other info
|
||||
"""
|
||||
facebook = OAuth2Session(
|
||||
self.app_id,
|
||||
redirect_uri=self.redirect_uri,
|
||||
scope=self.scopes,
|
||||
)
|
||||
facebook = facebook_compliance_fix(facebook)
|
||||
|
||||
token = facebook.fetch_token(
|
||||
self.token_url,
|
||||
client_secret=self.app_secret,
|
||||
authorization_response=authorization_response,
|
||||
)
|
||||
|
||||
return token
|
||||
|
||||
def exchange_for_long_lived_token(self, short_lived_token: str) -> Dict[str, any]:
|
||||
"""
|
||||
Exchange short-lived token for long-lived token (60 days).
|
||||
|
||||
Args:
|
||||
short_lived_token: The short-lived access token from OAuth flow
|
||||
|
||||
Returns:
|
||||
Dictionary with access_token and expires_in
|
||||
"""
|
||||
url = "https://graph.facebook.com/oauth/access_token"
|
||||
params = {
|
||||
"grant_type": "fb_exchange_token",
|
||||
"client_id": self.app_id,
|
||||
"client_secret": self.app_secret,
|
||||
"fb_exchange_token": short_lived_token,
|
||||
}
|
||||
|
||||
response = requests.get(url, params=params)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
return {
|
||||
"access_token": data["access_token"],
|
||||
"expires_in": data.get("expires_in", 5184000), # Default 60 days
|
||||
"token_type": data.get("token_type", "bearer"),
|
||||
}
|
||||
|
||||
def get_token_info(self, access_token: str) -> Dict[str, any]:
|
||||
"""
|
||||
Get information about an access token including expiry.
|
||||
|
||||
Args:
|
||||
access_token: Access token to inspect
|
||||
|
||||
Returns:
|
||||
Dictionary with token info including expires_at, is_valid, etc.
|
||||
"""
|
||||
url = "https://graph.facebook.com/debug_token"
|
||||
params = {
|
||||
"input_token": access_token,
|
||||
"access_token": f"{self.app_id}|{self.app_secret}",
|
||||
}
|
||||
|
||||
response = requests.get(url, params=params)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
return data.get("data", {})
|
||||
|
||||
def interactive_auth(self, exchange_for_long_lived: bool = True) -> str:
|
||||
"""
|
||||
Run interactive OAuth2 flow to get access token.
|
||||
|
||||
Args:
|
||||
exchange_for_long_lived: If True, exchanges short-lived for long-lived token (60 days)
|
||||
|
||||
Returns:
|
||||
Access token string (long-lived if exchange_for_long_lived=True)
|
||||
"""
|
||||
print("\n" + "="*60)
|
||||
print("META/FACEBOOK OAUTH2 AUTHENTICATION")
|
||||
print("="*60)
|
||||
|
||||
# Get authorization URL
|
||||
auth_url, state = self.get_authorization_url()
|
||||
|
||||
print("\n1. Please visit this URL in your browser:")
|
||||
print(f"\n {auth_url}\n")
|
||||
print("2. Authorize the application")
|
||||
print("3. You will be redirected to localhost (this is expected)")
|
||||
print("4. Copy the FULL URL from your browser's address bar")
|
||||
print(" (it will look like: https://localhost/?code=...)")
|
||||
|
||||
# Wait for user to paste the redirect URL
|
||||
redirect_response = input("\nPaste the full redirect URL here: ").strip()
|
||||
|
||||
# Fetch the short-lived token
|
||||
print("\nFetching short-lived access token...")
|
||||
token = self.fetch_token(redirect_response)
|
||||
short_lived_token = token["access_token"]
|
||||
|
||||
# Exchange for long-lived token
|
||||
if exchange_for_long_lived:
|
||||
print("Exchanging for long-lived token (60 days)...")
|
||||
long_lived_data = self.exchange_for_long_lived_token(short_lived_token)
|
||||
access_token = long_lived_data["access_token"]
|
||||
expires_in = long_lived_data["expires_in"]
|
||||
|
||||
print(f"\n✅ Long-lived token obtained!")
|
||||
print(f" Valid for: {expires_in / 86400:.0f} days (~{expires_in / 3600:.0f} hours)")
|
||||
else:
|
||||
access_token = short_lived_token
|
||||
print("\n✅ Short-lived token obtained (valid for ~1-2 hours)")
|
||||
|
||||
print("="*60)
|
||||
|
||||
# Save token with metadata
|
||||
self._offer_to_save_token_with_metadata(access_token)
|
||||
|
||||
return access_token
|
||||
|
||||
def _offer_to_save_token_with_metadata(self, access_token: str):
|
||||
"""Offer to save the access token with metadata to both .env and JSON file."""
|
||||
save = input("\nWould you like to save this token? (y/n): ").strip().lower()
|
||||
|
||||
if save == "y":
|
||||
# Get token info
|
||||
try:
|
||||
token_info = self.get_token_info(access_token)
|
||||
expires_at = token_info.get("expires_at", 0)
|
||||
is_valid = token_info.get("is_valid", False)
|
||||
issued_at = token_info.get("issued_at", int(time.time()))
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not get token info: {e}")
|
||||
expires_at = int(time.time()) + 5184000 # Assume 60 days
|
||||
is_valid = True
|
||||
issued_at = int(time.time())
|
||||
|
||||
# Save to .env
|
||||
env_path = Path(".env")
|
||||
if env_path.exists():
|
||||
env_content = env_path.read_text()
|
||||
else:
|
||||
env_content = ""
|
||||
|
||||
lines = env_content.split("\n")
|
||||
token_line = f"META_ACCESS_TOKEN={access_token}"
|
||||
updated = False
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
if line.startswith("META_ACCESS_TOKEN="):
|
||||
lines[i] = token_line
|
||||
updated = True
|
||||
break
|
||||
|
||||
if not updated:
|
||||
lines.append(token_line)
|
||||
|
||||
env_path.write_text("\n".join(lines))
|
||||
print(f"\n✅ Token saved to {env_path}")
|
||||
|
||||
# Save metadata to JSON for token refresh logic
|
||||
token_metadata = {
|
||||
"access_token": access_token,
|
||||
"expires_at": expires_at,
|
||||
"issued_at": issued_at,
|
||||
"is_valid": is_valid,
|
||||
"updated_at": int(time.time()),
|
||||
}
|
||||
|
||||
token_file = Path(".meta_token.json")
|
||||
token_file.write_text(json.dumps(token_metadata, indent=2))
|
||||
print(f"✅ Token metadata saved to {token_file}")
|
||||
|
||||
# Print expiry info
|
||||
if expires_at:
|
||||
expires_dt = datetime.fromtimestamp(expires_at)
|
||||
days_until_expiry = (expires_dt - datetime.now()).days
|
||||
print(f"\n📅 Token expires: {expires_dt.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
print(f" ({days_until_expiry} days from now)")
|
||||
else:
|
||||
print(f"\nAccess token: {access_token}")
|
||||
print("You can manually add it to .env as META_ACCESS_TOKEN")
|
||||
|
||||
|
||||
def main():
|
||||
"""Run interactive OAuth2 flow."""
|
||||
try:
|
||||
oauth = MetaOAuth2()
|
||||
access_token = oauth.interactive_auth()
|
||||
print(f"\nYou can now use this access token with the insights grabber.")
|
||||
|
||||
except ValueError as e:
|
||||
print(f"\nConfiguration error: {e}")
|
||||
print("\nPlease set META_APP_ID and META_APP_SECRET in .env")
|
||||
return 1
|
||||
except Exception as e:
|
||||
print(f"\nError: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit(main())
|
||||
@@ -85,16 +85,36 @@ class TimescaleDBClient:
|
||||
|
||||
# Execute schema statement by statement for better error handling
|
||||
async with self.pool.acquire() as conn:
|
||||
statements = [s.strip() for s in schema_sql.split(';') if s.strip()]
|
||||
# Set a reasonable timeout for schema operations (5 minutes per statement)
|
||||
await conn.execute("SET statement_timeout = '300s'")
|
||||
|
||||
# Split statements and track their line numbers in the original file
|
||||
statements = []
|
||||
current_line = 1
|
||||
for stmt in schema_sql.split(';'):
|
||||
stmt_stripped = stmt.strip()
|
||||
if stmt_stripped:
|
||||
# Count newlines before this statement to get starting line
|
||||
statements.append((stmt_stripped, current_line))
|
||||
# Update line counter (count newlines in the original chunk including ';')
|
||||
current_line += stmt.count('\n') + 1
|
||||
|
||||
errors = []
|
||||
compression_warnings = []
|
||||
|
||||
for i, stmt in enumerate(statements, 1):
|
||||
for i, (stmt, line_num) in enumerate(statements, 1):
|
||||
if not stmt:
|
||||
continue
|
||||
|
||||
# Show progress for potentially slow operations
|
||||
stmt_lower = stmt.lower()
|
||||
if any(keyword in stmt_lower for keyword in ['refresh materialized view', 'drop schema', 'create index']):
|
||||
print(f" Executing statement {i} (line {line_num})...")
|
||||
|
||||
try:
|
||||
await conn.execute(stmt)
|
||||
except asyncio.TimeoutError:
|
||||
errors.append((i, line_num, "Statement execution timed out (>5 minutes)"))
|
||||
except Exception as stmt_error:
|
||||
error_msg = str(stmt_error).lower()
|
||||
|
||||
@@ -104,19 +124,22 @@ class TimescaleDBClient:
|
||||
continue
|
||||
elif "columnstore not enabled" in error_msg:
|
||||
# Track compression warnings separately
|
||||
compression_warnings.append(i)
|
||||
compression_warnings.append((i, line_num))
|
||||
elif "'nonetype' object has no attribute 'decode'" in error_msg:
|
||||
# Silently ignore decode errors (usually comments/extensions)
|
||||
continue
|
||||
elif "canceling statement due to statement timeout" in error_msg:
|
||||
# Handle PostgreSQL timeout errors
|
||||
errors.append((i, line_num, "Statement execution timed out (>5 minutes)"))
|
||||
else:
|
||||
# Real errors
|
||||
errors.append((i, stmt_error))
|
||||
errors.append((i, line_num, stmt_error))
|
||||
|
||||
# Report results
|
||||
if errors:
|
||||
print(f"⚠️ {len(errors)} error(s) during schema initialization:")
|
||||
for stmt_num, error in errors:
|
||||
print(f" Statement {stmt_num}: {error}")
|
||||
for stmt_num, line_num, error in errors:
|
||||
print(f" Statement {stmt_num} (line {line_num}): {error}")
|
||||
|
||||
if compression_warnings:
|
||||
print("ℹ️ Note: Data compression not available (TimescaleDB columnstore not enabled)")
|
||||
@@ -310,7 +333,6 @@ class TimescaleDBClient:
|
||||
account_id: str,
|
||||
data: Dict[str, Any],
|
||||
date_preset: str = "today",
|
||||
cache_metadata: bool = True,
|
||||
):
|
||||
"""
|
||||
Insert campaign-level insights data.
|
||||
@@ -321,10 +343,9 @@ class TimescaleDBClient:
|
||||
account_id: Ad account ID
|
||||
data: Insights data dictionary from Meta API
|
||||
date_preset: Date preset used
|
||||
cache_metadata: If True, automatically cache campaign metadata from insights data
|
||||
"""
|
||||
# Cache campaign metadata if requested and available in the insights data
|
||||
if cache_metadata and data.get("campaign_name"):
|
||||
# Auto-cache campaign metadata if available in the insights data
|
||||
if data.get("campaign_name"):
|
||||
await self.upsert_campaign(
|
||||
campaign_id=campaign_id,
|
||||
account_id=account_id,
|
||||
@@ -389,7 +410,6 @@ class TimescaleDBClient:
|
||||
account_id: str,
|
||||
data: Dict[str, Any],
|
||||
date_preset: str = "today",
|
||||
cache_metadata: bool = True,
|
||||
):
|
||||
"""
|
||||
Insert ad set level insights data.
|
||||
@@ -401,21 +421,18 @@ class TimescaleDBClient:
|
||||
account_id: Ad account ID
|
||||
data: Insights data dictionary from Meta API
|
||||
date_preset: Date preset used
|
||||
cache_metadata: If True, automatically cache adset/campaign metadata from insights data
|
||||
"""
|
||||
# Cache metadata if requested and available in the insights data
|
||||
if cache_metadata:
|
||||
# Cache adset metadata if available
|
||||
# Note: Campaign should already exist from cache_campaigns_metadata or grab_campaign_insights
|
||||
# If it doesn't exist, the foreign key constraint will fail with a clear error
|
||||
# This is intentional - we should never silently create campaigns with 'Unknown' names
|
||||
if data.get("adset_name"):
|
||||
await self.upsert_adset(
|
||||
adset_id=adset_id,
|
||||
campaign_id=campaign_id,
|
||||
adset_name=data["adset_name"],
|
||||
status=None, # Not available in insights response
|
||||
)
|
||||
# Auto-cache adset metadata if available in the insights data
|
||||
# Note: Campaign should already exist from cache_campaigns_metadata or grab_campaign_insights
|
||||
# If it doesn't exist, the foreign key constraint will fail with a clear error
|
||||
# This is intentional - we should never silently create campaigns with 'Unknown' names
|
||||
if data.get("adset_name"):
|
||||
await self.upsert_adset(
|
||||
adset_id=adset_id,
|
||||
campaign_id=campaign_id,
|
||||
adset_name=data["adset_name"],
|
||||
status=None, # Not available in insights response
|
||||
)
|
||||
|
||||
query = """
|
||||
INSERT INTO adset_insights (
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,140 +0,0 @@
|
||||
"""
|
||||
Discover all ad accounts accessible via different methods:
|
||||
1. User ad accounts (personal access)
|
||||
2. Business Manager ad accounts (app-level access)
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from facebook_business.adobjects.user import User
|
||||
from facebook_business.adobjects.business import Business
|
||||
from facebook_business.api import FacebookAdsApi
|
||||
|
||||
|
||||
def discover_accounts():
|
||||
"""Discover ad accounts through multiple methods."""
|
||||
load_dotenv()
|
||||
|
||||
access_token = os.getenv("META_ACCESS_TOKEN")
|
||||
app_secret = os.getenv("META_APP_SECRET")
|
||||
app_id = os.getenv("META_APP_ID")
|
||||
|
||||
if not all([access_token, app_secret, app_id]):
|
||||
print("❌ Missing required environment variables")
|
||||
return 1
|
||||
|
||||
# Initialize Facebook Ads API
|
||||
FacebookAdsApi.init(
|
||||
app_id=app_id,
|
||||
app_secret=app_secret,
|
||||
access_token=access_token,
|
||||
)
|
||||
|
||||
print("="*70)
|
||||
print("AD ACCOUNT DISCOVERY")
|
||||
print("="*70)
|
||||
print()
|
||||
|
||||
# Method 1: User ad accounts (what we currently use)
|
||||
print("METHOD 1: User Ad Accounts (Personal Access)")
|
||||
print("-"*70)
|
||||
try:
|
||||
me = User(fbid='me')
|
||||
account_fields = ['name', 'account_id']
|
||||
user_accounts = me.get_ad_accounts(fields=account_fields)
|
||||
user_account_list = list(user_accounts)
|
||||
|
||||
print(f"Found {len(user_account_list)} user ad account(s):")
|
||||
for acc in user_account_list:
|
||||
print(f" - {acc.get('name')} ({acc.get('id')})")
|
||||
print()
|
||||
except Exception as e:
|
||||
print(f"❌ Error getting user accounts: {e}\n")
|
||||
user_account_list = []
|
||||
|
||||
# Method 2: Business Manager accounts
|
||||
print("METHOD 2: Business Manager Ad Accounts (App-Level Access)")
|
||||
print("-"*70)
|
||||
try:
|
||||
me = User(fbid='me')
|
||||
# Get businesses this user/app has access to
|
||||
businesses = me.get_businesses(fields=['id', 'name'])
|
||||
business_list = list(businesses)
|
||||
|
||||
if not business_list:
|
||||
print("No Business Managers found.")
|
||||
print()
|
||||
print("ℹ️ To access ad accounts at the app level, you need to:")
|
||||
print(" 1. Have a Meta Business Manager")
|
||||
print(" 2. Add your app to the Business Manager")
|
||||
print(" 3. Grant the app access to ad accounts")
|
||||
print()
|
||||
else:
|
||||
print(f"Found {len(business_list)} Business Manager(s):")
|
||||
all_business_accounts = []
|
||||
|
||||
for biz in business_list:
|
||||
biz_id = biz.get('id')
|
||||
biz_name = biz.get('name')
|
||||
print(f"\n Business: {biz_name} ({biz_id})")
|
||||
|
||||
try:
|
||||
business = Business(fbid=biz_id)
|
||||
# Get all ad accounts owned/managed by this business
|
||||
biz_accounts = business.get_owned_ad_accounts(
|
||||
fields=['id', 'name', 'account_status']
|
||||
)
|
||||
biz_account_list = list(biz_accounts)
|
||||
|
||||
print(f" Ad Accounts: {len(biz_account_list)}")
|
||||
for acc in biz_account_list:
|
||||
print(f" - {acc.get('name')} ({acc.get('id')})")
|
||||
all_business_accounts.append(acc)
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ Error accessing business accounts: {e}")
|
||||
|
||||
print()
|
||||
print(f"Total Business Manager ad accounts: {len(all_business_accounts)}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error getting businesses: {e}")
|
||||
print()
|
||||
|
||||
# Method 3: App-level access token (for reference)
|
||||
print()
|
||||
print("METHOD 3: App Access Token (NOT recommended for ad accounts)")
|
||||
print("-"*70)
|
||||
print("App access tokens can be generated with:")
|
||||
print(f" curl 'https://graph.facebook.com/oauth/access_token")
|
||||
print(f" ?client_id={app_id}")
|
||||
print(f" &client_secret=YOUR_SECRET")
|
||||
print(f" &grant_type=client_credentials'")
|
||||
print()
|
||||
print("⚠️ However, app access tokens CANNOT access ad accounts directly.")
|
||||
print(" The Marketing API requires user-level permissions for privacy/security.")
|
||||
print()
|
||||
|
||||
# Summary
|
||||
print("="*70)
|
||||
print("SUMMARY")
|
||||
print("="*70)
|
||||
print(f"User Ad Accounts: {len(user_account_list)}")
|
||||
print()
|
||||
print("💡 RECOMMENDATION:")
|
||||
print(" The current implementation using User.get_ad_accounts() is correct.")
|
||||
print(" To access MORE ad accounts, you have two options:")
|
||||
print()
|
||||
print(" Option 1: Use a System User token from Business Manager")
|
||||
print(" (grants access to all Business Manager ad accounts)")
|
||||
print()
|
||||
print(" Option 2: Have other users authorize your app")
|
||||
print(" (each user's token will see their ad accounts)")
|
||||
print()
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit(discover_accounts())
|
||||
228783
src/meta_api_grabber/geotargets-2025-10-29.csv
Normal file
228783
src/meta_api_grabber/geotargets-2025-10-29.csv
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,349 +0,0 @@
|
||||
"""
|
||||
Async script to grab ad insights data from Meta's Marketing API.
|
||||
Conservative rate limiting to avoid API limits.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from facebook_business.adobjects.adaccount import AdAccount
|
||||
from facebook_business.adobjects.adsinsights import AdsInsights
|
||||
from facebook_business.api import FacebookAdsApi
|
||||
|
||||
from .rate_limiter import MetaRateLimiter
|
||||
|
||||
|
||||
class MetaInsightsGrabber:
|
||||
"""
|
||||
Async grabber for Meta ad insights with intelligent rate limiting.
|
||||
|
||||
Features:
|
||||
- Monitors x-fb-ads-insights-throttle header
|
||||
- Auto-throttles when approaching rate limits (>75%)
|
||||
- Exponential backoff on rate limit errors
|
||||
- Automatic retries with progressive delays
|
||||
"""
|
||||
|
||||
def __init__(self, access_token: str = None):
|
||||
"""
|
||||
Initialize the grabber with credentials from environment.
|
||||
|
||||
Args:
|
||||
access_token: Optional access token. If not provided, will load from env.
|
||||
"""
|
||||
load_dotenv()
|
||||
|
||||
self.access_token = access_token or os.getenv("META_ACCESS_TOKEN")
|
||||
self.app_secret = os.getenv("META_APP_SECRET")
|
||||
self.app_id = os.getenv("META_APP_ID")
|
||||
self.ad_account_id = os.getenv("META_AD_ACCOUNT_ID")
|
||||
|
||||
if not self.access_token:
|
||||
raise ValueError(
|
||||
"Access token is required. Either:\n"
|
||||
"1. Set META_ACCESS_TOKEN in .env, or\n"
|
||||
"2. Run 'uv run python src/meta_api_grabber/auth.py' to get a token via OAuth2, or\n"
|
||||
"3. Pass access_token to the constructor"
|
||||
)
|
||||
|
||||
if not all([self.app_secret, self.app_id, self.ad_account_id]):
|
||||
raise ValueError(
|
||||
"Missing required environment variables (META_APP_SECRET, META_APP_ID, META_AD_ACCOUNT_ID). "
|
||||
"Please check your .env file against .env.example"
|
||||
)
|
||||
|
||||
# Initialize Facebook Ads API
|
||||
FacebookAdsApi.init(
|
||||
app_id=self.app_id,
|
||||
app_secret=self.app_secret,
|
||||
access_token=self.access_token,
|
||||
)
|
||||
|
||||
self.ad_account = AdAccount(self.ad_account_id)
|
||||
|
||||
# Rate limiter with backoff (Meta best practices)
|
||||
self.rate_limiter = MetaRateLimiter(
|
||||
base_delay=2.0,
|
||||
throttle_threshold=75.0,
|
||||
max_retry_delay=300.0,
|
||||
max_retries=5,
|
||||
)
|
||||
|
||||
async def _rate_limited_request(self, func, *args, **kwargs):
|
||||
"""
|
||||
Execute a request with intelligent rate limiting and backoff.
|
||||
|
||||
Monitors x-fb-ads-insights-throttle header and auto-throttles
|
||||
when usage exceeds 75%. Implements exponential backoff on errors.
|
||||
"""
|
||||
return await self.rate_limiter.execute_with_retry(func, *args, **kwargs)
|
||||
|
||||
async def get_account_insights(
|
||||
self,
|
||||
date_preset: str = "last_7d",
|
||||
fields: List[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get account-level insights for a given time period.
|
||||
|
||||
Args:
|
||||
date_preset: Time period (last_7d, last_14d, last_30d, etc.)
|
||||
fields: List of fields to retrieve. If None, uses default interesting fields.
|
||||
|
||||
Returns:
|
||||
Dictionary containing insights data
|
||||
"""
|
||||
if fields is None:
|
||||
# Default interesting fields - conservative selection
|
||||
fields = [
|
||||
AdsInsights.Field.impressions,
|
||||
AdsInsights.Field.clicks,
|
||||
AdsInsights.Field.spend,
|
||||
AdsInsights.Field.cpc,
|
||||
AdsInsights.Field.cpm,
|
||||
AdsInsights.Field.ctr,
|
||||
AdsInsights.Field.reach,
|
||||
AdsInsights.Field.frequency,
|
||||
AdsInsights.Field.actions,
|
||||
AdsInsights.Field.cost_per_action_type,
|
||||
]
|
||||
|
||||
params = {
|
||||
"date_preset": date_preset,
|
||||
"level": "account",
|
||||
}
|
||||
|
||||
print(f"Fetching account insights for {date_preset}...")
|
||||
insights = await self._rate_limited_request(
|
||||
self.ad_account.get_insights,
|
||||
fields=fields,
|
||||
params=params,
|
||||
)
|
||||
|
||||
# Convert to dict
|
||||
result = {
|
||||
"account_id": self.ad_account_id,
|
||||
"date_preset": date_preset,
|
||||
"insights": [dict(insight) for insight in insights],
|
||||
"fetched_at": datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
async def get_campaign_insights(
|
||||
self,
|
||||
date_preset: str = "last_7d",
|
||||
limit: int = 10,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get campaign-level insights.
|
||||
|
||||
Args:
|
||||
date_preset: Time period
|
||||
limit: Maximum number of campaigns to fetch (conservative default)
|
||||
|
||||
Returns:
|
||||
Dictionary containing campaign insights
|
||||
"""
|
||||
fields = [
|
||||
AdsInsights.Field.campaign_name,
|
||||
AdsInsights.Field.campaign_id,
|
||||
AdsInsights.Field.impressions,
|
||||
AdsInsights.Field.clicks,
|
||||
AdsInsights.Field.spend,
|
||||
AdsInsights.Field.ctr,
|
||||
AdsInsights.Field.cpc,
|
||||
]
|
||||
|
||||
params = {
|
||||
"date_preset": date_preset,
|
||||
"level": "campaign",
|
||||
"limit": limit,
|
||||
}
|
||||
|
||||
print(f"Fetching campaign insights for {date_preset} (limit: {limit})...")
|
||||
insights = await self._rate_limited_request(
|
||||
self.ad_account.get_insights,
|
||||
fields=fields,
|
||||
params=params,
|
||||
)
|
||||
|
||||
result = {
|
||||
"account_id": self.ad_account_id,
|
||||
"date_preset": date_preset,
|
||||
"level": "campaign",
|
||||
"campaigns": [dict(insight) for insight in insights],
|
||||
"fetched_at": datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
async def get_ad_set_insights(
|
||||
self,
|
||||
date_preset: str = "last_7d",
|
||||
limit: int = 10,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get ad set level insights.
|
||||
|
||||
Args:
|
||||
date_preset: Time period
|
||||
limit: Maximum number of ad sets to fetch
|
||||
|
||||
Returns:
|
||||
Dictionary containing ad set insights
|
||||
"""
|
||||
fields = [
|
||||
AdsInsights.Field.adset_name,
|
||||
AdsInsights.Field.adset_id,
|
||||
AdsInsights.Field.impressions,
|
||||
AdsInsights.Field.clicks,
|
||||
AdsInsights.Field.spend,
|
||||
AdsInsights.Field.ctr,
|
||||
AdsInsights.Field.cpm,
|
||||
]
|
||||
|
||||
params = {
|
||||
"date_preset": date_preset,
|
||||
"level": "adset",
|
||||
"limit": limit,
|
||||
}
|
||||
|
||||
print(f"Fetching ad set insights for {date_preset} (limit: {limit})...")
|
||||
insights = await self._rate_limited_request(
|
||||
self.ad_account.get_insights,
|
||||
fields=fields,
|
||||
params=params,
|
||||
)
|
||||
|
||||
result = {
|
||||
"account_id": self.ad_account_id,
|
||||
"date_preset": date_preset,
|
||||
"level": "adset",
|
||||
"ad_sets": [dict(insight) for insight in insights],
|
||||
"fetched_at": datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
async def grab_all_insights(self, date_preset: str = "last_7d") -> Dict[str, Any]:
|
||||
"""
|
||||
Grab all interesting insights (account, campaign, ad set level).
|
||||
|
||||
Args:
|
||||
date_preset: Time period to fetch
|
||||
|
||||
Returns:
|
||||
Dictionary containing all insights
|
||||
"""
|
||||
print(f"\nStarting conservative data grab for {date_preset}...")
|
||||
print(f"Rate limit: {self.request_delay}s between requests\n")
|
||||
|
||||
# Fetch sequentially to be conservative
|
||||
account_insights = await self.get_account_insights(date_preset)
|
||||
campaign_insights = await self.get_campaign_insights(date_preset, limit=10)
|
||||
ad_set_insights = await self.get_ad_set_insights(date_preset, limit=10)
|
||||
|
||||
result = {
|
||||
"account": account_insights,
|
||||
"campaigns": campaign_insights,
|
||||
"ad_sets": ad_set_insights,
|
||||
"summary": {
|
||||
"date_preset": date_preset,
|
||||
"total_requests": 3,
|
||||
"fetched_at": datetime.now().isoformat(),
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
async def save_insights_to_json(
|
||||
self,
|
||||
insights_data: Dict[str, Any],
|
||||
output_dir: str = "data",
|
||||
) -> Path:
|
||||
"""
|
||||
Save insights data to a JSON file.
|
||||
|
||||
Args:
|
||||
insights_data: The insights data to save
|
||||
output_dir: Directory to save the file
|
||||
|
||||
Returns:
|
||||
Path to the saved file
|
||||
"""
|
||||
# Create output directory if it doesn't exist
|
||||
output_path = Path(output_dir)
|
||||
output_path.mkdir(exist_ok=True)
|
||||
|
||||
# Generate filename with timestamp
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
filename = f"meta_insights_{timestamp}.json"
|
||||
filepath = output_path / filename
|
||||
|
||||
# Save to JSON with pretty formatting
|
||||
loop = asyncio.get_event_loop()
|
||||
await loop.run_in_executor(
|
||||
None,
|
||||
lambda: filepath.write_text(
|
||||
json.dumps(insights_data, indent=2, default=str)
|
||||
)
|
||||
)
|
||||
|
||||
print(f"\nData saved to: {filepath}")
|
||||
print(f"File size: {filepath.stat().st_size / 1024:.2f} KB")
|
||||
|
||||
return filepath
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main entry point for the insights grabber."""
|
||||
try:
|
||||
grabber = MetaInsightsGrabber()
|
||||
|
||||
# Grab insights for the last 7 days
|
||||
insights = await grabber.grab_all_insights(date_preset=AdsInsights.DatePreset.last_7d)
|
||||
|
||||
# Save to JSON
|
||||
filepath = await grabber.save_insights_to_json(insights)
|
||||
|
||||
# Print summary
|
||||
print("\n" + "="*50)
|
||||
print("SUMMARY")
|
||||
print("="*50)
|
||||
print(f"Account ID: {grabber.ad_account_id}")
|
||||
print(f"Date Range: last_7d")
|
||||
print(f"Output File: {filepath}")
|
||||
|
||||
if insights["account"]["insights"]:
|
||||
account_data = insights["account"]["insights"][0]
|
||||
print(f"\nAccount Metrics:")
|
||||
print(f" Impressions: {account_data.get('impressions', 'N/A')}")
|
||||
print(f" Clicks: {account_data.get('clicks', 'N/A')}")
|
||||
print(f" Spend: ${account_data.get('spend', 'N/A')}")
|
||||
print(f" CTR: {account_data.get('ctr', 'N/A')}%")
|
||||
|
||||
print(f"\nCampaigns fetched: {len(insights['campaigns']['campaigns'])}")
|
||||
print(f"Ad Sets fetched: {len(insights['ad_sets']['ad_sets'])}")
|
||||
print("\n" + "="*50)
|
||||
|
||||
except ValueError as e:
|
||||
print(f"Configuration error: {e}")
|
||||
return 1
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit_code = asyncio.run(main())
|
||||
exit(exit_code)
|
||||
@@ -1,6 +0,0 @@
|
||||
def main():
|
||||
print("Hello from meta-api-grabber!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,657 +0,0 @@
|
||||
"""
|
||||
Rate limiting and backoff mechanism for Meta Marketing API.
|
||||
|
||||
Based on Meta's best practices:
|
||||
https://developers.facebook.com/docs/marketing-api/insights/best-practices/
|
||||
https://developers.facebook.com/docs/graph-api/overview/rate-limiting
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from typing import Any, Callable, Dict, List, Optional
|
||||
|
||||
from facebook_business.api import FacebookAdsApi
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
class MetaRateLimiter:
|
||||
"""
|
||||
Rate limiter with exponential backoff for Meta Marketing API.
|
||||
|
||||
Features:
|
||||
- Monitors X-App-Usage header (platform rate limits)
|
||||
- Monitors X-Ad-Account-Usage header (ad account specific)
|
||||
- Monitors X-Business-Use-Case-Usage header (business use case limits)
|
||||
- Monitors x-fb-ads-insights-throttle header (legacy)
|
||||
- Automatic throttling when usage > 75%
|
||||
- Exponential backoff on rate limit errors
|
||||
- Uses reset_time_duration and estimated_time_to_regain_access
|
||||
- Configurable thresholds
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
base_delay: float = 2.0,
|
||||
throttle_threshold: float = 75.0,
|
||||
max_retry_delay: float = 300.0, # 5 minutes
|
||||
max_retries: int = 5,
|
||||
):
|
||||
"""
|
||||
Initialize rate limiter.
|
||||
|
||||
Args:
|
||||
base_delay: Base delay between requests in seconds
|
||||
throttle_threshold: Throttle when usage exceeds this % (0-100)
|
||||
max_retry_delay: Maximum delay for exponential backoff
|
||||
max_retries: Maximum number of retries on rate limit errors
|
||||
"""
|
||||
self.base_delay = base_delay
|
||||
self.throttle_threshold = throttle_threshold
|
||||
self.max_retry_delay = max_retry_delay
|
||||
self.max_retries = max_retries
|
||||
|
||||
# Track current usage percentages from different headers
|
||||
# X-App-Usage (platform rate limits)
|
||||
self.app_call_count: float = 0.0
|
||||
self.app_total_cputime: float = 0.0
|
||||
self.app_total_time: float = 0.0
|
||||
|
||||
# X-Ad-Account-Usage (ad account specific) - tracked per account
|
||||
# Key: account_id (e.g., "act_123456789"), Value: dict with metrics
|
||||
self.ad_account_usage: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
# X-Business-Use-Case-Usage (business use case limits)
|
||||
self.buc_usage: List[Dict[str, Any]] = []
|
||||
self.estimated_time_to_regain_access: int = 0 # minutes
|
||||
|
||||
# Legacy x-fb-ads-insights-throttle
|
||||
self.legacy_app_usage_pct: float = 0.0
|
||||
self.legacy_account_usage_pct: float = 0.0
|
||||
|
||||
self.last_check_time: float = time.time()
|
||||
|
||||
# Stats
|
||||
self.total_requests: int = 0
|
||||
self.throttled_requests: int = 0
|
||||
self.rate_limit_errors: int = 0
|
||||
|
||||
def _get_headers(self, response: Any) -> Optional[Dict[str, str]]:
|
||||
"""
|
||||
Extract headers from various response object types.
|
||||
|
||||
Args:
|
||||
response: API response object
|
||||
|
||||
Returns:
|
||||
Dictionary of headers or None
|
||||
"""
|
||||
# Facebook SDK response object
|
||||
if hasattr(response, '_headers'):
|
||||
return response._headers
|
||||
elif hasattr(response, 'headers'):
|
||||
return response.headers
|
||||
elif hasattr(response, '_api_response'):
|
||||
return getattr(response._api_response, 'headers', None)
|
||||
return None
|
||||
|
||||
def parse_x_app_usage(self, response: Any) -> Dict[str, float]:
|
||||
"""
|
||||
Parse X-App-Usage header (Platform rate limits).
|
||||
|
||||
Header format: {"call_count": 28, "total_time": 25, "total_cputime": 25}
|
||||
|
||||
Args:
|
||||
response: API response object
|
||||
|
||||
Returns:
|
||||
Dictionary with call_count, total_time, total_cputime
|
||||
"""
|
||||
try:
|
||||
headers = self._get_headers(response)
|
||||
if headers:
|
||||
header_value = headers.get('x-app-usage') or headers.get('X-App-Usage', '')
|
||||
if header_value:
|
||||
logger.debug(f"X-App-Usage header: {header_value}")
|
||||
data = json.loads(header_value)
|
||||
result = {
|
||||
'call_count': float(data.get('call_count', 0)),
|
||||
'total_time': float(data.get('total_time', 0)),
|
||||
'total_cputime': float(data.get('total_cputime', 0)),
|
||||
}
|
||||
logger.debug(f"Parsed X-App-Usage: {result}")
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to parse X-App-Usage header: {e}")
|
||||
return {'call_count': 0.0, 'total_time': 0.0, 'total_cputime': 0.0}
|
||||
|
||||
def parse_x_ad_account_usage(self, response: Any) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Parse X-Ad-Account-Usage header (Ad account specific limits).
|
||||
|
||||
Header format: {
|
||||
"acc_id_util_pct": 9.67,
|
||||
"reset_time_duration": 100,
|
||||
"ads_api_access_tier": "standard_access"
|
||||
}
|
||||
|
||||
Args:
|
||||
response: API response object
|
||||
|
||||
Returns:
|
||||
Dictionary with metrics, or None if header not present.
|
||||
To determine account_id, check response object or URL.
|
||||
"""
|
||||
try:
|
||||
headers = self._get_headers(response)
|
||||
if headers:
|
||||
header_value = headers.get('x-ad-account-usage') or headers.get('X-Ad-Account-Usage', '')
|
||||
if header_value:
|
||||
logger.debug(f"X-Ad-Account-Usage header: {header_value}")
|
||||
data = json.loads(header_value)
|
||||
result = {
|
||||
'acc_id_util_pct': float(data.get('acc_id_util_pct', 0)),
|
||||
'reset_time_duration': int(data.get('reset_time_duration', 0)),
|
||||
'ads_api_access_tier': data.get('ads_api_access_tier'),
|
||||
}
|
||||
logger.debug(f"Parsed X-Ad-Account-Usage: {result}")
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to parse X-Ad-Account-Usage header: {e}")
|
||||
return None
|
||||
|
||||
def _extract_account_id(self, response: Any) -> Optional[str]:
|
||||
"""
|
||||
Extract account ID from response object.
|
||||
|
||||
Tries multiple methods to find the account ID from the response.
|
||||
|
||||
Args:
|
||||
response: API response object
|
||||
|
||||
Returns:
|
||||
Account ID string (e.g., "act_123456789") or None
|
||||
"""
|
||||
# Try to get account_id from response attributes
|
||||
if hasattr(response, 'account_id'):
|
||||
return response.account_id
|
||||
if hasattr(response, '_data') and isinstance(response._data, dict):
|
||||
return response._data.get('account_id')
|
||||
|
||||
# Try to get from parent object
|
||||
if hasattr(response, '_parent_object'):
|
||||
parent = response._parent_object
|
||||
if hasattr(parent, 'get_id'):
|
||||
return parent.get_id()
|
||||
if hasattr(parent, '_data') and isinstance(parent._data, dict):
|
||||
return parent._data.get('account_id') or parent._data.get('id')
|
||||
|
||||
# Try to get from API context
|
||||
if hasattr(response, '_api_context'):
|
||||
context = response._api_context
|
||||
if hasattr(context, 'account_id'):
|
||||
return context.account_id
|
||||
|
||||
return None
|
||||
|
||||
def parse_x_business_use_case_usage(self, response: Any) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Parse X-Business-Use-Case-Usage header (Business use case limits).
|
||||
|
||||
Header format: {
|
||||
"business-id": [{
|
||||
"type": "ads_management",
|
||||
"call_count": 95,
|
||||
"total_cputime": 20,
|
||||
"total_time": 20,
|
||||
"estimated_time_to_regain_access": 0,
|
||||
"ads_api_access_tier": "development_access"
|
||||
}],
|
||||
...
|
||||
}
|
||||
|
||||
Args:
|
||||
response: API response object
|
||||
|
||||
Returns:
|
||||
List of usage dictionaries for each business use case
|
||||
"""
|
||||
try:
|
||||
headers = self._get_headers(response)
|
||||
if headers:
|
||||
header_value = headers.get('x-business-use-case-usage') or headers.get('X-Business-Use-Case-Usage', '')
|
||||
if header_value:
|
||||
logger.debug(f"X-Business-Use-Case-Usage header: {header_value}")
|
||||
data = json.loads(header_value)
|
||||
# Flatten the nested structure
|
||||
all_usage = []
|
||||
for business_id, use_cases in data.items():
|
||||
if isinstance(use_cases, list):
|
||||
for use_case in use_cases:
|
||||
use_case['business_id'] = business_id
|
||||
all_usage.append(use_case)
|
||||
logger.debug(f"Parsed X-Business-Use-Case-Usage: {len(all_usage)} use cases")
|
||||
return all_usage
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to parse X-Business-Use-Case-Usage header: {e}")
|
||||
return []
|
||||
|
||||
def parse_throttle_header(self, response: Any) -> Dict[str, float]:
|
||||
"""
|
||||
Parse x-fb-ads-insights-throttle header from response (legacy).
|
||||
|
||||
Header format: {"app_id_util_pct": 25.5, "acc_id_util_pct": 10.0}
|
||||
|
||||
Args:
|
||||
response: API response object
|
||||
|
||||
Returns:
|
||||
Dictionary with app_id_util_pct and acc_id_util_pct
|
||||
"""
|
||||
try:
|
||||
headers = self._get_headers(response)
|
||||
if headers:
|
||||
throttle_header = headers.get('x-fb-ads-insights-throttle', '')
|
||||
if throttle_header:
|
||||
logger.debug(f"x-fb-ads-insights-throttle header: {throttle_header}")
|
||||
throttle_data = json.loads(throttle_header)
|
||||
result = {
|
||||
'app_id_util_pct': float(throttle_data.get('app_id_util_pct', 0)),
|
||||
'acc_id_util_pct': float(throttle_data.get('acc_id_util_pct', 0)),
|
||||
}
|
||||
logger.debug(f"Parsed x-fb-ads-insights-throttle: {result}")
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to parse x-fb-ads-insights-throttle header: {e}")
|
||||
return {'app_id_util_pct': 0.0, 'acc_id_util_pct': 0.0}
|
||||
|
||||
def update_usage(self, response: Any, account_id: Optional[str] = None):
|
||||
"""
|
||||
Update usage statistics from all API response headers.
|
||||
|
||||
Parses and updates metrics from:
|
||||
- X-App-Usage
|
||||
- X-Ad-Account-Usage (per account)
|
||||
- X-Business-Use-Case-Usage
|
||||
- x-fb-ads-insights-throttle (legacy)
|
||||
|
||||
Args:
|
||||
response: API response object
|
||||
account_id: Optional account ID (e.g., "act_123456789").
|
||||
If not provided, will attempt to extract from response.
|
||||
"""
|
||||
# Parse all headers
|
||||
app_usage = self.parse_x_app_usage(response)
|
||||
ad_account_usage = self.parse_x_ad_account_usage(response)
|
||||
buc_usage = self.parse_x_business_use_case_usage(response)
|
||||
legacy_throttle = self.parse_throttle_header(response)
|
||||
|
||||
# Update X-App-Usage metrics
|
||||
self.app_call_count = app_usage['call_count']
|
||||
self.app_total_cputime = app_usage['total_cputime']
|
||||
self.app_total_time = app_usage['total_time']
|
||||
|
||||
# Update X-Ad-Account-Usage metrics (per account)
|
||||
if ad_account_usage:
|
||||
# Try to get account_id
|
||||
if not account_id:
|
||||
account_id = self._extract_account_id(response)
|
||||
|
||||
# Use 'unknown' as fallback if we can't determine account
|
||||
if not account_id:
|
||||
account_id = 'unknown'
|
||||
logger.debug("Could not determine account_id, using 'unknown'")
|
||||
|
||||
# Store usage for this account
|
||||
self.ad_account_usage[account_id] = ad_account_usage
|
||||
logger.debug(f"Updated ad account usage for {account_id}")
|
||||
|
||||
# Update X-Business-Use-Case-Usage metrics
|
||||
self.buc_usage = buc_usage
|
||||
# Find the maximum estimated_time_to_regain_access across all use cases
|
||||
if buc_usage:
|
||||
self.estimated_time_to_regain_access = max(
|
||||
(uc.get('estimated_time_to_regain_access', 0) for uc in buc_usage),
|
||||
default=0
|
||||
)
|
||||
|
||||
# Update legacy metrics
|
||||
self.legacy_app_usage_pct = legacy_throttle['app_id_util_pct']
|
||||
self.legacy_account_usage_pct = legacy_throttle['acc_id_util_pct']
|
||||
|
||||
self.last_check_time = time.time()
|
||||
|
||||
# Log warnings if approaching limits
|
||||
self._log_rate_limit_warnings()
|
||||
|
||||
def _log_rate_limit_warnings(self):
|
||||
"""Log warnings if any rate limit metric is approaching threshold."""
|
||||
warnings = []
|
||||
|
||||
# Check X-App-Usage metrics
|
||||
if self.app_call_count > self.throttle_threshold:
|
||||
warnings.append(f"App call count: {self.app_call_count:.1f}%")
|
||||
if self.app_total_cputime > self.throttle_threshold:
|
||||
warnings.append(f"App CPU time: {self.app_total_cputime:.1f}%")
|
||||
if self.app_total_time > self.throttle_threshold:
|
||||
warnings.append(f"App total time: {self.app_total_time:.1f}%")
|
||||
|
||||
# Check X-Ad-Account-Usage (per account)
|
||||
for account_id, usage in self.ad_account_usage.items():
|
||||
acc_pct = usage.get('acc_id_util_pct', 0)
|
||||
if acc_pct > self.throttle_threshold:
|
||||
warnings.append(f"Account {account_id}: {acc_pct:.1f}%")
|
||||
reset_time = usage.get('reset_time_duration', 0)
|
||||
if reset_time > 0:
|
||||
warnings.append(f"Resets in {reset_time}s")
|
||||
|
||||
# Check X-Business-Use-Case-Usage
|
||||
for buc in self.buc_usage:
|
||||
buc_type = buc.get('type', 'unknown')
|
||||
call_count = buc.get('call_count', 0)
|
||||
if call_count > self.throttle_threshold:
|
||||
warnings.append(f"BUC {buc_type}: {call_count:.1f}%")
|
||||
eta = buc.get('estimated_time_to_regain_access', 0)
|
||||
if eta > 0:
|
||||
warnings.append(f"Regain access in {eta} min")
|
||||
|
||||
# Check legacy metrics
|
||||
if self.legacy_app_usage_pct > self.throttle_threshold:
|
||||
warnings.append(f"Legacy app: {self.legacy_app_usage_pct:.1f}%")
|
||||
if self.legacy_account_usage_pct > self.throttle_threshold:
|
||||
warnings.append(f"Legacy account: {self.legacy_account_usage_pct:.1f}%")
|
||||
|
||||
if warnings:
|
||||
logger.warning(f"⚠️ Rate limit warning: {', '.join(warnings)}")
|
||||
|
||||
def get_max_usage_pct(self) -> float:
|
||||
"""
|
||||
Get the maximum usage percentage across all rate limit metrics.
|
||||
|
||||
Returns:
|
||||
Maximum usage percentage (0-100)
|
||||
"""
|
||||
usage_values = [
|
||||
self.app_call_count,
|
||||
self.app_total_cputime,
|
||||
self.app_total_time,
|
||||
self.legacy_app_usage_pct,
|
||||
self.legacy_account_usage_pct,
|
||||
]
|
||||
|
||||
# Add ad account usage percentages (per account)
|
||||
for usage in self.ad_account_usage.values():
|
||||
usage_values.append(usage.get('acc_id_util_pct', 0))
|
||||
|
||||
# Add BUC usage percentages
|
||||
for buc in self.buc_usage:
|
||||
usage_values.extend([
|
||||
buc.get('call_count', 0),
|
||||
buc.get('total_cputime', 0),
|
||||
buc.get('total_time', 0),
|
||||
])
|
||||
|
||||
return max(usage_values) if usage_values else 0.0
|
||||
|
||||
def should_throttle(self) -> bool:
|
||||
"""
|
||||
Check if we should throttle based on current usage.
|
||||
|
||||
Returns:
|
||||
True if usage exceeds threshold
|
||||
"""
|
||||
return self.get_max_usage_pct() > self.throttle_threshold
|
||||
|
||||
def get_throttle_delay(self) -> float:
|
||||
"""
|
||||
Calculate delay based on current usage and reset times.
|
||||
|
||||
Uses estimated_time_to_regain_access and reset_time_duration when available.
|
||||
|
||||
Returns:
|
||||
Delay in seconds
|
||||
"""
|
||||
max_usage = self.get_max_usage_pct()
|
||||
|
||||
if max_usage < self.throttle_threshold:
|
||||
return self.base_delay
|
||||
|
||||
# If we have estimated_time_to_regain_access from BUC header, use it
|
||||
if self.estimated_time_to_regain_access > 0:
|
||||
# Convert minutes to seconds and use as delay
|
||||
delay = self.estimated_time_to_regain_access * 60
|
||||
logger.info(f"Using BUC estimated_time_to_regain_access: {self.estimated_time_to_regain_access} min ({delay}s)")
|
||||
return min(delay, self.max_retry_delay)
|
||||
|
||||
# Check if any ad account has reset_time_duration and high usage
|
||||
for account_id, usage in self.ad_account_usage.items():
|
||||
acc_pct = usage.get('acc_id_util_pct', 0)
|
||||
reset_time = usage.get('reset_time_duration', 0)
|
||||
if reset_time > 0 and acc_pct >= 90:
|
||||
# Use a fraction of reset_time_duration as delay
|
||||
delay = min(reset_time * 0.5, self.max_retry_delay)
|
||||
logger.info(f"Using Ad Account {account_id} reset_time_duration: {reset_time}s (delay: {delay}s)")
|
||||
return delay
|
||||
|
||||
# Progressive delay based on usage
|
||||
# 75% = base_delay, 90% = 2x, 95% = 5x, 99% = 10x
|
||||
if max_usage >= 95:
|
||||
multiplier = 10.0
|
||||
elif max_usage >= 90:
|
||||
multiplier = 5.0
|
||||
elif max_usage >= 85:
|
||||
multiplier = 3.0
|
||||
else: # 75-85%
|
||||
multiplier = 2.0
|
||||
|
||||
delay = self.base_delay * multiplier
|
||||
return min(delay, self.max_retry_delay)
|
||||
|
||||
async def wait_with_backoff(self, retry_count: int = 0):
|
||||
"""
|
||||
Wait with exponential backoff.
|
||||
|
||||
Args:
|
||||
retry_count: Current retry attempt (0-indexed)
|
||||
"""
|
||||
if retry_count == 0:
|
||||
# Normal delay based on throttle
|
||||
delay = self.get_throttle_delay()
|
||||
else:
|
||||
# Exponential backoff: 2^retry * base_delay
|
||||
delay = min(
|
||||
(2 ** retry_count) * self.base_delay,
|
||||
self.max_retry_delay
|
||||
)
|
||||
|
||||
if delay > self.base_delay:
|
||||
self.throttled_requests += 1
|
||||
max_usage = self.get_max_usage_pct()
|
||||
logger.info(f"⏸️ Throttling for {delay:.1f}s (max usage: {max_usage:.1f}%)")
|
||||
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
async def execute_with_retry(
|
||||
self,
|
||||
func: Callable,
|
||||
*args,
|
||||
**kwargs
|
||||
) -> Any:
|
||||
"""
|
||||
Execute API call with automatic retry and backoff.
|
||||
|
||||
Args:
|
||||
func: Function to execute (blocking, will be run in executor)
|
||||
*args: Positional arguments for func
|
||||
**kwargs: Keyword arguments for func
|
||||
|
||||
Returns:
|
||||
Result from func
|
||||
|
||||
Raises:
|
||||
Exception: If all retries exhausted
|
||||
"""
|
||||
self.total_requests += 1
|
||||
|
||||
for retry in range(self.max_retries):
|
||||
try:
|
||||
# Wait before request (with potential throttling)
|
||||
await self.wait_with_backoff(retry_count=retry if retry > 0 else 0)
|
||||
|
||||
# Execute request in thread pool
|
||||
loop = asyncio.get_event_loop()
|
||||
result = await loop.run_in_executor(None, lambda: func(*args, **kwargs))
|
||||
|
||||
# Update usage from response
|
||||
self.update_usage(result)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
error_message = str(e).lower()
|
||||
|
||||
# Check if it's a rate limit error (expanded list based on Meta docs)
|
||||
is_rate_limit = (
|
||||
'rate limit' in error_message or
|
||||
'too many requests' in error_message or
|
||||
'throttle' in error_message or
|
||||
'error code 4' in error_message or # App rate limit
|
||||
'error code 17' in error_message or # User rate limit
|
||||
'error code 32' in error_message or # Pages rate limit
|
||||
'error code 613' in error_message or # Custom rate limit
|
||||
'error code 80000' in error_message or # Ads Insights BUC
|
||||
'error code 80001' in error_message or # Pages BUC
|
||||
'error code 80002' in error_message or # Instagram BUC
|
||||
'error code 80003' in error_message or # Custom Audience BUC
|
||||
'error code 80004' in error_message or # Ads Management BUC
|
||||
'error code 80005' in error_message or # LeadGen BUC
|
||||
'error code 80006' in error_message or # Messenger BUC
|
||||
'error code 80008' in error_message or # WhatsApp BUC
|
||||
'error code 80009' in error_message or # Catalog Management BUC
|
||||
'error code 80014' in error_message # Catalog Batch BUC
|
||||
)
|
||||
|
||||
if is_rate_limit:
|
||||
self.rate_limit_errors += 1
|
||||
|
||||
if retry < self.max_retries - 1:
|
||||
backoff_delay = min(
|
||||
(2 ** (retry + 1)) * self.base_delay,
|
||||
self.max_retry_delay
|
||||
)
|
||||
logger.warning(f"🔄 Rate limit hit! Retrying in {backoff_delay:.1f}s (attempt {retry + 1}/{self.max_retries})")
|
||||
logger.warning(f" Error: {e}")
|
||||
await asyncio.sleep(backoff_delay)
|
||||
continue
|
||||
else:
|
||||
logger.error(f"❌ Rate limit error - max retries exhausted: {e}")
|
||||
raise
|
||||
|
||||
# Not a rate limit error, re-raise immediately
|
||||
raise
|
||||
|
||||
# Should never reach here
|
||||
raise Exception("Max retries exhausted")
|
||||
|
||||
def get_stats(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get current rate limiter statistics.
|
||||
|
||||
Returns:
|
||||
Dictionary with comprehensive stats from all headers
|
||||
"""
|
||||
return {
|
||||
# Request stats
|
||||
'total_requests': self.total_requests,
|
||||
'throttled_requests': self.throttled_requests,
|
||||
'rate_limit_errors': self.rate_limit_errors,
|
||||
|
||||
# X-App-Usage metrics
|
||||
'app_call_count': self.app_call_count,
|
||||
'app_total_cputime': self.app_total_cputime,
|
||||
'app_total_time': self.app_total_time,
|
||||
|
||||
# X-Ad-Account-Usage metrics (per account)
|
||||
'ad_account_usage': self.ad_account_usage,
|
||||
|
||||
# X-Business-Use-Case-Usage metrics
|
||||
'buc_usage': self.buc_usage,
|
||||
'estimated_time_to_regain_access': self.estimated_time_to_regain_access,
|
||||
|
||||
# Legacy metrics
|
||||
'legacy_app_usage_pct': self.legacy_app_usage_pct,
|
||||
'legacy_account_usage_pct': self.legacy_account_usage_pct,
|
||||
|
||||
# Computed metrics
|
||||
'max_usage_pct': self.get_max_usage_pct(),
|
||||
'is_throttling': self.should_throttle(),
|
||||
}
|
||||
|
||||
def print_stats(self):
|
||||
"""Print current statistics with all rate limit metrics."""
|
||||
stats = self.get_stats()
|
||||
|
||||
output = []
|
||||
output.append("\n" + "="*70)
|
||||
output.append("RATE LIMITER STATISTICS")
|
||||
output.append("="*70)
|
||||
|
||||
# Request stats
|
||||
output.append(f"Total Requests: {stats['total_requests']}")
|
||||
output.append(f"Throttled Requests: {stats['throttled_requests']}")
|
||||
output.append(f"Rate Limit Errors: {stats['rate_limit_errors']}")
|
||||
output.append("")
|
||||
|
||||
# X-App-Usage
|
||||
output.append("X-App-Usage (Platform Rate Limits):")
|
||||
output.append(f" Call Count: {stats['app_call_count']:.1f}%")
|
||||
output.append(f" Total CPU Time: {stats['app_total_cputime']:.1f}%")
|
||||
output.append(f" Total Time: {stats['app_total_time']:.1f}%")
|
||||
output.append("")
|
||||
|
||||
# X-Ad-Account-Usage (per account)
|
||||
if stats['ad_account_usage']:
|
||||
# Only show accounts with data (skip "unknown" accounts with 0 usage)
|
||||
accounts_to_show = {
|
||||
account_id: usage
|
||||
for account_id, usage in stats['ad_account_usage'].items()
|
||||
if account_id != 'unknown' or usage.get('acc_id_util_pct', 0) > 0
|
||||
}
|
||||
|
||||
if accounts_to_show:
|
||||
output.append("X-Ad-Account-Usage (Per Account):")
|
||||
for account_id, usage in accounts_to_show.items():
|
||||
output.append(f" Account: {account_id}")
|
||||
output.append(f" Usage: {usage.get('acc_id_util_pct', 0):.1f}%")
|
||||
output.append(f" Reset Time: {usage.get('reset_time_duration', 0)}s")
|
||||
output.append(f" API Access Tier: {usage.get('ads_api_access_tier') or 'N/A'}")
|
||||
output.append("")
|
||||
|
||||
# X-Business-Use-Case-Usage
|
||||
if stats['buc_usage']:
|
||||
output.append("X-Business-Use-Case-Usage:")
|
||||
for buc in stats['buc_usage']:
|
||||
output.append(f" Type: {buc.get('type', 'unknown')}")
|
||||
output.append(f" Call Count: {buc.get('call_count', 0):.1f}%")
|
||||
output.append(f" Total CPU Time: {buc.get('total_cputime', 0):.1f}%")
|
||||
output.append(f" Total Time: {buc.get('total_time', 0):.1f}%")
|
||||
output.append(f" Est. Time to Regain: {buc.get('estimated_time_to_regain_access', 0)} min")
|
||||
output.append("")
|
||||
|
||||
# Legacy metrics
|
||||
output.append("Legacy (x-fb-ads-insights-throttle):")
|
||||
output.append(f" App Usage: {stats['legacy_app_usage_pct']:.1f}%")
|
||||
output.append(f" Account Usage: {stats['legacy_account_usage_pct']:.1f}%")
|
||||
output.append("")
|
||||
|
||||
# Summary
|
||||
output.append(f"Max Usage Across All Metrics: {stats['max_usage_pct']:.1f}%")
|
||||
output.append(f"Currently Throttled: {stats['is_throttling']}")
|
||||
output.append("="*70 + "\n")
|
||||
|
||||
logger.info("\n".join(output))
|
||||
File diff suppressed because it is too large
Load Diff
380
src/meta_api_grabber/setup_and_wait.py
Normal file
380
src/meta_api_grabber/setup_and_wait.py
Normal file
@@ -0,0 +1,380 @@
|
||||
"""
|
||||
View Manager Setup and Daemon
|
||||
|
||||
This script:
|
||||
1. Initializes database schema from db_schema.sql (drops and recreates public schema with all views)
|
||||
2. Loads metadata from metadata.yaml into the database
|
||||
3. Auto-detects materialized views from db_schema.sql
|
||||
4. Periodically refreshes materialized views (configurable interval)
|
||||
|
||||
Usage:
|
||||
uv run view-manager-setup
|
||||
|
||||
Environment variables:
|
||||
REFRESH_INTERVAL_MINUTES: How often to refresh materialized views (default: 60)
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import csv
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
import yaml
|
||||
|
||||
from meta_api_grabber.database import TimescaleDBClient
|
||||
|
||||
# Set up logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ViewManagerSetup:
|
||||
"""Manages database schema setup, metadata loading, and view refresh."""
|
||||
|
||||
def __init__(self, refresh_interval_minutes: int = 60):
|
||||
self.db = TimescaleDBClient()
|
||||
self.shutdown_event = asyncio.Event()
|
||||
self.refresh_interval_minutes = refresh_interval_minutes
|
||||
self.materialized_views: List[str] = []
|
||||
|
||||
async def load_metadata_from_yaml(self, yaml_path: Path):
|
||||
"""
|
||||
Load account metadata from YAML file and sync to database.
|
||||
|
||||
Args:
|
||||
yaml_path: Path to metadata.yaml file
|
||||
"""
|
||||
if not yaml_path.exists():
|
||||
logger.warning(f"Metadata file not found: {yaml_path}")
|
||||
logger.info("Skipping metadata loading. Create metadata.yaml to configure accounts.")
|
||||
return
|
||||
|
||||
logger.info(f"Loading metadata from {yaml_path}")
|
||||
|
||||
with open(yaml_path, 'r') as f:
|
||||
data = yaml.safe_load(f)
|
||||
|
||||
accounts = data.get('accounts', [])
|
||||
if not accounts:
|
||||
logger.warning("No accounts found in metadata.yaml")
|
||||
return
|
||||
|
||||
# Sync accounts to database using upsert logic
|
||||
async with self.db.pool.acquire() as conn:
|
||||
# Clear existing metadata (or implement smarter sync logic)
|
||||
await conn.execute("DELETE FROM public.account_metadata")
|
||||
|
||||
for account in accounts:
|
||||
label = account.get('label')
|
||||
meta_account_id = account.get('meta_account_id')
|
||||
google_account_id = account.get('google_account_id')
|
||||
alpinebits_hotel_code = account.get('alpinebits_hotel_code')
|
||||
|
||||
if not label:
|
||||
logger.warning(f"Skipping account without label: {account}")
|
||||
continue
|
||||
|
||||
await conn.execute(
|
||||
"""
|
||||
INSERT INTO public.account_metadata
|
||||
(label, meta_account_id, google_account_id, alpinebits_hotel_code, updated_at)
|
||||
VALUES ($1, $2, $3, $4, NOW())
|
||||
""",
|
||||
label, meta_account_id, google_account_id, alpinebits_hotel_code
|
||||
)
|
||||
|
||||
logger.info(f"Loaded metadata for: {label}")
|
||||
|
||||
logger.info(f"Successfully loaded {len(accounts)} accounts from metadata.yaml")
|
||||
|
||||
async def load_geotargets_from_csv(self, csv_path: Path):
|
||||
"""
|
||||
Load geotargets data from CSV file and sync to database.
|
||||
|
||||
This function is idempotent - it will:
|
||||
1. Clear existing geotargets data
|
||||
2. Load fresh data from CSV
|
||||
|
||||
This means if you swap the CSV file, the data will be updated.
|
||||
|
||||
Args:
|
||||
csv_path: Path to geotargets CSV file
|
||||
"""
|
||||
if not csv_path.exists():
|
||||
logger.warning(f"Geotargets CSV file not found: {csv_path}")
|
||||
logger.info("Skipping geotargets loading. Place geotargets CSV file to load location data.")
|
||||
return
|
||||
|
||||
logger.info(f"Loading geotargets from {csv_path}")
|
||||
|
||||
# Read CSV file
|
||||
geotargets = []
|
||||
with open(csv_path, 'r', encoding='utf-8') as f:
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
# Convert IDs to integers, handling empty parent_id
|
||||
criteria_id = int(row['Criteria ID'])
|
||||
parent_id = int(row['Parent ID']) if row['Parent ID'] else None
|
||||
|
||||
geotargets.append({
|
||||
'criteria_id': criteria_id,
|
||||
'name': row['Name'],
|
||||
'canonical_name': row['Canonical Name'],
|
||||
'parent_id': parent_id,
|
||||
'country_code': row['Country Code'],
|
||||
'target_type': row['Target Type'],
|
||||
'status': row['Status']
|
||||
})
|
||||
|
||||
if not geotargets:
|
||||
logger.warning("No geotargets found in CSV file")
|
||||
return
|
||||
|
||||
# Load data to database using idempotent approach
|
||||
async with self.db.pool.acquire() as conn:
|
||||
# Clear existing data to ensure we have latest data from CSV
|
||||
deleted_count = await conn.fetchval("SELECT COUNT(*) FROM public.geotargets")
|
||||
await conn.execute("TRUNCATE TABLE public.geotargets")
|
||||
logger.info(f"Cleared {deleted_count} existing geotarget records")
|
||||
|
||||
# Batch insert new data
|
||||
await conn.executemany(
|
||||
"""
|
||||
INSERT INTO public.geotargets
|
||||
(criteria_id, name, canonical_name, parent_id, country_code, target_type, status, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())
|
||||
""",
|
||||
[(g['criteria_id'], g['name'], g['canonical_name'], g['parent_id'],
|
||||
g['country_code'], g['target_type'], g['status'])
|
||||
for g in geotargets]
|
||||
)
|
||||
|
||||
logger.info(f"Successfully loaded {len(geotargets)} geotargets from CSV")
|
||||
|
||||
def detect_materialized_views(self, schema_path: Path) -> List[str]:
|
||||
"""
|
||||
Auto-detect materialized views from db_schema.sql.
|
||||
|
||||
Parses the SQL file to find:
|
||||
1. CREATE MATERIALIZED VIEW statements
|
||||
2. REFRESH MATERIALIZED VIEW statements (explicit refresh requests)
|
||||
|
||||
Args:
|
||||
schema_path: Path to db_schema.sql
|
||||
|
||||
Returns:
|
||||
List of materialized view names (deduplicated, in order of appearance)
|
||||
"""
|
||||
if not schema_path.exists():
|
||||
logger.warning(f"Schema file not found: {schema_path}")
|
||||
return []
|
||||
|
||||
with open(schema_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
views = []
|
||||
|
||||
# Find CREATE MATERIALIZED VIEW statements
|
||||
# Matches: CREATE MATERIALIZED VIEW [IF NOT EXISTS] view_name AS
|
||||
create_pattern = r'CREATE\s+MATERIALIZED\s+VIEW\s+(?:IF\s+NOT\s+EXISTS\s+)?(\w+)\s+AS'
|
||||
create_matches = re.findall(create_pattern, content, re.IGNORECASE)
|
||||
views.extend(create_matches)
|
||||
|
||||
# Find REFRESH MATERIALIZED VIEW statements
|
||||
# Matches: REFRESH MATERIALIZED VIEW [CONCURRENTLY] view_name
|
||||
refresh_pattern = r'REFRESH\s+MATERIALIZED\s+VIEW\s+(?:CONCURRENTLY\s+)?(?:public\.)?(\w+)'
|
||||
refresh_matches = re.findall(refresh_pattern, content, re.IGNORECASE)
|
||||
views.extend(refresh_matches)
|
||||
|
||||
# Deduplicate while preserving order
|
||||
seen = set()
|
||||
deduplicated = []
|
||||
for view in views:
|
||||
if view not in seen:
|
||||
seen.add(view)
|
||||
deduplicated.append(view)
|
||||
|
||||
if deduplicated:
|
||||
logger.info(f"Detected {len(deduplicated)} materialized view(s): {', '.join(deduplicated)}")
|
||||
else:
|
||||
logger.info("No materialized views detected in db_schema.sql")
|
||||
|
||||
return deduplicated
|
||||
|
||||
async def refresh_materialized_views(self):
|
||||
"""Refresh all detected materialized views."""
|
||||
if not self.materialized_views:
|
||||
logger.debug("No materialized views to refresh")
|
||||
return
|
||||
|
||||
logger.info(f"Refreshing {len(self.materialized_views)} materialized view(s)...")
|
||||
|
||||
for view_name in self.materialized_views:
|
||||
try:
|
||||
# Try CONCURRENTLY first (requires unique index)
|
||||
await self.db.pool.execute(
|
||||
f"REFRESH MATERIALIZED VIEW CONCURRENTLY public.{view_name}"
|
||||
)
|
||||
logger.info(f"✓ Refreshed {view_name} (CONCURRENTLY)")
|
||||
|
||||
except Exception as e:
|
||||
error_msg = str(e).lower()
|
||||
|
||||
# If CONCURRENTLY fails, try regular refresh
|
||||
if 'concurrently' in error_msg or 'unique index' in error_msg:
|
||||
try:
|
||||
await self.db.pool.execute(
|
||||
f"REFRESH MATERIALIZED VIEW public.{view_name}"
|
||||
)
|
||||
logger.info(f"✓ Refreshed {view_name}")
|
||||
except Exception as e2:
|
||||
logger.error(f"✗ Failed to refresh {view_name}: {e2}")
|
||||
else:
|
||||
logger.error(f"✗ Failed to refresh {view_name}: {e}")
|
||||
|
||||
logger.info("Materialized view refresh completed")
|
||||
|
||||
async def setup(self):
|
||||
"""Initialize database schema and load metadata."""
|
||||
try:
|
||||
# Connect to database
|
||||
await self.db.connect()
|
||||
|
||||
# Initialize schema (this will drop and recreate public schema)
|
||||
logger.info("Initializing database schema...")
|
||||
schema_path = Path(__file__).parent / "db_schema.sql"
|
||||
await self.db.initialize_schema()
|
||||
logger.info("Database schema initialized successfully")
|
||||
|
||||
# Auto-detect materialized views
|
||||
self.materialized_views = self.detect_materialized_views(schema_path)
|
||||
|
||||
# Load metadata from YAML
|
||||
metadata_path = Path(__file__).parent.parent.parent / "metadata.yaml"
|
||||
await self.load_metadata_from_yaml(metadata_path)
|
||||
|
||||
# Load geotargets from CSV
|
||||
# Look for the most recent geotargets CSV file in the package directory
|
||||
geotargets_dir = Path(__file__).parent
|
||||
geotargets_files = sorted(geotargets_dir.glob("geotargets*.csv"), reverse=True)
|
||||
if geotargets_files:
|
||||
geotargets_path = geotargets_files[0]
|
||||
await self.load_geotargets_from_csv(geotargets_path)
|
||||
else:
|
||||
logger.warning("No geotargets CSV file found in package directory")
|
||||
|
||||
# Initial refresh of materialized views
|
||||
if self.materialized_views:
|
||||
logger.info("Performing initial refresh of materialized views...")
|
||||
await self.refresh_materialized_views()
|
||||
|
||||
logger.info("Setup completed successfully")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Setup failed: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
async def periodic_refresh_loop(self):
|
||||
"""
|
||||
Periodically refresh materialized views.
|
||||
|
||||
Runs in a loop until shutdown is signaled.
|
||||
"""
|
||||
if not self.materialized_views:
|
||||
logger.info("No materialized views to refresh, skipping periodic refresh")
|
||||
return
|
||||
|
||||
logger.info(f"Starting periodic refresh loop (interval: {self.refresh_interval_minutes} minutes)")
|
||||
|
||||
while not self.shutdown_event.is_set():
|
||||
try:
|
||||
# Wait for the refresh interval or shutdown signal
|
||||
await asyncio.wait_for(
|
||||
self.shutdown_event.wait(),
|
||||
timeout=self.refresh_interval_minutes * 60
|
||||
)
|
||||
# If we get here, shutdown was signaled
|
||||
break
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
# Timeout means it's time to refresh
|
||||
logger.info("Starting scheduled materialized view refresh...")
|
||||
try:
|
||||
await self.refresh_materialized_views()
|
||||
except Exception as e:
|
||||
logger.error(f"Error during scheduled refresh: {e}", exc_info=True)
|
||||
# Continue the loop even if refresh fails
|
||||
|
||||
async def wait_for_tasks(self):
|
||||
"""
|
||||
Run periodic tasks and wait for shutdown signal.
|
||||
|
||||
Currently runs:
|
||||
- Periodic materialized view refresh (configurable interval)
|
||||
|
||||
In the future, this could also:
|
||||
- Poll Airbyte API for completed syncs
|
||||
- Serve health check endpoints
|
||||
- Listen to webhooks
|
||||
"""
|
||||
logger.info("View manager is ready")
|
||||
logger.info("Press Ctrl+C to shutdown")
|
||||
|
||||
# Run periodic refresh loop
|
||||
await self.periodic_refresh_loop()
|
||||
|
||||
logger.info("Periodic refresh loop stopped")
|
||||
|
||||
async def run(self):
|
||||
"""Main entry point."""
|
||||
try:
|
||||
# Setup database and metadata
|
||||
await self.setup()
|
||||
|
||||
# Wait for tasks or shutdown
|
||||
await self.wait_for_tasks()
|
||||
|
||||
finally:
|
||||
logger.info("Shutting down...")
|
||||
await self.db.close()
|
||||
logger.info("Shutdown complete")
|
||||
|
||||
def handle_shutdown(self, signum, frame):
|
||||
"""Handle shutdown signals gracefully."""
|
||||
logger.info(f"Received signal {signum}, initiating shutdown...")
|
||||
self.shutdown_event.set()
|
||||
|
||||
|
||||
def main():
|
||||
"""CLI entry point."""
|
||||
# Get refresh interval from environment variable
|
||||
refresh_interval = int(os.getenv('REFRESH_INTERVAL_MINUTES', '60'))
|
||||
logger.info(f"Configured refresh interval: {refresh_interval} minutes")
|
||||
|
||||
manager = ViewManagerSetup(refresh_interval_minutes=refresh_interval)
|
||||
|
||||
# Register signal handlers for graceful shutdown
|
||||
signal.signal(signal.SIGINT, manager.handle_shutdown)
|
||||
signal.signal(signal.SIGTERM, manager.handle_shutdown)
|
||||
|
||||
# Run the async main function
|
||||
try:
|
||||
asyncio.run(manager.run())
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Interrupted by user")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
logger.error(f"Fatal error: {e}", exc_info=True)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,145 +0,0 @@
|
||||
"""
|
||||
Simple test script to initialize database and grab ad_accounts metadata.
|
||||
This is useful for testing the database setup and verifying ad account access.
|
||||
Grabs ALL ad accounts accessible to the token.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from facebook_business.adobjects.adaccount import AdAccount
|
||||
from facebook_business.adobjects.user import User
|
||||
from facebook_business.api import FacebookAdsApi
|
||||
|
||||
from meta_api_grabber.database import TimescaleDBClient
|
||||
|
||||
|
||||
async def test_ad_accounts():
|
||||
"""Test database initialization and ad account metadata collection."""
|
||||
load_dotenv()
|
||||
|
||||
# Get credentials from environment
|
||||
access_token = os.getenv("META_ACCESS_TOKEN")
|
||||
app_secret = os.getenv("META_APP_SECRET")
|
||||
app_id = os.getenv("META_APP_ID")
|
||||
|
||||
if not all([access_token, app_secret, app_id]):
|
||||
print("❌ Missing required environment variables")
|
||||
print(" Please ensure META_ACCESS_TOKEN, META_APP_SECRET, and META_APP_ID")
|
||||
print(" are set in .env")
|
||||
return 1
|
||||
|
||||
print("="*60)
|
||||
print("AD ACCOUNTS TEST - GRABBING ALL ACCESSIBLE ACCOUNTS")
|
||||
print("="*60)
|
||||
print()
|
||||
|
||||
# Initialize Facebook Ads API
|
||||
FacebookAdsApi.init(
|
||||
app_id=app_id,
|
||||
app_secret=app_secret,
|
||||
access_token=access_token,
|
||||
)
|
||||
|
||||
print("\nFetching all ad accounts accessible to this token...")
|
||||
me = User(fbid='me')
|
||||
account_fields = ['name', 'currency', 'timezone_name', 'account_status']
|
||||
|
||||
ad_accounts = me.get_ad_accounts(fields=account_fields)
|
||||
|
||||
print(f"Found {len(ad_accounts)} ad account(s)\n")
|
||||
|
||||
stored_count = 0
|
||||
for account in ad_accounts:
|
||||
account_id = account['id']
|
||||
print(f"Ad Account {stored_count + 1}:")
|
||||
print(f" ID: {account_id}")
|
||||
print(f" Name: {account.get('name', 'N/A')}")
|
||||
print(f" Currency: {account.get('currency', 'N/A')}")
|
||||
print(f" Timezone: {account.get('timezone_name', 'N/A')}")
|
||||
print(f" Status: {account.get('account_status', 'N/A')}")
|
||||
|
||||
|
||||
try:
|
||||
|
||||
# Connect to database
|
||||
print("Connecting to database...")
|
||||
db = TimescaleDBClient()
|
||||
await db.connect()
|
||||
# Initialize schema
|
||||
print("\nInitializing database schema...")
|
||||
await db.initialize_schema()
|
||||
|
||||
# Get all ad accounts accessible to this token
|
||||
print("\nFetching all ad accounts accessible to this token...")
|
||||
me = User(fbid='me')
|
||||
account_fields = ['name', 'currency', 'timezone_name', 'account_status']
|
||||
|
||||
ad_accounts = me.get_ad_accounts(fields=account_fields)
|
||||
|
||||
print(f"Found {len(ad_accounts)} ad account(s)\n")
|
||||
|
||||
stored_count = 0
|
||||
for account in ad_accounts:
|
||||
account_id = account['id']
|
||||
print(f"Ad Account {stored_count + 1}:")
|
||||
print(f" ID: {account_id}")
|
||||
print(f" Name: {account.get('name', 'N/A')}")
|
||||
print(f" Currency: {account.get('currency', 'N/A')}")
|
||||
print(f" Timezone: {account.get('timezone_name', 'N/A')}")
|
||||
print(f" Status: {account.get('account_status', 'N/A')}")
|
||||
|
||||
# Store in database
|
||||
await db.upsert_ad_account(
|
||||
account_id=account_id,
|
||||
account_name=account.get('name'),
|
||||
currency=account.get('currency'),
|
||||
timezone_name=account.get('timezone_name'),
|
||||
)
|
||||
print(f" ✓ Stored in database\n")
|
||||
stored_count += 1
|
||||
|
||||
# Verify by querying the database
|
||||
print("="*60)
|
||||
print(f"Verifying database storage ({stored_count} account(s))...")
|
||||
print("="*60)
|
||||
async with db.pool.acquire() as conn:
|
||||
rows = await conn.fetch("SELECT * FROM ad_accounts ORDER BY account_name")
|
||||
if rows:
|
||||
print(f"\n✓ {len(rows)} ad account(s) found in database:\n")
|
||||
for i, row in enumerate(rows, 1):
|
||||
print(f"{i}. {row['account_name']} ({row['account_id']})")
|
||||
print(f" Currency: {row['currency']} | Timezone: {row['timezone_name']}")
|
||||
print(f" Updated: {row['updated_at']}\n")
|
||||
else:
|
||||
print("❌ No ad accounts found in database")
|
||||
|
||||
print("="*60)
|
||||
print("TEST COMPLETED SUCCESSFULLY")
|
||||
print("="*60)
|
||||
print(f"\n✓ Successfully grabbed and stored {stored_count} ad account(s)")
|
||||
print("\nNext steps:")
|
||||
print("1. Check your database: docker exec meta_timescaledb psql -U meta_user -d meta_insights -c 'SELECT * FROM ad_accounts;'")
|
||||
print("2. Run scheduled grabber for all accounts: meta-scheduled")
|
||||
print("3. The scheduled grabber will now process all these accounts!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return 1
|
||||
|
||||
finally:
|
||||
await db.close()
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
"""Entry point for the test script."""
|
||||
exit_code = asyncio.run(test_ad_accounts())
|
||||
exit(exit_code)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,91 +0,0 @@
|
||||
"""
|
||||
Test to see what campaign insights API actually returns for campaign_name.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
from dotenv import load_dotenv
|
||||
from facebook_business.api import FacebookAdsApi
|
||||
from facebook_business.adobjects.adaccount import AdAccount
|
||||
from facebook_business.adobjects.adsinsights import AdsInsights
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
|
||||
# Initialize Facebook Ads API
|
||||
access_token = os.getenv("META_ACCESS_TOKEN")
|
||||
app_secret = os.getenv("META_APP_SECRET")
|
||||
app_id = os.getenv("META_APP_ID")
|
||||
|
||||
if not all([access_token, app_secret, app_id]):
|
||||
raise ValueError("Missing required environment variables")
|
||||
|
||||
FacebookAdsApi.init(
|
||||
app_id=app_id,
|
||||
app_secret=app_secret,
|
||||
access_token=access_token,
|
||||
)
|
||||
|
||||
account_id = "act_238334370765317"
|
||||
|
||||
print(f"Testing campaign INSIGHTS fetch for account: {account_id}")
|
||||
print("=" * 60)
|
||||
|
||||
# Get campaign insights (this is what scheduled_grabber.py does)
|
||||
print("\nFetching campaign-level insights with 'today' preset...")
|
||||
print("-" * 60)
|
||||
|
||||
fields = [
|
||||
AdsInsights.Field.campaign_id,
|
||||
AdsInsights.Field.campaign_name,
|
||||
AdsInsights.Field.impressions,
|
||||
AdsInsights.Field.clicks,
|
||||
AdsInsights.Field.spend,
|
||||
AdsInsights.Field.ctr,
|
||||
AdsInsights.Field.cpc,
|
||||
AdsInsights.Field.cpm,
|
||||
AdsInsights.Field.reach,
|
||||
AdsInsights.Field.actions,
|
||||
AdsInsights.Field.date_start,
|
||||
AdsInsights.Field.date_stop,
|
||||
]
|
||||
|
||||
params = {
|
||||
"date_preset": "today",
|
||||
"level": "campaign",
|
||||
"limit": 10,
|
||||
}
|
||||
|
||||
ad_account = AdAccount(account_id)
|
||||
insights = ad_account.get_insights(
|
||||
fields=fields,
|
||||
params=params,
|
||||
)
|
||||
|
||||
insights_list = list(insights)
|
||||
print(f"Found {len(insights_list)} campaign insights\n")
|
||||
|
||||
campaigns_without_names = []
|
||||
for i, insight in enumerate(insights_list, 1):
|
||||
insight_dict = dict(insight)
|
||||
campaign_id = insight_dict.get('campaign_id')
|
||||
campaign_name = insight_dict.get('campaign_name')
|
||||
|
||||
print(f"Campaign Insight {i}:")
|
||||
print(f" Campaign ID: {campaign_id}")
|
||||
print(f" Campaign Name: {campaign_name if campaign_name else '❌ MISSING'}")
|
||||
print(f" Impressions: {insight_dict.get('impressions')}")
|
||||
print(f" Spend: {insight_dict.get('spend')}")
|
||||
print(f" Keys available: {list(insight_dict.keys())}")
|
||||
print(f" Raw JSON: {json.dumps(insight_dict, indent=4, default=str)}")
|
||||
print()
|
||||
|
||||
if not campaign_name:
|
||||
campaigns_without_names.append(campaign_id)
|
||||
|
||||
print("=" * 60)
|
||||
if campaigns_without_names:
|
||||
print(f"⚠️ {len(campaigns_without_names)} campaigns WITHOUT names in insights:")
|
||||
print(f" {campaigns_without_names}")
|
||||
else:
|
||||
print("✓ All campaigns have names in insights!")
|
||||
@@ -1,118 +0,0 @@
|
||||
"""
|
||||
Test script to diagnose campaign name issues for a specific ad account.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
from dotenv import load_dotenv
|
||||
from facebook_business.api import FacebookAdsApi
|
||||
from facebook_business.adobjects.adaccount import AdAccount
|
||||
from facebook_business.adobjects.campaign import Campaign
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
|
||||
# Initialize Facebook Ads API
|
||||
access_token = os.getenv("META_ACCESS_TOKEN")
|
||||
app_secret = os.getenv("META_APP_SECRET")
|
||||
app_id = os.getenv("META_APP_ID")
|
||||
|
||||
if not all([access_token, app_secret, app_id]):
|
||||
raise ValueError("Missing required environment variables")
|
||||
|
||||
FacebookAdsApi.init(
|
||||
app_id=app_id,
|
||||
app_secret=app_secret,
|
||||
access_token=access_token,
|
||||
)
|
||||
|
||||
# Test account ID
|
||||
account_id = "act_238334370765317"
|
||||
|
||||
print(f"Testing campaign fetch for account: {account_id}")
|
||||
print("=" * 60)
|
||||
|
||||
# Get campaigns using get_campaigns
|
||||
print("\n1. Testing AdAccount.get_campaigns()...")
|
||||
print("-" * 60)
|
||||
|
||||
ad_account = AdAccount(account_id)
|
||||
campaigns = ad_account.get_campaigns(
|
||||
fields=[
|
||||
Campaign.Field.name,
|
||||
Campaign.Field.status,
|
||||
Campaign.Field.objective,
|
||||
Campaign.Field.id,
|
||||
],
|
||||
params={'limit': 10}
|
||||
)
|
||||
|
||||
campaigns_list = list(campaigns)
|
||||
print(f"Found {len(campaigns_list)} campaigns\n")
|
||||
|
||||
campaigns_without_names = []
|
||||
for i, campaign in enumerate(campaigns_list, 1):
|
||||
campaign_dict = dict(campaign)
|
||||
campaign_id = campaign_dict.get('id')
|
||||
campaign_name = campaign_dict.get('name')
|
||||
|
||||
print(f"Campaign {i}:")
|
||||
print(f" ID: {campaign_id}")
|
||||
print(f" Name: {campaign_name if campaign_name else '❌ MISSING'}")
|
||||
print(f" Status: {campaign_dict.get('status')}")
|
||||
print(f" Objective: {campaign_dict.get('objective')}")
|
||||
print(f" Raw data: {json.dumps(campaign_dict, indent=4)}")
|
||||
print()
|
||||
|
||||
if not campaign_name:
|
||||
campaigns_without_names.append(campaign_id)
|
||||
|
||||
# If we found campaigns without names, try fetching them individually
|
||||
if campaigns_without_names:
|
||||
print("\n2. Retrying campaigns without names (individual fetch)...")
|
||||
print("-" * 60)
|
||||
|
||||
for campaign_id in campaigns_without_names:
|
||||
print(f"\nFetching campaign {campaign_id} directly...")
|
||||
try:
|
||||
campaign_obj = Campaign(campaign_id)
|
||||
campaign_data = campaign_obj.api_get(
|
||||
fields=[
|
||||
Campaign.Field.name,
|
||||
Campaign.Field.status,
|
||||
Campaign.Field.objective,
|
||||
Campaign.Field.account_id,
|
||||
]
|
||||
)
|
||||
|
||||
campaign_dict = dict(campaign_data)
|
||||
print(f" Direct fetch result:")
|
||||
print(f" Name: {campaign_dict.get('name', '❌ STILL MISSING')}")
|
||||
print(f" Status: {campaign_dict.get('status')}")
|
||||
print(f" Raw data: {json.dumps(campaign_dict, indent=4)}")
|
||||
except Exception as e:
|
||||
print(f" Error fetching campaign: {e}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print(f"Summary: {len(campaigns_without_names)} out of {len(campaigns_list)} campaigns have missing names")
|
||||
print(f"Missing campaign IDs: {campaigns_without_names}")
|
||||
else:
|
||||
print("\n" + "=" * 60)
|
||||
print("✓ All campaigns have names!")
|
||||
|
||||
print("\n3. Checking account permissions...")
|
||||
print("-" * 60)
|
||||
try:
|
||||
account_data = ad_account.api_get(
|
||||
fields=['name', 'account_status', 'capabilities', 'business']
|
||||
)
|
||||
account_dict = dict(account_data)
|
||||
print(f"Account Name: {account_dict.get('name')}")
|
||||
print(f"Account Status: {account_dict.get('account_status')}")
|
||||
print(f"Capabilities: {json.dumps(account_dict.get('capabilities', []), indent=2)}")
|
||||
print(f"Business ID: {account_dict.get('business')}")
|
||||
except Exception as e:
|
||||
print(f"Error fetching account details: {e}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Test complete!")
|
||||
@@ -1,125 +0,0 @@
|
||||
"""
|
||||
Test that date_start and date_stop are properly stored in the database.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
from dotenv import load_dotenv
|
||||
from facebook_business.adobjects.adaccount import AdAccount
|
||||
from facebook_business.adobjects.adsinsights import AdsInsights
|
||||
from facebook_business.api import FacebookAdsApi
|
||||
|
||||
from meta_api_grabber.database import TimescaleDBClient
|
||||
|
||||
|
||||
async def test_date_fields():
|
||||
"""Test that date fields are stored correctly."""
|
||||
load_dotenv()
|
||||
|
||||
access_token = os.getenv("META_ACCESS_TOKEN")
|
||||
app_secret = os.getenv("META_APP_SECRET")
|
||||
app_id = os.getenv("META_APP_ID")
|
||||
account_id = "act_238334370765317"
|
||||
|
||||
if not all([access_token, app_secret, app_id]):
|
||||
print("❌ Missing required environment variables")
|
||||
return 1
|
||||
|
||||
# Initialize Facebook Ads API
|
||||
FacebookAdsApi.init(
|
||||
app_id=app_id,
|
||||
app_secret=app_secret,
|
||||
access_token=access_token,
|
||||
)
|
||||
|
||||
print("="*70)
|
||||
print("TESTING DATE_START AND DATE_STOP STORAGE")
|
||||
print("="*70)
|
||||
print()
|
||||
|
||||
# Connect to database
|
||||
print("Connecting to database...")
|
||||
db = TimescaleDBClient()
|
||||
await db.connect()
|
||||
|
||||
try:
|
||||
print("Fetching insights for 'today'...")
|
||||
ad_account = AdAccount(account_id)
|
||||
|
||||
fields = [
|
||||
AdsInsights.Field.impressions,
|
||||
AdsInsights.Field.clicks,
|
||||
AdsInsights.Field.spend,
|
||||
AdsInsights.Field.date_start,
|
||||
AdsInsights.Field.date_stop,
|
||||
]
|
||||
|
||||
params = {
|
||||
"date_preset": "today",
|
||||
"level": "account",
|
||||
}
|
||||
|
||||
insights = ad_account.get_insights(fields=fields, params=params)
|
||||
|
||||
# Store in database
|
||||
timestamp = datetime.now(timezone.utc)
|
||||
for insight in insights:
|
||||
insight_dict = dict(insight)
|
||||
print(f"\nAPI Response:")
|
||||
print(f" date_start: {insight_dict.get('date_start')}")
|
||||
print(f" date_stop: {insight_dict.get('date_stop')}")
|
||||
print(f" impressions: {insight_dict.get('impressions')}")
|
||||
|
||||
await db.insert_account_insights(
|
||||
time=timestamp,
|
||||
account_id=account_id,
|
||||
data=insight_dict,
|
||||
date_preset="today",
|
||||
)
|
||||
print("\n✓ Stored in database")
|
||||
|
||||
# Verify from database
|
||||
print("\nQuerying database...")
|
||||
async with db.pool.acquire() as conn:
|
||||
row = await conn.fetchrow("""
|
||||
SELECT date_start, date_stop, date_preset, impressions, spend
|
||||
FROM account_insights
|
||||
WHERE account_id = $1
|
||||
ORDER BY time DESC
|
||||
LIMIT 1
|
||||
""", account_id)
|
||||
|
||||
if row:
|
||||
print("\n✓ Retrieved from database:")
|
||||
print(f" date_start: {row['date_start']}")
|
||||
print(f" date_stop: {row['date_stop']}")
|
||||
print(f" date_preset: {row['date_preset']}")
|
||||
print(f" impressions: {row['impressions']}")
|
||||
print(f" spend: {row['spend']}")
|
||||
|
||||
print("\n" + "="*70)
|
||||
print("✓ TEST PASSED - Date fields are stored correctly!")
|
||||
print("="*70)
|
||||
print("\nYou can now query historical data by date_stop:")
|
||||
print(" - For clean daily trends, use: GROUP BY date_stop")
|
||||
print(" - For latest value per day, use: ORDER BY time DESC with date_stop")
|
||||
print()
|
||||
else:
|
||||
print("\n❌ No data found in database")
|
||||
return 1
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return 1
|
||||
|
||||
finally:
|
||||
await db.close()
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit(asyncio.run(test_date_fields()))
|
||||
@@ -1,132 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to grab ad accounts from Google Ads API.
|
||||
|
||||
This script reads configuration from google-ads.yaml and authenticates using
|
||||
a service account JSON key file to retrieve accessible customer accounts.
|
||||
"""
|
||||
|
||||
from google.ads.googleads.client import GoogleAdsClient
|
||||
from google.ads.googleads.errors import GoogleAdsException
|
||||
import sys
|
||||
|
||||
|
||||
def list_accessible_customers(client):
|
||||
"""Lists all customer IDs accessible to the authenticated user.
|
||||
|
||||
Args:
|
||||
client: An initialized GoogleAdsClient instance.
|
||||
|
||||
Returns:
|
||||
List of customer resource names.
|
||||
"""
|
||||
customer_service = client.get_service("CustomerService")
|
||||
|
||||
try:
|
||||
accessible_customers = customer_service.list_accessible_customers()
|
||||
print(f"\nFound {len(accessible_customers.resource_names)} accessible customers:")
|
||||
|
||||
for resource_name in accessible_customers.resource_names:
|
||||
customer_id = resource_name.split('/')[-1]
|
||||
print(f" - Customer ID: {customer_id}")
|
||||
print(f" Resource Name: {resource_name}")
|
||||
|
||||
return accessible_customers.resource_names
|
||||
|
||||
except GoogleAdsException as ex:
|
||||
print(f"Request failed with status {ex.error.code().name}")
|
||||
for error in ex.failure.errors:
|
||||
print(f"\tError: {error.message}")
|
||||
if error.location:
|
||||
for field in error.location.field_path_elements:
|
||||
print(f"\t\tField: {field.field_name}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def get_customer_details(client, customer_id):
|
||||
"""Retrieves detailed information about a customer account.
|
||||
|
||||
Args:
|
||||
client: An initialized GoogleAdsClient instance.
|
||||
customer_id: The customer ID (without dashes).
|
||||
"""
|
||||
ga_service = client.get_service("GoogleAdsService")
|
||||
|
||||
query = """
|
||||
SELECT
|
||||
customer.id,
|
||||
customer.descriptive_name,
|
||||
customer.currency_code,
|
||||
customer.time_zone,
|
||||
customer.manager,
|
||||
customer.test_account
|
||||
FROM customer
|
||||
WHERE customer.id = {customer_id}
|
||||
""".format(customer_id=customer_id)
|
||||
|
||||
try:
|
||||
response = ga_service.search(customer_id=customer_id, query=query)
|
||||
|
||||
for row in response:
|
||||
customer = row.customer
|
||||
print(f"\n--- Customer Details for {customer_id} ---")
|
||||
print(f" ID: {customer.id}")
|
||||
print(f" Name: {customer.descriptive_name}")
|
||||
print(f" Currency: {customer.currency_code}")
|
||||
print(f" Time Zone: {customer.time_zone}")
|
||||
print(f" Is Manager: {customer.manager}")
|
||||
print(f" Is Test Account: {customer.test_account}")
|
||||
|
||||
except GoogleAdsException as ex:
|
||||
print(f"\nFailed to get details for customer {customer_id}")
|
||||
print(f"Status: {ex.error.code().name}")
|
||||
for error in ex.failure.errors:
|
||||
print(f" Error: {error.message}")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main function to test Google Ads API connection and list accounts."""
|
||||
|
||||
print("=" * 60)
|
||||
print("Google Ads API - Account Listing Test")
|
||||
print("=" * 60)
|
||||
|
||||
# Load client from YAML configuration
|
||||
# By default, this looks for google-ads.yaml in the current directory
|
||||
# or in the home directory
|
||||
try:
|
||||
print("\nLoading Google Ads client from configuration...")
|
||||
client = GoogleAdsClient.load_from_storage(path="google-ads.yaml")
|
||||
print("✓ Client loaded successfully")
|
||||
except Exception as e:
|
||||
print(f"✗ Failed to load client: {e}")
|
||||
print("\nPlease ensure:")
|
||||
print(" 1. google-ads.yaml exists and is properly configured")
|
||||
print(" 2. google_ads_key.json exists and contains valid credentials")
|
||||
print(" 3. All required fields are filled in google-ads.yaml")
|
||||
sys.exit(1)
|
||||
|
||||
# List accessible customers
|
||||
print("\n" + "=" * 60)
|
||||
print("Listing Accessible Customers")
|
||||
print("=" * 60)
|
||||
|
||||
resource_names = list_accessible_customers(client)
|
||||
|
||||
# Get detailed information for each customer
|
||||
if resource_names:
|
||||
print("\n" + "=" * 60)
|
||||
print("Customer Details")
|
||||
print("=" * 60)
|
||||
|
||||
for resource_name in resource_names:
|
||||
customer_id = resource_name.split('/')[-1]
|
||||
get_customer_details(client, customer_id)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Test completed successfully!")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,171 +0,0 @@
|
||||
"""
|
||||
Test script to retrieve leads from a specific Facebook page.
|
||||
This uses the existing Meta API credentials to test leads retrieval.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from datetime import datetime
|
||||
from dotenv import load_dotenv
|
||||
from facebook_business.api import FacebookAdsApi
|
||||
from facebook_business.adobjects.page import Page
|
||||
|
||||
|
||||
async def test_page_leads():
|
||||
"""Test retrieving leads from a specific Facebook page."""
|
||||
load_dotenv()
|
||||
|
||||
# Get credentials from environment
|
||||
access_token = os.getenv("META_ACCESS_TOKEN")
|
||||
app_secret = os.getenv("META_APP_SECRET")
|
||||
app_id = os.getenv("META_APP_ID")
|
||||
|
||||
if not all([access_token, app_secret, app_id]):
|
||||
print("❌ Missing required environment variables")
|
||||
print(" Please ensure META_ACCESS_TOKEN, META_APP_SECRET, and META_APP_ID")
|
||||
print(" are set in .env")
|
||||
return 1
|
||||
|
||||
print("="*60)
|
||||
print("PAGE LEADS TEST - RETRIEVING LEADS FROM A SPECIFIC PAGE")
|
||||
print("="*60)
|
||||
print()
|
||||
|
||||
# Initialize Facebook Ads API
|
||||
FacebookAdsApi.init(
|
||||
app_id=app_id,
|
||||
app_secret=app_secret,
|
||||
access_token=access_token,
|
||||
)
|
||||
|
||||
# Prompt for page ID
|
||||
page_id = input("Enter the Facebook Page ID: ").strip()
|
||||
|
||||
if not page_id:
|
||||
print("❌ No page ID provided")
|
||||
return 1
|
||||
|
||||
try:
|
||||
# Initialize the Page object
|
||||
print(f"\nConnecting to Page {page_id}...")
|
||||
page = Page(fbid=page_id)
|
||||
|
||||
# First, get basic page information to verify access
|
||||
print("\nFetching page information...")
|
||||
page_fields = ['name', 'id', 'access_token']
|
||||
page_info = page.api_get(fields=page_fields)
|
||||
|
||||
print(f"\n✓ Page Information:")
|
||||
print(f" Name: {page_info.get('name', 'N/A')}")
|
||||
print(f" ID: {page_info.get('id', 'N/A')}")
|
||||
print(f" Has Access Token: {'Yes' if page_info.get('access_token') else 'No'}")
|
||||
|
||||
# Get leadgen forms associated with this page
|
||||
print("\n" + "="*60)
|
||||
print("Fetching Lead Generation Forms...")
|
||||
print("="*60)
|
||||
|
||||
leadgen_forms = page.get_lead_gen_forms(
|
||||
fields=['id', 'name', 'status', 'leads_count', 'created_time']
|
||||
)
|
||||
|
||||
if not leadgen_forms or len(leadgen_forms) == 0:
|
||||
print("\n⚠️ No lead generation forms found for this page")
|
||||
print(" This could mean:")
|
||||
print(" 1. The page has no lead forms")
|
||||
print(" 2. The access token doesn't have permission to view lead forms")
|
||||
print(" 3. The page ID is incorrect")
|
||||
return 0
|
||||
|
||||
print(f"\nFound {len(leadgen_forms)} lead generation form(s):\n")
|
||||
|
||||
total_leads = 0
|
||||
for idx, form in enumerate(leadgen_forms, 1):
|
||||
form_id = form.get('id')
|
||||
form_name = form.get('name', 'N/A')
|
||||
form_status = form.get('status', 'N/A')
|
||||
leads_count = form.get('leads_count', 0)
|
||||
created_time = form.get('created_time', 'N/A')
|
||||
|
||||
print(f"Form {idx}:")
|
||||
print(f" ID: {form_id}")
|
||||
print(f" Name: {form_name}")
|
||||
print(f" Status: {form_status}")
|
||||
print(f" Leads Count: {leads_count}")
|
||||
print(f" Created: {created_time}")
|
||||
|
||||
# Try to fetch actual leads from this form
|
||||
try:
|
||||
print(f"\n Fetching leads from form '{form_name}'...")
|
||||
|
||||
# Get the form object to retrieve leads
|
||||
from facebook_business.adobjects.leadgenform import LeadgenForm
|
||||
form_obj = LeadgenForm(fbid=form_id)
|
||||
|
||||
leads = form_obj.get_leads(
|
||||
fields=['id', 'created_time', 'field_data']
|
||||
)
|
||||
|
||||
leads_list = list(leads)
|
||||
print(f" ✓ Retrieved {len(leads_list)} lead(s)")
|
||||
|
||||
if leads_list:
|
||||
print(f"\n Sample leads from '{form_name}':")
|
||||
for lead_idx, lead in enumerate(leads_list[:5], 1): # Show first 5 leads
|
||||
lead_id = lead.get('id')
|
||||
lead_created = lead.get('created_time', 'N/A')
|
||||
field_data = lead.get('field_data', [])
|
||||
|
||||
print(f"\n Lead {lead_idx}:")
|
||||
print(f" ID: {lead_id}")
|
||||
print(f" Created: {lead_created}")
|
||||
print(f" Fields:")
|
||||
|
||||
for field in field_data:
|
||||
field_name = field.get('name', 'unknown')
|
||||
field_values = field.get('values', [])
|
||||
print(f" {field_name}: {', '.join(field_values)}")
|
||||
|
||||
if len(leads_list) > 5:
|
||||
print(f"\n ... and {len(leads_list) - 5} more lead(s)")
|
||||
|
||||
total_leads += len(leads_list)
|
||||
|
||||
except Exception as lead_error:
|
||||
print(f" ❌ Error fetching leads: {lead_error}")
|
||||
|
||||
print()
|
||||
|
||||
print("="*60)
|
||||
print("TEST COMPLETED")
|
||||
print("="*60)
|
||||
print(f"\n✓ Total forms found: {len(leadgen_forms)}")
|
||||
print(f"✓ Total leads retrieved: {total_leads}")
|
||||
|
||||
if total_leads == 0:
|
||||
print("\n⚠️ No leads were retrieved. This could mean:")
|
||||
print(" 1. The forms have no leads yet")
|
||||
print(" 2. Your access token needs 'leads_retrieval' permission")
|
||||
print(" 3. You need to request advanced access for leads_retrieval")
|
||||
print("\nRequired permissions:")
|
||||
print(" - pages_manage_ads")
|
||||
print(" - pages_read_engagement")
|
||||
print(" - leads_retrieval")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
"""Entry point for the test script."""
|
||||
exit_code = asyncio.run(test_page_leads())
|
||||
exit(exit_code)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,108 +0,0 @@
|
||||
"""
|
||||
Test to diagnose if the rate limiter is causing campaign name issues.
|
||||
"""
|
||||
|
||||
import os
|
||||
import asyncio
|
||||
from dotenv import load_dotenv
|
||||
from facebook_business.api import FacebookAdsApi
|
||||
from facebook_business.adobjects.adaccount import AdAccount
|
||||
from facebook_business.adobjects.campaign import Campaign
|
||||
|
||||
from rate_limiter import MetaRateLimiter
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
|
||||
# Initialize Facebook Ads API
|
||||
access_token = os.getenv("META_ACCESS_TOKEN")
|
||||
app_secret = os.getenv("META_APP_SECRET")
|
||||
app_id = os.getenv("META_APP_ID")
|
||||
|
||||
if not all([access_token, app_secret, app_id]):
|
||||
raise ValueError("Missing required environment variables")
|
||||
|
||||
FacebookAdsApi.init(
|
||||
app_id=app_id,
|
||||
app_secret=app_secret,
|
||||
access_token=access_token,
|
||||
)
|
||||
|
||||
account_id = "act_238334370765317"
|
||||
|
||||
# Initialize rate limiter
|
||||
rate_limiter = MetaRateLimiter(
|
||||
base_delay=0.1, # Fast for testing
|
||||
throttle_threshold=75.0,
|
||||
max_retry_delay=300.0,
|
||||
max_retries=5,
|
||||
)
|
||||
|
||||
|
||||
async def _rate_limited_request(func, *args, **kwargs):
|
||||
"""Execute a request with rate limiting (same as in scheduled_grabber.py)."""
|
||||
return await rate_limiter.execute_with_retry(func, *args, **kwargs)
|
||||
|
||||
|
||||
async def test_direct_fetch():
|
||||
"""Test direct fetch without rate limiter."""
|
||||
print("1. DIRECT FETCH (no rate limiter)")
|
||||
print("=" * 60)
|
||||
|
||||
ad_account = AdAccount(account_id)
|
||||
campaigns = ad_account.get_campaigns(
|
||||
fields=[
|
||||
Campaign.Field.name,
|
||||
Campaign.Field.status,
|
||||
Campaign.Field.objective,
|
||||
],
|
||||
params={'limit': 5}
|
||||
)
|
||||
|
||||
print(f"Type of campaigns: {type(campaigns)}")
|
||||
print(f"Campaigns object: {campaigns}\n")
|
||||
|
||||
for i, campaign in enumerate(campaigns, 1):
|
||||
campaign_dict = dict(campaign)
|
||||
print(f"Campaign {i}:")
|
||||
print(f" ID: {campaign_dict.get('id')}")
|
||||
print(f" Name: {campaign_dict.get('name', '❌ MISSING')}")
|
||||
print(f" Keys in dict: {list(campaign_dict.keys())}")
|
||||
print()
|
||||
|
||||
|
||||
async def test_rate_limited_fetch():
|
||||
"""Test fetch WITH rate limiter."""
|
||||
print("\n2. RATE LIMITED FETCH (with rate limiter)")
|
||||
print("=" * 60)
|
||||
|
||||
ad_account = AdAccount(account_id)
|
||||
campaigns = await _rate_limited_request(
|
||||
ad_account.get_campaigns,
|
||||
fields=[
|
||||
Campaign.Field.name,
|
||||
Campaign.Field.status,
|
||||
Campaign.Field.objective,
|
||||
],
|
||||
params={'limit': 5}
|
||||
)
|
||||
|
||||
print(f"Type of campaigns: {type(campaigns)}")
|
||||
print(f"Campaigns object: {campaigns}\n")
|
||||
|
||||
for i, campaign in enumerate(campaigns, 1):
|
||||
campaign_dict = dict(campaign)
|
||||
print(f"Campaign {i}:")
|
||||
print(f" ID: {campaign_dict.get('id')}")
|
||||
print(f" Name: {campaign_dict.get('name', '❌ MISSING')}")
|
||||
print(f" Keys in dict: {list(campaign_dict.keys())}")
|
||||
print()
|
||||
|
||||
|
||||
async def main():
|
||||
await test_direct_fetch()
|
||||
await test_rate_limited_fetch()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -1,87 +0,0 @@
|
||||
"""
|
||||
Test script to check what date_start and date_stop look like for "today" preset.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
from dotenv import load_dotenv
|
||||
from facebook_business.adobjects.adaccount import AdAccount
|
||||
from facebook_business.adobjects.adsinsights import AdsInsights
|
||||
from facebook_business.api import FacebookAdsApi
|
||||
|
||||
|
||||
def test_today_preset():
|
||||
"""Test the 'today' date preset to see date_start and date_stop values."""
|
||||
load_dotenv()
|
||||
|
||||
access_token = os.getenv("META_ACCESS_TOKEN")
|
||||
app_secret = os.getenv("META_APP_SECRET")
|
||||
app_id = os.getenv("META_APP_ID")
|
||||
|
||||
if not all([access_token, app_secret, app_id]):
|
||||
print("❌ Missing required environment variables")
|
||||
return 1
|
||||
|
||||
# Initialize Facebook Ads API
|
||||
FacebookAdsApi.init(
|
||||
app_id=app_id,
|
||||
app_secret=app_secret,
|
||||
access_token=access_token,
|
||||
)
|
||||
|
||||
# Use the first account we know exists
|
||||
account_id = "act_238334370765317"
|
||||
ad_account = AdAccount(account_id)
|
||||
|
||||
print("="*70)
|
||||
print("TESTING DATE_PRESET='TODAY'")
|
||||
print("="*70)
|
||||
print(f"Account: {account_id}")
|
||||
print()
|
||||
|
||||
# Request with date_preset="today"
|
||||
fields = [
|
||||
AdsInsights.Field.impressions,
|
||||
AdsInsights.Field.clicks,
|
||||
AdsInsights.Field.spend,
|
||||
AdsInsights.Field.date_start,
|
||||
AdsInsights.Field.date_stop,
|
||||
]
|
||||
|
||||
params = {
|
||||
"date_preset": "today",
|
||||
"level": "account",
|
||||
}
|
||||
|
||||
print("Making API request with date_preset='today'...")
|
||||
insights = ad_account.get_insights(fields=fields, params=params)
|
||||
|
||||
print("\nResponse:")
|
||||
print("-"*70)
|
||||
for insight in insights:
|
||||
insight_dict = dict(insight)
|
||||
print(json.dumps(insight_dict, indent=2))
|
||||
print()
|
||||
print("Key observations:")
|
||||
print(f" date_start: {insight_dict.get('date_start')}")
|
||||
print(f" date_stop: {insight_dict.get('date_stop')}")
|
||||
print(f" Are they the same? {insight_dict.get('date_start') == insight_dict.get('date_stop')}")
|
||||
print(f" impressions: {insight_dict.get('impressions')}")
|
||||
print(f" spend: {insight_dict.get('spend')}")
|
||||
|
||||
print()
|
||||
print("="*70)
|
||||
print("CONCLUSION")
|
||||
print("="*70)
|
||||
print("For 'today' preset:")
|
||||
print(" - date_start and date_stop should both be today's date")
|
||||
print(" - Metrics are cumulative from midnight to now")
|
||||
print(" - Multiple collections during the day will have same dates")
|
||||
print(" but increasing metric values")
|
||||
print()
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit(test_today_preset())
|
||||
@@ -1,307 +0,0 @@
|
||||
"""
|
||||
Token manager for automatic refresh of Meta access tokens.
|
||||
|
||||
Handles:
|
||||
- Loading token metadata
|
||||
- Checking token validity and expiry
|
||||
- Automatic refresh before expiry
|
||||
- Persistence of new tokens
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from .auth import MetaOAuth2
|
||||
|
||||
|
||||
class MetaTokenManager:
|
||||
"""
|
||||
Manages Meta access tokens with automatic refresh.
|
||||
|
||||
Features:
|
||||
- Loads token from .env and metadata from .meta_token.json
|
||||
- Checks if token is expired or about to expire
|
||||
- Automatically refreshes tokens before expiry
|
||||
- Persists refreshed tokens
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
token_file: str = ".meta_token.json",
|
||||
refresh_before_days: int = 7,
|
||||
):
|
||||
"""
|
||||
Initialize token manager.
|
||||
|
||||
Args:
|
||||
token_file: Path to token metadata JSON file
|
||||
refresh_before_days: Refresh token this many days before expiry
|
||||
"""
|
||||
load_dotenv()
|
||||
|
||||
self.token_file = Path(token_file)
|
||||
self.refresh_before_days = refresh_before_days
|
||||
self.oauth = MetaOAuth2()
|
||||
|
||||
self._current_token: Optional[str] = None
|
||||
self._token_metadata: Optional[dict] = None
|
||||
|
||||
def load_token(self) -> Optional[str]:
|
||||
"""
|
||||
Load access token from environment.
|
||||
|
||||
Returns:
|
||||
Access token or None
|
||||
"""
|
||||
return os.getenv("META_ACCESS_TOKEN")
|
||||
|
||||
def load_metadata(self) -> Optional[dict]:
|
||||
"""
|
||||
Load token metadata from JSON file.
|
||||
|
||||
Returns:
|
||||
Token metadata dict or None
|
||||
"""
|
||||
if not self.token_file.exists():
|
||||
return None
|
||||
|
||||
try:
|
||||
return json.loads(self.token_file.read_text())
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not load token metadata: {e}")
|
||||
return None
|
||||
|
||||
def is_token_expired(self, metadata: dict) -> bool:
|
||||
"""
|
||||
Check if token is expired.
|
||||
|
||||
Args:
|
||||
metadata: Token metadata dictionary
|
||||
|
||||
Returns:
|
||||
True if expired
|
||||
"""
|
||||
expires_at = metadata.get("expires_at", 0)
|
||||
if not expires_at:
|
||||
return False
|
||||
|
||||
return time.time() >= expires_at
|
||||
|
||||
def should_refresh(self, metadata: dict) -> bool:
|
||||
"""
|
||||
Check if token should be refreshed.
|
||||
|
||||
Args:
|
||||
metadata: Token metadata dictionary
|
||||
|
||||
Returns:
|
||||
True if token should be refreshed
|
||||
"""
|
||||
expires_at = metadata.get("expires_at", 0)
|
||||
if not expires_at:
|
||||
return False
|
||||
|
||||
# Refresh if expiring within refresh_before_days
|
||||
threshold = time.time() + (self.refresh_before_days * 86400)
|
||||
return expires_at <= threshold
|
||||
|
||||
def refresh_token(self, current_token: str) -> str:
|
||||
"""
|
||||
Refresh token by exchanging for a new long-lived token.
|
||||
|
||||
Args:
|
||||
current_token: Current access token
|
||||
|
||||
Returns:
|
||||
New access token
|
||||
"""
|
||||
print("\n🔄 Refreshing access token...")
|
||||
|
||||
# Exchange current token for new long-lived token
|
||||
token_data = self.oauth.exchange_for_long_lived_token(current_token)
|
||||
new_token = token_data["access_token"]
|
||||
expires_in = token_data["expires_in"]
|
||||
|
||||
print(f"✅ Token refreshed! Valid for {expires_in / 86400:.0f} days")
|
||||
|
||||
# Get token info
|
||||
try:
|
||||
token_info = self.oauth.get_token_info(new_token)
|
||||
expires_at = token_info.get("expires_at", int(time.time()) + expires_in)
|
||||
is_valid = token_info.get("is_valid", True)
|
||||
except Exception:
|
||||
expires_at = int(time.time()) + expires_in
|
||||
is_valid = True
|
||||
|
||||
# Save new token
|
||||
self._save_token(new_token, expires_at, is_valid)
|
||||
|
||||
return new_token
|
||||
|
||||
def _save_token(self, access_token: str, expires_at: int, is_valid: bool):
|
||||
"""
|
||||
Save token to .env and metadata to JSON.
|
||||
|
||||
Args:
|
||||
access_token: New access token
|
||||
expires_at: Expiry timestamp
|
||||
is_valid: Whether token is valid
|
||||
"""
|
||||
# Update .env
|
||||
env_path = Path(".env")
|
||||
if env_path.exists():
|
||||
env_content = env_path.read_text()
|
||||
lines = env_content.split("\n")
|
||||
|
||||
updated = False
|
||||
for i, line in enumerate(lines):
|
||||
if line.startswith("META_ACCESS_TOKEN="):
|
||||
lines[i] = f"META_ACCESS_TOKEN={access_token}"
|
||||
updated = True
|
||||
break
|
||||
|
||||
if not updated:
|
||||
lines.append(f"META_ACCESS_TOKEN={access_token}")
|
||||
|
||||
env_path.write_text("\n".join(lines))
|
||||
|
||||
# Update metadata JSON
|
||||
metadata = {
|
||||
"access_token": access_token,
|
||||
"expires_at": expires_at,
|
||||
"issued_at": int(time.time()),
|
||||
"is_valid": is_valid,
|
||||
"updated_at": int(time.time()),
|
||||
}
|
||||
|
||||
self.token_file.write_text(json.dumps(metadata, indent=2))
|
||||
|
||||
# Reload environment
|
||||
load_dotenv(override=True)
|
||||
|
||||
def get_valid_token(self) -> str:
|
||||
"""
|
||||
Get a valid access token, refreshing if necessary.
|
||||
|
||||
Returns:
|
||||
Valid access token
|
||||
|
||||
Raises:
|
||||
ValueError: If no token available or refresh fails
|
||||
"""
|
||||
# Load token and metadata
|
||||
token = self.load_token()
|
||||
metadata = self.load_metadata()
|
||||
|
||||
if not token:
|
||||
raise ValueError(
|
||||
"No access token found. Run 'uv run python src/meta_api_grabber/auth.py' to authenticate."
|
||||
)
|
||||
|
||||
# If no metadata, assume token is valid (but warn)
|
||||
if not metadata:
|
||||
print("⚠️ Warning: No token metadata found. Cannot check expiry.")
|
||||
print(" Run 'uv run python src/meta_api_grabber/auth.py' to re-authenticate and save metadata.")
|
||||
return token
|
||||
|
||||
# Check if expired
|
||||
if self.is_token_expired(metadata):
|
||||
print("❌ Token expired! Attempting to refresh...")
|
||||
try:
|
||||
return self.refresh_token(token)
|
||||
except Exception as e:
|
||||
raise ValueError(
|
||||
f"Token expired and refresh failed: {e}\n"
|
||||
"Please re-authenticate: uv run python src/meta_api_grabber/auth.py"
|
||||
)
|
||||
|
||||
# Check if should refresh (within threshold)
|
||||
if self.should_refresh(metadata):
|
||||
expires_at = metadata.get("expires_at", 0)
|
||||
expires_dt = datetime.fromtimestamp(expires_at)
|
||||
days_left = (expires_dt - datetime.now()).days
|
||||
|
||||
print(f"\n⚠️ Token expiring in {days_left} days ({expires_dt.strftime('%Y-%m-%d')})")
|
||||
print(f" Refreshing token to extend validity...")
|
||||
|
||||
try:
|
||||
return self.refresh_token(token)
|
||||
except Exception as e:
|
||||
print(f"⚠️ Token refresh failed: {e}")
|
||||
print(f" Continuing with current token ({days_left} days remaining)")
|
||||
return token
|
||||
|
||||
# Token is valid
|
||||
expires_at = metadata.get("expires_at", 0)
|
||||
if expires_at:
|
||||
expires_dt = datetime.fromtimestamp(expires_at)
|
||||
days_left = (expires_dt - datetime.now()).days
|
||||
print(f"✅ Token valid ({days_left} days remaining)")
|
||||
|
||||
return token
|
||||
|
||||
def print_token_status(self):
|
||||
"""Print current token status."""
|
||||
token = self.load_token()
|
||||
metadata = self.load_metadata()
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("TOKEN STATUS")
|
||||
print("="*60)
|
||||
|
||||
if not token:
|
||||
print("❌ No access token found in .env")
|
||||
print("\nRun: uv run python src/meta_api_grabber/auth.py")
|
||||
return
|
||||
|
||||
print("✅ Access token found")
|
||||
|
||||
if not metadata:
|
||||
print("⚠️ No metadata file (.meta_token.json)")
|
||||
print(" Cannot determine expiry. Re-authenticate to save metadata.")
|
||||
return
|
||||
|
||||
expires_at = metadata.get("expires_at", 0)
|
||||
is_valid = metadata.get("is_valid", False)
|
||||
|
||||
if expires_at:
|
||||
expires_dt = datetime.fromtimestamp(expires_at)
|
||||
days_left = (expires_dt - datetime.now()).days
|
||||
|
||||
print(f"\nExpires: {expires_dt.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
print(f"Days remaining: {days_left}")
|
||||
|
||||
if days_left < 0:
|
||||
print("Status: ❌ EXPIRED")
|
||||
elif days_left <= self.refresh_before_days:
|
||||
print(f"Status: ⚠️ EXPIRING SOON (will auto-refresh)")
|
||||
else:
|
||||
print("Status: ✅ VALID")
|
||||
|
||||
print(f"Is valid: {'✅' if is_valid else '❌'}")
|
||||
print("="*60 + "\n")
|
||||
|
||||
|
||||
def main():
|
||||
"""Check token status and refresh if needed."""
|
||||
manager = MetaTokenManager()
|
||||
manager.print_token_status()
|
||||
|
||||
try:
|
||||
token = manager.get_valid_token()
|
||||
print(f"\n✅ Valid token ready for use")
|
||||
except ValueError as e:
|
||||
print(f"\n❌ Error: {e}")
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit(main())
|
||||
@@ -1,116 +0,0 @@
|
||||
"""
|
||||
View manager for TimescaleDB materialized views.
|
||||
Handles creation, updates, and refresh of materialized views for flattened insights data.
|
||||
Views are loaded from individual SQL files in the views directory.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import pathlib
|
||||
from typing import List, Optional
|
||||
|
||||
import asyncpg
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ViewManager:
|
||||
"""Manages materialized views for insights data flattening."""
|
||||
|
||||
def __init__(self, pool: asyncpg.Pool):
|
||||
"""
|
||||
Initialize view manager with a database connection pool.
|
||||
|
||||
Args:
|
||||
pool: asyncpg connection pool
|
||||
"""
|
||||
self.pool = pool
|
||||
self.views_dir = pathlib.Path(__file__).parent / "views"
|
||||
|
||||
async def initialize_views(self) -> None:
|
||||
"""
|
||||
Initialize all materialized views at startup.
|
||||
Loads and executes SQL files from the views directory in alphabetical order.
|
||||
Creates views if they don't exist, idempotent operation.
|
||||
"""
|
||||
logger.info("Initializing materialized views...")
|
||||
|
||||
if not self.views_dir.exists():
|
||||
logger.warning(f"Views directory not found at {self.views_dir}")
|
||||
return
|
||||
|
||||
# Get all .sql files in alphabetical order for consistent execution
|
||||
view_files = sorted(self.views_dir.glob("*.sql"))
|
||||
if not view_files:
|
||||
logger.warning(f"No SQL files found in {self.views_dir}")
|
||||
return
|
||||
|
||||
async with self.pool.acquire() as conn:
|
||||
for view_file in view_files:
|
||||
logger.debug(f"Loading view file: {view_file.name}")
|
||||
await self._execute_view_file(conn, view_file)
|
||||
|
||||
logger.info("✓ Materialized views initialized successfully")
|
||||
|
||||
async def _execute_view_file(self, conn: asyncpg.Connection, view_file: pathlib.Path) -> None:
|
||||
"""
|
||||
Execute SQL statements from a view file.
|
||||
|
||||
Args:
|
||||
conn: asyncpg connection
|
||||
view_file: Path to SQL file
|
||||
"""
|
||||
with open(view_file, 'r') as f:
|
||||
view_sql = f.read()
|
||||
|
||||
statements = [s.strip() for s in view_sql.split(';') if s.strip()]
|
||||
|
||||
for i, stmt in enumerate(statements, 1):
|
||||
if not stmt:
|
||||
continue
|
||||
|
||||
try:
|
||||
await conn.execute(stmt)
|
||||
logger.debug(f"{view_file.name}: Executed statement {i}")
|
||||
except Exception as e:
|
||||
error_msg = str(e).lower()
|
||||
if "does not exist" in error_msg:
|
||||
# Could be a missing dependent view or table, log it
|
||||
logger.debug(f"{view_file.name}: View or table does not exist (statement {i})")
|
||||
else:
|
||||
# Log other errors but don't fail - could be incompatible schema changes
|
||||
logger.warning(f"{view_file.name}: Error in statement {i}: {e}")
|
||||
|
||||
async def refresh_views(self, view_names: Optional[List[str]] = None) -> None:
|
||||
"""
|
||||
Refresh specified materialized views.
|
||||
|
||||
Args:
|
||||
view_names: List of view names to refresh. If None, refreshes all views.
|
||||
"""
|
||||
if view_names is None:
|
||||
view_names = [
|
||||
"adset_insights_flattened",
|
||||
"account_insights_flattened",
|
||||
"campaign_insights_flattened",
|
||||
"campaign_insights_by_country_flattened",
|
||||
]
|
||||
|
||||
async with self.pool.acquire() as conn:
|
||||
for view_name in view_names:
|
||||
try:
|
||||
# Use CONCURRENTLY to avoid locking
|
||||
await conn.execute(
|
||||
f"REFRESH MATERIALIZED VIEW CONCURRENTLY {view_name};"
|
||||
)
|
||||
logger.debug(f"Refreshed materialized view: {view_name}")
|
||||
except Exception as e:
|
||||
error_msg = str(e).lower()
|
||||
# View might not exist if not initialized, that's okay
|
||||
if "does not exist" in error_msg:
|
||||
logger.debug(f"View does not exist, skipping refresh: {view_name}")
|
||||
else:
|
||||
logger.warning(f"Error refreshing view {view_name}: {e}")
|
||||
|
||||
async def refresh_all_views(self) -> None:
|
||||
"""Refresh all materialized views."""
|
||||
await self.refresh_views()
|
||||
20
src/meta_api_grabber/views/account_insights_by_device.sql
Normal file
20
src/meta_api_grabber/views/account_insights_by_device.sql
Normal file
@@ -0,0 +1,20 @@
|
||||
--- account insights by gender
|
||||
|
||||
|
||||
DROP VIEW IF EXISTS account_insights_by_device CASCADE;
|
||||
|
||||
CREATE VIEW account_insights_by_device AS
|
||||
SELECT
|
||||
time,
|
||||
account_id,
|
||||
device_platform,
|
||||
SUM(impressions) AS impressions,
|
||||
SUM(clicks) AS clicks,
|
||||
SUM(spend) AS spend,
|
||||
SUM(link_click) AS link_click,
|
||||
SUM(landing_page_view) AS landing_page_view,
|
||||
SUM(lead) AS lead
|
||||
FROM campaign_insights_by_device_flattened
|
||||
GROUP BY time, account_id, device_platform;
|
||||
|
||||
|
||||
53
src/meta_api_grabber/views/account_insights_by_gender.sql
Normal file
53
src/meta_api_grabber/views/account_insights_by_gender.sql
Normal file
@@ -0,0 +1,53 @@
|
||||
|
||||
|
||||
|
||||
DROP VIEW IF EXISTS account_insights_by_gender CASCADE;
|
||||
|
||||
CREATE VIEW account_insights_by_gender AS
|
||||
SELECT
|
||||
time,
|
||||
account_id,
|
||||
gender,
|
||||
SUM(impressions) AS impressions,
|
||||
SUM(clicks) AS clicks,
|
||||
SUM(spend) AS spend,
|
||||
SUM(link_click) AS link_click,
|
||||
SUM(landing_page_view) AS landing_page_view,
|
||||
SUM(lead) AS lead
|
||||
FROM campaign_insights_by_gender
|
||||
GROUP BY time, account_id, gender;
|
||||
|
||||
|
||||
DROP VIEW IF EXISTS account_insights_by_age CASCADE;
|
||||
|
||||
CREATE VIEW account_insights_by_age AS
|
||||
SELECT
|
||||
time,
|
||||
account_id,
|
||||
age,
|
||||
SUM(impressions) AS impressions,
|
||||
SUM(clicks) AS clicks,
|
||||
SUM(spend) AS spend,
|
||||
SUM(link_click) AS link_click,
|
||||
SUM(landing_page_view) AS landing_page_view,
|
||||
SUM(lead) AS lead
|
||||
FROM campaign_insights_by_age
|
||||
GROUP BY time, account_id, age;
|
||||
|
||||
DROP VIEW IF EXISTS account_insights_by_gender_and_age CASCADE;
|
||||
|
||||
CREATE VIEW account_insights_by_gender_and_age AS
|
||||
SELECT
|
||||
time,
|
||||
account_id,
|
||||
gender,
|
||||
age,
|
||||
SUM(impressions) AS impressions,
|
||||
SUM(clicks) AS clicks,
|
||||
SUM(spend) AS spend,
|
||||
SUM(link_click) AS link_click,
|
||||
SUM(landing_page_view) AS landing_page_view,
|
||||
SUM(lead) AS lead
|
||||
FROM campaign_insights_by_gender_and_age
|
||||
GROUP BY time, account_id, age, gender;
|
||||
|
||||
55
src/meta_api_grabber/views/account_insights_google.sql
Normal file
55
src/meta_api_grabber/views/account_insights_google.sql
Normal file
@@ -0,0 +1,55 @@
|
||||
DROP VIEW IF EXISTS g_account_insights CASCADE;
|
||||
CREATE VIEW g_account_insights AS
|
||||
SELECT
|
||||
time,
|
||||
account_id,
|
||||
clicks,
|
||||
impressions,
|
||||
interactions,
|
||||
cost_micros,
|
||||
cost_micros / 1000000.0 as cost,
|
||||
leads,
|
||||
engagements,
|
||||
customer_currency_code,
|
||||
account_name,
|
||||
|
||||
-- CTR (Click-Through Rate)
|
||||
(clicks::numeric / impressions_nz) * 100 as ctr,
|
||||
|
||||
-- CPM (Cost Per Mille) in micros and standard units
|
||||
(cost_micros::numeric / impressions_nz) * 1000 as cpm_micros,
|
||||
(cost_micros::numeric / impressions_nz) * 1000 / 1000000.0 as cpm,
|
||||
|
||||
-- CPC (Cost Per Click) in micros and standard units
|
||||
cost_micros::numeric / clicks_nz as cpc_micros,
|
||||
cost_micros::numeric / clicks_nz / 1000000.0 as cpc,
|
||||
|
||||
-- CPL (Cost Per Lead) in micros and standard units
|
||||
cost_micros::numeric / leads_nz as cpl_micros,
|
||||
cost_micros::numeric / leads_nz / 1000000.0 as cpl,
|
||||
|
||||
-- Conversion Rate
|
||||
(leads::numeric / clicks_nz) * 100 as conversion_rate,
|
||||
|
||||
-- Engagement Rate
|
||||
(engagements::numeric / impressions_nz) * 100 as engagement_rate
|
||||
|
||||
FROM (
|
||||
SELECT
|
||||
segments_date as time,
|
||||
customer_id as account_id,
|
||||
sum(metrics_clicks) as clicks,
|
||||
sum(metrics_impressions) as impressions,
|
||||
sum(metrics_interactions) as interactions,
|
||||
sum(metrics_cost_micros) as cost_micros,
|
||||
sum(metrics_conversions) as leads,
|
||||
sum(metrics_engagements) as engagements,
|
||||
customer_currency_code,
|
||||
customer_descriptive_name as account_name,
|
||||
-- Null-safe denominators
|
||||
NULLIF(sum(metrics_clicks), 0) as clicks_nz,
|
||||
NULLIF(sum(metrics_impressions), 0) as impressions_nz,
|
||||
NULLIF(sum(metrics_conversions), 0) as leads_nz
|
||||
FROM google.account_performance_report
|
||||
GROUP BY account_id, time, customer_currency_code, account_name
|
||||
) base;
|
||||
@@ -35,4 +35,22 @@ CREATE INDEX idx_adset_insights_flat_date ON adset_insights_flattened(date_start
|
||||
|
||||
|
||||
CREATE UNIQUE INDEX idx_adset_insights_flat_unique ON adset_insights_flattened(time, adset_id);
|
||||
REFRESH MATERIALIZED VIEW CONCURRENTLY adset_insights_flattened;
|
||||
REFRESH MATERIALIZED VIEW CONCURRENTLY adset_insights_flattened;
|
||||
|
||||
|
||||
SELECT
|
||||
date_start as time,
|
||||
account_id,
|
||||
adset_id,
|
||||
campaign_id,
|
||||
SUM(impressions) AS impressions,
|
||||
SUM(clicks) AS clicks,
|
||||
SUM(spend) AS spend,
|
||||
AVG(frequency) as frequency,
|
||||
avg(cpc) as cpc,
|
||||
avg(cpm) as cpm,
|
||||
avg(cpp) as cpp,
|
||||
avg(ctr) as ctr
|
||||
|
||||
FROM meta.ads_insights
|
||||
group by time, account_id, adset_id, campaign_id
|
||||
@@ -3,10 +3,9 @@
|
||||
DROP MATERIALIZED VIEW IF EXISTS campaign_insights_flattened CASCADE;
|
||||
|
||||
CREATE MATERIALIZED VIEW campaign_insights_flattened AS
|
||||
SELECT
|
||||
time,
|
||||
account_id,
|
||||
campaign_id,
|
||||
SELECT date_start AS "time",
|
||||
concat('act_', account_id) AS account_id,
|
||||
campaign_id,
|
||||
impressions,
|
||||
clicks,
|
||||
spend,
|
||||
@@ -14,25 +13,21 @@ SELECT
|
||||
ctr,
|
||||
cpc,
|
||||
cpm,
|
||||
date_preset,
|
||||
date_start,
|
||||
date_stop,
|
||||
fetched_at,
|
||||
(SELECT (value->>'value')::numeric
|
||||
FROM jsonb_array_elements(actions)
|
||||
WHERE value->>'action_type' = 'link_click') AS link_click,
|
||||
(SELECT (value->>'value')::numeric
|
||||
FROM jsonb_array_elements(actions)
|
||||
WHERE value->>'action_type' = 'landing_page_view') AS landing_page_view,
|
||||
(SELECT (value->>'value')::numeric
|
||||
FROM jsonb_array_elements(actions)
|
||||
WHERE value->>'action_type' = 'lead') AS lead
|
||||
|
||||
|
||||
FROM campaign_insights;
|
||||
( SELECT (jsonb_array_elements.value ->> 'value'::text)::numeric AS "numeric"
|
||||
FROM jsonb_array_elements(customcampaign_insights.actions) jsonb_array_elements(value)
|
||||
WHERE (jsonb_array_elements.value ->> 'action_type'::text) = 'link_click'::text) AS link_click,
|
||||
( SELECT (jsonb_array_elements.value ->> 'value'::text)::numeric AS "numeric"
|
||||
FROM jsonb_array_elements(customcampaign_insights.actions) jsonb_array_elements(value)
|
||||
WHERE (jsonb_array_elements.value ->> 'action_type'::text) = 'landing_page_view'::text) AS landing_page_view,
|
||||
( SELECT (jsonb_array_elements.value ->> 'value'::text)::numeric AS "numeric"
|
||||
FROM jsonb_array_elements(customcampaign_insights.actions) jsonb_array_elements(value)
|
||||
WHERE (jsonb_array_elements.value ->> 'action_type'::text) = 'lead'::text) AS lead
|
||||
FROM meta.customcampaign_insights;
|
||||
|
||||
CREATE INDEX idx_campaign_insights_flat_date ON campaign_insights_flattened(date_start, date_stop);
|
||||
|
||||
CREATE UNIQUE INDEX idx_campaign_insights_flat_unique ON campaign_insights_flattened(time, campaign_id);
|
||||
|
||||
REFRESH MATERIALIZED VIEW CONCURRENTLY campaign_insights_flattened;
|
||||
REFRESH MATERIALIZED VIEW CONCURRENTLY campaign_insights_flattened;
|
||||
|
||||
@@ -3,22 +3,20 @@
|
||||
DROP MATERIALIZED VIEW IF EXISTS campaign_insights_by_country_flattened CASCADE;
|
||||
|
||||
CREATE MATERIALIZED VIEW campaign_insights_by_country_flattened AS
|
||||
SELECT
|
||||
time,
|
||||
account_id,
|
||||
SELECT date_start AS "time",
|
||||
concat('act_', account_id) AS account_id,
|
||||
campaign_id,
|
||||
country,
|
||||
impressions,
|
||||
clicks,
|
||||
spend,
|
||||
reach,
|
||||
frequency,
|
||||
ctr,
|
||||
cpc,
|
||||
cpm,
|
||||
date_preset,
|
||||
date_start,
|
||||
date_stop,
|
||||
fetched_at,
|
||||
(SELECT (value->>'value')::numeric
|
||||
FROM jsonb_array_elements(actions)
|
||||
WHERE value->>'action_type' = 'link_click') AS link_click,
|
||||
@@ -29,7 +27,7 @@ SELECT
|
||||
FROM jsonb_array_elements(actions)
|
||||
WHERE value->>'action_type' = 'lead') AS lead
|
||||
|
||||
FROM campaign_insights_by_country;
|
||||
FROM meta.custom_campaign_country;
|
||||
|
||||
CREATE INDEX idx_campaign_insights_by_country_flat_date ON campaign_insights_by_country_flattened(date_start, date_stop);
|
||||
|
||||
|
||||
32
src/meta_api_grabber/views/campaign_insights_by_device.sql
Normal file
32
src/meta_api_grabber/views/campaign_insights_by_device.sql
Normal file
@@ -0,0 +1,32 @@
|
||||
--- campaign insights by device
|
||||
|
||||
DROP MATERIALIZED VIEW IF EXISTS campaign_insights_by_device_flattened CASCADE;
|
||||
|
||||
CREATE MATERIALIZED VIEW campaign_insights_by_device_flattened AS
|
||||
SELECT date_start AS "time",
|
||||
concat('act_', account_id) AS account_id,
|
||||
campaign_id,
|
||||
device_platform,
|
||||
impressions,
|
||||
clicks,
|
||||
spend,
|
||||
reach,
|
||||
frequency,
|
||||
date_start,
|
||||
date_stop,
|
||||
(SELECT (value->>'value')::numeric
|
||||
FROM jsonb_array_elements(actions)
|
||||
WHERE value->>'action_type' = 'link_click') AS link_click,
|
||||
(SELECT (value->>'value')::numeric
|
||||
FROM jsonb_array_elements(actions)
|
||||
WHERE value->>'action_type' = 'landing_page_view') AS landing_page_view,
|
||||
(SELECT (value->>'value')::numeric
|
||||
FROM jsonb_array_elements(actions)
|
||||
WHERE value->>'action_type' = 'lead') AS lead
|
||||
|
||||
FROM meta.custom_campaign_device;
|
||||
|
||||
CREATE INDEX idx_campaign_insights_by_device_flat_date ON campaign_insights_by_device_flattened(date_start, date_stop);
|
||||
|
||||
CREATE UNIQUE INDEX idx_campaign_insights_by_device_flat_unique ON campaign_insights_by_device_flattened(time, campaign_id, device_platform);
|
||||
REFRESH MATERIALIZED VIEW CONCURRENTLY campaign_insights_by_device_flattened;
|
||||
71
src/meta_api_grabber/views/campaign_insights_by_gender.sql
Normal file
71
src/meta_api_grabber/views/campaign_insights_by_gender.sql
Normal file
@@ -0,0 +1,71 @@
|
||||
--- campaign insights by country
|
||||
|
||||
DROP MATERIALIZED VIEW IF EXISTS campaign_insights_by_gender_and_age CASCADE;
|
||||
|
||||
CREATE MATERIALIZED VIEW campaign_insights_by_gender_and_age AS
|
||||
SELECT date_start AS "time",
|
||||
concat('act_', account_id) AS account_id,
|
||||
campaign_id,
|
||||
gender,
|
||||
age,
|
||||
impressions,
|
||||
clicks,
|
||||
spend,
|
||||
reach,
|
||||
frequency,
|
||||
ctr,
|
||||
cpc,
|
||||
cpm,
|
||||
date_start,
|
||||
date_stop,
|
||||
(SELECT (value->>'value')::numeric
|
||||
FROM jsonb_array_elements(actions)
|
||||
WHERE value->>'action_type' = 'link_click') AS link_click,
|
||||
(SELECT (value->>'value')::numeric
|
||||
FROM jsonb_array_elements(actions)
|
||||
WHERE value->>'action_type' = 'landing_page_view') AS landing_page_view,
|
||||
(SELECT (value->>'value')::numeric
|
||||
FROM jsonb_array_elements(actions)
|
||||
WHERE value->>'action_type' = 'lead') AS lead
|
||||
|
||||
FROM meta.custom_campaign_gender;
|
||||
|
||||
CREATE INDEX idx_campaign_insights_by_gender_and_age_date ON campaign_insights_by_gender_and_age(date_start, date_stop);
|
||||
|
||||
CREATE UNIQUE INDEX idx_campaign_insights_by_gender_and_age_unique ON campaign_insights_by_gender_and_age(time, campaign_id, gender, age);
|
||||
REFRESH MATERIALIZED VIEW CONCURRENTLY campaign_insights_by_gender_and_age;
|
||||
|
||||
|
||||
DROP VIEW IF EXISTS campaign_insights_by_gender CASCADE;
|
||||
|
||||
create view campaign_insights_by_gender as
|
||||
Select time,
|
||||
sum(clicks) as clicks,
|
||||
sum(link_click) as link_click,
|
||||
sum(lead) as lead,
|
||||
sum(landing_page_view) as landing_page_view,
|
||||
sum(spend) as spend,
|
||||
sum(reach) as reach,
|
||||
sum(impressions) as impressions,
|
||||
gender,
|
||||
campaign_id,
|
||||
account_id
|
||||
from campaign_insights_by_gender_and_age
|
||||
group by time, gender, account_id, campaign_id, date_start, date_stop;
|
||||
|
||||
DROP VIEW IF EXISTS campaign_insights_by_age CASCADE;
|
||||
|
||||
create view campaign_insights_by_age as
|
||||
Select time,
|
||||
sum(clicks) as clicks,
|
||||
sum(link_click) as link_click,
|
||||
sum(lead) as lead,
|
||||
sum(landing_page_view) as landing_page_view,
|
||||
sum(spend) as spend,
|
||||
sum(reach) as reach,
|
||||
sum(impressions) as impressions,
|
||||
age,
|
||||
campaign_id,
|
||||
account_id
|
||||
from campaign_insights_by_gender_and_age
|
||||
group by time, age, account_id, campaign_id, date_start, date_stop;
|
||||
893
uv.lock
generated
893
uv.lock
generated
@@ -6,109 +6,6 @@ resolution-markers = [
|
||||
"python_full_version < '3.14'",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aiohappyeyeballs"
|
||||
version = "2.6.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aiohttp"
|
||||
version = "3.13.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohappyeyeballs" },
|
||||
{ name = "aiosignal" },
|
||||
{ name = "attrs" },
|
||||
{ name = "frozenlist" },
|
||||
{ name = "multidict" },
|
||||
{ name = "propcache" },
|
||||
{ name = "yarl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ba/fa/3ae643cd525cf6844d3dc810481e5748107368eb49563c15a5fb9f680750/aiohttp-3.13.1.tar.gz", hash = "sha256:4b7ee9c355015813a6aa085170b96ec22315dabc3d866fd77d147927000e9464", size = 7835344, upload-time = "2025-10-17T14:03:29.337Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/16/6d/d267b132342e1080f4c1bb7e1b4e96b168b3cbce931ec45780bff693ff95/aiohttp-3.13.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:55785a7f8f13df0c9ca30b5243d9909bd59f48b274262a8fe78cee0828306e5d", size = 730727, upload-time = "2025-10-17T14:00:39.681Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/c8/1cf495bac85cf71b80fad5f6d7693e84894f11b9fe876b64b0a1e7cbf32f/aiohttp-3.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bef5b83296cebb8167707b4f8d06c1805db0af632f7a72d7c5288a84667e7c3", size = 488678, upload-time = "2025-10-17T14:00:41.541Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/19/23c6b81cca587ec96943d977a58d11d05a82837022e65cd5502d665a7d11/aiohttp-3.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:27af0619c33f9ca52f06069ec05de1a357033449ab101836f431768ecfa63ff5", size = 487637, upload-time = "2025-10-17T14:00:43.527Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/58/8f9464afb88b3eed145ad7c665293739b3a6f91589694a2bb7e5778cbc72/aiohttp-3.13.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a47fe43229a8efd3764ef7728a5c1158f31cdf2a12151fe99fde81c9ac87019c", size = 1718975, upload-time = "2025-10-17T14:00:45.496Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/8b/c3da064ca392b2702f53949fd7c403afa38d9ee10bf52c6ad59a42537103/aiohttp-3.13.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6e68e126de5b46e8b2bee73cab086b5d791e7dc192056916077aa1e2e2b04437", size = 1686905, upload-time = "2025-10-17T14:00:47.707Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/a4/9c8a3843ecf526daee6010af1a66eb62579be1531d2d5af48ea6f405ad3c/aiohttp-3.13.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e65ef49dd22514329c55970d39079618a8abf856bae7147913bb774a3ab3c02f", size = 1754907, upload-time = "2025-10-17T14:00:49.702Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/80/1f470ed93e06436e3fc2659a9fc329c192fa893fb7ed4e884d399dbfb2a8/aiohttp-3.13.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0e425a7e0511648b3376839dcc9190098671a47f21a36e815b97762eb7d556b0", size = 1857129, upload-time = "2025-10-17T14:00:51.822Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/e6/33d305e6cce0a8daeb79c7d8d6547d6e5f27f4e35fa4883fc9c9eb638596/aiohttp-3.13.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:010dc9b7110f055006acd3648d5d5955bb6473b37c3663ec42a1b4cba7413e6b", size = 1738189, upload-time = "2025-10-17T14:00:53.976Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/42/8df03367e5a64327fe0c39291080697795430c438fc1139c7cc1831aa1df/aiohttp-3.13.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b5c722d0ca5f57d61066b5dfa96cdb87111e2519156b35c1f8dd17c703bee7a", size = 1553608, upload-time = "2025-10-17T14:00:56.144Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/17/6d5c73cd862f1cf29fddcbb54aac147037ff70a043a2829d03a379e95742/aiohttp-3.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:93029f0e9b77b714904a281b5aa578cdc8aa8ba018d78c04e51e1c3d8471b8ec", size = 1681809, upload-time = "2025-10-17T14:00:58.603Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/31/8926c8ab18533f6076ce28d2c329a203b58c6861681906e2d73b9c397588/aiohttp-3.13.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:d1824c7d08d8ddfc8cb10c847f696942e5aadbd16fd974dfde8bd2c3c08a9fa1", size = 1711161, upload-time = "2025-10-17T14:01:01.744Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/36/2f83e1ca730b1e0a8cf1c8ab9559834c5eec9f5da86e77ac71f0d16b521d/aiohttp-3.13.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:8f47d0ff5b3eb9c1278a2f56ea48fda667da8ebf28bd2cb378b7c453936ce003", size = 1731999, upload-time = "2025-10-17T14:01:04.626Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/ec/1f818cc368dfd4d5ab4e9efc8f2f6f283bfc31e1c06d3e848bcc862d4591/aiohttp-3.13.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8a396b1da9b51ded79806ac3b57a598f84e0769eaa1ba300655d8b5e17b70c7b", size = 1548684, upload-time = "2025-10-17T14:01:06.828Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/ad/33d36efd16e4fefee91b09a22a3a0e1b830f65471c3567ac5a8041fac812/aiohttp-3.13.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d9c52a65f54796e066b5d674e33b53178014752d28bca555c479c2c25ffcec5b", size = 1756676, upload-time = "2025-10-17T14:01:09.517Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/c4/4a526d84e77d464437713ca909364988ed2e0cd0cdad2c06cb065ece9e08/aiohttp-3.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a89da72d18d6c95a653470b78d8ee5aa3c4b37212004c103403d0776cbea6ff0", size = 1715577, upload-time = "2025-10-17T14:01:11.958Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/21/e39638b7d9c7f1362c4113a91870f89287e60a7ea2d037e258b81e8b37d5/aiohttp-3.13.1-cp313-cp313-win32.whl", hash = "sha256:02e0258b7585ddf5d01c79c716ddd674386bfbf3041fbbfe7bdf9c7c32eb4a9b", size = 424468, upload-time = "2025-10-17T14:01:14.344Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/00/f3a92c592a845ebb2f47d102a67f35f0925cb854c5e7386f1a3a1fdff2ab/aiohttp-3.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:ef56ffe60e8d97baac123272bde1ab889ee07d3419606fae823c80c2b86c403e", size = 450806, upload-time = "2025-10-17T14:01:16.437Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/be/0f6c41d2fd0aab0af133c509cabaf5b1d78eab882cb0ceb872e87ceeabf7/aiohttp-3.13.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:77f83b3dc5870a2ea79a0fcfdcc3fc398187ec1675ff61ec2ceccad27ecbd303", size = 733828, upload-time = "2025-10-17T14:01:18.58Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/14/24e2ac5efa76ae30e05813e0f50737005fd52da8ddffee474d4a5e7f38a6/aiohttp-3.13.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:9cafd2609ebb755e47323306c7666283fbba6cf82b5f19982ea627db907df23a", size = 489320, upload-time = "2025-10-17T14:01:20.644Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/5a/4cbe599358d05ea7db4869aff44707b57d13f01724d48123dc68b3288d5a/aiohttp-3.13.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9c489309a2ca548d5f11131cfb4092f61d67954f930bba7e413bcdbbb82d7fae", size = 489899, upload-time = "2025-10-17T14:01:22.638Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/96/3aec9d9cfc723273d4386328a1e2562cf23629d2f57d137047c49adb2afb/aiohttp-3.13.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79ac15fe5fdbf3c186aa74b656cd436d9a1e492ba036db8901c75717055a5b1c", size = 1716556, upload-time = "2025-10-17T14:01:25.406Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/99/39a3d250595b5c8172843831221fa5662884f63f8005b00b4034f2a7a836/aiohttp-3.13.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:095414be94fce3bc080684b4cd50fb70d439bc4662b2a1984f45f3bf9ede08aa", size = 1665814, upload-time = "2025-10-17T14:01:27.683Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/96/8319e7060a85db14a9c178bc7b3cf17fad458db32ba6d2910de3ca71452d/aiohttp-3.13.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c68172e1a2dca65fa1272c85ca72e802d78b67812b22827df01017a15c5089fa", size = 1755767, upload-time = "2025-10-17T14:01:29.914Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/c6/0a2b3d886b40aa740fa2294cd34ed46d2e8108696748492be722e23082a7/aiohttp-3.13.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3751f9212bcd119944d4ea9de6a3f0fee288c177b8ca55442a2cdff0c8201eb3", size = 1836591, upload-time = "2025-10-17T14:01:32.28Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/34/8ab5904b3331c91a58507234a1e2f662f837e193741609ee5832eb436251/aiohttp-3.13.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8619dca57d98a8353abdc7a1eeb415548952b39d6676def70d9ce76d41a046a9", size = 1714915, upload-time = "2025-10-17T14:01:35.138Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/d3/d36077ca5f447649112189074ac6c192a666bf68165b693e48c23b0d008c/aiohttp-3.13.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:97795a0cb0a5f8a843759620e9cbd8889f8079551f5dcf1ccd99ed2f056d9632", size = 1546579, upload-time = "2025-10-17T14:01:38.237Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/14/dbc426a1bb1305c4fc78ce69323498c9e7c699983366ef676aa5d3f949fa/aiohttp-3.13.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1060e058da8f9f28a7026cdfca9fc886e45e551a658f6a5c631188f72a3736d2", size = 1680633, upload-time = "2025-10-17T14:01:40.902Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/83/1e68e519aff9f3ef6d4acb6cdda7b5f592ef5c67c8f095dc0d8e06ce1c3e/aiohttp-3.13.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:f48a2c26333659101ef214907d29a76fe22ad7e912aa1e40aeffdff5e8180977", size = 1678675, upload-time = "2025-10-17T14:01:43.779Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/b9/7f3e32a81c08b6d29ea15060c377e1f038ad96cd9923a85f30e817afff22/aiohttp-3.13.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f1dfad638b9c91ff225162b2824db0e99ae2d1abe0dc7272b5919701f0a1e685", size = 1726829, upload-time = "2025-10-17T14:01:46.546Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/ce/610b1f77525a0a46639aea91377b12348e9f9412cc5ddcb17502aa4681c7/aiohttp-3.13.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:8fa09ab6dd567cb105db4e8ac4d60f377a7a94f67cf669cac79982f626360f32", size = 1542985, upload-time = "2025-10-17T14:01:49.082Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/39/3ac8dfdad5de38c401846fa071fcd24cb3b88ccfb024854df6cbd9b4a07e/aiohttp-3.13.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4159fae827f9b5f655538a4f99b7cbc3a2187e5ca2eee82f876ef1da802ccfa9", size = 1741556, upload-time = "2025-10-17T14:01:51.846Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/48/b1948b74fea7930b0f29595d1956842324336de200593d49a51a40607fdc/aiohttp-3.13.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ad671118c19e9cfafe81a7a05c294449fe0ebb0d0c6d5bb445cd2190023f5cef", size = 1696175, upload-time = "2025-10-17T14:01:54.232Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/26/063bba38e4b27b640f56cc89fe83cc3546a7ae162c2e30ca345f0ccdc3d1/aiohttp-3.13.1-cp314-cp314-win32.whl", hash = "sha256:c5c970c148c48cf6acb65224ca3c87a47f74436362dde75c27bc44155ccf7dfc", size = 430254, upload-time = "2025-10-17T14:01:56.451Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/aa/25fd764384dc4eab714023112d3548a8dd69a058840d61d816ea736097a2/aiohttp-3.13.1-cp314-cp314-win_amd64.whl", hash = "sha256:748a00167b7a88385756fa615417d24081cba7e58c8727d2e28817068b97c18c", size = 456256, upload-time = "2025-10-17T14:01:58.752Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/9f/9ba6059de4bad25c71cd88e3da53f93e9618ea369cf875c9f924b1c167e2/aiohttp-3.13.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:390b73e99d7a1f0f658b3f626ba345b76382f3edc65f49d6385e326e777ed00e", size = 765956, upload-time = "2025-10-17T14:02:01.515Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/30/b86da68b494447d3060f45c7ebb461347535dab4af9162a9267d9d86ca31/aiohttp-3.13.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:27e83abb330e687e019173d8fc1fd6a1cf471769624cf89b1bb49131198a810a", size = 503206, upload-time = "2025-10-17T14:02:03.818Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/21/d27a506552843ff9eeb9fcc2d45f943b09eefdfdf205aab044f4f1f39f6a/aiohttp-3.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2b20eed07131adbf3e873e009c2869b16a579b236e9d4b2f211bf174d8bef44a", size = 507719, upload-time = "2025-10-17T14:02:05.947Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/23/4042230ec7e4edc7ba43d0342b5a3d2fe0222ca046933c4251a35aaf17f5/aiohttp-3.13.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:58fee9ef8477fd69e823b92cfd1f590ee388521b5ff8f97f3497e62ee0656212", size = 1862758, upload-time = "2025-10-17T14:02:08.469Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/88/525c45bea7cbb9f65df42cadb4ff69f6a0dbf95931b0ff7d1fdc40a1cb5f/aiohttp-3.13.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1f62608fcb7b3d034d5e9496bea52d94064b7b62b06edba82cd38191336bbeda", size = 1717790, upload-time = "2025-10-17T14:02:11.37Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/80/21e9b5eb77df352a5788713f37359b570a793f0473f3a72db2e46df379b9/aiohttp-3.13.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fdc4d81c3dfc999437f23e36d197e8b557a3f779625cd13efe563a9cfc2ce712", size = 1842088, upload-time = "2025-10-17T14:02:13.872Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/bf/d1738f6d63fe8b2a0ad49533911b3347f4953cd001bf3223cb7b61f18dff/aiohttp-3.13.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:601d7ec812f746fd80ff8af38eeb3f196e1bab4a4d39816ccbc94c222d23f1d0", size = 1934292, upload-time = "2025-10-17T14:02:16.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/e6/26cab509b42610ca49573f2fc2867810f72bd6a2070182256c31b14f2e98/aiohttp-3.13.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47c3f21c469b840d9609089435c0d9918ae89f41289bf7cc4afe5ff7af5458db", size = 1791328, upload-time = "2025-10-17T14:02:19.051Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/6d/baf7b462852475c9d045bee8418d9cdf280efb687752b553e82d0c58bcc2/aiohttp-3.13.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d6c6cdc0750db88520332d4aaa352221732b0cafe89fd0e42feec7cb1b5dc236", size = 1622663, upload-time = "2025-10-17T14:02:21.397Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/48/396a97318af9b5f4ca8b3dc14a67976f71c6400a9609c622f96da341453f/aiohttp-3.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:58a12299eeb1fca2414ee2bc345ac69b0f765c20b82c3ab2a75d91310d95a9f6", size = 1787791, upload-time = "2025-10-17T14:02:24.212Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/e2/6925f6784134ce3ff3ce1a8502ab366432a3b5605387618c1a939ce778d9/aiohttp-3.13.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:0989cbfc195a4de1bb48f08454ef1cb47424b937e53ed069d08404b9d3c7aea1", size = 1775459, upload-time = "2025-10-17T14:02:26.971Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/e3/b372047ba739fc39f199b99290c4cc5578ce5fd125f69168c967dac44021/aiohttp-3.13.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:feb5ee664300e2435e0d1bc3443a98925013dfaf2cae9699c1f3606b88544898", size = 1789250, upload-time = "2025-10-17T14:02:29.686Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/8c/9f48b93d7d57fc9ef2ad4adace62e4663ea1ce1753806c4872fb36b54c39/aiohttp-3.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:58a6f8702da0c3606fb5cf2e669cce0ca681d072fe830968673bb4c69eb89e88", size = 1616139, upload-time = "2025-10-17T14:02:32.151Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/c6/c64e39d61aaa33d7de1be5206c0af3ead4b369bf975dac9fdf907a4291c1/aiohttp-3.13.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a417ceb433b9d280e2368ffea22d4bc6e3e0d894c4bc7768915124d57d0964b6", size = 1815829, upload-time = "2025-10-17T14:02:34.635Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/75/e19e93965ea675f1151753b409af97a14f1d888588a555e53af1e62b83eb/aiohttp-3.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8ac8854f7b0466c5d6a9ea49249b3f6176013859ac8f4bb2522ad8ed6b94ded2", size = 1760923, upload-time = "2025-10-17T14:02:37.364Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/a4/06ed38f1dabd98ea136fd116cba1d02c9b51af5a37d513b6850a9a567d86/aiohttp-3.13.1-cp314-cp314t-win32.whl", hash = "sha256:be697a5aeff42179ed13b332a411e674994bcd406c81642d014ace90bf4bb968", size = 463318, upload-time = "2025-10-17T14:02:39.924Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/0f/27e4fdde899e1e90e35eeff56b54ed63826435ad6cdb06b09ed312d1b3fa/aiohttp-3.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f1d6aa90546a4e8f20c3500cb68ab14679cd91f927fa52970035fd3207dfb3da", size = 496721, upload-time = "2025-10-17T14:02:42.199Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aiosignal"
|
||||
version = "1.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "frozenlist" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alembic"
|
||||
version = "1.17.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mako" },
|
||||
{ name = "sqlalchemy" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6b/45/6f4555f2039f364c3ce31399529dcf48dd60726ff3715ad67f547d87dfd2/alembic-1.17.0.tar.gz", hash = "sha256:4652a0b3e19616b57d652b82bfa5e38bf5dbea0813eed971612671cb9e90c0fe", size = 1975526, upload-time = "2025-10-11T18:40:13.585Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/44/1f/38e29b06bfed7818ebba1f84904afdc8153ef7b6c7e0d8f3bc6643f5989c/alembic-1.17.0-py3-none-any.whl", hash = "sha256:80523bc437d41b35c5db7e525ad9d908f79de65c27d6a5a5eab6df348a352d99", size = 247449, upload-time = "2025-10-11T18:40:16.288Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asyncpg"
|
||||
version = "0.30.0"
|
||||
@@ -125,74 +22,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/a4/cec76b3389c4c5ff66301cd100fe88c318563ec8a520e0b2e792b5b84972/asyncpg-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:f59b430b8e27557c3fb9869222559f7417ced18688375825f8f12302c34e915e", size = 621623, upload-time = "2024-10-20T00:30:09.024Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "attrs"
|
||||
version = "25.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cachetools"
|
||||
version = "6.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cc/7e/b975b5814bd36faf009faebe22c1072a1fa1168db34d285ef0ba071ad78c/cachetools-6.2.1.tar.gz", hash = "sha256:3f391e4bd8f8bf0931169baf7456cc822705f4e2a31f840d218f445b9a854201", size = 31325, upload-time = "2025-10-12T14:55:30.139Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/96/c5/1e741d26306c42e2bf6ab740b2202872727e0f606033c9dd713f8b93f5a8/cachetools-6.2.1-py3-none-any.whl", hash = "sha256:09868944b6dde876dfd44e1d47e18484541eaf12f26f29b7af91b26cc892d701", size = 11280, upload-time = "2025-10-12T14:55:28.382Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.10.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
@@ -202,263 +31,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curlify"
|
||||
version = "3.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "requests" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/62/df/e9c2720d32c5de985ce64d5dedf883b52c26ff5e9aa2ba2becea00098320/curlify-3.0.0.tar.gz", hash = "sha256:7b488ff3c924dba3433a1cc74044c0942da21f0a97fa26c3138319ba640ca412", size = 3668, upload-time = "2025-05-25T11:50:00.785Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/f8/912ebddbff8ea603d4c90fa31557096f927b17efd30a166ce7ac1242910a/curlify-3.0.0-py3-none-any.whl", hash = "sha256:52060c0eb7a656b7bde6b668c32f337bed4d736ce230755767e3ada56a09c338", size = 3580, upload-time = "2025-05-25T11:49:59.335Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "facebook-business"
|
||||
version = "23.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
{ name = "curlify" },
|
||||
{ name = "pycountry" },
|
||||
{ name = "requests" },
|
||||
{ name = "six" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ef/56/93d9d801a6cfea902d7383d45038499508aa248afea64d7ec1939f931486/facebook_business-23.0.3.tar.gz", hash = "sha256:5e58a0b953c32fd53337c2acef11b8b889bf3a12481353deaaa30cfb67fd25b0", size = 672233, upload-time = "2025-10-14T16:16:57.808Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/09/a8/8c594fcee90b2776a28256e1a3547872b47f7813b100c064a84270be24c9/facebook_business-23.0.3-py3-none-any.whl", hash = "sha256:2beafd128d97f48d074400aa18112fff758ac1e34f8d87cb4717f200f8e7e32c", size = 1391948, upload-time = "2025-10-14T16:16:56.015Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "frozenlist"
|
||||
version = "1.8.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "google-ads"
|
||||
version = "28.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "google-api-core" },
|
||||
{ name = "google-auth-oauthlib" },
|
||||
{ name = "googleapis-common-protos" },
|
||||
{ name = "grpcio" },
|
||||
{ name = "grpcio-status" },
|
||||
{ name = "proto-plus" },
|
||||
{ name = "protobuf" },
|
||||
{ name = "pyyaml" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a7/ee/30bf06a8334333a43805050c7530637b7c308f371945e3cad7d78b4c5287/google_ads-28.3.0.tar.gz", hash = "sha256:d544e7e3792974e9dc6a016e0eb264f9218526be698c8c6b8a438717a6dcc95b", size = 9222858, upload-time = "2025-10-22T16:22:43.726Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/cf/21541e673e47582ac46b164817ff359370ed9897db977225587f5290b202/google_ads-28.3.0-py3-none-any.whl", hash = "sha256:11ec6227784a565de3ad3f0047ac82eb13c6bfca1d4a5862df9b3c63162fbb40", size = 17781520, upload-time = "2025-10-22T16:22:40.881Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "google-api-core"
|
||||
version = "2.28.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "google-auth" },
|
||||
{ name = "googleapis-common-protos" },
|
||||
{ name = "proto-plus" },
|
||||
{ name = "protobuf" },
|
||||
{ name = "requests" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/61/da/83d7043169ac2c8c7469f0e375610d78ae2160134bf1b80634c482fa079c/google_api_core-2.28.1.tar.gz", hash = "sha256:2b405df02d68e68ce0fbc138559e6036559e685159d148ae5861013dc201baf8", size = 176759, upload-time = "2025-10-28T21:34:51.529Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/d4/90197b416cb61cefd316964fd9e7bd8324bcbafabf40eef14a9f20b81974/google_api_core-2.28.1-py3-none-any.whl", hash = "sha256:4021b0f8ceb77a6fb4de6fde4502cecab45062e66ff4f2895169e0b35bc9466c", size = 173706, upload-time = "2025-10-28T21:34:50.151Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "google-auth"
|
||||
version = "2.42.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cachetools" },
|
||||
{ name = "pyasn1-modules" },
|
||||
{ name = "rsa" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/11/75/28881e9d7de9b3d61939bc9624bd8fa594eb787a00567aba87173c790f09/google_auth-2.42.0.tar.gz", hash = "sha256:9bbbeef3442586effb124d1ca032cfb8fb7acd8754ab79b55facd2b8f3ab2802", size = 295400, upload-time = "2025-10-28T17:38:08.599Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/87/24/ec82aee6ba1a076288818fe5cc5125f4d93fffdc68bb7b381c68286c8aaa/google_auth-2.42.0-py2.py3-none-any.whl", hash = "sha256:f8f944bcb9723339b0ef58a73840f3c61bc91b69bf7368464906120b55804473", size = 222550, upload-time = "2025-10-28T17:38:05.496Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "google-auth-oauthlib"
|
||||
version = "1.2.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "google-auth" },
|
||||
{ name = "requests-oauthlib" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fb/87/e10bf24f7bcffc1421b84d6f9c3377c30ec305d082cd737ddaa6d8f77f7c/google_auth_oauthlib-1.2.2.tar.gz", hash = "sha256:11046fb8d3348b296302dd939ace8af0a724042e8029c1b872d87fabc9f41684", size = 20955, upload-time = "2025-04-22T16:40:29.172Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/84/40ee070be95771acd2f4418981edb834979424565c3eec3cd88b6aa09d24/google_auth_oauthlib-1.2.2-py3-none-any.whl", hash = "sha256:fd619506f4b3908b5df17b65f39ca8d66ea56986e5472eb5978fd8f3786f00a2", size = 19072, upload-time = "2025-04-22T16:40:28.174Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "googleapis-common-protos"
|
||||
version = "1.71.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "protobuf" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/30/43/b25abe02db2911397819003029bef768f68a974f2ece483e6084d1a5f754/googleapis_common_protos-1.71.0.tar.gz", hash = "sha256:1aec01e574e29da63c80ba9f7bbf1ccfaacf1da877f23609fe236ca7c72a2e2e", size = 146454, upload-time = "2025-10-20T14:58:08.732Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/25/e8/eba9fece11d57a71e3e22ea672742c8f3cf23b35730c9e96db768b295216/googleapis_common_protos-1.71.0-py3-none-any.whl", hash = "sha256:59034a1d849dc4d18971997a72ac56246570afdd17f9369a0ff68218d50ab78c", size = 294576, upload-time = "2025-10-20T14:56:21.295Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "greenlet"
|
||||
version = "3.2.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/03/b8/704d753a5a45507a7aab61f18db9509302ed3d0a27ac7e0359ec2905b1a6/greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", size = 188260, upload-time = "2025-08-07T13:24:33.51Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191, upload-time = "2025-08-07T13:45:29.752Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/d6/6adde57d1345a8d0f14d31e4ab9c23cfe8e2cd39c3baf7674b4b0338d266/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", size = 649516, upload-time = "2025-08-07T13:53:16.314Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/3b/3a3328a788d4a473889a2d403199932be55b1b0060f4ddd96ee7cdfcad10/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", size = 652169, upload-time = "2025-08-07T13:18:32.861Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/53/f9c440463b3057485b8594d7a638bed53ba531165ef0ca0e6c364b5cc807/greenlet-3.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e343822feb58ac4d0a1211bd9399de2b3a04963ddeec21530fc426cc121f19b", size = 1564759, upload-time = "2025-11-04T12:42:19.395Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/e4/3bb4240abdd0a8d23f4f88adec746a3099f0d86bfedb623f063b2e3b4df0/greenlet-3.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca7f6f1f2649b89ce02f6f229d7c19f680a6238af656f61e0115b24857917929", size = 1634288, upload-time = "2025-11-04T12:42:21.174Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/aa/687d6b12ffb505a4447567d1f3abea23bd20e73a5bed63871178e0831b7a/greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", size = 699218, upload-time = "2025-08-07T13:45:30.969Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/6e/74407aed965a4ab6ddd93a7ded3180b730d281c77b765788419484cdfeef/greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269", size = 1612508, upload-time = "2025-11-04T12:42:23.427Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/da/343cd760ab2f92bac1845ca07ee3faea9fe52bee65f7bcb19f16ad7de08b/greenlet-3.2.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:015d48959d4add5d6c9f6c5210ee3803a830dce46356e3bc326d6776bde54681", size = 1680760, upload-time = "2025-11-04T12:42:25.341Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "grpcio"
|
||||
version = "1.76.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182, upload-time = "2025-10-21T16:23:12.106Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/ed/71467ab770effc9e8cef5f2e7388beb2be26ed642d567697bb103a790c72/grpcio-1.76.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2", size = 5807716, upload-time = "2025-10-21T16:21:48.475Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/85/c6ed56f9817fab03fa8a111ca91469941fb514e3e3ce6d793cb8f1e1347b/grpcio-1.76.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:45e0111e73f43f735d70786557dc38141185072d7ff8dc1829d6a77ac1471468", size = 11821522, upload-time = "2025-10-21T16:21:51.142Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/31/2b8a235ab40c39cbc141ef647f8a6eb7b0028f023015a4842933bc0d6831/grpcio-1.76.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83d57312a58dcfe2a3a0f9d1389b299438909a02db60e2f2ea2ae2d8034909d3", size = 6362558, upload-time = "2025-10-21T16:21:54.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/64/9784eab483358e08847498ee56faf8ff6ea8e0a4592568d9f68edc97e9e9/grpcio-1.76.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3e2a27c89eb9ac3d81ec8835e12414d73536c6e620355d65102503064a4ed6eb", size = 7049990, upload-time = "2025-10-21T16:21:56.476Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/94/8c12319a6369434e7a184b987e8e9f3b49a114c489b8315f029e24de4837/grpcio-1.76.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae", size = 6575387, upload-time = "2025-10-21T16:21:59.051Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/0f/f12c32b03f731f4a6242f771f63039df182c8b8e2cf8075b245b409259d4/grpcio-1.76.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a15c17af8839b6801d554263c546c69c4d7718ad4321e3166175b37eaacca77", size = 7166668, upload-time = "2025-10-21T16:22:02.049Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/2d/3ec9ce0c2b1d92dd59d1c3264aaec9f0f7c817d6e8ac683b97198a36ed5a/grpcio-1.76.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25a18e9810fbc7e7f03ec2516addc116a957f8cbb8cbc95ccc80faa072743d03", size = 8124928, upload-time = "2025-10-21T16:22:04.984Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/74/fd3317be5672f4856bcdd1a9e7b5e17554692d3db9a3b273879dc02d657d/grpcio-1.76.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:931091142fd8cc14edccc0845a79248bc155425eee9a98b2db2ea4f00a235a42", size = 7589983, upload-time = "2025-10-21T16:22:07.881Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/bb/ca038cf420f405971f19821c8c15bcbc875505f6ffadafe9ffd77871dc4c/grpcio-1.76.0-cp313-cp313-win32.whl", hash = "sha256:5e8571632780e08526f118f74170ad8d50fb0a48c23a746bef2a6ebade3abd6f", size = 3984727, upload-time = "2025-10-21T16:22:10.032Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/80/84087dc56437ced7cdd4b13d7875e7439a52a261e3ab4e06488ba6173b0a/grpcio-1.76.0-cp313-cp313-win_amd64.whl", hash = "sha256:f9f7bd5faab55f47231ad8dba7787866b69f5e93bc306e3915606779bbfb4ba8", size = 4702799, upload-time = "2025-10-21T16:22:12.709Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/46/39adac80de49d678e6e073b70204091e76631e03e94928b9ea4ecf0f6e0e/grpcio-1.76.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:ff8a59ea85a1f2191a0ffcc61298c571bc566332f82e5f5be1b83c9d8e668a62", size = 5808417, upload-time = "2025-10-21T16:22:15.02Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/f5/a4531f7fb8b4e2a60b94e39d5d924469b7a6988176b3422487be61fe2998/grpcio-1.76.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:06c3d6b076e7b593905d04fdba6a0525711b3466f43b3400266f04ff735de0cd", size = 11828219, upload-time = "2025-10-21T16:22:17.954Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/1c/de55d868ed7a8bd6acc6b1d6ddc4aa36d07a9f31d33c912c804adb1b971b/grpcio-1.76.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd5ef5932f6475c436c4a55e4336ebbe47bd3272be04964a03d316bbf4afbcbc", size = 6367826, upload-time = "2025-10-21T16:22:20.721Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/64/99e44c02b5adb0ad13ab3adc89cb33cb54bfa90c74770f2607eea629b86f/grpcio-1.76.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b331680e46239e090f5b3cead313cc772f6caa7d0fc8de349337563125361a4a", size = 7049550, upload-time = "2025-10-21T16:22:23.637Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/28/40a5be3f9a86949b83e7d6a2ad6011d993cbe9b6bd27bea881f61c7788b6/grpcio-1.76.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2229ae655ec4e8999599469559e97630185fdd53ae1e8997d147b7c9b2b72cba", size = 6575564, upload-time = "2025-10-21T16:22:26.016Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/a9/1be18e6055b64467440208a8559afac243c66a8b904213af6f392dc2212f/grpcio-1.76.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:490fa6d203992c47c7b9e4a9d39003a0c2bcc1c9aa3c058730884bbbb0ee9f09", size = 7176236, upload-time = "2025-10-21T16:22:28.362Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/55/dba05d3fcc151ce6e81327541d2cc8394f442f6b350fead67401661bf041/grpcio-1.76.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:479496325ce554792dba6548fae3df31a72cef7bad71ca2e12b0e58f9b336bfc", size = 8125795, upload-time = "2025-10-21T16:22:31.075Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/45/122df922d05655f63930cf42c9e3f72ba20aadb26c100ee105cad4ce4257/grpcio-1.76.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c9b93f79f48b03ada57ea24725d83a30284a012ec27eab2cf7e50a550cbbbcc", size = 7592214, upload-time = "2025-10-21T16:22:33.831Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/6e/0b899b7f6b66e5af39e377055fb4a6675c9ee28431df5708139df2e93233/grpcio-1.76.0-cp314-cp314-win32.whl", hash = "sha256:747fa73efa9b8b1488a95d0ba1039c8e2dca0f741612d80415b1e1c560febf4e", size = 4062961, upload-time = "2025-10-21T16:22:36.468Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/41/0b430b01a2eb38ee887f88c1f07644a1df8e289353b78e82b37ef988fb64/grpcio-1.76.0-cp314-cp314-win_amd64.whl", hash = "sha256:922fa70ba549fce362d2e2871ab542082d66e2aaf0c19480ea453905b01f384e", size = 4834462, upload-time = "2025-10-21T16:22:39.772Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "grpcio-status"
|
||||
version = "1.76.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "googleapis-common-protos" },
|
||||
{ name = "grpcio" },
|
||||
{ name = "protobuf" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3f/46/e9f19d5be65e8423f886813a2a9d0056ba94757b0c5007aa59aed1a961fa/grpcio_status-1.76.0.tar.gz", hash = "sha256:25fcbfec74c15d1a1cb5da3fab8ee9672852dc16a5a9eeb5baf7d7a9952943cd", size = 13679, upload-time = "2025-10-21T16:28:52.545Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/cc/27ba60ad5a5f2067963e6a858743500df408eb5855e98be778eaef8c9b02/grpcio_status-1.76.0-py3-none-any.whl", hash = "sha256:380568794055a8efbbd8871162df92012e0228a5f6dffaf57f2a00c534103b18", size = 14425, upload-time = "2025-10-21T16:28:40.853Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.11"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.3.0"
|
||||
@@ -468,83 +40,14 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mako"
|
||||
version = "1.3.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "3.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "meta-api-grabber"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
{ name = "alembic" },
|
||||
{ name = "asyncpg" },
|
||||
{ name = "facebook-business" },
|
||||
{ name = "google-ads" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "requests-oauthlib" },
|
||||
{ name = "sqlalchemy", extra = ["asyncio"] },
|
||||
{ name = "pyyaml" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
@@ -555,109 +58,14 @@ test = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "aiohttp", specifier = ">=3.13.1" },
|
||||
{ name = "alembic", specifier = ">=1.17.0" },
|
||||
{ name = "asyncpg", specifier = ">=0.30.0" },
|
||||
{ name = "facebook-business", specifier = ">=23.0.3" },
|
||||
{ name = "google-ads", specifier = ">=28.3.0" },
|
||||
{ name = "pytest", marker = "extra == 'test'", specifier = ">=8.0.0" },
|
||||
{ name = "pytest-asyncio", marker = "extra == 'test'", specifier = ">=0.25.0" },
|
||||
{ name = "python-dotenv", specifier = ">=1.1.1" },
|
||||
{ name = "requests-oauthlib", specifier = ">=2.0.0" },
|
||||
{ name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.44" },
|
||||
{ name = "pyyaml", specifier = ">=6.0.0" },
|
||||
]
|
||||
provides-extras = ["test"]
|
||||
|
||||
[[package]]
|
||||
name = "multidict"
|
||||
version = "6.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834, upload-time = "2025-10-06T14:52:30.657Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/86/33272a544eeb36d66e4d9a920602d1a2f57d4ebea4ef3cdfe5a912574c95/multidict-6.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6", size = 76135, upload-time = "2025-10-06T14:49:54.26Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/1c/eb97db117a1ebe46d457a3d235a7b9d2e6dcab174f42d1b67663dd9e5371/multidict-6.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159", size = 45117, upload-time = "2025-10-06T14:49:55.82Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/d8/6c3442322e41fb1dd4de8bd67bfd11cd72352ac131f6368315617de752f1/multidict-6.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca", size = 43472, upload-time = "2025-10-06T14:49:57.048Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/3f/e2639e80325af0b6c6febdf8e57cc07043ff15f57fa1ef808f4ccb5ac4cd/multidict-6.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8", size = 249342, upload-time = "2025-10-06T14:49:58.368Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/cc/84e0585f805cbeaa9cbdaa95f9a3d6aed745b9d25700623ac89a6ecff400/multidict-6.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60", size = 257082, upload-time = "2025-10-06T14:49:59.89Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/9c/ac851c107c92289acbbf5cfb485694084690c1b17e555f44952c26ddc5bd/multidict-6.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4", size = 240704, upload-time = "2025-10-06T14:50:01.485Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/cc/5f93e99427248c09da95b62d64b25748a5f5c98c7c2ab09825a1d6af0e15/multidict-6.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f", size = 266355, upload-time = "2025-10-06T14:50:02.955Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/0c/2ec1d883ceb79c6f7f6d7ad90c919c898f5d1c6ea96d322751420211e072/multidict-6.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf", size = 267259, upload-time = "2025-10-06T14:50:04.446Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/2d/f0b184fa88d6630aa267680bdb8623fb69cb0d024b8c6f0d23f9a0f406d3/multidict-6.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32", size = 254903, upload-time = "2025-10-06T14:50:05.98Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/c9/11ea263ad0df7dfabcad404feb3c0dd40b131bc7f232d5537f2fb1356951/multidict-6.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036", size = 252365, upload-time = "2025-10-06T14:50:07.511Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/88/d714b86ee2c17d6e09850c70c9d310abac3d808ab49dfa16b43aba9d53fd/multidict-6.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec", size = 250062, upload-time = "2025-10-06T14:50:09.074Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/fe/ad407bb9e818c2b31383f6131ca19ea7e35ce93cf1310fce69f12e89de75/multidict-6.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e", size = 249683, upload-time = "2025-10-06T14:50:10.714Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/a4/a89abdb0229e533fb925e7c6e5c40201c2873efebc9abaf14046a4536ee6/multidict-6.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64", size = 261254, upload-time = "2025-10-06T14:50:12.28Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/aa/0e2b27bd88b40a4fb8dc53dd74eecac70edaa4c1dd0707eb2164da3675b3/multidict-6.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd", size = 257967, upload-time = "2025-10-06T14:50:14.16Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/8e/0c67b7120d5d5f6d874ed85a085f9dc770a7f9d8813e80f44a9fec820bb7/multidict-6.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288", size = 250085, upload-time = "2025-10-06T14:50:15.639Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/55/b73e1d624ea4b8fd4dd07a3bb70f6e4c7c6c5d9d640a41c6ffe5cdbd2a55/multidict-6.7.0-cp313-cp313-win32.whl", hash = "sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17", size = 41713, upload-time = "2025-10-06T14:50:17.066Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/31/75c59e7d3b4205075b4c183fa4ca398a2daf2303ddf616b04ae6ef55cffe/multidict-6.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390", size = 45915, upload-time = "2025-10-06T14:50:18.264Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/2a/8987831e811f1184c22bc2e45844934385363ee61c0a2dcfa8f71b87e608/multidict-6.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e", size = 43077, upload-time = "2025-10-06T14:50:19.853Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/68/7b3a5170a382a340147337b300b9eb25a9ddb573bcdfff19c0fa3f31ffba/multidict-6.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00", size = 83114, upload-time = "2025-10-06T14:50:21.223Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/5c/3fa2d07c84df4e302060f555bbf539310980362236ad49f50eeb0a1c1eb9/multidict-6.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb", size = 48442, upload-time = "2025-10-06T14:50:22.871Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/56/67212d33239797f9bd91962bb899d72bb0f4c35a8652dcdb8ed049bef878/multidict-6.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b", size = 46885, upload-time = "2025-10-06T14:50:24.258Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/d1/908f896224290350721597a61a69cd19b89ad8ee0ae1f38b3f5cd12ea2ac/multidict-6.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c", size = 242588, upload-time = "2025-10-06T14:50:25.716Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/67/8604288bbd68680eee0ab568fdcb56171d8b23a01bcd5cb0c8fedf6e5d99/multidict-6.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1", size = 249966, upload-time = "2025-10-06T14:50:28.192Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/33/9228d76339f1ba51e3efef7da3ebd91964d3006217aae13211653193c3ff/multidict-6.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b", size = 228618, upload-time = "2025-10-06T14:50:29.82Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/2d/25d9b566d10cab1c42b3b9e5b11ef79c9111eaf4463b8c257a3bd89e0ead/multidict-6.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5", size = 257539, upload-time = "2025-10-06T14:50:31.731Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/b1/8d1a965e6637fc33de3c0d8f414485c2b7e4af00f42cab3d84e7b955c222/multidict-6.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad", size = 256345, upload-time = "2025-10-06T14:50:33.26Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/0c/06b5a8adbdeedada6f4fb8d8f193d44a347223b11939b42953eeb6530b6b/multidict-6.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c", size = 247934, upload-time = "2025-10-06T14:50:34.808Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/31/b2491b5fe167ca044c6eb4b8f2c9f3b8a00b24c432c365358eadac5d7625/multidict-6.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5", size = 245243, upload-time = "2025-10-06T14:50:36.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/1a/982913957cb90406c8c94f53001abd9eafc271cb3e70ff6371590bec478e/multidict-6.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10", size = 235878, upload-time = "2025-10-06T14:50:37.953Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/c0/21435d804c1a1cf7a2608593f4d19bca5bcbd7a81a70b253fdd1c12af9c0/multidict-6.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754", size = 243452, upload-time = "2025-10-06T14:50:39.574Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/0a/4349d540d4a883863191be6eb9a928846d4ec0ea007d3dcd36323bb058ac/multidict-6.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c", size = 252312, upload-time = "2025-10-06T14:50:41.612Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/64/d5416038dbda1488daf16b676e4dbfd9674dde10a0cc8f4fc2b502d8125d/multidict-6.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762", size = 246935, upload-time = "2025-10-06T14:50:43.972Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/8c/8290c50d14e49f35e0bd4abc25e1bc7711149ca9588ab7d04f886cdf03d9/multidict-6.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6", size = 243385, upload-time = "2025-10-06T14:50:45.648Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/a0/f83ae75e42d694b3fbad3e047670e511c138be747bc713cf1b10d5096416/multidict-6.7.0-cp313-cp313t-win32.whl", hash = "sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d", size = 47777, upload-time = "2025-10-06T14:50:47.154Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/80/9b174a92814a3830b7357307a792300f42c9e94664b01dee8e457551fa66/multidict-6.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6", size = 53104, upload-time = "2025-10-06T14:50:48.851Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/28/04baeaf0428d95bb7a7bea0e691ba2f31394338ba424fb0679a9ed0f4c09/multidict-6.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792", size = 45503, upload-time = "2025-10-06T14:50:50.16Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/b1/3da6934455dd4b261d4c72f897e3a5728eba81db59959f3a639245891baa/multidict-6.7.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842", size = 75128, upload-time = "2025-10-06T14:50:51.92Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/2c/f069cab5b51d175a1a2cb4ccdf7a2c2dabd58aa5bd933fa036a8d15e2404/multidict-6.7.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b", size = 44410, upload-time = "2025-10-06T14:50:53.275Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/e2/64bb41266427af6642b6b128e8774ed84c11b80a90702c13ac0a86bb10cc/multidict-6.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38", size = 43205, upload-time = "2025-10-06T14:50:54.911Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/68/6b086fef8a3f1a8541b9236c594f0c9245617c29841f2e0395d979485cde/multidict-6.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128", size = 245084, upload-time = "2025-10-06T14:50:56.369Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/ee/f524093232007cd7a75c1d132df70f235cfd590a7c9eaccd7ff422ef4ae8/multidict-6.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34", size = 252667, upload-time = "2025-10-06T14:50:57.991Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/a5/eeb3f43ab45878f1895118c3ef157a480db58ede3f248e29b5354139c2c9/multidict-6.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99", size = 233590, upload-time = "2025-10-06T14:50:59.589Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/1e/76d02f8270b97269d7e3dbd45644b1785bda457b474315f8cf999525a193/multidict-6.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202", size = 264112, upload-time = "2025-10-06T14:51:01.183Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/0b/c28a70ecb58963847c2a8efe334904cd254812b10e535aefb3bcce513918/multidict-6.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1", size = 261194, upload-time = "2025-10-06T14:51:02.794Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/63/2ab26e4209773223159b83aa32721b4021ffb08102f8ac7d689c943fded1/multidict-6.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3", size = 248510, upload-time = "2025-10-06T14:51:04.724Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/cd/06c1fa8282af1d1c46fd55c10a7930af652afdce43999501d4d68664170c/multidict-6.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d", size = 248395, upload-time = "2025-10-06T14:51:06.306Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/ac/82cb419dd6b04ccf9e7e61befc00c77614fc8134362488b553402ecd55ce/multidict-6.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6", size = 239520, upload-time = "2025-10-06T14:51:08.091Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/f3/a0f9bf09493421bd8716a362e0cd1d244f5a6550f5beffdd6b47e885b331/multidict-6.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7", size = 245479, upload-time = "2025-10-06T14:51:10.365Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/01/476d38fc73a212843f43c852b0eee266b6971f0e28329c2184a8df90c376/multidict-6.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb", size = 258903, upload-time = "2025-10-06T14:51:12.466Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/6d/23faeb0868adba613b817d0e69c5f15531b24d462af8012c4f6de4fa8dc3/multidict-6.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f", size = 252333, upload-time = "2025-10-06T14:51:14.48Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/cc/48d02ac22b30fa247f7dad82866e4b1015431092f4ba6ebc7e77596e0b18/multidict-6.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f", size = 243411, upload-time = "2025-10-06T14:51:16.072Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/03/29a8bf5a18abf1fe34535c88adbdfa88c9fb869b5a3b120692c64abe8284/multidict-6.7.0-cp314-cp314-win32.whl", hash = "sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885", size = 40940, upload-time = "2025-10-06T14:51:17.544Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/16/7ed27b680791b939de138f906d5cf2b4657b0d45ca6f5dd6236fdddafb1a/multidict-6.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c", size = 45087, upload-time = "2025-10-06T14:51:18.875Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/3c/e3e62eb35a1950292fe39315d3c89941e30a9d07d5d2df42965ab041da43/multidict-6.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000", size = 42368, upload-time = "2025-10-06T14:51:20.225Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/40/cd499bd0dbc5f1136726db3153042a735fffd0d77268e2ee20d5f33c010f/multidict-6.7.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63", size = 82326, upload-time = "2025-10-06T14:51:21.588Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/8a/18e031eca251c8df76daf0288e6790561806e439f5ce99a170b4af30676b/multidict-6.7.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718", size = 48065, upload-time = "2025-10-06T14:51:22.93Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/71/5e6701277470a87d234e433fb0a3a7deaf3bcd92566e421e7ae9776319de/multidict-6.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2", size = 46475, upload-time = "2025-10-06T14:51:24.352Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/6a/bab00cbab6d9cfb57afe1663318f72ec28289ea03fd4e8236bb78429893a/multidict-6.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e", size = 239324, upload-time = "2025-10-06T14:51:25.822Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/5f/8de95f629fc22a7769ade8b41028e3e5a822c1f8904f618d175945a81ad3/multidict-6.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064", size = 246877, upload-time = "2025-10-06T14:51:27.604Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/b4/38881a960458f25b89e9f4a4fdcb02ac101cfa710190db6e5528841e67de/multidict-6.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e", size = 225824, upload-time = "2025-10-06T14:51:29.664Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/39/6566210c83f8a261575f18e7144736059f0c460b362e96e9cf797a24b8e7/multidict-6.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd", size = 253558, upload-time = "2025-10-06T14:51:31.684Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/a3/67f18315100f64c269f46e6c0319fa87ba68f0f64f2b8e7fd7c72b913a0b/multidict-6.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a", size = 252339, upload-time = "2025-10-06T14:51:33.699Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/2a/1cb77266afee2458d82f50da41beba02159b1d6b1f7973afc9a1cad1499b/multidict-6.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96", size = 244895, upload-time = "2025-10-06T14:51:36.189Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/72/09fa7dd487f119b2eb9524946ddd36e2067c08510576d43ff68469563b3b/multidict-6.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e", size = 241862, upload-time = "2025-10-06T14:51:41.291Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/92/bc1f8bd0853d8669300f732c801974dfc3702c3eeadae2f60cef54dc69d7/multidict-6.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599", size = 232376, upload-time = "2025-10-06T14:51:43.55Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/86/ac39399e5cb9d0c2ac8ef6e10a768e4d3bc933ac808d49c41f9dc23337eb/multidict-6.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394", size = 240272, upload-time = "2025-10-06T14:51:45.265Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/b6/fed5ac6b8563ec72df6cb1ea8dac6d17f0a4a1f65045f66b6d3bf1497c02/multidict-6.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38", size = 248774, upload-time = "2025-10-06T14:51:46.836Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/8d/b954d8c0dc132b68f760aefd45870978deec6818897389dace00fcde32ff/multidict-6.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9", size = 242731, upload-time = "2025-10-06T14:51:48.541Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/9d/a2dac7009125d3540c2f54e194829ea18ac53716c61b655d8ed300120b0f/multidict-6.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0", size = 240193, upload-time = "2025-10-06T14:51:50.355Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/ca/c05f144128ea232ae2178b008d5011d4e2cea86e4ee8c85c2631b1b94802/multidict-6.7.0-cp314-cp314t-win32.whl", hash = "sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13", size = 48023, upload-time = "2025-10-06T14:51:51.883Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/8f/0a60e501584145588be1af5cc829265701ba3c35a64aec8e07cbb71d39bb/multidict-6.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd", size = 53507, upload-time = "2025-10-06T14:51:53.672Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/ae/3148b988a9c6239903e786eac19c889fab607c31d6efa7fb2147e5680f23/multidict-6.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827", size = 44804, upload-time = "2025-10-06T14:51:55.415Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317, upload-time = "2025-10-06T14:52:29.272Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "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 = "packaging"
|
||||
version = "25.0"
|
||||
@@ -676,132 +84,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "propcache"
|
||||
version = "0.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proto-plus"
|
||||
version = "1.26.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "protobuf" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f4/ac/87285f15f7cce6d4a008f33f1757fb5a13611ea8914eb58c3d0d26243468/proto_plus-1.26.1.tar.gz", hash = "sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012", size = 56142, upload-time = "2025-03-10T15:54:38.843Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/6d/280c4c2ce28b1593a19ad5239c8b826871fc6ec275c21afc8e1820108039/proto_plus-1.26.1-py3-none-any.whl", hash = "sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66", size = 50163, upload-time = "2025-03-10T15:54:37.335Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "protobuf"
|
||||
version = "6.33.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/19/ff/64a6c8f420818bb873713988ca5492cba3a7946be57e027ac63495157d97/protobuf-6.33.0.tar.gz", hash = "sha256:140303d5c8d2037730c548f8c7b93b20bb1dc301be280c378b82b8894589c954", size = 443463, upload-time = "2025-10-15T20:39:52.159Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/ee/52b3fa8feb6db4a833dfea4943e175ce645144532e8a90f72571ad85df4e/protobuf-6.33.0-cp310-abi3-win32.whl", hash = "sha256:d6101ded078042a8f17959eccd9236fb7a9ca20d3b0098bbcb91533a5680d035", size = 425593, upload-time = "2025-10-15T20:39:40.29Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/c6/7a465f1825872c55e0341ff4a80198743f73b69ce5d43ab18043699d1d81/protobuf-6.33.0-cp310-abi3-win_amd64.whl", hash = "sha256:9a031d10f703f03768f2743a1c403af050b6ae1f3480e9c140f39c45f81b13ee", size = 436882, upload-time = "2025-10-15T20:39:42.841Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/a9/b6eee662a6951b9c3640e8e452ab3e09f117d99fc10baa32d1581a0d4099/protobuf-6.33.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:905b07a65f1a4b72412314082c7dbfae91a9e8b68a0cc1577515f8df58ecf455", size = 427521, upload-time = "2025-10-15T20:39:43.803Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/35/16d31e0f92c6d2f0e77c2a3ba93185130ea13053dd16200a57434c882f2b/protobuf-6.33.0-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e0697ece353e6239b90ee43a9231318302ad8353c70e6e45499fa52396debf90", size = 324445, upload-time = "2025-10-15T20:39:44.932Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/eb/2a981a13e35cda8b75b5585aaffae2eb904f8f351bdd3870769692acbd8a/protobuf-6.33.0-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:e0a1715e4f27355afd9570f3ea369735afc853a6c3951a6afe1f80d8569ad298", size = 339159, upload-time = "2025-10-15T20:39:46.186Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/51/0b1cbad62074439b867b4e04cc09b93f6699d78fd191bed2bbb44562e077/protobuf-6.33.0-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:35be49fd3f4fefa4e6e2aacc35e8b837d6703c37a2168a55ac21e9b1bc7559ef", size = 323172, upload-time = "2025-10-15T20:39:47.465Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/d1/0a28c21707807c6aacd5dc9c3704b2aa1effbf37adebd8caeaf68b17a636/protobuf-6.33.0-py3-none-any.whl", hash = "sha256:25c9e1963c6734448ea2d308cfa610e692b801304ba0908d7bfa564ac5132995", size = 170477, upload-time = "2025-10-15T20:39:51.311Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyasn1"
|
||||
version = "0.6.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" },
|
||||
]
|
||||
|
||||
[[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 = "pycountry"
|
||||
version = "24.6.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/76/57/c389fa68c50590881a75b7883eeb3dc15e9e73a0fdc001cdd45c13290c92/pycountry-24.6.1.tar.gz", hash = "sha256:b61b3faccea67f87d10c1f2b0fc0be714409e8fcdcc1315613174f6466c10221", size = 6043910, upload-time = "2024-06-01T04:12:15.05Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/ec/1fb891d8a2660716aadb2143235481d15ed1cbfe3ad669194690b0604492/pycountry-24.6.1-py3-none-any.whl", hash = "sha256:f1a4fb391cd7214f8eefd39556d740adcc233c778a27f8942c8dca351d6ce06f", size = 6335189, upload-time = "2024-06-01T04:11:49.711Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.2"
|
||||
@@ -883,174 +165,3 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "idna" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
|
||||
]
|
||||
|
||||
[[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 = "rsa"
|
||||
version = "4.9.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pyasn1" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.17.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlalchemy"
|
||||
version = "2.0.44"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f0/f2/840d7b9496825333f532d2e3976b8eadbf52034178aac53630d09fe6e1ef/sqlalchemy-2.0.44.tar.gz", hash = "sha256:0ae7454e1ab1d780aee69fd2aae7d6b8670a581d8847f2d1e0f7ddfbf47e5a22", size = 9819830, upload-time = "2025-10-10T14:39:12.935Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/45/d3/c67077a2249fdb455246e6853166360054c331db4613cda3e31ab1cadbef/sqlalchemy-2.0.44-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ff486e183d151e51b1d694c7aa1695747599bb00b9f5f604092b54b74c64a8e1", size = 2135479, upload-time = "2025-10-10T16:03:37.671Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/91/eabd0688330d6fd114f5f12c4f89b0d02929f525e6bf7ff80aa17ca802af/sqlalchemy-2.0.44-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b1af8392eb27b372ddb783b317dea0f650241cea5bd29199b22235299ca2e45", size = 2123212, upload-time = "2025-10-10T16:03:41.755Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/bb/43e246cfe0e81c018076a16036d9b548c4cc649de241fa27d8d9ca6f85ab/sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b61188657e3a2b9ac4e8f04d6cf8e51046e28175f79464c67f2fd35bceb0976", size = 3255353, upload-time = "2025-10-10T15:35:31.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/96/c6105ed9a880abe346b64d3b6ddef269ddfcab04f7f3d90a0bf3c5a88e82/sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b87e7b91a5d5973dda5f00cd61ef72ad75a1db73a386b62877d4875a8840959c", size = 3260222, upload-time = "2025-10-10T15:43:50.124Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/16/1857e35a47155b5ad927272fee81ae49d398959cb749edca6eaa399b582f/sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:15f3326f7f0b2bfe406ee562e17f43f36e16167af99c4c0df61db668de20002d", size = 3189614, upload-time = "2025-10-10T15:35:32.578Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/ee/4afb39a8ee4fc786e2d716c20ab87b5b1fb33d4ac4129a1aaa574ae8a585/sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e77faf6ff919aa8cd63f1c4e561cac1d9a454a191bb864d5dd5e545935e5a40", size = 3226248, upload-time = "2025-10-10T15:43:51.862Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/d5/0e66097fc64fa266f29a7963296b40a80d6a997b7ac13806183700676f86/sqlalchemy-2.0.44-cp313-cp313-win32.whl", hash = "sha256:ee51625c2d51f8baadf2829fae817ad0b66b140573939dd69284d2ba3553ae73", size = 2101275, upload-time = "2025-10-10T15:03:26.096Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/51/665617fe4f8c6450f42a6d8d69243f9420f5677395572c2fe9d21b493b7b/sqlalchemy-2.0.44-cp313-cp313-win_amd64.whl", hash = "sha256:c1c80faaee1a6c3428cecf40d16a2365bcf56c424c92c2b6f0f9ad204b899e9e", size = 2127901, upload-time = "2025-10-10T15:03:27.548Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/5e/6a29fa884d9fb7ddadf6b69490a9d45fded3b38541713010dad16b77d015/sqlalchemy-2.0.44-py3-none-any.whl", hash = "sha256:19de7ca1246fbef9f9d1bff8f1ab25641569df226364a0e40457dc5457c54b05", size = 1928718, upload-time = "2025-10-10T15:29:45.32Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
asyncio = [
|
||||
{ name = "greenlet" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.15.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yarl"
|
||||
version = "1.22.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "multidict" },
|
||||
{ name = "propcache" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243, upload-time = "2025-10-06T14:10:19.44Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361, upload-time = "2025-10-06T14:10:21.124Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036, upload-time = "2025-10-06T14:10:22.902Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671, upload-time = "2025-10-06T14:10:24.523Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059, upload-time = "2025-10-06T14:10:26.406Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356, upload-time = "2025-10-06T14:10:28.461Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331, upload-time = "2025-10-06T14:10:30.541Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590, upload-time = "2025-10-06T14:10:33.352Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316, upload-time = "2025-10-06T14:10:35.034Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431, upload-time = "2025-10-06T14:10:37.76Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555, upload-time = "2025-10-06T14:10:39.649Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965, upload-time = "2025-10-06T14:10:41.313Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205, upload-time = "2025-10-06T14:10:43.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209, upload-time = "2025-10-06T14:10:44.643Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966, upload-time = "2025-10-06T14:10:46.554Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312, upload-time = "2025-10-06T14:10:48.007Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967, upload-time = "2025-10-06T14:10:49.997Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949, upload-time = "2025-10-06T14:10:52.004Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818, upload-time = "2025-10-06T14:10:54.078Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626, upload-time = "2025-10-06T14:10:55.767Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129, upload-time = "2025-10-06T14:10:57.985Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776, upload-time = "2025-10-06T14:10:59.633Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879, upload-time = "2025-10-06T14:11:01.454Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996, upload-time = "2025-10-06T14:11:03.452Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047, upload-time = "2025-10-06T14:11:05.115Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947, upload-time = "2025-10-06T14:11:08.137Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943, upload-time = "2025-10-06T14:11:10.284Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080, upload-time = "2025-10-06T14:11:20.996Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696, upload-time = "2025-10-06T14:11:22.847Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121, upload-time = "2025-10-06T14:11:24.889Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080, upload-time = "2025-10-06T14:11:27.307Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661, upload-time = "2025-10-06T14:11:29.387Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645, upload-time = "2025-10-06T14:11:31.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361, upload-time = "2025-10-06T14:11:33.055Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451, upload-time = "2025-10-06T14:11:35.136Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814, upload-time = "2025-10-06T14:11:37.094Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799, upload-time = "2025-10-06T14:11:38.83Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990, upload-time = "2025-10-06T14:11:40.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292, upload-time = "2025-10-06T14:11:42.578Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888, upload-time = "2025-10-06T14:11:44.863Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223, upload-time = "2025-10-06T14:11:46.796Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981, upload-time = "2025-10-06T14:11:48.845Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303, upload-time = "2025-10-06T14:11:50.897Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820, upload-time = "2025-10-06T14:11:52.549Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203, upload-time = "2025-10-06T14:11:54.225Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173, upload-time = "2025-10-06T14:11:56.069Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562, upload-time = "2025-10-06T14:11:58.783Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828, upload-time = "2025-10-06T14:12:00.686Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551, upload-time = "2025-10-06T14:12:02.628Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512, upload-time = "2025-10-06T14:12:04.871Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400, upload-time = "2025-10-06T14:12:06.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140, upload-time = "2025-10-06T14:12:08.362Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473, upload-time = "2025-10-06T14:12:10.994Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056, upload-time = "2025-10-06T14:12:13.317Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292, upload-time = "2025-10-06T14:12:15.398Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user