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.
@@ -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)