@ -8,6 +8,7 @@ from concho.config import Config
from concho . config import Chassis
from concho . config import Chassis
from concho . config import Cpu , Dimm , Price , MemSizeGb , MemSize , CpuId , SdramChip , PmmChip
from concho . config import Cpu , Dimm , Price , MemSizeGb , MemSize , CpuId , SdramChip , PmmChip
from concho . config import IHtmlConfiguratorParser
from concho . config import IHtmlConfiguratorParser
import numpy
from abc import abstractmethod
from abc import abstractmethod
# from xml.dom.minidom import parse
# from xml.dom.minidom import parse
@ -15,6 +16,7 @@ from lxml.html import parse as html_parse
from lxml . html import HtmlElement
from lxml . html import HtmlElement
import re
import re
import copy
import copy
import logging
def clean_string ( string ) :
def clean_string ( string ) :
@ -386,60 +388,68 @@ class DellPowerEdgeC6420(TableBasedConfigurator):
class DellConfiguratorParser ( IHtmlConfiguratorParser ) :
class DellConfiguratorParser ( IHtmlConfiguratorParser ) :
use_additional_cpus_module : bool # whether to use the additional CPUs module to parse CPU change options
def __init__ ( self ):
def __init__ ( self , use_additional_cpus_module : bool ):
pass
self . use_additional_cpus_module = use_additional_cpus_module
def _get_module ( self , root_element : HtmlElement , section_label : str ) - > HtmlElement :
def _get_module ( self , root_element : HtmlElement , section_label : str ) - > HtmlElement :
modules_element = root_element . xpath ( self . get_xpath_filter ( ' root_to_modules_element ' ) ) [ 0 ]
modules_elements = root_element . xpath ( self . get_xpath_filter ( ' root_to_modules_element ' ) )
assert len ( modules_elements ) > 0 , ' unable to find modules element '
modules_element = modules_elements [ 0 ]
# print(modules_element)
# print(modules_element)
for module_root in modules_element . xpath ( self . get_xpath_filter ( ' modules_element_to_modules ' ) ) :
for module_root in modules_element . xpath ( self . get_xpath_filter ( ' modules_element_to_modules ' ) ) :
# print(module_root )
logging . debug ( f ' module_root = { module_root } ' )
# blue modules such as "Processeurs (Passage)"
# blue modules such as "Processeurs (Passage)"
module_titles = module_root . xpath ( self . get_xpath_filter ( ' module_to_blue_title ' ) )
if len ( module_titles ) > 0 :
# print(len(module_title.text))
module_title = module_titles [ 0 ]
# print('module_title.text = %s ' % module_title.text)
# print(module_title.text_content())
if module_title . text == section_label :
return module_root
# grey modules such as 'Base'
# grey modules such as 'Base'
module_titles = module_root . xpath ( self . get_xpath_filter ( ' module_to_grey_title ' ) )
for filter_id in [ ' module_to_blue_title ' , ' module_to_grey_title ' ] :
module_titles = module_root . xpath ( self . get_xpath_filter ( filter_id ) )
if len ( module_titles ) > 0 :
if len ( module_titles ) > 0 :
# print(module_title.text)
# print(len(module_title.text))
# print(len(module_title.text))
module_title = module_titles [ 0 ]
raw_module_title = module_titles [ 0 ]
m = re . match ( r ' ^ \ s*(?P<label>.+?) \ s*$ ' , raw_module_title . text_content ( ) , re . DOTALL )
assert m , f ' unable to parse module title " { raw_module_title . text_content ( ) } " '
module_title = m . group ( ' label ' )
logging . debug ( f ' module_title = { module_title } ' )
# print(module_title.text_content())
# print(module_title.text_content())
if module_title . text == section_label :
if module_title == section_label :
return module_root
return module_root
# assert False, 'failed to find module "%s"' % section_label
assert False , ' failed to find module " %s " ' % section_label
@abstractmethod
@abstractmethod
def price_str_as_float ( self , price_as_str ) :
def price_str_as_float ( self , price_as_str ) :
assert False
assert False
@abstractmethod
@abstractmethod
def get_module_label ( self , module_id ) :
def get_module_label ( self , module_id ) - > str :
assert False
assert False
@abstractmethod
@abstractmethod
def get_xpath_filter ( self , filter_id ) :
def get_xpath_filter ( self , filter_id ) - > str :
assert False
assert False
@abstractmethod
@abstractmethod
def get_base_price ( self , html_root : HtmlElement ) - > Price :
def get_base_price ( self , html_root : HtmlElement ) - > Price :
assert False
assert False
def _parse_proc_change_options ( self , html_root : HtmlElement ) - > Module :
def _parse_chassis ( self , html_root : HtmlElement ) - > str :
proc_options = Module ( ' processor-change ' )
module_root_element = self . _get_module ( html_root , ' Base ' )
# module_root_element = self._get_module(html_root, 'Processeurs (Passage)')
assert module_root_element is not None
module_root_element = self . _get_module ( html_root , self . get_module_label ( ' cpu_change ' ) )
# option_root_elements = module_root_element.xpath(".//div[@class='row']")
for option_root_element in module_root_element . xpath ( self . get_xpath_filter ( ' module_to_options ' ) ) :
# assert len(option_root_elements) > 0
label_elements = option_root_element . xpath ( self . get_xpath_filter ( ' option_to_label ' ) )
if len ( label_elements ) > 0 :
# option_root_element = option_root_elements[0]
label = clean_string ( label_elements [ 0 ] . text_content ( ) . replace ( ' \n ' , ' ' ) )
label_elements = module_root_element . xpath ( self . get_xpath_filter ( ' base_module_to_label ' ) )
price = self . price_str_as_float ( option_root_element . xpath ( self . get_xpath_filter ( ' option_to_price ' ) ) [ 0 ] . text_content ( ) )
assert len ( label_elements ) > 0
label = label_elements [ 0 ] . text_content ( ) . replace ( ' \n ' , ' ' )
# PowerEdge R640
match = re . match ( r ' ^PowerEdge (?P<chassis_type>[CR][0-9][0-9][0-9][0-9]?).* ' , label )
assert match , ' unhandled label : %s ' % label
# print(match['cpu_class'], match['cpu_number'])
chassis_id = " dell-poweredge- %s " % ( match [ ' chassis_type ' ] . lower ( ) , )
return chassis_id
def _parse_cpu_upgrade ( self , label : str , price : Price ) - > Option :
# print(label, price)
# print(label, price)
num_cpus = 1
num_cpus = 1
# Passage à processeur Intel Xeon Gold 6240L 2.6GHz, 24.75M Cache,10.40GT/s, 2UPI, Turbo, HT,18C/36T (150W) - DDR4-2933
# Passage à processeur Intel Xeon Gold 6240L 2.6GHz, 24.75M Cache,10.40GT/s, 2UPI, Turbo, HT,18C/36T (150W) - DDR4-2933
@ -464,23 +474,36 @@ class DellConfiguratorParser(IHtmlConfiguratorParser):
if match :
if match :
num_cpus = 2
num_cpus = 2
cpu_id = " amd-epyc- %s " % ( match [ ' cpu_number ' ] . lower ( ) )
cpu_id = " amd-epyc- %s " % ( match [ ' cpu_number ' ] . lower ( ) )
if match is None :
# examples from dell 20/10/2025:
# Processeur Intel® Xeon® 6 Performance 6507P 3,5 GHz, 8C/16T, 24 GT/s, 48 Mo de cache, Turbo (150 W), mémoire DDR5-6400
# Intel® Xeon® 6 Performance 6714P 4,0 GHz, 8C/16T, 24 GT/s, 330 Mo de cache, Turbo (165 W), mémoire DDR5-6400
match = re . match ( r ' ^(Processeur \ s)?Intel[®]? \ sXeon[®]? \ s+6 \ s+(?P<cpu_class>Silver|Gold|Platinium|Performance) \ s+(?P<cpu_number>[0-9][0-9][0-9][0-9][PRLYU]?).* ' , label )
if match :
cpu_class = match [ ' cpu_class ' ] . lower ( )
if cpu_class == ' platinium ' :
cpu_class = ' platinum '
cpu_id = " intel-xeon- %s - %s " % ( cpu_class , match [ ' cpu_number ' ] . lower ( ) )
assert match , ' unhandled label : %s ' % label
assert match , ' unhandled label : %s ' % label
# print(match['cpu_class'], match['cpu_number'])
# print(match['cpu_class'], match['cpu_number'])
option = Option ( Cpu ( cpu_id ) , price / num_cpus )
option = Option ( Cpu ( cpu_id ) , price / num_cpus )
return option
def _parse_proc_change_options ( self , html_root : HtmlElement ) - > Module :
proc_options = Module ( ' processor-change ' )
# module_root_element = self._get_module(html_root, 'Processeurs (Passage)')
module_root_element = self . _get_module ( html_root , self . get_module_label ( ' cpu_change ' ) )
for option_root_element in module_root_element . xpath ( self . get_xpath_filter ( ' module_to_options ' ) ) :
label_elements = option_root_element . xpath ( self . get_xpath_filter ( ' option_to_label ' ) )
if len ( label_elements ) > 0 :
label = clean_string ( label_elements [ 0 ] . text_content ( ) . replace ( ' \n ' , ' ' ) . strip ( ) )
price = self . price_str_as_float ( option_root_element . xpath ( self . get_xpath_filter ( ' option_to_price ' ) ) [ 0 ] . text_content ( ) )
option = self . _parse_cpu_upgrade ( label , price )
# print('_parse_proc_change_options : adding cpu %s (price = %f)' % (cpu_id, price / num_cpus))
# print('_parse_proc_change_options : adding cpu %s (price = %f)' % (cpu_id, price / num_cpus))
proc_options . add_option ( option )
proc_options . add_option ( option )
return proc_options
return proc_options
def _parse_proc_options ( self , html_root : HtmlElement ) - > Module :
def _parse_additional_cpu ( self , label : str , price : Price ) - > Option :
proc_options = Module ( ' processor ' )
# module_root_element = self._get_module(html_root, 'Processeurs (Passage)')
module_root_element = self . _get_module ( html_root , self . get_module_label ( ' additional_cpus ' ) )
if module_root_element is not None :
for option_root_element in module_root_element . xpath ( self . get_xpath_filter ( ' module_to_options ' ) ) :
label_elements = option_root_element . xpath ( self . get_xpath_filter ( ' option_to_label ' ) )
if len ( label_elements ) > 0 :
label = label_elements [ 0 ] . text_content ( )
price = self . price_str_as_float ( option_root_element . xpath ( self . get_xpath_filter ( ' option_to_price ' ) ) [ 0 ] . text_content ( ) )
# print(label, price)
# print(label, price)
num_additional_cpus = 1
num_additional_cpus = 1
match = re . match ( r ' ^Processeur additionnel Intel Xeon (?P<cpu_class>Silver|Gold|Platinium) (?P<cpu_number>[0-9][0-9][0-9][0-9][RLY]?).* ' , label )
match = re . match ( r ' ^Processeur additionnel Intel Xeon (?P<cpu_class>Silver|Gold|Platinium) (?P<cpu_number>[0-9][0-9][0-9][0-9][RLY]?).* ' , label )
@ -495,22 +518,32 @@ class DellConfiguratorParser(IHtmlConfiguratorParser):
cpu_class = ' platinum '
cpu_class = ' platinum '
cpu_id = " intel-xeon- %s - %s " % ( cpu_class , match [ ' cpu_number ' ] . lower ( ) )
cpu_id = " intel-xeon- %s - %s " % ( cpu_class , match [ ' cpu_number ' ] . lower ( ) )
option = Option ( Cpu ( cpu_id ) , price / num_additional_cpus )
option = Option ( Cpu ( cpu_id ) , price / num_additional_cpus )
# print('_parse_proc_options : adding cpu %s (price = %f)' % (cpu_id, price / num_additional_cpus))
return option
def _parse_additional_proc_options ( self , html_root : HtmlElement ) - > Module :
proc_options = Module ( ' processor ' )
# module_root_element = self._get_module(html_root, 'Processeurs (Passage)')
module_label = self . get_module_label ( ' additional_cpus ' )
module_root_element = self . _get_module ( html_root , module_label )
if module_root_element is not None :
logging . debug ( f ' parsing processor options from module " { module_label } " ' )
for option_root_element in module_root_element . xpath ( self . get_xpath_filter ( ' module_to_options ' ) ) :
logging . debug ( f ' option_root_element = { option_root_element } ' )
label_elements = option_root_element . xpath ( self . get_xpath_filter ( ' option_to_label ' ) )
if len ( label_elements ) > 0 :
label = clean_string ( label_elements [ 0 ] . text_content ( ) . replace ( ' \n ' , ' ' ) . strip ( ) )
price = self . price_str_as_float ( option_root_element . xpath ( self . get_xpath_filter ( ' option_to_price ' ) ) [ 0 ] . text_content ( ) )
option = self . _parse_additional_cpu ( label , price )
proc_options . add_option ( option )
proc_options . add_option ( option )
else :
assert False
assert len ( proc_options . options ) > 0
assert len ( proc_options . options ) > 0
return proc_options
return proc_options
def _parse_ram_options ( self , html_root : HtmlElement ) - > Module :
def _parse_ram_option ( self , label : str , price : Price ) - > Option :
ram_options = Module ( ' ram ' )
# module_root_element = self._get_module(html_root, 'Processeurs (Passage)')
module_root_element = self . _get_module ( html_root , self . get_module_label ( ' ram_additions ' ) )
for option_root_element in module_root_element . xpath ( self . get_xpath_filter ( ' module_to_options ' ) ) :
label_elements = option_root_element . xpath ( self . get_xpath_filter ( ' option_to_label ' ) )
if len ( label_elements ) > 0 :
label = label_elements [ 0 ] . text_content ( )
price = self . price_str_as_float ( option_root_element . xpath ( self . get_xpath_filter ( ' option_to_price ' ) ) [ 0 ] . text_content ( ) )
# print(label, price)
# print(label, price)
# Ajout d'une barette de 128Go 2667 Mhz LRDIMM
# Ajout d'une barette de 128Go 2667 Mhz LRDIMM
ram_option = None
match = re . match ( r ' ^Ajout d \' une barette de (?P<num_gb>[0-9]+)Go (?P<num_mhz>[0-9][0-9][0-9][0-9]) *M[Hh]z (?P<mem_type>LRDIMM|RDIMM)$ ' , label )
match = re . match ( r ' ^Ajout d \' une barette de (?P<num_gb>[0-9]+)Go (?P<num_mhz>[0-9][0-9][0-9][0-9]) *M[Hh]z (?P<mem_type>LRDIMM|RDIMM)$ ' , label )
if match :
if match :
@ -522,8 +555,7 @@ class DellConfiguratorParser(IHtmlConfiguratorParser):
num_mhz = 2933 # error in r940 configurator : incoherence between 'Mémoire 32 Go DDR4 à 2933MHz (4x8Go)' and 'Ajout d'une barette de 8Go 2667 Mhz RDIMM'
num_mhz = 2933 # error in r940 configurator : incoherence between 'Mémoire 32 Go DDR4 à 2933MHz (4x8Go)' and 'Ajout d'une barette de 8Go 2667 Mhz RDIMM'
sdram_chip = SdramChip ( chip_type = ' ddr ' , generation = 4 , transfer_rate = num_mhz )
sdram_chip = SdramChip ( chip_type = ' ddr ' , generation = 4 , transfer_rate = num_mhz )
dimm = Dimm ( num_gb = num_gb , sdram_chip = sdram_chip , cas = None , mem_type = mem_type )
dimm = Dimm ( num_gb = num_gb , sdram_chip = sdram_chip , cas = None , mem_type = mem_type )
option = Option ( dimm , price )
ram_option = Option ( dimm , price )
ram_options . add_option ( option )
else :
else :
# Optane DC Persistent Memory - 128Go 2666Mhz
# Optane DC Persistent Memory - 128Go 2666Mhz
match = re . match ( r ' ^Optane DC Persistent Memory - (?P<num_gb>[0-9]+)Go (?P<num_mhz>[0-9][0-9][0-9][0-9])Mhz$ ' , label )
match = re . match ( r ' ^Optane DC Persistent Memory - (?P<num_gb>[0-9]+)Go (?P<num_mhz>[0-9][0-9][0-9][0-9])Mhz$ ' , label )
@ -531,15 +563,30 @@ class DellConfiguratorParser(IHtmlConfiguratorParser):
# print(match['num_gb'], match['num_mhz'])
# print(match['num_gb'], match['num_mhz'])
chip = PmmChip ( transfer_rate = int ( match [ ' num_mhz ' ] ) )
chip = PmmChip ( transfer_rate = int ( match [ ' num_mhz ' ] ) )
dimm = Dimm ( num_gb = int ( match [ ' num_gb ' ] ) , sdram_chip = chip , cas = None , mem_type = ' pmm ' ) # persistent memory module
dimm = Dimm ( num_gb = int ( match [ ' num_gb ' ] ) , sdram_chip = chip , cas = None , mem_type = ' pmm ' ) # persistent memory module
option = Option ( dimm , price )
ram_option = Option ( dimm , price )
ram_options . add_option ( option )
else :
else :
assert False , ' unhandled label : %s ' % label
assert False , ' unhandled label : %s ' % label
assert ram_option is not None
return ram_option
def _parse_ram_options ( self , html_root : HtmlElement ) - > Module :
ram_options = Module ( ' ram ' )
# module_root_element = self._get_module(html_root, 'Processeurs (Passage)')
ram_additions_label = self . get_module_label ( ' ram_additions ' )
module_root_element = self . _get_module ( html_root , ram_additions_label )
for option_root_element in module_root_element . xpath ( self . get_xpath_filter ( ' module_to_options ' ) ) :
label_elements = option_root_element . xpath ( self . get_xpath_filter ( ' option_to_label ' ) )
if len ( label_elements ) > 0 :
label = label_elements [ 0 ] . text_content ( )
price = self . price_str_as_float ( option_root_element . xpath ( self . get_xpath_filter ( ' option_to_price ' ) ) [ 0 ] . text_content ( ) )
ram_option = self . _parse_ram_option ( label , price )
ram_options . add_option ( ram_option )
assert len ( ram_options . options ) > 0
assert len ( ram_options . options ) > 0
return ram_options
return ram_options
@abstractmethod
@abstractmethod
def _get_module_default_item ( self , module_label , html_root : HtmlElement ) :
def _get_module_default_item _label ( self , module_label , html_root : HtmlElement ) :
assert False
assert False
def _parse_base_config ( self , html_root : HtmlElement , configurator : Configurator ) - > Config :
def _parse_base_config ( self , html_root : HtmlElement , configurator : Configurator ) - > Config :
@ -548,7 +595,7 @@ class DellConfiguratorParser(IHtmlConfiguratorParser):
base_config . num_cpu_per_server = 1
base_config . num_cpu_per_server = 1
# initialize cpu
# initialize cpu
item_label = self . _get_module_default_item ( ' Processeurs (Passage) ' , html_root )
item_label = self . _get_module_default_item _label ( ' Processeurs (Passage) ' , html_root )
# Processeur Intel Xeon Silver 4208 2.1GHz,11M Cache,9.60GT/s, 2UPI,No Turbo, HT,8C/16T (85W) - DDR4-2400
# Processeur Intel Xeon Silver 4208 2.1GHz,11M Cache,9.60GT/s, 2UPI,No Turbo, HT,8C/16T (85W) - DDR4-2400
match = re . match ( r ' ^Processeur Intel Xeon (?P<cpu_class>Silver|Gold|Platinium) (?P<cpu_number>[0-9][0-9][0-9][0-9][RLYU]?).* ' , item_label )
match = re . match ( r ' ^Processeur Intel Xeon (?P<cpu_class>Silver|Gold|Platinium) (?P<cpu_number>[0-9][0-9][0-9][0-9][RLYU]?).* ' , item_label )
if match :
if match :
@ -572,7 +619,7 @@ class DellConfiguratorParser(IHtmlConfiguratorParser):
base_config . set_cpu ( Cpu ( cpu_id ) )
base_config . set_cpu ( Cpu ( cpu_id ) )
# initialize the default ram dimms
# initialize the default ram dimms
item_label = self . _get_module_default_item ( self . get_module_label ( ' ram_change ' ) , html_root )
item_label = self . _get_module_default_item _label ( self . get_module_label ( ' ram_change ' ) , html_root )
# Mémoire 16 Go DDR4 à 2933MHz (1x16Go)
# Mémoire 16 Go DDR4 à 2933MHz (1x16Go)
match = re . match ( r ' ^Mémoire (?P<num_gb>[0-9]+) Go DDR[ \ -]?4 à (?P<num_mhz>[0-9]+)MHz \ ((?P<num_dimms>[0-9]+)x(?P<num_gb_per_dimm>[0-9]+)Go \ ) ' , item_label . replace ( ' Mémoire de base : ' , ' ' ) . replace ( ' De base ' , ' ' ) )
match = re . match ( r ' ^Mémoire (?P<num_gb>[0-9]+) Go DDR[ \ -]?4 à (?P<num_mhz>[0-9]+)MHz \ ((?P<num_dimms>[0-9]+)x(?P<num_gb_per_dimm>[0-9]+)Go \ ) ' , item_label . replace ( ' Mémoire de base : ' , ' ' ) . replace ( ' De base ' , ' ' ) )
assert match , ' unhandled label : %s ' % item_label
assert match , ' unhandled label : %s ' % item_label
@ -629,6 +676,80 @@ class DellConfiguratorParser(IHtmlConfiguratorParser):
return base_cpu_price
return base_cpu_price
@staticmethod
def _estimate_base_cpu_price_from_public_prices ( cpu_uid : CpuId ) - > Price :
# estimation of xeon 6505p price on matinfo 6:
# matinfo 6 prices:
# - upgrade from 2 x 6505p to 2 x 6507p: 186.0 €
# - upgrade from 2 x 6505p to 2 x 6515p: 242.0 €
# - upgrade from 2 x 6505p to 2 x 6737p: 6465.0 €
# retail prices [https://en.wikipedia.org/wiki/Granite_Rapids#Granite_Rapids-SP]:
public_prices_usd = {
' intel-xeon-performance-6505p ' : 563.0 ,
' intel-xeon-performance-6507p ' : 765.0 ,
' intel-xeon-performance-6515p ' : 740.0 ,
' intel-xeon-performance-6517p ' : 1195.0 ,
# 'intel-xeon-performance-6730p': 2234.0,
# 'intel-xeon-performance-6737p': 4995.0,
# 'intel-xeon-performance-6740p': 4650.0,
# 'intel-xeon-performance-6767p': 9595.0,
# 'intel-xeon-performance-6787p': 10400.0,
}
matinfo6_upgrade_prices_eur = {
' intel-xeon-performance-6507p ' : 186.0 ,
' intel-xeon-performance-6515p ' : 242.0 ,
' intel-xeon-performance-6517p ' : 902.0 ,
# 'intel-xeon-performance-6730p': 3674.0,
# 'intel-xeon-performance-6737p': 6465.0,
# 'intel-xeon-performance-6740p': 6724.0,
# 'intel-xeon-performance-6767p': 9672.0,
# 'intel-xeon-performance-6787p': 10775.0,
}
# let p_x be the public price of cpu x in usd (known)
# let m_x be the matinfo6 price of cpu x in eur (unknown)
# we assume m_x = p_x * r, where the unknown r encompasses currency conversion usd->eur and reseller discount
# we have:
# u_x = 2 m_x - 2 m_base_cpu (1) (because the upgrades are for 2 cpus)
# we want to find m_base_cpu
# => m_base_cpu = m_x - u_x / 2 (2) (from (1))
# but m_x is unknown, so we use (2) and m_x = p_x * r
# => m_base_cpu = p_x * r - u_x / 2
# we can do this for each upgrade cpu and average the results to eliminate r
# so we have a system of equations with 2 unknowns (m_base_cpu and r) and multiple equations:
# => 2 * m_base_cpu = 2 * p_x * r - u_x
# => u_x = 2 * p_x * r - 2 * m_base_cpu
# in matrix form:
# [ u_1 ] = [ 2*p_1 -2 ] * [ r ]
# [ u_2 ] [ 2*p_2 -2 ] [ m_base_cpu ]
# [ ... ] [ ... ... ]
# [ u_n ] [ 2*p_n -2 ]
# we can solve this system using least squares to find m_base_cpu
# U = A * X
# where U is the vector of upgrade prices u_x
# A is the matrix with rows [2*p_x -2]
# X is the vector [r, m_base_cpu] to find
# => X = (A^T A)^-1 A^T U
upgrade_keys = list ( matinfo6_upgrade_prices_eur . keys ( ) )
A = numpy . zeros ( ( len ( matinfo6_upgrade_prices_eur ) , 2 ) )
U = numpy . zeros ( ( len ( matinfo6_upgrade_prices_eur ) , 1 ) )
P = numpy . array ( [ public_prices_usd [ cpu_uid ] for cpu_uid in upgrade_keys ] )
A [ : , 0 ] = 2.0 * P
A [ : , 1 ] = - 2.0
U [ : , 0 ] = numpy . array ( [ matinfo6_upgrade_prices_eur [ cpu_uid ] for cpu_uid in upgrade_keys ] )
AtA_inv = numpy . linalg . inv ( numpy . matmul ( A . T , A ) )
AtU = numpy . matmul ( A . T , U )
X = numpy . matmul ( AtA_inv , AtU )
r_estimated = X [ 0 , 0 ]
base_cpu_price = X [ 1 , 0 ]
logging . info ( ' estimated currency conversion and discount rate r : %f ' % r_estimated )
logging . info ( ' estimated price of base cpu %s from linear system : %f ' % ( cpu_uid , base_cpu_price ) )
estimated_prices = P * r_estimated
estimated_u = 2 * estimated_prices - 2 * base_cpu_price
for i , cpu_uid in enumerate ( matinfo6_upgrade_prices_eur . keys ( ) ) :
logging . info ( ' estimated price of cpu %s : %f €, matinfo6 upgrade price : %f €, estimated upgrade price : %f € ' % ( cpu_uid , estimated_prices [ i ] , matinfo6_upgrade_prices_eur [ cpu_uid ] , estimated_u [ i ] ) )
return base_cpu_price
def parse ( self , dell_configurator_html_file_path : Path , configurator : Configurator ) :
def parse ( self , dell_configurator_html_file_path : Path , configurator : Configurator ) :
html_root = html_parse ( dell_configurator_html_file_path ) . getroot ( )
html_root = html_parse ( dell_configurator_html_file_path ) . getroot ( )
@ -645,27 +766,19 @@ class DellConfiguratorParser(IHtmlConfiguratorParser):
# modules_element = body.xpath("//div[@class='col-md-10']")
# modules_element = body.xpath("//div[@class='col-md-10']")
module_root_element = self . _get_module ( html_root , ' Base ' )
chassis_id = self . _parse_chassis ( html_root )
assert module_root_element is not None
# option_root_elements = module_root_element.xpath(".//div[@class='row']")
# assert len(option_root_elements) > 0
# option_root_element = option_root_elements[0]
label_elements = module_root_element . xpath ( self . get_xpath_filter ( ' base_module_to_label ' ) )
assert len ( label_elements ) > 0
label = label_elements [ 0 ] . text_content ( ) . replace ( ' \n ' , ' ' )
# PowerEdge R640
match = re . match ( r ' ^PowerEdge (?P<chassis_type>[CR][0-9][0-9][0-9][0-9]?).* ' , label )
assert match , ' unhandled label : %s ' % label
# print(match['cpu_class'], match['cpu_number'])
chassis_id = " dell-poweredge- %s " % ( match [ ' chassis_type ' ] . lower ( ) , )
configurator . chassis = Option ( Chassis ( chassis_id ) , 0.0 )
configurator . chassis = Option ( Chassis ( chassis_id ) , 0.0 )
configurator . base_config = self . _parse_base_config ( html_root , configurator )
configurator . base_config = self . _parse_base_config ( html_root , configurator )
proc_change_module = self . _parse_proc_change_options ( html_root )
proc_change_module = self . _parse_proc_change_options ( html_root )
configurator . add_module ( proc_change_module )
configurator . add_module ( proc_change_module )
configurator . add_module ( self . _parse_proc_options ( html_root ) )
if self . use_additional_cpus_module :
proc_add_module = self . _parse_additional_proc_options ( html_root )
else :
proc_add_module = Module ( ' processor ' )
configurator . add_module ( proc_add_module )
# if self.get_module_label('ram_additions') != '':
configurator . add_module ( self . _parse_ram_options ( html_root ) )
configurator . add_module ( self . _parse_ram_options ( html_root ) )
# compute the price of the base config cpu because for example in r940 configurator, the xeon gold 5215 appears in the basic config but not in additional cpus
# compute the price of the base config cpu because for example in r940 configurator, the xeon gold 5215 appears in the basic config but not in additional cpus
@ -675,8 +788,10 @@ class DellConfiguratorParser(IHtmlConfiguratorParser):
if base_cpu_price is None :
if base_cpu_price is None :
# in the case of r6525, there was no additional processor module, and therefore we have no way to estimate the price of the base processor (amd-epyc-7262)
# in the case of r6525, there was no additional processor module, and therefore we have no way to estimate the price of the base processor (amd-epyc-7262)
# so we fallback to an hardcoded estimated price from wikipedia
# so we fallback to an hardcoded estimated price from wikipedia
base_cpu_price = {
base_cpu_price = {
' amd-epyc-7262 ' : 550.0 ,
' amd-epyc-7262 ' : 550.0 ,
' intel-xeon-performance-6505p ' : DellConfiguratorParser . _estimate_base_cpu_price_from_public_prices ( cpu_uid = base_cpu . uid ) ,
} [ base_cpu . uid ]
} [ base_cpu . uid ]
configurator . modules [ ' processor ' ] . add_option ( Option ( base_cpu , base_cpu_price ) )
configurator . modules [ ' processor ' ] . add_option ( Option ( base_cpu , base_cpu_price ) )
assert configurator . get_item_price ( base_cpu . uid ) is not None , ' failed to find the price of base cpu %s ' % base_cpu . uid
assert configurator . get_item_price ( base_cpu . uid ) is not None , ' failed to find the price of base cpu %s ' % base_cpu . uid
@ -711,7 +826,7 @@ class DellConfiguratorParser2020(DellConfiguratorParser):
def __init__ ( self ) :
def __init__ ( self ) :
super ( ) . __init__ ( )
super ( ) . __init__ ( )
def get_module_label ( self , module_id ) :
def get_module_label ( self , module_id ) - > str :
return {
return {
' cpu_change ' : ' Processeurs (Passage) ' ,
' cpu_change ' : ' Processeurs (Passage) ' ,
' additional_cpus ' : ' Processeurs additionnels ' ,
' additional_cpus ' : ' Processeurs additionnels ' ,
@ -719,7 +834,7 @@ class DellConfiguratorParser2020(DellConfiguratorParser):
' ram_additions ' : ' Mémoire: Ajout de barettes additionnelles ' ,
' ram_additions ' : ' Mémoire: Ajout de barettes additionnelles ' ,
} [ module_id ]
} [ module_id ]
def get_xpath_filter ( self , filter_id ) :
def get_xpath_filter ( self , filter_id ) - > str :
return {
return {
' root_to_modules_element ' : " .//div[@class= ' col-md-10 ' ] " ,
' root_to_modules_element ' : " .//div[@class= ' col-md-10 ' ] " ,
' modules_element_to_modules ' : " .//div[@class= ' col-md-12 module ' ] " ,
' modules_element_to_modules ' : " .//div[@class= ' col-md-12 module ' ] " ,
@ -739,7 +854,7 @@ class DellConfiguratorParser2020(DellConfiguratorParser):
price_as_float = float ( " %s %s " % ( match [ ' sign ' ] , match [ ' numbers ' ] ) )
price_as_float = float ( " %s %s " % ( match [ ' sign ' ] , match [ ' numbers ' ] ) )
return price_as_float
return price_as_float
def _get_module_default_item ( self , module_label , html_root ) :
def _get_module_default_item _label ( self , module_label , html_root ) - > str :
module_root_element = self . _get_module ( html_root , module_label )
module_root_element = self . _get_module ( html_root , module_label )
assert module_root_element is not None
assert module_root_element is not None
for option_root_element in module_root_element . xpath ( " .//div[@class= ' row ' ] " ) :
for option_root_element in module_root_element . xpath ( " .//div[@class= ' row ' ] " ) :
@ -752,7 +867,7 @@ class DellConfiguratorParser2020(DellConfiguratorParser):
return label
return label
assert False , ' failed to find the default item of module %s ' % module_label
assert False , ' failed to find the default item of module %s ' % module_label
def get_base_price ( self , html_root ) :
def get_base_price ( self , html_root ) - > Price :
base_price = None
base_price = None
price_preview_element = html_root . xpath ( " .//div[@class= ' price-preview ' ] " ) [ 0 ]
price_preview_element = html_root . xpath ( " .//div[@class= ' price-preview ' ] " ) [ 0 ]
assert price_preview_element is not None
assert price_preview_element is not None
@ -783,7 +898,7 @@ class DellConfiguratorParser2021(DellConfiguratorParser):
def __init__ ( self ) :
def __init__ ( self ) :
super ( ) . __init__ ( )
super ( ) . __init__ ( )
def get_module_label ( self , module_id ) :
def get_module_label ( self , module_id ) - > str :
return {
return {
' cpu_change ' : ' Processeurs (Passage) ' ,
' cpu_change ' : ' Processeurs (Passage) ' ,
' additional_cpus ' : ' Processeurs additionnels ' ,
' additional_cpus ' : ' Processeurs additionnels ' ,
@ -791,7 +906,7 @@ class DellConfiguratorParser2021(DellConfiguratorParser):
' ram_additions ' : ' Mémoire: Ajout de barettes additionnelles ' ,
' ram_additions ' : ' Mémoire: Ajout de barettes additionnelles ' ,
} [ module_id ]
} [ module_id ]
def get_xpath_filter ( self , filter_id ) :
def get_xpath_filter ( self , filter_id ) - > str :
return {
return {
' root_to_modules_element ' : " .//div[@class= ' modules ' ] " ,
' root_to_modules_element ' : " .//div[@class= ' modules ' ] " ,
' modules_element_to_modules ' : " .//div[@class= ' product-module-configuration ' ] " ,
' modules_element_to_modules ' : " .//div[@class= ' product-module-configuration ' ] " ,
@ -812,7 +927,7 @@ class DellConfiguratorParser2021(DellConfiguratorParser):
price_as_float = float ( " %s %s " % ( match [ ' sign ' ] , match [ ' numbers ' ] ) )
price_as_float = float ( " %s %s " % ( match [ ' sign ' ] , match [ ' numbers ' ] ) )
return price_as_float
return price_as_float
def _get_module_default_item ( self , module_label , html_root ) :
def _get_module_default_item _label ( self , module_label , html_root ) - > str :
module_root_element = self . _get_module ( html_root , module_label )
module_root_element = self . _get_module ( html_root , module_label )
assert module_root_element is not None
assert module_root_element is not None
@ -843,7 +958,7 @@ class DellConfiguratorParser2021(DellConfiguratorParser):
return label
return label
assert False , ' failed to find the default item of module %s ' % module_label
assert False , ' failed to find the default item of module %s ' % module_label
def get_base_price ( self , html_root ) :
def get_base_price ( self , html_root ) - > Price :
base_price = None
base_price = None
price_preview_element = html_root . xpath ( " .//div[@class= ' product-info ' ] " ) [ 0 ]
price_preview_element = html_root . xpath ( " .//div[@class= ' product-info ' ] " ) [ 0 ]
assert price_preview_element is not None
assert price_preview_element is not None
@ -866,28 +981,32 @@ class DellConfiguratorParser2021(DellConfiguratorParser):
return base_price
return base_price
class Dell V2 ConfiguratorParser( DellConfiguratorParser ) :
class Dell ConfiguratorParser2025 ( DellConfiguratorParser ) :
def __init__ ( self ) :
def __init__ ( self ) :
super ( ) . __init__ ( )
super ( ) . __init__ ( use_additional_cpus_module = False ) # no information is available in additional_cpus module. use cpu_change module instead
def get_module_label ( self , module_id ) :
def _parse_chassis ( self , html_root : HtmlElement ) - > str :
chassis_id = " dell-poweredge-r670 "
return chassis_id
def get_module_label ( self , module_id ) - > str :
return {
return {
' cpu_change ' : ' Processeurs (Passage) ' ,
' cpu_change ' : ' Processeur ' ,
' additional_cpus ' : ' Processeurs additionnels ' ,
' additional_cpus ' : ' Processeur supplémentaire s' ,
' ram_change ' : ' Mémoires (Passage) ' ,
' ram_change ' : ' Capacité de mémoire ' ,
' ram_additions ' : ' Mémoire: Ajout de barettes additionnelles' ,
' ram_additions ' : ' ', # no ram additions module in 10/2025
} [ module_id ]
} [ module_id ]
def get_xpath_filter ( self , filter_id ) :
def get_xpath_filter ( self , filter_id ) - > str :
return {
return {
' root_to_modules_element ' : " .//div[@id= ' technicalspecification_section ' ] " ,
' root_to_modules_element ' : " .//div[@id= ' technicalspecification_section ' ] " ,
' modules_element_to_modules ' : " .//div[@c lass=' product-module-configuration ' ] " ,
' modules_element_to_modules ' : " .//div[@c f-module-rank=' 0 ' ] " , # cf-module-rank="0"
' module_to_blue_title ' : " .// header " ,
' module_to_blue_title ' : " .// span[@class=' dds__subtitle-2 ' ] " ,
' module_to_grey_title ' : " .// div[@class=' col-md-4 module-title color-808080 ' ] " ,
' module_to_grey_title ' : " .// span[@class=' dds__subtitle-2 ' ] " , # no grey titles in v2 ?
' module_to_options ' : " .//div[@class= ' product-options-configuration-line' ] " ,
' module_to_options ' : " .//div[@class= ' cf-input-wrap ' ] " , # <div class="cf-input-wrap ">
' option_to_label ' : " .//div[@class= ' option-info' ] " ,
' option_to_label ' : " .//div[@class= ' dds__option-title1' ] " , # <div class="dds__option-title1" cf-option-modcount="0">
' option_to_price ' : " .//div[@class= ' option -price' ] " ,
' option_to_price ' : " .//div[@class= ' cf- opt-price' ] " , # <div class="cf-opt-price">
' base_module_to_label ' : " .//div[@class= ' product-options-configuration-block option-selected ' ] " ,
' base_module_to_label ' : " .//div[@class= ' product-options-configuration-block option-selected ' ] " ,
} [ filter_id ]
} [ filter_id ]
@ -900,54 +1019,227 @@ class DellV2ConfiguratorParser(DellConfiguratorParser):
price_as_float = float ( " %s %s " % ( match [ ' sign ' ] , match [ ' numbers ' ] ) )
price_as_float = float ( " %s %s " % ( match [ ' sign ' ] , match [ ' numbers ' ] ) )
return price_as_float
return price_as_float
def _get_module_default_item ( self , module_label , html_root ) :
def _get_module_selected_items ( self , module_root_element : HtmlElement ) - > List [ HtmlElement ] :
logging . debug ( ' getting selected items for module %s ' % module_root_element )
selected_option_filter = " .//div[@class= ' cf-input-wrap selected-dds2 ' ] "
selected_items = [ selected_option_root_element for selected_option_root_element in module_root_element . xpath ( selected_option_filter ) ]
logging . debug ( ' found %d selected items for module %s ' % ( len ( selected_items ) , module_root_element ) )
return selected_items
def _get_item_label ( self , item_root_element : HtmlElement ) - > str :
label_filter = self . get_xpath_filter ( ' option_to_label ' )
label_elements = item_root_element . xpath ( label_filter )
assert label_elements is not None
assert len ( label_elements ) > 0
label = label_elements [ 0 ] . text_content ( ) . replace ( ' \n ' , ' ' ) . strip ( )
return label
def _get_item_count ( self , item_root_element : HtmlElement ) - > int :
qty_roller_filter = " .//div[@class= ' cf-qty-roller-wrap ' ] " # <div class="cf-qty-roller-wrap">
qty_roller_elements = item_root_element . xpath ( qty_roller_filter )
if len ( qty_roller_elements ) > 0 :
assert len ( qty_roller_elements ) == 1 , ' unexpected multiple qty roller elements '
# <input type="text" value="2" data-cf-min="2"
# data-cf-max="32" class="cf-s-input"
# aria-label="NumberSpinner "
# data-cf-fixed-duration-json=""
# data-cf-spinner-type=""
# data-cf-selected-year="2"
# data-cf-maximum-term="0"
# data-cf-minimum-term="0"
# data-cf-selected-term="0"
# data-cf-trigger="cf-qty-roller-changed"
# readonly="readonly">
input_elements = qty_roller_elements [ 0 ] . xpath ( " .//input[@type= ' text ' ] " )
assert input_elements is not None
assert len ( input_elements ) > 0
qty_as_str = input_elements [ 0 ] . get ( ' value ' )
qty = int ( qty_as_str )
else :
qty = 0
return qty
def _get_item_price ( self , item_root_element : HtmlElement ) - > float :
price_filter = self . get_xpath_filter ( ' option_to_price ' )
price_elements = item_root_element . xpath ( price_filter )
assert price_elements is not None
assert len ( price_elements ) > 0
price_as_str = price_elements [ 0 ] . text_content ( ) . strip ( )
# remove potential trailing per unit symbol, such as in '142,00 € /U.'
price_as_str = price_as_str . replace ( ' /U. ' , ' ' ) . strip ( )
logging . debug ( ' item price string : %s ' % price_as_str )
match = re . match ( r ' ^Inclus dans le prix$ ' , price_as_str )
logging . debug ( ' item price match : %s ' % str ( match ) )
if match is not None :
logging . debug ( ' item price is not defined ' )
price = 0.0
else :
logging . debug ( ' item price is defined ' )
price = self . price_str_as_float ( price_as_str )
return price
def _get_module_default_item_label ( self , module_label , html_root ) - > str :
logging . debug ( ' getting default item label for module %s ' % module_label )
module_root_element = self . _get_module ( html_root , module_label )
module_root_element = self . _get_module ( html_root , module_label )
assert module_root_element is not None
assert module_root_element is not None
if module_label == self . get_module_label ( ' ram_change ' ) :
selected_option_root_elements = self . _get_module_selected_items ( module_root_element )
# <div
assert len ( selected_option_root_elements ) == 1 , ' we expect 1 selected items for module %s , not %d ' % ( module_label , len ( selected_option_root_elements ) )
# class="product-options-configuration-block option-selected">
selected_option_root_element = selected_option_root_elements [ 0 ]
# <header>Mémoire 16 Go DDR4 à 3200MHz (1x16Go)<div
label = self . _get_item_label ( selected_option_root_element )
# class="option-selector"><i
logging . debug ( ' default item for module %s : %s ' % ( module_label , label ) )
# class="fas fa-check "></i></div>
price = self . _get_item_price ( selected_option_root_element )
# </header>
assert price == 0.0 , ' unexpected price for default item ( %d €) ' % price
# <div class="mt-2 option-price">+ 0,00 €</div>
# </div>
selected_option_filter = " .//div[@class= ' product-options-configuration-block option-selected ' ] "
label_filter = " .//header "
price_filter = " .//div[@class= ' mt-2 option-price ' ] "
else :
selected_option_filter = " .//div[@class= ' product-options-configuration-line option-selected ' ] "
label_filter = " .//div[@class= ' option-info ' ] "
price_filter = " .//div[@class= ' option-price ' ] "
for selected_option_root_element in module_root_element . xpath ( selected_option_filter ) :
label_elements = selected_option_root_element . xpath ( label_filter )
assert label_elements is not None
if len ( label_elements ) > 0 :
label = label_elements [ 0 ] . text_content ( ) . replace ( ' \n ' , ' ' )
price = self . price_str_as_float ( selected_option_root_element . xpath ( price_filter ) [ 0 ] . text_content ( ) )
assert price == 0.0 , ' default items are expected to have a price of 0.0 € ( %s s price is %f ) ' % ( label , price )
return label
return label
assert False , ' failed to find the default item of module %s ' % module_label
def get_base_price ( self , html_root ) :
def _parse_ram_option ( self , label : str , price : Price ) - > Option :
# print(label, price)
# Ajout d'une barette de 128Go 2667 Mhz LRDIMM
ram_option = None
# Mémoire 16 Go DDR4 à 2933MHz (1x16Go)
# 32 Go RDIMM, 6 400 MT/s, double rangée
# match = re.match(r'^Mémoire (?P<num_gb>[0-9]+) Go DDR[\-]?4 à (?P<num_mhz>[0-9]+)MHz \((?P<num_dimms>[0-9]+)x(?P<num_gb_per_dimm>[0-9]+)Go\)', item_label.replace('Mémoire de base : ', '').replace('De base ', ''))
# match = re.match(r'^(?P<num_gb>[0-9]+) Go (de mémoire|) RDIMM, (?P<num_mts>[0-9] ?[0-9]+)\s+MT/s, (une|double) rangée', item_label)
match = re . match ( r ' ^(?P<num_gb_per_dimm>[0-9]+) \ s+Go( \ s+de mémoire|) \ s+RDIMM, \ s+(?P<num_mts>[0-9] \ s?[0-9]+) \ s+MT/s, \ s+(une|double) \ s+rangée ' , label )
assert match , ' unhandled label : %s ' % label
# DDR5 RDIMM 6400 MT/s
num_mts = match [ ' num_mts ' ]
# remove non ascii characters from num_mts
num_mts = re . sub ( r ' [^0-9] ' , ' ' , num_mts )
sdram_chip = SdramChip ( chip_type = ' ddr ' , generation = 5 , transfer_rate = int ( num_mts ) )
dimm = Dimm ( num_gb = int ( match [ ' num_gb_per_dimm ' ] ) , sdram_chip = sdram_chip , cas = None , mem_type = ' rdimm ' )
ram_option = Option ( dimm , price )
assert ram_option is not None
return ram_option
def _parse_ram_options ( self , html_root : HtmlElement ) - > Module :
ram_options = Module ( ' ram ' )
# module_root_element = self._get_module(html_root, 'Processeurs (Passage)')
module_root_element = self . _get_module ( html_root , self . get_module_label ( ' ram_change ' ) )
for pass_id in [ ' non_selected_rams ' , ' selected_rams ' ] :
options_xpath_filter = self . get_xpath_filter ( ' module_to_options ' )
if pass_id == ' selected_rams ' :
options_xpath_filter = options_xpath_filter . replace ( " " , " selected-dds2 " )
for option_root_element in module_root_element . xpath ( options_xpath_filter ) :
label_elements = option_root_element . xpath ( self . get_xpath_filter ( ' option_to_label ' ) )
assert len ( label_elements ) > 0 , ' failed to find label for ram option '
label = label_elements [ 0 ] . text_content ( ) . strip ( )
price = self . _get_item_price ( option_root_element )
if price == 0.0 :
assert pass_id == ' selected_rams ' , ' unexpected ram option price %f for label %s ' % ( price , label )
# find the price in the selector widget, which displays as: '[-][2][+] 235,00 € /U.'
# <div class="cf-qty-wrap">
# <div class="cf-qty-roller-wrap">
# <div class="cf-s-box">
# <button class="cf-s-minus stop">-</button>
# <input type="text" value="2" data-cf-min="2"
# data-cf-max="32" class="cf-s-input"
# aria-label="NumberSpinner "
# data-cf-fixed-duration-json=""
# data-cf-spinner-type=""
# data-cf-selected-year="2"
# data-cf-maximum-term="0"
# data-cf-minimum-term="0"
# data-cf-selected-term="0"
# data-cf-trigger="cf-qty-roller-changed"
# readonly="readonly">
# <button class="cf-s-plus ">+</button>
# </div>
# </div>
# <div>235,00 € /U.</div>
# </div>
qty_roller_wrap_elements = option_root_element . xpath ( " .//div[@class= ' cf-qty-wrap ' ] " )
assert len ( qty_roller_wrap_elements ) == 1 , ' failed to find qty roller wrap for ram option with label %s ' % label
price_elements = qty_roller_wrap_elements [ 0 ] . xpath ( " ./div " )
assert len ( price_elements ) == 2 , ' unexpected number of price elements ( %d ) in qty roller wrap for ram option with label %s ' % ( len ( price_elements ) , label )
price_as_str = price_elements [ 1 ] . text_content ( ) . strip ( ) . replace ( ' /U. ' , ' ' ) . strip ( )
price = self . price_str_as_float ( price_as_str )
assert price > 0.0 , ' unexpected ram option price %f for label %s ' % ( price , label )
ram_option = self . _parse_ram_option ( label , price )
logging . debug ( ' adding ram option : %s with price %f ' % ( label , price ) )
ram_options . add_option ( ram_option )
assert len ( ram_options . options ) > 0
return ram_options
def _parse_base_config ( self , html_root : HtmlElement , configurator : Configurator ) - > Config :
base_config = Config ( configurator )
base_config . num_servers = configurator . chassis . item . max_num_servers
base_config . num_cpu_per_server = 1
# initialize cpu
item_label = self . _get_module_default_item_label ( ' Processeur ' , html_root )
# Processeur Intel Xeon Silver 4208 2.1GHz,11M Cache,9.60GT/s, 2UPI,No Turbo, HT,8C/16T (85W) - DDR4-2400
# match = re.match(r'^Processeur Intel[®]? Xeon[®]? 6 (?P<cpu_class>Silver|Gold|Platinium|Performance) (?P<cpu_number>[0-9][0-9][0-9][0-9][PRLYU]?).*', item_label)
match = re . match ( r ' ^Processeur Intel[®]? Xeon[®]? \ s+6 \ s+(?P<cpu_class>Silver|Gold|Platinium|Performance) \ s+(?P<cpu_number>[0-9][0-9][0-9][0-9][PRLYU]?).* ' , item_label )
assert match , ' unhandled label : %s ' % item_label
if match :
cpu_id = " intel-xeon- %s - %s " % ( match [ ' cpu_class ' ] . lower ( ) , match [ ' cpu_number ' ] . lower ( ) )
if match is None :
match = re . match ( r ' ^2 processeurs Intel Xeon (?P<cpu_class>Silver|Gold|Platinium) (?P<cpu_number>[0-9][0-9][0-9][0-9][RLYU]?).* ' , item_label )
if match :
base_config . num_cpu_per_server = 2
cpu_id = " intel-xeon- %s - %s " % ( match [ ' cpu_class ' ] . lower ( ) , match [ ' cpu_number ' ] . lower ( ) )
if match is None :
print ( ' item_label= %s ' % item_label )
# match = re.match(r'^2 Processeurs AMD EPYC (?P<cpu_number>[0-9][0-9][0-9][0-9]).*', item_label)
match = re . match ( r ' ^2 Processeurs AMD EPYC (?P<cpu_number>[0-9][0-9][0-9][0-9]).* ' , clean_string ( item_label ) )
if match :
base_config . num_cpu_per_server = 2
cpu_id = " amd-epyc- %s " % ( match [ ' cpu_number ' ] . lower ( ) )
assert match , ' unhandled label : %s ' % item_label
# print(match['cpu_class'], match['cpu_number'])
base_config . set_cpu ( Cpu ( cpu_id ) )
# initialize the default ram dimms
ram_module_root_element = self . _get_module ( html_root , self . get_module_label ( ' ram_change ' ) )
assert ram_module_root_element is not None
selected_ram_items = self . _get_module_selected_items ( ram_module_root_element )
assert ( len ( selected_ram_items ) == 1 ) , ' unexpected number of selected ram items : %d ' % len ( selected_ram_items )
selected_ram_item = selected_ram_items [ 0 ]
item_label = self . _get_item_label ( selected_ram_item )
num_dimms = self . _get_item_count ( selected_ram_item )
ram_option = self . _parse_ram_option ( item_label , 0.0 )
dimm = ram_option . item
if num_dimms == 1 :
# print(match['cpu_class'], match['cpu_number'])
cpu_slot_index = 0
mem_channel_index = 0
dimm_slot = 0
base_config . cpu_slots_mem [ cpu_slot_index ] . mem_channels [ mem_channel_index ] . dimms [ dimm_slot ] = dimm
else :
# evenly split dimms on channels
assert ( num_dimms % base_config . num_cpu_per_server ) == 0
num_dimms_per_cpu = num_dimms / / base_config . num_cpu_per_server
for cpu_slot_index in range ( base_config . num_cpu_per_server ) :
cpu_slots_mem = base_config . cpu_slots_mem [ cpu_slot_index ]
assert len ( cpu_slots_mem . mem_channels ) > = num_dimms_per_cpu
for channel_index in range ( num_dimms_per_cpu ) :
mem_channel = cpu_slots_mem . mem_channels [ channel_index ]
dimm_slot = 0
mem_channel . dimms [ dimm_slot ] = dimm
return base_config
def get_base_price ( self , html_root ) - > Price :
# <div class="cf-hero-starting-price">
# <div class="cf-disable-tooltip" js-tooltip="" js-is-clickable="">
# <div id="cf-hero-starting-price-with-currency">2 672,00 €</div>
# </div>
# </div>
base_price = None
base_price = None
price_preview_element = html_root . xpath ( " .//div[@class= ' product-info ' ] " ) [ 0 ]
price_value_element = html_root . xpath ( " .//div[@id= ' cf-hero-starting-price-with-currency ' ] " ) [ 0 ]
assert price_preview_element is not None
for price_element in price_preview_element . xpath ( " .//div[@class= ' info ' ] " ) :
price_label_element = price_element . xpath ( " .//span[@class= ' info-label ' ] " ) [ 0 ]
# <div class="info"><span class="info-label">Prix de base</span><span
# class="info-value strong">1175 € HT</span></div>
# <hr>
# <div class="info"><span class="info-label">Avec options</span><span
# class="info-value strong">1175 € HT</span></div>
# <hr>
assert price_label_element is not None
label = price_label_element . text_content ( ) . replace ( ' \n ' , ' ' )
if label == ' Prix de base ' :
price_value_element = price_element . xpath ( " .//span[@class= ' info-value strong ' ] " ) [ 0 ]
assert price_value_element is not None
assert price_value_element is not None
base_price = self . price_str_as_float ( price_value_element . text_content ( ) . replace ( ' HT ' , ' ' ) )
base_price = self . price_str_as_float ( price_value_element . text_content ( ) . replace ( ' HT ' , ' ' ) )
assert base_price is not None
assert base_price is not None
@ -1036,5 +1328,3 @@ class DellMatinfoCsvConfigurator(Configurator):
config . cpu_slots_mem = copy . deepcopy ( self . base_config . cpu_slots_mem )
config . cpu_slots_mem = copy . deepcopy ( self . base_config . cpu_slots_mem )
return config
return config