Add official DOL and EIA economic calendar feeds

Collect weekly claims and precise commercial petroleum inventory changes with official schedule exceptions, distinct reporting periods, raw evidence, and revisions.

Extend explicit history backfills without changing their default source scope. Report archive gaps and reject mismatched weeks instead of filling current values into history.
This commit is contained in:
eezd committed 2026-10-30 23:11:40 +08:00
1 parent 62d8c7eb65
commit 8d6cbeeb40
16 files changed
+956 -10

No files matched your search

+46
View File
@@ -0,0 +1,46 @@
1
News Release
Connect with DOL at
https://blog.dol.gov
TRANSMISSION OF MATERIALS IN THIS RELEASE IS EMBARGOED UNTIL
8:30 A.M. (Eastern) Wednesday, January 8, 2025
UNEMPLOYMENT INSURANCE WEEKLY CLAIMS
SEASONALLY ADJUSTED DATA
In the week ending January 4, the advance figure for seasonally adjusted initial claims was 201,000, a decrease of
10,000 from the previous week's unrevised level of 211,000. The 4-week moving average was 213,000, a decrease of
10,250 from the previous week's unrevised average of 223,250.
The advance seasonally adjusted insured unemployment rate was 1.2 percent for the week ending December 28,
unchanged from the previous week's unrevised rate. The advance number for seasonally adjusted insured
unemployment during the week ending December 28 was 1,867,000, an increase of 33,000 from the previous week's
revised level. The previous week's level was revised down by 10,000 from 1,844,000 to 1,834,000. The 4-week moving
average was 1,865,500, a decrease of 3,000 from the previous week's revised average. The previous week's average was
revised down by 2,250 from 1,870,750 to 1,868,500.
2
UNADJUSTED DATA
The advance number of actual initial claims under state programs, unadjusted, totaled 304,741 in the week ending
January 4, an increase of 21,253 (or 7.5 percent) from the previous week. The seasonal factors had expected an increase
of 35,647 (or 12.6 percent) from the previous week. There were 318,906 initial claims in the comparable week in 2024.
The advance unadjusted insured unemployment rate was 1.4 percent during the week ending December 28, an increase of
0.2 percentage point from the prior week. The advance unadjusted level of insured unemployment in state programs
totaled 2,175,478, an increase of 316,122 (or 17.0 percent) from the preceding week. The seasonal factors had expected
an increase of 276,886 (or 14.9 percent) from the previous week. A year earlier the rate was 1.4 percent and the volume
was 2,104,272.
The total number of continued weeks claimed for benefits in all programs for the week ending December 21 was
1,886,283, a decrease of 88,519 from the previous week. There were 1,927,770 weekly claims filed for benefits in all
programs in the comparable week in 2023.
No state was triggered "on" the Extended Benefits program during the week ending December 21.
Initial claims for UI benefits filed by former Federal civilian employees totaled 340 in the week ending December 28, a
decrease of 166 from the prior week. There were 201 initial claims filed by newly discharged veterans, a decrease of 113
from the preceding week.
+49
View File
@@ -0,0 +1,49 @@
1
News Release
Connect with DOL at
https://blog.dol.gov
TRANSMISSION OF MATERIALS IN THIS RELEASE IS EMBARGOED UNTIL
8:30 A.M. (Eastern) Thursday, October 29, 2026
UNEMPLOYMENT INSURANCE WEEKLY CLAIMS
SEASONALLY ADJUSTED DATA
In the week ending October 24, the advance figure for seasonally adjusted initial claims was 194,000, a decrease of
1,000 from the previous week's revised level. The previous week's level was revised up by 1,000 from 194,000 to
195,000. The 4-week moving average was 196,750, a decrease of 1,250 from the previous week's revised average. The
previous week's average was revised up by 250 from 197,750 to 198,000.
The advance seasonally adjusted insured unemployment rate was 1.1 percent for the week ending October 17,
unchanged from the previous week's unrevised rate. The advance number for seasonally adjusted insured
unemployment during the week ending October 17 was 1,701,000, a decrease of 21,000 from the previous week's
revised level. The previous week's level was revised down by 6,000 from 1,728,000 to 1,722,000. The 4-week moving
average was 1,710,000, an increase of 500 from the previous week's revised average. The previous week's average was
revised down by 1,500 from 1,711,000 to 1,709,500.
2
UNADJUSTED DATA
The advance number of actual initial claims under state programs, unadjusted, totaled 178,851 in the week ending
October 24, an increase of 5,648 (or 3.3 percent) from the previous week. The seasonal factors had expected an increase
of 6,040 (or 3.5 percent) from the previous week. There were 203,012 initial claims in the comparable week in 2025.
The advance unadjusted insured unemployment rate was 1.0 percent during the week ending October 17, unchanged
from the prior week. The advance unadjusted level of insured unemployment in state programs totaled 1,490,071, an
increase of 14,007 (or 0.9 percent) from the preceding week. The seasonal factors had expected an increase of 32,725 (or
2.2 percent) from the previous week. A year earlier the rate was 1.1 percent and the volume was 1,708,691.
The total number of continued weeks claimed for benefits in all programs for the week ending October 10 was
1,500,078, an increase of 14,467 from the previous week. There were 1,712,123 weekly claims filed for benefits in all
programs in the comparable week in 2025.
No state was triggered "on" the Extended Benefits program during the week ending October 10.
Initial claims for UI benefits filed by former Federal civilian employees totaled 442 in the week ending October 17, a
decrease of 113 from the prior week. There were 387 initial claims filed by newly discharged veterans, a decrease of 40
from the preceding week.
There were 4,934 continued weeks claimed filed by former Federal civilian employees the week ending October 10, a
decrease of 100 from the previous week. Newly discharged veterans claiming benefits totaled 5,875, an increase of 1,057
from the prior week.
+59
View File
@@ -0,0 +1,59 @@
"STUB_1","10/23/26","10/16/26","Difference","Percent Change","10/24/25","Difference","Percent Change"
"Crude Oil","693.208","695.200","-1.992","-0.300","825.063","-131.855","-16.000"
"Commercial (Excluding SPR)","410.625","412.517","-1.892","-0.500","415.966","-5.341","-1.300"
"Strategic Petroleum Reserve (SPR)","282.583","282.683","-0.100","0.000","409.097","-126.514","-30.900"
"Total Motor Gasoline","209.180","205.310","3.870","1.900","210.738","-1.558","-0.700"
"Reformulated","0.022","0.025","-0.003","-12.000","0.028","-0.006","-21.400"
"Conventional","14.103","13.710","0.393","2.900","15.235","-1.131","-7.400"
"Blending Components","195.055","191.575","3.480","1.800","195.476","-0.421","-0.200"
"Fuel Ethanol","24.523","24.558","-0.036","-0.100","22.367","2.156","9.600"
"Kerosene-Type Jet Fuel","42.217","42.606","-0.389","-0.900","41.420","0.797","1.900"
"Distillate Fuel Oil","102.824","104.286","-1.462","-1.400","112.189","-9.365","-8.300"
"15 ppm sulfur and Under","91.071","92.880","-1.809","-1.900","101.624","-10.553","-10.400"
"> 15 ppm to 500 ppm sulfur","4.423","4.239","0.183","4.300","3.515","0.907","25.800"
"> 500 ppm sulfur","7.331","7.167","0.164","2.300","7.050","0.281","4.000"
"Residual Fuel Oil","23.399","23.733","-0.334","-1.400","21.802","1.597","7.300"
"Propane/Propylene","105.735","105.846","-0.111","-0.100","105.681","0.054","0.100"
"Other Oils","294.053","296.705","-2.652","-0.900","338.582","-44.529","-13.200"
"Unfinished Oils","76.191","76.051","0.140","0.200","85.483","-9.293","-10.900"
"Total Stocks (Including SPR)","1,495.139","1,498.245","-3.106","-0.200","1,677.842","-182.703","-10.900"
"Total Stocks (Excluding SPR)","1,212.556","1,215.562","-3.006","-0.200","1,268.745","-56.189","-4.400"
"STUB_1","STUB_2","10/23/26","10/16/26","Difference","10/24/25","Difference","10/23/26","10/24/25","Percent Change","10/23/26","10/24/25","Percent Change"
"Crude Oil Supply ","(1) Domestic Production","13,992","13,269","723","13,644","348","13,730","13,635","0.7","13,740","13,459","2.1"
"Crude Oil Supply ","(2) Alaska","482","512","-30","432","50","487","429","13.6","432","419","3.2"
"Crude Oil Supply ","(3) Lower 48","13,510","12,757","753","13,212","298","13,243","13,206","0.3","13,308","13,040","2.1"
"Crude Oil Supply ","(4) Transfers to Crude Oil Supply","540","537","3","463","77","517","411","25.6","539","524","2.8"
"Crude Oil Supply ","(5) Alaskan NGL production","47","46","1","47","0","45","46","-2.3","44","47","-6.7"
"Crude Oil Supply ","(6) Natural Gasoline, Condensate, and Unfinished Oils","493","491","2","416","78","472","366","29.1","495","477","3.7"
"Crude Oil Supply ","(7) Net Imports (Including SPR)","1,520","2,316","-796","690","830","1,847","1,569","17.7","1,910","2,211","-13.6"
"Crude Oil Supply ","(8) Imports","6,374","5,758","616","5,051","1,323","6,255","5,724","9.3","6,165","6,101","1.0"
"Crude Oil Supply ","(9) Commercial Crude Oil","6,374","5,758","616","5,051","1,323","6,255","5,724","9.3","6,165","6,101","1.0"
"Crude Oil Supply ","(10) Imports by SPR","0","0","0","0","0","0","0","0.0","0","0","0.0"
"Crude Oil Supply ","(11) Imports into SPR by Others","0","0","0","0","0","0","0","0.0","0","0","0.0"
"Crude Oil Supply ","(12) Exports","4,854","3,442","1,412","4,361","493","4,408","4,155","6.1","4,254","3,891","9.3"
"Crude Oil Supply ","(13) Stock Change (+/build; -/draw)","-285","-480","196","-904","619","-639","65","� �","-474","55","� �"
"Crude Oil Supply ","(14) Commercial Stock Change","-270","-437","167","-980","709","-596","-21","� �","-30","4","� �"
"Crude Oil Supply ","(15) SPR Stock Change","-14","-43","29","76","-90","-42","86","� �","-444","52","� �"
"Crude Oil Supply ","(16) Adjustment","189","47","142","-481","670","-165","44","� �","-5","109","� �"
"Crude Oil Supply ","(17) Crude Oil Input to Refineries","16,526","16,649","-123","15,219","1,307","16,567","15,594","6.2","16,658","16,247","2.5"
"Other Supply ","(18) Production","10,189","10,144","45","10,053","137","10,188","10,026","1.6","10,072","9,551","5.5"
"Other Supply ","(19) Natural Gas Plant Liquids","7,724","7,697","27","7,739","-14","7,755","7,693","0.8","7,677","7,208","6.5"
"Other Supply ","(20) Renewable Fuels/Oxygenate Plant","1,508","1,484","25","1,396","112","1,473","1,393","5.7","1,410","1,367","3.1"
"Other Supply ","(21) Fuel Ethanol","1,118","1,094","25","1,091","27","1,083","1,087","-0.4","1,090","1,070","1.9"
"Other Supply ","(22) Other","390","390","0","305","85","390","306","27.7","319","297","7.5"
"Other Supply ","(23) Refinery Processing Gain","956","964","-7","918","39","960","940","2.2","986","977","0.9"
"Other Supply ","(24) Net Imports","-6,359","-6,030","-329","-5,026","-1,334","-6,213","-5,344","� �","-6,012","-5,012","� �"
"Other Supply ","(25) Imports","1,315","1,377","-62","1,640","-326","1,419","1,614","-12.1","1,548","1,731","-10.5"
"Other Supply ","(26) Exports","7,674","7,407","267","6,666","1,008","7,632","6,957","9.7","7,560","6,743","12.1"
"Other Supply ","(27) Stock Change (+/build; -/draw)","-159","-770","611","-1,292","1,133","-538","-681","� �","-241","116","� �"
"Other Supply ","(28) Adjustment","234","234","0","206","28","235","207","� �","224","212","� �"
"Other Supply ","(29) Transfers to Crude Oil Supply","-540","-537","-3","-463","-77","-517","-411","25.6","-539","-524","�"
"Products Supplied ","(30) Total","20,209","21,231","-1,022","21,281","-1,072","20,799","20,753","0.2","20,645","20,358","1.4"
"Products Supplied ","(31) Finished Motor Gasoline","8,358","8,982","-625","8,924","-566","8,629","8,688","-0.7","8,766","8,782","-0.2"
"Products Supplied ","(32) Kerosene-Type Jet Fuel","1,679","1,813","-133","1,948","-269","1,703","1,764","-3.5","1,725","1,716","0.5"
"Products Supplied ","(33) Distillate Fuel Oil","4,207","3,732","475","3,580","627","3,978","4,002","-0.6","3,846","3,809","1.0"
"Products Supplied ","(34) Residual Fuel Oil","372","431","-60","376","-4","323","225","43.6","308","281","9.8"
"Products Supplied ","(35) Propane/Propylene","760","1,020","-259","964","-204","941","962","-2.2","969","974","-0.5"
"Products Supplied ","(36) Other Oils","4,834","5,254","-420","5,489","-655","5,226","5,113","2.2","5,030","4,797","4.9"
"Net Imports of Crude and Petroleum Products ","(33) Total","-4,839","-3,714","-1,125","-4,336","-503","-4,365","-3,774","15.7","-4,101","-2,801","46.4"

