1+ import json
12import os
23from textwrap import dedent
4+ import xml .etree .ElementTree as ET
35
46import requests
57
@@ -19,9 +21,12 @@ def get_table_res_field(field):
1921 'Edm.Int32' : 'integer' , # 123
2022 'Edm.Int16' : 'integer' , # 123
2123 'Edm.Byte' : 'integer' , # 123
22- 'Edm.DateTimeOffset' : 'datetime' , # "2016-02-28T10:22:10.843+02:00"
24+ 'Edm.DateTimeOffset' : 'datetime' , # "2016-02-28T10:22:10.843+02:00",
25+ 'Edm.DateTime' : 'datetime' ,
2326 None : 'string' , # "string",
2427 'Edm.Boolean' : 'boolean' ,
28+ 'Edm.String' : 'string' ,
29+ 'Edm.Decimal' : 'string' ,
2530 }
2631 assert field .get ("$Type" ) in field_types , f"unknown field type { field .get ('$Type' )} "
2732 return {
@@ -30,8 +35,8 @@ def get_table_res_field(field):
3035
3136
3237def get_parliamentinfo_tables ():
33- data = requests .get ('https://knesset.gov.il/OdataV4/ParliamentInfo/$metadata?$format=json' ).json ()
3438 res = {}
39+ data = requests .get ('https://knesset.gov.il/OdataV4/ParliamentInfo/$metadata?$format=json' ).json ()
3540 for table_name , fields in data ['OdataService.DAL.ParliamentInfo' ].items ():
3641 assert fields .pop ("$Kind" ) == 'EntityType'
3742 keys = fields .pop ("$Key" ) or []
@@ -42,11 +47,38 @@ def get_parliamentinfo_tables():
4247 field_name : get_table_res_field (field ) for field_name , field in fields .items () if field .get ('$Kind' ) is None
4348 }
4449 }
50+ for service_name in (
51+ 'Lobbyists' ,
52+ 'Votes' ,
53+ ):
54+ xmlstr = requests .get (f'https://knesset.gov.il/Odata/{ service_name } .svc/$metadata' ).text
55+ root = ET .fromstring (xmlstr )
56+ ns = {
57+ "edmx" : "http://schemas.microsoft.com/ado/2007/06/edmx" ,
58+ "edm" : "http://schemas.microsoft.com/ado/2009/11/edm" ,
59+ }
60+ for schema in root .findall (".//edm:Schema" , ns ):
61+ for entity_type in schema .findall ("edm:EntityType" , ns ):
62+ table_name = f'{ service_name .lower ()} _{ entity_type .get ("Name" )} '
63+ fields = {}
64+ for prop in entity_type .findall ("edm:Property" , ns ):
65+ field_name = prop .get ("Name" )
66+ fields [field_name ] = get_table_res_field ({
67+ "$Type" : prop .get ("Type" ),
68+ })
69+ edm_keys = entity_type .findall ("edm:Key" , ns )
70+ assert len (edm_keys ) == 1
71+ edm_key = edm_keys [0 ]
72+ keys = [k .get ("Name" ) for k in edm_key .findall ("edm:PropertyRef" , ns )]
73+ res [table_name ] = {
74+ 'primary_key' : keys [0 ] if len (keys ) == 1 else keys ,
75+ 'fields' : fields ,
76+ }
4577 return res
4678
4779
48- def get_pipelines_res_field (field ):
49- assert field ['source' ] == '{name}' , f'unexpected source { field ["source" ]} '
80+ def get_pipelines_res_field (field , pipeline_id = None , field_name = None ):
81+ assert field ['source' ] == '{name}' , f'unexpected source { field ["source" ]} in field { field_name } of pipeline { pipeline_id } '
5082 return {
5183 'type' : field ['type' ],
5284 'primary_key' : bool (field .get ('primaryKey' )),
@@ -56,88 +88,126 @@ def get_pipelines_res_field(field):
5688def get_parliamentinfo_pipelines ():
5789 res = {}
5890 for pipeline in list_pipelines (full = True ):
59- if pipeline .get ('dataservice_params' ) and pipeline ['dataservice_params' ].get ('service-name' ) == 'api' :
60- res [pipeline ['dataservice_params' ]['method-name' ]] = {}
61- res [pipeline ['dataservice_params' ]['method-name' ]]['fields' ] = {
62- name : get_pipelines_res_field (field ) for name , field in pipeline ['dataservice_params' ]['fields' ].items ()
91+ pipeline_id = pipeline ['pipeline_id' ]
92+ if pipeline .get ('dataservice_params' ):
93+ dataservice_params = pipeline ['dataservice_params' ]
94+ odata_v4 = dataservice_params .get ('odata-v4' , True )
95+ if odata_v4 :
96+ assert dataservice_params .get ('service-name' ) == 'api' , f'{ pipeline_id } : api service should have service-name set to api'
97+ method_name = dataservice_params ['method-name' ]
98+ else :
99+ method_name = f'{ dataservice_params ["service-name" ]} _{ dataservice_params ["method-name" ]} '
100+ res [method_name ] = {}
101+ res [method_name ]['fields' ] = {
102+ name : get_pipelines_res_field (field , pipeline_id = pipeline_id , field_name = name )
103+ for name , field in dataservice_params ['fields' ].items ()
63104 }
64- primary_keys = [name for name , field in res [pipeline ['dataservice_params' ]['method-name' ]]['fields' ].items () if field .get ('primary_key' )]
65- assert len (primary_keys ) <= 1 , f"unexpected number of primary keys in pipeline { pipeline ['pipeline_id' ]} : { primary_keys } "
66- res [pipeline ['dataservice_params' ]['method-name' ]]['primary_key' ] = primary_keys [0 ] if primary_keys else None
105+ primary_keys = [name for name , field in res [method_name ]['fields' ].items () if field .get ('primary_key' )]
106+ if len (primary_keys ) == 1 :
107+ res [method_name ]['primary_key' ] = primary_keys [0 ]
108+ elif len (primary_keys ) > 1 :
109+ res [method_name ]['primary_key' ] = primary_keys
110+ else :
111+ raise Exception (f'no primary key found in pipeline { pipeline_id } for method { method_name } ' )
67112 return res
68113
69114
70- class MissingPipeline :
115+ class MissingObject :
71116
72- def __init__ (self , table_name , table ):
117+ def __init__ (self , dst_obj_type , table_name , table = None ):
118+ self .dst_obj_type = dst_obj_type
73119 self .table_name = table_name
74120 self .table = table
75121
76122 def __str__ (self ):
77- res = f'table { self .table_name } is missing from pipelines\n '
78- filename = f'knesset/{ self .table_name .lower ()} .yaml'
79- filecontent = dedent (f'''
80- pipeline-type: knesset dataservice
81- dataservice-parameters:
82- service-name: api
83- method-name: "{ self .table_name } "
84- fields:
85- ''' )
86- for field_name , field in self .table ['fields' ].items ():
87- filecontent += f' { field_name } :\n '
88- filecontent += f' source: "{{name}}"\n '
89- filecontent += f' type: "{ field ["type" ]} "\n '
90- if field_name == self .table ['primary_key' ]:
91- filecontent += f' primaryKey: true\n '
92- with open (os .path .join (os .path .dirname (__file__ ), '..' , 'pipelines' , filename ), 'w' ) as f :
93- f .write (filecontent )
94- return res
95-
96-
97- class IncompletePipeline :
98-
99- def __init__ (self , table_name , table , pipeline ):
123+ if self .dst_obj_type == 'pipelines' :
124+ filename = f'knesset/{ self .table_name .lower ()} .yaml'
125+ filecontent = dedent (f'''
126+ pipeline-type: knesset dataservice
127+ dataservice-parameters:
128+ service-name: api
129+ method-name: "{ self .table_name } "
130+ fields:
131+ ''' )
132+ for field_name , field in self .table ['fields' ].items ():
133+ filecontent += f' { field_name } :\n '
134+ filecontent += f' source: "{{name}}"\n '
135+ filecontent += f' type: "{ field ["type" ]} "\n '
136+ if field_name == self .table ['primary_key' ]:
137+ filecontent += f' primaryKey: true\n '
138+ with open (os .path .join (os .path .dirname (__file__ ), '..' , 'pipelines' , filename ), 'w' ) as f :
139+ f .write (filecontent )
140+ return f'{ self .table_name } is missing from { self .dst_obj_type } , wrote to filename { filename } \n '
141+ else :
142+ return f'{ self .table_name } exists in pipelines but missing in tables'
143+
144+
145+ class InvalidPipeline :
146+
147+ def __init__ (self , dst_obj_type , table_name , table ):
148+ self .dst_obj_type = dst_obj_type
100149 self .table_name = table_name
101150 self .table = table
102- self .pipeline = pipeline
103151 self .missing_fields = []
104152 self .wrong_type_fields = []
153+ self .extra_fields = []
154+ self .primary_key_mismatch = False
105155
106156 def __str__ (self ):
107- res = f'pipeline for table { self .table_name } is incomplete :\n '
157+ res = f'mismatch in pipeline for table { self .table_name } : \n ' if self . dst_obj_type == 'pipelines' else f'mismatch in table for pipeline { self . table_name } :\n '
108158 if len (self .missing_fields ) > 0 :
109- res += ' - missing fields:\n '
159+ res += ' - missing fields in pipeline: \n ' if self . dst_obj_type == 'pipelines' else ' - extra fields in pipeline do not appear in table :\n '
110160 for field_name in self .missing_fields :
111- res += f'{ field_name } : {{source: "{{name}}", type: "{ self .table ["fields" ][ field_name ][ "type" ] } "}}\n '
161+ res += f'{ field_name } : {{source: "{{name}}", type: "{ self .table ["fields" ]. get ( field_name , {}). get ( "type" ) } "}}\n '
112162 if len (self .wrong_type_fields ) > 0 :
113- res += ' - wrong type fields:\n '
163+ res += ' - wrong type fields:\n ' if self . dst_obj_type == 'pipelines' else ' - wrong type fields (type in table shown below does not match type in pipeline): \n '
114164 for field_name in self .wrong_type_fields :
115165 res += f'{ field_name } : {{source: "{{name}}", type: "{ self .table ["fields" ][field_name ]["type" ]} "}}\n '
166+ if len (self .extra_fields ) > 0 :
167+ res += ' - extra fields in pipeline which are not in table:\n ' if self .dst_obj_type == 'pipelines' else ' - missing fields in pipeline which appear in table:\n '
168+ for field_name in self .extra_fields :
169+ res += f'{ field_name } \n '
170+ if self .primary_key_mismatch :
171+ res += ' - primary key mismatch:\n '
172+ res += f'table primary key: { self .table ["primary_key" ]} \n '
116173 return res
117174
175+ def has_errors (self ):
176+ return sum ((
177+ len (self .missing_fields ),
178+ len (self .wrong_type_fields ),
179+ len (self .extra_fields ),
180+ )) > 0 or self .primary_key_mismatch
181+
118182
119183def compare_parliamentinfo_tables_pipelines ():
120184 fix_objects = []
121185 tables = get_parliamentinfo_tables ()
122186 pipelines = get_parliamentinfo_pipelines ()
123- for table_name , table in tables .items ():
124- if table_name not in pipelines :
125- fix_objects .append (MissingPipeline (table_name , table ))
126- continue
127- pipeline = pipelines [table_name ]
128- incomplete_pipeline = IncompletePipeline (table_name , table , pipeline )
129- for field_name , field in table ['fields' ].items ():
130- if field_name not in pipeline ['fields' ]:
131- if table ['primary_key' ] == field_name and pipeline ['primary_key' ]:
132- field_name = pipeline ['primary_key' ]
133- else :
134- incomplete_pipeline .missing_fields .append (field_name )
135- continue
136- pipeline_field = pipeline ['fields' ][field_name ]
137- if field ['type' ] != pipeline_field ['type' ] and field_name not in IGNORE_FIELD_TYPE_ERRORS .get (table_name , []):
138- incomplete_pipeline .wrong_type_fields .append (field_name )
139- if len (incomplete_pipeline .missing_fields ) > 0 or len (incomplete_pipeline .wrong_type_fields ) > 0 :
140- fix_objects .append (incomplete_pipeline )
187+ for dst_obj_type , src , dst in (('pipelines' , tables , pipelines ), ('tables' , pipelines , tables )):
188+ for src_name , src_obj in src .items ():
189+ if src_name not in dst :
190+ fix_objects .append (MissingObject (dst_obj_type , src_name , src_obj if dst_obj_type == 'pipelines' else None ))
191+ else :
192+ dst_obj = dst [src_name ]
193+ invalid_pipeline = InvalidPipeline (dst_obj_type , src_name , tables [src_name ])
194+ for field_name , field in src_obj ['fields' ].items ():
195+ if field_name not in dst_obj ['fields' ]:
196+ if dst_obj_type == 'pipelines' and src_obj ['primary_key' ] == field_name and dst_obj ['primary_key' ]:
197+ # handle the special case of primary key for v4 being renamed to Id
198+ field_name = dst_obj ['primary_key' ]
199+ elif dst_obj_type == 'tables' and src_obj ['primary_key' ] == field_name and dst_obj ['primary_key' ] == 'Id' :
200+ # handle the special case of primary key for v4 being renamed to Id
201+ field_name = 'Id'
202+ else :
203+ invalid_pipeline .missing_fields .append (field_name )
204+ continue
205+ if field ['type' ] != dst_obj ['fields' ][field_name ]['type' ] and field_name not in IGNORE_FIELD_TYPE_ERRORS .get (src_name , []):
206+ invalid_pipeline .wrong_type_fields .append (field_name )
207+ if set (src_obj ['primary_key' ]) != set (dst_obj ['primary_key' ]) and tables [src_name ]['primary_key' ] != 'Id' :
208+ invalid_pipeline .primary_key_mismatch = True
209+ if invalid_pipeline .has_errors ():
210+ fix_objects .append (invalid_pipeline )
141211 if len (fix_objects ) > 0 :
142212 for fix_object in fix_objects :
143213 print (fix_object )
@@ -148,3 +218,7 @@ def compare_parliamentinfo_tables_pipelines():
148218
149219if __name__ == '__main__' :
150220 compare_parliamentinfo_tables_pipelines ()
221+ # print(json.dumps(list(get_parliamentinfo_pipelines().keys()), indent=2))
222+ # print(json.dumps(get_parliamentinfo_pipelines()["lobbyists_V_Lobbyists"], indent=2))
223+ # print(json.dumps(list(get_parliamentinfo_tables().keys()), indent=2))
224+ # print(json.dumps(get_parliamentinfo_tables()["lobbyists_V_Lobbyists"], indent=2))
0 commit comments