bethkit 1.0.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.
@@ -0,0 +1,510 @@
1
+ """
2
+ Copyright (c) Modding Forge
3
+ """
4
+ from __future__ import annotations
5
+
6
+ import ctypes
7
+ from pathlib import Path
8
+
9
+ from .. import _ffi
10
+ from .._error import BethkitClosedError, BethkitOwnershipError
11
+ from ..enums import Game
12
+
13
+
14
+ def _sig_buf(sig: bytes | str) -> ctypes.Array[ctypes.c_uint8]:
15
+ """
16
+ Convert a 4-byte signature to a ctypes ``c_uint8`` array.
17
+
18
+ Args:
19
+ sig (bytes | str): Four-byte ASCII signature.
20
+
21
+ Returns:
22
+ ctypes.Array: A ``c_uint8[4]`` array containing the signature.
23
+
24
+ Raises:
25
+ ValueError: If *sig* is not exactly 4 bytes.
26
+ """
27
+
28
+ if isinstance(sig, str):
29
+ sig = sig.encode("ascii")
30
+ if len(sig) != 4:
31
+ raise ValueError("signature must be exactly 4 bytes")
32
+ return (ctypes.c_uint8 * 4)(*sig)
33
+
34
+
35
+ class WritableRecord:
36
+ """
37
+ A plugin record under construction.
38
+
39
+ Create via :meth:`new`, add sub-records with :meth:`add_subrecord`,
40
+ then hand the record off to a :class:`WritableGroup` with
41
+ :meth:`WritableGroup.add_record`. Ownership transfers on that call
42
+ and this wrapper becomes invalid.
43
+
44
+ Use as a context manager to free the handle if the record is never
45
+ added to a group::
46
+
47
+ with WritableRecord.new(b"NPC_") as rec:
48
+ rec.add_subrecord(b"EDID", b"MyNPC\x00")
49
+ group.add_record(rec)
50
+ """
51
+
52
+ __ptr: int
53
+
54
+ def __init__(self, ptr: int) -> None:
55
+ """
56
+ Args:
57
+ ptr (int): Native handle returned by the FFI new call.
58
+ """
59
+
60
+ self.__ptr = ptr
61
+
62
+ def __check_open(self) -> int:
63
+ """
64
+ Return the native pointer, raising if the handle has been closed.
65
+
66
+ Returns:
67
+ int: Valid native pointer.
68
+
69
+ Raises:
70
+ BethkitClosedError: If the record has been closed or transferred.
71
+ """
72
+
73
+ if not self.__ptr:
74
+ raise BethkitClosedError("WritableRecord is closed or transferred")
75
+ return self.__ptr
76
+
77
+ def _transfer_ptr(self) -> int:
78
+ """
79
+ Transfer ownership to the caller.
80
+
81
+ Returns:
82
+ int: The raw native pointer.
83
+
84
+ Raises:
85
+ BethkitOwnershipError: If the record has already been transferred
86
+ or closed.
87
+ """
88
+
89
+ if not self.__ptr:
90
+ raise BethkitOwnershipError(
91
+ "WritableRecord has already been transferred or closed"
92
+ )
93
+ ptr = self.__ptr
94
+ self.__ptr = 0
95
+ return ptr
96
+
97
+ @classmethod
98
+ def new(
99
+ cls,
100
+ signature: bytes | str,
101
+ flags: int = 0,
102
+ form_id: int = 0,
103
+ form_version: int = 44,
104
+ ) -> WritableRecord:
105
+ """
106
+ Create a new writable record with the given header fields.
107
+
108
+ Args:
109
+ signature (bytes | str): Four-byte record type signature.
110
+ flags (int): Record header flags bitmask. Defaults to ``0``.
111
+ form_id (int): Raw FormID. Defaults to ``0``.
112
+ form_version (int): Form version. Defaults to ``44``.
113
+
114
+ Returns:
115
+ WritableRecord: A new, empty record.
116
+
117
+ Raises:
118
+ BethkitNativeError: If the native record cannot be created.
119
+ ValueError: If *signature* is not exactly 4 bytes.
120
+ """
121
+
122
+ buf = _sig_buf(signature)
123
+ lib = _ffi.load_lib()
124
+ ptr = lib.bethkit_writable_record_new(
125
+ buf, flags, form_id, form_version
126
+ )
127
+ if not ptr:
128
+ _ffi.raise_last_error(lib)
129
+ return cls(ptr)
130
+
131
+ def close(self) -> None:
132
+ """
133
+ Release the native record handle.
134
+
135
+ Safe to call multiple times; subsequent calls are no-ops.
136
+ """
137
+
138
+ if self.__ptr:
139
+ _ffi.load_lib().bethkit_writable_record_free(self.__ptr)
140
+ self.__ptr = 0
141
+
142
+ def __enter__(self) -> WritableRecord:
143
+ """Return *self* for use as a context manager."""
144
+
145
+ return self
146
+
147
+ def __exit__(self, *_: object) -> None:
148
+ """Free the record when exiting the context."""
149
+
150
+ self.close()
151
+
152
+ def __del__(self) -> None:
153
+ """Free the native handle on garbage collection."""
154
+
155
+ try:
156
+ self.close()
157
+ except Exception:
158
+ pass
159
+
160
+ def add_subrecord(
161
+ self, signature: bytes | str, data: bytes
162
+ ) -> None:
163
+ """
164
+ Append a sub-record to this record.
165
+
166
+ Args:
167
+ signature (bytes | str): Four-byte sub-record type signature.
168
+ data (bytes): Raw sub-record payload.
169
+
170
+ Raises:
171
+ BethkitClosedError: If the record has been closed or transferred.
172
+ BethkitNativeError: If the native call fails.
173
+ ValueError: If *signature* is not exactly 4 bytes.
174
+ """
175
+
176
+ lib = _ffi.load_lib()
177
+ sig_buf = _sig_buf(signature)
178
+ data_buf = (ctypes.c_uint8 * len(data)).from_buffer_copy(data)
179
+ if (
180
+ lib.bethkit_writable_record_add_subrecord(
181
+ self.__check_open(), sig_buf, data_buf, len(data)
182
+ )
183
+ != 0
184
+ ):
185
+ _ffi.raise_last_error(lib)
186
+
187
+ def __repr__(self) -> str:
188
+ """
189
+ Returns:
190
+ str: Developer-friendly representation with native pointer.
191
+ """
192
+
193
+ if not self.__ptr:
194
+ return "<WritableRecord transferred>"
195
+ return f"<WritableRecord ptr=0x{self.__ptr:016X}>"
196
+
197
+
198
+ class WritableGroup:
199
+ """
200
+ A plugin group under construction.
201
+
202
+ Add records with :meth:`add_record` and sub-groups with
203
+ :meth:`add_group`, then hand the group off to a
204
+ :class:`PluginWriter` with :meth:`PluginWriter.add_group`.
205
+ Ownership transfers on that call.
206
+ """
207
+
208
+ __ptr: int
209
+
210
+ def __init__(self, ptr: int) -> None:
211
+ """
212
+ Args:
213
+ ptr (int): Native handle returned by the FFI new call.
214
+ """
215
+
216
+ self.__ptr = ptr
217
+
218
+ def __check_open(self) -> int:
219
+ """
220
+ Return the native pointer, raising if the handle has been closed.
221
+
222
+ Returns:
223
+ int: Valid native pointer.
224
+
225
+ Raises:
226
+ BethkitClosedError: If the group has been closed or transferred.
227
+ """
228
+
229
+ if not self.__ptr:
230
+ raise BethkitClosedError("WritableGroup is closed or transferred")
231
+ return self.__ptr
232
+
233
+ def _transfer_ptr(self) -> int:
234
+ """
235
+ Transfer ownership to the caller.
236
+
237
+ Returns:
238
+ int: The raw native pointer.
239
+
240
+ Raises:
241
+ BethkitOwnershipError: If the group has already been transferred
242
+ or closed.
243
+ """
244
+
245
+ if not self.__ptr:
246
+ raise BethkitOwnershipError(
247
+ "WritableGroup has already been transferred or closed"
248
+ )
249
+ ptr = self.__ptr
250
+ self.__ptr = 0
251
+ return ptr
252
+
253
+ @classmethod
254
+ def new(
255
+ cls, label: bytes | str, group_type: int = 0
256
+ ) -> WritableGroup:
257
+ """
258
+ Create a new writable group.
259
+
260
+ Args:
261
+ label (bytes | str): Four-byte group label (top-level
262
+ groups use a record-type signature).
263
+ group_type (int): Numeric group type. Defaults to ``0``
264
+ (top-level).
265
+
266
+ Returns:
267
+ WritableGroup: A new, empty group.
268
+
269
+ Raises:
270
+ BethkitNativeError: If the native group cannot be created.
271
+ ValueError: If *label* is not exactly 4 bytes.
272
+ """
273
+
274
+ lib = _ffi.load_lib()
275
+ buf = _sig_buf(label)
276
+ ptr = lib.bethkit_writable_group_new(buf, group_type)
277
+ if not ptr:
278
+ _ffi.raise_last_error(lib)
279
+ return cls(ptr)
280
+
281
+ def close(self) -> None:
282
+ """
283
+ Release the native group handle.
284
+
285
+ Safe to call multiple times; subsequent calls are no-ops.
286
+ """
287
+
288
+ if self.__ptr:
289
+ _ffi.load_lib().bethkit_writable_group_free(self.__ptr)
290
+ self.__ptr = 0
291
+
292
+ def __enter__(self) -> WritableGroup:
293
+ """Return *self* for use as a context manager."""
294
+
295
+ return self
296
+
297
+ def __exit__(self, *_: object) -> None:
298
+ """Free the group when exiting the context."""
299
+
300
+ self.close()
301
+
302
+ def __del__(self) -> None:
303
+ """Free the native handle on garbage collection."""
304
+
305
+ try:
306
+ self.close()
307
+ except Exception:
308
+ pass
309
+
310
+ def add_record(self, record: WritableRecord) -> None:
311
+ """
312
+ Append a record to this group, transferring ownership.
313
+
314
+ After this call *record* is invalid.
315
+
316
+ Args:
317
+ record (WritableRecord): The record to add.
318
+
319
+ Raises:
320
+ BethkitClosedError: If this group has been closed or transferred.
321
+ BethkitOwnershipError: If *record* has already been transferred
322
+ or closed.
323
+ BethkitNativeError: If the native call fails.
324
+ """
325
+
326
+ lib = _ffi.load_lib()
327
+ rec_ptr = record._transfer_ptr()
328
+ if lib.bethkit_writable_group_add_record(
329
+ self.__check_open(), rec_ptr
330
+ ) != 0:
331
+ _ffi.raise_last_error(lib)
332
+
333
+ def add_group(self, child: WritableGroup) -> None:
334
+ """
335
+ Append a sub-group to this group, transferring ownership.
336
+
337
+ After this call *child* is invalid.
338
+
339
+ Args:
340
+ child (WritableGroup): The sub-group to add.
341
+
342
+ Raises:
343
+ BethkitClosedError: If this group has been closed or transferred.
344
+ BethkitOwnershipError: If *child* has already been transferred
345
+ or closed.
346
+ BethkitNativeError: If the native call fails.
347
+ """
348
+
349
+ lib = _ffi.load_lib()
350
+ child_ptr = child._transfer_ptr()
351
+ if lib.bethkit_writable_group_add_group(
352
+ self.__check_open(), child_ptr
353
+ ) != 0:
354
+ _ffi.raise_last_error(lib)
355
+
356
+ def __repr__(self) -> str:
357
+ """
358
+ Returns:
359
+ str: Developer-friendly representation with native pointer.
360
+ """
361
+
362
+ if not self.__ptr:
363
+ return "<WritableGroup transferred>"
364
+ return f"<WritableGroup ptr=0x{self.__ptr:016X}>"
365
+
366
+
367
+ class PluginWriter:
368
+ """
369
+ Assembles and serialises a complete plugin file.
370
+
371
+ Add top-level groups with :meth:`add_group`, then call
372
+ :meth:`write_to_file` or :meth:`write_to_bytes` to produce the
373
+ finished plugin.
374
+ """
375
+
376
+ __ptr: int
377
+
378
+ def __init__(self, game: Game, form_version: int = 44) -> None:
379
+ """
380
+ Args:
381
+ game (Game): Target game; determines the correct format.
382
+ form_version (int): Default form version written to record
383
+ headers. Defaults to ``44`` (Skyrim SE).
384
+
385
+ Raises:
386
+ BethkitNativeError: If the native writer cannot be created.
387
+ """
388
+
389
+ lib = _ffi.load_lib()
390
+ ptr = lib.bethkit_plugin_writer_new(int(game), form_version)
391
+ if not ptr:
392
+ _ffi.raise_last_error(lib)
393
+ self.__ptr = ptr
394
+
395
+ def __check_open(self) -> int:
396
+ """
397
+ Return the native pointer, raising if the handle has been closed.
398
+
399
+ Returns:
400
+ int: Valid native pointer.
401
+
402
+ Raises:
403
+ BethkitClosedError: If the writer has been closed.
404
+ """
405
+
406
+ if not self.__ptr:
407
+ raise BethkitClosedError("PluginWriter is closed")
408
+ return self.__ptr
409
+
410
+ def close(self) -> None:
411
+ """
412
+ Release the native writer handle.
413
+
414
+ Safe to call multiple times; subsequent calls are no-ops.
415
+ """
416
+
417
+ if self.__ptr:
418
+ _ffi.load_lib().bethkit_plugin_writer_free(self.__ptr)
419
+ self.__ptr = 0
420
+
421
+ def __enter__(self) -> PluginWriter:
422
+ """Return *self* for use as a context manager."""
423
+
424
+ return self
425
+
426
+ def __exit__(self, *_: object) -> None:
427
+ """Free the writer when exiting the context."""
428
+
429
+ self.close()
430
+
431
+ def __del__(self) -> None:
432
+ """Free the native handle on garbage collection."""
433
+
434
+ try:
435
+ self.close()
436
+ except Exception:
437
+ pass
438
+
439
+ def add_group(self, group: WritableGroup) -> None:
440
+ """
441
+ Append a top-level group to the plugin, transferring ownership.
442
+
443
+ After this call *group* is invalid.
444
+
445
+ Args:
446
+ group (WritableGroup): The group to add.
447
+
448
+ Raises:
449
+ BethkitClosedError: If the writer has been closed.
450
+ BethkitOwnershipError: If *group* has already been transferred
451
+ or closed.
452
+ BethkitNativeError: If the native call fails.
453
+ """
454
+
455
+ lib = _ffi.load_lib()
456
+ grp_ptr = group._transfer_ptr()
457
+ if lib.bethkit_plugin_writer_add_group(
458
+ self.__check_open(), grp_ptr
459
+ ) != 0:
460
+ _ffi.raise_last_error(lib)
461
+
462
+ def write_to_file(self, path: Path) -> None:
463
+ """
464
+ Serialise and write the plugin to *path* on disk.
465
+
466
+ Args:
467
+ path (Path): Destination file path.
468
+
469
+ Raises:
470
+ BethkitClosedError: If the writer has been closed.
471
+ BethkitNativeError: If serialisation or the write fails.
472
+ """
473
+
474
+ lib = _ffi.load_lib()
475
+ if lib.bethkit_plugin_writer_write_to_file(
476
+ self.__check_open(), _ffi.enc(path)
477
+ ) != 0:
478
+ _ffi.raise_last_error(lib)
479
+
480
+ def write_to_bytes(self) -> bytes:
481
+ """
482
+ Serialise the plugin and return it as a byte buffer.
483
+
484
+ Returns:
485
+ bytes: Complete serialised plugin data.
486
+
487
+ Raises:
488
+ BethkitClosedError: If the writer has been closed.
489
+ BethkitNativeError: If serialisation fails.
490
+ """
491
+
492
+ lib = _ffi.load_lib()
493
+ out_len = ctypes.c_size_t(0)
494
+ ptr = lib.bethkit_plugin_writer_write_to_bytes(
495
+ self.__check_open(), ctypes.byref(out_len)
496
+ )
497
+ if not ptr:
498
+ _ffi.raise_last_error(lib)
499
+ try:
500
+ return bytes(ctypes.string_at(ptr, out_len.value))
501
+ finally:
502
+ lib.bethkit_bytes_free(ptr, out_len.value)
503
+
504
+ def __repr__(self) -> str:
505
+ """
506
+ Returns:
507
+ str: Developer-friendly representation of the writer.
508
+ """
509
+
510
+ return "<PluginWriter>"
bethkit/py.typed ADDED
File without changes
@@ -0,0 +1,26 @@
1
+ """
2
+ Copyright (c) Modding Forge
3
+
4
+ Schema subpackage — schema-driven record field decoding and type information.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from .schema import (
9
+ EnumVal,
10
+ FieldValue,
11
+ FlagsVal,
12
+ NamedField,
13
+ RecordView,
14
+ SchemaRegistry,
15
+ TypedFormId,
16
+ )
17
+
18
+ __all__ = [
19
+ "EnumVal",
20
+ "FieldValue",
21
+ "FlagsVal",
22
+ "NamedField",
23
+ "RecordView",
24
+ "SchemaRegistry",
25
+ "TypedFormId",
26
+ ]