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/_solver.py ADDED
@@ -0,0 +1,512 @@
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
+ """Solving images: the solver, which keeps the catalogs open, and the results of its solves."""
15
+ import ctypes as c
16
+ import json
17
+ import sys
18
+ import threading
19
+ import weakref
20
+ from contextlib import contextmanager
21
+ from dataclasses import dataclass
22
+ from functools import cached_property
23
+ from typing import Any, Optional
24
+
25
+ from . import _native
26
+ from ._catalogs import PathLike
27
+ from ._native import AsterionError
28
+
29
+ # The sample types by their names, then by their kinds, as __array_interface__ names them: code, size
30
+ _SAMPLE_TYPES = {"uint8": (_native.UINT8, 1), "uint16": (_native.UINT16, 2), "float32": (_native.FLOAT32, 4)}
31
+ _KINDS = {"u1": "uint8", "u2": "uint16", "f4": "float32"}
32
+ # the formats of the buffer protocol, and their byte orders
33
+ _FORMATS = {"B": "u1", "H": "u2", "f": "f4"}
34
+ _NATIVE = "<" if sys.byteorder == "little" else ">"
35
+ _ORDERS = {"@": _NATIVE, "=": _NATIVE, "|": _NATIVE, "<": "<", ">": ">", "!": ">"}
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class DeepSkyObject:
40
+ """
41
+ A deep sky object of an image: a galaxy, a nebula, a cluster...
42
+
43
+ Attributes:
44
+ name: the name it's best known by: "M 31", "NGC 7000"
45
+ designation: its designation in its catalog: "NGC 224"
46
+ type: its kind, in words: "galaxy", "HII region"...
47
+ common_name: its common names, separated by commas, or ""
48
+ aliases: its designations in other catalogs, separated by commas, or ""
49
+ messier: its number in the Messier catalog, or None
50
+ ra: the right ascension of its center, J2000, in degrees
51
+ dec: the declination of its center, in degrees
52
+ x: its position in the image, in pixels, which may be outside of it
53
+ y: its position in the image, in pixels
54
+ distance: the distance of its center to the one of the image, in degrees
55
+ major_arcmin: its major axis, in arcminutes, or None
56
+ minor_arcmin: its minor axis, in arcminutes, or None
57
+ angle: the position angle of its major axis, in degrees east of north, or None
58
+ magnitude: its visual magnitude, or None
59
+ """
60
+
61
+ name: str
62
+ designation: str
63
+ type: str
64
+ common_name: str
65
+ aliases: str
66
+ messier: Optional[int]
67
+ ra: float
68
+ dec: float
69
+ x: float
70
+ y: float
71
+ distance: float
72
+ major_arcmin: Optional[float]
73
+ minor_arcmin: Optional[float]
74
+ angle: Optional[float]
75
+ magnitude: Optional[float]
76
+
77
+
78
+ @dataclass(frozen=True)
79
+ class Match:
80
+ """
81
+ A star of an image which was matched with the catalog.
82
+
83
+ Attributes:
84
+ x: its position in the image, in pixels
85
+ y: its position in the image, in pixels
86
+ ra: the right ascension of the catalog star at the date of the image, in degrees
87
+ dec: the declination of the catalog star at the date of the image, in degrees
88
+ residual: the distance between the two according to the solution, in arcseconds
89
+ """
90
+
91
+ x: float
92
+ y: float
93
+ ra: float
94
+ dec: float
95
+ residual: float
96
+
97
+
98
+ class Result:
99
+ """
100
+ The result of a solve. Without a solution, solved is False and the attributes of the solution are None: the
101
+ stars which were detected, their width and the timings are known still.
102
+
103
+ Pixel positions are counted from 0: the center of the first pixel of the image is (0, 0), whatever the binning
104
+ which Asterion applied to it. The WCS follows the conventions of FITS instead, where it's (1, 1).
105
+
106
+ Results come from Solver.solve, and stay valid once the solver is closed.
107
+
108
+ Attributes:
109
+ solved: whether the image was solved
110
+ ra: the right ascension of the center of the image, J2000, in degrees
111
+ dec: the declination of the center of the image, J2000, in degrees
112
+ scale: the scale, in arcseconds per pixel
113
+ rotation: the position angle of the Y axis of the image, in degrees east of north
114
+ field_width: the width of the field of view, in degrees
115
+ field_height: the height of the field of view, in degrees
116
+ flipped: whether the image is mirrored
117
+ rms: the residuals of the matched stars, in arcseconds
118
+ log_odds: the confidence in the solution
119
+ matched_stars: the number of stars matched with the catalog
120
+ catalog: the name of the catalog which solved the image, such as "gaia-500"
121
+ focal_length: the focal length, in millimeters, when pixel_size was given
122
+ pixel_size: the size of the pixels used for the focal length, in micrometers
123
+ fwhm_arcsec: the median width of the stars, in arcseconds
124
+ stars: the number of stars detected in the image
125
+ fwhm_pixels: the median width of the stars, in pixels, or None without stars
126
+ read_ms: how long copying the pixels took, in milliseconds
127
+ detect_ms: how long detecting the stars took, in milliseconds
128
+ solve_ms: how long solving took, in milliseconds
129
+ """
130
+
131
+ solved: bool
132
+ ra: Optional[float]
133
+ dec: Optional[float]
134
+ scale: Optional[float]
135
+ rotation: Optional[float]
136
+ field_width: Optional[float]
137
+ field_height: Optional[float]
138
+ flipped: Optional[bool]
139
+ rms: Optional[float]
140
+ log_odds: Optional[float]
141
+ matched_stars: Optional[int]
142
+ catalog: Optional[str]
143
+ focal_length: Optional[float]
144
+ pixel_size: Optional[float]
145
+ fwhm_arcsec: Optional[float]
146
+ stars: int
147
+ fwhm_pixels: Optional[float]
148
+ read_ms: float
149
+ detect_ms: float
150
+ solve_ms: float
151
+
152
+ def __init__(self, solved, solution, handle):
153
+ """Results come from Solver.solve."""
154
+ self.solved = solved
155
+
156
+ def known(value):
157
+ return _native.optional(value) if solved else None
158
+
159
+ self.ra, self.dec = known(solution.ra), known(solution.dec)
160
+ self.scale, self.rotation = known(solution.scale), known(solution.rotation)
161
+ self.field_width, self.field_height = known(solution.field_width), known(solution.field_height)
162
+ self.flipped = bool(solution.flipped) if solved else None
163
+ self.rms, self.log_odds = known(solution.rms), known(solution.log_odds)
164
+ self.matched_stars = solution.matched_stars if solved else None
165
+ self.catalog = _native.text(solution.index) if solved else None
166
+ self.focal_length, self.pixel_size = known(solution.focal_length), known(solution.pixel_size)
167
+ self.fwhm_arcsec = known(solution.fwhm_arcsec)
168
+ self.stars = solution.stars
169
+ self.fwhm_pixels = _native.optional(solution.fwhm_pixels)
170
+ self.read_ms, self.detect_ms, self.solve_ms = solution.read_ms, solution.detect_ms, solution.solve_ms
171
+ self._handle = handle
172
+ # freed when the result is garbage collected
173
+ weakref.finalize(self, _native.release, "asterion_result_free", handle)
174
+
175
+ def __repr__(self):
176
+ if not self.solved:
177
+ return f"Result(solved=False, stars={self.stars})"
178
+ return (f"Result(solved=True, ra={self.ra:.6f}, dec={self.dec:.6f}, scale={self.scale:.4f}, "
179
+ f"rotation={self.rotation:.2f}, catalog={self.catalog!r})")
180
+
181
+ @cached_property
182
+ def json(self) -> str:
183
+ """The result as JSON, the output of asterion --json."""
184
+ return _native.string("asterion_result_json", self._handle)
185
+
186
+ @cached_property
187
+ def wcs(self) -> Optional[dict[str, Any]]:
188
+ """The WCS of the solution, as the keywords of a FITS header: CTYPE1, CRPIX1, CD1_1, A_ORDER, A_2_0...
189
+ astropy.wcs.WCS(result.wcs) makes the WCS of astropy. None without a solution."""
190
+ return json.loads(self.json).get("wcs") if self.solved else None
191
+
192
+ @cached_property
193
+ def objects(self) -> list[DeepSkyObject]:
194
+ """The deep sky objects of the image, the most prominent first."""
195
+ found = _native.Object()
196
+ objects = []
197
+ for i in range(self._count("asterion_result_object_count")):
198
+ _native.check(_native.call("asterion_result_object", self._handle, i, c.byref(found)))
199
+ objects.append(DeepSkyObject(
200
+ _native.text(found.name), _native.text(found.designation), _native.text(found.type),
201
+ _native.text(found.common_name), _native.text(found.aliases), found.messier or None,
202
+ found.ra, found.dec, found.x, found.y, found.distance, _native.optional(found.major_arcmin),
203
+ _native.optional(found.minor_arcmin), _native.optional(found.angle), _native.optional(found.magnitude)))
204
+ return objects
205
+
206
+ @cached_property
207
+ def matches(self) -> list[Match]:
208
+ """The stars of the image which were matched with the catalog."""
209
+ found = _native.Match()
210
+ matches = []
211
+ for i in range(self._count("asterion_result_match_count")):
212
+ _native.check(_native.call("asterion_result_match", self._handle, i, c.byref(found)))
213
+ matches.append(Match(found.x, found.y, found.ra, found.dec, found.residual))
214
+ return matches
215
+
216
+ def pixel_to_sky(self, x: float, y: float) -> tuple[float, float]:
217
+ """The position on the sky of a pixel, in degrees, distortion included.
218
+
219
+ Raises:
220
+ AsterionError: if the image wasn't solved
221
+ """
222
+ return self._convert("asterion_result_pixel_to_sky", x, y)
223
+
224
+ def sky_to_pixel(self, ra: float, dec: float) -> tuple[float, float]:
225
+ """The pixel of a position on the sky, in degrees, distortion included.
226
+
227
+ Raises:
228
+ AsterionError: if the image wasn't solved, or the position is too far from the image to be projected
229
+ """
230
+ return self._convert("asterion_result_sky_to_pixel", ra, dec)
231
+
232
+ def _count(self, function):
233
+ return _native.check(_native.call(function, self._handle)) if self.solved else 0
234
+
235
+ def _convert(self, function, a, b):
236
+ first, second = c.c_double(), c.c_double()
237
+ _native.check(_native.call(function, self._handle, a, b, c.byref(first), c.byref(second)))
238
+ return first.value, second.value
239
+
240
+
241
+ class Solver:
242
+ """
243
+ A solver, with the catalogs which are installed in a directory: they stay open between solves, which keeps
244
+ them fast, until the solver is closed. A solver can be used by several threads at once.
245
+
246
+ with asterion.Solver() as solver:
247
+ result = solver.solve(frame)
248
+ """
249
+
250
+ def __init__(self, catalog_directory: Optional[PathLike] = None):
251
+ """
252
+ Opens the catalogs of a directory.
253
+
254
+ Args:
255
+ catalog_directory: the directory of the catalogs, by default the one of the command line tool
256
+
257
+ Raises:
258
+ AsterionError: if no catalog is installed
259
+ """
260
+ handle = c.c_void_p()
261
+ _native.check(_native.call("asterion_open", _native.path(catalog_directory), c.byref(handle)))
262
+ self._handle = handle
263
+ self._lock = threading.Lock()
264
+ self._solving = 0
265
+ # closed when the solver is garbage collected, if it wasn't
266
+ self._finalizer = weakref.finalize(self, _native.release, "asterion_close", handle)
267
+
268
+ def __enter__(self):
269
+ return self
270
+
271
+ def __exit__(self, *exception):
272
+ self.close()
273
+
274
+ def close(self) -> None:
275
+ """Closes the solver, if it isn't: its catalogs are unmapped at once, which releases their memory. The results
276
+ stay valid.
277
+
278
+ Raises:
279
+ AsterionError: if a solve runs meanwhile
280
+ """
281
+ with self._lock:
282
+ if self._solving:
283
+ raise AsterionError("The solver can't be closed while it solves")
284
+ if self._handle is not None:
285
+ _native.check(_native.call("asterion_close", self._handle))
286
+ self._handle = None
287
+ self._finalizer.detach()
288
+
289
+ def solve(self, pixels: Any, width: Optional[int] = None, height: Optional[int] = None, *,
290
+ sample_type: Optional[str] = None, row_stride: Optional[int] = None, bayer: bool = False,
291
+ ra: Optional[float] = None, dec: Optional[float] = None, radius: Optional[float] = None,
292
+ fov: Optional[float] = None, scale_low: Optional[float] = None, scale_high: Optional[float] = None,
293
+ pixel_size: Optional[float] = None, epoch: Optional[float] = None, parity: str = "both",
294
+ timeout: Optional[float] = None, sip_order: Optional[int] = None, binning: Optional[int] = None,
295
+ sigma: Optional[float] = None, max_stars: Optional[int] = None, quad_stars: Optional[int] = None) -> Result:
296
+ """
297
+ Solves an image: its pixels are copied, converted and binned in a single pass, then searched for in the
298
+ catalogs, so that they can be changed as soon as solve returns.
299
+
300
+ Args:
301
+ pixels: the gray levels of the image, one per pixel: unsigned 8 or 16-bit integers, or 32-bit floats, in
302
+ the native byte order. An array of the shape (height, width), which describes itself with
303
+ __array_interface__, such as a NumPy array, whose rows may be apart; a contiguous two-dimensional
304
+ buffer, such as a memoryview; or a one-dimensional buffer, such as bytes or array.array, with the
305
+ width and the height. The pixels of a row must be contiguous.
306
+ width: the width of the image, for one-dimensional buffers
307
+ height: the height of the image, for one-dimensional buffers
308
+ sample_type: "uint8", "uint16" or "float32", for buffers of bytes
309
+ row_stride: the distance between the starts of two rows, in bytes, for one-dimensional buffers whose rows
310
+ are apart
311
+ bayer: whether the image is a raw frame of a color sensor, which is binned 2x2 at least to remove its
312
+ pattern, unless the binning is given
313
+ ra: the right ascension of the center of the region to search, in degrees, with dec and radius: the whole
314
+ sky by default
315
+ dec: the declination of the center of the region to search, in degrees
316
+ radius: the radius of the region to search, in degrees
317
+ fov: the approximate width of the field of view, in degrees: 30% is tolerated
318
+ scale_low: the smallest scale of the image, in arcseconds per pixel
319
+ scale_high: the largest scale of the image, in arcseconds per pixel
320
+ pixel_size: the size of the pixels, in micrometers, binning included, which gives the focal length
321
+ epoch: the date of the image, as a Julian year, for the proper motions of the stars
322
+ parity: "both", "normal" or "flipped", for mirrored images
323
+ timeout: how long to search before giving up, in seconds
324
+ sip_order: the order of the SIP polynomials of the distortion: 0 to disable them, 2 to 5, or -1 to choose
325
+ binning: the binning applied before detecting stars, or 0 to choose
326
+ sigma: the threshold of the detection of stars, in units of background noise
327
+ max_stars: the largest number of stars to detect
328
+ quad_stars: the number of stars, the brightest first, used to search for a match
329
+
330
+ Returns:
331
+ the result, solved or not
332
+
333
+ Raises:
334
+ ValueError: if the pixels can't be read: their type, their byte order, their layout, their size
335
+ AsterionError: if the solver is closed, or the options are out of range
336
+ """
337
+ options = _options(parity, ra=ra, dec=dec, radius=radius, fov=fov, scale_low=scale_low, scale_high=scale_high,
338
+ pixel_size=pixel_size, epoch=epoch, timeout=timeout, sip_order=sip_order, binning=binning,
339
+ sigma=sigma, max_stars=max_stars, quad_stars=quad_stars)
340
+ image = _Image.of(pixels, width, height, sample_type, row_stride)
341
+ solution = _native.Solution()
342
+ result = c.c_void_p()
343
+ with self._solve() as handle, image.address() as address:
344
+ status = _native.call("asterion_solve", handle, address, image.code, image.width, image.height,
345
+ image.row_stride, int(bayer), c.byref(options), c.byref(solution), c.byref(result))
346
+ _native.check(status)
347
+ return Result(status == _native.SOLVED, solution, result)
348
+
349
+ @contextmanager
350
+ def _solve(self):
351
+ """The handle of the solver, which can't be closed meanwhile."""
352
+ with self._lock:
353
+ if self._handle is None:
354
+ raise AsterionError("The solver is closed")
355
+ self._solving += 1
356
+ try:
357
+ yield self._handle
358
+ finally:
359
+ with self._lock:
360
+ self._solving -= 1
361
+
362
+
363
+ def _options(parity, **given):
364
+ """The options of a solve: the defaults of the library, and the ones which are given."""
365
+ if parity not in _native.PARITIES:
366
+ raise ValueError(f"Unknown parity {parity!r}: expected one of {', '.join(_native.PARITIES)}")
367
+ options = _native.Options()
368
+ _native.check(_native.call("asterion_default_options", c.byref(options)))
369
+ options.parity = _native.PARITIES[parity]
370
+ for name, value in given.items():
371
+ if value is not None:
372
+ setattr(options, name, value)
373
+ return options
374
+
375
+
376
+ @dataclass(frozen=True)
377
+ class _Image:
378
+ """The pixels of an image, as the library reads them: their memory, the type and the size of their samples, and
379
+ the layout of their rows. The memory is an address, or an object which exports it with the buffer protocol."""
380
+
381
+ memory: Any
382
+ offset: int
383
+ code: int
384
+ width: int
385
+ height: int
386
+ row_stride: int
387
+
388
+ @staticmethod
389
+ def of(pixels, width, height, sample_type, row_stride):
390
+ interface = getattr(pixels, "__array_interface__", None)
391
+ if interface is not None:
392
+ if width is not None or height is not None or row_stride is not None:
393
+ raise ValueError("The width, the height and the row stride of an array are its own")
394
+ return _Image._array(pixels, interface, sample_type)
395
+ try:
396
+ view = memoryview(pixels)
397
+ except TypeError:
398
+ raise ValueError(f"Expected an array or a buffer of pixels, not {type(pixels).__name__}") from None
399
+ if view.ndim == 2:
400
+ if width is not None or height is not None or row_stride is not None:
401
+ raise ValueError("The width, the height and the row stride of a two-dimensional buffer are its own")
402
+ if not view.c_contiguous:
403
+ raise ValueError("A two-dimensional buffer must be contiguous: pass the array, or a one-dimensional "
404
+ "buffer with its row stride")
405
+ name = _sample_type(*_format(view.format), sample_type)
406
+ height, width = view.shape
407
+ return _Image._of(view, 0, name, width, height, view.strides[0])
408
+ if view.ndim != 1:
409
+ raise ValueError(f"Expected pixels in one or two dimensions, not {view.ndim}")
410
+ if width is None or height is None:
411
+ raise ValueError("The width and the height are required with a one-dimensional buffer")
412
+ if not view.c_contiguous:
413
+ raise ValueError("A one-dimensional buffer must be contiguous")
414
+ # a buffer of bytes needs the type of its samples; a typed one, such as array.array, has its own
415
+ kind = _format(view.format) if view.format not in ("B", "c") else None
416
+ name = _sample_type(*kind, sample_type) if kind else sample_type
417
+ if name is None:
418
+ raise ValueError("The sample type of a buffer of bytes is required: uint8, uint16 or float32")
419
+ return _Image._of(view, 0, name, width, height, row_stride)
420
+
421
+ @staticmethod
422
+ def _array(pixels, interface, sample_type):
423
+ """An array which describes itself with __array_interface__: its data is an address, or an object which
424
+ exports its memory, or the array itself when it's None."""
425
+ shape = interface["shape"]
426
+ if len(shape) != 2:
427
+ raise ValueError(f"Expected an array of the shape (height, width), not {tuple(shape)}")
428
+ height, width = shape
429
+ typestr = interface["typestr"]
430
+ name = _sample_type(typestr[0], typestr[1:], sample_type)
431
+ strides = interface.get("strides")
432
+ row_stride = None
433
+ if strides is not None:
434
+ if strides[1] != _SAMPLE_TYPES[name][1]:
435
+ raise ValueError("The pixels of a row must be contiguous")
436
+ row_stride = strides[0]
437
+ data = interface.get("data")
438
+ if isinstance(data, tuple):
439
+ return _Image._of(data[0], 0, name, width, height, row_stride)
440
+ return _Image._of(memoryview(pixels if data is None else data), interface.get("offset", 0), name, width, height,
441
+ row_stride)
442
+
443
+ @staticmethod
444
+ def _of(memory, offset, name, width, height, row_stride):
445
+ code, size = _SAMPLE_TYPES[name]
446
+ if row_stride is None:
447
+ row_stride = width * size
448
+ if row_stride < width * size:
449
+ raise ValueError(f"The rows of {width} {name} samples can't be {row_stride} bytes apart: arrays whose rows "
450
+ f"are reversed or overlap can be copied with numpy.ascontiguousarray")
451
+ if isinstance(memory, memoryview):
452
+ needed = offset + (height - 1) * row_stride + width * size if height > 0 else 0
453
+ if memory.nbytes < needed:
454
+ raise ValueError(f"The pixels are {memory.nbytes} bytes, {width}x{height} {name} samples need {needed}")
455
+ return _Image(memory, offset, code, width, height, row_stride)
456
+
457
+ @contextmanager
458
+ def address(self):
459
+ """The address of the pixels, which stay where they are meanwhile."""
460
+ if not isinstance(self.memory, memoryview):
461
+ yield self.memory
462
+ return
463
+ with _pinned(self.memory.cast("B") if self.memory.c_contiguous else self.memory) as address:
464
+ yield address + self.offset
465
+
466
+
467
+ def _format(code):
468
+ """The byte order and the kind of the samples of a format of the buffer protocol: "<", "u2" for "<H"."""
469
+ order, code = (code[0], code[1:]) if code[:1] in "@=<>!" else ("@", code)
470
+ if code not in _FORMATS:
471
+ raise ValueError(f"Unsupported samples {code!r}: expected unsigned 8 or 16-bit integers, or 32-bit floats")
472
+ return order, _FORMATS[code]
473
+
474
+
475
+ def _sample_type(order, kind, given):
476
+ """The sample type of pixels which tell their byte order and kind, which must be the given one, if any."""
477
+ if kind not in _KINDS:
478
+ raise ValueError(f"Unsupported samples {kind!r}: expected unsigned 8 or 16-bit integers, or 32-bit floats")
479
+ if _ORDERS.get(order) != _NATIVE and kind != "u1":
480
+ raise ValueError("The samples must be in the native byte order: with NumPy, astype(dtype.newbyteorder('='))")
481
+ name = _KINDS[kind]
482
+ if given is not None and given != name:
483
+ raise ValueError(f"The samples are {name}, not {given}")
484
+ return name
485
+
486
+
487
+ class _Buffer(c.Structure):
488
+ """Py_buffer, which the buffer protocol fills: the memory of an object, which stays where it is until released."""
489
+
490
+ _fields_ = [("buf", c.c_void_p), ("obj", c.py_object), ("len", c.c_ssize_t), ("itemsize", c.c_ssize_t),
491
+ ("readonly", c.c_int), ("ndim", c.c_int), ("format", c.c_char_p), ("shape", c.c_void_p),
492
+ ("strides", c.c_void_p), ("suboffsets", c.c_void_p), ("internal", c.c_void_p)]
493
+
494
+
495
+ _GET_BUFFER = c.pythonapi.PyObject_GetBuffer
496
+ _GET_BUFFER.argtypes = [c.py_object, c.POINTER(_Buffer), c.c_int]
497
+ _GET_BUFFER.restype = c.c_int
498
+ _RELEASE_BUFFER = c.pythonapi.PyBuffer_Release
499
+ _RELEASE_BUFFER.argtypes = [c.POINTER(_Buffer)]
500
+ _RELEASE_BUFFER.restype = None
501
+
502
+
503
+ @contextmanager
504
+ def _pinned(view):
505
+ """The address of the memory of a contiguous buffer, read-only ones included, which ctypes doesn't give."""
506
+ buffer = _Buffer()
507
+ # PyBUF_SIMPLE: the memory as contiguous bytes. A failure raises the error of Python
508
+ _GET_BUFFER(view, c.byref(buffer), 0)
509
+ try:
510
+ yield buffer.buf
511
+ finally:
512
+ _RELEASE_BUFFER(c.byref(buffer))
Binary file
asterion/py.typed ADDED
File without changes