pardox 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.
pardox/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ # pardox/__init__.py
2
+
3
+ from .frame import DataFrame
4
+ from .io import read_csv, read_sql, from_arrow, read_prdx
5
+
6
+ # Y lo exponemos públicamente aquí
7
+ __all__ = ["DataFrame", "read_csv", "read_sql", "from_arrow", "read_prdx", "Series"]
pardox/frame.py ADDED
@@ -0,0 +1,439 @@
1
+ import ctypes
2
+ import json
3
+ from .wrapper import lib, c_char_p, c_size_t, c_int32, c_double
4
+
5
+ class DataFrame:
6
+ def __init__(self, manager_ptr):
7
+ """
8
+ Initializes a PardoX DataFrame.
9
+ DO NOT call directly. Use:
10
+ - px.read_csv() (Native)
11
+ - px.read_sql() (Native via Postgres/ODBC)
12
+ - px.from_arrow() (Universal Bridge)
13
+ """
14
+ if not manager_ptr:
15
+ raise ValueError("Null pointer received when creating DataFrame.")
16
+ self._ptr = manager_ptr
17
+
18
+ def __del__(self):
19
+ """
20
+ Destructor: Frees Rust memory when the Python object dies.
21
+ """
22
+ if self._ptr and lib:
23
+ # Check if lib is still available (interpreter shutdown safety)
24
+ if hasattr(lib, 'pardox_free_manager'):
25
+ lib.pardox_free_manager(self._ptr)
26
+ self._ptr = None
27
+
28
+ # =========================================================================
29
+ # VISUALIZATION MAGIC
30
+ # =========================================================================
31
+
32
+ def __repr__(self):
33
+ """
34
+ Esta es la función mágica que Jupyter llama para mostrar el objeto.
35
+ En lugar de devolver el objeto raw, devolvemos la tabla ASCII.
36
+ """
37
+ # Por defecto mostramos 10 filas al imprimir el objeto
38
+ return self._fetch_ascii_table(10) or "<Empty PardoX DataFrame>"
39
+
40
+ def head(self, n=5):
41
+ """
42
+ Ahora devuelve un NUEVO DataFrame con las primeras n filas.
43
+ Al devolver un objeto, Jupyter llamará a su __repr__ y se verá bonito.
44
+ """
45
+ return self.iloc[0:n]
46
+
47
+ def tail(self, n=5):
48
+ """
49
+ Devuelve un NUEVO DataFrame con las últimas n filas.
50
+ """
51
+ if not hasattr(lib, 'pardox_tail_manager'):
52
+ raise NotImplementedError("tail() API not available in Core.")
53
+
54
+ new_ptr = lib.pardox_tail_manager(self._ptr, n)
55
+ if not new_ptr:
56
+ raise RuntimeError("Failed to fetch tail.")
57
+
58
+ return DataFrame(new_ptr)
59
+
60
+ def show(self, n=10):
61
+ """
62
+ Prints the first n rows to the console explicitly.
63
+ """
64
+ ascii_table = self._fetch_ascii_table(n)
65
+ if ascii_table:
66
+ print(ascii_table)
67
+ else:
68
+ print(f"<PardoX DataFrame at {hex(self._ptr or 0)}> (Empty or Error)")
69
+
70
+ # =========================================================================
71
+ # METADATA & INSPECTION
72
+ # =========================================================================
73
+
74
+ @property
75
+ def shape(self):
76
+ """
77
+ Returns a tuple representing the dimensionality of the DataFrame.
78
+ Format: (rows, columns)
79
+ """
80
+ if hasattr(lib, 'pardox_get_row_count'):
81
+ rows = lib.pardox_get_row_count(self._ptr)
82
+ cols = len(self.columns)
83
+ return (rows, cols)
84
+ return (0, 0)
85
+
86
+ @property
87
+ def columns(self):
88
+ """
89
+ Returns the column labels of the DataFrame.
90
+ """
91
+ schema = self._get_schema_metadata()
92
+ if schema:
93
+ return [col['name'] for col in schema.get('columns', [])]
94
+ return []
95
+
96
+ @property
97
+ def dtypes(self):
98
+ """
99
+ Returns the data types in the DataFrame.
100
+ """
101
+ schema = self._get_schema_metadata()
102
+ if schema:
103
+ return {col['name']: col['type'] for col in schema.get('columns', [])}
104
+ return {}
105
+
106
+ # =========================================================================
107
+ # SELECTION & SLICING (Indexer)
108
+ # =========================================================================
109
+
110
+ def __getitem__(self, key):
111
+ """
112
+ Column Selection: df["col"] -> Series
113
+ Filtering: df[mask_series] -> DataFrame (Filtered)
114
+ """
115
+ # Case 1: Column Selection
116
+ if isinstance(key, str):
117
+ from .series import Series
118
+ return Series(self, key)
119
+
120
+ # Case 2: Boolean Filtering (df[df['A'] > 5])
121
+ if hasattr(key, '_df') and hasattr(key, 'dtype'):
122
+
123
+ if 'Boolean' not in str(key.dtype):
124
+ raise TypeError(f"Filter key must be a Boolean Series. Got: {key.dtype}")
125
+
126
+ if not hasattr(lib, 'pardox_apply_filter'):
127
+ raise NotImplementedError("Filter application API missing in Core.")
128
+
129
+ # Ahora sí accedemos al puntero a través del dataframe padre de la serie
130
+ mask_ptr = key._df._ptr
131
+ mask_col = key.name.encode('utf-8')
132
+
133
+ res_ptr = lib.pardox_apply_filter(self._ptr, mask_ptr, mask_col)
134
+ if not res_ptr:
135
+ raise RuntimeError("Filter operation returned null pointer.")
136
+
137
+ return DataFrame(res_ptr)
138
+
139
+ raise NotImplementedError(f"Selection with type {type(key)} not supported yet.")
140
+
141
+ @property
142
+ def iloc(self):
143
+ """
144
+ Purely integer-location based indexing for selection by position.
145
+ Usage: df.iloc[100:200]
146
+ """
147
+ return self._IlocIndexer(self)
148
+
149
+ class _IlocIndexer:
150
+ def __init__(self, df):
151
+ self._df = df
152
+
153
+ def __getitem__(self, key):
154
+ if isinstance(key, slice):
155
+ # Resolve slice indices
156
+ start = key.start if key.start is not None else 0
157
+ stop = key.stop if key.stop is not None else self._df.shape[0]
158
+
159
+ if start < 0: start = 0 # Simple clamping
160
+ if stop < start: stop = start
161
+
162
+ length = stop - start
163
+
164
+ # Call Rust Slicing API
165
+ if hasattr(lib, 'pardox_slice_manager'):
166
+ new_ptr = lib.pardox_slice_manager(self._df._ptr, start, length)
167
+ if not new_ptr:
168
+ raise RuntimeError("Slice operation returned null pointer.")
169
+ return DataFrame(new_ptr)
170
+ else:
171
+ raise NotImplementedError("Slicing API missing in Core.")
172
+ else:
173
+ raise TypeError("iloc only supports slices (e.g., [0:10]) for now.")
174
+
175
+ # =========================================================================
176
+ # MUTATION & TRANSFORMATION
177
+ # =========================================================================
178
+
179
+ def cast(self, col_name, target_type):
180
+ """
181
+ Casts a column to a new type in-place.
182
+ """
183
+ if not hasattr(lib, 'pardox_cast_column'):
184
+ raise NotImplementedError("Cast API missing.")
185
+
186
+ res = lib.pardox_cast_column(
187
+ self._ptr,
188
+ col_name.encode('utf-8'),
189
+ target_type.encode('utf-8')
190
+ )
191
+
192
+ if res != 1:
193
+ raise RuntimeError(f"Failed to cast column '{col_name}' to '{target_type}'. Check compatibility.")
194
+
195
+ return self # Enable method chaining
196
+
197
+ def join(self, other, on=None, left_on=None, right_on=None, how="inner"):
198
+ """
199
+ Joins with another PardoX DataFrame.
200
+ """
201
+ if not isinstance(other, DataFrame):
202
+ raise TypeError("The object to join must be a pardox.DataFrame")
203
+
204
+ l_col = on if on else left_on
205
+ r_col = on if on else right_on
206
+
207
+ if not l_col or not r_col:
208
+ raise ValueError("You must specify 'on' or ('left_on' and 'right_on')")
209
+
210
+ # Call Rust Hash Join
211
+ result_ptr = lib.pardox_hash_join(
212
+ self._ptr,
213
+ other._ptr,
214
+ l_col.encode('utf-8'),
215
+ r_col.encode('utf-8')
216
+ )
217
+
218
+ if not result_ptr:
219
+ raise RuntimeError("Join failed (Rust returned null pointer).")
220
+
221
+ return DataFrame(result_ptr)
222
+
223
+ # =========================================================================
224
+ # PERSISTENCE (IO WRITERS)
225
+ # =========================================================================
226
+
227
+ def to_csv(self, path_or_buf):
228
+ """
229
+ Exports the DataFrame to a CSV file.
230
+
231
+ Args:
232
+ path_or_buf (str): The file path where the CSV will be written.
233
+
234
+ Returns:
235
+ bool: True if successful.
236
+ """
237
+ if not isinstance(path_or_buf, str):
238
+ raise TypeError("PardoX currently only supports writing to file paths (str).")
239
+
240
+ if not hasattr(lib, 'pardox_to_csv'):
241
+ raise NotImplementedError("API 'pardox_to_csv' not found in Core DLL. Re-compile Rust.")
242
+
243
+ # Call Rust Core
244
+ # Rust handles headers, buffering, and parallel iteration.
245
+ res = lib.pardox_to_csv(self._ptr, path_or_buf.encode('utf-8'))
246
+
247
+ if res != 1:
248
+ error_map = {
249
+ -1: "Invalid Manager Pointer",
250
+ -2: "Invalid Path String",
251
+ -3: "Failed to initialize CSV Writer (Check permissions/path)",
252
+ -4: "Failed to write Header",
253
+ -5: "Failed to write Data Block",
254
+ -6: "Failed to flush buffer to disk"
255
+ }
256
+ msg = error_map.get(res, f"Unknown Error Code: {res}")
257
+ raise RuntimeError(f"CSV Export Failed: {msg}")
258
+
259
+ return True
260
+
261
+ def to_prdx(self, path_or_buf):
262
+ """
263
+ Exports the DataFrame to the native PardoX binary format (.prdx).
264
+ This format supports Zero-Copy loading in future sessions.
265
+
266
+ Args:
267
+ path_or_buf (str): The file path (e.g., 'data.prdx').
268
+ """
269
+ if not isinstance(path_or_buf, str):
270
+ raise TypeError("Path must be a string.")
271
+
272
+ if not hasattr(lib, 'pardox_to_prdx'):
273
+ # Fallback warning if you haven't exposed api_to_prdx in Rust yet
274
+ raise NotImplementedError("API 'pardox_to_prdx' not available. Check api_writers.rs")
275
+
276
+ res = lib.pardox_to_prdx(self._ptr, path_or_buf.encode('utf-8'))
277
+
278
+ if res != 1:
279
+ raise RuntimeError(f"PRDX Export Failed with error code: {res}")
280
+
281
+ return True
282
+
283
+ # =========================================================================
284
+ # INTERNAL HELPERS
285
+ # =========================================================================
286
+
287
+ def _fetch_ascii_table(self, limit):
288
+ """
289
+ Internal helper to fetch the ASCII table string from Rust.
290
+ """
291
+ if not hasattr(lib, 'pardox_manager_to_ascii'):
292
+ return self._fetch_json_dump(limit)
293
+
294
+ # 1. Call Rust to get the ASCII table string
295
+ ascii_ptr = lib.pardox_manager_to_ascii(self._ptr, limit)
296
+
297
+ if not ascii_ptr:
298
+ return None
299
+
300
+ try:
301
+ # 2. Decode the C-String
302
+ return ctypes.cast(ascii_ptr, c_char_p).value.decode('utf-8')
303
+ finally:
304
+ # 3. Free memory
305
+ if hasattr(lib, 'pardox_free_string'):
306
+ lib.pardox_free_string(ascii_ptr)
307
+
308
+ def _fetch_json_dump(self, limit):
309
+ """Legacy helper for older DLLs."""
310
+ if hasattr(lib, 'pardox_manager_to_json'):
311
+ json_ptr = lib.pardox_manager_to_json(self._ptr, limit)
312
+ if json_ptr:
313
+ try:
314
+ return ctypes.cast(json_ptr, c_char_p).value.decode('utf-8')
315
+ finally:
316
+ if hasattr(lib, 'pardox_free_string'):
317
+ lib.pardox_free_string(json_ptr)
318
+ return "Inspection API missing."
319
+
320
+ def _get_schema_metadata(self):
321
+ """
322
+ Internal helper to fetch schema JSON from Rust.
323
+ """
324
+ if not hasattr(lib, 'pardox_get_schema_json'):
325
+ return {}
326
+
327
+ json_ptr = lib.pardox_get_schema_json(self._ptr)
328
+ if not json_ptr:
329
+ return {}
330
+
331
+ try:
332
+ json_str = ctypes.cast(json_ptr, c_char_p).value.decode('utf-8')
333
+ return json.loads(json_str)
334
+ finally:
335
+ if hasattr(lib, 'pardox_free_string'):
336
+ lib.pardox_free_string(json_ptr)
337
+
338
+ @property
339
+ def _manager_ptr(self):
340
+ """Internal access to the pointer."""
341
+ return self._ptr
342
+
343
+ # =========================================================================
344
+ # MUTATION & FEATURE ENGINEERING (New in v0.1.5)
345
+ # =========================================================================
346
+
347
+ def __setitem__(self, key, value):
348
+ """
349
+ Enables column assignment: df['new_col'] = df['a'] * df['b']
350
+ """
351
+ # 1. Check if value is a PardoX Series (Result of arithmetic)
352
+ # CORRECCIÓN: Cambiamos '_col_name' por 'name' para coincidir con series.py
353
+ if hasattr(value, '_df') and hasattr(value, 'name'):
354
+ # It's a Series! We need to fuse it into this DataFrame.
355
+
356
+ # Use the Series' parent DataFrame (which is a 1-column temporary DF)
357
+ source_mgr_ptr = value._df._ptr
358
+ col_name = key.encode('utf-8')
359
+
360
+ if not hasattr(lib, 'pardox_add_column'):
361
+ raise NotImplementedError("pardox_add_column API missing in Core.")
362
+
363
+ # Call Rust Core to Move the column
364
+ res = lib.pardox_add_column(self._ptr, source_mgr_ptr, col_name)
365
+
366
+ if res != 1:
367
+ error_map = {
368
+ -1: "Invalid Pointers",
369
+ -2: "Invalid Column Name String",
370
+ -3: "Engine Logic Error (Row mismatch or Duplicate Name)"
371
+ }
372
+ msg = error_map.get(res, f"Unknown Error: {res}")
373
+ raise RuntimeError(f"Failed to assign column '{key}': {msg}")
374
+
375
+ return # Success!
376
+
377
+ # 2. Future support for scalar assignment (df['new'] = 0)
378
+ # elif isinstance(value, (int, float, str)):
379
+ # self._assign_scalar(key, value)
380
+
381
+ else:
382
+ # Tip de Debugging: Imprimimos los atributos disponibles para ver qué pasó
383
+ available_attrs = dir(value)
384
+ raise TypeError(f"Assignment only supported for PardoX Series. Got: {type(value)}. Attributes detected: {available_attrs}")
385
+
386
+ def fillna(self, value):
387
+ """
388
+ Fills Null/NaN values in the ENTIRE DataFrame with the specified scalar.
389
+ This modifies the DataFrame in-place.
390
+ """
391
+ if not isinstance(value, (int, float)):
392
+ raise TypeError("fillna currently only supports numeric scalars.")
393
+
394
+ if not hasattr(lib, 'pardox_fill_na'):
395
+ raise NotImplementedError("pardox_fill_na API missing in Core.")
396
+
397
+ # Iterate over all numeric columns and apply fillna kernel
398
+ # This is fast because the heavy lifting is done in Rust per column.
399
+ current_schema = self.dtypes
400
+ c_val = c_double(float(value))
401
+
402
+ for col_name, dtype in current_schema.items():
403
+ # Only apply to numeric types (Float/Int)
404
+ if dtype in ["Float64", "Int64"]:
405
+ res = lib.pardox_fill_na(
406
+ self._ptr,
407
+ col_name.encode('utf-8'),
408
+ c_val
409
+ )
410
+ if res != 1:
411
+ print(f"Warning: fillna failed for column '{col_name}'")
412
+
413
+ return self # Enable method chaining
414
+
415
+ def round(self, decimals=0):
416
+ """
417
+ Rounds all numeric columns to the specified number of decimals.
418
+ This modifies the DataFrame in-place.
419
+ """
420
+ if not isinstance(decimals, int):
421
+ raise TypeError("decimals must be an integer.")
422
+
423
+ if not hasattr(lib, 'pardox_round'):
424
+ raise NotImplementedError("pardox_round API missing in Core.")
425
+
426
+ # Iterate over all columns. Rust kernel will safely ignore non-floats.
427
+ current_columns = self.columns
428
+ c_decimals = c_int32(decimals)
429
+
430
+ for col_name in current_columns:
431
+ # We call Rust blindly; the Kernel checks types internally for safety
432
+ lib.pardox_round(
433
+ self._ptr,
434
+ col_name.encode('utf-8'),
435
+ c_decimals
436
+ )
437
+
438
+ return self # Enable method chaining
439
+
pardox/io.py ADDED
@@ -0,0 +1,193 @@
1
+ import ctypes
2
+ import json
3
+ import os
4
+ from .wrapper import lib, c_char_p
5
+ from .frame import DataFrame
6
+
7
+ # Default configuration for Rust CSV Reader
8
+ DEFAULT_CSV_CONFIG = {
9
+ "delimiter": 44, # Comma (,)
10
+ "quote_char": 34, # Double Quote (")
11
+ "has_header": True,
12
+ "chunk_size": 16 * 1024 * 1024 # 16MB Chunk
13
+ }
14
+
15
+ # =============================================================================
16
+ # ARROW C DATA INTERFACE (ABI)
17
+ # =============================================================================
18
+ class ArrowSchema(ctypes.Structure):
19
+ _fields_ = [
20
+ ("format", ctypes.c_char_p),
21
+ ("name", ctypes.c_char_p),
22
+ ("metadata", ctypes.c_char_p),
23
+ ("flags", ctypes.c_int64),
24
+ ("n_children", ctypes.c_int64),
25
+ ("children", ctypes.POINTER(ctypes.c_void_p)),
26
+ ("dictionary", ctypes.c_void_p),
27
+ ("release", ctypes.c_void_p),
28
+ ("private_data", ctypes.c_void_p),
29
+ ]
30
+
31
+ class ArrowArray(ctypes.Structure):
32
+ _fields_ = [
33
+ ("length", ctypes.c_int64),
34
+ ("null_count", ctypes.c_int64),
35
+ ("offset", ctypes.c_int64),
36
+ ("n_buffers", ctypes.c_int64),
37
+ ("n_children", ctypes.c_int64),
38
+ ("buffers", ctypes.POINTER(ctypes.c_void_p)),
39
+ ("children", ctypes.POINTER(ctypes.c_void_p)),
40
+ ("dictionary", ctypes.c_void_p),
41
+ ("release", ctypes.c_void_p),
42
+ ("private_data", ctypes.c_void_p),
43
+ ]
44
+
45
+ # =============================================================================
46
+ # PUBLIC API (NATIVE INGESTION)
47
+ # =============================================================================
48
+
49
+ def read_csv(path, schema=None):
50
+ """
51
+ Reads a CSV file directly into PardoX using the native Rust engine.
52
+
53
+ Args:
54
+ path (str): Path to the CSV file.
55
+ schema (dict, optional): Manual schema definition.
56
+ """
57
+ if not os.path.exists(path):
58
+ raise FileNotFoundError(f"File not found: {path}")
59
+
60
+ path_bytes = path.encode('utf-8')
61
+ config_bytes = json.dumps(DEFAULT_CSV_CONFIG).encode('utf-8')
62
+
63
+ if schema:
64
+ cols = [{"name": k, "type": v} for k, v in schema.items()]
65
+ schema_json_str = json.dumps({"columns": cols})
66
+ else:
67
+ schema_json_str = "{}"
68
+
69
+ schema_bytes = schema_json_str.encode('utf-8')
70
+
71
+ manager_ptr = lib.pardox_load_manager_csv(path_bytes, schema_bytes, config_bytes)
72
+
73
+ if not manager_ptr:
74
+ raise RuntimeError(f"Failed to load CSV: {path}.")
75
+
76
+ return DataFrame(manager_ptr)
77
+
78
+
79
+ def read_sql(connection_string, query):
80
+ """
81
+ Reads data directly from a SQL database using PardoX's NATIVE Rust drivers.
82
+
83
+ This function bypasses Python completely. The Rust engine connects to the
84
+ database, executes the query, and fills the memory buffers directly.
85
+
86
+ Args:
87
+ connection_string (str): URL (e.g., "postgresql://user:pass@localhost:5432/db")
88
+ query (str): SQL Query (e.g., "SELECT * FROM clients")
89
+
90
+ Returns:
91
+ pardox.DataFrame: A PardoX DataFrame containing the query results.
92
+ """
93
+ # 1. Check if the Core has Native SQL capabilities
94
+ if not hasattr(lib, 'pardox_scan_sql'):
95
+ raise NotImplementedError("This PardoX Core build does not support Native SQL.")
96
+
97
+ # 2. Encode to C-Strings (UTF-8)
98
+ conn_bytes = connection_string.encode('utf-8')
99
+ query_bytes = query.encode('utf-8')
100
+
101
+ # 3. Call Rust Core (Native Driver)
102
+ manager_ptr = lib.pardox_scan_sql(conn_bytes, query_bytes)
103
+
104
+ if not manager_ptr:
105
+ raise RuntimeError("SQL Query failed. Check console/stderr for Rust driver errors.")
106
+
107
+ return DataFrame(manager_ptr)
108
+
109
+
110
+ def from_arrow(data):
111
+ """
112
+ Ingests an Apache Arrow Table or RecordBatch into PardoX using Zero-Copy.
113
+
114
+ Use this for sources not yet supported natively (e.g., Parquet, Snowflake via Arrow).
115
+ """
116
+ try:
117
+ import pyarrow as pa
118
+ except ImportError as e:
119
+ raise ImportError("from_arrow requires 'pyarrow' installed.") from e
120
+
121
+ try:
122
+ if isinstance(data, pa.Table):
123
+ data = data.combine_chunks()
124
+ if data.num_rows == 0:
125
+ raise ValueError("Input Arrow Table is empty.")
126
+ batch = data.to_batches()[0]
127
+ elif isinstance(data, pa.RecordBatch):
128
+ batch = data
129
+ else:
130
+ raise TypeError("Input must be a pyarrow.Table or pyarrow.RecordBatch.")
131
+
132
+ if batch.num_rows == 0:
133
+ raise ValueError("Input Arrow Batch is empty.")
134
+
135
+ c_schema = ArrowSchema()
136
+ c_array = ArrowArray()
137
+
138
+ batch._export_to_c(
139
+ ctypes.addressof(c_array),
140
+ ctypes.addressof(c_schema)
141
+ )
142
+
143
+ mgr_ptr = lib.pardox_ingest_arrow_stream(
144
+ ctypes.byref(c_array),
145
+ ctypes.byref(c_schema)
146
+ )
147
+
148
+ if not mgr_ptr:
149
+ raise RuntimeError("PardoX Core returned NULL pointer (Ingestion Failed).")
150
+
151
+ return DataFrame(mgr_ptr)
152
+
153
+ except Exception as e:
154
+ raise RuntimeError(f"PardoX Arrow Ingestion Failed: {e}")
155
+
156
+
157
+ def read_prdx(path, limit=100):
158
+ """
159
+ Reads a native PardoX (.prdx) file.
160
+
161
+ NOTE: Currently in V0.1 Beta Showcase Mode.
162
+ This uses the Native Reader to inspect the file structure and data integrity.
163
+ It returns a list of dictionaries (JSON equivalent) for preview purposes.
164
+
165
+ Args:
166
+ path (str): Path to the .prdx file.
167
+ limit (int): Number of rows to inspect (Head).
168
+
169
+ Returns:
170
+ list: A list of dicts containing the data rows.
171
+ """
172
+ if not os.path.exists(path):
173
+ raise FileNotFoundError(f"File not found: {path}")
174
+
175
+ path_bytes = path.encode('utf-8')
176
+
177
+ if not hasattr(lib, 'pardox_read_head_json'):
178
+ raise NotImplementedError("API 'pardox_read_head_json' not found in Core.")
179
+
180
+ # Call Rust Native Reader
181
+ json_ptr = lib.pardox_read_head_json(path_bytes, limit)
182
+
183
+ if not json_ptr:
184
+ raise RuntimeError("Failed to read PRDX file (Rust returned NULL).")
185
+
186
+ try:
187
+ # Decode Pointer -> String
188
+ json_str = ctypes.cast(json_ptr, c_char_p).value.decode('utf-8')
189
+ return json.loads(json_str)
190
+ finally:
191
+ # Free Rust Memory
192
+ if hasattr(lib, 'pardox_free_string'):
193
+ lib.pardox_free_string(json_ptr)