galley-jl-python 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.
- galley_jl_python/__init__.py +361 -0
- galley_jl_python/_array_api_info.py +94 -0
- galley_jl_python/_sysimage.py +222 -0
- galley_jl_python/compiled.py +171 -0
- galley_jl_python/dtypes.py +61 -0
- galley_jl_python/einstein.py +561 -0
- galley_jl_python/errors.py +2 -0
- galley_jl_python/finch_fused.py +33 -0
- galley_jl_python/io.py +15 -0
- galley_jl_python/julia.py +37 -0
- galley_jl_python/juliapkg.json +386 -0
- galley_jl_python/juliapkg_dev.json +22 -0
- galley_jl_python/levels.py +110 -0
- galley_jl_python/linalg/__init__.py +3 -0
- galley_jl_python/linalg/_linalg.py +26 -0
- galley_jl_python/tensor.py +1739 -0
- galley_jl_python/typing.py +17 -0
- galley_jl_python-0.1.0.dist-info/METADATA +214 -0
- galley_jl_python-0.1.0.dist-info/RECORD +21 -0
- galley_jl_python-0.1.0.dist-info/WHEEL +4 -0
- galley_jl_python-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
from operator import (
|
|
2
|
+
abs as abs,
|
|
3
|
+
)
|
|
4
|
+
from operator import (
|
|
5
|
+
add as add,
|
|
6
|
+
)
|
|
7
|
+
from operator import (
|
|
8
|
+
and_ as bitwise_and,
|
|
9
|
+
)
|
|
10
|
+
from operator import (
|
|
11
|
+
eq as equal,
|
|
12
|
+
)
|
|
13
|
+
from operator import (
|
|
14
|
+
floordiv as floor_divide,
|
|
15
|
+
)
|
|
16
|
+
from operator import (
|
|
17
|
+
ge as greater_equal,
|
|
18
|
+
)
|
|
19
|
+
from operator import (
|
|
20
|
+
gt as greater,
|
|
21
|
+
)
|
|
22
|
+
from operator import (
|
|
23
|
+
invert as bitwise_invert,
|
|
24
|
+
)
|
|
25
|
+
from operator import (
|
|
26
|
+
le as less_equal,
|
|
27
|
+
)
|
|
28
|
+
from operator import (
|
|
29
|
+
lshift as bitwise_left_shift,
|
|
30
|
+
)
|
|
31
|
+
from operator import (
|
|
32
|
+
lt as less,
|
|
33
|
+
)
|
|
34
|
+
from operator import (
|
|
35
|
+
matmul as matmul,
|
|
36
|
+
)
|
|
37
|
+
from operator import (
|
|
38
|
+
mod as remainder,
|
|
39
|
+
)
|
|
40
|
+
from operator import (
|
|
41
|
+
mul as multiply,
|
|
42
|
+
)
|
|
43
|
+
from operator import (
|
|
44
|
+
ne as not_equal,
|
|
45
|
+
)
|
|
46
|
+
from operator import (
|
|
47
|
+
neg as negative,
|
|
48
|
+
)
|
|
49
|
+
from operator import (
|
|
50
|
+
or_ as bitwise_or,
|
|
51
|
+
)
|
|
52
|
+
from operator import (
|
|
53
|
+
pos as positive,
|
|
54
|
+
)
|
|
55
|
+
from operator import (
|
|
56
|
+
pow as pow,
|
|
57
|
+
)
|
|
58
|
+
from operator import (
|
|
59
|
+
rshift as bitwise_right_shift,
|
|
60
|
+
)
|
|
61
|
+
from operator import (
|
|
62
|
+
sub as subtract,
|
|
63
|
+
)
|
|
64
|
+
from operator import (
|
|
65
|
+
truediv as divide,
|
|
66
|
+
)
|
|
67
|
+
from operator import (
|
|
68
|
+
xor as bitwise_xor,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
from numpy import (
|
|
72
|
+
e as e,
|
|
73
|
+
)
|
|
74
|
+
from numpy import (
|
|
75
|
+
inf as inf,
|
|
76
|
+
)
|
|
77
|
+
from numpy import (
|
|
78
|
+
nan as nan,
|
|
79
|
+
)
|
|
80
|
+
from numpy import (
|
|
81
|
+
newaxis as newaxis,
|
|
82
|
+
)
|
|
83
|
+
from numpy import (
|
|
84
|
+
pi as pi,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
from . import linalg
|
|
88
|
+
from ._array_api_info import __array_namespace_info__
|
|
89
|
+
from .compiled import (
|
|
90
|
+
DefaultScheduler,
|
|
91
|
+
GalleyScheduler,
|
|
92
|
+
compiled,
|
|
93
|
+
compute,
|
|
94
|
+
lazy,
|
|
95
|
+
set_optimizer,
|
|
96
|
+
)
|
|
97
|
+
from .dtypes import (
|
|
98
|
+
bool,
|
|
99
|
+
can_cast,
|
|
100
|
+
complex64,
|
|
101
|
+
complex128,
|
|
102
|
+
finfo,
|
|
103
|
+
float16,
|
|
104
|
+
float32,
|
|
105
|
+
float64,
|
|
106
|
+
iinfo,
|
|
107
|
+
int8,
|
|
108
|
+
int16,
|
|
109
|
+
int32,
|
|
110
|
+
int64,
|
|
111
|
+
int_,
|
|
112
|
+
uint,
|
|
113
|
+
uint8,
|
|
114
|
+
uint16,
|
|
115
|
+
uint32,
|
|
116
|
+
uint64,
|
|
117
|
+
)
|
|
118
|
+
from .finch_fused import jit
|
|
119
|
+
from .io import (
|
|
120
|
+
read,
|
|
121
|
+
write,
|
|
122
|
+
)
|
|
123
|
+
from .levels import (
|
|
124
|
+
Dense,
|
|
125
|
+
DenseStorage,
|
|
126
|
+
Element,
|
|
127
|
+
Pattern,
|
|
128
|
+
RepeatRLE,
|
|
129
|
+
SparseByteMap,
|
|
130
|
+
SparseCOO,
|
|
131
|
+
SparseHash,
|
|
132
|
+
SparseList,
|
|
133
|
+
SparseVBL,
|
|
134
|
+
Storage,
|
|
135
|
+
)
|
|
136
|
+
from .tensor import (
|
|
137
|
+
SparseArray,
|
|
138
|
+
Tensor,
|
|
139
|
+
acos,
|
|
140
|
+
acosh,
|
|
141
|
+
all,
|
|
142
|
+
any,
|
|
143
|
+
arange,
|
|
144
|
+
argmax,
|
|
145
|
+
argmin,
|
|
146
|
+
asarray,
|
|
147
|
+
asin,
|
|
148
|
+
asinh,
|
|
149
|
+
astype,
|
|
150
|
+
atan,
|
|
151
|
+
atan2,
|
|
152
|
+
atanh,
|
|
153
|
+
ceil,
|
|
154
|
+
conj,
|
|
155
|
+
cos,
|
|
156
|
+
cosh,
|
|
157
|
+
diagonal,
|
|
158
|
+
einop,
|
|
159
|
+
einsum,
|
|
160
|
+
empty,
|
|
161
|
+
empty_like,
|
|
162
|
+
exp,
|
|
163
|
+
expand_dims,
|
|
164
|
+
expm1,
|
|
165
|
+
eye,
|
|
166
|
+
floor,
|
|
167
|
+
full,
|
|
168
|
+
full_like,
|
|
169
|
+
imag,
|
|
170
|
+
isfinite,
|
|
171
|
+
isinf,
|
|
172
|
+
isnan,
|
|
173
|
+
linspace,
|
|
174
|
+
log,
|
|
175
|
+
log1p,
|
|
176
|
+
log2,
|
|
177
|
+
log10,
|
|
178
|
+
logaddexp,
|
|
179
|
+
logical_and,
|
|
180
|
+
logical_or,
|
|
181
|
+
logical_xor,
|
|
182
|
+
max,
|
|
183
|
+
mean,
|
|
184
|
+
min,
|
|
185
|
+
moveaxis,
|
|
186
|
+
nonzero,
|
|
187
|
+
ones,
|
|
188
|
+
ones_like,
|
|
189
|
+
permute_dims,
|
|
190
|
+
power,
|
|
191
|
+
prod,
|
|
192
|
+
random,
|
|
193
|
+
real,
|
|
194
|
+
reshape,
|
|
195
|
+
round,
|
|
196
|
+
sign,
|
|
197
|
+
sin,
|
|
198
|
+
sinh,
|
|
199
|
+
sqrt,
|
|
200
|
+
square,
|
|
201
|
+
squeeze,
|
|
202
|
+
std,
|
|
203
|
+
sum,
|
|
204
|
+
tan,
|
|
205
|
+
tanh,
|
|
206
|
+
tensordot,
|
|
207
|
+
trunc,
|
|
208
|
+
var,
|
|
209
|
+
where,
|
|
210
|
+
zeros,
|
|
211
|
+
zeros_like,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
__all__ = [
|
|
215
|
+
"DefaultScheduler",
|
|
216
|
+
"Dense",
|
|
217
|
+
"DenseStorage",
|
|
218
|
+
"Element",
|
|
219
|
+
"GalleyScheduler",
|
|
220
|
+
"Pattern",
|
|
221
|
+
"RepeatRLE",
|
|
222
|
+
"SparseArray",
|
|
223
|
+
"SparseByteMap",
|
|
224
|
+
"SparseCOO",
|
|
225
|
+
"SparseHash",
|
|
226
|
+
"SparseList",
|
|
227
|
+
"SparseVBL",
|
|
228
|
+
"Storage",
|
|
229
|
+
"Tensor",
|
|
230
|
+
"__array_namespace_info__",
|
|
231
|
+
"abs",
|
|
232
|
+
"acos",
|
|
233
|
+
"acosh",
|
|
234
|
+
"add",
|
|
235
|
+
"all",
|
|
236
|
+
"any",
|
|
237
|
+
"arange",
|
|
238
|
+
"argmax",
|
|
239
|
+
"argmin",
|
|
240
|
+
"asarray",
|
|
241
|
+
"asin",
|
|
242
|
+
"asinh",
|
|
243
|
+
"astype",
|
|
244
|
+
"atan",
|
|
245
|
+
"atan2",
|
|
246
|
+
"atanh",
|
|
247
|
+
"bitwise_and",
|
|
248
|
+
"bitwise_invert",
|
|
249
|
+
"bitwise_left_shift",
|
|
250
|
+
"bitwise_or",
|
|
251
|
+
"bitwise_right_shift",
|
|
252
|
+
"bitwise_xor",
|
|
253
|
+
"bool",
|
|
254
|
+
"can_cast",
|
|
255
|
+
"ceil",
|
|
256
|
+
"compiled",
|
|
257
|
+
"complex64",
|
|
258
|
+
"complex128",
|
|
259
|
+
"compute",
|
|
260
|
+
"conj",
|
|
261
|
+
"cos",
|
|
262
|
+
"cosh",
|
|
263
|
+
"diagonal",
|
|
264
|
+
"divide",
|
|
265
|
+
"e",
|
|
266
|
+
"einop",
|
|
267
|
+
"einsum",
|
|
268
|
+
"empty",
|
|
269
|
+
"empty_like",
|
|
270
|
+
"equal",
|
|
271
|
+
"exp",
|
|
272
|
+
"expand_dims",
|
|
273
|
+
"expm1",
|
|
274
|
+
"eye",
|
|
275
|
+
"finfo",
|
|
276
|
+
"float16",
|
|
277
|
+
"float32",
|
|
278
|
+
"float64",
|
|
279
|
+
"floor",
|
|
280
|
+
"floor_divide",
|
|
281
|
+
"full",
|
|
282
|
+
"full_like",
|
|
283
|
+
"greater",
|
|
284
|
+
"greater_equal",
|
|
285
|
+
"iinfo",
|
|
286
|
+
"imag",
|
|
287
|
+
"inf",
|
|
288
|
+
"int8",
|
|
289
|
+
"int16",
|
|
290
|
+
"int32",
|
|
291
|
+
"int64",
|
|
292
|
+
"int_",
|
|
293
|
+
"isfinite",
|
|
294
|
+
"isinf",
|
|
295
|
+
"isnan",
|
|
296
|
+
"jit",
|
|
297
|
+
"lazy",
|
|
298
|
+
"less",
|
|
299
|
+
"less_equal",
|
|
300
|
+
"linalg",
|
|
301
|
+
"linspace",
|
|
302
|
+
"log",
|
|
303
|
+
"log1p",
|
|
304
|
+
"log2",
|
|
305
|
+
"log10",
|
|
306
|
+
"logaddexp",
|
|
307
|
+
"logical_and",
|
|
308
|
+
"logical_or",
|
|
309
|
+
"logical_xor",
|
|
310
|
+
"matmul",
|
|
311
|
+
"max",
|
|
312
|
+
"mean",
|
|
313
|
+
"min",
|
|
314
|
+
"moveaxis",
|
|
315
|
+
"multiply",
|
|
316
|
+
"nan",
|
|
317
|
+
"negative",
|
|
318
|
+
"newaxis",
|
|
319
|
+
"nonzero",
|
|
320
|
+
"not_equal",
|
|
321
|
+
"ones",
|
|
322
|
+
"ones_like",
|
|
323
|
+
"permute_dims",
|
|
324
|
+
"pi",
|
|
325
|
+
"positive",
|
|
326
|
+
"pow",
|
|
327
|
+
"power",
|
|
328
|
+
"prod",
|
|
329
|
+
"random",
|
|
330
|
+
"read",
|
|
331
|
+
"real",
|
|
332
|
+
"remainder",
|
|
333
|
+
"reshape",
|
|
334
|
+
"round",
|
|
335
|
+
"set_optimizer",
|
|
336
|
+
"sign",
|
|
337
|
+
"sin",
|
|
338
|
+
"sinh",
|
|
339
|
+
"sqrt",
|
|
340
|
+
"square",
|
|
341
|
+
"squeeze",
|
|
342
|
+
"std",
|
|
343
|
+
"subtract",
|
|
344
|
+
"sum",
|
|
345
|
+
"tan",
|
|
346
|
+
"tanh",
|
|
347
|
+
"tensordot",
|
|
348
|
+
"trunc",
|
|
349
|
+
"uint",
|
|
350
|
+
"uint8",
|
|
351
|
+
"uint16",
|
|
352
|
+
"uint32",
|
|
353
|
+
"uint64",
|
|
354
|
+
"var",
|
|
355
|
+
"where",
|
|
356
|
+
"write",
|
|
357
|
+
"zeros",
|
|
358
|
+
"zeros_like",
|
|
359
|
+
]
|
|
360
|
+
|
|
361
|
+
__array_api_version__: str = "2024.12"
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from . import dtypes
|
|
2
|
+
from .typing import DType
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class __array_namespace_info__:
|
|
6
|
+
def capabilities(self) -> dict[str, bool]:
|
|
7
|
+
return {
|
|
8
|
+
"boolean indexing": True,
|
|
9
|
+
"data-dependent shapes": True,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
def default_device(self) -> str:
|
|
13
|
+
return "cpu"
|
|
14
|
+
|
|
15
|
+
def default_dtypes(self, *, device: str | None = None) -> dict[str, DType]:
|
|
16
|
+
if device not in ["cpu", None]:
|
|
17
|
+
raise ValueError(
|
|
18
|
+
f'Device not understood. Only "cpu" is allowed, but received: {device}'
|
|
19
|
+
)
|
|
20
|
+
return {
|
|
21
|
+
"real floating": dtypes.float64,
|
|
22
|
+
"complex floating": dtypes.complex128,
|
|
23
|
+
"integral": dtypes.int_,
|
|
24
|
+
"indexing": dtypes.int_,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
_bool_dtypes = {"bool": dtypes.bool}
|
|
28
|
+
_signed_integer_dtypes = {
|
|
29
|
+
"int8": dtypes.int8,
|
|
30
|
+
"int16": dtypes.int16,
|
|
31
|
+
"int32": dtypes.int32,
|
|
32
|
+
"int64": dtypes.int64,
|
|
33
|
+
}
|
|
34
|
+
_unsigned_integer_dtypes = {
|
|
35
|
+
"uint8": dtypes.uint8,
|
|
36
|
+
"uint16": dtypes.uint16,
|
|
37
|
+
"uint32": dtypes.uint32,
|
|
38
|
+
"uint64": dtypes.uint64,
|
|
39
|
+
}
|
|
40
|
+
_real_floating_dtypes = {
|
|
41
|
+
"float32": dtypes.float32,
|
|
42
|
+
"float64": dtypes.float64,
|
|
43
|
+
}
|
|
44
|
+
_complex_floating_dtypes = {
|
|
45
|
+
"complex64": dtypes.complex64,
|
|
46
|
+
"complex128": dtypes.complex128,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
def dtypes(
|
|
50
|
+
self,
|
|
51
|
+
*,
|
|
52
|
+
device: str | None = None,
|
|
53
|
+
kind: str | tuple[str, ...] | None = None,
|
|
54
|
+
) -> dict[str, DType]:
|
|
55
|
+
if device not in ["cpu", None]:
|
|
56
|
+
raise ValueError(
|
|
57
|
+
f'Device not understood. Only "cpu" is allowed, but received: {device}'
|
|
58
|
+
)
|
|
59
|
+
if kind is None:
|
|
60
|
+
return (
|
|
61
|
+
self._bool_dtypes
|
|
62
|
+
| self._signed_integer_dtypes
|
|
63
|
+
| self._unsigned_integer_dtypes
|
|
64
|
+
| self._real_floating_dtypes
|
|
65
|
+
| self._complex_floating_dtypes
|
|
66
|
+
)
|
|
67
|
+
if kind == "bool":
|
|
68
|
+
return self._bool_dtypes
|
|
69
|
+
if kind == "signed integer":
|
|
70
|
+
return self._signed_integer_dtypes
|
|
71
|
+
if kind == "unsigned integer":
|
|
72
|
+
return self._unsigned_integer_dtypes
|
|
73
|
+
if kind == "integral":
|
|
74
|
+
return self._signed_integer_dtypes | self._unsigned_integer_dtypes
|
|
75
|
+
if kind == "real floating":
|
|
76
|
+
return self._real_floating_dtypes
|
|
77
|
+
if kind == "complex floating":
|
|
78
|
+
return self._complex_floating_dtypes
|
|
79
|
+
if kind == "numeric":
|
|
80
|
+
return (
|
|
81
|
+
self._signed_integer_dtypes
|
|
82
|
+
| self._unsigned_integer_dtypes
|
|
83
|
+
| self._real_floating_dtypes
|
|
84
|
+
| self._complex_floating_dtypes
|
|
85
|
+
)
|
|
86
|
+
if isinstance(kind, tuple):
|
|
87
|
+
res = {}
|
|
88
|
+
for k in kind:
|
|
89
|
+
res.update(self.dtypes(kind=k))
|
|
90
|
+
return res
|
|
91
|
+
raise ValueError(f"unsupported kind: {kind!r}")
|
|
92
|
+
|
|
93
|
+
def devices(self) -> list[str]:
|
|
94
|
+
return ["cpu"]
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""Locate, and if needed download, a prebuilt Julia sysimage for juliacall.
|
|
2
|
+
|
|
3
|
+
This module must not import juliacall: it runs before Julia starts.
|
|
4
|
+
|
|
5
|
+
A sysimage only works with the exact Julia version and package versions it was
|
|
6
|
+
built from, so each image is named after a hash of both (see ``env_key``) and
|
|
7
|
+
lives in a per-user cache directory. An image is used only when its name
|
|
8
|
+
matches the current environment; otherwise Julia starts normally.
|
|
9
|
+
|
|
10
|
+
On import, galley downloads the image CI published for this platform and
|
|
11
|
+
environment if it isn't cached yet, and uses it right away. Set
|
|
12
|
+
``GALLEY_JL_PYTHON_SYSIMAGE=0`` to never download or load one, or
|
|
13
|
+
``PYTHON_JULIACALL_SYSIMAGE`` to use a specific image. In a galley checkout,
|
|
14
|
+
``pixi run build-sysimage`` builds one locally.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import hashlib
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import platform
|
|
21
|
+
import subprocess
|
|
22
|
+
import sys
|
|
23
|
+
import tempfile
|
|
24
|
+
import time
|
|
25
|
+
import tomllib
|
|
26
|
+
import urllib.error
|
|
27
|
+
import urllib.request
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
RELEASES_URL = "https://github.com/finch-tensor/galley-jl-python/releases/download"
|
|
31
|
+
# Bump when images built from the same environment become incompatible, so the
|
|
32
|
+
# new images get new names instead of reusing the published ones.
|
|
33
|
+
# 2: PythonCall finds juliacall from Python (v1 only worked on the build host)
|
|
34
|
+
IMAGE_FORMAT = 2
|
|
35
|
+
_EXTENSIONS = {"linux": "so", "darwin": "dylib", "win32": "dll"}
|
|
36
|
+
# after a 404, don't ask again for this long (images are published after CI)
|
|
37
|
+
_MISSING_TTL = 24 * 60 * 60
|
|
38
|
+
# a lock older than this is left over from a download that was killed
|
|
39
|
+
_STALE_LOCK = 2 * 60 * 60
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def cache_dir() -> Path:
|
|
43
|
+
if custom := os.environ.get("GALLEY_JL_PYTHON_CACHE"):
|
|
44
|
+
return Path(custom)
|
|
45
|
+
if sys.platform == "win32":
|
|
46
|
+
base = Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData" / "Local"))
|
|
47
|
+
elif sys.platform == "darwin":
|
|
48
|
+
base = Path.home() / "Library" / "Caches"
|
|
49
|
+
else:
|
|
50
|
+
base = Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache"))
|
|
51
|
+
return base / "galley-jl-python"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def env_key(project: str) -> str:
|
|
55
|
+
"""Hash the Julia version and resolved package set of a juliapkg project."""
|
|
56
|
+
manifest = tomllib.loads((Path(project) / "Manifest.toml").read_text())
|
|
57
|
+
resolved = sorted(
|
|
58
|
+
(name, entry.get("uuid"), entry.get("version"), entry.get("git-tree-sha1"))
|
|
59
|
+
for name, entries in manifest["deps"].items()
|
|
60
|
+
for entry in entries
|
|
61
|
+
)
|
|
62
|
+
payload = json.dumps([manifest["julia_version"], resolved])
|
|
63
|
+
return hashlib.sha256(payload.encode()).hexdigest()[:16]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def release_tag(key: str) -> str:
|
|
67
|
+
return f"sysimage-{key}"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def image_name(key: str) -> str:
|
|
71
|
+
ext = _EXTENSIONS.get(sys.platform, "so")
|
|
72
|
+
arch = platform.machine().lower()
|
|
73
|
+
return f"galley-v{IMAGE_FORMAT}-{sys.platform}-{arch}-{key}.{ext}"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def image_path(key: str) -> Path:
|
|
77
|
+
return cache_dir() / image_name(key)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def current_key() -> str:
|
|
81
|
+
import juliapkg
|
|
82
|
+
|
|
83
|
+
return env_key(juliapkg.project())
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _log(message: str) -> None:
|
|
87
|
+
print(f"[galley-jl-python] {message}", file=sys.stderr, flush=True)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _acquire_lock(lock: Path) -> bool:
|
|
91
|
+
for _ in range(2):
|
|
92
|
+
try:
|
|
93
|
+
os.close(os.open(lock, os.O_CREAT | os.O_EXCL | os.O_WRONLY))
|
|
94
|
+
return True
|
|
95
|
+
except FileExistsError:
|
|
96
|
+
try:
|
|
97
|
+
if time.time() - lock.stat().st_mtime < _STALE_LOCK:
|
|
98
|
+
return False
|
|
99
|
+
lock.unlink()
|
|
100
|
+
except FileNotFoundError:
|
|
101
|
+
pass
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _download(url: str, dest: Path, attempts: int = 4) -> None:
|
|
106
|
+
"""Download ``url`` to ``dest``, resuming after a dropped connection."""
|
|
107
|
+
size = done = 0
|
|
108
|
+
next_report = 0.25
|
|
109
|
+
for attempt in range(attempts):
|
|
110
|
+
headers = {"Range": f"bytes={done}-"} if done else {}
|
|
111
|
+
try:
|
|
112
|
+
req = urllib.request.Request(url, headers=headers)
|
|
113
|
+
with (
|
|
114
|
+
urllib.request.urlopen(req, timeout=60) as resp,
|
|
115
|
+
dest.open("ab") as out,
|
|
116
|
+
):
|
|
117
|
+
if done and resp.status != 206: # server ignored the range
|
|
118
|
+
out.truncate(0)
|
|
119
|
+
done = 0
|
|
120
|
+
if not size:
|
|
121
|
+
size = done + int(resp.headers.get("Content-Length") or 0)
|
|
122
|
+
_log(
|
|
123
|
+
f"downloading the prebuilt Julia sysimage ({size >> 20} MB), "
|
|
124
|
+
"once per environment; set GALLEY_JL_PYTHON_SYSIMAGE=0 to skip"
|
|
125
|
+
)
|
|
126
|
+
while chunk := resp.read(1 << 20):
|
|
127
|
+
out.write(chunk)
|
|
128
|
+
done += len(chunk)
|
|
129
|
+
if size and done / size >= next_report:
|
|
130
|
+
_log(f" {done * 100 // size}%")
|
|
131
|
+
next_report += 0.25
|
|
132
|
+
if not size or done >= size:
|
|
133
|
+
return
|
|
134
|
+
except urllib.error.HTTPError:
|
|
135
|
+
raise
|
|
136
|
+
except (urllib.error.URLError, OSError):
|
|
137
|
+
if attempt == attempts - 1:
|
|
138
|
+
raise
|
|
139
|
+
_log(
|
|
140
|
+
f" connection dropped at {done >> 20} MB; resuming"
|
|
141
|
+
if done
|
|
142
|
+
else " couldn't connect; retrying"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _works(image: Path) -> bool:
|
|
147
|
+
"""Check that juliacall starts with ``image``, in a throwaway process."""
|
|
148
|
+
env = {**os.environ, "PYTHON_JULIACALL_SYSIMAGE": str(image)}
|
|
149
|
+
env["GALLEY_JL_PYTHON_SYSIMAGE"] = "0"
|
|
150
|
+
try:
|
|
151
|
+
result = subprocess.run(
|
|
152
|
+
[sys.executable, "-c", "from juliacall import Main; Main.seval('1 + 1')"],
|
|
153
|
+
env=env,
|
|
154
|
+
capture_output=True,
|
|
155
|
+
timeout=600,
|
|
156
|
+
)
|
|
157
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
158
|
+
return False
|
|
159
|
+
return result.returncode == 0
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def fetch(key: str) -> Path | None:
|
|
163
|
+
"""Download the published image for ``key`` into the cache.
|
|
164
|
+
|
|
165
|
+
Returns the image path, or None if there is no image or it couldn't be
|
|
166
|
+
downloaded. Never raises: galley works without an image, just with a slow
|
|
167
|
+
first call. If another process is already downloading, returns None
|
|
168
|
+
immediately instead of waiting.
|
|
169
|
+
"""
|
|
170
|
+
image = image_path(key)
|
|
171
|
+
if image.is_file():
|
|
172
|
+
return image
|
|
173
|
+
missing = image.with_name(image.name + ".missing")
|
|
174
|
+
lock = image.with_name(image.name + ".lock")
|
|
175
|
+
try:
|
|
176
|
+
if missing.is_file() and time.time() - missing.stat().st_mtime < _MISSING_TTL:
|
|
177
|
+
return None
|
|
178
|
+
image.parent.mkdir(parents=True, exist_ok=True)
|
|
179
|
+
if not _acquire_lock(lock):
|
|
180
|
+
return None
|
|
181
|
+
except OSError:
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
url = f"{RELEASES_URL}/{release_tag(key)}/{image.name}"
|
|
185
|
+
partial = None
|
|
186
|
+
try:
|
|
187
|
+
fd, partial = tempfile.mkstemp(dir=image.parent, suffix=".part")
|
|
188
|
+
os.close(fd)
|
|
189
|
+
_download(url, Path(partial))
|
|
190
|
+
if not _works(Path(partial)):
|
|
191
|
+
_log("the downloaded sysimage doesn't load here; starting without it")
|
|
192
|
+
missing.touch()
|
|
193
|
+
return None
|
|
194
|
+
os.chmod(partial, 0o644)
|
|
195
|
+
os.replace(partial, image)
|
|
196
|
+
partial = None
|
|
197
|
+
_log(f"saved {image}")
|
|
198
|
+
return image
|
|
199
|
+
except urllib.error.HTTPError as e:
|
|
200
|
+
if e.code == 404:
|
|
201
|
+
missing.touch()
|
|
202
|
+
else:
|
|
203
|
+
_log(f"sysimage download failed ({e}); starting without it")
|
|
204
|
+
return None
|
|
205
|
+
except (urllib.error.URLError, OSError) as e:
|
|
206
|
+
_log(f"sysimage download failed ({e}); starting without it")
|
|
207
|
+
return None
|
|
208
|
+
finally:
|
|
209
|
+
if partial is not None and os.path.exists(partial):
|
|
210
|
+
os.remove(partial)
|
|
211
|
+
lock.unlink(missing_ok=True)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def configure() -> None:
|
|
215
|
+
"""Point juliacall at this environment's sysimage, downloading it if needed."""
|
|
216
|
+
if os.environ.get("GALLEY_JL_PYTHON_SYSIMAGE") == "0":
|
|
217
|
+
return
|
|
218
|
+
if "PYTHON_JULIACALL_SYSIMAGE" in os.environ:
|
|
219
|
+
return
|
|
220
|
+
image = fetch(current_key())
|
|
221
|
+
if image is not None:
|
|
222
|
+
os.environ["PYTHON_JULIACALL_SYSIMAGE"] = str(image)
|