Skip to content

Commit 05bd113

Browse files
committed
feat: added 19 and 20 metadata
1 parent 5b92fbb commit 05bd113

4 files changed

Lines changed: 5621 additions & 116 deletions

File tree

senate/main.py

Lines changed: 123 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -295,62 +295,62 @@ async def fetch_bill(self, congress: int, bill_type: str, number: int) -> Option
295295
if title_elem:
296296
bill_data['title'] = self.clean_text(title_elem.get_text())
297297

298-
# Extract long title
299-
long_title_elem = soup.find('span', text=re.compile('Long Title:'))
300-
if long_title_elem and long_title_elem.next_sibling:
301-
long_title = self.clean_text(str(long_title_elem.next_sibling))
302-
if long_title and long_title != bill_data.get('title'):
303-
bill_data['longTitle'] = long_title
304-
305-
# Extract scope
306-
scope_elem = soup.find('span', text=re.compile('Scope:'))
307-
if scope_elem and scope_elem.next_sibling:
308-
bill_data['scope'] = self.clean_text(str(scope_elem.next_sibling))
309-
310-
# Extract subject(s)
311-
subject_elem = soup.find('span', text=re.compile('Subject\\(s\\):'))
312-
if subject_elem and subject_elem.next_sibling:
313-
subjects = self.clean_text(str(subject_elem.next_sibling))
314-
if subjects:
315-
bill_data['subject'] = [s.strip() for s in subjects.split(';')]
316-
317-
# Extract status
318-
status_elem = soup.find('span', text=re.compile('Status:'))
319-
if status_elem and status_elem.next_sibling:
320-
status_text = self.clean_text(str(status_elem.next_sibling))
321-
bill_data['status'] = {
322-
'status': status_text,
323-
'statusCode': ''
324-
}
325-
326-
# Extract committee info with proper structure
327-
committee_elem = soup.find('span', text=re.compile('Committee:'))
328-
if committee_elem and committee_elem.next_sibling:
329-
committees_text = self.clean_text(str(committee_elem.next_sibling))
330-
if committees_text:
331-
# Parse committee text - may contain codes and names
332-
committees = []
333-
for comm_text in committees_text.split(';'):
334-
comm_text = comm_text.strip()
335-
if comm_text:
336-
# Try to extract code if present (e.g., "FINAN - Finance")
337-
code_match = re.match(r'([A-Z]+)\s*-\s*(.+)', comm_text)
338-
if code_match:
339-
committees.append({
340-
'code': code_match.group(1),
341-
'name': code_match.group(2).strip()
342-
})
343-
else:
344-
committees.append({
345-
'code': '',
346-
'name': comm_text
347-
})
348-
349-
if committees:
350-
bill_data['primaryCommittee'] = committees[0]
351-
if len(committees) > 1:
352-
bill_data['secondaryCommittees'] = committees[1:]
353-
bill_data['committees'] = committees
298+
# Extract author from "Filed on" text
299+
content_td = soup.find('td', id='content')
300+
if content_td:
301+
filed_text = content_td.get_text()
302+
filed_match = re.search(r'Filed on ([^\n]+) by ([^\n]+)', filed_text)
303+
if filed_match:
304+
bill_data['filedDate'] = filed_match.group(1).strip()
305+
bill_data['author'] = filed_match.group(2).strip()
306+
307+
# Extract data from p/blockquote pairs
308+
paragraphs = soup.find_all('p')
309+
for p in paragraphs:
310+
p_text = self.clean_text(p.get_text()).lower()
311+
next_elem = p.find_next_sibling('blockquote')
312+
if next_elem:
313+
content = self.clean_text(next_elem.get_text())
314+
315+
if 'long title' in p_text and content:
316+
bill_data['longTitle'] = content
317+
elif 'scope' in p_text and content:
318+
bill_data['scope'] = content
319+
elif 'subject' in p_text:
320+
# Handle multiple subjects separated by <br> tags
321+
# Get the raw HTML to preserve br tags
322+
subjects = []
323+
for br in next_elem.find_all('br'):
324+
br.replace_with('|||') # Replace br with delimiter
325+
content = self.clean_text(next_elem.get_text())
326+
if '|||' in content:
327+
# Split by our delimiter
328+
subjects = [s.strip() for s in content.split('|||') if s.strip()]
329+
elif ';' in content or '/' in content:
330+
# Fallback to semicolon or slash separation
331+
subjects = re.split(r'[;/]', content)
332+
subjects = [s.strip() for s in subjects if s.strip()]
333+
else:
334+
# Single subject
335+
subjects = [content.strip()] if content.strip() else []
336+
337+
if subjects:
338+
bill_data['subject'] = subjects
339+
elif 'legislative status' in p_text and content:
340+
# Extract status and date
341+
status_match = re.match(r'(.+?)\s*\((\d+/\d+/\d+)\)', content)
342+
if status_match:
343+
bill_data['status'] = {
344+
'status': status_match.group(1).strip(),
345+
'date': status_match.group(2).strip()
346+
}
347+
else:
348+
bill_data['status'] = {'status': content}
349+
elif 'primary committee' in p_text and content:
350+
bill_data['committee'] = {
351+
'name': content,
352+
'type': 'primary'
353+
}
354354

