strip whitespace

This commit is contained in:
James Powell
2018-10-05 20:59:25 -04:00
parent 9898dc85a9
commit 83444b329e
7 changed files with 85 additions and 85 deletions

View File

@@ -7,11 +7,11 @@ xls to json parser, that can be called directly from the transmission_main_examp
xls examples are meshTopologyExampleV2.xls and CORONET_Global_Topology.xls
Require Nodes and Links sheets, Eqpt sheet is optional
*in Nodes sheet, only the 'City' column is mandatory. The column 'Type' is discovered based
on the topology: degree 2 = ILA, other degrees = ROADM. The value is also corrected if the user
on the topology: degree 2 = ILA, other degrees = ROADM. The value is also corrected if the user
specifies an ILA of degree != 2.
*In Links sheet only the 3 first columns (Node A, Node Z and east Distance (km)) are mandatory.
*In Links sheet only the 3 first columns (Node A, Node Z and east Distance (km)) are mandatory.
Missing west information are copied from east information so it is possible to input undir data
*in Eqpt sheet
*in Eqpt sheet
"""
from sys import exit
@@ -39,19 +39,19 @@ class Link(namedtuple('Link', 'from_city to_city \
west_distance west_fiber west_lineic west_con_in west_con_out west_pmd west_cable \
distance_units')):
def __new__(cls, from_city, to_city,
east_distance, east_fiber='SSMF', east_lineic=0.2,
east_con_in=None, east_con_out=None, east_pmd=0.1, east_cable='',
west_distance='', west_fiber='', west_lineic='',
east_distance, east_fiber='SSMF', east_lineic=0.2,
east_con_in=None, east_con_out=None, east_pmd=0.1, east_cable='',
west_distance='', west_fiber='', west_lineic='',
west_con_in='', west_con_out='', west_pmd='', west_cable='',
distance_units='km'):
east_values = [east_distance, east_fiber, east_lineic, east_con_in, east_con_out,
east_values = [east_distance, east_fiber, east_lineic, east_con_in, east_con_out,
east_pmd, east_cable]
west_values = [west_distance, west_fiber, west_lineic, west_con_in, west_con_out,
west_values = [west_distance, west_fiber, west_lineic, west_con_in, west_con_out,
west_pmd, west_cable]
default_values = [80,'SSMF',0.2,None,None,0.1,'']
east_values = [x[0] if x[0] != '' else x[1] for x in zip(east_values,default_values)]
west_values = [x[0] if x[0] != '' else x[1] for x in zip(west_values,east_values)]
return super().__new__(cls, from_city, to_city, *east_values, *west_values, distance_units)
return super().__new__(cls, from_city, to_city, *east_values, *west_values, distance_units)
class Eqpt(namedtuple('Eqpt', 'from_city to_city \
egress_amp_type egress_att_in egress_amp_gain egress_amp_tilt egress_amp_att_out\
@@ -64,7 +64,7 @@ class Eqpt(namedtuple('Eqpt', 'from_city to_city \
ingress_amp_type, ingress_att_in, ingress_amp_gain, ingress_amp_tilt, ingress_amp_att_out]
default_values = ['','','',0,0,0,0,'',0,0,0,0]
values = [x[0] if x[0] != '' else x[1] for x in zip(values,default_values)]
return super().__new__(cls, *values)
return super().__new__(cls, *values)
def sanity_check(nodes, nodes_by_city, links_by_city, eqpts_by_city):
try :
@@ -83,7 +83,7 @@ def sanity_check(nodes, nodes_by_city, links_by_city, eqpts_by_city):
for city,link in links_by_city.items():
if nodes_by_city[city].node_type.lower()=='ila' and len(link) != 2:
#wrong input: ILA sites can only be Degree 2
#wrong input: ILA sites can only be Degree 2
# => correct to make it a ROADM and remove entry in links_by_city
#TODO : put in log rather than print
print(f'invalid node type ({nodes_by_city[city].node_type})\
@@ -115,7 +115,7 @@ def convert_file(input_filename, filter_region=[]):
global eqpts_by_city
eqpts_by_city = defaultdict(list)
for eqpt in eqpts:
eqpts_by_city[eqpt.from_city].append(eqpt)
eqpts_by_city[eqpt.from_city].append(eqpt)
nodes = sanity_check(nodes, nodes_by_city, links_by_city, eqpts_by_city)
@@ -148,7 +148,7 @@ def convert_file(input_filename, filter_region=[]):
'latitude': x.latitude,
'longitude': x.longitude}},
'type': 'Fused'}
for x in nodes_by_city.values() if x.node_type.lower() == 'fused'] +
for x in nodes_by_city.values() if x.node_type.lower() == 'fused'] +
[{'uid': f'fiber ({x.from_city}{x.to_city})-{x.east_cable}',
'metadata': {'location': midpoint(nodes_by_city[x.from_city],
nodes_by_city[x.to_city])},
@@ -171,7 +171,7 @@ def convert_file(input_filename, filter_region=[]):
'loss_coef': x.west_lineic,
'con_in':x.west_con_in,
'con_out':x.west_con_out}
} # missing ILA construction
} # missing ILA construction
for x in links] +
[{'uid': f'egress edfa in {e.from_city} to {e.to_city}',
'metadata': {'location': {'city': nodes_by_city[e.from_city].city,
@@ -193,7 +193,7 @@ def convert_file(input_filename, filter_region=[]):
'type_variety': e.ingress_amp_type,
'operational': {'gain_target': e.ingress_amp_gain,
'tilt_target': e.ingress_amp_tilt}
}
}
for e in eqpts if e.ingress_amp_type.lower() != ''],
'connections':
list(chain.from_iterable([eqpt_connection_by_city(n.city)
@@ -205,7 +205,7 @@ def convert_file(input_filename, filter_region=[]):
for x in nodes_by_city.values() if x.node_type.lower()=='roadm'],
[{'from_node': f'roadm {x.city}',
'to_node': f'trx {x.city}'}
for x in nodes_by_city.values() if x.node_type.lower()=='roadm'])))
for x in nodes_by_city.values() if x.node_type.lower()=='roadm'])))
}
#print(dumps(data, indent=2))
@@ -235,20 +235,20 @@ def parse_excel(input_filename):
expected = ['City', 'State', 'Country', 'Region', 'Latitude', 'Longitude']
if header != expected:
raise ValueError(f'Malformed header on Nodes sheet: {header} != {expected}')
"""
"""
nodes = []
for row in all_rows(nodes_sheet, start=5):
nodes.append(Node(*(x.value for x in row[0:NODES_COLUMN])))
#check input
expected_node_types = ('ROADM', 'ILA', 'FUSED')
nodes = [n._replace(node_type='ILA')
nodes = [n._replace(node_type='ILA')
if not (n.node_type in expected_node_types) else n for n in nodes]
# sanity check
"""
header = [x.value.strip() for x in links_sheet.row(4)]
expected = ['Node A', 'Node Z',
expected = ['Node A', 'Node Z',
'Distance (km)', 'Fiber type', 'lineic att', 'Con_in', 'Con_out', 'PMD', 'Cable id',
'Distance (km)', 'Fiber type', 'lineic att', 'Con_in', 'Con_out', 'PMD', 'Cable id']
if header != expected:
@@ -280,7 +280,7 @@ def eqpt_connection_by_city(city_name):
if nodes_by_city[city_name].node_type.lower() in ('ila', 'fused'):
# Then len(other_cities) == 2
direction = ['ingress', 'egress']
for i in range(2):
for i in range(2):
from_ = fiber_link(other_cities[i], city_name)
in_ = eqpt_in_city_to_city(city_name, other_cities[0],direction[i])
to_ = fiber_link(city_name, other_cities[1-i])
@@ -334,7 +334,7 @@ def fiber_dest_from_source(city_name):
destinations = []
links_from_city = links_by_city[city_name]
for l in links_from_city:
if l.from_city == city_name:
if l.from_city == city_name:
destinations.append(l.to_city)
else:
destinations.append(l.from_city)

View File

@@ -109,7 +109,7 @@ class Roadm(Node):
'metadata' : {
'location': self.metadata['location']._asdict()
}
}
}
def __repr__(self):
return f'{type(self).__name__}(uid={self.uid!r}, loss={self.loss!r})'
@@ -174,7 +174,7 @@ class Fused(Node):
nonlinear_interference=pwr.nli/attenuation,
amplified_spontaneous_emission=pwr.ase/attenuation)
yield carrier._replace(power=pwr)
def update_pref(self, pref):
return pref._replace(p_span0=pref.p0, p_spani=pref.pi - self.loss)
@@ -192,7 +192,7 @@ class Fiber(Node):
params = {}
if 'con_in' not in params:
# if not defined in the network json connector loss in/out
# the None value will be updated in network.py[build_network]
# the None value will be updated in network.py[build_network]
# with default values from eqpt_config.json[Spans]
params['con_in'] = None
params['con_out'] = None
@@ -209,8 +209,8 @@ class Fiber(Node):
self.con_in = self.params.con_in
self.con_out = self.params.con_out
self.dispersion = self.params.dispersion # s/m/m
self.gamma = self.params.gamma # 1/W/m
self.pch_out = None
self.gamma = self.params.gamma # 1/W/m
self.pch_out = None
# TODO|jla: discuss factor 2 in the linear lineic attenuation
@property
@@ -249,15 +249,15 @@ class Fiber(Node):
def fiber_loss(self):
# dB fiber loss, not including padding attenuator
return self.loss_coef * self.length + self.con_in + self.con_out
@property
def loss(self):
#total loss incluiding padding att_in: useful for polymorphism with roadm loss
return self.loss_coef * self.length + self.con_in + self.con_out + self.att_in
@property
def passive(self):
return True
return True
@property
def lin_attenuation(self):
@@ -459,7 +459,7 @@ class Edfa(Node):
if self.pin_db is None or self.pout_db is None:
return f'{type(self).__name__} {self.uid}'
nf = mean(self.nf)
return '\n'.join([f'{type(self).__name__} {self.uid}',
return '\n'.join([f'{type(self).__name__} {self.uid}',
f' type_variety: {self.params.type_variety}',
f' effective gain(dB): {self.effective_gain:.2f}',
f' (before att_in and before output VOA)',
@@ -519,13 +519,13 @@ class Edfa(Node):
g1a = gain_target - self.params.nf_model.delta_p - dg
nf_avg = lin2db(db2lin(self.params.nf_model.nf1) + db2lin(self.params.nf_model.nf2)/db2lin(g1a))
elif self.params.type_def == 'fixed_gain':
nf_avg = self.params.nf_model.nf0
nf_avg = self.params.nf_model.nf0
else:
nf_avg = polyval(self.params.nf_fit_coeff, -dg)
if avg:
return nf_avg + pad
else:
return self.interpol_nf_ripple + nf_avg + pad # input VOA = 1 for 1 NF degradation
return self.interpol_nf_ripple + nf_avg + pad # input VOA = 1 for 1 NF degradation
def noise_profile(self, df):
""" noise_profile(bw) computes amplifier ase (W) in signal bw (Hz)
@@ -704,7 +704,7 @@ class Edfa(Node):
yield carrier._replace(power=pwr)
def update_pref(self, pref):
return pref._replace(p_span0=pref.p0,
return pref._replace(p_span0=pref.p0,
p_spani=pref.pi + self.effective_gain - self.operational.out_voa)
def __call__(self, spectral_info):

View File

@@ -4,7 +4,7 @@
'''
nf model parameters calculation
calculate nf1, nf2 and Delta_P of a 2 coils edfa with internal VOA
from nf_min and nf_max inputs
from nf_min and nf_max inputs
'''
from numpy import clip, polyval
from sys import exit
@@ -31,12 +31,12 @@ AmpBase = namedtuple(
' nf_model nf_fit_coeff nf_ripple dgt gain_ripple out_voa_auto allowed_for_design')
class Amp(AmpBase):
def __new__(cls,
type_variety, type_def, gain_flatmax, gain_min, p_max, nf_model=None,
nf_fit_coeff=None, nf_ripple=None, dgt=None, gain_ripple=None,
type_variety, type_def, gain_flatmax, gain_min, p_max, nf_model=None,
nf_fit_coeff=None, nf_ripple=None, dgt=None, gain_ripple=None,
out_voa_auto=False, allowed_for_design=True):
return super().__new__(cls,
type_variety, type_def, gain_flatmax, gain_min, p_max,
nf_model, nf_fit_coeff, nf_ripple, dgt, gain_ripple,
type_variety, type_def, gain_flatmax, gain_min, p_max,
nf_model, nf_fit_coeff, nf_ripple, dgt, gain_ripple,
out_voa_auto, allowed_for_design)
@classmethod
@@ -162,7 +162,7 @@ def trx_mode_params(equipment, trx_type_variety='', trx_mode='', error_message=F
print('Computation stopped.')
exit()
else:
# default transponder charcteristics
# default transponder charcteristics
trx_params['frequency'] = {'min': default_si_data.f_min, 'max': default_si_data.f_max}
trx_params['baud_rate'] = default_si_data.baud_rate
trx_params['spacing'] = default_si_data.spacing
@@ -178,10 +178,10 @@ def trx_mode_params(equipment, trx_type_variety='', trx_mode='', error_message=F
def automatic_spacing(baud_rate):
"""return the min possible channel spacing for a given baud rate"""
spacing_list = [(38e9,50e9), (67e9,75e9), (92e9,100e9)] #list of possible tuples
spacing_list = [(38e9,50e9), (67e9,75e9), (92e9,100e9)] #list of possible tuples
#[(max_baud_rate, spacing_for_this_baud_rate)]
acceptable_spacing_list = list(filter(lambda x : x[0]>baud_rate, spacing_list))
if len(acceptable_spacing_list) < 1:
if len(acceptable_spacing_list) < 1:
#can't find an adequate spacing from the list, so default to:
return baud_rate*1.2
else:

View File

@@ -25,7 +25,7 @@ class ConvenienceAccess:
if abbrev in kwargs:
kwargs[field] = kwargs.pop(abbrev)
return self._replace(**kwargs)
#def ptot_dbm(self):
# p = array([c.power.signal+c.power.nli+c.power.ase for c in self.carriers])
# return lin2db(sum(p*1e3))
@@ -64,7 +64,7 @@ def create_input_spectral_information(f_min, roll_off, baud_rate, power, spacing
pref = lin2db(power * 1e3)
si = SpectralInformation(pref=Pref(pref, pref))
si = si.update(carriers=[
Channel(f, (f_min+spacing*f),
Channel(f, (f_min+spacing*f),
baud_rate, roll_off, Power(power, 0, 0)) for f in range(1,nb_channel+1)
])
return si

View File

@@ -26,7 +26,7 @@ logger = getLogger(__name__)
def load_network(filename, equipment):
json_filename = ''
if filename.suffix.lower() == '.xls':
logger.info('Automatically generating topology JSON file')
logger.info('Automatically generating topology JSON file')
json_filename = convert_file(filename)
elif filename.suffix.lower() == '.json':
json_filename = filename
@@ -95,7 +95,7 @@ def select_edfa(gain_target, power_target, equipment):
power=min(
pin
+edfa.gain_flatmax
+TARGET_EXTENDED_GAIN,
+TARGET_EXTENDED_GAIN,
edfa.p_max
)
-power_target,
@@ -106,7 +106,7 @@ def select_edfa(gain_target, power_target, equipment):
acceptable_gain_list = \
list(filter(lambda x : x.gain>-TARGET_EXTENDED_GAIN, edfa_list))
if len(acceptable_gain_list) < 1:
if len(acceptable_gain_list) < 1:
#no amplifier satisfies the required gain, so pick the highest gain:
gain_max = max(edfa_list, key=itemgetter(2)).gain
#pick up all amplifiers that share this max gain:
@@ -127,10 +127,10 @@ def select_edfa(gain_target, power_target, equipment):
def set_roadm_loss(network, equipment, pref_ch_db):
roadms = [roadm for roadm in network if isinstance(roadm, Roadm)]
power_mode = equipment['Spans']['default'].power_mode
power_mode = equipment['Spans']['default'].power_mode
default_roadm_loss = equipment['Roadms']['default'].gain_mode_default_loss
pref_roadm_db = equipment['Roadms']['default'].power_mode_pref
roadm_loss = pref_ch_db - pref_roadm_db
roadm_loss = pref_ch_db - pref_roadm_db
for roadm in roadms:
if power_mode:
@@ -165,7 +165,7 @@ def target_power(dp_from_gain, network, node, equipment): #get_fiber_dp
#print(f'{repr(node)} delta power in:\n{dp}dB')
return dp
def prev_node_generator(network, node):
"""fused spans interest:
@@ -187,7 +187,7 @@ def next_node_generator(network, node):
yield next_node
yield from next_node_generator(network, next_node)
else:
StopIteration
StopIteration
def span_loss(network, node):
"""Fused span interest:
@@ -197,10 +197,10 @@ def span_loss(network, node):
prev_node = next(n for n in network.predecessors(node))
if isinstance(prev_node, Fused):
loss += sum(n.loss for n in prev_node_generator(network, node))
except StopIteration:
except StopIteration:
pass
try:
next_node = next(n for n in network.successors(node))
next_node = next(n for n in network.successors(node))
if isinstance(next_node, Fused):
loss += sum(n.loss for n in next_node_generator(network, node))
except StopIteration:
@@ -209,7 +209,7 @@ def span_loss(network, node):
def find_first_node(network, node):
"""Fused node interest:
returns the 1st node at the origin of a succession of fused nodes
returns the 1st node at the origin of a succession of fused nodes
(aka no amp in between)"""
this_node = node
for this_node in prev_node_generator(network, node):
@@ -218,7 +218,7 @@ def find_first_node(network, node):
def find_last_node(network, node):
"""Fused node interest:
returns the last node in a succession of fused nodes
returns the last node in a succession of fused nodes
(aka no amp in between)"""
this_node = node
for this_node in next_node_generator(network, node):
@@ -231,7 +231,7 @@ def set_amplifier_voa(amp, pref_total_db, power_mode):
if power_mode:
gain_target = amp.operational.gain_target
pout = pref_total_db + amp.dp_db
voa = min(amp.params.p_max-pout,
voa = min(amp.params.p_max-pout,
amp.params.gain_flatmax-amp.operational.gain_target)
voa = round2float(max(voa, 0), 0.5) - VOA_MARGIN if amp.params.out_voa_auto else 0
amp.dp_db = amp.dp_db + voa
@@ -283,7 +283,7 @@ def set_egress_amplifier(network, roadm, equipment, pref_total_db):
def add_egress_amplifier(network, node):
next_nodes = [n for n in network.successors(node)
next_nodes = [n for n in network.successors(node)
if not (isinstance(n, Transceiver) or isinstance(n, Fused) or isinstance(n, Edfa))]
#no amplification for fused spans or TRX
for i, next_node in enumerate(next_nodes):
@@ -393,16 +393,16 @@ def build_network(network, equipment, pref_ch_db, pref_total_db):
padding = default_span_data.padding
#set raodm loss for gain_mode before to build network
set_roadm_loss(network, equipment, pref_ch_db)
set_roadm_loss(network, equipment, pref_ch_db)
fibers = [f for f in network.nodes() if isinstance(f, Fiber)]
add_connector_loss(fibers, con_in, con_out, default_span_data.EOL)
add_fiber_padding(network, fibers, padding)
# don't group split fiber and add amp in the same loop
# don't group split fiber and add amp in the same loop
# =>for code clarity (at the expense of speed):
for fiber in fibers:
split_fiber(network, fiber, bounds, target_length, equipment)
amplified_nodes = [n for n in network.nodes()
amplified_nodes = [n for n in network.nodes()
if isinstance(n, Fiber) or isinstance(n, Roadm)]
for node in amplified_nodes:
add_egress_amplifier(network, node)
@@ -411,9 +411,9 @@ def build_network(network, equipment, pref_ch_db, pref_total_db):
for roadm in roadms:
set_egress_amplifier(network, roadm, equipment, pref_total_db)
#support older json input topology wo Roadms:
if len(roadms) == 0:
#support older json input topology wo Roadms:
if len(roadms) == 0:
trx = [t for t in network.nodes() if isinstance(t, Transceiver)]
for t in trx:
set_egress_amplifier(network, t, equipment, pref_total_db)
set_egress_amplifier(network, t, equipment, pref_total_db)

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# TelecomInfraProject/gnpy/examples
# Module name : path_requests_run.py
# Version :
# Version :
# License : BSD 3-Clause Licence
# Copyright (c) 2018, Telecom Infra Project
@@ -10,7 +10,7 @@
@author: jeanluc-auge
read json request file in accordance with:
Yang model for requesting Path Computation
draft-ietf-teas-yang-path-computation-01.txt.
draft-ietf-teas-yang-path-computation-01.txt.
and returns path results in terms of path and feasibility
"""
@@ -76,8 +76,8 @@ class Result_element(Element):
self.computed_path = computed_path
hop_type = []
for e in computed_path :
if isinstance(e, Transceiver) :
hop_type.append(' - '.join([path_request.tsp,path_request.tsp_mode]))
if isinstance(e, Transceiver) :
hop_type.append(' - '.join([path_request.tsp,path_request.tsp_mode]))
else:
hop_type.append('not recorded')
self.hop_type = hop_type
@@ -203,10 +203,10 @@ class Result_element(Element):
]
}
}
@property
def json(self):
return self.pathresult
return self.pathresult
def compute_constrained_path(network, req):
trx = [n for n in network.nodes() if isinstance(n, Transceiver)]
@@ -226,7 +226,7 @@ def compute_constrained_path(network, req):
node = next(el for el in roadm if el.uid == f'roadm {n}')
except StopIteration:
try:
node = next(el for el in edfa
node = next(el for el in edfa
if el.uid.startswith(f'egress edfa in {n}'))
except StopIteration:
msg = f'could not find node : {n} in network topology: \
@@ -257,7 +257,7 @@ def compute_constrained_path(network, req):
# target=next(el for el in trx if el.uid == req.destination)):
# print([e.uid for e in p if isinstance(e,Roadm)])
return total_path
return total_path
def propagate(path, req, equipment, show=False):
#update roadm loss in case of power sweep (power mode only)
@@ -269,13 +269,13 @@ def propagate(path, req, equipment, show=False):
si = el(si)
if show :
print(el)
return path
return path
def jsontocsv(json_data,equipment,fileout):
# read json path result file in accordance with:
# Yang model for requesting Path Computation
# draft-ietf-teas-yang-path-computation-01.txt.
# draft-ietf-teas-yang-path-computation-01.txt.
# and write results in an CSV file
mywriter = writer(fileout)
@@ -290,32 +290,32 @@ def jsontocsv(json_data,equipment,fileout):
['path-route-object']['unnumbered-hop']['node-id']
destination = p['path-properties']['path-route-objects'][-1]\
['path-route-object']['unnumbered-hop']['node-id']
pth = ' | '.join([ e['path-route-object']['unnumbered-hop']['node-id']
pth = ' | '.join([ e['path-route-object']['unnumbered-hop']['node-id']
for e in p['path-properties']['path-route-objects']])
[tsp,mode] = p['path-properties']['path-route-objects'][0]\
['path-route-object']['unnumbered-hop']['hop-type'].split(' - ')
# find the min acceptable OSNR, baud rate from the eqpt library based on tsp (tupe) and mode (format)
try:
[minosnr, baud_rate] = next([m['OSNR'] , m['baud_rate']]
[minosnr, baud_rate] = next([m['OSNR'] , m['baud_rate']]
for m in equipment['Transceiver'][tsp].mode if m['format']==mode)
# for debug
# print(f'coucou {baud_rate}')
except IndexError:
msg = f'could not find tsp : {self.tsp} with mode: {self.tsp_mode} in eqpt library'
raise ValueError(msg)
output_snr = next(e['accumulative-value']
output_snr = next(e['accumulative-value']
for e in p['path-properties']['path-metric'] if e['metric-type'] == 'SNR@0.1nm')
output_snrbandwidth = next(e['accumulative-value']
output_snrbandwidth = next(e['accumulative-value']
for e in p['path-properties']['path-metric'] if e['metric-type'] == 'SNR@bandwidth')
output_osnr = next(e['accumulative-value']
output_osnr = next(e['accumulative-value']
for e in p['path-properties']['path-metric'] if e['metric-type'] == 'OSNR@0.1nm')
output_osnrbandwidth = next(e['accumulative-value']
output_osnrbandwidth = next(e['accumulative-value']
for e in p['path-properties']['path-metric'] if e['metric-type'] == 'OSNR@bandwidth')
power = next(e['accumulative-value']
power = next(e['accumulative-value']
for e in p['path-properties']['path-metric'] if e['metric-type'] == 'reference_power')
if isinstance(output_snr, str):
isok = ''
@@ -333,5 +333,5 @@ def jsontocsv(json_data,equipment,fileout):
output_osnr,
output_snrbandwidth,
output_snr,
isok
))
isok
))

View File

@@ -61,10 +61,10 @@ def write_csv(obj, filename):
#main header
w.writerow([data_key])
#sub headers:
headers = [_ for _ in data_list[0].keys()]
headers = [_ for _ in data_list[0].keys()]
w.writerow(headers)
for data_dict in data_list:
w.writerow([_ for _ in data_dict.values()])
w.writerow([_ for _ in data_dict.values()])
def c():
"""