2026-06-16 15:07:59 -04:00
import csv
import os
2026-06-25 11:31:20 -04:00
import re
2026-06-16 15:07:59 -04:00
from dataclasses import dataclass
from pathlib import Path
2026-06-25 11:31:20 -04:00
from typing import Any
2026-07-29 11:08:17 -04:00
from db import DUT , REF , TestRecord
from file_manager import (
open_csv_handle
)
2026-06-16 15:07:59 -04:00
2026-07-29 11:08:17 -04:00
TEST_TYPES = { " P2P " , " COE " , " P3P " }
POWER_MODES = { " LPI " , " SP " }
RX_TX = { " RX " , " TX " }
2026-06-16 15:07:59 -04:00
2026-06-16 16:09:54 -04:00
P2P_COE_REQUIRED_COLUMNS = [
2026-06-16 15:07:59 -04:00
" Priority " ,
" Index " ,
" Interferer " ,
" COE Pair " ,
" Rotation " ,
" TC ID " ,
" Victim Band " ,
" 6GHz Power Mode " ,
2026-07-12 14:19:58 -04:00
" 5G_Test Point " ,
" 5G_Channel " ,
" 5G_Bandwidth " ,
" 5G_RSSI " ,
" 5G_Direction " ,
" 5G_STA " ,
" 6G_Test Point " ,
" 6G_Channel " ,
" 6G_Bandwidth " ,
" 6G_RSSI " ,
" 6G_Direction " ,
" 6G_STA " ,
" 2G_Test Point " ,
" 2G_Channel " ,
" 2G_Bandwidth " ,
" 2G_RSSI " ,
" 2G_Direction " ,
" 2G_STA " ,
2026-06-16 15:07:59 -04:00
]
2026-06-16 16:09:54 -04:00
P3P_REQUIRED_COLUMNS = [
" Priority " ,
" Index " ,
" Throttled " ,
" Rotation " ,
" TC ID " ,
" Band " ,
" 6GHz Power Mode " ,
2026-07-12 14:19:58 -04:00
" STATION 1_Test Point " ,
" STATION 1_Channel " ,
" STATION 1_Bandwidth " ,
" STATION 1_RSSI " ,
" STATION 1_Direction " ,
" STATION 1_Rate " ,
" STATION 1_STA " ,
" STATION 2_Test Point " ,
" STATION 2_Channel " ,
" STATION 2_Bandwidth " ,
" STATION 2_RSSI " ,
" STATION 2_Direction " ,
" STATION 2_Rate " ,
" STATION 2_STA " ,
" STATION 3_Test Point " ,
" STATION 3_Channel " ,
" STATION 3_Bandwidth " ,
" STATION 3_RSSI " ,
" STATION 3_Direction " ,
" STATION 3_Rate " ,
" STATION 3_STA " ,
2026-06-16 16:09:54 -04:00
]
2026-06-16 15:07:59 -04:00
RUNTIME_DEFAULTS = {
" P2P " : 80 ,
" COE " : 115 ,
" P3P " : 105 ,
}
@dataclass ( frozen = True )
class ParseResult :
tests : list [ TestRecord ]
warnings : list [ str ]
class CsvValidationError ( ValueError ) :
pass
2026-06-25 11:31:20 -04:00
_SMB_SESSIONS : set [ str ] = set ( )
2026-06-16 16:09:54 -04:00
def _columns_missing ( normalized_fieldnames : set [ str ] , required_columns : list [ str ] ) - > list [ str ] :
return [
column
for column in required_columns
2026-07-13 16:17:36 -04:00
if _normalize_column_name ( column ) not in normalized_fieldnames
2026-06-16 16:09:54 -04:00
]
def _detect_csv_format ( normalized_fieldnames : set [ str ] ) - > str :
p2p_coe_missing = _columns_missing ( normalized_fieldnames , P2P_COE_REQUIRED_COLUMNS )
if not p2p_coe_missing :
return " p2p_coe "
p3p_missing = _columns_missing ( normalized_fieldnames , P3P_REQUIRED_COLUMNS )
if not p3p_missing :
return " p3p "
2026-06-16 15:07:59 -04:00
2026-06-16 16:09:54 -04:00
raise CsvValidationError (
" CSV format not recognized. Missing columns for P2P/COE format: "
f " { ' , ' . join ( p2p_coe_missing ) } ; missing columns for P3P format: { ' , ' . join ( p3p_missing ) } "
)
2026-06-25 11:31:20 -04:00
def parse_target_csv (
2026-07-29 11:08:17 -04:00
paths : str | Path | list [ str | Path ] | tuple [ str | Path , . . . ] ,
2026-06-25 11:31:20 -04:00
smb_credentials : dict [ str , Any ] | None = None ,
runtime_overrides : dict [ str , Any ] | None = None ,
2026-07-29 11:08:17 -04:00
) - > dict [ str , Any ] :
2026-07-13 02:43:52 -04:00
runtime_defaults_by_device = _resolve_runtime_defaults ( runtime_overrides )
2026-07-29 11:08:17 -04:00
results = { }
if isinstance ( paths , ( str , Path ) ) :
paths = [ paths ]
2026-06-16 16:09:54 -04:00
for path in paths :
2026-07-29 11:08:17 -04:00
csv_result = _parse_single_csv (
2026-07-13 02:43:52 -04:00
path ,
smb_credentials = smb_credentials ,
runtime_defaults_by_device = runtime_defaults_by_device ,
)
2026-06-16 16:09:54 -04:00
2026-07-29 11:08:17 -04:00
results . update ( csv_result )
return results
2026-06-16 16:09:54 -04:00
2026-07-13 02:43:52 -04:00
def _coerce_runtime_defaults ( raw_overrides : dict [ str , Any ] | None ) - > dict [ str , int ] :
2026-06-25 11:31:20 -04:00
defaults = dict ( RUNTIME_DEFAULTS )
2026-07-13 02:43:52 -04:00
if not raw_overrides :
2026-06-25 11:31:20 -04:00
return defaults
for test_type in ( " P2P " , " COE " , " P3P " ) :
2026-07-13 02:43:52 -04:00
raw_value = raw_overrides . get ( test_type )
2026-06-25 11:31:20 -04:00
if raw_value is None :
continue
if isinstance ( raw_value , str ) :
raw_value = raw_value . strip ( )
if not raw_value :
continue
try :
minutes = int ( raw_value )
except ( TypeError , ValueError ) :
continue
if minutes > 0 :
defaults [ test_type ] = minutes
return defaults
2026-07-13 02:43:52 -04:00
def _resolve_runtime_defaults ( runtime_overrides : dict [ str , Any ] | None ) - > dict [ str , dict [ str , int ] ] :
legacy_defaults = _coerce_runtime_defaults ( runtime_overrides )
if not runtime_overrides :
return {
2026-07-29 11:08:17 -04:00
DUT : dict ( legacy_defaults ) ,
REF : dict ( legacy_defaults ) ,
2026-07-13 02:43:52 -04:00
}
resolved : dict [ str , dict [ str , int ] ] = { }
2026-07-29 11:08:17 -04:00
for device in ( DUT , REF ) :
2026-07-13 02:43:52 -04:00
device_defaults = dict ( legacy_defaults )
raw_device_overrides = runtime_overrides . get ( device )
if isinstance ( raw_device_overrides , dict ) :
device_defaults . update ( _coerce_runtime_defaults ( raw_device_overrides ) )
resolved [ device ] = device_defaults
return resolved
2026-06-25 11:31:20 -04:00
def _parse_single_csv (
path : str ,
smb_credentials : dict [ str , Any ] | None ,
2026-07-13 02:43:52 -04:00
runtime_defaults_by_device : dict [ str , dict [ str , int ] ] ,
2026-07-29 11:08:17 -04:00
) - > dict [ str , Any ] :
records : dict [ str , Any ] = { }
with open_csv_handle ( path , smb_credentials = smb_credentials ) as handle :
2026-06-16 15:07:59 -04:00
reader = csv . DictReader ( handle )
if not reader . fieldnames :
raise CsvValidationError ( " CSV is missing a header row. " )
2026-07-13 16:17:36 -04:00
normalized_fieldnames = { _normalize_column_name ( name ) for name in reader . fieldnames if name }
2026-06-16 16:09:54 -04:00
csv_format = _detect_csv_format ( normalized_fieldnames )
2026-06-16 15:07:59 -04:00
tests : list [ TestRecord ] = [ ]
warnings : list [ str ] = [ ]
seen_test_ids : set [ str ] = set ( )
records_with_signature : list [ tuple [ TestRecord , tuple [ str , . . . ] | None ] ] = [ ]
for row_num , row in enumerate ( reader , start = 2 ) :
test_id = ( row . get ( " TC ID " ) or " " ) . strip ( )
if not test_id :
2026-07-29 11:08:17 -04:00
warnings . append ( f " File { path } , row { row_num } : missing TC ID, row skipped. " )
2026-06-16 15:07:59 -04:00
continue
if test_id in seen_test_ids :
2026-07-29 11:08:17 -04:00
warnings . append ( f " File { path } , row { row_num } : duplicate TC ID ' { test_id } ' , row skipped. " )
2026-06-16 15:07:59 -04:00
continue
seen_test_ids . add ( test_id )
2026-06-16 16:09:54 -04:00
throttled = _normalize_yes_no ( _row_get ( row , " Throttled " ) ) if csv_format == " p3p " else False
2026-06-16 15:07:59 -04:00
test_type = _infer_test_type ( test_id )
rx_tx = _infer_rx_tx ( test_id )
rotation = _empty_to_none ( _row_get ( row , " Rotation " ) )
2026-07-17 15:17:08 -04:00
power_mode = _empty_to_none ( _row_get ( row , " 6GHz Power Mode " ) )
2026-06-16 16:09:54 -04:00
has_coe_pair = _normalize_yes_no ( _row_get ( row , " COE Pair " ) ) if csv_format == " p2p_coe " else False
config = _build_config ( row , csv_format )
2026-07-17 15:17:08 -04:00
signature = _first_populated_p2p_signature ( row ) if csv_format == " p2p_coe " and test_type == " P2P " else None
2026-06-16 16:09:54 -04:00
victim_band_source = " Victim Band " if csv_format == " p2p_coe " else " Band "
victim_band = _normalize_victim_band ( _row_get ( row , victim_band_source ) )
2026-07-29 11:08:17 -04:00
records [ test_id ] = {
" test_id " : test_id ,
" test_type " : test_type ,
" rotation " : rotation ,
" rx_tx " : rx_tx ,
" power_mode " : power_mode ,
" has_coe_pair " : has_coe_pair ,
" coe_pairing " : [ ] ,
" victim_band " : victim_band ,
" config " : config ,
" throttled " : throttled ,
" estimated_minutes " : { device : defaults [ test_type ] for device , defaults in runtime_defaults_by_device . items ( ) } ,
" status " : " pending " ,
" excluded " : False ,
" raw_payload " : row ,
}
records_with_signature . append ( ( records [ test_id ] , signature ) )
2026-06-16 15:07:59 -04:00
2026-07-17 15:17:08 -04:00
# Build COE pairing based on full signature and RX/TX+band suffix.
2026-06-16 15:07:59 -04:00
# Example key suffixes: RXAX, TXAX, RXBE, TXBE.
2026-07-17 15:17:08 -04:00
coe_by_signature_and_suffix : dict [ tuple [ tuple [ str , . . . ] , str ] , list [ str ] ] = { }
2026-06-16 15:07:59 -04:00
for record , signature in records_with_signature :
2026-07-29 11:08:17 -04:00
if record [ " test_type " ] == " COE " :
suffix = _extract_pairing_suffix ( record [ " test_id " ] )
2026-07-17 15:17:08 -04:00
if not suffix :
continue
# Index all populated COE band signatures so any one can match the selected P2P signature.
2026-07-29 11:08:17 -04:00
for coe_signature in _all_band_signatures ( record [ " raw_payload " ] or { } ) :
2026-07-17 15:17:08 -04:00
key = ( coe_signature , suffix )
2026-07-29 11:08:17 -04:00
coe_by_signature_and_suffix . setdefault ( key , [ ] ) . append ( record [ " test_id " ] )
2026-06-16 15:07:59 -04:00
for record , signature in records_with_signature :
2026-07-29 11:08:17 -04:00
pairs = record [ " coe_pairing " ]
if record [ " test_type " ] == " P2P " and signature is not None :
suffix = _extract_pairing_suffix ( record [ " test_id " ] )
2026-06-16 15:07:59 -04:00
key = ( signature , suffix ) if suffix else None
2026-07-29 11:08:17 -04:00
print ( f " [parser] P2P test { record [ ' test_id ' ] } with signature { signature } and suffix { suffix } has COE pairs: { coe_by_signature_and_suffix . get ( key , [ ] ) } " )
2026-07-17 15:17:08 -04:00
pairs = sorted ( set ( coe_by_signature_and_suffix . get ( key , [ ] ) ) ) if key else [ ]
2026-06-16 15:07:59 -04:00
2026-07-29 11:08:17 -04:00
records [ record [ " test_id " ] ] . update (
has_coe_pair = bool ( pairs ) ,
coe_pairing = pairs ,
)
return records
2026-06-16 15:07:59 -04:00
def _infer_test_type ( test_id : str ) - > str :
token = test_id . upper ( )
if token . startswith ( " COE " ) :
return " COE "
if token . startswith ( " P3P " ) :
return " P3P "
return " P2P "
def _infer_rx_tx ( test_id : str ) - > str | None :
token = test_id . upper ( )
if " RX " in token :
return " RX "
if " TX " in token :
return " TX "
return None
def _extract_pairing_suffix ( test_id : str ) - > str | None :
""" Extract the RX/TX+band suffix used for pairing.
Format: [COE|P2P][RX|TX][Band][Number]
Example: P2PRXAC004 -> ' RXAC ' , COETXAX012 -> ' TXAX '
"""
token = test_id . upper ( )
# Remove COE/P2P prefix
if token . startswith ( " COE " ) :
token = token [ 3 : ]
elif token . startswith ( " P2P " ) or token . startswith ( " P3P " ) :
token = token [ 3 : ]
else :
return None
# Extract RX/TX + band (for example RXAX, TXBE).
if len ( token ) > = 4 and ( token . startswith ( " RX " ) or token . startswith ( " TX " ) ) :
return token [ : 4 ]
return None
def _empty_to_none ( value : str | None ) - > str | None :
if value is None :
return None
cleaned = value . strip ( )
return cleaned if cleaned else None
2026-07-17 15:17:08 -04:00
def _first_populated_p2p_signature ( row : dict [ str , str ] ) - > tuple [ str , . . . ] | None :
for band in ( " 5G " , " 6G " , " 2G " ) :
signature = _band_signature ( row , band )
if signature is not None :
return signature
return None
def _band_signature ( row : dict [ str , str ] , band : str ) - > tuple [ str , . . . ] | None :
normalized_band = _normalize_victim_band ( band )
if normalized_band is None :
2026-06-16 15:07:59 -04:00
return None
2026-07-12 14:19:58 -04:00
# Source CSVs may use either "5G_Test Point" or "5G Test Point" style headers.
2026-07-17 15:17:08 -04:00
prefixes = ( f " { normalized_band } _ " , f " { normalized_band } " )
2026-07-12 14:19:58 -04:00
def _pick_value ( suffix : str ) - > str :
for prefix in prefixes :
value = _row_get ( row , f " { prefix } { suffix } " )
if _normalize_value ( value ) :
return _normalize_value ( value )
return " "
test_point = _pick_value ( " Test Point " )
channel = _pick_value ( " Channel " )
rssi = _pick_value ( " RSSI " )
bandwidth = _pick_value ( " Bandwidth " )
2026-07-23 14:02:14 -04:00
# Pairing should ignore direction so reverse-signed COE and P2P rows still match.
2026-07-17 15:17:08 -04:00
sta = _pick_value ( " STA " )
2026-06-16 15:07:59 -04:00
2026-07-23 14:02:14 -04:00
if not all ( [ test_point , channel , rssi , bandwidth , sta ] ) :
2026-06-16 15:07:59 -04:00
return None
2026-07-23 14:02:14 -04:00
return ( test_point , channel , bandwidth , rssi , sta )
2026-07-17 15:17:08 -04:00
def _all_band_signatures ( row : dict [ str , str ] ) - > list [ tuple [ str , . . . ] ] :
seen : set [ tuple [ str , . . . ] ] = set ( )
signatures : list [ tuple [ str , . . . ] ] = [ ]
for band in ( " 5G " , " 6G " , " 2G " ) :
signature = _band_signature ( row , band )
if signature is not None and signature not in seen :
seen . add ( signature )
signatures . append ( signature )
return signatures
2026-06-16 15:07:59 -04:00
def _normalize_victim_band ( value : str | None ) - > str | None :
if value is None :
return None
normalized = value . strip ( ) . upper ( ) . replace ( " GHZ " , " G " )
if normalized . startswith ( " 5 " ) :
return " 5G "
if normalized . startswith ( " 6 " ) :
return " 6G "
if normalized . startswith ( " 2 " ) :
return " 2G "
return None
def _normalize_value ( value : str | None ) - > str :
if value is None :
return " "
return " " . join ( value . strip ( ) . upper ( ) . split ( ) )
2026-07-13 16:17:36 -04:00
def _normalize_column_name ( value : str | None ) - > str :
if value is None :
return " "
# Accept common header variants, for example: "TC ID", "TC_ID", or "TC\u00A0ID".
return re . sub ( r " [^A-Z0-9]+ " , " " , value . upper ( ) )
2026-06-16 15:07:59 -04:00
def _normalize_yes_no ( value : str | None ) - > bool :
return _normalize_value ( value ) == " YES "
2026-07-29 11:08:17 -04:00
def _build_p2p_coe_config ( row : dict [ str , str ] ) - > dict [ str , dict [ str , str | None ] ] :
2026-06-16 15:07:59 -04:00
return {
" 5G " : {
2026-07-12 14:19:58 -04:00
" test_point " : _empty_to_none ( _row_get ( row , " 5G_Test Point " ) ) ,
" channel " : _empty_to_none ( _row_get ( row , " 5G_Channel " ) ) ,
" bandwidth " : _empty_to_none ( _row_get ( row , " 5G_Bandwidth " ) ) ,
" rssi " : _empty_to_none ( _row_get ( row , " 5G_RSSI " ) ) ,
" direction " : _empty_to_none ( _row_get ( row , " 5G_Direction " ) ) ,
" sta " : _empty_to_none ( _row_get ( row , " 5G_STA " ) ) ,
2026-06-16 15:07:59 -04:00
} ,
" 6G " : {
2026-07-12 14:19:58 -04:00
" test_point " : _empty_to_none ( _row_get ( row , " 6G_Test Point " ) ) ,
" channel " : _empty_to_none ( _row_get ( row , " 6G_Channel " ) ) ,
" bandwidth " : _empty_to_none ( _row_get ( row , " 6G_Bandwidth " ) ) ,
" rssi " : _empty_to_none ( _row_get ( row , " 6G_RSSI " ) ) ,
" direction " : _empty_to_none ( _row_get ( row , " 6G_Direction " ) ) ,
" sta " : _empty_to_none ( _row_get ( row , " 6G_STA " ) ) ,
2026-06-16 15:07:59 -04:00
} ,
" 2G " : {
2026-07-12 14:19:58 -04:00
" test_point " : _empty_to_none ( _row_get ( row , " 2G_Test Point " ) ) ,
" channel " : _empty_to_none ( _row_get ( row , " 2G_Channel " ) ) ,
" bandwidth " : _empty_to_none ( _row_get ( row , " 2G_Bandwidth " ) ) ,
" rssi " : _empty_to_none ( _row_get ( row , " 2G_RSSI " ) ) ,
" direction " : _empty_to_none ( _row_get ( row , " 2G_Direction " ) ) ,
" sta " : _empty_to_none ( _row_get ( row , " 2G_STA " ) ) ,
2026-06-16 15:07:59 -04:00
} ,
}
2026-06-16 16:09:54 -04:00
def _build_p3p_config ( row : dict [ str , str ] ) - > dict [ str , dict [ str , str | None ] ] :
return {
2026-07-12 14:19:58 -04:00
" STATION1 " : {
" test_point " : _empty_to_none ( _row_get ( row , " STATION 1_Test Point " ) ) ,
" channel " : _empty_to_none ( _row_get ( row , " STATION 1_Channel " ) ) ,
" bandwidth " : _empty_to_none ( _row_get ( row , " STATION 1_Bandwidth " ) ) ,
" rssi " : _empty_to_none ( _row_get ( row , " STATION 1_RSSI " ) ) ,
" direction " : _empty_to_none ( _row_get ( row , " STATION 1_Direction " ) ) ,
" rate " : _empty_to_none ( _row_get ( row , " STATION 1_Rate " ) ) ,
" sta " : _empty_to_none ( _row_get ( row , " STATION 1_STA " ) ) ,
2026-06-16 16:09:54 -04:00
} ,
2026-07-12 14:19:58 -04:00
" STATION2 " : {
" test_point " : _empty_to_none ( _row_get ( row , " STATION 2_Test Point " ) ) ,
" channel " : _empty_to_none ( _row_get ( row , " STATION 2_Channel " ) ) ,
" bandwidth " : _empty_to_none ( _row_get ( row , " STATION 2_Bandwidth " ) ) ,
" rssi " : _empty_to_none ( _row_get ( row , " STATION 2_RSSI " ) ) ,
" direction " : _empty_to_none ( _row_get ( row , " STATION 2_Direction " ) ) ,
" rate " : _empty_to_none ( _row_get ( row , " STATION 2_Rate " ) ) ,
" sta " : _empty_to_none ( _row_get ( row , " STATION 2_STA " ) ) ,
2026-06-16 16:09:54 -04:00
} ,
2026-07-12 14:19:58 -04:00
" STATION3 " : {
" test_point " : _empty_to_none ( _row_get ( row , " STATION 3_Test Point " ) ) ,
" channel " : _empty_to_none ( _row_get ( row , " STATION 3_Channel " ) ) ,
" bandwidth " : _empty_to_none ( _row_get ( row , " STATION 3_Bandwidth " ) ) ,
" rssi " : _empty_to_none ( _row_get ( row , " STATION 3_RSSI " ) ) ,
" direction " : _empty_to_none ( _row_get ( row , " STATION 3_Direction " ) ) ,
" rate " : _empty_to_none ( _row_get ( row , " STATION 3_Rate " ) ) ,
" sta " : _empty_to_none ( _row_get ( row , " STATION 3_STA " ) ) ,
2026-06-16 16:09:54 -04:00
} ,
}
def _build_config ( row : dict [ str , str ] , csv_format : str ) - > dict [ str , dict [ str , str | None ] ] :
if csv_format == " p3p " :
return _build_p3p_config ( row )
2026-07-29 11:08:17 -04:00
return _build_p2p_coe_config ( row )
2026-06-16 16:09:54 -04:00
2026-06-16 15:07:59 -04:00
def _row_get ( row : dict [ str , str ] , key : str ) - > str | None :
if key in row :
return row . get ( key )
2026-07-13 16:17:36 -04:00
normalized_key = _normalize_column_name ( key )
2026-06-16 15:07:59 -04:00
for existing_key , value in row . items ( ) :
2026-07-13 16:17:36 -04:00
if existing_key and _normalize_column_name ( existing_key ) == normalized_key :
2026-06-16 15:07:59 -04:00
return value
2026-07-13 16:17:36 -04:00
if normalized_key == _normalize_column_name ( " COE PAIR " ) :
2026-06-16 15:07:59 -04:00
for alias in ( " COE PAIRING " , " COE_PAIRING " ) :
for existing_key , value in row . items ( ) :
2026-07-13 16:17:36 -04:00
if existing_key and _normalize_column_name ( existing_key ) == _normalize_column_name ( alias ) :
2026-06-16 15:07:59 -04:00
return value
return None
2026-07-29 11:08:17 -04:00
def parse_target_filename ( filename , parent_dir ) :
base_name = re . sub ( r " \ .ini$ " , " " , filename , flags = re . IGNORECASE ) . upper ( )
segments = base_name . split ( " _ " )
test_type = next ( ( s for s in segments if s in TEST_TYPES ) , None )
test_id = next ( ( s for s in segments if re . match ( r " ^R \ d+[A-Z0-9]+$ " , s , re . IGNORECASE ) ) , None )
device = next ( ( s for s in segments if s in ( DUT , REF ) ) , None )
if test_id is None or test_type is None or device is None :
print ( f " Warning: Could not parse test_id, test_type, or device from filename ' { filename } ' " )
return None
test_id = test_id [ 2 : ] if test_id else None
throttled = None
if test_type == " P3P " and test_id :
# P3P test_id carries throttle marker: TH = throttled, otherwise UT.
throttled = True if " TH " in test_id else False
rx_tx = " RX " if " RX " in test_id else " TX "
band = next ( ( s for s in segments if re . match ( r " ^ \ dGHZ$ " , s ) ) , None )
bandwidth = next ( ( s for s in segments if re . match ( r " ^BW \ d+$ " , s ) ) , None )
pm = next ( ( s for s in segments if s in POWER_MODES ) , None )
rotation = None
if parent_dir :
rotation = next ( ( s for s in parent_dir . split ( " _ " ) if re . match ( r " ^ROT \ d+$ " , s ) ) , None )
return {
" test_id " : test_id ,
" device " : device ,
" test_type " : test_type ,
" rotation " : rotation ,
" rx_tx " : rx_tx ,
" power_mode " : pm ,
" has_coe_pair " : None ,
" coe_pairing " : [ ] ,
" victim_band " : band ,
" config_json " : { } ,
" throttled " : throttled ,
" estimated_minutes " : None ,
" status " : " pending " ,
" excluded " : False ,
" raw_payload " : { } ,
" station_testpoint_map " : { } ,
}