355355
# Extract abstract
356356
abstract_elem = soup.find('div', {'class': 'lis_billabstract'})
@@ -362,27 +362,17 @@ async def fetch_bill(self, congress: int, bill_type: str, number: int) -> Option
362362
if history:
363363
bill_data['legislativeHistory'] = history
364364

365-
# Extract filed date and author from first history entry
365+
# Extract additional info from history (e.g., co-authors)
366366
for entry in history:
367367
action = entry.get('action', '')
368-
if 'Filed by' in action:
369-
# Extract author
370-
match = re.search(r'Filed by (.+)', action)
368+
# Check for introduced by senator (alternative author extraction)
369+
if 'Introduced by Senator' in action and 'author' not in bill_data:
370+
match = re.search(r'Introduced by Senator (.+?)(?:;|$)', action)
371371
if match:
372-
author_name = match.group(1).strip()
373-
bill_data['principalAuthor'] = {
374-
'name': author_name,
375-
'code': ''
376-
}
377-
# Use date from this entry
378-
if 'date' in entry:
379-
bill_data['dateFiled'] = entry['date']
380-
break
381-
382-
# Extract co-authors
383-
for entry in history:
384-
if 'Co-Authors:' in entry.get('action', ''):
385-
match = re.search(r'Co-Authors?:\s*(.+)', entry['action'])
372+
bill_data['author'] = match.group(1).strip()
373+
# Extract co-authors
374+
if 'Co-Author' in action:
375+
match = re.search(r'Co-Authors?:\s*(.+)', action)
386376
if match:
387377
coauthors = match.group(1).strip()
388378
bill_data['coAuthors'] = [a.strip() for a in coauthors.split(',')]
@@ -392,13 +382,28 @@ async def fetch_bill(self, congress: int, bill_type: str, number: int) -> Option
392382
if related:
393383
bill_data.update(related)
394384

395-
# Extract PDF URL
396-
pdf_link = soup.find('a', href=re.compile(r'\.pdf$', re.I))
397-
if pdf_link:
398-
pdf_url = pdf_link.get('href')
399-
if not pdf_url.startswith('http'):
400-
pdf_url = f"https://web.senate.gov.ph{pdf_url}"
401-
bill_data['pdfUrl'] = pdf_url
385+
# Extract PDF URL from download section
386+
download_div = soup.find('div', id='lis_download')
387+
if download_div:
388+
pdf_links = download_div.find_all('a', href=re.compile(r'\.pdf', re.I))
389+
if pdf_links:
390+
pdf_url = pdf_links[0].get('href')
391+
if not pdf_url.startswith('http'):
392+
pdf_url = f"https://web.senate.gov.ph{pdf_url}"
393+
bill_data['pdfUrl'] = pdf_url
394+
395+
# Get PDF info (filename, date, size)
396+
pdf_text = pdf_links[0].get_text(strip=True)
397+
if pdf_text:
398+
bill_data['pdfFileName'] = pdf_text
399+
else:
400+
# Fallback: look for any PDF link
401+
pdf_link = soup.find('a', href=re.compile(r'\.pdf$', re.I))
402+
if pdf_link:
403+
pdf_url = pdf_link.get('href')
404+
if not pdf_url.startswith('http'):
405+
pdf_url = f"https://web.senate.gov.ph{pdf_url}"
406+
bill_data['pdfUrl'] = pdf_url
402407

