warn-scraper 1.2.155.dev0__py3-none-any.whl → 1.2.157.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
warn/scrapers/al.py CHANGED
@@ -1,14 +1,14 @@
1
+ import csv
1
2
  import logging
2
- import re
3
+ from io import StringIO
3
4
  from pathlib import Path
4
5
 
5
- from bs4 import BeautifulSoup
6
-
7
6
  from .. import utils
7
+ from ..cache import Cache
8
8
 
9
- __authors__ = ["zstumgoren", "Dilcia19"]
9
+ __authors__ = ["zstumgoren", "Dilcia19", "stucka"]
10
10
  __tags__ = [
11
- "html",
11
+ "csv",
12
12
  ]
13
13
  __source__ = {
14
14
  "name": "Alabama Department of Commerce",
@@ -31,45 +31,40 @@ def scrape(
31
31
 
32
32
  Returns: the Path where the file is written
33
33
  """
34
+ cache = Cache()
34
35
  output_csv = data_dir / "al.csv"
36
+
35
37
  # page = utils.get_url("https://www.madeinalabama.com/warn-list/")
36
38
  # URL change in June 2026, maybe led to a HTTP 415 error
37
- page = utils.get_url("https://workforce.alabama.gov/warn-list/")
38
-
39
- # can't see 2020 listings when I open web page, but they are on the summary in the google search
40
- soup = BeautifulSoup(page.text, "html.parser")
41
- table = soup.find_all("table") # output is list-type
42
- table_rows = table[0].find_all("tr")
43
- logger.debug(f"{len(table_rows):,} total table rows (including header) found")
44
- # Handle the header
45
- raw_header = table_rows.pop(0)
46
- header_row = _extract_fields_from_row(raw_header, "th")
47
- output_rows = [header_row]
48
- # Process remaining rows
49
- discarded_rows = []
50
- for table_row in table_rows:
51
- # Discard bogus data lines (see last lines of source data)
52
- # based on check of first field ("Closing or Layoff")
53
- data = _extract_fields_from_row(table_row, "td")
54
- layoff_type = data[0]
55
- if re.match(r"(clos|lay)", layoff_type, re.I):
56
- output_rows.append(data)
57
- else:
58
- discarded_rows.append(data)
59
- if discarded_rows:
60
- logger.warn(f"Warning: Discarded {len(discarded_rows)} dirty data row(s)")
61
- utils.write_rows_to_csv(output_csv, output_rows)
62
- return output_csv
39
+ # page = utils.get_url("https://workforce.alabama.gov/warn-list/")
40
+ # Later in June 2026, they're detecting the automation and blocking us.
41
+ # But say they won't for the CSV download which, OK.
42
+ # No headers on the CSV, which they'll probably realize ... sometime.
43
+
44
+ targeturl = "https://workforce.alabama.gov/documents/warn-list/"
45
+ page = utils.get_url(targeturl).text
46
+ cache.write("al/rawcsv.csv", page)
63
47
 
48
+ headers = [
49
+ "_id1",
50
+ "action_type",
51
+ "date_notice",
52
+ "date_action",
53
+ "company",
54
+ "location",
55
+ "affected",
56
+ "_id2",
57
+ ]
64
58
 
65
- def _extract_fields_from_row(row, element):
66
- """Pluck data from the provided row and element."""
67
- row_data = []
68
- fields = row.find_all(element)
69
- for raw_field in fields:
70
- field = raw_field.text.strip()
71
- row_data.append(field)
72
- return row_data
59
+ fileholder = StringIO(page)
60
+
61
+ reader = list(csv.DictReader(fileholder, fieldnames=headers))
62
+
63
+ utils.write_disparate_dict_rows_to_csv(
64
+ output_csv, reader, mode="w", prefixes=["_id"]
65
+ )
66
+
67
+ return output_csv
73
68
 
74
69
 
75
70
  if __name__ == "__main__":
warn/utils.py CHANGED
@@ -59,6 +59,8 @@ def fetch_if_not_cached(filename, url, **kwargs):
59
59
  url: The URL from which the file may be downloaded.
60
60
  Notes: Should this even be in utils vs. cache? Should it exist?
61
61
  """
62
+ if "timeout" not in kwargs:
63
+ kwargs["timeout"] = 120
62
64
  create_directory(Path(filename), is_file=True)
63
65
  if not os.path.exists(filename):
64
66
  logger.debug(f"Fetching {filename} from {url}")
@@ -81,6 +83,8 @@ def save_if_good_url(filename, url, **kwargs):
81
83
  Notes: Should this even be in utils vs. cache? Should it exist?
82
84
  """
83
85
  create_directory(Path(filename), is_file=True)
86
+ if "timeout" not in kwargs:
87
+ kwargs["timeout"] = 120
84
88
  response = requests.get(url, **kwargs)
85
89
  if not response.ok:
86
90
  logger.error(f"URL {url} fetch failed with {response.status_code}")
@@ -96,7 +100,7 @@ def save_if_good_url(filename, url, **kwargs):
96
100
  return success_flag, content
97
101
 
98
102
 
99
- def get_with_zyte(url, json_extras=None):
103
+ def get_with_zyte(url, json_extras=None, **kwargs):
100
104
  """Use Zyte as a proxy server to retrieve data not available without it.
101
105
 
102
106
  Args:
@@ -119,6 +123,9 @@ def get_with_zyte(url, json_extras=None):
119
123
  )
120
124
  return (None, None)
121
125
 
126
+ if "timeout" not in kwargs:
127
+ kwargs["timeout"] = 120
128
+
122
129
  myjson = {
123
130
  "url": url,
124
131
  "httpResponseBody": True,
@@ -129,7 +136,10 @@ def get_with_zyte(url, json_extras=None):
129
136
  myjson[item] = json_extras[item]
130
137
 
131
138
  api_response = requests.post(
132
- "https://api.zyte.com/v1/extract", auth=(zyte_api_key, ""), json=myjson
139
+ "https://api.zyte.com/v1/extract",
140
+ auth=(zyte_api_key, ""),
141
+ json=myjson,
142
+ **kwargs,
133
143
  )
134
144
 
135
145
  if not api_response.ok:
@@ -150,7 +160,7 @@ def get_with_zyte(url, json_extras=None):
150
160
  return (returnbin, returntext)
151
161
 
152
162
 
153
- def post_with_zyte(url, payload):
163
+ def post_with_zyte(url, payload, **kwargs):
154
164
  """Use Zyte as a proxy server to retrieve data not available without it.
155
165
 
156
166
  Args:
@@ -175,6 +185,9 @@ def post_with_zyte(url, payload):
175
185
  )
176
186
  return (None, None)
177
187
 
188
+ if "timeout" not in kwargs:
189
+ kwargs["timeout"] = 120
190
+
178
191
  if isinstance(payload, dict):
179
192
  payload = json.dumps(payload)
180
193
 
@@ -191,6 +204,7 @@ def post_with_zyte(url, payload):
191
204
  "httpResponseBody": True,
192
205
  "followRedirect": True,
193
206
  },
207
+ **kwargs,
194
208
  )
195
209
 
196
210
  if not api_response.ok:
@@ -326,6 +340,9 @@ def get_url(
326
340
  kwargs["headers"] = {}
327
341
  kwargs["headers"]["User-Agent"] = user_agent
328
342
 
343
+ if "timeout" not in kwargs:
344
+ kwargs["timeout"] = 120
345
+
329
346
  # Go get it
330
347
  if session is not None:
331
348
  logger.debug(f"Requesting with session {session}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: warn-scraper
3
- Version: 1.2.155.dev0
3
+ Version: 1.2.157.dev0
4
4
  Summary: Command-line interface for downloading WARN Act notices of qualified plant closings and mass layoffs from state government websites
5
5
  Author-email: Big Local News <biglocalnews@stanford.edu>
6
6
  License-Expression: Apache-2.0
@@ -2,7 +2,7 @@ warn/__init__.py,sha256=A07JFY1TyaPtVIndBa7IvTk13DETqIkLgRdk0A-MCoE,85
2
2
  warn/cache.py,sha256=QBSHycchvRTkOQfHptOtZeTYiPgLP383jS8MTiGln_c,5969
3
3
  warn/cli.py,sha256=ZqyJwICdHFkn2hEgbArj_upbElR9-TSDlYDqyEGeexE,2019
4
4
  warn/runner.py,sha256=oeGRybGwpnkQKlPzRMlKxhsDt1GN4PZoX-vUwrsPgos,1894
5
- warn/utils.py,sha256=h1V5IISj9c2REtNnGctp0JqVjCi_Mi0r_FlvzNlrKb0,13093
5
+ warn/utils.py,sha256=MTqbzF4h_TlrtBWqH4pEVA-X8yfw6b6ObafSGkqxKb4,13489
6
6
  warn/pdfrodent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  warn/pdfrodent/pdfrodent.py,sha256=S2LXkKOdP0Jqzcg10hzGijTu83TMS72DzZqQceslJ70,14914
8
8
  warn/platforms/__init__.py,sha256=wIZRDf4tbTuC8oKM4ZrTAtwNgbtMQGzPXMwDYCFyrog,81
@@ -13,7 +13,7 @@ warn/platforms/job_center/urls.py,sha256=IWhpuzN_xcNdHh23GbZPGvuHCsMcmb03qx3pRn1
13
13
  warn/platforms/job_center/utils.py,sha256=HdUKgKirmpPP7e4Cu_ZyB3zPVS_p-_ylo-lXFhxK2QM,5696
14
14
  warn/scrapers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
15
  warn/scrapers/ak.py,sha256=h7BYMTV0whwWAPhbzVDVKMMoVCFphKly70aiTHabPq4,1847
16
- warn/scrapers/al.py,sha256=XSDEGC7F6_3GZ2m_uSiIG-1v8jMNH9pw_wNUCZyaMK0,2460
16
+ warn/scrapers/al.py,sha256=3_OtgUp4K6OL6H63SdtLQWE998r8PQpTNOv6PuiJEEc,1888
17
17
  warn/scrapers/az.py,sha256=elGbue01Gjf_DQ66Wy9qqGIOJsiY-KIKJOVeft8pCXg,1447
18
18
  warn/scrapers/ca.py,sha256=VQOfjHXPCc-jYwh-EPGVVfnzvXB7pdmCt2uJ6QnMPRM,8600
19
19
  warn/scrapers/co.py,sha256=83OdikIrWGxt22mlI-_zLSNqJg1NO5C2Xjm3FF6DPYY,18252
@@ -54,9 +54,9 @@ warn/scrapers/va.py,sha256=7Nle7qL0VNPiE653XyaP9HQqSfuJFDRr2kEkjOqLvFM,11269
54
54
  warn/scrapers/vt.py,sha256=d-bo4WK2hkrk4BhCCmLpEovcoZltlvdIUB6O0uaMx5A,1186
55
55
  warn/scrapers/wa.py,sha256=UXdVtHZo_a-XfoiyOooTRfTb9W3PErSZdKca6SRORgs,4282
56
56
  warn/scrapers/wi.py,sha256=ClEzXkwZbop0W4fkQgsb5oHAPUrb4luUPGV-jOKwkcg,4855
57
- warn_scraper-1.2.155.dev0.dist-info/licenses/LICENSE,sha256=ZV-QHyqPwyMuwuj0lI05JeSjV1NyzVEk8Yeu7FPtYS0,585
58
- warn_scraper-1.2.155.dev0.dist-info/METADATA,sha256=Vz4NRjIMPAx1mNm6337Va3PAEhD4Zd4VTNeiGBumCG0,1780
59
- warn_scraper-1.2.155.dev0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
60
- warn_scraper-1.2.155.dev0.dist-info/entry_points.txt,sha256=poh_oSweObGlBSs1_2qZmnTodlOYD0KfO7-h7W2UQIw,47
61
- warn_scraper-1.2.155.dev0.dist-info/top_level.txt,sha256=dZfms6N3kqVXufiPOo7YqOrAcUtYfNH_oyGvYUk9FB4,5
62
- warn_scraper-1.2.155.dev0.dist-info/RECORD,,
57
+ warn_scraper-1.2.157.dev0.dist-info/licenses/LICENSE,sha256=ZV-QHyqPwyMuwuj0lI05JeSjV1NyzVEk8Yeu7FPtYS0,585
58
+ warn_scraper-1.2.157.dev0.dist-info/METADATA,sha256=b5BB7-TNVZCGVMlKhEBamlTc9lwjcnNLYlsBG5UrgKw,1780
59
+ warn_scraper-1.2.157.dev0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
60
+ warn_scraper-1.2.157.dev0.dist-info/entry_points.txt,sha256=poh_oSweObGlBSs1_2qZmnTodlOYD0KfO7-h7W2UQIw,47
61
+ warn_scraper-1.2.157.dev0.dist-info/top_level.txt,sha256=dZfms6N3kqVXufiPOo7YqOrAcUtYfNH_oyGvYUk9FB4,5
62
+ warn_scraper-1.2.157.dev0.dist-info/RECORD,,