technologydata 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.
- technologydata/__init__.py +41 -0
- technologydata/constants/__init__.py +12 -0
- technologydata/constants/energy_density.py +324 -0
- technologydata/datapackage.py +145 -0
- technologydata/package_data/dea_energy_storage/dea_energy_storage.py +691 -0
- technologydata/package_data/dea_energy_storage/sources.json +12 -0
- technologydata/package_data/dea_energy_storage/technologies.json +17508 -0
- technologydata/package_data/raw/Technology_datasheet_for_energy_storage.xlsx +0 -0
- technologydata/parameter.py +662 -0
- technologydata/source.py +430 -0
- technologydata/source_collection.py +243 -0
- technologydata/technologies/__init__.py +5 -0
- technologydata/technologies/growth_models.py +503 -0
- technologydata/technology.py +191 -0
- technologydata/technology_collection.py +466 -0
- technologydata/utils/__init__.py +13 -0
- technologydata/utils/carriers.txt +33 -0
- technologydata/utils/commons.py +310 -0
- technologydata/utils/heating_values.txt +17 -0
- technologydata/utils/units.py +519 -0
- technologydata-0.1.0.dist-info/METADATA +135 -0
- technologydata-0.1.0.dist-info/RECORD +25 -0
- technologydata-0.1.0.dist-info/WHEEL +5 -0
- technologydata-0.1.0.dist-info/licenses/LICENSE +18 -0
- technologydata-0.1.0.dist-info/top_level.txt +1 -0
technologydata/source.py
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: technologydata contributors
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Source class for representing bibliographic and web sources, with archiving support.
|
|
7
|
+
|
|
8
|
+
Examples
|
|
9
|
+
--------
|
|
10
|
+
>>> src = Source(title="Example Source", authors="The Authors")
|
|
11
|
+
>>> src._store_in_wayback()
|
|
12
|
+
>>> src.retrieve_from_wayback()
|
|
13
|
+
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
import pathlib
|
|
18
|
+
from typing import Annotated, Any
|
|
19
|
+
|
|
20
|
+
import pydantic
|
|
21
|
+
import requests
|
|
22
|
+
import savepagenow
|
|
23
|
+
|
|
24
|
+
import technologydata
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Source(pydantic.BaseModel):
|
|
30
|
+
"""
|
|
31
|
+
Represent a data source, including bibliographic and web information.
|
|
32
|
+
|
|
33
|
+
Attributes
|
|
34
|
+
----------
|
|
35
|
+
title : str
|
|
36
|
+
Title of the source.
|
|
37
|
+
authors : str
|
|
38
|
+
Authors of the source.
|
|
39
|
+
url : Optional[str]
|
|
40
|
+
URL of the source.
|
|
41
|
+
url_archive : Optional[str]
|
|
42
|
+
Archived URL.
|
|
43
|
+
url_date : Optional[str]
|
|
44
|
+
Date the URL was accessed.
|
|
45
|
+
url_date_archive : Optional[str]
|
|
46
|
+
Date the URL was archived.
|
|
47
|
+
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
title: Annotated[str, pydantic.Field(description="Title of the source.")]
|
|
51
|
+
authors: Annotated[str, pydantic.Field(description="Authors of the source.")]
|
|
52
|
+
url: Annotated[str | None, pydantic.Field(description="URL of the source.")] = None
|
|
53
|
+
url_archive: Annotated[str | None, pydantic.Field(description="Archived URL.")] = (
|
|
54
|
+
None
|
|
55
|
+
)
|
|
56
|
+
url_date: Annotated[
|
|
57
|
+
str | None, pydantic.Field(description="Date the URL was accessed.")
|
|
58
|
+
] = None
|
|
59
|
+
url_date_archive: Annotated[
|
|
60
|
+
str | None, pydantic.Field(description="Date the URL was archived.")
|
|
61
|
+
] = None
|
|
62
|
+
|
|
63
|
+
def __eq__(self, other: object) -> bool:
|
|
64
|
+
"""
|
|
65
|
+
Check for equality with another Source object.
|
|
66
|
+
|
|
67
|
+
Compares all attributes of the current instance with those of the other object.
|
|
68
|
+
|
|
69
|
+
Parameters
|
|
70
|
+
----------
|
|
71
|
+
other : object
|
|
72
|
+
The object to compare with. Expected to be an instance of Source.
|
|
73
|
+
|
|
74
|
+
Returns
|
|
75
|
+
-------
|
|
76
|
+
bool
|
|
77
|
+
True if all non-None attributes are equal between self and other, False otherwise.
|
|
78
|
+
Returns False if other is not a Source instance.
|
|
79
|
+
|
|
80
|
+
"""
|
|
81
|
+
if not isinstance(other, Source):
|
|
82
|
+
return NotImplemented
|
|
83
|
+
|
|
84
|
+
if not isinstance(other, Source):
|
|
85
|
+
logger.error("The object is not a Source instance.")
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
for field in self.__class__.model_fields.keys():
|
|
89
|
+
value_self = getattr(self, field)
|
|
90
|
+
value_other = getattr(other, field)
|
|
91
|
+
if value_self != value_other:
|
|
92
|
+
return False
|
|
93
|
+
return True
|
|
94
|
+
|
|
95
|
+
def __hash__(self) -> int:
|
|
96
|
+
"""
|
|
97
|
+
Return a hash value for the Source instance based on all attributes.
|
|
98
|
+
|
|
99
|
+
This method computes a combined hash of the instance's attributes to
|
|
100
|
+
uniquely identify the object in hash-based collections such as sets and dictionaries.
|
|
101
|
+
|
|
102
|
+
Returns
|
|
103
|
+
-------
|
|
104
|
+
int
|
|
105
|
+
The hash value of the Source instance.
|
|
106
|
+
|
|
107
|
+
"""
|
|
108
|
+
# Retrieve all attribute values dynamically
|
|
109
|
+
attribute_values = tuple(
|
|
110
|
+
getattr(self, field) for field in self.__class__.model_fields.keys()
|
|
111
|
+
)
|
|
112
|
+
return hash(attribute_values)
|
|
113
|
+
|
|
114
|
+
def __str__(self) -> str:
|
|
115
|
+
"""
|
|
116
|
+
Return a string representation of the Source, including all available attributes.
|
|
117
|
+
|
|
118
|
+
Returns
|
|
119
|
+
-------
|
|
120
|
+
str
|
|
121
|
+
A string detailing the source's information.
|
|
122
|
+
|
|
123
|
+
"""
|
|
124
|
+
parts = [f"'{self.authors}': '{self.title}'"]
|
|
125
|
+
if self.url:
|
|
126
|
+
parts.append(f"from url '{self.url}'")
|
|
127
|
+
if self.url_date:
|
|
128
|
+
parts.append(f"last accessed on '{self.url_date}'")
|
|
129
|
+
if self.url_archive:
|
|
130
|
+
parts.append(f"archived at '{self.url_archive}'")
|
|
131
|
+
if self.url_date_archive:
|
|
132
|
+
parts.append(f"on '{self.url_date_archive}'.")
|
|
133
|
+
return ", ".join(parts)
|
|
134
|
+
|
|
135
|
+
def ensure_in_wayback(self) -> None:
|
|
136
|
+
"""
|
|
137
|
+
Ensure that the source URL is archived in the Wayback Machine.
|
|
138
|
+
|
|
139
|
+
This method checks if the source's `url` attribute is set and whether
|
|
140
|
+
an archived URL or archive date is already present. If neither is available, it attempts to archive the
|
|
141
|
+
URL using the Wayback Machine and updates the corresponding attributes.
|
|
142
|
+
|
|
143
|
+
Returns
|
|
144
|
+
-------
|
|
145
|
+
None
|
|
146
|
+
This method updates the Source object's `url_archive` and `url_date_archive` attributes in place.
|
|
147
|
+
|
|
148
|
+
Raises
|
|
149
|
+
------
|
|
150
|
+
ValueError
|
|
151
|
+
If the `url` attribute is not set (None or NaN).
|
|
152
|
+
|
|
153
|
+
Examples
|
|
154
|
+
--------
|
|
155
|
+
>>> from technologydata import Source
|
|
156
|
+
>>> source = Source(url="http://example.com", title="Example Site", authors="The Authors")
|
|
157
|
+
>>> source.ensure_in_wayback()
|
|
158
|
+
A new snapshot has been stored for the url http://example.com with timestamp 2023-10-01T12:00:00Z and Archive.org url http://web.archive.org/web/20231001120000/http://example.com.
|
|
159
|
+
>>> source.url_archive
|
|
160
|
+
'http://web.archive.org/web/20231001120000/http://example.com'
|
|
161
|
+
>>> source.url_date_archive
|
|
162
|
+
'2023-10-01T12:00:00Z'
|
|
163
|
+
|
|
164
|
+
"""
|
|
165
|
+
if self.url is None:
|
|
166
|
+
raise ValueError(
|
|
167
|
+
f"The url attribute of the source {self.title} is not set or contains a NaN value."
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
if self.url_archive is None and self.url_date_archive is None:
|
|
171
|
+
archived_info = self._store_in_wayback(self.url)
|
|
172
|
+
if archived_info is not None:
|
|
173
|
+
archived_url, new_capture_flag, timestamp = archived_info
|
|
174
|
+
if new_capture_flag:
|
|
175
|
+
logger.info(
|
|
176
|
+
f"A new snapshot has been stored for the url {self.url} with timestamp {timestamp} and Archive.org url {archived_url}."
|
|
177
|
+
)
|
|
178
|
+
else:
|
|
179
|
+
logger.info(
|
|
180
|
+
f"There is already a snapshot for the url {self.url} with timestamp {timestamp} and Archive.org url {archived_url}."
|
|
181
|
+
)
|
|
182
|
+
self.url_date_archive = timestamp
|
|
183
|
+
self.url_archive = archived_url
|
|
184
|
+
|
|
185
|
+
@staticmethod
|
|
186
|
+
def _store_in_wayback(
|
|
187
|
+
url_to_archive: str,
|
|
188
|
+
) -> tuple[Any, bool | None, str | None] | None:
|
|
189
|
+
"""
|
|
190
|
+
Store a snapshot of the given URL on the Wayback Machine and extract the timestamp.
|
|
191
|
+
|
|
192
|
+
The method captures the specified URL using the Wayback Machine and retrieves the
|
|
193
|
+
corresponding archive URL along with a formatted timestamp. The timestamp is extracted
|
|
194
|
+
from the archive URL and converted to a more readable format.
|
|
195
|
+
|
|
196
|
+
Parameters
|
|
197
|
+
----------
|
|
198
|
+
url_to_archive : str
|
|
199
|
+
The URL that you want to archive on the Wayback Machine.
|
|
200
|
+
|
|
201
|
+
Returns
|
|
202
|
+
-------
|
|
203
|
+
tuple[str, bool, str] | None
|
|
204
|
+
A tuple containing the archive URL, a boolean indicating if a new capture was conducted (if the boolean is
|
|
205
|
+
True, archive.org conducted a new capture. If it is False, archive.org has returned a recently cached capture
|
|
206
|
+
instead, likely taken in the previous minutes) and the formatted timestamp (with format YYYY-MM-DD hh:mm:ss)
|
|
207
|
+
if the operation is successful. Returns None if the timestamp cannot be extracted due to a ValueError (e.g.,
|
|
208
|
+
if the expected substrings are not found in the archive URL).
|
|
209
|
+
|
|
210
|
+
Examples
|
|
211
|
+
--------
|
|
212
|
+
>>> from technologydata import Source
|
|
213
|
+
>>> some_url = "some_url"
|
|
214
|
+
>>> archived_info = Source._store_in_wayback(some_url)
|
|
215
|
+
|
|
216
|
+
"""
|
|
217
|
+
archive_url = savepagenow.capture_or_cache(url_to_archive)
|
|
218
|
+
try:
|
|
219
|
+
# The timestamp is between "web/" and the next "/" afterward
|
|
220
|
+
# Find the starting index of "web/"
|
|
221
|
+
start_index = archive_url[0].index("web/") + len("web/")
|
|
222
|
+
# Find the ending index of the timestamp by locating the next "/" after the start_index
|
|
223
|
+
end_index = archive_url[0].index("/", start_index)
|
|
224
|
+
# Extract the timestamp substring
|
|
225
|
+
timestamp = archive_url[0][start_index:end_index]
|
|
226
|
+
output_timestamp = technologydata.Commons.change_datetime_format(
|
|
227
|
+
timestamp,
|
|
228
|
+
technologydata.DateFormatEnum.SOURCES_CSV,
|
|
229
|
+
)
|
|
230
|
+
return archive_url[0], archive_url[1], output_timestamp
|
|
231
|
+
except ValueError:
|
|
232
|
+
# If "web/" or next "/" not found, return empty string
|
|
233
|
+
return None
|
|
234
|
+
|
|
235
|
+
def retrieve_from_wayback(
|
|
236
|
+
self, download_directory: pathlib.Path
|
|
237
|
+
) -> pathlib.Path | None:
|
|
238
|
+
"""
|
|
239
|
+
Download a file from the Wayback Machine and save it to a specified path.
|
|
240
|
+
|
|
241
|
+
The method retrieves an archived file from the Wayback Machine using the URL
|
|
242
|
+
from the url_archive attribute of the instance. The file is saved in the
|
|
243
|
+
specified format based on its Content-Type field in the Response Header or the extension
|
|
244
|
+
that can be extracted from the URL.
|
|
245
|
+
|
|
246
|
+
Parameters
|
|
247
|
+
----------
|
|
248
|
+
download_directory : pathlib.Path
|
|
249
|
+
The base path where the file will be saved.
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
Returns
|
|
253
|
+
-------
|
|
254
|
+
pathlib.Path | None
|
|
255
|
+
The specified path where the file is stored, or None if an error occurs.
|
|
256
|
+
|
|
257
|
+
Raises
|
|
258
|
+
------
|
|
259
|
+
requests.exceptions.RequestException
|
|
260
|
+
If there is an issue with the HTTP request.
|
|
261
|
+
|
|
262
|
+
Notes
|
|
263
|
+
-----
|
|
264
|
+
- The attribute "url_archived" should contain a valid URL.
|
|
265
|
+
|
|
266
|
+
Examples
|
|
267
|
+
--------
|
|
268
|
+
>>> from technologydata import Source
|
|
269
|
+
>>> source = Source(title="example01", authors="The Authors")
|
|
270
|
+
>>> output_path = source.retrieve_from_wayback(pathlib.Path("base_path"))
|
|
271
|
+
|
|
272
|
+
"""
|
|
273
|
+
if self.url_archive is None:
|
|
274
|
+
logger.error(
|
|
275
|
+
f"The url_archive attribute of source {self.title} is not set."
|
|
276
|
+
)
|
|
277
|
+
return None
|
|
278
|
+
if download_directory is None:
|
|
279
|
+
logger.error(f"The base path of the source {self.title} is not set.")
|
|
280
|
+
return None
|
|
281
|
+
|
|
282
|
+
source_title = technologydata.Commons.replace_special_characters(self.title)
|
|
283
|
+
save_path = self._get_save_path(
|
|
284
|
+
self.url_archive, download_directory, source_title
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
if save_path is None:
|
|
288
|
+
logger.warning(
|
|
289
|
+
f"It was not possible to determine a file path for the source {source_title}."
|
|
290
|
+
)
|
|
291
|
+
return None
|
|
292
|
+
|
|
293
|
+
if save_path.is_file():
|
|
294
|
+
logger.warning(
|
|
295
|
+
f"There is already a file stored at the path {save_path}. Not downloading or overwriting this file."
|
|
296
|
+
)
|
|
297
|
+
return None
|
|
298
|
+
|
|
299
|
+
storage_path = self._download_file(self.url_archive, save_path)
|
|
300
|
+
return storage_path
|
|
301
|
+
|
|
302
|
+
@staticmethod
|
|
303
|
+
def _get_save_path(
|
|
304
|
+
url_archived: str, source_path: pathlib.Path, source_title: str
|
|
305
|
+
) -> pathlib.Path | None:
|
|
306
|
+
"""
|
|
307
|
+
Determine the save path based on the content type or archived URL.
|
|
308
|
+
|
|
309
|
+
The method retrieves the content type of the archived URL and determines the appropriate
|
|
310
|
+
file extension based on the content type or based on the archived URL. It constructs the full save path using
|
|
311
|
+
the provided source path and source title.
|
|
312
|
+
|
|
313
|
+
Parameters
|
|
314
|
+
----------
|
|
315
|
+
url_archived : str
|
|
316
|
+
The URL of the archived file from which the content type will be determined.
|
|
317
|
+
source_path : pathlib.Path
|
|
318
|
+
The base path where the file will be saved.
|
|
319
|
+
source_title : str
|
|
320
|
+
The title of the given source from sources.csv, used as the filename.
|
|
321
|
+
|
|
322
|
+
Returns
|
|
323
|
+
-------
|
|
324
|
+
pathlib.Path | None
|
|
325
|
+
The full path where the file should be saved, including the appropriate file extension,
|
|
326
|
+
or None if the content type is unsupported or an error occurs.
|
|
327
|
+
|
|
328
|
+
Raises
|
|
329
|
+
------
|
|
330
|
+
ValueError
|
|
331
|
+
If the extension is not among the supported ones.
|
|
332
|
+
|
|
333
|
+
"""
|
|
334
|
+
content_type = Source._get_content_type(url_archived)
|
|
335
|
+
if content_type is None:
|
|
336
|
+
return None
|
|
337
|
+
|
|
338
|
+
extension = technologydata.FileExtensionEnum.get_extension(
|
|
339
|
+
content_type
|
|
340
|
+
) or technologydata.FileExtensionEnum.search_file_extension_in_url(url_archived)
|
|
341
|
+
if extension is None:
|
|
342
|
+
raise ValueError(
|
|
343
|
+
f"Unable to infer file extension from content type: {content_type} or URL: {url_archived}"
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
if source_path is not None and source_title is not None:
|
|
347
|
+
return pathlib.Path(source_path, source_title + extension)
|
|
348
|
+
else:
|
|
349
|
+
return None
|
|
350
|
+
|
|
351
|
+
@staticmethod
|
|
352
|
+
def _get_content_type(url_archived: str) -> Any:
|
|
353
|
+
"""
|
|
354
|
+
Fetch the content type of the archived URL.
|
|
355
|
+
|
|
356
|
+
The method sends a HEAD request to the specified archived URL to retrieve the
|
|
357
|
+
Content-Type from the response headers. It returns the content type as a string
|
|
358
|
+
if the request is successful; otherwise, it logs an error and returns None.
|
|
359
|
+
|
|
360
|
+
Parameters
|
|
361
|
+
----------
|
|
362
|
+
url_archived : str
|
|
363
|
+
The URL of the archived resource for which the content type is to be fetched.
|
|
364
|
+
|
|
365
|
+
Returns
|
|
366
|
+
-------
|
|
367
|
+
str | None
|
|
368
|
+
The Content-Type of the archived URL if the request is successful, or None
|
|
369
|
+
if an error occurs during the request.
|
|
370
|
+
|
|
371
|
+
Raises
|
|
372
|
+
------
|
|
373
|
+
requests.exceptions.RequestException
|
|
374
|
+
If there is an issue with the HTTP request, an error is logged, and None is returned.
|
|
375
|
+
|
|
376
|
+
"""
|
|
377
|
+
try:
|
|
378
|
+
response = requests.head(url_archived)
|
|
379
|
+
response.raise_for_status()
|
|
380
|
+
return response.headers.get("Content-Type")
|
|
381
|
+
except requests.exceptions.RequestException as e:
|
|
382
|
+
raise requests.exceptions.RequestException(
|
|
383
|
+
f"Failed to retrieve content type: {e}"
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
@staticmethod
|
|
387
|
+
def _download_file(
|
|
388
|
+
url_archived: str, save_path: pathlib.Path
|
|
389
|
+
) -> pathlib.Path | None:
|
|
390
|
+
"""
|
|
391
|
+
Download the file and save it to the specified path.
|
|
392
|
+
|
|
393
|
+
The method retrieves the content from the specified archived URL and saves it
|
|
394
|
+
to the provided file path. It handles HTTP errors and logs appropriate messages
|
|
395
|
+
based on the outcome of the download operation.
|
|
396
|
+
|
|
397
|
+
Parameters
|
|
398
|
+
----------
|
|
399
|
+
url_archived : str
|
|
400
|
+
The URL of the archived file to be downloaded.
|
|
401
|
+
|
|
402
|
+
save_path : pathlib.Path
|
|
403
|
+
The path where the downloaded file will be saved, including the file name.
|
|
404
|
+
|
|
405
|
+
Returns
|
|
406
|
+
-------
|
|
407
|
+
pathlib.Path | None
|
|
408
|
+
The path where the file has been saved if the download is successful, or None
|
|
409
|
+
if an error occurs during the download process.
|
|
410
|
+
|
|
411
|
+
Raises
|
|
412
|
+
------
|
|
413
|
+
requests.exceptions.RequestException
|
|
414
|
+
If there is an issue with the HTTP request, an error is logged, and None is returned.
|
|
415
|
+
|
|
416
|
+
"""
|
|
417
|
+
try:
|
|
418
|
+
response = requests.get(url_archived)
|
|
419
|
+
response.raise_for_status() # Check for HTTP errors
|
|
420
|
+
|
|
421
|
+
with open(save_path, "wb") as file:
|
|
422
|
+
file.write(response.content)
|
|
423
|
+
|
|
424
|
+
logger.info(f"File downloaded successfully and saved to {save_path}")
|
|
425
|
+
return save_path
|
|
426
|
+
except requests.exceptions.RequestException as e:
|
|
427
|
+
requests.exceptions.RequestException(
|
|
428
|
+
f"An error occurred during file download: {e}"
|
|
429
|
+
)
|
|
430
|
+
return None
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: technologydata contributors
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
"""SourceCollection class for representing an iterable of Source Objects."""
|
|
6
|
+
|
|
7
|
+
import csv
|
|
8
|
+
import json
|
|
9
|
+
import pathlib
|
|
10
|
+
import re
|
|
11
|
+
from collections.abc import Iterator
|
|
12
|
+
from typing import Annotated, Self
|
|
13
|
+
|
|
14
|
+
import pandas
|
|
15
|
+
import pydantic
|
|
16
|
+
import pydantic_core
|
|
17
|
+
|
|
18
|
+
from technologydata.source import Source
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SourceCollection(pydantic.BaseModel):
|
|
22
|
+
"""
|
|
23
|
+
Represent a collection of sources.
|
|
24
|
+
|
|
25
|
+
Attributes
|
|
26
|
+
----------
|
|
27
|
+
sources : List[Source]
|
|
28
|
+
List of Source objects.
|
|
29
|
+
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
sources: Annotated[
|
|
33
|
+
list[Source], pydantic.Field(description="List of Source objects.")
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
def __iter__(self) -> Iterator["Source"]: # type: ignore
|
|
37
|
+
"""
|
|
38
|
+
Return an iterator over the list of Source objects.
|
|
39
|
+
|
|
40
|
+
Returns
|
|
41
|
+
-------
|
|
42
|
+
Iterator[Source]
|
|
43
|
+
An iterator over the Source objects contained in the collection.
|
|
44
|
+
|
|
45
|
+
"""
|
|
46
|
+
return iter(self.sources)
|
|
47
|
+
|
|
48
|
+
def __len__(self) -> int:
|
|
49
|
+
"""
|
|
50
|
+
Return the number of sources in this collection.
|
|
51
|
+
|
|
52
|
+
Returns
|
|
53
|
+
-------
|
|
54
|
+
int
|
|
55
|
+
The number of Source objects in the sources list.
|
|
56
|
+
|
|
57
|
+
"""
|
|
58
|
+
return len(self.sources)
|
|
59
|
+
|
|
60
|
+
def __str__(self) -> str:
|
|
61
|
+
"""
|
|
62
|
+
Return a string representation of the SourceCollection.
|
|
63
|
+
|
|
64
|
+
Returns
|
|
65
|
+
-------
|
|
66
|
+
str
|
|
67
|
+
A string representation of the SourceCollection, showing the number of sources.
|
|
68
|
+
|
|
69
|
+
"""
|
|
70
|
+
sources_str = ", ".join(str(source) for source in self.sources)
|
|
71
|
+
return f"SourceCollection with {len(self.sources)} sources: {sources_str}"
|
|
72
|
+
|
|
73
|
+
def get(self, title: str, authors: str) -> Self:
|
|
74
|
+
"""
|
|
75
|
+
Filter sources based on regex patterns for non-optional attributes.
|
|
76
|
+
|
|
77
|
+
Parameters
|
|
78
|
+
----------
|
|
79
|
+
title : str
|
|
80
|
+
Regex pattern to filter titles.
|
|
81
|
+
authors : str
|
|
82
|
+
Regex pattern to filter authors.
|
|
83
|
+
|
|
84
|
+
Returns
|
|
85
|
+
-------
|
|
86
|
+
SourceCollection
|
|
87
|
+
A new SourceCollection with filtered sources.
|
|
88
|
+
|
|
89
|
+
"""
|
|
90
|
+
filtered_sources = self.sources
|
|
91
|
+
|
|
92
|
+
if title is not None:
|
|
93
|
+
pattern_title = re.compile(title, re.IGNORECASE)
|
|
94
|
+
filtered_sources = [
|
|
95
|
+
s for s in filtered_sources if pattern_title.search(s.title)
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
if authors is not None:
|
|
99
|
+
pattern_authors = re.compile(authors, re.IGNORECASE)
|
|
100
|
+
filtered_sources = [
|
|
101
|
+
s for s in filtered_sources if pattern_authors.search(s.authors)
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
return SourceCollection(sources=filtered_sources) # type: ignore
|
|
105
|
+
|
|
106
|
+
def retrieve_all_from_wayback(
|
|
107
|
+
self, download_directory: pathlib.Path
|
|
108
|
+
) -> list[pathlib.Path | None]:
|
|
109
|
+
"""
|
|
110
|
+
Download archived files for all sources in the collection using retrieve_from_wayback.
|
|
111
|
+
|
|
112
|
+
Parameters
|
|
113
|
+
----------
|
|
114
|
+
download_directory : pathlib.Path
|
|
115
|
+
The base directory where all files will be saved.
|
|
116
|
+
|
|
117
|
+
Returns
|
|
118
|
+
-------
|
|
119
|
+
list[pathlib.Path | None]
|
|
120
|
+
List of paths where each file was stored, or None if download failed for a source.
|
|
121
|
+
|
|
122
|
+
"""
|
|
123
|
+
return [
|
|
124
|
+
source.retrieve_from_wayback(download_directory) for source in self.sources
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
def to_dataframe(self) -> pandas.DataFrame:
|
|
128
|
+
"""
|
|
129
|
+
Convert the SourceCollection to a pandas DataFrame.
|
|
130
|
+
|
|
131
|
+
Returns
|
|
132
|
+
-------
|
|
133
|
+
pd.DataFrame
|
|
134
|
+
A DataFrame containing the source data.
|
|
135
|
+
|
|
136
|
+
"""
|
|
137
|
+
return pandas.DataFrame([source.model_dump() for source in self.sources])
|
|
138
|
+
|
|
139
|
+
def to_csv(self, **kwargs: pathlib.Path | str | bool) -> None:
|
|
140
|
+
"""
|
|
141
|
+
Export the SourceCollection to a CSV file.
|
|
142
|
+
|
|
143
|
+
Parameters
|
|
144
|
+
----------
|
|
145
|
+
**kwargs : dict
|
|
146
|
+
Additional keyword arguments passed to pandas.DataFrame.to_csv().
|
|
147
|
+
Common options include:
|
|
148
|
+
- path_or_buf : str or pathlib.Path or file-like object, optional
|
|
149
|
+
File path or object, if None, the result is returned as a string.
|
|
150
|
+
Default is None.
|
|
151
|
+
- sep : str
|
|
152
|
+
String of length 1. Field delimiter for the output file.
|
|
153
|
+
Default is ','.
|
|
154
|
+
- index : bool
|
|
155
|
+
Write row names (index). Default is True.
|
|
156
|
+
- encoding : str
|
|
157
|
+
String representing the encoding to use in the output file.
|
|
158
|
+
Default is 'utf-8'.
|
|
159
|
+
|
|
160
|
+
Notes
|
|
161
|
+
-----
|
|
162
|
+
The method converts the collection to a pandas DataFrame using
|
|
163
|
+
`self.to_dataframe()` and then writes it to a CSV file using the provided
|
|
164
|
+
kwargs.
|
|
165
|
+
|
|
166
|
+
"""
|
|
167
|
+
default_kwargs = {
|
|
168
|
+
"sep": ",",
|
|
169
|
+
"index": False,
|
|
170
|
+
"encoding": "utf-8",
|
|
171
|
+
"quoting": csv.QUOTE_ALL,
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
# Merge default_kwargs with user-provided kwargs, giving precedence to user kwargs
|
|
175
|
+
merged_kwargs = {**default_kwargs, **kwargs}
|
|
176
|
+
output_dataframe = self.to_dataframe()
|
|
177
|
+
output_dataframe.to_csv(**merged_kwargs)
|
|
178
|
+
|
|
179
|
+
def to_json(
|
|
180
|
+
self,
|
|
181
|
+
file_path: pathlib.Path,
|
|
182
|
+
schema_path: pathlib.Path | None = None,
|
|
183
|
+
output_schema: bool = False,
|
|
184
|
+
) -> None:
|
|
185
|
+
"""
|
|
186
|
+
Export the SourceCollection to a JSON file, together with a data schema.
|
|
187
|
+
|
|
188
|
+
Parameters
|
|
189
|
+
----------
|
|
190
|
+
file_path : pathlib.Path
|
|
191
|
+
The path to the JSON file to be created.
|
|
192
|
+
schema_path : pathlib.Path
|
|
193
|
+
The path to the JSON schema file to be created. By default, created with a `schema` suffix next to `file_path`.
|
|
194
|
+
output_schema : bool, default False
|
|
195
|
+
If True, generates a JSON schema file describing the data structure.
|
|
196
|
+
The schema will include field descriptions and type information.
|
|
197
|
+
|
|
198
|
+
"""
|
|
199
|
+
if output_schema:
|
|
200
|
+
if schema_path is None:
|
|
201
|
+
schema_path = file_path.with_suffix(".schema.json")
|
|
202
|
+
|
|
203
|
+
# Export the model's schema with descriptions to a dict
|
|
204
|
+
schema = self.model_json_schema()
|
|
205
|
+
|
|
206
|
+
# Save the schema (which includes descriptions) to a JSON file
|
|
207
|
+
with open(schema_path, "w") as f:
|
|
208
|
+
json.dump(schema, f, indent=4)
|
|
209
|
+
|
|
210
|
+
with open(file_path, mode="w", encoding="utf-8") as jsonfile:
|
|
211
|
+
json_data = self.model_dump_json(indent=4) # Convert to JSON string
|
|
212
|
+
jsonfile.write(json_data)
|
|
213
|
+
|
|
214
|
+
@classmethod
|
|
215
|
+
def from_json(
|
|
216
|
+
cls,
|
|
217
|
+
file_path: pathlib.Path | str,
|
|
218
|
+
) -> Self:
|
|
219
|
+
"""
|
|
220
|
+
Import the SourceCollection from a JSON file.
|
|
221
|
+
|
|
222
|
+
Parameters
|
|
223
|
+
----------
|
|
224
|
+
file_path : pathlib.Path | str
|
|
225
|
+
The path to the JSON file to be imported.
|
|
226
|
+
|
|
227
|
+
"""
|
|
228
|
+
if isinstance(file_path, (pathlib.Path | str)):
|
|
229
|
+
file_path = pathlib.Path(file_path)
|
|
230
|
+
else:
|
|
231
|
+
raise TypeError("file_path must be a pathlib.Path or str")
|
|
232
|
+
|
|
233
|
+
json_data = None
|
|
234
|
+
|
|
235
|
+
# Load data from file if file_path is provided
|
|
236
|
+
with open(file_path, encoding="utf-8") as jsonfile:
|
|
237
|
+
json_data = jsonfile.read()
|
|
238
|
+
|
|
239
|
+
# pydantic_core.from_json return Any. Therefore, typing.cast makes sure that
|
|
240
|
+
# the output is indeed a TechnologyCollection
|
|
241
|
+
return cls.model_validate(
|
|
242
|
+
pydantic_core.from_json(json_data, allow_partial=True)
|
|
243
|
+
)
|