1 STUB_1 10/23/26 10/16/26 Difference Percent Change 10/24/25 Difference Percent Change
2 Crude Oil 693.208 695.200 -1.992 -0.300 825.063 -131.855 -16.000
3 Commercial (Excluding SPR) 410.625 412.517 -1.892 -0.500 415.966 -5.341 -1.300
4 Strategic Petroleum Reserve (SPR) 282.583 282.683 -0.100 0.000 409.097 -126.514 -30.900
5 Total Motor Gasoline 209.180 205.310 3.870 1.900 210.738 -1.558 -0.700
6 Reformulated 0.022 0.025 -0.003 -12.000 0.028 -0.006 -21.400
7 Conventional 14.103 13.710 0.393 2.900 15.235 -1.131 -7.400
8 Blending Components 195.055 191.575 3.480 1.800 195.476 -0.421 -0.200
9 Fuel Ethanol 24.523 24.558 -0.036 -0.100 22.367 2.156 9.600
10 Kerosene-Type Jet Fuel 42.217 42.606 -0.389 -0.900 41.420 0.797 1.900
11 Distillate Fuel Oil 102.824 104.286 -1.462 -1.400 112.189 -9.365 -8.300
12 15 ppm sulfur and Under 91.071 92.880 -1.809 -1.900 101.624 -10.553 -10.400
13 > 15 ppm to 500 ppm sulfur 4.423 4.239 0.183 4.300 3.515 0.907 25.800
14 > 500 ppm sulfur 7.331 7.167 0.164 2.300 7.050 0.281 4.000
15 Residual Fuel Oil 23.399 23.733 -0.334 -1.400 21.802 1.597 7.300
16 Propane/Propylene 105.735 105.846 -0.111 -0.100 105.681 0.054 0.100
17 Other Oils 294.053 296.705 -2.652 -0.900 338.582 -44.529 -13.200
18 Unfinished Oils 76.191 76.051 0.140 0.200 85.483 -9.293 -10.900
19 Total Stocks (Including SPR) 1,495.139 1,498.245 -3.106 -0.200 1,677.842 -182.703 -10.900
20 Total Stocks (Excluding SPR) 1,212.556 1,215.562 -3.006 -0.200 1,268.745 -56.189 -4.400
21 STUB_1 STUB_2 10/23/26 10/16/26 Difference 10/24/25 Difference 10/23/26 10/24/25 Percent Change 10/23/26 10/24/25 Percent Change
22 Crude Oil Supply (1) Domestic Production 13,992 13,269 723 13,644 348 13,730 13,635 0.7 13,740 13,459 2.1
23 Crude Oil Supply (2) Alaska 482 512 -30 432 50 487 429 13.6 432 419 3.2
24 Crude Oil Supply (3) Lower 48 13,510 12,757 753 13,212 298 13,243 13,206 0.3 13,308 13,040 2.1
25 Crude Oil Supply (4) Transfers to Crude Oil Supply 540 537 3 463 77 517 411 25.6 539 524 2.8
26 Crude Oil Supply (5) Alaskan NGL production 47 46 1 47 0 45 46 -2.3 44 47 -6.7
27 Crude Oil Supply (6) Natural Gasoline, Condensate, and Unfinished Oils 493 491 2 416 78 472 366 29.1 495 477 3.7
28 Crude Oil Supply (7) Net Imports (Including SPR) 1,520 2,316 -796 690 830 1,847 1,569 17.7 1,910 2,211 -13.6
29 Crude Oil Supply (8) Imports 6,374 5,758 616 5,051 1,323 6,255 5,724 9.3 6,165 6,101 1.0
30 Crude Oil Supply (9) Commercial Crude Oil 6,374 5,758 616 5,051 1,323 6,255 5,724 9.3 6,165 6,101 1.0
31 Crude Oil Supply (10) Imports by SPR 0 0 0 0 0 0 0 0.0 0 0 0.0
32 Crude Oil Supply (11) Imports into SPR by Others 0 0 0 0 0 0 0 0.0 0 0 0.0
33 Crude Oil Supply (12) Exports 4,854 3,442 1,412 4,361 493 4,408 4,155 6.1 4,254 3,891 9.3
34 Crude Oil Supply (13) Stock Change (+/build; -/draw) -285 -480 196 -904 619 -639 65 � � -474 55 � �
35 Crude Oil Supply (14) Commercial Stock Change -270 -437 167 -980 709 -596 -21 � � -30 4 � �
36 Crude Oil Supply (15) SPR Stock Change -14 -43 29 76 -90 -42 86 � � -444 52 � �
37 Crude Oil Supply (16) Adjustment 189 47 142 -481 670 -165 44 � � -5 109 � �
38 Crude Oil Supply (17) Crude Oil Input to Refineries 16,526 16,649 -123 15,219 1,307 16,567 15,594 6.2 16,658 16,247 2.5
39 Other Supply (18) Production 10,189 10,144 45 10,053 137 10,188 10,026 1.6 10,072 9,551 5.5
40 Other Supply (19) Natural Gas Plant Liquids 7,724 7,697 27 7,739 -14 7,755 7,693 0.8 7,677 7,208 6.5
41 Other Supply (20) Renewable Fuels/Oxygenate Plant 1,508 1,484 25 1,396 112 1,473 1,393 5.7 1,410 1,367 3.1
42 Other Supply (21) Fuel Ethanol 1,118 1,094 25 1,091 27 1,083 1,087 -0.4 1,090 1,070 1.9
43 Other Supply (22) Other 390 390 0 305 85 390 306 27.7 319 297 7.5
44 Other Supply (23) Refinery Processing Gain 956 964 -7 918 39 960 940 2.2 986 977 0.9
45 Other Supply (24) Net Imports -6,359 -6,030 -329 -5,026 -1,334 -6,213 -5,344 � � -6,012 -5,012 � �
46 Other Supply (25) Imports 1,315 1,377 -62 1,640 -326 1,419 1,614 -12.1 1,548 1,731 -10.5
47 Other Supply (26) Exports 7,674 7,407 267 6,666 1,008 7,632 6,957 9.7 7,560 6,743 12.1
48 Other Supply (27) Stock Change (+/build; -/draw) -159 -770 611 -1,292 1,133 -538 -681 � � -241 116 � �
49 Other Supply (28) Adjustment 234 234 0 206 28 235 207 � � 224 212 � �
50 Other Supply (29) Transfers to Crude Oil Supply -540 -537 -3 -463 -77 -517 -411 25.6 -539 -524 �
51 Products Supplied (30) Total 20,209 21,231 -1,022 21,281 -1,072 20,799 20,753 0.2 20,645 20,358 1.4
52 Products Supplied (31) Finished Motor Gasoline 8,358 8,982 -625 8,924 -566 8,629 8,688 -0.7 8,766 8,782 -0.2
53 Products Supplied (32) Kerosene-Type Jet Fuel 1,679 1,813 -133 1,948 -269 1,703 1,764 -3.5 1,725 1,716 0.5
54 Products Supplied (33) Distillate Fuel Oil 4,207 3,732 475 3,580 627 3,978 4,002 -0.6 3,846 3,809 1.0
55 Products Supplied (34) Residual Fuel Oil 372 431 -60 376 -4 323 225 43.6 308 281 9.8
56 Products Supplied (35) Propane/Propylene 760 1,020 -259 964 -204 941 962 -2.2 969 974 -0.5
57 Products Supplied (36) Other Oils 4,834 5,254 -420 5,489 -655 5,226 5,113 2.2 5,030 4,797 4.9
58 Net Imports of Crude and Petroleum Products (33) Total -4,839 -3,714 -1,125 -4,336 -503 -4,365 -3,774 15.7 -4,101 -2,801 46.4
59 
+143
View File
@@ -0,0 +1,143 @@
<html><body>The standard release time and day of the week will be at 10:30 a.m. eastern time on Wednesday with the following exceptions. All times are eastern.<table class="basic-table schedule full-width align-left-col-2 align-left-col-3 align-left-col-4 align-left-col-5">
<thead>
<tr>
<th width="121" scope="col">Data for the week ending</th>
<th width="114" scope="col">Alternate release date</th>
<th width="71" scope="col">Release day</th>
<th width="100" scope="col">Release time</th>
<th width="155" scope="col">Holiday</th>
</tr>
</thead>
<tbody>
<tr class="past-holiday">
<th scope="row">December 27, 2024</th>
<td>January 2, 2025</td>
<td>Thursday</td>
<td>11:00 a.m.</td>
<td>New Year's Day</td>
</tr>
<tr class="past-holiday">
<th scope="row">January 17, 2025</th>
<td>January 23, 2025</td>
<td>Thursday</td>
<td>12:00 p.m.</td>
<td>Martin Luther King Jr. Day / Inauguration</td>
</tr>
<tr class="past-holiday">
<th scope="row">February 14, 2025</th>
<td>February 20, 2025</td>
<td>Thursday</td>
<td>12:00 p.m.</td>
<td>President's Day</td>
</tr>
<tr class="past-holiday">
<th scope="row">May 23, 2025</th>
<td>May 29, 2025</td>
<td>Thursday</td>
<td>12:00 p.m.</td>
<td>Memorial Day</td>
</tr>
<tr class="past-holiday">
<th scope="row">August 29, 2025</th>
<td>September 4, 2025</td>
<td>Thursday</td>
<td>12:00 p.m.</td>
<td>Labor Day</td>
</tr>
<tr class="past-holiday">
<th scope="row">October 10, 2025</th>
<td>October 16, 2025</td>
<td>Thursday</td>
<td>12:00 p.m.</td>
<td>Columbus Day</td>
</tr>
<tr class="past-holiday">
<th scope="row">November 7, 2025</th>
<td>November 13, 2025</td>
<td>Thursday</td>
<td>12:00 p.m.</td>
<td>Veterans Day</td>
</tr>
<tr class="past-holiday">
<th scope="row">December 19, 2025</th>
<td class="message-updated">December 29, 2025</td>
<td>Monday</td>
<td>5:00 p.m.</td>
<td>Christmas</td>
</tr>
<tr class="past-holiday">
<th scope="row">January 16, 2026</th>
<td>January 22, 2026</td>
<td>Thursday</td>
<td>12:00 p.m.</td>
<td>Martin Luther King Jr. Day</td>
</tr>
<tr class="past-holiday">
<th scope="row">February 13, 2026</th>
<td>February 19, 2026</td>
<td>Thursday</td>
<td>12:00 p.m.</td>
<td>President's Day</td>
</tr>
<tr class="past-holiday">
<th scope="row">May 22, 2026</th>
<td>May 28, 2026</td>
<td>Thursday</td>
<td>12:00 p.m.</td>
<td>Memorial Day</td>
</tr>
<tr class="past-holiday">
<th scope="row">September 4, 2026</th>
<td>September 10, 2026</td>
<td>Thursday</td>
<td>12:00 p.m.</td>
<td>Labor Day</td>
</tr>
<tr class="past-holiday">
<th scope="row">October 9, 2026</th>
<td>October 15, 2026</td>
<td>Thursday</td>
<td>12:00 p.m.</td>
<td>Columbus Day</td>
</tr>
<tr>
<th scope="row">November 6, 2026</th>
<td>November 12, 2026</td>
<td>Thursday</td>
<td>12:00 p.m.</td>
<td>Veterans Day</td>
</tr>
</tbody>
</table></body></html>
+1 -1
View File
@@ -282,7 +282,7 @@ def test_worker_initializes_all_sources_and_drops_expired_month_status(trading):
worker = CalendarWorker()
worker.sources["https://www.federalreserve.gov/old-month.htm"] = {"error": "old"}
async def unavailable(source, url, *, query=None):
assert len(worker.sources) == 15
assert len(worker.sources) == 17
assert all(s["error"] for s in worker.sources.values())
raise httpx.ConnectError("source unavailable")
worker.fetch = unavailable
+180
View File
@@ -0,0 +1,180 @@
"""DOL regression coverage uses a public release extract and isolated I/O."""
import asyncio
from datetime import UTC, date, datetime, timedelta
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import AsyncMock
import httpx
import pytest
from alphapulse.calendar import dol
REPORT = (Path(__file__).parent / "fixtures/calendar/dol-current.txt").read_text()
SCHEDULE = """<html><body>
Publication Schedule: The UI Weekly Claims News Release is published each week on Thursday morning at 8:30am EST.
<table><thead><tr><th>Release Date</th><th>Release Time</th></tr></thead><tbody>
<tr><td>Wednesday, November 25, 2026</td><td>8:30 AM EST</td></tr>
</tbody></table></body></html>"""
def test_report_separates_statistical_weeks_and_revised_previous():
initial, continued = (dol.report(REPORT)[family] for family in dol.FAMILIES)
assert initial.at == continued.at == datetime(2026, 10, 29, 12, 30, tzinfo=UTC)
assert initial.period == "2026-10-24" and continued.period == "2026-10-17"
assert initial.values["initial_claims"]["actual"] == "19.4"
assert initial.values["initial_claims"]["previous"] == "19.5"
assert continued.values["continued_claims"]["actual"] == "170.1"
assert continued.values["continued_claims"]["previous"] == "172.2"
assert "修订" in continued.values["continued_claims"]["previous_basis"]
def test_report_resolves_new_year_from_publication_date():
text = REPORT.replace("October 29, 2026", "January 2, 2027").replace("October 24", "December 26").replace("October 17", "December 19")
result = dol.report(text)
assert result["initial_claims"].period == "2026-12-26"
assert result["continued_claims"].period == "2026-12-19"
assert result["initial_claims"].at == datetime(2027, 1, 2, 13, 30, tzinfo=UTC)
def test_historical_report_keeps_explicit_unrevised_previous():
# Source: https://oui.doleta.gov/press/2025/010825.pdf (first two pages).
text = (Path(__file__).parent / "fixtures/calendar/dol-2025-01-08.txt").read_text()
result = dol.report(text)
assert result["initial_claims"].at == datetime(2025, 1, 8, 13, 30, tzinfo=UTC)
assert result["initial_claims"].values["initial_claims"]["actual"] == "20.1"
assert result["initial_claims"].values["initial_claims"]["previous"] == "21.1"
assert result["continued_claims"].period == "2024-12-28"
assert result["continued_claims"].values["continued_claims"]["previous"] == "183.4"
@pytest.mark.parametrize("text", [
"Access denied",
REPORT.replace("SEASONALLY ADJUSTED DATA", "CHANGED HEADER"),
REPORT.replace("initial claims was 194,000", "initial claims pending"),
REPORT.replace("October 24", "September 1"),
REPORT.replace("195,000.", "unavailable."),
])
def test_unrecognized_report_does_not_fabricate_values(text):
with pytest.raises(ValueError):
dol.report(text)
def test_schedule_respects_dst_and_thanksgiving_exception():
events = dol.schedule(SCHEDULE, datetime(2026, 10, 30, tzinfo=UTC))
initial = {item.at.date(): item for item in events if item.family == "initial_claims"}
assert initial[date(2026, 10, 29)].at.hour == 12
assert initial[date(2026, 11, 5)].at.hour == 13
assert initial[date(2026, 11, 25)].at.hour == 13
assert date(2026, 11, 26) not in initial
assert all(item.period is None for item in events)
assert len({item.external_id for item in events}) == len(events)
@pytest.mark.parametrize("body", [
SCHEDULE.replace("Thursday morning at 8:30am EST", "Schedule changed"),
SCHEDULE.replace("8:30 AM EST", "9:00 AM EST"),
SCHEDULE.replace("November 25, 2026", "11/25/2026"),
SCHEDULE.replace("Release Date", "Changed heading"),
])
def test_changed_schedule_or_exception_format_fails_explicitly(body):
with pytest.raises(ValueError):
dol.schedule(body, datetime(2026, 10, 30, tzinfo=UTC))
def test_archives_only_follow_official_links_and_requested_range():
body = '''<a href="/press/2025/010825.pdf">8</a><a href="/press/2025/010825.pdf">Duplicate</a>
<a href="https://example.com/press/2025/011625.pdf">Other host</a>
<a href="/press/2024/122624.pdf">Old</a>'''
assert dol.archive_links(body, date(2025, 1, 1), date(2025, 12, 31)) == ["https://oui.doleta.gov/press/2025/010825.pdf"]
with pytest.raises(ValueError, match="历史覆盖尚未确认"):
dol.archive_links(body, date(2026, 1, 1), date(2026, 12, 31))
with pytest.raises(ValueError, match="年份"):
dol.archive_links('<a href="/press/2025/010826.pdf">Bad year</a>', date(2025, 1, 1), date(2026, 12, 31))
@pytest.mark.parametrize("offset,received,expected", [(0, False, "page"), (7, False, "pending"), (-7, True, "pending"), (-7, False, "error")])
def test_live_result_never_applies_another_week(trading, monkeypatch, offset, received, expected):
from alphapulse.calendar import worker as module
async def run():
worker = module.CalendarWorker()
try:
response = httpx.Response(200, content=b"synthetic PDF", request=httpx.Request("GET", dol.LATEST))
worker.fetch = AsyncMock(return_value=(response, 7))
monkeypatch.setattr(module, "pdf_text", lambda body: REPORT)
at = datetime(2026, 10, 29, 12, 30, tzinfo=UTC) + timedelta(days=offset)
row = SimpleNamespace(source="dol", family="initial_claims", release_url=None, scheduled_at=at,
first_received_at=at if received else None)
if expected == "error":
with pytest.raises(ValueError, match="公告时间"):
await worker.result_page(row)
else:
page = await worker.result_page(row)
assert (page is not None) == (expected == "page")
finally:
await worker.http.aclose()
await worker.bls.aclose()
asyncio.run(run())
def test_weekly_rows_show_their_own_week_and_uncertain_crypto_impact(trading):
from alphapulse.calendar.service import release_rows
announcement = dol.report(REPORT)["continued_claims"]
row = SimpleNamespace(id=1, family="continued_claims", source="dol", title="Weekly claims",
period=announcement.period, stage=None, scheduled_at=announcement.at, schedule_status="scheduled",
first_received_at=announcement.at, last_checked_at=announcement.at, values=announcement.values,
error=None, release_url=dol.LATEST, schedule_url=dol.SCHEDULE, revision_no=2)
[item] = release_rows(row, announcement.at)
assert item["actual"] == "170.1" and item["unit"] == "万人"
assert "2026-10-17" in item["event_name"]
assert item["impact"] == "uncertain" and item["forecast"] is None
def test_history_preview_includes_both_series_without_writing(trading, monkeypatch):
from alphapulse.calendar import backfill
class Session:
async def __aenter__(self):
return self
async def __aexit__(self, *_):
pass
async def execute(self, _):
return SimpleNamespace(all=lambda: [])
async def scalars(self, _):
return []
doc = backfill.Document("dol", dol.LATEST, b"%PDF synthetic report", datetime(2026, 10, 30, tzinfo=UTC))
monkeypatch.setattr(backfill, "SessionLocal", Session)
monkeypatch.setattr(backfill, "fetch_documents", AsyncMock(return_value=[doc]))
monkeypatch.setattr(backfill, "pdf_text", lambda body: REPORT)
save = AsyncMock()
monkeypatch.setattr(backfill, "save_announcement", save)
result = asyncio.run(backfill.run(date(2026, 10, 29), date(2026, 10, 29), sources={"dol"}))
assert result["mode"] == "dry-run"
assert result["announcements"] == {"initial_claims": 1, "continued_claims": 1}
assert not result["errors"] and not result["missing_announcements"]
save.assert_not_awaited()
def test_archive_year_gaps_are_explicit_and_current_report_is_still_collected(trading, monkeypatch):
from alphapulse.calendar import backfill
requests = []
def respond(request):
requests.append(str(request.url))
if request.url.path.endswith("archive.asp"):
return httpx.Response(200, text='<a href="/press/2025/010825.pdf">8</a>')
return httpx.Response(200, content=b"%PDF synthetic report")
client = httpx.AsyncClient
monkeypatch.setattr(httpx, "AsyncClient", lambda **kwargs: client(**kwargs, transport=httpx.MockTransport(respond)))
monkeypatch.setattr(backfill, "ensure_quiet_window", AsyncMock())
monkeypatch.setattr(backfill.asyncio, "sleep", AsyncMock())
errors = []
documents = asyncio.run(backfill.fetch_documents(date(2025, 1, 1), date(2026, 10, 30), set(), errors, sources={"dol"}))
assert len(documents) == 2 and dol.LATEST in requests
assert len(errors) == 1 and "2026" in errors[0]["error"] and "不完整" in errors[0]["error"]
+125
View File
@@ -0,0 +1,125 @@
"""Verify EIA units, calendar exceptions, archives and reporting boundaries."""
import asyncio
from datetime import UTC, date, datetime, timedelta
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import AsyncMock
from urllib.parse import urlparse
import httpx
import pytest
from alphapulse.calendar import eia
FIXTURES = Path(__file__).parent / "fixtures/calendar"
CSV = (FIXTURES / "eia-2026-10-23.csv").read_text()
SCHEDULE = (FIXTURES / "eia-schedule.html").read_text()
def test_official_csv_preserves_precision_units_and_excludes_spr():
report = eia.report(CSV, SCHEDULE)
assert report.at == datetime(2026, 10, 28, 14, 30, tzinfo=UTC)
assert report.period == "2026-10-23"
assert report.values["eia_crude"]["actual"] == "-189.200"
assert report.values["eia_gasoline"]["actual"] == "387.000"
assert report.values["eia_distillate"]["actual"] == "-146.200"
assert all(v["previous"] is None for v in report.values.values())
def test_schedule_uses_holiday_times_and_eastern_dst():
events = eia.schedule(SCHEDULE, datetime(2026, 10, 30, tzinfo=UTC))
by_period = {item.period: item.at for item in events}
assert by_period["2026-10-23"] == datetime(2026, 10, 28, 14, 30, tzinfo=UTC)
assert by_period["2026-10-30"] == datetime(2026, 11, 4, 15, 30, tzinfo=UTC)
assert by_period["2026-11-06"] == datetime(2026, 11, 12, 17, tzinfo=UTC)
assert eia.release_at(date(2025, 12, 19), eia.exceptions(SCHEDULE)) == datetime(2025, 12, 29, 22, tzinfo=UTC)
@pytest.mark.parametrize("body", [
CSV.replace('"Difference"', '"Changed heading"', 1),
CSV.replace('"10/16/26"', '"10/15/26"', 1),
CSV.replace('"Commercial (Excluding SPR)"', '"Other stock"'),
CSV.replace('"-1.892"', '"NaN"', 1),
])
def test_unrecognized_csv_fails_instead_of_filling_other_data(body):
with pytest.raises(ValueError):
eia.report(body, SCHEDULE)
def test_following_supply_table_does_not_override_inventory_data():
body = CSV + '\n"Commercial (Excluding SPR)",1,2,3,4,5,6,7'
assert eia.report(body, SCHEDULE).values == eia.report(CSV, SCHEDULE).values
@pytest.mark.parametrize("body", ["Access denied", SCHEDULE.replace("10:30 a.m. eastern time on Wednesday", "Changed schedule"),
SCHEDULE.replace("12:00 p.m.", "Time pending", 1)])
def test_unrecognized_schedule_fails(body):
with pytest.raises(ValueError):
eia.exceptions(body)
def test_archive_discovery_follows_published_report_and_csv_links():
url = "https://www.eia.gov/petroleum/supply/weekly/archive/2026/2026_10_28/wpsr_2026_10_28.php"
body = f'<a href="{url}">28</a><a href="https://example.com{urlparse(url).path}">Other host</a>'
assert eia.archive_links(body, date(2026, 10, 1), date(2026, 10, 30)) == [url]
assert eia.csv_link('<a href="download/table1.csv">CSV</a>', url) == url.rsplit("/", 1)[0] + "/download/table1.csv"
with pytest.raises(ValueError):
eia.archive_links(body, date(2025, 1, 1), date(2025, 12, 31))
with pytest.raises(ValueError):
eia.csv_link('<a href="other/table1.csv">Other file</a>', url)
@pytest.mark.parametrize("offset,received,expected", [(0, False, "page"), (7, False, "pending"), (-7, True, "pending"), (-7, False, "error")])
def test_worker_does_not_apply_latest_values_to_another_week(trading, offset, received, expected):
from alphapulse.calendar.worker import CalendarWorker
async def run():
worker = CalendarWorker()
try:
worker.eia_schedule_body = SCHEDULE
response = httpx.Response(200, text=CSV, request=httpx.Request("GET", eia.LATEST))
worker.fetch = AsyncMock(return_value=(response, 1))
at = datetime(2026, 10, 28, 14, 30, tzinfo=UTC) + timedelta(days=offset)
row = SimpleNamespace(source="eia", family="eia_stocks", period="2026-10-23", release_url=None,
scheduled_at=at, first_received_at=at if received else None)
if expected == "error":
with pytest.raises(ValueError, match="所选日程不符"):
await worker.result_page(row)
else:
result = await worker.result_page(row)
assert (result is not None) == (expected == "page")
finally:
await worker.http.aclose()
await worker.bls.aclose()
asyncio.run(run())
@pytest.mark.parametrize("archive_date,valid", [("2026_10_28", True), ("2026_10_21", False)])
def test_history_preview_requires_matching_archive_date_without_writes(trading, monkeypatch, archive_date, valid):
from alphapulse.calendar import backfill
class Session:
async def __aenter__(self):
return self
async def __aexit__(self, *_):
pass
async def execute(self, _):
return SimpleNamespace(all=lambda: [])
async def scalars(self, _):
return []
now = datetime(2026, 10, 30, tzinfo=UTC)
docs = [backfill.Document("eia", eia.SCHEDULE, SCHEDULE.encode(), now),
backfill.Document("eia", f"https://www.eia.gov/petroleum/supply/weekly/archive/2026/{archive_date}/download/table1.csv", CSV.encode(), now)]
monkeypatch.setattr(backfill, "SessionLocal", Session)
monkeypatch.setattr(backfill, "fetch_documents", AsyncMock(return_value=docs))
save = AsyncMock()
monkeypatch.setattr(backfill, "save_announcement", save)
result = asyncio.run(backfill.run(date(2026, 10, 28), date(2026, 10, 28), sources={"eia"}))
assert result["mode"] == "dry-run"
assert result["announcements"].get("eia_stocks", 0) == int(valid)
assert bool(result["errors"]) != valid
save.assert_not_awaited()