9292 AdsModerateRequest ,
9393 AdsModerateResult ,
9494 ProviderWarning ,
95+ # Autotargeting
96+ YandexAutotargetingReadResult ,
97+ YandexAutotargetingRequest ,
98+ YandexAutotargetingResult ,
9599)
96100from app .store import store
97101from app .yandex_direct import YandexDirectClient , YandexDirectError
@@ -2754,8 +2758,14 @@ def yandex_time_targeting(
27542758
27552759def _build_strategy_summary (
27562760 strategy : dict | None ,
2761+ priority_goals : dict | None = None ,
27572762) -> dict | None :
2758- """Build a human-readable strategy summary from a raw BiddingStrategy block."""
2763+ """Build a human-readable strategy summary from a raw BiddingStrategy block.
2764+
2765+ ``priority_goals`` is the optional raw ``TextCampaign.PriorityGoals``
2766+ dict (``{\" Items\" : [{GoalId, Value}, ...]}`` as returned by v5 readback).
2767+ Values are converted from Direct micros to RUBLES.
2768+ """
27592769 import re
27602770
27612771 if not isinstance (strategy , dict ):
@@ -2794,6 +2804,21 @@ def _to_snake(name: str) -> str:
27942804 "type" : network .get ("BiddingStrategyType" , "UNKNOWN" ),
27952805 }
27962806
2807+ # Add PriorityGoals summary if present
2808+ if priority_goals is not None and isinstance (priority_goals , dict ):
2809+ items = priority_goals .get ("Items" )
2810+ if isinstance (items , list ) and items :
2811+ summary ["priority_goals" ] = []
2812+ for item in items :
2813+ if isinstance (item , dict ) and "GoalId" in item :
2814+ pg : dict [str , Any ] = {"goal_id" : item ["GoalId" ]}
2815+ if "Value" in item :
2816+ try :
2817+ pg ["value_rub" ] = float (item ["Value" ]) / 1_000_000
2818+ except (TypeError , ValueError ):
2819+ pg ["value" ] = item ["Value" ]
2820+ summary ["priority_goals" ].append (pg )
2821+
27972822 return summary if summary else None
27982823
27992824
@@ -2839,6 +2864,7 @@ def yandex_strategy_read(
28392864 tc = camp .get ("TextCampaign" )
28402865 raw_counter_ids = tc .get ("CounterIds" ) if isinstance (tc , dict ) else None
28412866 raw_strategy = tc .get ("BiddingStrategy" ) if isinstance (tc , dict ) else None
2867+ raw_priority_goals = tc .get ("PriorityGoals" ) if isinstance (tc , dict ) else None
28422868 else :
28432869 campaign_name = None
28442870 campaign_type = None
@@ -2847,6 +2873,7 @@ def yandex_strategy_read(
28472873 raw_daily_budget = None
28482874 raw_counter_ids = None
28492875 raw_strategy = None
2876+ raw_priority_goals = None
28502877
28512878 daily_budget = (
28522879 dict (raw_daily_budget )
@@ -2861,7 +2888,9 @@ def yandex_strategy_read(
28612888 strategy = (
28622889 dict (raw_strategy ) if isinstance (raw_strategy , dict ) else None
28632890 )
2864- strategy_summary = _build_strategy_summary (strategy )
2891+ strategy_summary = _build_strategy_summary (
2892+ strategy , priority_goals = raw_priority_goals
2893+ )
28652894
28662895 return YandexStrategyReadResult (
28672896 campaign_id = campaign_id ,
@@ -2873,6 +2902,7 @@ def yandex_strategy_read(
28732902 status = str (status ) if status else None ,
28742903 daily_budget = daily_budget ,
28752904 counter_ids = counter_ids ,
2905+ priority_goals = raw_priority_goals ,
28762906 strategy = strategy ,
28772907 strategy_summary = strategy_summary ,
28782908 )
@@ -2888,6 +2918,7 @@ def yandex_strategy_read(
28882918 status = mock .get ("status" ),
28892919 daily_budget = mock .get ("daily_budget" ),
28902920 counter_ids = mock .get ("counter_ids" ),
2921+ priority_goals = mock .get ("priority_goals" ),
28912922 strategy = mock .get ("strategy" ),
28922923 strategy_summary = mock .get ("strategy_summary" ),
28932924 )
@@ -2979,6 +3010,129 @@ def yandex_strategy_update(
29793010 ) from exc
29803011
29813012
3013+ # ---------------------------------------------------------------------------
3014+ # Autotargeting settings read (GET) / update (POST)
3015+ # ---------------------------------------------------------------------------
3016+
3017+
3018+ @app .get (
3019+ "/yandex/campaigns/{campaign_id}/autotargeting" ,
3020+ response_model = YandexAutotargetingReadResult ,
3021+ )
3022+ def yandex_autotargeting_read (
3023+ campaign_id : str ,
3024+ settings : Settings = Depends (get_settings ),
3025+ client : YandexDirectClient | None = Depends (get_yandex_client ),
3026+ ) -> YandexAutotargetingReadResult :
3027+ """Read autotargeting settings for all ad groups in a campaign.
3028+
3029+ Pure read-only — no write gate. Available in all modes.
3030+ Returns per-ad-group autotargeting categories and brand options
3031+ from the ``---autotargeting`` keyword rows.
3032+ """
3033+ try :
3034+ return store .yandex_autotargeting_read (
3035+ campaign_id , settings = settings , client = client
3036+ )
3037+ except YandexDirectError as exc :
3038+ raise _yandex_error_to_502 (exc ) from exc
3039+
3040+
3041+ @app .post (
3042+ "/yandex/campaigns/{campaign_id}/autotargeting" ,
3043+ response_model = YandexAutotargetingResult ,
3044+ )
3045+ def yandex_autotargeting_update (
3046+ campaign_id : str ,
3047+ payload : YandexAutotargetingRequest ,
3048+ settings : Settings = Depends (get_settings ),
3049+ client : YandexDirectClient | None = Depends (get_yandex_client ),
3050+ ) -> YandexAutotargetingResult :
3051+ """Update autotargeting settings for ad groups in a campaign.
3052+
3053+ Standard product gate contract:
3054+ ``dry_run=True`` (default) is preview-only and never performs a network
3055+ write; the response includes the exact v5 ``keywords.update`` payload
3056+ that WOULD be sent, with ``applied=False``.
3057+
3058+ ``dry_run=False`` requires ``DIRECTPILOT_MODE=live_write``,
3059+ ``approved=True`` and a valid ``idempotency_key``.
3060+
3061+ Categories are always sent with all five booleans explicitly (``YES``
3062+ or ``NO``) to avoid the Direct API pitfall where missing categories
3063+ default to ``YES``.
3064+
3065+ Default preset for local service-search campaigns: ``exact_narrow``
3066+ (Exact=YES, Narrow=YES, Alternative=NO, Accessory=NO, Broader=NO).
3067+ Brand options default: WithoutBrands=YES, WithAdvertiserBrand=YES,
3068+ WithCompetitorsBrand=NO.
3069+ """
3070+ if not payload .approved :
3071+ raise HTTPException (
3072+ status_code = 409 ,
3073+ detail = "Action requires explicit approval before autotargeting update" ,
3074+ )
3075+ if not payload .dry_run and settings .directpilot_mode != "live_write" :
3076+ raise HTTPException (
3077+ status_code = 409 ,
3078+ detail = (
3079+ f"Live writes require DIRECTPILOT_MODE=live_write; "
3080+ f"current mode is { settings .directpilot_mode !r} ; "
3081+ f"autotargeting apply is not allowed in this mode "
3082+ f"(dry_run=True is the only allowed path)"
3083+ ),
3084+ )
3085+ try :
3086+ return store .yandex_autotargeting_update (
3087+ campaign_id , payload , settings = settings , client = client
3088+ )
3089+ except ValueError as exc :
3090+ raise HTTPException (status_code = 409 , detail = str (exc )) from exc
3091+ except YandexDirectError as exc :
3092+ diagnostics = exc .diagnostics or {}
3093+ detail : dict [str , Any ] = {
3094+ "error_type" : "YandexDirectError" ,
3095+ "message" : str (exc ),
3096+ }
3097+ if "error_code" in diagnostics :
3098+ detail ["error_code" ] = diagnostics ["error_code" ]
3099+ if "error_detail" in diagnostics :
3100+ detail ["error_detail" ] = diagnostics ["error_detail" ]
3101+ if "payload_preview" in diagnostics :
3102+ detail ["payload_preview" ] = diagnostics ["payload_preview" ]
3103+ raise HTTPException (status_code = 502 , detail = detail ) from exc
3104+ except Exception as exc :
3105+ try :
3106+ store .append_audit (
3107+ "yandex_autotargeting_failed" ,
3108+ campaign_id ,
3109+ dry_run = False ,
3110+ details = {
3111+ "campaign_id" : campaign_id ,
3112+ "approved" : payload .approved ,
3113+ "idempotency_key" : payload .idempotency_key ,
3114+ "endpoint_safety_net" : True ,
3115+ "yandex_error" : (
3116+ f"unexpected error in autotargeting endpoint: "
3117+ f"{ type (exc ).__name__ } : { exc } "
3118+ ),
3119+ "exception_type" : type (exc ).__name__ ,
3120+ },
3121+ )
3122+ except Exception :
3123+ pass
3124+ raise HTTPException (
3125+ status_code = 502 ,
3126+ detail = {
3127+ "error_type" : "YandexDirectError" ,
3128+ "message" : (
3129+ f"unexpected error during autotargeting update: "
3130+ f"{ type (exc ).__name__ } "
3131+ ),
3132+ },
3133+ ) from exc
3134+
3135+
29823136# ---------------------------------------------------------------------------
29833137# Yandex Metrika — read-only (counters, goals, summary, traffic-sources)
29843138#
0 commit comments