wiki-dump-extractor 0.1.0__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.
- wiki_dump_extractor/__init__.py +14 -0
- wiki_dump_extractor/date_utils.py +529 -0
- wiki_dump_extractor/download_utils.py +29 -0
- wiki_dump_extractor/llm_utils.py +343 -0
- wiki_dump_extractor/page_utils.py +520 -0
- wiki_dump_extractor/wiki_dump_extractor.py +661 -0
- wiki_dump_extractor/wiki_sql_extractor.py +104 -0
- wiki_dump_extractor-0.1.0.dist-info/METADATA +172 -0
- wiki_dump_extractor-0.1.0.dist-info/RECORD +12 -0
- wiki_dump_extractor-0.1.0.dist-info/WHEEL +5 -0
- wiki_dump_extractor-0.1.0.dist-info/licenses/LICENSE +21 -0
- wiki_dump_extractor-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""My Project package."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
from .wiki_dump_extractor import WikiXmlDumpExtractor, WikiAvroDumpExtractor
|
|
6
|
+
from .wiki_sql_extractor import WikiSqlExtractor
|
|
7
|
+
from .download_utils import download_file
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"WikiXmlDumpExtractor",
|
|
11
|
+
"WikiAvroDumpExtractor",
|
|
12
|
+
"WikiSqlExtractor",
|
|
13
|
+
"download_file",
|
|
14
|
+
]
|
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import List, Dict, ClassVar, Pattern, Optional
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from dataclasses import dataclass, asdict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(slots=True)
|
|
9
|
+
class Date:
|
|
10
|
+
year: int
|
|
11
|
+
month: Optional[int] = None
|
|
12
|
+
day: Optional[int] = None
|
|
13
|
+
is_approximate: bool = False
|
|
14
|
+
|
|
15
|
+
def __post_init__(self):
|
|
16
|
+
self.validate()
|
|
17
|
+
|
|
18
|
+
def validate(self):
|
|
19
|
+
if self.month is None:
|
|
20
|
+
return True
|
|
21
|
+
if not 1 <= self.month <= 12:
|
|
22
|
+
raise ValueError(f"Month must be between 1 and 12, got {self.month}")
|
|
23
|
+
|
|
24
|
+
if self.day is None:
|
|
25
|
+
return True
|
|
26
|
+
|
|
27
|
+
# Validate day based on month and year
|
|
28
|
+
max_days = 31 # Default for most months
|
|
29
|
+
|
|
30
|
+
if self.month in [4, 6, 9, 11]: # April, June, September, November
|
|
31
|
+
max_days = 30
|
|
32
|
+
elif self.month == 2: # February
|
|
33
|
+
# Check for leap year
|
|
34
|
+
if (self.year % 4 == 0 and self.year % 100 != 0) or (self.year % 400 == 0):
|
|
35
|
+
max_days = 29
|
|
36
|
+
else:
|
|
37
|
+
max_days = 28
|
|
38
|
+
|
|
39
|
+
if not 1 <= self.day <= max_days:
|
|
40
|
+
raise ValueError(
|
|
41
|
+
f"Day must be between 1 and {max_days} for month {self.month}, got {self.day}"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def to_string(self) -> str:
|
|
45
|
+
if self.year < 0:
|
|
46
|
+
result = f"{-self.year:04d} BC"
|
|
47
|
+
else:
|
|
48
|
+
result = f"{self.year:04d}"
|
|
49
|
+
if self.month is not None:
|
|
50
|
+
result += f"/{self.month:02d}"
|
|
51
|
+
if self.day is not None:
|
|
52
|
+
result += f"/{self.day:02d}"
|
|
53
|
+
return result
|
|
54
|
+
|
|
55
|
+
def to_dict(self) -> Dict:
|
|
56
|
+
return asdict(self)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# Define month name to number mapping
|
|
60
|
+
_MONTH_MAP = {
|
|
61
|
+
"january": 1,
|
|
62
|
+
"jan": 1,
|
|
63
|
+
"february": 2,
|
|
64
|
+
"feb": 2,
|
|
65
|
+
"march": 3,
|
|
66
|
+
"mar": 3,
|
|
67
|
+
"april": 4,
|
|
68
|
+
"apr": 4,
|
|
69
|
+
"may": 5,
|
|
70
|
+
"june": 6,
|
|
71
|
+
"jun": 6,
|
|
72
|
+
"july": 7,
|
|
73
|
+
"jul": 7,
|
|
74
|
+
"august": 8,
|
|
75
|
+
"aug": 8,
|
|
76
|
+
"september": 9,
|
|
77
|
+
"sep": 9,
|
|
78
|
+
"october": 10,
|
|
79
|
+
"oct": 10,
|
|
80
|
+
"november": 11,
|
|
81
|
+
"nov": 11,
|
|
82
|
+
"december": 12,
|
|
83
|
+
"dec": 12,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# Dictionary to convert written numbers to integers
|
|
87
|
+
_WRITTEN_NUMBERS = {
|
|
88
|
+
"first": 1,
|
|
89
|
+
"second": 2,
|
|
90
|
+
"third": 3,
|
|
91
|
+
"fourth": 4,
|
|
92
|
+
"fifth": 5,
|
|
93
|
+
"sixth": 6,
|
|
94
|
+
"seventh": 7,
|
|
95
|
+
"eighth": 8,
|
|
96
|
+
"ninth": 9,
|
|
97
|
+
"tenth": 10,
|
|
98
|
+
"eleventh": 11,
|
|
99
|
+
"twelfth": 12,
|
|
100
|
+
"thirteenth": 13,
|
|
101
|
+
"fourteenth": 14,
|
|
102
|
+
"fifteenth": 15,
|
|
103
|
+
"sixteenth": 16,
|
|
104
|
+
"seventeenth": 17,
|
|
105
|
+
"eighteenth": 18,
|
|
106
|
+
"nineteenth": 19,
|
|
107
|
+
"twentieth": 20,
|
|
108
|
+
"twenty-first": 21,
|
|
109
|
+
"twenty-second": 22,
|
|
110
|
+
"twenty-third": 23,
|
|
111
|
+
"twenty-fourth": 24,
|
|
112
|
+
"twenty-fifth": 25,
|
|
113
|
+
"twenty-sixth": 26,
|
|
114
|
+
"twenty-seventh": 27,
|
|
115
|
+
"twenty-eighth": 28,
|
|
116
|
+
"twenty-ninth": 29,
|
|
117
|
+
"thirtieth": 30,
|
|
118
|
+
"thirty-first": 31,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# Common month pattern for reuse
|
|
122
|
+
_MONTHS_PATTERN = (
|
|
123
|
+
"January|February|March|April|May|June|July|August|September|October|November|December|"
|
|
124
|
+
"Jan|Feb|Mar|Apr|Jun|Jul|Aug|Sep|Oct|Nov|Dec"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass(slots=True)
|
|
129
|
+
class DetectedDate:
|
|
130
|
+
date: Date
|
|
131
|
+
format: str
|
|
132
|
+
date_str: str
|
|
133
|
+
|
|
134
|
+
def to_dict(self) -> Dict:
|
|
135
|
+
return {
|
|
136
|
+
"date": self.date.to_dict(),
|
|
137
|
+
"format": self.format,
|
|
138
|
+
"date_str": self.date_str,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class DateFormat(ABC):
|
|
143
|
+
"""Base class for all date format detectors."""
|
|
144
|
+
|
|
145
|
+
name: ClassVar[str]
|
|
146
|
+
pattern: ClassVar[Pattern]
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
@abstractmethod
|
|
150
|
+
def match_to_date(cls, match: re.Match) -> datetime:
|
|
151
|
+
"""Convert a regex match to a datetime object.
|
|
152
|
+
|
|
153
|
+
Parameters
|
|
154
|
+
----------
|
|
155
|
+
match : re.Match
|
|
156
|
+
The regex match object containing the date information
|
|
157
|
+
|
|
158
|
+
Returns
|
|
159
|
+
-------
|
|
160
|
+
datetime
|
|
161
|
+
The parsed datetime object
|
|
162
|
+
|
|
163
|
+
Raises
|
|
164
|
+
------
|
|
165
|
+
ValueError
|
|
166
|
+
If the match cannot be converted to a valid datetime
|
|
167
|
+
"""
|
|
168
|
+
pass
|
|
169
|
+
|
|
170
|
+
@classmethod
|
|
171
|
+
def convert_month_to_number(cls, month_name: str) -> int:
|
|
172
|
+
"""Convert month name to its numerical representation.
|
|
173
|
+
|
|
174
|
+
Raises
|
|
175
|
+
------
|
|
176
|
+
ValueError
|
|
177
|
+
If the month name is not recognized
|
|
178
|
+
"""
|
|
179
|
+
month = _MONTH_MAP.get(month_name.lower())
|
|
180
|
+
if month is None:
|
|
181
|
+
raise ValueError(f"Unknown month name: {month_name}")
|
|
182
|
+
return month
|
|
183
|
+
|
|
184
|
+
@classmethod
|
|
185
|
+
def list_dates(cls, text: str) -> bool:
|
|
186
|
+
"""Check if the text contains any dates detected by regex patterns."""
|
|
187
|
+
results = []
|
|
188
|
+
errors = []
|
|
189
|
+
for match in cls.pattern.finditer(text):
|
|
190
|
+
try:
|
|
191
|
+
date = cls.match_to_date(match)
|
|
192
|
+
if date is not None:
|
|
193
|
+
detected_date = DetectedDate(
|
|
194
|
+
date_str=match.group(0), format=cls.name, date=date
|
|
195
|
+
)
|
|
196
|
+
results.append(detected_date)
|
|
197
|
+
|
|
198
|
+
except ValueError as err:
|
|
199
|
+
errors.append(
|
|
200
|
+
f"Error parsing {cls.name} date: {match.group(0)} - {err}"
|
|
201
|
+
)
|
|
202
|
+
return results, errors
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class SlashDMYMDYFormat(DateFormat):
|
|
206
|
+
"""Format for DD/MM/YYYY or MM/DD/YYYY dates."""
|
|
207
|
+
|
|
208
|
+
name = "SLASH_DMY_MDY"
|
|
209
|
+
pattern = re.compile(
|
|
210
|
+
r"\B[^|](\d{1,2})[-/](\d{1,2})[-/](\d{1,4})(?:\s+(BC|BCE))?\b", re.IGNORECASE
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
@classmethod
|
|
214
|
+
def match_to_date(cls, match: re.Match) -> Date:
|
|
215
|
+
day, month, year, bc = match.groups()
|
|
216
|
+
day, month, year = int(day), int(month), int(year)
|
|
217
|
+
if bc:
|
|
218
|
+
year = -year
|
|
219
|
+
|
|
220
|
+
# Try MM/DD/YYYY first (American format)
|
|
221
|
+
try:
|
|
222
|
+
return Date(year, month, day)
|
|
223
|
+
except (ValueError, IndexError):
|
|
224
|
+
# Try DD/MM/YYYY (European format)
|
|
225
|
+
try:
|
|
226
|
+
return Date(year, day, month)
|
|
227
|
+
except (ValueError, IndexError):
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class DashYMDFormat(DateFormat):
|
|
232
|
+
"""Format for YYYY-MM-DD dates."""
|
|
233
|
+
|
|
234
|
+
name = "DASH_YMD"
|
|
235
|
+
pattern = re.compile(
|
|
236
|
+
r"\b(\d{1,4})[-/](\d{1,2})[-/](\d{1,2})(?:\s+(BC|BCE))?\b", re.IGNORECASE
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
@classmethod
|
|
240
|
+
def match_to_date(cls, match: re.Match) -> Date:
|
|
241
|
+
groups = match.groups()
|
|
242
|
+
year, month, day, bc = groups
|
|
243
|
+
if bc:
|
|
244
|
+
year = -int(year)
|
|
245
|
+
return Date(int(year), int(month), int(day))
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class DayMonthYearFormat(DateFormat):
|
|
249
|
+
"""Format for DD Month YYYY dates."""
|
|
250
|
+
|
|
251
|
+
name = "DAY_MONTH_YEAR"
|
|
252
|
+
re_dmy = rf"""\b
|
|
253
|
+
(\d{{1,2}}) # Day (1-2 digits)
|
|
254
|
+
\s+
|
|
255
|
+
({_MONTHS_PATTERN}) # Month (provided externally)
|
|
256
|
+
[,\s]+
|
|
257
|
+
(?:AD\s*)?
|
|
258
|
+
(\d{{1,4}}) # Year (1-4 digits)
|
|
259
|
+
(?:\s+(BC|BCE))? # Optional ' BC'
|
|
260
|
+
\b
|
|
261
|
+
"""
|
|
262
|
+
pattern = re.compile(re_dmy, re.VERBOSE | re.IGNORECASE)
|
|
263
|
+
|
|
264
|
+
@classmethod
|
|
265
|
+
def match_to_date(cls, match: re.Match) -> Date:
|
|
266
|
+
day, month_str, year, bc = match.groups()
|
|
267
|
+
month = cls.convert_month_to_number(month_str)
|
|
268
|
+
if bc:
|
|
269
|
+
year = -int(year)
|
|
270
|
+
return Date(int(year), month, int(day))
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class MonthDayYearFormat(DateFormat):
|
|
274
|
+
"""Format for Month DD YYYY dates."""
|
|
275
|
+
|
|
276
|
+
name = "MONTH_DAY_YEAR"
|
|
277
|
+
re_mdy = rf"""
|
|
278
|
+
\b
|
|
279
|
+
({_MONTHS_PATTERN}) # Month name
|
|
280
|
+
\s+
|
|
281
|
+
(\d{{1,2}}) # Day (1 or 2 digits)
|
|
282
|
+
(?:st|nd|rd|th)? # Optional ordinal suffix
|
|
283
|
+
[,\s]+
|
|
284
|
+
(?:AD\s*)?
|
|
285
|
+
(\d{{1,4}}) # Year (1 to 4 digits)
|
|
286
|
+
(?:\s+(BC|BCE))? # Optional ' BC'
|
|
287
|
+
\b
|
|
288
|
+
"""
|
|
289
|
+
pattern = re.compile(re_mdy, re.VERBOSE | re.IGNORECASE)
|
|
290
|
+
|
|
291
|
+
@classmethod
|
|
292
|
+
def match_to_date(cls, match: re.Match) -> Date:
|
|
293
|
+
month_str, day, year, bc = match.groups()
|
|
294
|
+
month = cls.convert_month_to_number(month_str)
|
|
295
|
+
if bc:
|
|
296
|
+
year = -int(year)
|
|
297
|
+
return Date(int(year), month, int(day))
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
class MonthYearFormat(DateFormat):
|
|
301
|
+
"""Format for Month YYYY dates."""
|
|
302
|
+
|
|
303
|
+
name = "MONTH_YEAR"
|
|
304
|
+
pattern = re.compile(
|
|
305
|
+
rf"\b({_MONTHS_PATTERN})\s*(?:AD\s*)?(\d{{2,4}})(?:\s+(BC|BCE))?\b",
|
|
306
|
+
re.IGNORECASE,
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
@classmethod
|
|
310
|
+
def match_to_date(cls, match: re.Match) -> Date:
|
|
311
|
+
# Check if there's a digit before the month
|
|
312
|
+
start_pos = match.start()
|
|
313
|
+
|
|
314
|
+
# If this is preceded by a digit and space, it's likely a "7 December 2012" format
|
|
315
|
+
# which should be handled by DayMonthYearFormat instead
|
|
316
|
+
if start_pos >= 2 and match.string[start_pos - 2 : start_pos].strip().isdigit():
|
|
317
|
+
return None
|
|
318
|
+
|
|
319
|
+
month_str, year, bc = match.groups()
|
|
320
|
+
month = cls.convert_month_to_number(month_str)
|
|
321
|
+
if bc:
|
|
322
|
+
year = -int(year)
|
|
323
|
+
return Date(year=int(year), month=month, day=None)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
class YearFormat(DateFormat):
|
|
327
|
+
"""Format for YYYY dates."""
|
|
328
|
+
|
|
329
|
+
name = "YEAR"
|
|
330
|
+
pattern = re.compile(
|
|
331
|
+
r"\b(?:c\.|in|from|to)\s*(?:AD\s*)?(\d{1,4})(?:\s*(BC|BCE))?[\s,\.,\)]",
|
|
332
|
+
re.IGNORECASE,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
@classmethod
|
|
336
|
+
def match_to_date(cls, match: re.Match) -> Date:
|
|
337
|
+
year, bc = match.groups()
|
|
338
|
+
year = int(year)
|
|
339
|
+
if bc:
|
|
340
|
+
year = -year
|
|
341
|
+
return Date(year=year, month=None, day=None)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
class WrittenDateFormat(DateFormat):
|
|
345
|
+
"""Format for Month the day, year dates."""
|
|
346
|
+
|
|
347
|
+
name = "WRITTEN_DATE"
|
|
348
|
+
pattern = re.compile(
|
|
349
|
+
rf"\b({_MONTHS_PATTERN})\s+the\s+(?:(\d{{1,2}})(?:st|nd|rd|th)?|([a-z]+))[,\s]+(\d{{1,4}})+(?:\s+(BC|BCE))?\b",
|
|
350
|
+
re.IGNORECASE,
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
@classmethod
|
|
354
|
+
def match_to_date(cls, match: re.Match) -> datetime:
|
|
355
|
+
groups = match.groups()
|
|
356
|
+
month_str = groups[0]
|
|
357
|
+
|
|
358
|
+
# Check if the day is a number or written out
|
|
359
|
+
if groups[1] is not None: # Numeric day like "the 15th"
|
|
360
|
+
day = int(groups[1])
|
|
361
|
+
else: # Written day like "the third"
|
|
362
|
+
written_day = groups[2].lower()
|
|
363
|
+
if written_day in _WRITTEN_NUMBERS:
|
|
364
|
+
day = _WRITTEN_NUMBERS[written_day]
|
|
365
|
+
else:
|
|
366
|
+
raise ValueError(f"Unsupported written day number: {match.group(0)}")
|
|
367
|
+
|
|
368
|
+
year = int(groups[3])
|
|
369
|
+
if groups[4]:
|
|
370
|
+
year = -year
|
|
371
|
+
month = cls.convert_month_to_number(month_str)
|
|
372
|
+
|
|
373
|
+
return Date(year, month, day)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
class WikiDateFormat(DateFormat):
|
|
377
|
+
"""Format for {{Birth date|YYYY|MM|DD|...}}."""
|
|
378
|
+
|
|
379
|
+
name = "WIKI_BIRTH_DATE"
|
|
380
|
+
pattern = re.compile(r"{{[^|]*\|(\d{1,4})\|(\d{1,2})\|(\d{1,2}).*}}", re.IGNORECASE)
|
|
381
|
+
|
|
382
|
+
@classmethod
|
|
383
|
+
def match_to_date(cls, match: re.Match) -> datetime:
|
|
384
|
+
year, month, day = match.groups()
|
|
385
|
+
# Check if the template is a birth date template
|
|
386
|
+
return Date(int(year), int(month), int(day))
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
# Register all date format handlers
|
|
390
|
+
_DATE_FORMATS = [
|
|
391
|
+
SlashDMYMDYFormat,
|
|
392
|
+
DashYMDFormat,
|
|
393
|
+
DayMonthYearFormat,
|
|
394
|
+
MonthDayYearFormat,
|
|
395
|
+
MonthYearFormat,
|
|
396
|
+
WrittenDateFormat,
|
|
397
|
+
WikiDateFormat,
|
|
398
|
+
YearFormat,
|
|
399
|
+
]
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def extract_dates(text: str) -> List[Dict]:
|
|
403
|
+
"""Extract dates from text with context information.
|
|
404
|
+
|
|
405
|
+
Parameters
|
|
406
|
+
----------
|
|
407
|
+
text : str
|
|
408
|
+
The text to extract dates from.
|
|
409
|
+
|
|
410
|
+
Returns
|
|
411
|
+
-------
|
|
412
|
+
List[Dict]
|
|
413
|
+
A list of dictionaries containing:
|
|
414
|
+
- 'date_str': The original date string found
|
|
415
|
+
- 'format': The name of the date format
|
|
416
|
+
- 'datetime': The parsed datetime object (if parsing was successful)
|
|
417
|
+
"""
|
|
418
|
+
all_results = []
|
|
419
|
+
all_errors = []
|
|
420
|
+
for date_format in _DATE_FORMATS:
|
|
421
|
+
results, errors = date_format.list_dates(text)
|
|
422
|
+
all_results.extend(results)
|
|
423
|
+
all_errors.extend(errors)
|
|
424
|
+
return all_results, all_errors
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
@dataclass
|
|
428
|
+
class DateRange:
|
|
429
|
+
start: Date
|
|
430
|
+
end: Date
|
|
431
|
+
|
|
432
|
+
def to_string(self) -> str:
|
|
433
|
+
start = self.start.to_string()
|
|
434
|
+
end = self.end.to_string()
|
|
435
|
+
if self.start.is_approximate:
|
|
436
|
+
start = f"~{start}"
|
|
437
|
+
if self.end.is_approximate:
|
|
438
|
+
end = f"~{end}"
|
|
439
|
+
return f"{start} - {end}"
|
|
440
|
+
|
|
441
|
+
@classmethod
|
|
442
|
+
def from_parsed_string(cls, date: str) -> "DateRange":
|
|
443
|
+
"""
|
|
444
|
+
Parse a string representation of a date or date range into a DateRange object.
|
|
445
|
+
|
|
446
|
+
Examples:
|
|
447
|
+
1810 -> ~1810/01/01 - ~1810/12/31
|
|
448
|
+
1810-1812 -> ~1810/01/01 - ~1812/12/31
|
|
449
|
+
1810/1812 -> ~1810/01/01 - ~1812/12/31
|
|
450
|
+
1810/03/05 -> 1810/03/05 - 1810/03/05
|
|
451
|
+
1810/03 -> ~1810/03/01 - ~1810/03/31
|
|
452
|
+
1810/03 - 1812/05 -> ~1810/03/01 - ~1812/05/31
|
|
453
|
+
1810/03/05 - 1812/05/07 -> 1810/03/05 - 1812/05/07
|
|
454
|
+
1611/1612 - 1615/1617 -> ~1611/01/01 - ~1617/12/31
|
|
455
|
+
1930s - 1940s -> ~1930/01/01 - ~1949/12/31
|
|
456
|
+
1930s -> ~1930/01/01 - ~1939/12/31
|
|
457
|
+
"""
|
|
458
|
+
date = date.strip()
|
|
459
|
+
|
|
460
|
+
if "-" in date:
|
|
461
|
+
start_str, end_str = date.split("-")
|
|
462
|
+
start_range = cls.from_parsed_string(start_str)
|
|
463
|
+
end_range = cls.from_parsed_string(end_str)
|
|
464
|
+
return DateRange(start=start_range.start, end=end_range.end)
|
|
465
|
+
|
|
466
|
+
# Replace YYYY BC with negative year
|
|
467
|
+
date = re.sub(r"\b(\d{1,4})\s*BC\b", r"-\1", date)
|
|
468
|
+
|
|
469
|
+
match date:
|
|
470
|
+
case _ if match := re.match(r"^-?\d{1,4}$", date):
|
|
471
|
+
# Single year (e.g., "1810")
|
|
472
|
+
year = int(match.group(0))
|
|
473
|
+
return DateRange(
|
|
474
|
+
start=Date(year, 1, 1, is_approximate=True),
|
|
475
|
+
end=Date(year, 12, 31, is_approximate=True),
|
|
476
|
+
)
|
|
477
|
+
case _ if match := re.match(r"^(\d{1,3}0)s$", date):
|
|
478
|
+
# Decade (e.g., "1930s")
|
|
479
|
+
decade_start = int(match.group(1))
|
|
480
|
+
return DateRange(
|
|
481
|
+
start=Date(decade_start, 1, 1, is_approximate=True),
|
|
482
|
+
end=Date(decade_start + 9, 12, 31, is_approximate=True),
|
|
483
|
+
)
|
|
484
|
+
case _ if match := re.match(r"^(-?\d{1,4})/(-?\d{1,4})$", date):
|
|
485
|
+
# Year range (e.g., "1810/1812")
|
|
486
|
+
|
|
487
|
+
# Only treat as year/year if the second number is > 12 (not a month)
|
|
488
|
+
if not 1 <= int(match.group(2)) <= 12:
|
|
489
|
+
# Year range (e.g., "1810/1812")
|
|
490
|
+
start_year, end_year = map(int, match.groups())
|
|
491
|
+
return DateRange(
|
|
492
|
+
start=Date(start_year, 1, 1, is_approximate=True),
|
|
493
|
+
end=Date(end_year, 12, 31, is_approximate=True),
|
|
494
|
+
)
|
|
495
|
+
else:
|
|
496
|
+
year, month = map(int, match.groups())
|
|
497
|
+
# Get last day of month
|
|
498
|
+
if month == 12:
|
|
499
|
+
last_day = 31
|
|
500
|
+
elif month in [4, 6, 9, 11]:
|
|
501
|
+
last_day = 30
|
|
502
|
+
elif month == 2:
|
|
503
|
+
# Simple leap year calculation
|
|
504
|
+
last_day = (
|
|
505
|
+
29
|
|
506
|
+
if (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0)
|
|
507
|
+
else 28
|
|
508
|
+
)
|
|
509
|
+
else:
|
|
510
|
+
last_day = 31
|
|
511
|
+
return DateRange(
|
|
512
|
+
start=Date(year, month, 1, is_approximate=True),
|
|
513
|
+
end=Date(year, month, last_day, is_approximate=True),
|
|
514
|
+
)
|
|
515
|
+
case _ if match := re.match(r"^(-?\d{1,4})/(\d{1,2})/(\d{1,2})$", date):
|
|
516
|
+
# Full date (e.g., "1810/03/05")
|
|
517
|
+
year, month, day = map(int, match.groups())
|
|
518
|
+
return DateRange(
|
|
519
|
+
start=Date(year, month, day, is_approximate=False),
|
|
520
|
+
end=Date(year, month, day, is_approximate=False),
|
|
521
|
+
)
|
|
522
|
+
case _:
|
|
523
|
+
raise ValueError(f"Unsupported date format: {date}")
|
|
524
|
+
|
|
525
|
+
def to_dict(self) -> Dict:
|
|
526
|
+
return {
|
|
527
|
+
"start": self.start.to_dict(),
|
|
528
|
+
"end": self.end.to_dict(),
|
|
529
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import urllib.request
|
|
2
|
+
import os
|
|
3
|
+
from tqdm.auto import tqdm
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def download_file(url, filepath, replace=False):
|
|
7
|
+
"""Download a web file to a filepath, with the option to skip."""
|
|
8
|
+
if os.path.exists(filepath) and not replace:
|
|
9
|
+
print(f"{filepath} already exists, skipping download.")
|
|
10
|
+
return
|
|
11
|
+
|
|
12
|
+
print(f"Downloading {filepath} from {url}...")
|
|
13
|
+
|
|
14
|
+
# Get file size for progress bar
|
|
15
|
+
response = urllib.request.urlopen(url)
|
|
16
|
+
total_size = int(response.headers.get('content-length', 0))
|
|
17
|
+
|
|
18
|
+
# Create progress bar instance
|
|
19
|
+
progress_bar = tqdm(total=total_size, unit='iB', unit_scale=True)
|
|
20
|
+
|
|
21
|
+
def reporthook(blocknum, blocksize, totalsize):
|
|
22
|
+
progress_bar.update(blocksize)
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
urllib.request.urlretrieve(url, filepath, reporthook=reporthook)
|
|
26
|
+
finally:
|
|
27
|
+
progress_bar.close()
|
|
28
|
+
|
|
29
|
+
print("Download complete")
|