Restore 0.1.5 version from stash

This commit is contained in:
liaibo
2025-12-08 19:56:24 +08:00
parent de189e938d
commit 8db3f4e32d
8578 changed files with 2703426 additions and 217 deletions
@@ -0,0 +1,4 @@
from typing import Any
JSONReturnType = dict[str, Any] | list[Any] | str | float | int | bool | None
STRING_DELIMITERS: list[str] = ['"', "'", "", ""]
@@ -0,0 +1,42 @@
from enum import Enum, auto
class ContextValues(Enum):
OBJECT_KEY = auto()
OBJECT_VALUE = auto()
ARRAY = auto()
class JsonContext:
def __init__(self) -> None:
self.context: list[ContextValues] = []
self.current: ContextValues | None = None
self.empty: bool = True
def set(self, value: ContextValues) -> None:
"""
Set a new context value.
Args:
value (ContextValues): The context value to be added.
Returns:
None
"""
self.context.append(value)
self.current = value
self.empty = False
def reset(self) -> None:
"""
Remove the most recent context value.
Returns:
None
"""
try:
self.context.pop()
self.current = self.context[-1]
except IndexError:
self.current = None
self.empty = True
@@ -0,0 +1,47 @@
from typing import Any
class ObjectComparer: # pragma: no cover
def __init__(self) -> None:
pass # No operation performed in the constructor
@staticmethod
def is_same_object(obj1: Any, obj2: Any) -> bool:
"""
Recursively compares two objects and ensures that:
- Their types match
- Their keys/structure match
"""
if type(obj1) is not type(obj2):
# Fail immediately if the types don't match
return False
if isinstance(obj1, dict):
# Check that both are dicts and same length
if not isinstance(obj2, dict) or len(obj1) != len(obj2):
return False
for key in obj1:
if key not in obj2:
return False
# Recursively compare each value
if not ObjectComparer.is_same_object(obj1[key], obj2[key]):
return False
return True
elif isinstance(obj1, list):
# Check that both are lists and same length
if not isinstance(obj2, list) or len(obj1) != len(obj2):
return False
# Recursively compare each item
return all(ObjectComparer.is_same_object(obj1[i], obj2[i]) for i in range(len(obj1)))
# For atomic values: types already match, so return True
return True
@staticmethod
def is_strictly_empty(value: Any) -> bool:
"""
Returns True if value is an empty container (str, list, dict, set, tuple).
Returns False for non-containers like None, 0, False, etc.
"""
return isinstance(value, str | list | dict | set | tuple) and len(value) == 0
@@ -0,0 +1,176 @@
import os
from typing import TextIO
class StringFileWrapper:
# This is a trick to simplify the code, transform the filedescriptor handling into a string handling
def __init__(self, fd: TextIO, chunk_length: int) -> None:
"""
Initialize the StringFileWrapper with a file descriptor and chunk length.
Args:
fd (TextIO): The file descriptor to wrap.
CHUNK_LENGTH (int): The length of each chunk to read from the file.
Attributes:
fd (TextIO): The wrapped file descriptor.
length (int): The total length of the file content.
buffers (dict[int, str]): Dictionary to store chunks of file content.
buffer_length (int): The length of each buffer chunk.
"""
self.fd = fd
# Buffers are chunks of text read from the file and cached to reduce disk access.
self.buffers: dict[int, str] = {}
if not chunk_length or chunk_length < 2:
chunk_length = 1_000_000
# chunk_length now refers to the number of characters per chunk.
self.buffer_length = chunk_length
# Keep track of the starting file position ("cookie") for each chunk so we can
# seek safely without landing in the middle of a multibyte code point.
self._chunk_positions: list[int] = [0]
self.length: int | None = None
def get_buffer(self, index: int) -> str:
"""
Retrieve or load a buffer chunk from the file.
Args:
index (int): The index of the buffer chunk to retrieve.
Returns:
str: The buffer chunk at the specified index.
"""
if index < 0:
raise IndexError("Negative indexing is not supported")
cached = self.buffers.get(index)
if cached is not None:
return cached
self._ensure_chunk_position(index)
start_pos = self._chunk_positions[index]
self.fd.seek(start_pos)
chunk = self.fd.read(self.buffer_length)
if not chunk:
raise IndexError("Chunk index out of range")
end_pos = self.fd.tell()
if len(self._chunk_positions) <= index + 1:
self._chunk_positions.append(end_pos)
if len(chunk) < self.buffer_length:
self.length = index * self.buffer_length + len(chunk)
self.buffers[index] = chunk
# Save memory by keeping max 2MB buffer chunks and min 2 chunks
max_buffers = max(2, int(2_000_000 / self.buffer_length))
if len(self.buffers) > max_buffers:
oldest_key = next(iter(self.buffers))
if oldest_key != index:
self.buffers.pop(oldest_key)
return chunk
def __getitem__(self, index: int | slice) -> str:
"""
Retrieve a character or a slice of characters from the file.
Args:
index (Union[int, slice]): The index or slice of characters to retrieve.
Returns:
str: The character(s) at the specified index or slice.
"""
# The buffer is an array that is seek like a RAM:
# self.buffers[index]: the row in the array of length 1MB, index is `i` modulo CHUNK_LENGTH
# self.buffures[index][j]: the column of the row that is `i` remainder CHUNK_LENGTH
if isinstance(index, slice):
total_len = len(self)
start = 0 if index.start is None else index.start
stop = total_len if index.stop is None else index.stop
step = 1 if index.step is None else index.step
if start < 0:
start += total_len
if stop < 0:
stop += total_len
start = max(start, 0)
stop = min(stop, total_len)
if step == 0:
raise ValueError("slice step cannot be zero")
if step != 1:
return "".join(self[i] for i in range(start, stop, step))
if start >= stop:
return ""
buffer_index = start // self.buffer_length
buffer_end = (stop - 1) // self.buffer_length
start_mod = start % self.buffer_length
stop_mod = stop % self.buffer_length
if stop_mod == 0 and stop > start:
stop_mod = self.buffer_length
if buffer_index == buffer_end:
buffer = self.get_buffer(buffer_index)
return buffer[start_mod:stop_mod]
start_slice = self.get_buffer(buffer_index)[start_mod:]
end_slice = self.get_buffer(buffer_end)[:stop_mod]
middle_slices = [self.get_buffer(i) for i in range(buffer_index + 1, buffer_end)]
return start_slice + "".join(middle_slices) + end_slice
else:
if index < 0:
index += len(self)
if index < 0:
raise IndexError("string index out of range")
buffer_index = index // self.buffer_length
buffer = self.get_buffer(buffer_index)
return buffer[index % self.buffer_length]
def __len__(self) -> int:
"""
Get the total length of the file.
Returns:
int: The total number of characters in the file.
"""
if self.length is None:
while self.length is None:
chunk_index = len(self._chunk_positions)
self._ensure_chunk_position(chunk_index)
return self.length
def __setitem__(self, index: int | slice, value: str) -> None: # pragma: no cover
"""
Set a character or a slice of characters in the file.
Args:
index (slice): The slice of characters to set.
value (str): The value to set at the specified index or slice.
"""
start = index.start or 0 if isinstance(index, slice) else index or 0
if start < 0:
start += len(self)
current_position = self.fd.tell()
self.fd.seek(start)
self.fd.write(value)
self.fd.seek(current_position)
def _ensure_chunk_position(self, chunk_index: int) -> None:
"""
Ensure that we know the starting file position for the given chunk index.
"""
while len(self._chunk_positions) <= chunk_index:
prev_index = len(self._chunk_positions) - 1
start_pos = self._chunk_positions[-1]
self.fd.seek(start_pos, os.SEEK_SET)
chunk = self.fd.read(self.buffer_length)
end_pos = self.fd.tell()
if len(chunk) < self.buffer_length:
self.length = prev_index * self.buffer_length + len(chunk)
self._chunk_positions.append(end_pos)
if not chunk:
break
if len(self._chunk_positions) <= chunk_index:
raise IndexError("Chunk index out of range")