11import datetime as dt
2+ from functools import partial
23import json
4+ from json .decoder import JSONDecodeError
5+ from requests .exceptions import JSONDecodeError as RequestsJSONDecodeError
6+ import random
7+ import time
38
49import requests
510from openstates .scrape import Scraper , Bill , VoteEvent
@@ -33,6 +38,40 @@ class DEBillScraper(Scraper, LXMLMixin):
3338 "senatedems" : "https://senatedems.delaware.gov/members/senate-district-" ,
3439 }
3540
41+ def decode_and_retry_request (
42+ self , log_label , request_method , attempt = 1 , retries = 3 , raise_exception = True
43+ ):
44+ """
45+ Delaware is giving us a lot of malformed responses that are HTTP OK nonetheless
46+ Normally flakey requests could be handled by http-resilience mode, but because the HTTP response
47+ is normal (just empty/not-decodable-to-JSON)
48+ So we have several places where we need to retry the decode step
49+ """
50+ try :
51+ response = request_method ()
52+ data = json .loads (response .content .decode ("utf-8" ))
53+ return data
54+ except (JSONDecodeError , RequestsJSONDecodeError ) as error :
55+ if attempt < retries :
56+ min_seconds = 5
57+ max_seconds = 10
58+ delay = random .uniform (min_seconds , max_seconds )
59+ self .warning (
60+ f"Failed to fetch { log_label } on attempt { attempt } , retrying w delay { delay } "
61+ )
62+ time .sleep (delay )
63+ return self .decode_and_retry_request (
64+ log_label , request_method , attempt + 1 , retries , raise_exception
65+ )
66+ else :
67+ self .warning (
68+ f"Failed to fetch { log_label } on attempt { attempt } of { retries } , giving up"
69+ )
70+ if raise_exception :
71+ raise error
72+ else :
73+ return None
74+
3675 def scrape (self , session = None ):
3776 self .headers = {
3877 "x-oxylabs-force-headers" : "1" ,
@@ -67,7 +106,13 @@ def scrape(self, session=None):
67106 bills_and_votes = []
68107 page_number = 1
69108 while True :
70- page = self .post_search (session , page_number , per_page )
109+ post_search = partial (
110+ self .post_search ,
111+ session = session ,
112+ page_number = page_number ,
113+ per_page = per_page ,
114+ )
115+ page = self .decode_and_retry_request ("post_search" , post_search )
71116 if not page ["Data" ]:
72117 self .info ("Found no more bills in pagination" )
73118 break
@@ -296,18 +341,20 @@ def scrape_votes(self, bill, legislation_id, session):
296341 )
297342 form = {"legislationId" : legislation_id , "sort" : "" , "group" : "" , "filter" : "" }
298343 self .info (f"Searching for votes for { bill .identifier } " )
299- response = self .session .post (
344+ request_method = partial (
345+ self .session .post ,
300346 url = votes_url ,
301347 data = form ,
302348 allow_redirects = True ,
303349 verify = False ,
304350 headers = self .headers ,
305351 )
306- if response .content :
307- page = json .loads (response .content .decode ("utf-8" ))
308- if page ["Total" ] > 0 :
309- for row in page ["Data" ]:
310- yield from self .scrape_vote (bill , row ["RollCallId" ], session )
352+ page = self .decode_and_retry_request (
353+ "scrape_votes" , request_method , retries = 1 , raise_exception = False
354+ )
355+ if page and page ["Total" ] > 0 :
356+ for row in page ["Data" ]:
357+ yield from self .scrape_vote (bill , row ["RollCallId" ], session )
311358
312359 def scrape_vote (self , bill , vote_id , session ):
313360 vote_url = (
@@ -316,14 +363,17 @@ def scrape_vote(self, bill, vote_id, session):
316363 form = {"rollCallId" : vote_id , "sort" : "" , "group" : "" , "filter" : "" }
317364
318365 self .info (f"Fetching vote { vote_id } for { bill .identifier } " )
319- response = self .session .post (
366+ request_method = partial (
367+ self .session .post ,
320368 url = vote_url ,
321369 data = form ,
322370 allow_redirects = True ,
323371 verify = False ,
324372 headers = self .headers ,
325373 )
326- page = response .json ()
374+ page = self .decode_and_retry_request (
375+ "scrape_vote" , request_method , retries = 1 , raise_exception = False
376+ )
327377
328378 if page :
329379 roll = page ["Model" ]
@@ -419,14 +469,15 @@ def scrape_actions(self, bill, legislation_id):
419469 )
420470 form = {"legislationId" : legislation_id , "sort" : "" , "group" : "" , "filter" : "" }
421471 self .info (f"Fetching actions for { bill .identifier } " )
422- response = self .session .post (
472+ request_method = partial (
473+ self .session .post ,
423474 url = actions_url ,
424475 data = form ,
425476 allow_redirects = True ,
426477 verify = False ,
427478 headers = self .headers ,
428479 )
429- page = response . json ( )
480+ page = self . decode_and_retry_request ( "scrape_actions" , request_method )
430481 for row in page ["Data" ]:
431482 action_name = row ["ActionDescription" ]
432483 action_date = dt .datetime .strptime (
@@ -558,8 +609,7 @@ def post_search(self, session, page_number, per_page):
558609 verify = False ,
559610 headers = self .headers ,
560611 )
561- page = response .json ()
562- return page
612+ return response
563613
564614 def mime_from_link (self , link ):
565615 if "HtmlDocument" in link :
0 commit comments