timewise 0.4.10__tar.gz → 0.5.2__tar.gz
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.
- {timewise-0.4.10 → timewise-0.5.2}/PKG-INFO +12 -7
- timewise-0.5.2/pyproject.toml +50 -0
- {timewise-0.4.10 → timewise-0.5.2}/timewise/__init__.py +0 -2
- {timewise-0.4.10 → timewise-0.5.2}/timewise/big_parent_sample.py +4 -5
- {timewise-0.4.10 → timewise-0.5.2}/timewise/config_loader.py +2 -2
- timewise-0.5.2/timewise/general.py +52 -0
- {timewise-0.4.10 → timewise-0.5.2}/timewise/parent_sample_base.py +9 -7
- {timewise-0.4.10 → timewise-0.5.2}/timewise/utils.py +85 -15
- {timewise-0.4.10 → timewise-0.5.2}/timewise/wise_bigdata_desy_cluster.py +57 -51
- {timewise-0.4.10 → timewise-0.5.2}/timewise/wise_data_base.py +161 -74
- {timewise-0.4.10 → timewise-0.5.2}/timewise/wise_data_by_visit.py +8 -2
- timewise-0.4.10/pyproject.toml +0 -50
- timewise-0.4.10/timewise/general.py +0 -44
- {timewise-0.4.10 → timewise-0.5.2}/LICENSE +0 -0
- {timewise-0.4.10 → timewise-0.5.2}/README.md +0 -0
- {timewise-0.4.10 → timewise-0.5.2}/timewise/cli.py +0 -0
- {timewise-0.4.10 → timewise-0.5.2}/timewise/point_source_utils.py +0 -0
- {timewise-0.4.10 → timewise-0.5.2}/timewise/wise_flux_conversion_correction.dat +0 -0
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: timewise
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.2
|
|
4
4
|
Summary: A small package to download infrared data from the WISE satellite
|
|
5
|
-
Home-page: https://github.com/JannisNe/timewise
|
|
6
5
|
License: MIT
|
|
7
6
|
Author: Jannis Necker
|
|
8
7
|
Author-email: jannis.necker@gmail.com
|
|
@@ -13,25 +12,31 @@ Classifier: Programming Language :: Python :: 3.8
|
|
|
13
12
|
Classifier: Programming Language :: Python :: 3.9
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.10
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
-
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Provides-Extra: docs
|
|
17
|
+
Requires-Dist: astropy (>=5.1,<6.0.0)
|
|
17
18
|
Requires-Dist: backoff (>=2.1.2,<3.0.0)
|
|
19
|
+
Requires-Dist: coveralls (>=3.3.1,<4.0.0) ; extra == "dev"
|
|
18
20
|
Requires-Dist: jupyter[jupyter] (>=1.0.0,<2.0.0)
|
|
19
21
|
Requires-Dist: jupyterlab[jupyter] (>=4.0.6,<5.0.0)
|
|
20
22
|
Requires-Dist: matplotlib (>=3.5.3,<4.0.0)
|
|
23
|
+
Requires-Dist: myst-parser (>=1,<3) ; extra == "docs"
|
|
21
24
|
Requires-Dist: numpy (>=1.23.2,<2.0.0)
|
|
22
25
|
Requires-Dist: pandas (>=1.4.3,<3.0.0)
|
|
23
26
|
Requires-Dist: pydantic (>=1.9.0,<2.0.0)
|
|
27
|
+
Requires-Dist: pytest (>=7.2.2,<8.0.0) ; extra == "dev"
|
|
24
28
|
Requires-Dist: pyvo (>=1.4.1,<2.0.0)
|
|
25
29
|
Requires-Dist: requests (>=2.28.1,<3.0.0)
|
|
26
|
-
Requires-Dist: scikit-image (>=0.19.3,<0.
|
|
30
|
+
Requires-Dist: scikit-image (>=0.19.3,<0.22.0)
|
|
27
31
|
Requires-Dist: scikit-learn (>=1.3.0,<2.0.0)
|
|
28
|
-
Requires-Dist: seaborn (>=0.11.2,<0.
|
|
32
|
+
Requires-Dist: seaborn (>=0.11.2,<0.14.0)
|
|
33
|
+
Requires-Dist: sphinx-rtd-theme (>=1.3.0,<2.0.0) ; extra == "docs"
|
|
29
34
|
Requires-Dist: tqdm (>=4.64.0,<5.0.0)
|
|
30
35
|
Requires-Dist: urllib3 (==1.26.15)
|
|
31
36
|
Requires-Dist: virtualenv (>=20.16.3,<21.0.0)
|
|
32
37
|
Project-URL: Bug Tracker, https://github.com/JannisNe/timewise/issues
|
|
33
38
|
Project-URL: Documentation, https://timewise.readthedocs.io/en/latest
|
|
34
|
-
Project-URL:
|
|
39
|
+
Project-URL: Homepage, https://github.com/JannisNe/timewise
|
|
35
40
|
Description-Content-Type: text/markdown
|
|
36
41
|
|
|
37
42
|
[](https://github.com/JannisNe/timewise/actions/workflows/continous_integration.yml)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["poetry-core>=2.0.0"]
|
|
3
|
+
build-backend = "poetry.core.masonry.api"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "timewise"
|
|
7
|
+
version = "0.5.2"
|
|
8
|
+
description = "A small package to download infrared data from the WISE satellite"
|
|
9
|
+
authors = [
|
|
10
|
+
{ name = "Jannis Necker", email = "jannis.necker@gmail.com" },
|
|
11
|
+
]
|
|
12
|
+
license = { text = "MIT" }
|
|
13
|
+
readme = "README.md"
|
|
14
|
+
requires-python = ">=3.8,<3.12"
|
|
15
|
+
dependencies = [
|
|
16
|
+
"tqdm>=4.64.0,<5.0.0",
|
|
17
|
+
"requests>=2.28.1,<3.0.0",
|
|
18
|
+
"pandas>=1.4.3,<3.0.0",
|
|
19
|
+
"numpy>=1.23.2,<2.0.0",
|
|
20
|
+
"pyvo>=1.4.1,<2.0.0",
|
|
21
|
+
"astropy>=5.1,<6.0.0",
|
|
22
|
+
"matplotlib>=3.5.3,<4.0.0",
|
|
23
|
+
"scikit-image>=0.19.3,<0.22.0",
|
|
24
|
+
"backoff>=2.1.2,<3.0.0",
|
|
25
|
+
"virtualenv>=20.16.3,<21.0.0",
|
|
26
|
+
"seaborn>=0.11.2,<0.14.0",
|
|
27
|
+
"urllib3==1.26.15",
|
|
28
|
+
"pydantic>=1.9.0,<2.0.0",
|
|
29
|
+
"scikit-learn>=1.3.0,<2.0.0",
|
|
30
|
+
"jupyterlab[jupyter]>=4.0.6,<5.0.0",
|
|
31
|
+
"jupyter[jupyter]>=1.0.0,<2.0.0",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
timewise = "timewise.cli:timewise_cli"
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://github.com/JannisNe/timewise"
|
|
39
|
+
Documentation = "https://timewise.readthedocs.io/en/latest"
|
|
40
|
+
"Bug Tracker" = "https://github.com/JannisNe/timewise/issues"
|
|
41
|
+
|
|
42
|
+
[project.optional-dependencies]
|
|
43
|
+
dev = [
|
|
44
|
+
"coveralls>=3.3.1,<4.0.0",
|
|
45
|
+
"pytest>=7.2.2,<8.0.0",
|
|
46
|
+
]
|
|
47
|
+
docs = [
|
|
48
|
+
"myst-parser>=1,<3",
|
|
49
|
+
"sphinx-rtd-theme>=1.3.0,<2.0.0",
|
|
50
|
+
]
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import gc
|
|
2
|
-
import os
|
|
3
2
|
import pickle
|
|
4
3
|
import threading
|
|
5
4
|
import time
|
|
@@ -30,7 +29,7 @@ class BigParentSampleBase(ParentSampleBase):
|
|
|
30
29
|
self._keep_df_in_memory = keep_file_in_memory
|
|
31
30
|
self._time_when_df_was_used_last = time.time()
|
|
32
31
|
self._df = None
|
|
33
|
-
self._cache_file =
|
|
32
|
+
self._cache_file = self.cache_dir / "cache.pkl"
|
|
34
33
|
self._lock_cache_file = False
|
|
35
34
|
|
|
36
35
|
self._clean_thread = threading.Thread(target=self._periodically_drop_df_to_disk, daemon=True, name='ParentSampleCleanThread').start()
|
|
@@ -50,7 +49,7 @@ class BigParentSampleBase(ParentSampleBase):
|
|
|
50
49
|
|
|
51
50
|
if isinstance(self._df, type(None)):
|
|
52
51
|
|
|
53
|
-
if
|
|
52
|
+
if self._cache_file.is_file():
|
|
54
53
|
logger.debug(f'loading from {self._cache_file}')
|
|
55
54
|
self._wait_for_unlock_cache_file()
|
|
56
55
|
self._lock_cache_file = True
|
|
@@ -97,9 +96,9 @@ class BigParentSampleBase(ParentSampleBase):
|
|
|
97
96
|
logger.debug('stopped clean thread')
|
|
98
97
|
|
|
99
98
|
def __del__(self):
|
|
100
|
-
if hasattr(self, "_cache_file") and
|
|
99
|
+
if hasattr(self, "_cache_file") and self._cache_file.is_file():
|
|
101
100
|
logger.debug(f'removing {self._cache_file}')
|
|
102
|
-
|
|
101
|
+
self._cache_file.unlink()
|
|
103
102
|
|
|
104
103
|
if hasattr(self, "clean_thread"):
|
|
105
104
|
logger.debug(f'stopping clean thread')
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import yaml
|
|
3
3
|
import json
|
|
4
|
-
import os
|
|
5
4
|
import inspect
|
|
6
5
|
from pydantic import BaseModel, validator
|
|
7
6
|
import pandas as pd
|
|
8
7
|
import importlib
|
|
8
|
+
from pathlib import Path
|
|
9
9
|
|
|
10
10
|
from timewise.parent_sample_base import ParentSampleBase
|
|
11
11
|
from timewise.wise_data_base import WISEDataBase
|
|
@@ -80,7 +80,7 @@ class TimewiseConfigLoader(BaseModel):
|
|
|
80
80
|
@validator("filename")
|
|
81
81
|
def validate_file(cls, v: str):
|
|
82
82
|
if v is not None:
|
|
83
|
-
if not
|
|
83
|
+
if not Path(v).is_file():
|
|
84
84
|
raise ValueError(f"No file {v}!")
|
|
85
85
|
return v
|
|
86
86
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# Setting up the Logger
|
|
7
|
+
main_logger = logging.getLogger('timewise')
|
|
8
|
+
logger_format = logging.Formatter(
|
|
9
|
+
'%(levelname)s:%(name)s:%(funcName)s - [%(threadName)s] - %(asctime)s: \n\t%(message)s', "%H:%M:%S"
|
|
10
|
+
)
|
|
11
|
+
stream_handler = logging.StreamHandler()
|
|
12
|
+
stream_handler.setFormatter(logger_format)
|
|
13
|
+
main_logger.addHandler(stream_handler)
|
|
14
|
+
main_logger.propagate = False # do not propagate to root logger
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_directories() -> dict[str, Path | None]:
|
|
20
|
+
# Setting up data directory
|
|
21
|
+
DATA_DIR_KEY = 'TIMEWISE_DATA'
|
|
22
|
+
if DATA_DIR_KEY in os.environ:
|
|
23
|
+
data_dir = Path(os.environ[DATA_DIR_KEY]).expanduser()
|
|
24
|
+
else:
|
|
25
|
+
logger.warning(f'{DATA_DIR_KEY} not set! Using home directory.')
|
|
26
|
+
data_dir = Path('~/').expanduser()
|
|
27
|
+
|
|
28
|
+
BIGDATA_DIR_KEY = 'TIMEWISE_BIGDATA'
|
|
29
|
+
if BIGDATA_DIR_KEY in os.environ:
|
|
30
|
+
bigdata_dir = Path(os.environ[BIGDATA_DIR_KEY]).expanduser()
|
|
31
|
+
logger.info(f"Using bigdata directory {bigdata_dir}")
|
|
32
|
+
else:
|
|
33
|
+
bigdata_dir = None
|
|
34
|
+
logger.info(f"No bigdata directory set.")
|
|
35
|
+
|
|
36
|
+
output_dir = data_dir / 'output'
|
|
37
|
+
plots_dir = output_dir / 'plots'
|
|
38
|
+
cache_dir = data_dir / 'cache'
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
'data_dir': data_dir,
|
|
42
|
+
'bigdata_dir': bigdata_dir,
|
|
43
|
+
'output_dir': output_dir,
|
|
44
|
+
'plots_dir': plots_dir,
|
|
45
|
+
'cache_dir': cache_dir
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def backoff_hndlr(details):
|
|
50
|
+
logger.info("Backing off {wait:0.1f} seconds after {tries} tries "
|
|
51
|
+
"calling function {target} with args {args} and kwargs "
|
|
52
|
+
"{kwargs}".format(**details))
|
|
@@ -3,7 +3,7 @@ import pandas as pd
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
import logging
|
|
5
5
|
|
|
6
|
-
from timewise.general import
|
|
6
|
+
from timewise.general import get_directories
|
|
7
7
|
from timewise.utils import plot_sdss_cutout, plot_panstarrs_cutout
|
|
8
8
|
|
|
9
9
|
|
|
@@ -26,14 +26,14 @@ class ParentSampleBase(abc.ABC):
|
|
|
26
26
|
|
|
27
27
|
def __init__(self, base_name):
|
|
28
28
|
# set up directories
|
|
29
|
-
|
|
30
|
-
self.
|
|
29
|
+
d = get_directories()
|
|
30
|
+
self.cache_dir = d["cache_dir"] / base_name
|
|
31
|
+
self.plots_dir = d["plots_dir"] / base_name
|
|
31
32
|
|
|
32
33
|
for d in [self.cache_dir, self.plots_dir]:
|
|
33
|
-
|
|
34
|
-
os.makedirs(d)
|
|
34
|
+
d.parent.mkdir(parents=True, exist_ok=True)
|
|
35
35
|
|
|
36
|
-
self.local_sample_copy =
|
|
36
|
+
self.local_sample_copy = self.cache_dir / 'sample.csv'
|
|
37
37
|
|
|
38
38
|
def plot_cutout(self, ind, arcsec=20, interactive=False, **kwargs):
|
|
39
39
|
"""
|
|
@@ -54,9 +54,10 @@ class ParentSampleBase(abc.ABC):
|
|
|
54
54
|
|
|
55
55
|
fn = kwargs.pop(
|
|
56
56
|
"fn",
|
|
57
|
-
[
|
|
57
|
+
[self.plots_dir / f"{i}_{r[self.default_keymap['id']]}.pdf"
|
|
58
58
|
for i, r in sel.iterrows()]
|
|
59
59
|
)
|
|
60
|
+
self.plots_dir.mkdir(parents=True, exist_ok=True)
|
|
60
61
|
|
|
61
62
|
logger.debug(f"\nRA: {ra}\nDEC: {dec}\nTITLE: {title}\nFN: {fn}")
|
|
62
63
|
ou = list()
|
|
@@ -84,4 +85,5 @@ class ParentSampleBase(abc.ABC):
|
|
|
84
85
|
|
|
85
86
|
def save_local(self):
|
|
86
87
|
logger.debug(f"saving under {self.local_sample_copy}")
|
|
88
|
+
self.local_sample_copy.parent.mkdir(parents=True, exist_ok=True)
|
|
87
89
|
self.df.to_csv(self.local_sample_copy)
|
|
@@ -12,14 +12,20 @@ from functools import cache
|
|
|
12
12
|
from astropy.table import Table
|
|
13
13
|
from PIL import Image
|
|
14
14
|
from io import BytesIO
|
|
15
|
+
import hashlib
|
|
16
|
+
from threading import Thread
|
|
17
|
+
from queue import Queue
|
|
18
|
+
import sys
|
|
15
19
|
|
|
16
|
-
|
|
17
|
-
from timewise.general import cache_dir, backoff_hndlr
|
|
20
|
+
from timewise.general import backoff_hndlr, get_directories
|
|
18
21
|
|
|
19
22
|
|
|
20
23
|
logger = logging.getLogger(__name__)
|
|
21
24
|
mirong_url = 'http://staff.ustc.edu.cn/~jnac/data_public/wisevar.txt'
|
|
22
|
-
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_mirong_path():
|
|
28
|
+
return get_directories()['cache_dir'] / 'mirong_sample.csv'
|
|
23
29
|
|
|
24
30
|
|
|
25
31
|
@cache
|
|
@@ -29,7 +35,8 @@ def get_2d_gaussian_correction(cl):
|
|
|
29
35
|
|
|
30
36
|
def get_mirong_sample():
|
|
31
37
|
|
|
32
|
-
|
|
38
|
+
mirong_path = get_mirong_path()
|
|
39
|
+
if not mirong_path.is_file():
|
|
33
40
|
|
|
34
41
|
logger.info(f'getting MIRONG sample from {mirong_url}')
|
|
35
42
|
r = requests.get(mirong_url)
|
|
@@ -44,16 +51,16 @@ def get_mirong_sample():
|
|
|
44
51
|
mirong_sample = pd.DataFrame(lll[1:-1], columns=lll[0])
|
|
45
52
|
mirong_sample['ra'] = mirong_sample['RA']
|
|
46
53
|
mirong_sample['dec'] = mirong_sample['DEC']
|
|
47
|
-
logger.debug(f'saving to {local_copy}')
|
|
48
|
-
|
|
49
|
-
mirong_sample.to_csv(local_copy, index=False)
|
|
50
54
|
logger.info(f'found {len(mirong_sample)} objects in MIRONG Sample')
|
|
55
|
+
|
|
51
56
|
mirong_sample.drop(columns=['ra', 'dec'], inplace=True)
|
|
52
|
-
|
|
57
|
+
logger.debug(f'saving to {mirong_path}')
|
|
58
|
+
mirong_path.parent.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
mirong_sample.to_csv(mirong_path, index=False)
|
|
53
60
|
|
|
54
61
|
else:
|
|
55
|
-
logger.debug(f'loading {
|
|
56
|
-
mirong_sample = pd.read_csv(
|
|
62
|
+
logger.debug(f'loading {mirong_path}')
|
|
63
|
+
mirong_sample = pd.read_csv(mirong_path)
|
|
57
64
|
|
|
58
65
|
return mirong_sample
|
|
59
66
|
|
|
@@ -150,6 +157,24 @@ class PanSTARRSQueryError(Exception):
|
|
|
150
157
|
pass
|
|
151
158
|
|
|
152
159
|
|
|
160
|
+
def load_cache_or_download(url):
|
|
161
|
+
logger.debug(f"loading or downloading {url}")
|
|
162
|
+
h = hashlib.md5(url.encode()).hexdigest()
|
|
163
|
+
cache_dir = get_directories()['cache_dir']
|
|
164
|
+
cache_file = cache_dir / (h + ".cache")
|
|
165
|
+
logger.debug(f"cache file is {cache_file}")
|
|
166
|
+
if not cache_file.is_file():
|
|
167
|
+
logger.debug(f"downloading {url}")
|
|
168
|
+
r = requests.get(url)
|
|
169
|
+
with open(cache_file, 'wb') as f:
|
|
170
|
+
f.write(r.content)
|
|
171
|
+
return r.content
|
|
172
|
+
else:
|
|
173
|
+
logger.debug(f"loading {cache_file}")
|
|
174
|
+
with open(cache_file, 'rb') as f:
|
|
175
|
+
return f.read()
|
|
176
|
+
|
|
177
|
+
|
|
153
178
|
def annotate_not_available(ax):
|
|
154
179
|
xlim = ax.get_xlim()
|
|
155
180
|
ylim = ax.get_ylim()
|
|
@@ -170,7 +195,8 @@ def getimages(ra, dec, filters="grizy"):
|
|
|
170
195
|
|
|
171
196
|
service = "https://ps1images.stsci.edu/cgi-bin/ps1filenames.py"
|
|
172
197
|
url = f"{service}?ra={ra}&dec={dec}&filters={filters}"
|
|
173
|
-
|
|
198
|
+
content = load_cache_or_download(url)
|
|
199
|
+
table = Table.read(content.decode(), format='ascii')
|
|
174
200
|
return table
|
|
175
201
|
|
|
176
202
|
|
|
@@ -231,8 +257,8 @@ def getcolorim(ra, dec, size=240, output_size=None, filters="grizy", format="jpg
|
|
|
231
257
|
if format not in ("jpg", "png"):
|
|
232
258
|
raise ValueError("format must be jpg or png")
|
|
233
259
|
url = geturl(ra, dec, size=size, filters=filters, output_size=output_size, format=format, color=True)
|
|
234
|
-
|
|
235
|
-
im = Image.open(BytesIO(
|
|
260
|
+
content = load_cache_or_download(url)
|
|
261
|
+
im = Image.open(BytesIO(content))
|
|
236
262
|
return im
|
|
237
263
|
|
|
238
264
|
|
|
@@ -253,8 +279,8 @@ def getgrayim(ra, dec, size=240, output_size=None, filter="g", format="jpg"):
|
|
|
253
279
|
if filter not in list("grizy"):
|
|
254
280
|
raise ValueError("filter must be one of grizy")
|
|
255
281
|
url = geturl(ra, dec, size=size, filters=filter, output_size=output_size, format=format)
|
|
256
|
-
|
|
257
|
-
im = Image.open(BytesIO(
|
|
282
|
+
content = load_cache_or_download(url[0])
|
|
283
|
+
im = Image.open(BytesIO(content))
|
|
258
284
|
return im
|
|
259
285
|
|
|
260
286
|
|
|
@@ -446,3 +472,47 @@ class StableTAPService(vo.dal.TAPService):
|
|
|
446
472
|
#######################################################
|
|
447
473
|
# END CUSTOM TAP Service #
|
|
448
474
|
###########################################################################################################
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
###########################################################################################################
|
|
478
|
+
# START CUSTOM TAP Service #
|
|
479
|
+
#######################################################
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
class ErrorQueue(Queue):
|
|
483
|
+
"""Queue subclass whose join() re-raises exceptions from worker threads."""
|
|
484
|
+
|
|
485
|
+
def __init__(self, *args, **kwargs):
|
|
486
|
+
super().__init__(*args, **kwargs)
|
|
487
|
+
self.error_queue = Queue()
|
|
488
|
+
|
|
489
|
+
def report_error(self, exc_info):
|
|
490
|
+
"""Called by workers to push an exception into the error queue."""
|
|
491
|
+
self.error_queue.put(exc_info)
|
|
492
|
+
# Also decrement unfinished_tasks, so join() won't block forever
|
|
493
|
+
with self.all_tasks_done:
|
|
494
|
+
self.unfinished_tasks = max(0, self.unfinished_tasks - 1)
|
|
495
|
+
self.all_tasks_done.notify_all()
|
|
496
|
+
|
|
497
|
+
def join(self):
|
|
498
|
+
"""Wait until all tasks are done, or raise if a worker failed."""
|
|
499
|
+
with self.all_tasks_done:
|
|
500
|
+
while self.unfinished_tasks:
|
|
501
|
+
if not self.error_queue.empty():
|
|
502
|
+
exc_info = self.error_queue.get()
|
|
503
|
+
raise exc_info[1].with_traceback(exc_info[2])
|
|
504
|
+
self.all_tasks_done.wait()
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
class ExceptionSafeThread(Thread):
|
|
508
|
+
"""Thread subclass that reports uncaught exceptions to the ErrorQueue."""
|
|
509
|
+
|
|
510
|
+
def __init__(self, error_queue: ErrorQueue, *args, **kwargs):
|
|
511
|
+
super().__init__(*args, **kwargs)
|
|
512
|
+
self.error_queue = error_queue
|
|
513
|
+
|
|
514
|
+
def run(self):
|
|
515
|
+
try:
|
|
516
|
+
super().run()
|
|
517
|
+
except Exception:
|
|
518
|
+
self.error_queue.report_error(sys.exc_info())
|