warn-scraper 1.2.169.dev0__py3-none-any.whl → 1.2.170.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/ma.py +271 -0
- warn/scrapers/nd.py +4 -1
- {warn_scraper-1.2.169.dev0.dist-info → warn_scraper-1.2.170.dev0.dist-info}/METADATA +1 -1
- {warn_scraper-1.2.169.dev0.dist-info → warn_scraper-1.2.170.dev0.dist-info}/RECORD +8 -9
- warn_scraper-1.2.169.dev0.dist-info/scm_file_list.json +0 -138
- warn_scraper-1.2.169.dev0.dist-info/scm_version.json +0 -8
- {warn_scraper-1.2.169.dev0.dist-info → warn_scraper-1.2.170.dev0.dist-info}/WHEEL +0 -0
- {warn_scraper-1.2.169.dev0.dist-info → warn_scraper-1.2.170.dev0.dist-info}/entry_points.txt +0 -0
- {warn_scraper-1.2.169.dev0.dist-info → warn_scraper-1.2.170.dev0.dist-info}/licenses/LICENSE +0 -0
- {warn_scraper-1.2.169.dev0.dist-info → warn_scraper-1.2.170.dev0.dist-info}/top_level.txt +0 -0
warn/scrapers/ma.py
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from urllib.parse import unquote, urljoin
|
|
7
|
+
|
|
8
|
+
from bs4 import BeautifulSoup
|
|
9
|
+
from openpyxl import load_workbook
|
|
10
|
+
|
|
11
|
+
from .. import utils
|
|
12
|
+
from ..cache import Cache
|
|
13
|
+
|
|
14
|
+
__authors__ = ["riordan"]
|
|
15
|
+
__tags__ = ["excel", "csv", "historical"]
|
|
16
|
+
__source__ = {
|
|
17
|
+
"name": "Massachusetts Executive Office of Labor and Workforce Development",
|
|
18
|
+
"url": "https://www.mass.gov/info-details/worker-adjustment-and-retraining-notification-act-warn-layoff-and-closure-updates",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
BASE_URL = "https://www.mass.gov"
|
|
24
|
+
|
|
25
|
+
# The unified header for the combined export. It mirrors the columns the state
|
|
26
|
+
# currently publishes in its flat fiscal-year workbooks and its weekly CSV feed.
|
|
27
|
+
CANONICAL_HEADER = [
|
|
28
|
+
"RECEIVED",
|
|
29
|
+
"EMPLOYER",
|
|
30
|
+
"CITY/TOWN",
|
|
31
|
+
"REGION",
|
|
32
|
+
"DATE(S) OF LAYOFFS",
|
|
33
|
+
"# EMPLOYEES IMPACTED",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
# The older fiscal-year workbooks (FY2022, FY2023) split notices into one
|
|
37
|
+
# worksheet per region and lay the columns out in this fixed order. The region
|
|
38
|
+
# for those rows comes from the worksheet name rather than a column.
|
|
39
|
+
REGION_SHEET_ORDER = [
|
|
40
|
+
"RECEIVED",
|
|
41
|
+
"EMPLOYER",
|
|
42
|
+
"CITY/TOWN",
|
|
43
|
+
"DATE(S) OF LAYOFFS",
|
|
44
|
+
"# EMPLOYEES IMPACTED",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def scrape(
|
|
49
|
+
data_dir: Path = utils.WARN_DATA_DIR,
|
|
50
|
+
cache_dir: Path = utils.WARN_CACHE_DIR,
|
|
51
|
+
) -> Path:
|
|
52
|
+
"""
|
|
53
|
+
Scrape data from Massachusetts.
|
|
54
|
+
|
|
55
|
+
Keyword arguments:
|
|
56
|
+
data_dir -- the Path were the result will be saved (default WARN_DATA_DIR)
|
|
57
|
+
cache_dir -- the Path where results can be cached (default WARN_CACHE_DIR)
|
|
58
|
+
|
|
59
|
+
Returns: the Path where the file is written
|
|
60
|
+
"""
|
|
61
|
+
# Fire up the cache
|
|
62
|
+
cache = Cache(cache_dir)
|
|
63
|
+
state_code = "ma"
|
|
64
|
+
|
|
65
|
+
# Download the index page. Mass.gov sits behind Akamai bot protection that
|
|
66
|
+
# rejects ordinary requests, so we route everything through Zyte, as the
|
|
67
|
+
# Louisiana and Texas scrapers do.
|
|
68
|
+
_, html = utils.get_with_zyte(__source__["url"])
|
|
69
|
+
cache.write(f"{state_code}/index.html", html)
|
|
70
|
+
|
|
71
|
+
# Find the links to the fiscal-year workbooks and the weekly CSV feed.
|
|
72
|
+
soup = BeautifulSoup(html, "html.parser")
|
|
73
|
+
excel_urls, csv_urls = _find_source_links(soup)
|
|
74
|
+
logger.debug(
|
|
75
|
+
f"Found {len(excel_urls)} workbook link(s) and {len(csv_urls)} CSV link(s)"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
master_list: list = []
|
|
79
|
+
|
|
80
|
+
# The fiscal-year workbooks hold the historical record.
|
|
81
|
+
for url in excel_urls:
|
|
82
|
+
name = url.split("/doc/")[1].split("/")[0]
|
|
83
|
+
raw, _ = utils.get_with_zyte(url)
|
|
84
|
+
excel_path = cache.write_binary(f"{state_code}/{name}.xlsx", raw)
|
|
85
|
+
master_list.extend(_parse_workbook(excel_path))
|
|
86
|
+
|
|
87
|
+
# The weekly CSV feed carries the current fiscal year's running notices,
|
|
88
|
+
# which are not yet folded into a workbook.
|
|
89
|
+
for url in csv_urls:
|
|
90
|
+
name = os.path.basename(unquote(url))
|
|
91
|
+
raw, text = utils.get_with_zyte(url)
|
|
92
|
+
cache.write_binary(f"{state_code}/{name}", raw)
|
|
93
|
+
rows = list(csv.reader(text.splitlines()))
|
|
94
|
+
master_list.extend(_parse_flat_rows(rows))
|
|
95
|
+
|
|
96
|
+
# Drop exact duplicate rows while preserving order. Duplicates can appear at
|
|
97
|
+
# fiscal-year boundaries or across re-runs; distinct notices are untouched.
|
|
98
|
+
deduped = _dedupe(master_list)
|
|
99
|
+
logger.debug(
|
|
100
|
+
f"Parsed {len(master_list):,} rows, {len(deduped):,} after removing duplicates"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Write it out
|
|
104
|
+
data_path = data_dir / f"{state_code}.csv"
|
|
105
|
+
utils.write_dict_rows_to_csv(
|
|
106
|
+
data_path, CANONICAL_HEADER, deduped, extrasaction="raise"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
return data_path
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _find_source_links(soup: BeautifulSoup) -> tuple:
|
|
113
|
+
"""Pull the workbook and CSV download links off the index page.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
soup (BeautifulSoup): the parsed index page
|
|
117
|
+
|
|
118
|
+
Returns: a tuple of (sorted workbook urls, sorted csv urls)
|
|
119
|
+
"""
|
|
120
|
+
excel_urls = set()
|
|
121
|
+
csv_urls = set()
|
|
122
|
+
for link in soup.find_all("a", href=True):
|
|
123
|
+
href = link["href"]
|
|
124
|
+
lower = href.lower()
|
|
125
|
+
if "http" not in lower:
|
|
126
|
+
href = urljoin(BASE_URL, href)
|
|
127
|
+
# Fiscal-year workbooks live at /doc/fyNN-warn-report.../download
|
|
128
|
+
if "/doc/" in lower and "warn-report" in lower:
|
|
129
|
+
excel_urls.add(href)
|
|
130
|
+
# The weekly feed is a WARN Report CSV under /files/
|
|
131
|
+
elif lower.endswith(".csv") and "warn" in lower:
|
|
132
|
+
csv_urls.add(href)
|
|
133
|
+
return sorted(excel_urls), sorted(csv_urls)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _parse_workbook(excel_path: Path) -> list:
|
|
137
|
+
"""Parse a fiscal-year workbook into a list of canonical row dicts.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
excel_path (Path): the path to a cached .xlsx file
|
|
141
|
+
|
|
142
|
+
Returns: a list of dicts keyed by CANONICAL_HEADER
|
|
143
|
+
"""
|
|
144
|
+
workbook = load_workbook(filename=excel_path, read_only=True, data_only=True)
|
|
145
|
+
rows: list = []
|
|
146
|
+
for sheet in workbook.worksheets:
|
|
147
|
+
sheet_rows = [list(r) for r in sheet.iter_rows(values_only=True)]
|
|
148
|
+
if _has_region_column(sheet_rows):
|
|
149
|
+
rows.extend(_parse_flat_rows(sheet_rows))
|
|
150
|
+
else:
|
|
151
|
+
rows.extend(_parse_region_sheet(sheet_rows, sheet.title.strip()))
|
|
152
|
+
workbook.close()
|
|
153
|
+
return rows
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _has_region_column(rows: list) -> bool:
|
|
157
|
+
"""Return True if the sheet is a flat table with its own REGION column."""
|
|
158
|
+
header_index = _find_header_row(rows)
|
|
159
|
+
if header_index is None:
|
|
160
|
+
return False
|
|
161
|
+
return any(_norm(cell) == "REGION" for cell in rows[header_index])
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _find_header_row(rows: list):
|
|
165
|
+
"""Find the index of the header row, or None if there isn't one."""
|
|
166
|
+
for i, row in enumerate(rows):
|
|
167
|
+
labels = {_norm(cell) for cell in row}
|
|
168
|
+
if "EMPLOYER" in labels or "COMPANY NAME" in labels:
|
|
169
|
+
return i
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _parse_flat_rows(rows: list) -> list:
|
|
174
|
+
"""Parse a flat table (FY2024+ workbooks and the weekly CSV) into row dicts.
|
|
175
|
+
|
|
176
|
+
The header row names its own columns, including REGION, so we map each
|
|
177
|
+
canonical field to the column where its header appears.
|
|
178
|
+
"""
|
|
179
|
+
header_index = _find_header_row(rows)
|
|
180
|
+
if header_index is None:
|
|
181
|
+
return []
|
|
182
|
+
|
|
183
|
+
# Map each canonical column name to its position in the header row.
|
|
184
|
+
positions = {}
|
|
185
|
+
for col, cell in enumerate(rows[header_index]):
|
|
186
|
+
label = _norm(cell)
|
|
187
|
+
if label in CANONICAL_HEADER and label not in positions:
|
|
188
|
+
positions[label] = col
|
|
189
|
+
|
|
190
|
+
parsed = []
|
|
191
|
+
for row in rows[header_index + 1 :]:
|
|
192
|
+
record = {
|
|
193
|
+
field: _clean(row[pos]) if pos < len(row) else ""
|
|
194
|
+
for field, pos in positions.items()
|
|
195
|
+
}
|
|
196
|
+
record = {field: record.get(field, "") for field in CANONICAL_HEADER}
|
|
197
|
+
if _keep_row(record):
|
|
198
|
+
parsed.append(record)
|
|
199
|
+
return parsed
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _parse_region_sheet(rows: list, region: str) -> list:
|
|
203
|
+
"""Parse an older per-region worksheet (FY2022, FY2023) into row dicts.
|
|
204
|
+
|
|
205
|
+
These sheets have no REGION column and lay their columns out positionally,
|
|
206
|
+
so the region comes from the worksheet name.
|
|
207
|
+
"""
|
|
208
|
+
header_index = _find_header_row(rows)
|
|
209
|
+
if header_index is None:
|
|
210
|
+
return []
|
|
211
|
+
|
|
212
|
+
parsed = []
|
|
213
|
+
for row in rows[header_index + 1 :]:
|
|
214
|
+
record = {field: "" for field in CANONICAL_HEADER}
|
|
215
|
+
for pos, field in enumerate(REGION_SHEET_ORDER):
|
|
216
|
+
if pos < len(row):
|
|
217
|
+
record[field] = _clean(row[pos])
|
|
218
|
+
record["REGION"] = region
|
|
219
|
+
if _keep_row(record):
|
|
220
|
+
parsed.append(record)
|
|
221
|
+
return parsed
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _keep_row(record: dict) -> bool:
|
|
225
|
+
"""Decide whether a parsed row is a real notice worth keeping."""
|
|
226
|
+
employer = record.get("EMPLOYER", "")
|
|
227
|
+
if not employer:
|
|
228
|
+
return False
|
|
229
|
+
# Skip stray header echoes and any total/count footer rows.
|
|
230
|
+
if _norm(employer) in ("EMPLOYER", "COMPANY NAME"):
|
|
231
|
+
return False
|
|
232
|
+
if _norm(employer).startswith("TOTAL"):
|
|
233
|
+
return False
|
|
234
|
+
return True
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _dedupe(rows: list) -> list:
|
|
238
|
+
"""Drop exact duplicate rows while preserving their first-seen order."""
|
|
239
|
+
seen = set()
|
|
240
|
+
deduped = []
|
|
241
|
+
for row in rows:
|
|
242
|
+
key = tuple(row[field] for field in CANONICAL_HEADER)
|
|
243
|
+
if key not in seen:
|
|
244
|
+
seen.add(key)
|
|
245
|
+
deduped.append(row)
|
|
246
|
+
return deduped
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _clean(value) -> str:
|
|
250
|
+
"""Normalize a cell value to a clean string for output.
|
|
251
|
+
|
|
252
|
+
Dates arrive from openpyxl as datetime objects; render them the way the
|
|
253
|
+
state writes them elsewhere (M/D/YYYY). Everything else is stringified and
|
|
254
|
+
stripped.
|
|
255
|
+
"""
|
|
256
|
+
if value is None:
|
|
257
|
+
return ""
|
|
258
|
+
if isinstance(value, datetime):
|
|
259
|
+
return f"{value.month}/{value.day}/{value.year}"
|
|
260
|
+
return str(value).strip()
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _norm(value) -> str:
|
|
264
|
+
"""Uppercase, whitespace-collapsed form of a cell, for label matching."""
|
|
265
|
+
if value is None:
|
|
266
|
+
return ""
|
|
267
|
+
return " ".join(str(value).split()).upper()
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
if __name__ == "__main__":
|
|
271
|
+
scrape()
|
warn/scrapers/nd.py
CHANGED
|
@@ -48,7 +48,7 @@ def scrape(
|
|
|
48
48
|
for page_index, page in enumerate(pdf.pages):
|
|
49
49
|
rows = page.extract_table() or []
|
|
50
50
|
|
|
51
|
-
for row in rows:
|
|
51
|
+
for rowindex, row in enumerate(rows):
|
|
52
52
|
# Standardize each cell
|
|
53
53
|
output_row = [_clean_text(cell) for cell in row]
|
|
54
54
|
|
|
@@ -65,6 +65,9 @@ def scrape(
|
|
|
65
65
|
continue
|
|
66
66
|
header_written = True
|
|
67
67
|
|
|
68
|
+
if rowindex > 0: # If not a header row
|
|
69
|
+
if output_row[1].endswith(", ND"):
|
|
70
|
+
output_row[1] = output_row[1][:-4]
|
|
68
71
|
output_rows.append(output_row)
|
|
69
72
|
|
|
70
73
|
# Write out to CSV
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: warn-scraper
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.170.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
|
|
@@ -30,13 +30,14 @@ warn/scrapers/in.py,sha256=0hOCdbUyls3xX7Q1z5OB6yCrXJFpsu2CmvfwRh9NTfY,2202
|
|
|
30
30
|
warn/scrapers/ks.py,sha256=F_3biEMF7zgCX2XVuUACR74Vyzapta4SaM9SY3EuZCU,1266
|
|
31
31
|
warn/scrapers/ky.py,sha256=7kJTNOzxChyXlcyBImmdwwmrczYksU8XNxbhQ2owmJs,9688
|
|
32
32
|
warn/scrapers/la.py,sha256=52i7ICYH0R7omFyu-kmKi_s-8Rhy0vaPjNIKlW_OMC8,6938
|
|
33
|
+
warn/scrapers/ma.py,sha256=oJa3GMBm2YsG2nFqGbb2ZKKhurfN59ETeTcLABcVF00,8809
|
|
33
34
|
warn/scrapers/md.py,sha256=Zjudr5X8-E3fjBXbOqnB830XEZqLznGzQbaJ5HSY0zQ,4425
|
|
34
35
|
warn/scrapers/me.py,sha256=q36F4yJ7hvZsLayA3uBS1romo4X3Qf-sEi2Y7LAQCi8,1172
|
|
35
36
|
warn/scrapers/mi.py,sha256=Ppyawp4nbzSBODuzDKeqnO9_9do5MFwK4Y_f3uc6blE,5846
|
|
36
37
|
warn/scrapers/mo.py,sha256=wnnwQAiVPwuheMqptMXZpyQdiKNghhKwTO-Bnh9oXoU,3492
|
|
37
38
|
warn/scrapers/ms.py,sha256=BZZoMw3TNtwzBeBsqKLsPznBcDjPaO49I5-yBU0e9AI,5502
|
|
38
39
|
warn/scrapers/mt.py,sha256=t2MP4OCcuCEnrnvNgOu289P0eekZq4XaCK65qzgZX88,2457
|
|
39
|
-
warn/scrapers/nd.py,sha256=
|
|
40
|
+
warn/scrapers/nd.py,sha256=3hXgOpu59QElYf44mFWYlBbTWDOlgmwjo8XXaN1GS6k,2848
|
|
40
41
|
warn/scrapers/ne.py,sha256=JawuGJ3tCKvMd-N-p03gnltB4rol4QUJshMk2oyMPO4,4143
|
|
41
42
|
warn/scrapers/nj.py,sha256=nwbMbeQuUJbYRVoyUyKZBmNqvqsXu3Habt-10r8DvZE,2230
|
|
42
43
|
warn/scrapers/nm.py,sha256=55kDh65uOc-uMO8wuxPHhHN4Aw8Di2Fx66JBds8f2Io,3694
|
|
@@ -55,11 +56,9 @@ warn/scrapers/va.py,sha256=7Nle7qL0VNPiE653XyaP9HQqSfuJFDRr2kEkjOqLvFM,11269
|
|
|
55
56
|
warn/scrapers/vt.py,sha256=d-bo4WK2hkrk4BhCCmLpEovcoZltlvdIUB6O0uaMx5A,1186
|
|
56
57
|
warn/scrapers/wa.py,sha256=UXdVtHZo_a-XfoiyOooTRfTb9W3PErSZdKca6SRORgs,4282
|
|
57
58
|
warn/scrapers/wi.py,sha256=ClEzXkwZbop0W4fkQgsb5oHAPUrb4luUPGV-jOKwkcg,4855
|
|
58
|
-
warn_scraper-1.2.
|
|
59
|
-
warn_scraper-1.2.
|
|
60
|
-
warn_scraper-1.2.
|
|
61
|
-
warn_scraper-1.2.
|
|
62
|
-
warn_scraper-1.2.
|
|
63
|
-
warn_scraper-1.2.
|
|
64
|
-
warn_scraper-1.2.169.dev0.dist-info/top_level.txt,sha256=dZfms6N3kqVXufiPOo7YqOrAcUtYfNH_oyGvYUk9FB4,5
|
|
65
|
-
warn_scraper-1.2.169.dev0.dist-info/RECORD,,
|
|
59
|
+
warn_scraper-1.2.170.dev0.dist-info/licenses/LICENSE,sha256=ZV-QHyqPwyMuwuj0lI05JeSjV1NyzVEk8Yeu7FPtYS0,585
|
|
60
|
+
warn_scraper-1.2.170.dev0.dist-info/METADATA,sha256=J76_LvKxe-qvqSaB0Hqkhi-7bEfvP6q5_kieFszDXxY,1780
|
|
61
|
+
warn_scraper-1.2.170.dev0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
62
|
+
warn_scraper-1.2.170.dev0.dist-info/entry_points.txt,sha256=poh_oSweObGlBSs1_2qZmnTodlOYD0KfO7-h7W2UQIw,47
|
|
63
|
+
warn_scraper-1.2.170.dev0.dist-info/top_level.txt,sha256=dZfms6N3kqVXufiPOo7YqOrAcUtYfNH_oyGvYUk9FB4,5
|
|
64
|
+
warn_scraper-1.2.170.dev0.dist-info/RECORD,,
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"files": [
|
|
3
|
-
".pre-commit-config.yaml",
|
|
4
|
-
"README.md",
|
|
5
|
-
"Makefile",
|
|
6
|
-
"setup.cfg",
|
|
7
|
-
"LICENSE",
|
|
8
|
-
"setup.py",
|
|
9
|
-
"pyproject.toml",
|
|
10
|
-
"Pipfile.lock",
|
|
11
|
-
"MANIFEST.in",
|
|
12
|
-
"Pipfile",
|
|
13
|
-
".gitignore",
|
|
14
|
-
".devcontainer/devcontainer.json",
|
|
15
|
-
"docs/reference.rst",
|
|
16
|
-
"docs/releasing.md",
|
|
17
|
-
"docs/Makefile",
|
|
18
|
-
"docs/sources.md",
|
|
19
|
-
"docs/contributing.rst",
|
|
20
|
-
"docs/index.rst",
|
|
21
|
-
"docs/requirements.txt",
|
|
22
|
-
"docs/make.bat",
|
|
23
|
-
"docs/usage.md",
|
|
24
|
-
"docs/conf.py",
|
|
25
|
-
"docs/scrapers/az.md",
|
|
26
|
-
"docs/scrapers/de.md",
|
|
27
|
-
"docs/scrapers/or.md",
|
|
28
|
-
"docs/scrapers/tx.md",
|
|
29
|
-
"docs/scrapers/sc.md",
|
|
30
|
-
"docs/scrapers/vt.md",
|
|
31
|
-
"docs/scrapers/job_center.md",
|
|
32
|
-
"docs/scrapers/dc.md",
|
|
33
|
-
"docs/scrapers/ut.md",
|
|
34
|
-
"docs/scrapers/mo.md",
|
|
35
|
-
"docs/scrapers/al.md",
|
|
36
|
-
"docs/scrapers/me.md",
|
|
37
|
-
"docs/scrapers/ks.md",
|
|
38
|
-
"docs/scrapers/in.md",
|
|
39
|
-
"docs/scrapers/ok.md",
|
|
40
|
-
"docs/scrapers/co.md",
|
|
41
|
-
"docs/scrapers/ia.md",
|
|
42
|
-
"docs/scrapers/va.md",
|
|
43
|
-
"docs/scrapers/md.md",
|
|
44
|
-
"docs/scrapers/ny.md",
|
|
45
|
-
"docs/scrapers/wi.md",
|
|
46
|
-
"docs/scrapers/ca.md",
|
|
47
|
-
"docs/_static/releasing-changelog-entered.png",
|
|
48
|
-
"docs/_static/releasing-publish-button.png",
|
|
49
|
-
"docs/_static/releasing-actions-start.png",
|
|
50
|
-
"docs/_static/gao-03-1003.pdf",
|
|
51
|
-
"docs/_static/releasing-tag-button.png",
|
|
52
|
-
"docs/_static/releasing-name-tag.png",
|
|
53
|
-
"docs/_static/releasing-draft-button.png",
|
|
54
|
-
"docs/_static/releasing-releases-button.png",
|
|
55
|
-
"docs/_static/R42693.pdf",
|
|
56
|
-
"docs/_static/releasing-actions-finished.png",
|
|
57
|
-
"docs/_static/releasing-pypi.png",
|
|
58
|
-
"docs/_static/releasing-name-release.png",
|
|
59
|
-
"docs/_static/releasing-release-published.png",
|
|
60
|
-
"docs/_static/releasing-changelog-button.png",
|
|
61
|
-
"docs/_templates/sources.md.tmpl",
|
|
62
|
-
"warn/__init__.py",
|
|
63
|
-
"warn/utils.py",
|
|
64
|
-
"warn/runner.py",
|
|
65
|
-
"warn/cache.py",
|
|
66
|
-
"warn/cli.py",
|
|
67
|
-
"warn/scrapers/az.py",
|
|
68
|
-
"warn/scrapers/la.py",
|
|
69
|
-
"warn/scrapers/tx.py",
|
|
70
|
-
"warn/scrapers/wa.py",
|
|
71
|
-
"warn/scrapers/wi.py",
|
|
72
|
-
"warn/scrapers/ga.py",
|
|
73
|
-
"warn/scrapers/co.py",
|
|
74
|
-
"warn/scrapers/de.py",
|
|
75
|
-
"warn/scrapers/__init__.py",
|
|
76
|
-
"warn/scrapers/ia.py",
|
|
77
|
-
"warn/scrapers/al.py",
|
|
78
|
-
"warn/scrapers/fl.py",
|
|
79
|
-
"warn/scrapers/nj.py",
|
|
80
|
-
"warn/scrapers/nd.py",
|
|
81
|
-
"warn/scrapers/mi.py",
|
|
82
|
-
"warn/scrapers/vt.py",
|
|
83
|
-
"warn/scrapers/mt.py",
|
|
84
|
-
"warn/scrapers/md.py",
|
|
85
|
-
"warn/scrapers/sc.py",
|
|
86
|
-
"warn/scrapers/ut.py",
|
|
87
|
-
"warn/scrapers/il.py",
|
|
88
|
-
"warn/scrapers/ky.py",
|
|
89
|
-
"warn/scrapers/or.py",
|
|
90
|
-
"warn/scrapers/in.py",
|
|
91
|
-
"warn/scrapers/ca.py",
|
|
92
|
-
"warn/scrapers/hi.py",
|
|
93
|
-
"warn/scrapers/ak.py",
|
|
94
|
-
"warn/scrapers/ri.py",
|
|
95
|
-
"warn/scrapers/ny.py",
|
|
96
|
-
"warn/scrapers/pa.py",
|
|
97
|
-
"warn/scrapers/dc.py",
|
|
98
|
-
"warn/scrapers/ct.py",
|
|
99
|
-
"warn/scrapers/mo.py",
|
|
100
|
-
"warn/scrapers/ok.py",
|
|
101
|
-
"warn/scrapers/tn.py",
|
|
102
|
-
"warn/scrapers/ks.py",
|
|
103
|
-
"warn/scrapers/oh.py",
|
|
104
|
-
"warn/scrapers/id.py",
|
|
105
|
-
"warn/scrapers/nm.py",
|
|
106
|
-
"warn/scrapers/me.py",
|
|
107
|
-
"warn/scrapers/va.py",
|
|
108
|
-
"warn/scrapers/ne.py",
|
|
109
|
-
"warn/scrapers/sd.py",
|
|
110
|
-
"warn/scrapers/ms.py",
|
|
111
|
-
"warn/platforms/__init__.py",
|
|
112
|
-
"warn/platforms/job_center/__init__.py",
|
|
113
|
-
"warn/platforms/job_center/utils.py",
|
|
114
|
-
"warn/platforms/job_center/cache.py",
|
|
115
|
-
"warn/platforms/job_center/site.py",
|
|
116
|
-
"warn/platforms/job_center/urls.py",
|
|
117
|
-
"warn/pdfrodent/__init__.py",
|
|
118
|
-
"warn/pdfrodent/pdfrodent.py",
|
|
119
|
-
"tests/__init__.py",
|
|
120
|
-
"tests/test_job_center.py",
|
|
121
|
-
"tests/test_job_center_cache.py",
|
|
122
|
-
"tests/test_openpyxl.py",
|
|
123
|
-
"tests/conftest.py",
|
|
124
|
-
"tests/test_cache.py",
|
|
125
|
-
"tests/test_delete.py",
|
|
126
|
-
"tests/fixtures/2021_page_2.html",
|
|
127
|
-
"tests/fixtures/2021_page_1.html",
|
|
128
|
-
"tests/cassettes/test_scrape_integration.yaml",
|
|
129
|
-
"tests/cassettes/test_cached_search_results.yaml",
|
|
130
|
-
"tests/cassettes/test_no_results.yaml",
|
|
131
|
-
"tests/cassettes/test_paged_results.yaml",
|
|
132
|
-
"tests/cassettes/test_missing_detail_page_values.yaml",
|
|
133
|
-
"tests/cassettes/test_cached_detail_pages.yaml",
|
|
134
|
-
".github/dependabot-disabled.yml",
|
|
135
|
-
".github/workflows/continuous-deployment.yml",
|
|
136
|
-
".github/workflows/continuous-deployment.yml.broken-tests"
|
|
137
|
-
]
|
|
138
|
-
}
|
|
File without changes
|
{warn_scraper-1.2.169.dev0.dist-info → warn_scraper-1.2.170.dev0.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{warn_scraper-1.2.169.dev0.dist-info → warn_scraper-1.2.170.dev0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|