403408
return bill_data
404409

@@ -446,38 +451,40 @@ def extract_legislative_history(self, soup: BeautifulSoup) -> List[Dict]:
446451
"""Extract legislative history from bill page."""
447452
history = []
448453

449-
# Look for Legislative History table
450-
history_section = None
451-
452-
# Try to find the section with "Legislative History" header
453-
for header in soup.find_all(['h3', 'h4', 'span'], text=re.compile('Legislative History')):
454-
# Find the next table after this header
455-
next_sibling = header.find_next_sibling('table')
456-
if next_sibling:
457-
history_section = next_sibling
458-
break
459-
460-
# Alternative: look for table that contains legislative history
461-
if not history_section:
462-
for table in soup.find_all('table'):
463-
if 'Legislative History' in str(table) or 'Date' in str(table) and 'Action' in str(table):
464-
history_section = table
465-
break
466-
467-
if history_section:
468-
rows = history_section.find_all('tr')
469-
for row in rows:
470-
cells = row.find_all('td')
471-
if len(cells) >= 2:
472-
date_text = self.clean_text(cells[0].get_text())
473-
action_text = self.clean_text(cells[1].get_text())
474-
475-
# Skip header row
476-
if date_text and action_text and date_text != 'Date':
477-
history.append({
478-
'date': date_text,
479-
'action': action_text
480-
})
454+
# Look for Legislative History in blockquote with table
455+
for p in soup.find_all('p'):
456+
if 'legislative history' in p.get_text().lower():
457+
blockquote = p.find_next_sibling('blockquote')
458+
if blockquote:
459+
table = blockquote.find('table', id='lis_table')
460+
if table:
461+
rows = table.find_all('tr')
462+
for row in rows:
463+
cells = row.find_all('td')
464+
465+
# Handle different row types
466+
if len(cells) == 2:
467+
# Date and action row
468+
date_text = self.clean_text(cells[0].get_text())
469+
action_text = self.clean_text(cells[1].get_text())
470+
471+
# Check if it's actually a date row (has date format)
472+
if re.match(r'\d+/\d+/\d+', date_text):
473+
history.append({
474+
'date': date_text,
475+
'action': action_text
476+
})
477+
elif len(cells) == 1 and cells[0].get('colspan') == '2':
478+
# Full width row (like "Entitled:" or session info)
479+
text = self.clean_text(cells[0].get_text())
480+
if text and not text.startswith('['):
481+
# Add as special entry without date
482+
if 'Entitled:' in text:
483+
history.append({
484+
'date': '',
485+
'action': text
486+
})
487+
break
481488

482489
return history
483490

@@ -486,23 +493,23 @@ def extract_related_bills(self, soup: BeautifulSoup) -> Dict:
486493
related = {}
487494

488495
# Look for consolidated bills
489-
consolidated_elem = soup.find('span', text=re.compile('Consolidated.*with'))
496+
consolidated_elem = soup.find('span', string=re.compile('Consolidated.*with'))
490497
if consolidated_elem:
491498
text = self.clean_text(consolidated_elem.get_text())
492499
match = re.findall(r'[SH]BN-\d+', text)
493500
if match:
494501
related['consolidatedWith'] = match
495502

496503
# Look for substitute bills
497-
substitute_elem = soup.find('span', text=re.compile('In substitution'))
504+
substitute_elem = soup.find('span', string=re.compile('In substitution'))
498505
if substitute_elem:
499506
text = self.clean_text(substitute_elem.get_text())
500507
match = re.findall(r'[SH]BN-\d+', text)
501508
if match:
502509
related['substituteFor'] = match
503510

504511
# Look for related bills section
505-
related_section = soup.find('span', text=re.compile('Related.*Bill'))
512+
related_section = soup.find('span', string=re.compile('Related.*Bill'))
506513
if related_section:
507514
text = self.clean_text(related_section.get_text())
508515
match = re.findall(r'[SH]BN-\d+', text)

0 commit comments

Comments
 (0)