aiojournal 0.0.1__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.
aiojournal/__init__.py
ADDED
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
"""Pool of function calls."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import csv
|
|
7
|
+
import logging
|
|
8
|
+
import time
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from typing import TYPE_CHECKING, Any, TypeVar, cast
|
|
11
|
+
|
|
12
|
+
from tqdm.asyncio import tqdm_asyncio
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from collections.abc import Awaitable, Callable, Generator
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
# Set up logging
|
|
19
|
+
logging.basicConfig(
|
|
20
|
+
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
21
|
+
)
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
# Type definitions
|
|
25
|
+
T = TypeVar("T") # Return type for the callable
|
|
26
|
+
|
|
27
|
+
MAX_CONCURRENCY = 100
|
|
28
|
+
|
|
29
|
+
# Scalar and dictionary files deliberately place ``error`` differently. The header
|
|
30
|
+
# order determines the return format
|
|
31
|
+
RESULTS_FILE_COMMON_COLUMNS = (
|
|
32
|
+
"task_id",
|
|
33
|
+
"start_time",
|
|
34
|
+
"end_time",
|
|
35
|
+
"duration_seconds",
|
|
36
|
+
"success",
|
|
37
|
+
)
|
|
38
|
+
RESULTS_FILE_SCALAR_COLUMNS = (
|
|
39
|
+
*RESULTS_FILE_COMMON_COLUMNS,
|
|
40
|
+
"result",
|
|
41
|
+
"error",
|
|
42
|
+
)
|
|
43
|
+
RESULTS_FILE_DICTIONARY_METADATA_COLUMNS = (*RESULTS_FILE_COMMON_COLUMNS, "error")
|
|
44
|
+
RESULTS_FILE_RESERVED_DICTIONARY_KEYS = frozenset(
|
|
45
|
+
RESULTS_FILE_DICTIONARY_METADATA_COLUMNS
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# TODO make possible to subclass it and add a run abstract method called instead of
|
|
50
|
+
# provided func callable
|
|
51
|
+
class AsyncBatchRunner:
|
|
52
|
+
"""A simple async runner that limits the number of concurrent function calls."""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
max_concurrency: int = MAX_CONCURRENCY,
|
|
57
|
+
results_file_path: Path | None = None,
|
|
58
|
+
) -> None:
|
|
59
|
+
"""
|
|
60
|
+
Initialize the runner and its concurrency limit.
|
|
61
|
+
|
|
62
|
+
:param max_concurrency: Maximum number of concurrent function calls.
|
|
63
|
+
:param results_file_path: File where task results are appended, or ``None`` to
|
|
64
|
+
disable journaling.
|
|
65
|
+
:raises ValueError: If ``max_concurrency`` is less than one or an existing
|
|
66
|
+
results file has an incompatible header.
|
|
67
|
+
"""
|
|
68
|
+
if max_concurrency < 1:
|
|
69
|
+
raise ValueError(_ := "max_concurrency must be at least 1")
|
|
70
|
+
|
|
71
|
+
self.max_concurrency = max_concurrency
|
|
72
|
+
self.semaphore = asyncio.Semaphore(max_concurrency)
|
|
73
|
+
self._tasks = set()
|
|
74
|
+
self.results_file_path = results_file_path
|
|
75
|
+
self.csv_lock = asyncio.Lock() # Lock to prevent concurrent writes to CSV
|
|
76
|
+
self._returns_dictionary_results: bool | None = None
|
|
77
|
+
self._dictionary_result_columns: tuple[str, ...] = ()
|
|
78
|
+
|
|
79
|
+
# Restore the return format fixed by an existing results file. A file containing
|
|
80
|
+
# only failed rows has not established its return format yet.
|
|
81
|
+
if (
|
|
82
|
+
results_file_path is not None
|
|
83
|
+
and results_file_path.is_file()
|
|
84
|
+
and results_file_path.stat().st_size > 0
|
|
85
|
+
):
|
|
86
|
+
with results_file_path.open(newline="") as results_file:
|
|
87
|
+
reader = csv.DictReader(results_file)
|
|
88
|
+
if reader.fieldnames is not None:
|
|
89
|
+
# Scalar and dictionary files use different column ordering so a
|
|
90
|
+
# dictionary containing only "result" remains distinguishable.
|
|
91
|
+
if reader.fieldnames == list(RESULTS_FILE_SCALAR_COLUMNS):
|
|
92
|
+
# Adding success condition here as previous run might only have
|
|
93
|
+
# save failed tasks, which use the scalar format. In that case
|
|
94
|
+
# the returned format is still not determined.
|
|
95
|
+
if any(row["success"] == "True" for row in reader):
|
|
96
|
+
self._returns_dictionary_results = False
|
|
97
|
+
elif reader.fieldnames[
|
|
98
|
+
: len(RESULTS_FILE_DICTIONARY_METADATA_COLUMNS)
|
|
99
|
+
] == list(RESULTS_FILE_DICTIONARY_METADATA_COLUMNS):
|
|
100
|
+
self._returns_dictionary_results = True
|
|
101
|
+
self._dictionary_result_columns = tuple(
|
|
102
|
+
reader.fieldnames[
|
|
103
|
+
len(RESULTS_FILE_DICTIONARY_METADATA_COLUMNS) :
|
|
104
|
+
]
|
|
105
|
+
)
|
|
106
|
+
else:
|
|
107
|
+
message = "Existing results file has an incompatible CSV header"
|
|
108
|
+
raise ValueError(message)
|
|
109
|
+
|
|
110
|
+
def _create_csv_headers(self) -> None:
|
|
111
|
+
"""Create the CSV file with headers."""
|
|
112
|
+
if self.results_file_path is None:
|
|
113
|
+
return
|
|
114
|
+
if not self.results_file_path.parent.is_dir():
|
|
115
|
+
self.results_file_path.parent.mkdir(parents=True)
|
|
116
|
+
with self.results_file_path.open("w") as csvfile:
|
|
117
|
+
writer = csv.writer(csvfile)
|
|
118
|
+
writer.writerow(
|
|
119
|
+
[
|
|
120
|
+
*RESULTS_FILE_DICTIONARY_METADATA_COLUMNS,
|
|
121
|
+
*self._dictionary_result_columns,
|
|
122
|
+
]
|
|
123
|
+
if self._returns_dictionary_results
|
|
124
|
+
else RESULTS_FILE_SCALAR_COLUMNS
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def _expand_results_file_headers(self) -> None:
|
|
128
|
+
"""Add inferred dictionary columns while preserving previous failed rows."""
|
|
129
|
+
if self.results_file_path is None:
|
|
130
|
+
return
|
|
131
|
+
temporary_results_file_path = self.results_file_path.with_suffix(
|
|
132
|
+
f"{self.results_file_path.suffix}.tmp"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
with self.results_file_path.open(newline="") as results_file:
|
|
136
|
+
reader = csv.DictReader(results_file)
|
|
137
|
+
with temporary_results_file_path.open("w", newline="") as temporary_file:
|
|
138
|
+
writer = csv.DictWriter(
|
|
139
|
+
temporary_file,
|
|
140
|
+
fieldnames=[
|
|
141
|
+
*RESULTS_FILE_DICTIONARY_METADATA_COLUMNS,
|
|
142
|
+
*self._dictionary_result_columns,
|
|
143
|
+
],
|
|
144
|
+
)
|
|
145
|
+
writer.writeheader()
|
|
146
|
+
|
|
147
|
+
# Existing rows are failures written before the return format was known.
|
|
148
|
+
# Copy their shared metadata and leave inferred result columns empty.
|
|
149
|
+
for row in reader:
|
|
150
|
+
writer.writerow(
|
|
151
|
+
{
|
|
152
|
+
column_name: row.get(column_name, "")
|
|
153
|
+
for column_name in writer.fieldnames
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
temporary_results_file_path.replace(self.results_file_path)
|
|
158
|
+
|
|
159
|
+
async def _save_to_csv(
|
|
160
|
+
self,
|
|
161
|
+
task_id: str,
|
|
162
|
+
start_time: float,
|
|
163
|
+
end_time: float,
|
|
164
|
+
success: bool,
|
|
165
|
+
result: Any, # noqa:ANN401
|
|
166
|
+
error: str | None = None,
|
|
167
|
+
) -> None:
|
|
168
|
+
"""
|
|
169
|
+
Append a function-call result to the CSV journal.
|
|
170
|
+
|
|
171
|
+
:param task_id: Identifier of the task.
|
|
172
|
+
:param start_time: Unix timestamp recorded when the task started.
|
|
173
|
+
:param end_time: Unix timestamp recorded when the task ended.
|
|
174
|
+
:param success: Whether the task completed successfully.
|
|
175
|
+
:param result: Scalar task result or dictionary of result-column values.
|
|
176
|
+
:param error: Error message when the task failed.
|
|
177
|
+
:raises TypeError: If a successful result does not match the established return
|
|
178
|
+
format or contains a non-string dictionary key.
|
|
179
|
+
:raises ValueError: If a dictionary cannot establish or does not match the
|
|
180
|
+
inferred column schema.
|
|
181
|
+
"""
|
|
182
|
+
if self.results_file_path is None:
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
# Format timestamps
|
|
186
|
+
start_time_str = datetime.fromtimestamp(start_time, tz=timezone.utc).isoformat()
|
|
187
|
+
end_time_str = datetime.fromtimestamp(end_time, tz=timezone.utc).isoformat()
|
|
188
|
+
duration = end_time - start_time
|
|
189
|
+
|
|
190
|
+
# Ensure only one task writes to the CSV at a time
|
|
191
|
+
async with self.csv_lock:
|
|
192
|
+
result_is_dictionary = isinstance(result, dict)
|
|
193
|
+
|
|
194
|
+
# Check every successful result. The first fixes the scalar or dictionary
|
|
195
|
+
# format; later results must keep that type and cannot add columns.
|
|
196
|
+
if success and result_is_dictionary:
|
|
197
|
+
# Validate column names
|
|
198
|
+
if not all(isinstance(column_name, str) for column_name in result):
|
|
199
|
+
message = "Dictionary results must only use string keys"
|
|
200
|
+
raise TypeError(message)
|
|
201
|
+
dictionary_result = cast("dict[str, Any]", result)
|
|
202
|
+
reserved_column_names_in_result = (
|
|
203
|
+
set(dictionary_result) & RESULTS_FILE_RESERVED_DICTIONARY_KEYS
|
|
204
|
+
)
|
|
205
|
+
if reserved_column_names_in_result:
|
|
206
|
+
names = ", ".join(sorted(reserved_column_names_in_result))
|
|
207
|
+
message = f"Dictionary result uses reserved columns: {names}"
|
|
208
|
+
raise ValueError(message)
|
|
209
|
+
|
|
210
|
+
# Infer every result column from the first successful dictionary.
|
|
211
|
+
returned_dictionary_result_columns = tuple(dictionary_result)
|
|
212
|
+
if self._returns_dictionary_results is None:
|
|
213
|
+
# This is the first successful return, so its key order permanently
|
|
214
|
+
# defines the dictionary columns for this results file.
|
|
215
|
+
self._returns_dictionary_results = True
|
|
216
|
+
self._dictionary_result_columns = returned_dictionary_result_columns
|
|
217
|
+
# If result file already existed, because of previous failed tasks,
|
|
218
|
+
# rewrite it with the now determined columns names
|
|
219
|
+
if (
|
|
220
|
+
self.results_file_path.is_file()
|
|
221
|
+
and self.results_file_path.stat().st_size > 0
|
|
222
|
+
):
|
|
223
|
+
self._expand_results_file_headers()
|
|
224
|
+
elif not self._returns_dictionary_results:
|
|
225
|
+
message = "Expected a scalar result, received a dictionary"
|
|
226
|
+
raise TypeError(message)
|
|
227
|
+
|
|
228
|
+
# Once inferred, later dictionaries may omit columns but cannot add any.
|
|
229
|
+
unexpected_result_columns = set(
|
|
230
|
+
returned_dictionary_result_columns
|
|
231
|
+
) - set(self._dictionary_result_columns)
|
|
232
|
+
if unexpected_result_columns:
|
|
233
|
+
names = ", ".join(sorted(unexpected_result_columns))
|
|
234
|
+
message = f"Dictionary result contains unexpected columns: {names}"
|
|
235
|
+
raise ValueError(message)
|
|
236
|
+
|
|
237
|
+
# Scalar result format
|
|
238
|
+
elif success:
|
|
239
|
+
if self._returns_dictionary_results:
|
|
240
|
+
message = "Expected a dictionary result, received a scalar"
|
|
241
|
+
raise TypeError(message)
|
|
242
|
+
self._returns_dictionary_results = False
|
|
243
|
+
|
|
244
|
+
# Create the file after a successful result has had a chance to infer its
|
|
245
|
+
# columns. Failed tasks create the unresolved scalar-style header.
|
|
246
|
+
if (
|
|
247
|
+
not self.results_file_path.is_file()
|
|
248
|
+
or self.results_file_path.stat().st_size == 0
|
|
249
|
+
):
|
|
250
|
+
self._create_csv_headers()
|
|
251
|
+
|
|
252
|
+
# Project dictionary values onto the inferred columns. Missing keys and
|
|
253
|
+
# failed-task dictionary values become empty CSV cells.
|
|
254
|
+
dictionary_result_values = (
|
|
255
|
+
[
|
|
256
|
+
result.get(column_name, "")
|
|
257
|
+
for column_name in self._dictionary_result_columns
|
|
258
|
+
]
|
|
259
|
+
if result_is_dictionary
|
|
260
|
+
else [""] * len(self._dictionary_result_columns)
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
# Append one complete task record using the established column order.
|
|
264
|
+
with self.results_file_path.open("a", newline="") as csvfile:
|
|
265
|
+
writer = csv.writer(csvfile)
|
|
266
|
+
task_metadata = [
|
|
267
|
+
task_id,
|
|
268
|
+
start_time_str,
|
|
269
|
+
end_time_str,
|
|
270
|
+
f"{duration:.4f}",
|
|
271
|
+
success,
|
|
272
|
+
]
|
|
273
|
+
if self._returns_dictionary_results:
|
|
274
|
+
# Dictionary files store error metadata first, followed by values in
|
|
275
|
+
# the exact order established by the first dictionary result.
|
|
276
|
+
writer.writerow(
|
|
277
|
+
[*task_metadata, error or "", *dictionary_result_values]
|
|
278
|
+
)
|
|
279
|
+
else:
|
|
280
|
+
# Scalar and unresolved failure rows keep the standard result column
|
|
281
|
+
# immediately before the error column.
|
|
282
|
+
writer.writerow(
|
|
283
|
+
[
|
|
284
|
+
*task_metadata,
|
|
285
|
+
str(result) if result is not None else "",
|
|
286
|
+
error or "",
|
|
287
|
+
]
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
async def call(
|
|
291
|
+
self,
|
|
292
|
+
func: Callable[..., Awaitable[T]],
|
|
293
|
+
task_id: str,
|
|
294
|
+
*,
|
|
295
|
+
_first_task_exception_future: asyncio.Future[Exception] | None = None,
|
|
296
|
+
**kwargs,
|
|
297
|
+
) -> T:
|
|
298
|
+
"""
|
|
299
|
+
Execute an async callable within the configured concurrency limit.
|
|
300
|
+
|
|
301
|
+
When a results file is configured, the attempt is appended to its CSV journal.
|
|
302
|
+
|
|
303
|
+
:param func: Async callable to execute.
|
|
304
|
+
:param task_id: Identifier written to the CSV journal.
|
|
305
|
+
:param _first_task_exception_future: Internal batch signal containing the first
|
|
306
|
+
task exception.
|
|
307
|
+
:param kwargs: Keyword arguments passed to ``func``.
|
|
308
|
+
:return: The callable's result.
|
|
309
|
+
:raises Exception: Any exception raised by ``func``.
|
|
310
|
+
"""
|
|
311
|
+
# Start timing only after a concurrency slot is available.
|
|
312
|
+
async with self.semaphore:
|
|
313
|
+
if (
|
|
314
|
+
_first_task_exception_future is not None
|
|
315
|
+
and _first_task_exception_future.done()
|
|
316
|
+
):
|
|
317
|
+
raise asyncio.CancelledError
|
|
318
|
+
|
|
319
|
+
error_msg = None
|
|
320
|
+
result = None
|
|
321
|
+
success = False
|
|
322
|
+
start_time = time.time()
|
|
323
|
+
|
|
324
|
+
try:
|
|
325
|
+
result = await func(**kwargs)
|
|
326
|
+
success = True
|
|
327
|
+
return result
|
|
328
|
+
|
|
329
|
+
except Exception as exception:
|
|
330
|
+
if (
|
|
331
|
+
_first_task_exception_future is not None
|
|
332
|
+
and not _first_task_exception_future.done()
|
|
333
|
+
):
|
|
334
|
+
_first_task_exception_future.set_result(exception)
|
|
335
|
+
error_msg = str(exception)
|
|
336
|
+
logger.info(
|
|
337
|
+
_ := f"{task_id} - Task failed: {error_msg}",
|
|
338
|
+
extra={"task_id": task_id, "error": error_msg},
|
|
339
|
+
)
|
|
340
|
+
raise
|
|
341
|
+
|
|
342
|
+
finally:
|
|
343
|
+
# Save result to CSV
|
|
344
|
+
if self.results_file_path is not None:
|
|
345
|
+
# Saving can fail if the result has the wrong type or dictionary
|
|
346
|
+
# keys, or if the file cannot be written. Signal that failure before
|
|
347
|
+
# releasing the semaphore so queued calls do not start.
|
|
348
|
+
try:
|
|
349
|
+
await self._save_to_csv(
|
|
350
|
+
task_id=task_id,
|
|
351
|
+
start_time=start_time,
|
|
352
|
+
end_time=time.time(),
|
|
353
|
+
success=success,
|
|
354
|
+
result=result,
|
|
355
|
+
error=error_msg,
|
|
356
|
+
)
|
|
357
|
+
except Exception as exception:
|
|
358
|
+
if (
|
|
359
|
+
_first_task_exception_future is not None
|
|
360
|
+
and not _first_task_exception_future.done()
|
|
361
|
+
):
|
|
362
|
+
_first_task_exception_future.set_result(exception)
|
|
363
|
+
raise
|
|
364
|
+
|
|
365
|
+
async def gather(
|
|
366
|
+
self,
|
|
367
|
+
funcs_with_args: list[dict[str, Any]],
|
|
368
|
+
task_ids: list[str],
|
|
369
|
+
progress_bar_desc: str | None = None,
|
|
370
|
+
*,
|
|
371
|
+
raise_on_failure: bool = False,
|
|
372
|
+
) -> list[Any]:
|
|
373
|
+
"""
|
|
374
|
+
Execute multiple functions concurrently.
|
|
375
|
+
|
|
376
|
+
:param funcs_with_args: Call specifications containing a ``func`` entry and
|
|
377
|
+
keyword arguments for that callable.
|
|
378
|
+
:param task_ids: Task identifiers corresponding to ``funcs_with_args``.
|
|
379
|
+
:param progress_bar_desc: Progress-bar description, or ``None`` to disable the
|
|
380
|
+
progress bar.
|
|
381
|
+
:param raise_on_failure: Stop queued calls after the first task exception, wait
|
|
382
|
+
for active calls to finish, and then raise that exception.
|
|
383
|
+
:return: Results or exceptions in the same order as ``funcs_with_args``.
|
|
384
|
+
:raises ValueError: If the number of task IDs does not match the number of call
|
|
385
|
+
specifications.
|
|
386
|
+
"""
|
|
387
|
+
if not funcs_with_args:
|
|
388
|
+
return []
|
|
389
|
+
|
|
390
|
+
if len(task_ids) != len(funcs_with_args):
|
|
391
|
+
raise ValueError(
|
|
392
|
+
_ := "Length of task_ids must match length of funcs_with_args"
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
first_task_exception_future: asyncio.Future[Exception] | None = (
|
|
396
|
+
asyncio.get_running_loop().create_future() if raise_on_failure else None
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
# Create tasks and start them immediately
|
|
400
|
+
tasks = []
|
|
401
|
+
for item, task_id in zip(funcs_with_args, task_ids, strict=True):
|
|
402
|
+
item_copy = item.copy()
|
|
403
|
+
func = item_copy.pop("func")
|
|
404
|
+
# Create a task that will call the function with semaphore management
|
|
405
|
+
task = asyncio.create_task(
|
|
406
|
+
self.call(
|
|
407
|
+
func,
|
|
408
|
+
task_id=task_id,
|
|
409
|
+
_first_task_exception_future=first_task_exception_future,
|
|
410
|
+
**item_copy,
|
|
411
|
+
)
|
|
412
|
+
)
|
|
413
|
+
tasks.append(task)
|
|
414
|
+
|
|
415
|
+
# Execute with or without progress bar
|
|
416
|
+
if progress_bar_desc is not None:
|
|
417
|
+
results = await self.tqdm_gather(
|
|
418
|
+
*tasks, desc=progress_bar_desc, return_exceptions=True
|
|
419
|
+
)
|
|
420
|
+
else:
|
|
421
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
422
|
+
|
|
423
|
+
# Remove completed tasks
|
|
424
|
+
for task in tasks:
|
|
425
|
+
self._tasks.discard(task)
|
|
426
|
+
|
|
427
|
+
if first_task_exception_future is not None:
|
|
428
|
+
if first_task_exception_future.done():
|
|
429
|
+
raise first_task_exception_future.result()
|
|
430
|
+
for task in tasks:
|
|
431
|
+
if task_exception := task.exception():
|
|
432
|
+
raise task_exception
|
|
433
|
+
|
|
434
|
+
return results
|
|
435
|
+
|
|
436
|
+
@staticmethod
|
|
437
|
+
async def tqdm_gather(
|
|
438
|
+
*fs: Awaitable[T],
|
|
439
|
+
return_exceptions: bool = False,
|
|
440
|
+
**kwargs,
|
|
441
|
+
) -> list[T | BaseException]:
|
|
442
|
+
"""
|
|
443
|
+
Gather awaitables with progress and optional exception results.
|
|
444
|
+
|
|
445
|
+
:param fs: Awaitables to execute.
|
|
446
|
+
:param return_exceptions: Return exceptions alongside successful results when
|
|
447
|
+
``True``.
|
|
448
|
+
:param kwargs: Keyword arguments passed to ``tqdm_asyncio.gather``.
|
|
449
|
+
:return: Results in the same order as ``fs``.
|
|
450
|
+
"""
|
|
451
|
+
if not return_exceptions:
|
|
452
|
+
return await tqdm_asyncio.gather(*fs, **kwargs)
|
|
453
|
+
|
|
454
|
+
async def wrap(f: Awaitable[T]) -> T | BaseException:
|
|
455
|
+
try:
|
|
456
|
+
return await f
|
|
457
|
+
except (Exception, asyncio.CancelledError) as exception: # noqa: BLE001
|
|
458
|
+
return exception
|
|
459
|
+
|
|
460
|
+
return await tqdm_asyncio.gather(*map(wrap, fs), **kwargs)
|
|
461
|
+
|
|
462
|
+
async def map(
|
|
463
|
+
self,
|
|
464
|
+
func: Callable[..., Awaitable[T]],
|
|
465
|
+
args_list: list[dict[str, Any]],
|
|
466
|
+
task_ids: list[str],
|
|
467
|
+
progress_bar_desc: str | None = None,
|
|
468
|
+
*,
|
|
469
|
+
raise_on_failure: bool = False,
|
|
470
|
+
) -> list[T | BaseException]:
|
|
471
|
+
"""
|
|
472
|
+
Apply one async callable to multiple argument sets concurrently.
|
|
473
|
+
|
|
474
|
+
:param func: Async callable to execute.
|
|
475
|
+
:param args_list: Keyword-argument dictionaries passed to ``func``.
|
|
476
|
+
:param task_ids: Task identifiers corresponding to ``args_list``.
|
|
477
|
+
:param progress_bar_desc: Progress-bar description, or ``None`` to disable the
|
|
478
|
+
progress bar.
|
|
479
|
+
:param raise_on_failure: Stop queued calls after the first task exception, wait
|
|
480
|
+
for active calls to finish, and then raise that exception.
|
|
481
|
+
:return: Results or exceptions in the same order as ``args_list``.
|
|
482
|
+
:raises ValueError: If the number of task IDs does not match the number of
|
|
483
|
+
argument sets.
|
|
484
|
+
"""
|
|
485
|
+
funcs_with_args = [{"func": func, **args} for args in args_list]
|
|
486
|
+
return await self.gather(
|
|
487
|
+
funcs_with_args,
|
|
488
|
+
task_ids,
|
|
489
|
+
progress_bar_desc,
|
|
490
|
+
raise_on_failure=raise_on_failure,
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
@property
|
|
494
|
+
def num_active_count(self) -> int:
|
|
495
|
+
"""
|
|
496
|
+
Return the number of currently active calls.
|
|
497
|
+
|
|
498
|
+
:return: Number of acquired concurrency slots.
|
|
499
|
+
"""
|
|
500
|
+
return self.max_concurrency - self.semaphore._value
|
|
501
|
+
|
|
502
|
+
def read_results_csv(
|
|
503
|
+
self, csv_path: Path | None = None
|
|
504
|
+
) -> Generator[dict[str, str]]:
|
|
505
|
+
"""
|
|
506
|
+
Read the results CSV file, yielding one row at a time.
|
|
507
|
+
|
|
508
|
+
:param csv_path: CSV file to read, or ``None`` to use the configured journal.
|
|
509
|
+
:return: Rows from the CSV journal.
|
|
510
|
+
"""
|
|
511
|
+
if csv_path is None:
|
|
512
|
+
csv_path = self.results_file_path
|
|
513
|
+
|
|
514
|
+
if csv_path is None or not csv_path.is_file():
|
|
515
|
+
return
|
|
516
|
+
|
|
517
|
+
with csv_path.open() as csvfile:
|
|
518
|
+
reader = csv.DictReader(csvfile)
|
|
519
|
+
yield from reader
|
|
520
|
+
|
|
521
|
+
def remove_tasks_failed_from_res_file(
|
|
522
|
+
self, remove_only_those_succeeded: bool = True
|
|
523
|
+
) -> None:
|
|
524
|
+
"""
|
|
525
|
+
Remove rows marked as failed from the pool's results file.
|
|
526
|
+
|
|
527
|
+
This streams the CSV to a temporary file and replaces the original to avoid
|
|
528
|
+
loading the entire file in memory.
|
|
529
|
+
|
|
530
|
+
:param remove_only_those_succeeded: If ``True``, only remove rows that failed
|
|
531
|
+
for which another row with the same task_id is succeeded. This allows to
|
|
532
|
+
clean multi-attempt failures while keeping failures with no success.
|
|
533
|
+
"""
|
|
534
|
+
if self.results_file_path is None or not self.results_file_path.is_file():
|
|
535
|
+
return
|
|
536
|
+
|
|
537
|
+
csv_path = self.results_file_path
|
|
538
|
+
temp_path = csv_path.with_suffix(f"{csv_path.suffix}.tmp")
|
|
539
|
+
|
|
540
|
+
# First get ids of requests succeeded
|
|
541
|
+
seen_success_ids: set[str] = set()
|
|
542
|
+
if remove_only_those_succeeded:
|
|
543
|
+
with csv_path.open() as csvfile:
|
|
544
|
+
reader = csv.DictReader(csvfile)
|
|
545
|
+
if reader.fieldnames is None:
|
|
546
|
+
return
|
|
547
|
+
for row in reader:
|
|
548
|
+
if row["success"] == "True":
|
|
549
|
+
seen_success_ids.add(row["task_id"])
|
|
550
|
+
|
|
551
|
+
# Write rows with success or failed without success
|
|
552
|
+
with csv_path.open() as csvfile:
|
|
553
|
+
reader = csv.DictReader(csvfile)
|
|
554
|
+
if (fieldnames := reader.fieldnames) is None:
|
|
555
|
+
return
|
|
556
|
+
with temp_path.open("w") as tmp_file:
|
|
557
|
+
writer = csv.DictWriter(tmp_file, fieldnames=fieldnames)
|
|
558
|
+
writer.writeheader()
|
|
559
|
+
for row in reader:
|
|
560
|
+
# Write success rows and failed one with no success (same task_id)
|
|
561
|
+
# if requested
|
|
562
|
+
if row["success"] == "True" or (
|
|
563
|
+
remove_only_those_succeeded
|
|
564
|
+
and row["task_id"] not in seen_success_ids
|
|
565
|
+
):
|
|
566
|
+
writer.writerow(row)
|
|
567
|
+
|
|
568
|
+
temp_path.replace(csv_path)
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: aiojournal
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Simple runner to perform batches of asynchronous tasks.
|
|
5
|
+
Author: Nathan Fradet
|
|
6
|
+
License: Creative Commons Legal Code
|
|
7
|
+
|
|
8
|
+
CC0 1.0 Universal
|
|
9
|
+
|
|
10
|
+
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
|
|
11
|
+
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
|
|
12
|
+
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
|
|
13
|
+
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
|
|
14
|
+
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
|
|
15
|
+
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
|
|
16
|
+
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
|
|
17
|
+
HEREUNDER.
|
|
18
|
+
|
|
19
|
+
Statement of Purpose
|
|
20
|
+
|
|
21
|
+
The laws of most jurisdictions throughout the world automatically confer
|
|
22
|
+
exclusive Copyright and Related Rights (defined below) upon the creator
|
|
23
|
+
and subsequent owner(s) (each and all, an "owner") of an original work of
|
|
24
|
+
authorship and/or a database (each, a "Work").
|
|
25
|
+
|
|
26
|
+
Certain owners wish to permanently relinquish those rights to a Work for
|
|
27
|
+
the purpose of contributing to a commons of creative, cultural and
|
|
28
|
+
scientific works ("Commons") that the public can reliably and without fear
|
|
29
|
+
of later claims of infringement build upon, modify, incorporate in other
|
|
30
|
+
works, reuse and redistribute as freely as possible in any form whatsoever
|
|
31
|
+
and for any purposes, including without limitation commercial purposes.
|
|
32
|
+
These owners may contribute to the Commons to promote the ideal of a free
|
|
33
|
+
culture and the further production of creative, cultural and scientific
|
|
34
|
+
works, or to gain reputation or greater distribution for their Work in
|
|
35
|
+
part through the use and efforts of others.
|
|
36
|
+
|
|
37
|
+
For these and/or other purposes and motivations, and without any
|
|
38
|
+
expectation of additional consideration or compensation, the person
|
|
39
|
+
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
|
|
40
|
+
is an owner of Copyright and Related Rights in the Work, voluntarily
|
|
41
|
+
elects to apply CC0 to the Work and publicly distribute the Work under its
|
|
42
|
+
terms, with knowledge of his or her Copyright and Related Rights in the
|
|
43
|
+
Work and the meaning and intended legal effect of CC0 on those rights.
|
|
44
|
+
|
|
45
|
+
1. Copyright and Related Rights. A Work made available under CC0 may be
|
|
46
|
+
protected by copyright and related or neighboring rights ("Copyright and
|
|
47
|
+
Related Rights"). Copyright and Related Rights include, but are not
|
|
48
|
+
limited to, the following:
|
|
49
|
+
|
|
50
|
+
i. the right to reproduce, adapt, distribute, perform, display,
|
|
51
|
+
communicate, and translate a Work;
|
|
52
|
+
ii. moral rights retained by the original author(s) and/or performer(s);
|
|
53
|
+
iii. publicity and privacy rights pertaining to a person's image or
|
|
54
|
+
likeness depicted in a Work;
|
|
55
|
+
iv. rights protecting against unfair competition in regards to a Work,
|
|
56
|
+
subject to the limitations in paragraph 4(a), below;
|
|
57
|
+
v. rights protecting the extraction, dissemination, use and reuse of data
|
|
58
|
+
in a Work;
|
|
59
|
+
vi. database rights (such as those arising under Directive 96/9/EC of the
|
|
60
|
+
European Parliament and of the Council of 11 March 1996 on the legal
|
|
61
|
+
protection of databases, and under any national implementation
|
|
62
|
+
thereof, including any amended or successor version of such
|
|
63
|
+
directive); and
|
|
64
|
+
vii. other similar, equivalent or corresponding rights throughout the
|
|
65
|
+
world based on applicable law or treaty, and any national
|
|
66
|
+
implementations thereof.
|
|
67
|
+
|
|
68
|
+
2. Waiver. To the greatest extent permitted by, but not in contravention
|
|
69
|
+
of, applicable law, Affirmer hereby overtly, fully, permanently,
|
|
70
|
+
irrevocably and unconditionally waives, abandons, and surrenders all of
|
|
71
|
+
Affirmer's Copyright and Related Rights and associated claims and causes
|
|
72
|
+
of action, whether now known or unknown (including existing as well as
|
|
73
|
+
future claims and causes of action), in the Work (i) in all territories
|
|
74
|
+
worldwide, (ii) for the maximum duration provided by applicable law or
|
|
75
|
+
treaty (including future time extensions), (iii) in any current or future
|
|
76
|
+
medium and for any number of copies, and (iv) for any purpose whatsoever,
|
|
77
|
+
including without limitation commercial, advertising or promotional
|
|
78
|
+
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
|
|
79
|
+
member of the public at large and to the detriment of Affirmer's heirs and
|
|
80
|
+
successors, fully intending that such Waiver shall not be subject to
|
|
81
|
+
revocation, rescission, cancellation, termination, or any other legal or
|
|
82
|
+
equitable action to disrupt the quiet enjoyment of the Work by the public
|
|
83
|
+
as contemplated by Affirmer's express Statement of Purpose.
|
|
84
|
+
|
|
85
|
+
3. Public License Fallback. Should any part of the Waiver for any reason
|
|
86
|
+
be judged legally invalid or ineffective under applicable law, then the
|
|
87
|
+
Waiver shall be preserved to the maximum extent permitted taking into
|
|
88
|
+
account Affirmer's express Statement of Purpose. In addition, to the
|
|
89
|
+
extent the Waiver is so judged Affirmer hereby grants to each affected
|
|
90
|
+
person a royalty-free, non transferable, non sublicensable, non exclusive,
|
|
91
|
+
irrevocable and unconditional license to exercise Affirmer's Copyright and
|
|
92
|
+
Related Rights in the Work (i) in all territories worldwide, (ii) for the
|
|
93
|
+
maximum duration provided by applicable law or treaty (including future
|
|
94
|
+
time extensions), (iii) in any current or future medium and for any number
|
|
95
|
+
of copies, and (iv) for any purpose whatsoever, including without
|
|
96
|
+
limitation commercial, advertising or promotional purposes (the
|
|
97
|
+
"License"). The License shall be deemed effective as of the date CC0 was
|
|
98
|
+
applied by Affirmer to the Work. Should any part of the License for any
|
|
99
|
+
reason be judged legally invalid or ineffective under applicable law, such
|
|
100
|
+
partial invalidity or ineffectiveness shall not invalidate the remainder
|
|
101
|
+
of the License, and in such case Affirmer hereby affirms that he or she
|
|
102
|
+
will not (i) exercise any of his or her remaining Copyright and Related
|
|
103
|
+
Rights in the Work or (ii) assert any associated claims and causes of
|
|
104
|
+
action with respect to the Work, in either case contrary to Affirmer's
|
|
105
|
+
express Statement of Purpose.
|
|
106
|
+
|
|
107
|
+
4. Limitations and Disclaimers.
|
|
108
|
+
|
|
109
|
+
a. No trademark or patent rights held by Affirmer are waived, abandoned,
|
|
110
|
+
surrendered, licensed or otherwise affected by this document.
|
|
111
|
+
b. Affirmer offers the Work as-is and makes no representations or
|
|
112
|
+
warranties of any kind concerning the Work, express, implied,
|
|
113
|
+
statutory or otherwise, including without limitation warranties of
|
|
114
|
+
title, merchantability, fitness for a particular purpose, non
|
|
115
|
+
infringement, or the absence of latent or other defects, accuracy, or
|
|
116
|
+
the present or absence of errors, whether or not discoverable, all to
|
|
117
|
+
the greatest extent permissible under applicable law.
|
|
118
|
+
c. Affirmer disclaims responsibility for clearing rights of other persons
|
|
119
|
+
that may apply to the Work or any use thereof, including without
|
|
120
|
+
limitation any person's Copyright and Related Rights in the Work.
|
|
121
|
+
Further, Affirmer disclaims responsibility for obtaining any necessary
|
|
122
|
+
consents, permissions or other rights required for any use of the
|
|
123
|
+
Work.
|
|
124
|
+
d. Affirmer understands and acknowledges that Creative Commons is not a
|
|
125
|
+
party to this document and has no duty or obligation with respect to
|
|
126
|
+
this CC0 or use of the Work.
|
|
127
|
+
License-File: LICENSE
|
|
128
|
+
Keywords: asyncio,batch-processing,concurrency,journaling
|
|
129
|
+
Classifier: Intended Audience :: Developers
|
|
130
|
+
Classifier: Operating System :: OS Independent
|
|
131
|
+
Classifier: Programming Language :: Python
|
|
132
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
133
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
134
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
135
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
136
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
137
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
138
|
+
Requires-Python: >=3.10.0
|
|
139
|
+
Requires-Dist: tqdm
|
|
140
|
+
Provides-Extra: tests
|
|
141
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'tests'
|
|
142
|
+
Requires-Dist: pytest-cov>=5; extra == 'tests'
|
|
143
|
+
Requires-Dist: pytest-rerunfailures>=14; extra == 'tests'
|
|
144
|
+
Requires-Dist: pytest-xdist>=3; extra == 'tests'
|
|
145
|
+
Requires-Dist: pytest>=8; extra == 'tests'
|
|
146
|
+
Requires-Dist: ruff>=0.12; extra == 'tests'
|
|
147
|
+
Requires-Dist: ty>=0.0.66; extra == 'tests'
|
|
148
|
+
Description-Content-Type: text/markdown
|
|
149
|
+
|
|
150
|
+
# aiojournal
|
|
151
|
+
|
|
152
|
+
An asynchronous batch execution runner with results persistence and resumability.
|
|
153
|
+
|
|
154
|
+
Especially useful when making concurrent API calls while directly saving each request's result, for example when benchmarking LLM models, from big providers or inference engines.
|
|
155
|
+
It is minimal and lightweight, only dependency is [tqdm](https://github.com/tqdm/tqdm).
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
pip install aiojournal
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Example
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
import asyncio
|
|
165
|
+
from pathlib import Path
|
|
166
|
+
|
|
167
|
+
from aiojournal import AsyncBatchRunner
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
async def describe(value: int) -> dict[str, int | str]:
|
|
171
|
+
await asyncio.sleep(0.1)
|
|
172
|
+
return {
|
|
173
|
+
"result": value * 2,
|
|
174
|
+
"input": value,
|
|
175
|
+
"parity": "even" if value % 2 == 0 else "odd",
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
async def main() -> None:
|
|
180
|
+
runner = AsyncBatchRunner(
|
|
181
|
+
max_concurrency=2,
|
|
182
|
+
results_file_path=Path("tasks.csv"),
|
|
183
|
+
)
|
|
184
|
+
results = await runner.map(
|
|
185
|
+
describe,
|
|
186
|
+
args_list=[{"value": 1}, {"value": 2}, {"value": 3}],
|
|
187
|
+
task_ids=["one", "two", "three"],
|
|
188
|
+
)
|
|
189
|
+
# {"result": 2, "input": 1, "parity": "odd"}
|
|
190
|
+
# {"result": 4, "input": 2, "parity": "even"}
|
|
191
|
+
# {"result": 6, "input": 3, "parity": "odd"}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
asyncio.run(main())
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
Each task appends a row to `tasks.csv`. The journal can be read without loading the whole file:
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
for result in runner.read_results_csv():
|
|
202
|
+
print(result["task_id"], result["success"], result["result"])
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
And will look like this (with different timestamps and durations):
|
|
206
|
+
|
|
207
|
+
```csv
|
|
208
|
+
task_id,start_time,end_time,duration_seconds,success,result,error,input,parity
|
|
209
|
+
one,2026-08-23T16:00:00+00:00,2026-08-23T16:00:00.100000+00:00,0.1000,True,2,,1,odd
|
|
210
|
+
two,2026-08-23T16:00:00+00:00,2026-08-23T16:00:00.100000+00:00,0.1000,True,4,,2,even
|
|
211
|
+
three,2026-08-23T16:00:00.100000+00:00,2026-08-23T16:00:00.200000+00:00,0.1000,True,6,,3,odd
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Columns names are inferred from the dictionary returned by the function provided to the
|
|
215
|
+
`map` method. The function may also return plain text, which in this case will be
|
|
216
|
+
written in a `result` column.
|
|
217
|
+
|
|
218
|
+
By default, if a task fails, `map` and `gather` returns/saves it's exception while
|
|
219
|
+
running the rest of the batch.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
aiojournal/__init__.py,sha256=FStyevk3UForRqQ9qoC5aL9pna1T7biM_pXo35Hf2I4,150
|
|
2
|
+
aiojournal/async_runner.py,sha256=Y5QBkdA4O6TqzdWTytRFlaDTmXwzMpXpyt6zBV5DJFg,23375
|
|
3
|
+
aiojournal-0.0.1.dist-info/METADATA,sha256=ybYGLiXtowMQ_oWjtlu916h6GfVx1CI47UnEFacxabo,11279
|
|
4
|
+
aiojournal-0.0.1.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
5
|
+
aiojournal-0.0.1.dist-info/licenses/LICENSE,sha256=ogEPNDSH0_dhiv_lT3ifVIdgIzHAqNA_SemnxUfPBJk,7048
|
|
6
|
+
aiojournal-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
Creative Commons Legal Code
|
|
2
|
+
|
|
3
|
+
CC0 1.0 Universal
|
|
4
|
+
|
|
5
|
+
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
|
|
6
|
+
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
|
|
7
|
+
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
|
|
8
|
+
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
|
|
9
|
+
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
|
|
10
|
+
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
|
|
11
|
+
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
|
|
12
|
+
HEREUNDER.
|
|
13
|
+
|
|
14
|
+
Statement of Purpose
|
|
15
|
+
|
|
16
|
+
The laws of most jurisdictions throughout the world automatically confer
|
|
17
|
+
exclusive Copyright and Related Rights (defined below) upon the creator
|
|
18
|
+
and subsequent owner(s) (each and all, an "owner") of an original work of
|
|
19
|
+
authorship and/or a database (each, a "Work").
|
|
20
|
+
|
|
21
|
+
Certain owners wish to permanently relinquish those rights to a Work for
|
|
22
|
+
the purpose of contributing to a commons of creative, cultural and
|
|
23
|
+
scientific works ("Commons") that the public can reliably and without fear
|
|
24
|
+
of later claims of infringement build upon, modify, incorporate in other
|
|
25
|
+
works, reuse and redistribute as freely as possible in any form whatsoever
|
|
26
|
+
and for any purposes, including without limitation commercial purposes.
|
|
27
|
+
These owners may contribute to the Commons to promote the ideal of a free
|
|
28
|
+
culture and the further production of creative, cultural and scientific
|
|
29
|
+
works, or to gain reputation or greater distribution for their Work in
|
|
30
|
+
part through the use and efforts of others.
|
|
31
|
+
|
|
32
|
+
For these and/or other purposes and motivations, and without any
|
|
33
|
+
expectation of additional consideration or compensation, the person
|
|
34
|
+
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
|
|
35
|
+
is an owner of Copyright and Related Rights in the Work, voluntarily
|
|
36
|
+
elects to apply CC0 to the Work and publicly distribute the Work under its
|
|
37
|
+
terms, with knowledge of his or her Copyright and Related Rights in the
|
|
38
|
+
Work and the meaning and intended legal effect of CC0 on those rights.
|
|
39
|
+
|
|
40
|
+
1. Copyright and Related Rights. A Work made available under CC0 may be
|
|
41
|
+
protected by copyright and related or neighboring rights ("Copyright and
|
|
42
|
+
Related Rights"). Copyright and Related Rights include, but are not
|
|
43
|
+
limited to, the following:
|
|
44
|
+
|
|
45
|
+
i. the right to reproduce, adapt, distribute, perform, display,
|
|
46
|
+
communicate, and translate a Work;
|
|
47
|
+
ii. moral rights retained by the original author(s) and/or performer(s);
|
|
48
|
+
iii. publicity and privacy rights pertaining to a person's image or
|
|
49
|
+
likeness depicted in a Work;
|
|
50
|
+
iv. rights protecting against unfair competition in regards to a Work,
|
|
51
|
+
subject to the limitations in paragraph 4(a), below;
|
|
52
|
+
v. rights protecting the extraction, dissemination, use and reuse of data
|
|
53
|
+
in a Work;
|
|
54
|
+
vi. database rights (such as those arising under Directive 96/9/EC of the
|
|
55
|
+
European Parliament and of the Council of 11 March 1996 on the legal
|
|
56
|
+
protection of databases, and under any national implementation
|
|
57
|
+
thereof, including any amended or successor version of such
|
|
58
|
+
directive); and
|
|
59
|
+
vii. other similar, equivalent or corresponding rights throughout the
|
|
60
|
+
world based on applicable law or treaty, and any national
|
|
61
|
+
implementations thereof.
|
|
62
|
+
|
|
63
|
+
2. Waiver. To the greatest extent permitted by, but not in contravention
|
|
64
|
+
of, applicable law, Affirmer hereby overtly, fully, permanently,
|
|
65
|
+
irrevocably and unconditionally waives, abandons, and surrenders all of
|
|
66
|
+
Affirmer's Copyright and Related Rights and associated claims and causes
|
|
67
|
+
of action, whether now known or unknown (including existing as well as
|
|
68
|
+
future claims and causes of action), in the Work (i) in all territories
|
|
69
|
+
worldwide, (ii) for the maximum duration provided by applicable law or
|
|
70
|
+
treaty (including future time extensions), (iii) in any current or future
|
|
71
|
+
medium and for any number of copies, and (iv) for any purpose whatsoever,
|
|
72
|
+
including without limitation commercial, advertising or promotional
|
|
73
|
+
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
|
|
74
|
+
member of the public at large and to the detriment of Affirmer's heirs and
|
|
75
|
+
successors, fully intending that such Waiver shall not be subject to
|
|
76
|
+
revocation, rescission, cancellation, termination, or any other legal or
|
|
77
|
+
equitable action to disrupt the quiet enjoyment of the Work by the public
|
|
78
|
+
as contemplated by Affirmer's express Statement of Purpose.
|
|
79
|
+
|
|
80
|
+
3. Public License Fallback. Should any part of the Waiver for any reason
|
|
81
|
+
be judged legally invalid or ineffective under applicable law, then the
|
|
82
|
+
Waiver shall be preserved to the maximum extent permitted taking into
|
|
83
|
+
account Affirmer's express Statement of Purpose. In addition, to the
|
|
84
|
+
extent the Waiver is so judged Affirmer hereby grants to each affected
|
|
85
|
+
person a royalty-free, non transferable, non sublicensable, non exclusive,
|
|
86
|
+
irrevocable and unconditional license to exercise Affirmer's Copyright and
|
|
87
|
+
Related Rights in the Work (i) in all territories worldwide, (ii) for the
|
|
88
|
+
maximum duration provided by applicable law or treaty (including future
|
|
89
|
+
time extensions), (iii) in any current or future medium and for any number
|
|
90
|
+
of copies, and (iv) for any purpose whatsoever, including without
|
|
91
|
+
limitation commercial, advertising or promotional purposes (the
|
|
92
|
+
"License"). The License shall be deemed effective as of the date CC0 was
|
|
93
|
+
applied by Affirmer to the Work. Should any part of the License for any
|
|
94
|
+
reason be judged legally invalid or ineffective under applicable law, such
|
|
95
|
+
partial invalidity or ineffectiveness shall not invalidate the remainder
|
|
96
|
+
of the License, and in such case Affirmer hereby affirms that he or she
|
|
97
|
+
will not (i) exercise any of his or her remaining Copyright and Related
|
|
98
|
+
Rights in the Work or (ii) assert any associated claims and causes of
|
|
99
|
+
action with respect to the Work, in either case contrary to Affirmer's
|
|
100
|
+
express Statement of Purpose.
|
|
101
|
+
|
|
102
|
+
4. Limitations and Disclaimers.
|
|
103
|
+
|
|
104
|
+
a. No trademark or patent rights held by Affirmer are waived, abandoned,
|
|
105
|
+
surrendered, licensed or otherwise affected by this document.
|
|
106
|
+
b. Affirmer offers the Work as-is and makes no representations or
|
|
107
|
+
warranties of any kind concerning the Work, express, implied,
|
|
108
|
+
statutory or otherwise, including without limitation warranties of
|
|
109
|
+
title, merchantability, fitness for a particular purpose, non
|
|
110
|
+
infringement, or the absence of latent or other defects, accuracy, or
|
|
111
|
+
the present or absence of errors, whether or not discoverable, all to
|
|
112
|
+
the greatest extent permissible under applicable law.
|
|
113
|
+
c. Affirmer disclaims responsibility for clearing rights of other persons
|
|
114
|
+
that may apply to the Work or any use thereof, including without
|
|
115
|
+
limitation any person's Copyright and Related Rights in the Work.
|
|
116
|
+
Further, Affirmer disclaims responsibility for obtaining any necessary
|
|
117
|
+
consents, permissions or other rights required for any use of the
|
|
118
|
+
Work.
|
|
119
|
+
d. Affirmer understands and acknowledges that Creative Commons is not a
|
|
120
|
+
party to this document and has no duty or obligation with respect to
|
|
121
|
+
this CC0 or use of the Work.
|