asterion-solver 0.4.0__py3-none-win_amd64.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.
- asterion/__init__.py +41 -0
- asterion/_catalogs.py +183 -0
- asterion/_native.py +259 -0
- asterion/_solver.py +512 -0
- asterion/libasterion.dll +0 -0
- asterion/py.typed +0 -0
- asterion_solver-0.4.0.dist-info/METADATA +176 -0
- asterion_solver-0.4.0.dist-info/RECORD +13 -0
- asterion_solver-0.4.0.dist-info/WHEEL +4 -0
- asterion_solver-0.4.0.dist-info/licenses/LICENSE.txt +202 -0
- asterion_solver-0.4.0.dist-info/licenses/third-party/ijg-libjpeg.md +87 -0
- asterion_solver-0.4.0.dist-info/licenses/third-party/openngc.md +21 -0
- asterion_solver-0.4.0.dist-info/licenses/third-party/vizier-nebulae.md +21 -0
asterion/__init__.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Copyright 2026 the original author or authors.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
"""
|
|
15
|
+
Asterion Solver, a fast blind astrometric plate solver: finds the celestial coordinates, scale and orientation of
|
|
16
|
+
images which are in memory, such as the frames of a camera.
|
|
17
|
+
|
|
18
|
+
import asterion
|
|
19
|
+
|
|
20
|
+
asterion.install_catalog("tycho2") # once per machine
|
|
21
|
+
|
|
22
|
+
with asterion.Solver() as solver: # the catalogs stay open between solves
|
|
23
|
+
result = solver.solve(frame) # a 2D array of gray levels
|
|
24
|
+
if result.solved:
|
|
25
|
+
print(result.ra, result.dec, result.scale)
|
|
26
|
+
"""
|
|
27
|
+
from importlib import metadata as _metadata
|
|
28
|
+
|
|
29
|
+
from ._catalogs import (Catalog, InstallationCancelled, Progress, catalogs, default_catalog_directory, install_catalog,
|
|
30
|
+
version)
|
|
31
|
+
from ._native import AsterionError
|
|
32
|
+
from ._solver import DeepSkyObject, Match, Result, Solver
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
__version__ = _metadata.version("asterion-solver")
|
|
36
|
+
except _metadata.PackageNotFoundError:
|
|
37
|
+
# not installed: the sources
|
|
38
|
+
__version__ = "unknown"
|
|
39
|
+
|
|
40
|
+
__all__ = ["AsterionError", "Catalog", "DeepSkyObject", "InstallationCancelled", "Match", "Progress", "Result", "Solver",
|
|
41
|
+
"catalogs", "default_catalog_directory", "install_catalog", "version"]
|
asterion/_catalogs.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# Copyright 2026 the original author or authors.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
"""The catalogs of stars, which Asterion installs once on a machine."""
|
|
15
|
+
import os
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Callable, Optional, Union
|
|
19
|
+
|
|
20
|
+
from . import _native
|
|
21
|
+
from ._native import AsterionError
|
|
22
|
+
|
|
23
|
+
PathLike = Union[str, bytes, os.PathLike]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class InstallationCancelled(AsterionError):
|
|
27
|
+
"""The progress callback stopped the installation of a catalog. Installing it again resumes it."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
InstallationCancelled.__module__ = "asterion"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class Catalog:
|
|
35
|
+
"""
|
|
36
|
+
A catalog: one which can be installed, or one which is installed in a directory. The numbers of the index are
|
|
37
|
+
None when it isn't installed.
|
|
38
|
+
|
|
39
|
+
Attributes:
|
|
40
|
+
name: its name, such as "gaia-500", or the one of its index file, without .astx
|
|
41
|
+
description: what it is, and the fields of view it's for, or "" if Asterion doesn't know it
|
|
42
|
+
installed: whether it's installed in the directory
|
|
43
|
+
available: whether install_catalog can install it
|
|
44
|
+
size: the size of its index file, in bytes
|
|
45
|
+
stars: the number of stars of its index
|
|
46
|
+
quads: the number of quads of its index, the patterns of stars it searches for
|
|
47
|
+
magnitude_limit: the magnitude of its faintest stars
|
|
48
|
+
min_quad_arcmin: the size of its smallest quads, in arcminutes
|
|
49
|
+
max_quad_deg: the size of its largest quads, in degrees
|
|
50
|
+
error: why its index file can't be read, or None
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
name: str
|
|
54
|
+
description: str
|
|
55
|
+
installed: bool
|
|
56
|
+
available: bool
|
|
57
|
+
size: Optional[int]
|
|
58
|
+
stars: Optional[int]
|
|
59
|
+
quads: Optional[int]
|
|
60
|
+
magnitude_limit: Optional[float]
|
|
61
|
+
min_quad_arcmin: Optional[float]
|
|
62
|
+
max_quad_deg: Optional[float]
|
|
63
|
+
error: Optional[str]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True)
|
|
67
|
+
class Progress:
|
|
68
|
+
"""
|
|
69
|
+
An event of the installation of a catalog, the ones of asterion --download-catalog --json.
|
|
70
|
+
|
|
71
|
+
Attributes:
|
|
72
|
+
event: "step" when a step starts, "progress" when it made progress, "detail" when it did something worth
|
|
73
|
+
telling, "done" when it's over, whether it succeeded or failed, or "info" for what isn't part of a step
|
|
74
|
+
step: what the step does, for people, such as "Downloading gaia-500 (1,234 MB)", or None for "info"
|
|
75
|
+
unit: the unit of the work of the step, in plural form, such as "MB"
|
|
76
|
+
completed: the units of work done since the step started
|
|
77
|
+
total: the units of work of the step, or None if it isn't known
|
|
78
|
+
fraction: the part of the step which is done, from 0 to 1, or None if it isn't known
|
|
79
|
+
bytes: the bytes transferred since the step started, or None if the step transfers no data
|
|
80
|
+
message: the message of the "detail" and "info" events
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
event: str
|
|
84
|
+
step: Optional[str]
|
|
85
|
+
unit: Optional[str]
|
|
86
|
+
completed: int
|
|
87
|
+
total: Optional[int]
|
|
88
|
+
fraction: Optional[float]
|
|
89
|
+
bytes: Optional[int]
|
|
90
|
+
message: Optional[str]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def version() -> str:
|
|
94
|
+
"""The version of the library, such as "0.4.0"."""
|
|
95
|
+
return _native.string("asterion_version")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def default_catalog_directory() -> Path:
|
|
99
|
+
"""The directory of the catalogs which the functions use by default: $ASTERION_CATALOGS, or ~/.asterion/catalogs,
|
|
100
|
+
the one of the command line tool."""
|
|
101
|
+
return Path(_native.string("asterion_default_catalog_directory"))
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def catalogs(directory: Optional[PathLike] = None) -> list[Catalog]:
|
|
105
|
+
"""The catalogs which can be installed, then the other ones which are installed in a directory, by default the one
|
|
106
|
+
of the command line tool."""
|
|
107
|
+
directory = _native.path(directory)
|
|
108
|
+
count = _native.check(_native.call("asterion_catalogs", directory, None, 0))
|
|
109
|
+
described = (_native.Catalog * count)()
|
|
110
|
+
# an index file may have been removed meanwhile
|
|
111
|
+
count = min(count, _native.check(_native.call("asterion_catalogs", directory, described, count)))
|
|
112
|
+
return [_catalog(catalog) for catalog in described[:count]]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def install_catalog(name: str, directory: Optional[PathLike] = None,
|
|
116
|
+
progress: Optional[Callable[[Progress], Optional[bool]]] = None) -> None:
|
|
117
|
+
"""
|
|
118
|
+
Installs a catalog, such as "tycho2" or "gaia-500", the way asterion --download-catalog does, and returns when
|
|
119
|
+
it's over. An interrupted installation resumes its download the next time. Ctrl-C cancels it, between two events
|
|
120
|
+
of its progress. A solver which uses the catalog must be closed before installing it again.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
name: the name of the catalog, as catalogs() lists them
|
|
124
|
+
directory: the directory of the catalogs, by default the one of the command line tool
|
|
125
|
+
progress: receives the progress, on the calling thread: returning True cancels the installation, which raises
|
|
126
|
+
InstallationCancelled. An exception of the callback, such as KeyboardInterrupt, cancels it too, and is
|
|
127
|
+
raised once the installation stopped.
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
InstallationCancelled: if the callback cancelled the installation
|
|
131
|
+
AsterionError: if the catalog is unknown, or can't be installed
|
|
132
|
+
"""
|
|
133
|
+
failure = None
|
|
134
|
+
|
|
135
|
+
def receive(event, user_data):
|
|
136
|
+
# Python code which runs during the installation: KeyboardInterrupt is raised there
|
|
137
|
+
nonlocal failure
|
|
138
|
+
try:
|
|
139
|
+
return 1 if progress is not None and progress(_progress(event.contents)) else 0
|
|
140
|
+
except BaseException as e:
|
|
141
|
+
failure = e
|
|
142
|
+
return 1
|
|
143
|
+
|
|
144
|
+
# the callback must stay referenced while the library may call it
|
|
145
|
+
callback = _native.ProgressCallback(receive)
|
|
146
|
+
status = _native.call("asterion_install_catalog", _native.path(directory), name.encode("utf-8"), callback, None)
|
|
147
|
+
if failure is not None:
|
|
148
|
+
raise failure
|
|
149
|
+
if status == _native.CANCELLED:
|
|
150
|
+
raise InstallationCancelled(f"The installation of {name} was cancelled")
|
|
151
|
+
_native.check(status)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _catalog(catalog):
|
|
155
|
+
def number(value):
|
|
156
|
+
return _native.optional(value) if catalog.installed else None
|
|
157
|
+
|
|
158
|
+
return Catalog(
|
|
159
|
+
name=_native.text(catalog.name),
|
|
160
|
+
description=_native.text(catalog.description),
|
|
161
|
+
installed=bool(catalog.installed),
|
|
162
|
+
available=bool(catalog.available),
|
|
163
|
+
size=catalog.size if catalog.installed else None,
|
|
164
|
+
stars=catalog.stars if catalog.installed else None,
|
|
165
|
+
quads=catalog.quads if catalog.installed else None,
|
|
166
|
+
magnitude_limit=number(catalog.magnitude_limit),
|
|
167
|
+
min_quad_arcmin=number(catalog.min_quad_arcmin),
|
|
168
|
+
max_quad_deg=number(catalog.max_quad_deg),
|
|
169
|
+
error=_native.text(catalog.error) or None,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _progress(event):
|
|
174
|
+
return Progress(
|
|
175
|
+
event=_native.EVENTS.get(event.event, str(event.event)),
|
|
176
|
+
step=_native.text(event.step),
|
|
177
|
+
unit=_native.text(event.unit),
|
|
178
|
+
completed=event.completed,
|
|
179
|
+
total=event.total if event.total >= 0 else None,
|
|
180
|
+
fraction=_native.optional(event.fraction),
|
|
181
|
+
bytes=event.bytes if event.bytes >= 0 else None,
|
|
182
|
+
message=_native.text(event.message),
|
|
183
|
+
)
|
asterion/_native.py
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# Copyright 2026 the original author or authors.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
"""
|
|
15
|
+
The native library: the structures and the functions of asterion.h, and the GraalVM isolate in which the
|
|
16
|
+
library runs. The isolate is created the first time the library is used, and each Python thread which
|
|
17
|
+
calls the library is attached to it.
|
|
18
|
+
"""
|
|
19
|
+
import ctypes as c
|
|
20
|
+
import math
|
|
21
|
+
import os
|
|
22
|
+
import sys
|
|
23
|
+
import threading
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AsterionError(Exception):
|
|
28
|
+
"""An error which the library reports, with its message."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
AsterionError.__module__ = "asterion"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# The structures of asterion.h, field by field: ASTERION_MAX_SIP_ORDER and ASTERION_MAX_SIP_INVERSE_ORDER
|
|
35
|
+
_SIP = c.c_double * 6 * 6
|
|
36
|
+
_SIP_INVERSE = c.c_double * 8 * 8
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Options(c.Structure):
|
|
40
|
+
_fields_ = [(name, c.c_double) for name in
|
|
41
|
+
("ra", "dec", "radius", "scale_low", "scale_high", "fov", "pixel_size", "timeout", "sigma", "epoch")] + \
|
|
42
|
+
[(name, c.c_int32) for name in ("parity", "sip_order", "binning", "max_stars", "quad_stars", "padding")]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Solution(c.Structure):
|
|
46
|
+
_fields_ = [(name, c.c_double) for name in
|
|
47
|
+
("ra", "dec", "scale", "rotation", "field_width", "field_height", "rms", "log_odds", "focal_length",
|
|
48
|
+
"pixel_size", "fwhm_pixels", "fwhm_arcsec")] + \
|
|
49
|
+
[("crpix", c.c_double * 2), ("crval", c.c_double * 2), ("cd", c.c_double * 4),
|
|
50
|
+
("sip_a", _SIP), ("sip_b", _SIP), ("sip_ap", _SIP_INVERSE), ("sip_bp", _SIP_INVERSE),
|
|
51
|
+
("read_ms", c.c_double), ("detect_ms", c.c_double), ("solve_ms", c.c_double)] + \
|
|
52
|
+
[(name, c.c_int32) for name in ("sip_order", "sip_inverse_order", "flipped", "matched_stars", "stars")] + \
|
|
53
|
+
[("index", c.c_char * 64), ("padding", c.c_int32)]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Object(c.Structure):
|
|
57
|
+
_fields_ = [(name, c.c_double) for name in
|
|
58
|
+
("ra", "dec", "x", "y", "distance", "major_arcmin", "minor_arcmin", "angle", "magnitude")] + \
|
|
59
|
+
[("messier", c.c_int32), ("name", c.c_char * 32), ("designation", c.c_char * 32),
|
|
60
|
+
("common_name", c.c_char * 128), ("aliases", c.c_char * 256), ("type", c.c_char * 32)]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Match(c.Structure):
|
|
64
|
+
_fields_ = [(name, c.c_double) for name in ("x", "y", "ra", "dec", "residual")]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Catalog(c.Structure):
|
|
68
|
+
_fields_ = [("name", c.c_char * 64), ("description", c.c_char * 256), ("installed", c.c_int32), ("available", c.c_int32),
|
|
69
|
+
("size", c.c_int64), ("stars", c.c_int64), ("quads", c.c_int64), ("magnitude_limit", c.c_double),
|
|
70
|
+
("min_quad_arcmin", c.c_double), ("max_quad_deg", c.c_double), ("error", c.c_char * 256)]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class Progress(c.Structure):
|
|
74
|
+
_fields_ = [("event", c.c_int32), ("padding", c.c_int32), ("completed", c.c_int64), ("total", c.c_int64),
|
|
75
|
+
("bytes", c.c_int64), ("fraction", c.c_double), ("step", c.c_char_p), ("unit", c.c_char_p),
|
|
76
|
+
("message", c.c_char_p)]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
ProgressCallback = c.CFUNCTYPE(c.c_int, c.POINTER(Progress), c.c_void_p)
|
|
80
|
+
|
|
81
|
+
UINT8, UINT16, FLOAT32 = 1, 2, 3
|
|
82
|
+
SOLVED, NOT_SOLVED, ERROR, CANCELLED = 1, 0, -1, -2
|
|
83
|
+
PARITIES = {"both": 0, "normal": 1, "flipped": 2}
|
|
84
|
+
EVENTS = {1: "step", 2: "progress", 3: "detail", 4: "done", 5: "info"}
|
|
85
|
+
|
|
86
|
+
_STRUCTURES = {"asterion_options": Options, "asterion_solution": Solution, "asterion_object": Object,
|
|
87
|
+
"asterion_match": Match, "asterion_catalog": Catalog, "asterion_progress": Progress}
|
|
88
|
+
|
|
89
|
+
_p = c.c_void_p
|
|
90
|
+
_SIGNATURES = {
|
|
91
|
+
"graal_create_isolate": (c.c_int, [_p, c.POINTER(_p), c.POINTER(_p)]),
|
|
92
|
+
"graal_attach_thread": (c.c_int, [_p, c.POINTER(_p)]),
|
|
93
|
+
"graal_detach_thread": (c.c_int, [_p]),
|
|
94
|
+
"asterion_version": (c.c_int, [_p, c.c_char_p, c.c_size_t]),
|
|
95
|
+
"asterion_default_catalog_directory": (c.c_int, [_p, c.c_char_p, c.c_size_t]),
|
|
96
|
+
"asterion_catalogs": (c.c_int, [_p, c.c_char_p, c.POINTER(Catalog), c.c_int]),
|
|
97
|
+
"asterion_install_catalog": (c.c_int, [_p, c.c_char_p, c.c_char_p, ProgressCallback, _p]),
|
|
98
|
+
"asterion_open": (c.c_int, [_p, c.c_char_p, c.POINTER(_p)]),
|
|
99
|
+
"asterion_close": (c.c_int, [_p, _p]),
|
|
100
|
+
"asterion_default_options": (c.c_int, [_p, c.POINTER(Options)]),
|
|
101
|
+
"asterion_solve": (c.c_int, [_p, _p, _p, c.c_int, c.c_int, c.c_int, c.c_longlong, c.c_int, c.POINTER(Options),
|
|
102
|
+
c.POINTER(Solution), c.POINTER(_p)]),
|
|
103
|
+
"asterion_result_free": (None, [_p, _p]),
|
|
104
|
+
"asterion_result_json": (c.c_int, [_p, _p, c.c_char_p, c.c_size_t]),
|
|
105
|
+
"asterion_result_object_count": (c.c_int, [_p, _p]),
|
|
106
|
+
"asterion_result_object": (c.c_int, [_p, _p, c.c_int, c.POINTER(Object)]),
|
|
107
|
+
"asterion_result_match_count": (c.c_int, [_p, _p]),
|
|
108
|
+
"asterion_result_match": (c.c_int, [_p, _p, c.c_int, c.POINTER(Match)]),
|
|
109
|
+
"asterion_result_pixel_to_sky": (c.c_int, [_p, _p, c.c_double, c.c_double, c.POINTER(c.c_double), c.POINTER(c.c_double)]),
|
|
110
|
+
"asterion_result_sky_to_pixel": (c.c_int, [_p, _p, c.c_double, c.c_double, c.POINTER(c.c_double), c.POINTER(c.c_double)]),
|
|
111
|
+
"asterion_error": (c.c_int, [_p, c.c_char_p, c.c_size_t]),
|
|
112
|
+
"asterion_sizeof": (c.c_int, [_p, c.c_char_p]),
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _library_file():
|
|
117
|
+
"""The library next to this module, or the one which ASTERION_LIBRARY names, to use another build."""
|
|
118
|
+
configured = os.environ.get("ASTERION_LIBRARY")
|
|
119
|
+
if configured:
|
|
120
|
+
return configured
|
|
121
|
+
name = {"win32": "libasterion.dll", "darwin": "libasterion.dylib"}.get(sys.platform, "libasterion.so")
|
|
122
|
+
return str(Path(__file__).with_name(name))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class _Attachment:
|
|
126
|
+
"""The thread of the isolate of a Python thread, which is detached when the Python thread ends."""
|
|
127
|
+
|
|
128
|
+
def __init__(self, library, thread):
|
|
129
|
+
self.library = library
|
|
130
|
+
self.thread = thread
|
|
131
|
+
self.ident = threading.get_ident()
|
|
132
|
+
self.pid = os.getpid()
|
|
133
|
+
|
|
134
|
+
def __del__(self):
|
|
135
|
+
# Python releases the thread-local data of a thread when it ends, on that thread: a thread can only detach
|
|
136
|
+
# itself. A forked process has a copy of the isolate, without its threads, which it must not touch. At exit,
|
|
137
|
+
# the library may be gone already
|
|
138
|
+
if threading.get_ident() == self.ident and os.getpid() == self.pid:
|
|
139
|
+
try:
|
|
140
|
+
self.library.graal_detach_thread(self.thread)
|
|
141
|
+
except Exception:
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class _Isolate:
|
|
146
|
+
def __init__(self):
|
|
147
|
+
self._lock = threading.Lock()
|
|
148
|
+
self._library = None
|
|
149
|
+
self._isolate = None
|
|
150
|
+
self._forked = False
|
|
151
|
+
self._local = threading.local()
|
|
152
|
+
if hasattr(os, "register_at_fork"):
|
|
153
|
+
os.register_at_fork(after_in_child=self._after_fork)
|
|
154
|
+
|
|
155
|
+
def _after_fork(self):
|
|
156
|
+
# the isolate of the parent can't be used, and the lock may have been held by one of its threads
|
|
157
|
+
self._lock = threading.Lock()
|
|
158
|
+
self._forked = self._library is not None
|
|
159
|
+
|
|
160
|
+
def usable(self):
|
|
161
|
+
"""Whether the library can be called: it's loaded, and this process isn't a fork of the one which loaded it."""
|
|
162
|
+
return self._library is not None and not self._forked
|
|
163
|
+
|
|
164
|
+
def thread(self):
|
|
165
|
+
"""The library, and the thread of the isolate of the calling thread, attached the first time."""
|
|
166
|
+
if self._forked:
|
|
167
|
+
raise AsterionError("The library can't be used in a process forked after it was started: "
|
|
168
|
+
"start the processes with multiprocessing.set_start_method('spawn')")
|
|
169
|
+
attachment = getattr(self._local, "attachment", None)
|
|
170
|
+
if attachment is not None:
|
|
171
|
+
return self._library, attachment.thread
|
|
172
|
+
with self._lock:
|
|
173
|
+
if self._library is None:
|
|
174
|
+
self._create()
|
|
175
|
+
return self._library, self._local.attachment.thread
|
|
176
|
+
thread = c.c_void_p()
|
|
177
|
+
if self._library.graal_attach_thread(self._isolate, c.byref(thread)) != 0:
|
|
178
|
+
raise AsterionError("Unable to attach the thread to the isolate of the library")
|
|
179
|
+
# a finalizer which ran meanwhile, on this thread, may have attached it already: to the same isolate thread
|
|
180
|
+
attachment = getattr(self._local, "attachment", None)
|
|
181
|
+
if attachment is None:
|
|
182
|
+
attachment = self._local.attachment = _Attachment(self._library, thread)
|
|
183
|
+
return self._library, attachment.thread
|
|
184
|
+
|
|
185
|
+
def _create(self):
|
|
186
|
+
library = c.CDLL(_library_file())
|
|
187
|
+
for name, (result, arguments) in _SIGNATURES.items():
|
|
188
|
+
function = getattr(library, name)
|
|
189
|
+
function.restype, function.argtypes = result, arguments
|
|
190
|
+
isolate, thread = c.c_void_p(), c.c_void_p()
|
|
191
|
+
if library.graal_create_isolate(None, c.byref(isolate), c.byref(thread)) != 0:
|
|
192
|
+
raise AsterionError("Unable to create the isolate of the library")
|
|
193
|
+
for name, structure in _STRUCTURES.items():
|
|
194
|
+
if library.asterion_sizeof(thread, name.encode()) != c.sizeof(structure):
|
|
195
|
+
raise AsterionError(f"{name} doesn't match the library {_library_file()}")
|
|
196
|
+
self._library, self._isolate = library, isolate
|
|
197
|
+
self._local.attachment = _Attachment(library, thread)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
_ISOLATE = _Isolate()
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def call(name, *arguments):
|
|
204
|
+
"""Calls a function of the library on the thread of the caller, which it receives first."""
|
|
205
|
+
library, thread = _ISOLATE.thread()
|
|
206
|
+
return getattr(library, name)(thread, *arguments)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def release(name, handle):
|
|
210
|
+
"""Releases a handle when its object is garbage collected, which may happen at exit, or in a forked process:
|
|
211
|
+
nothing is reported, as nobody can handle it."""
|
|
212
|
+
if _ISOLATE.usable():
|
|
213
|
+
try:
|
|
214
|
+
call(name, handle)
|
|
215
|
+
except Exception:
|
|
216
|
+
pass
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def error():
|
|
220
|
+
"""The message of the last error of the calling thread."""
|
|
221
|
+
return string("asterion_error")
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def check(status):
|
|
225
|
+
"""The status of a call, or AsterionError with its message if it failed."""
|
|
226
|
+
if status == ERROR:
|
|
227
|
+
raise AsterionError(error())
|
|
228
|
+
return status
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def string(name, *arguments):
|
|
232
|
+
"""A string which a function copies to a buffer: the function tells the size it needs, if the first one is
|
|
233
|
+
too small."""
|
|
234
|
+
buffer = c.create_string_buffer(4096)
|
|
235
|
+
length = call(name, *arguments, buffer, len(buffer))
|
|
236
|
+
if length < 0 and name != "asterion_error":
|
|
237
|
+
raise AsterionError(error())
|
|
238
|
+
if length >= len(buffer):
|
|
239
|
+
buffer = c.create_string_buffer(length + 1)
|
|
240
|
+
call(name, *arguments, buffer, len(buffer))
|
|
241
|
+
return buffer.value.decode("utf-8", errors="replace")
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def path(directory):
|
|
245
|
+
"""A directory for the library, UTF-8 encoded, or None for the default one."""
|
|
246
|
+
if directory is None:
|
|
247
|
+
return None
|
|
248
|
+
directory = os.fspath(directory)
|
|
249
|
+
return directory if isinstance(directory, bytes) else directory.encode("utf-8")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def text(value):
|
|
253
|
+
"""A string of a structure, or of a progress event."""
|
|
254
|
+
return value.decode("utf-8", errors="replace") if value is not None else None
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def optional(value):
|
|
258
|
+
"""A number of a structure, where NAN means unknown: None."""
|
|
259
|
+
return None if math.isnan(value) else value
|