CoolFace
Apppublic

Frknrg/RgReport

sourceHugging Faceupdated 5mo agoView on Hugging Face
0likes
shopify_graphql.py413 linesDownload Raw Back to root
1import requests2import pandas as pd3import os4import json5try:6    import streamlit as st7    _HAS_STREAMLIT = True8except ImportError:9    _HAS_STREAMLIT = False10import time11import random12 13def get_config():14    token = os.environ.get("SHOPIFY_ACCESS_TOKEN")15    store_domain = os.environ.get("SHOPIFY_STORE_DOMAIN")16    api_version = "2026-01"17 18    if _HAS_STREAMLIT:19        try:20            # Try to get from st.secrets if not in environ or to override21            if not token:22                token = st.secrets.get("SHOPIFY_ACCESS_TOKEN")23            if not store_domain:24                store_domain = st.secrets.get("SHOPIFY_STORE_DOMAIN")25        except Exception:26            # Silently fallback to environ if st.secrets fails (common in bg threads)27            pass28            29    return token, store_domain, api_version30 31def execute_shopifyql(query):32    """33    Executes a ShopifyQL query via the GraphQL Admin API (version 2026-01).34    35    Args:36        query (str): The ShopifyQL query string37        38    Returns:39        pd.DataFrame: A DataFrame containing the query results, or empty DataFrame on error.40    """41    token, store_domain, api_version = get_config()42    43    if not token or not store_domain:44        print("❌ Error: SHOPIFY_ACCESS_TOKEN or SHOPIFY_STORE_DOMAIN not set.")45        return pd.DataFrame()46 47    url = f"https://{store_domain}/admin/api/{api_version}/graphql.json"48    49    headers = {50        "Content-Type": "application/json",51        "X-Shopify-Access-Token": token52    }53    54    graphql_query = '''55    query ShopifyQLQuery($query: String!) {56      shopifyqlQuery(query: $query) {57        tableData {58          rows59          columns {60            name61            dataType62            displayName63          }64        }65        parseErrors66      }67    }68    '''69    70    payload = {71        "query": graphql_query,72        "variables": {73            "query": query74        }75    }76    77    max_retries = 878    base_delay = 30  # Shopify Plus: generous base delay79    80    for attempt in range(max_retries):81        try:82            response = requests.post(url, headers=headers, json=payload, timeout=60)83            84            # Check HTTP-level rate limit (429)85            if response.status_code == 429:86                retry_after = int(response.headers.get("Retry-After", 60))87                print(f"⏳ HTTP 429 Rate Limited. Waiting {retry_after}s (Retry-After header)...", flush=True)88                time.sleep(retry_after)89                continue90            91            if response.status_code != 200:92                wait_time = base_delay * (attempt + 1)93                print(f"⚠️ HTTP {response.status_code}. Waiting {wait_time}s...", flush=True)94                time.sleep(wait_time)95                continue96                97            data = response.json()98            99            # Read GraphQL throttle cost info from extensions100            extensions = data.get("extensions", {})101            cost_info = extensions.get("cost", {})102            throttle_status = cost_info.get("throttleStatus", {})103            104            if throttle_status:105                currently_available = throttle_status.get("currentlyAvailable", 0)106                max_available = throttle_status.get("maximumAvailable", 1000)107                restore_rate = throttle_status.get("restoreRate", 50)108                requested_cost = cost_info.get("requestedQueryCost", 0)109                actual_cost = cost_info.get("actualQueryCost", 0)110                print(f"  💰 API Budget: {currently_available}/{max_available} (restore: {restore_rate}/s, query cost: {actual_cost})", flush=True)111            112            if "errors" in data:113                # Check for THROTTLED error114                is_throttled = False115                for error in data["errors"]:116                    if error.get("extensions", {}).get("code") == "THROTTLED" or "Rate limited" in error.get("message", ""):117                        is_throttled = True118                        break119                120                if is_throttled:121                    # ShopifyQL has server-side throttle SEPARATE from token bucket.122                    # Even with full budget (19999/20000), queries get throttled server-side.123                    # Minimum 60s wait is needed between retries.124                    wait_time = min(60 * (attempt + 1), 300)  # 60s, 120s, 180s... max 300s125                    print(f"⏳ Throttled. Waiting {wait_time:.0f}s before retry {attempt+1}/{max_retries}...", flush=True)126                    time.sleep(wait_time)127                    continue128                129                print(f"❌ GraphQL Error: {json.dumps(data['errors'], indent=2)}", flush=True)130                return pd.DataFrame()131            132            result = data.get("data", {}).get("shopifyqlQuery", {})133            134            if result.get("parseErrors"):135                print(f"❌ ShopifyQL Parse Error: {result['parseErrors']}")136                return pd.DataFrame()137                138            table_data = result.get("tableData")139            if not table_data:140                return pd.DataFrame()141                142            columns_info = table_data.get("columns", [])143            column_names = [col["name"] for col in columns_info]144            rows = table_data.get("rows", [])145            146            df = pd.DataFrame(rows, columns=column_names)147            148            # Convert data types149            for col in columns_info:150                col_name = col["name"]151                dtype = col["dataType"]152                153                if dtype in ["MONEY", "INTEGER", "DECIMAL"]:154                    df[col_name] = pd.to_numeric(df[col_name], errors='coerce')155                elif dtype in ["DATE", "DAY_TIMESTAMP"]:156                    df[col_name] = pd.to_datetime(df[col_name], errors='coerce')157            158            return df159 160        except Exception as e:161            print(f"❌ Request Error: {e}")162            if attempt < max_retries - 1:163                time.sleep(base_delay)164                continue165            return pd.DataFrame()166            167    print("❌ Max retries exceeded.")168    return pd.DataFrame()169 170# --- Report Functions (From User's Shopify Reports) ---171 172def get_unified_sales_data(start_date, end_date):173    """174    Unified sales query - Combines multiple sales queries into one175    Reduces API calls from 4+ to 1176    Returns all sales metrics in a single DataFrame177    """178    query = f"""179    FROM sales180    SHOW 181        gross_sales,182        net_sales,183        total_sales,184        discounts,185        orders,186        average_order_value,187        quantity_ordered_per_order,188        returning_customers,189        customers,190        returning_customer_rate191    WHERE excludes_post_order_adjustments = true192    TIMESERIES day193    WITH TOTALS, PERCENT_CHANGE194    SINCE {start_date} UNTIL {end_date}195    COMPARE TO previous_period196    ORDER BY day ASC197    LIMIT 1000198    """199    return execute_shopifyql(query)200 201 202def get_total_sales_breakdown(start_date, end_date):203    """Uses unified query for better performance"""204    unified_data = get_unified_sales_data(start_date, end_date)205    if not unified_data.empty:206        return unified_data[['day', 'gross_sales', 'net_sales', 'total_sales', 'discounts', 'orders', 'average_order_value']]207    return unified_data208 209def get_average_order_value_trend(start_date, end_date):210    """Uses unified query for better performance"""211    unified_data = get_unified_sales_data(start_date, end_date)212    if not unified_data.empty:213        return unified_data[['day', 'average_order_value', 'orders', 'gross_sales']]214    return unified_data215 216def get_quantity_per_order_trend(start_date, end_date):217    """Uses unified query for better performance"""218    unified_data = get_unified_sales_data(start_date, end_date)219    if not unified_data.empty:220        return unified_data[['day', 'quantity_ordered_per_order']]221    return unified_data222 223def get_visitors_over_time(start_date, end_date):224    """Zamana göre ziyaretçiler"""225    query = f"""226    FROM sessions227    SHOW online_store_visitors, sessions228    WHERE human_or_bot_session IN ('human', 'bot')229    TIMESERIES day230    WITH TOTALS, PERCENT_CHANGE231    SINCE {start_date} UNTIL {end_date}232    COMPARE TO previous_period233    ORDER BY day ASC234    LIMIT 1000235    """236    return execute_shopifyql(query)237 238def get_marketing_attributed_sales(start_date, end_date):239    """Pazarlamayla ilişkilendirilen satışlar"""240    query = f"""241    FROM sales242    SHOW orders, total_sales, gross_sales, net_sales243    WHERE referring_channel IS NOT NULL244      AND referring_channel != 'direct'245    GROUP BY referring_channel, referring_medium246    WITH TOTALS247    SINCE {start_date} UNTIL {end_date}248    COMPARE TO previous_period249    ORDER BY total_sales DESC250    LIMIT 1000251    """252    return execute_shopifyql(query)253 254def get_sessions_by_device(start_date, end_date):255    """Cihaz türüne göre oturumlar"""256    query = f"""257    FROM sessions258    SHOW online_store_visitors, sessions259    WHERE session_device_type IS NOT NULL260      AND human_or_bot_session IN ('human', 'bot')261    GROUP BY session_device_type262    WITH TOTALS263    SINCE {start_date} UNTIL {end_date}264    ORDER BY sessions DESC265    LIMIT 1000266    """267    return execute_shopifyql(query)268 269def get_current_visitors():270    """Şu anki ziyaretçiler (real-time) - Son 60 dakika"""271    query = """272    FROM sessions273    SHOW online_store_visitors274    WHERE human_or_bot_session IN ('human', 'bot')275    SINCE startOfMinute(-60min) UNTIL now276    LIMIT 100277    """278    return execute_shopifyql(query)279 280def get_conversion_rate_monitoring(date="today"):281    """Conversion rate monitoring (hourly for US)"""282    query = f"""283    FROM sessions284    SHOW sessions, sessions_with_cart_additions, sessions_that_reached_checkout,285      sessions_that_completed_checkout, conversion_rate286    WHERE human_or_bot_session IN ('human')287      AND session_country = 'United States'288    TIMESERIES hour289    WITH TOTALS, CURRENCY 'USD'290    DURING {date}291    ORDER BY hour ASC292    LIMIT 1000293    """294    return execute_shopifyql(query)295 296def get_conversion_rate_over_time(start_date, end_date):297    """Zamana göre dönüşüm oranı - United States, Human sessions only"""298    query = f"""299    FROM sessions300    SHOW sessions, sessions_with_cart_additions, sessions_that_reached_checkout,301      sessions_that_completed_checkout, conversion_rate302    WHERE human_or_bot_session IN ('human')303      AND session_country = 'United States'304    TIMESERIES day305    WITH TOTALS, PERCENT_CHANGE, CURRENCY 'USD'306    SINCE {start_date} UNTIL {end_date}307    COMPARE TO previous_period308    ORDER BY day ASC309    LIMIT 1000310    """311    return execute_shopifyql(query)312 313def get_birthstone_conversion_rate(date="today"):314    """Birthstone conversion rate monitoring"""315    query = f"""316    FROM sessions317    SHOW sessions, sessions_with_cart_additions, sessions_that_reached_checkout,318      sessions_that_completed_checkout, conversion_rate319    WHERE human_or_bot_session IN ('human')320      AND session_country = 'United States'321      AND landing_page_url CONTAINS 'birthstone'322    TIMESERIES hour323    WITH TOTALS, CURRENCY 'USD'324    DURING {date}325    ORDER BY hour ASC326    LIMIT 1000327    """328    return execute_shopifyql(query)329 330def get_returning_customer_rate(start_date, end_date):331    """Uses unified query for better performance"""332    unified_data = get_unified_sales_data(start_date, end_date)333    if not unified_data.empty:334        return unified_data[['day', 'returning_customers', 'customers', 'returning_customer_rate']]335    return unified_data336 337def get_returning_customer_rate_excluding_try_on(months_back=12):338    # Returning Customer Rate (Excluding Home Try On & Exchanges)339    query = f"""340    FROM sales341    SHOW sales, orders, returning_customer_sales, returning_customer_orders342    WHERE line_item_requires_shipping == true343      AND product_id NOT IN (4582606004304, 6639950553168)344      AND total_discounts_set_amount < (total_price_set_amount + discount_applications_value)345      AND transaction_kind IN ('sale')346    SINCE -{months_back}month UNTIL today347    """348    return execute_shopifyql(query)349 350def get_rfm_customer_analysis(start_date=None, end_date=None):351    """352    RFM Müşteri Analizi - Parçalı sorgu yaklaşımı.353    Shopify, customers tablosunda çok alanlı sorguları dahili throttle ile engelliyor.354    Bu yüzden 3 ayrı basit sorgu yapıp birleştiriyoruz.355    """356    import time as _time357    358    base = "FROM customers"359    group_order = "GROUP BY rfm_group ORDER BY new_customer_records DESC LIMIT 20"360    361    # Part 1: Counts and percentages362    q1 = f"{base} SHOW percent_of_customers, new_customer_records {group_order}"363    df1 = execute_shopifyql(q1)364    365    if df1 is None or df1.empty:366        return pd.DataFrame()367    368    _time.sleep(30)  # Let Shopify's internal throttle cool down369    370    # Part 2: Order counts371    q2 = f"{base} SHOW new_customer_records, total_number_of_orders {group_order}"372    df2 = execute_shopifyql(q2)373    374    _time.sleep(30)375    376    # Part 3: Amount spent377    q3 = f"{base} SHOW new_customer_records, total_amount_spent {group_order}"378    df3 = execute_shopifyql(q3)379    380    # Merge results381    combined = df1.copy()382    if df2 is not None and not df2.empty and 'total_number_of_orders' in df2.columns:383        combined = combined.merge(df2[['rfm_group', 'total_number_of_orders']], on='rfm_group', how='left')384    if df3 is not None and not df3.empty and 'total_amount_spent' in df3.columns:385        combined = combined.merge(df3[['rfm_group', 'total_amount_spent']], on='rfm_group', how='left')386    387    return combined388 389 390# Backward compatibility aliases391get_sales_report = get_total_sales_breakdown392get_visits_report = get_visitors_over_time393get_top_channels_report = get_marketing_attributed_sales394get_device_report = get_sessions_by_device395get_conversion_report = get_conversion_rate_over_time396 397if __name__ == "__main__":398    print("Testing ShopifyQL with corrected field names...")399    400    # Test device report401    print("\n1. Testing Device Report...")402    df_devices = get_sessions_by_device("-7d", "today")403    if not df_devices.empty:404        print(f"✅ Device Report: {len(df_devices)} rows")405        print(df_devices)406    407    # Test marketing channels408    print("\n2. Testing Marketing Channels...")409    df_channels = get_marketing_attributed_sales("-30d", "today")410    if not df_channels.empty:411        print(f"✅ Marketing Channels: {len(df_channels)} rows")412        print(df_channels.head())413