exonware-xwsystem 0.0.1.409__py3-none-any.whl → 0.0.1.411__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.
- exonware/__init__.py +2 -2
- exonware/conf.py +10 -20
- exonware/xwsystem/__init__.py +6 -16
- exonware/xwsystem/caching/__init__.py +1 -1
- exonware/xwsystem/caching/base.py +16 -16
- exonware/xwsystem/caching/bloom_cache.py +5 -5
- exonware/xwsystem/caching/cache_manager.py +2 -2
- exonware/xwsystem/caching/conditional.py +4 -4
- exonware/xwsystem/caching/contracts.py +12 -12
- exonware/xwsystem/caching/decorators.py +2 -2
- exonware/xwsystem/caching/defs.py +1 -1
- exonware/xwsystem/caching/disk_cache.py +4 -4
- exonware/xwsystem/caching/distributed.py +1 -1
- exonware/xwsystem/caching/errors.py +1 -1
- exonware/xwsystem/caching/events.py +8 -8
- exonware/xwsystem/caching/eviction_strategies.py +9 -9
- exonware/xwsystem/caching/fluent.py +1 -1
- exonware/xwsystem/caching/integrity.py +1 -1
- exonware/xwsystem/caching/lfu_cache.py +24 -9
- exonware/xwsystem/caching/lfu_optimized.py +21 -21
- exonware/xwsystem/caching/lru_cache.py +14 -7
- exonware/xwsystem/caching/memory_bounded.py +8 -8
- exonware/xwsystem/caching/metrics_exporter.py +6 -6
- exonware/xwsystem/caching/observable_cache.py +1 -1
- exonware/xwsystem/caching/pluggable_cache.py +9 -9
- exonware/xwsystem/caching/rate_limiter.py +1 -1
- exonware/xwsystem/caching/read_through.py +6 -6
- exonware/xwsystem/caching/secure_cache.py +1 -1
- exonware/xwsystem/caching/serializable.py +3 -3
- exonware/xwsystem/caching/stats.py +7 -7
- exonware/xwsystem/caching/tagging.py +11 -11
- exonware/xwsystem/caching/ttl_cache.py +21 -6
- exonware/xwsystem/caching/two_tier_cache.py +5 -5
- exonware/xwsystem/caching/utils.py +3 -3
- exonware/xwsystem/caching/validation.py +1 -1
- exonware/xwsystem/caching/warming.py +9 -9
- exonware/xwsystem/caching/write_behind.py +5 -5
- exonware/xwsystem/cli/__init__.py +1 -1
- exonware/xwsystem/cli/args.py +10 -10
- exonware/xwsystem/cli/base.py +15 -15
- exonware/xwsystem/cli/colors.py +1 -1
- exonware/xwsystem/cli/console.py +1 -1
- exonware/xwsystem/cli/contracts.py +5 -5
- exonware/xwsystem/cli/defs.py +1 -1
- exonware/xwsystem/cli/errors.py +1 -1
- exonware/xwsystem/cli/progress.py +1 -1
- exonware/xwsystem/cli/prompts.py +1 -1
- exonware/xwsystem/cli/tables.py +7 -7
- exonware/xwsystem/config/__init__.py +1 -1
- exonware/xwsystem/config/base.py +14 -14
- exonware/xwsystem/config/contracts.py +22 -22
- exonware/xwsystem/config/defaults.py +2 -2
- exonware/xwsystem/config/defs.py +1 -1
- exonware/xwsystem/config/errors.py +2 -2
- exonware/xwsystem/config/logging.py +1 -1
- exonware/xwsystem/config/logging_setup.py +2 -2
- exonware/xwsystem/config/performance.py +7 -7
- exonware/xwsystem/config/performance_modes.py +20 -20
- exonware/xwsystem/config/version_manager.py +4 -4
- exonware/xwsystem/{http → http_client}/__init__.py +1 -1
- exonware/xwsystem/{http → http_client}/advanced_client.py +20 -20
- exonware/xwsystem/{http → http_client}/base.py +13 -13
- exonware/xwsystem/{http → http_client}/client.py +43 -43
- exonware/xwsystem/{http → http_client}/contracts.py +5 -5
- exonware/xwsystem/{http → http_client}/defs.py +2 -2
- exonware/xwsystem/{http → http_client}/errors.py +2 -2
- exonware/xwsystem/io/__init__.py +1 -1
- exonware/xwsystem/io/archive/__init__.py +1 -1
- exonware/xwsystem/io/archive/archive.py +5 -5
- exonware/xwsystem/io/archive/archive_files.py +8 -8
- exonware/xwsystem/io/archive/archivers.py +3 -3
- exonware/xwsystem/io/archive/base.py +17 -17
- exonware/xwsystem/io/archive/codec_integration.py +1 -1
- exonware/xwsystem/io/archive/compression.py +1 -1
- exonware/xwsystem/io/archive/formats/__init__.py +1 -1
- exonware/xwsystem/io/archive/formats/brotli_format.py +12 -9
- exonware/xwsystem/io/archive/formats/lz4_format.py +12 -9
- exonware/xwsystem/io/archive/formats/rar.py +12 -9
- exonware/xwsystem/io/archive/formats/sevenzip.py +12 -9
- exonware/xwsystem/io/archive/formats/squashfs_format.py +7 -7
- exonware/xwsystem/io/archive/formats/tar.py +8 -8
- exonware/xwsystem/io/archive/formats/wim_format.py +12 -9
- exonware/xwsystem/io/archive/formats/zip.py +8 -8
- exonware/xwsystem/io/archive/formats/zpaq_format.py +7 -7
- exonware/xwsystem/io/archive/formats/zstandard.py +12 -9
- exonware/xwsystem/io/base.py +17 -17
- exonware/xwsystem/io/codec/__init__.py +1 -1
- exonware/xwsystem/io/codec/base.py +261 -14
- exonware/xwsystem/io/codec/contracts.py +3 -6
- exonware/xwsystem/io/codec/registry.py +29 -29
- exonware/xwsystem/io/common/__init__.py +1 -1
- exonware/xwsystem/io/common/atomic.py +2 -2
- exonware/xwsystem/io/common/base.py +1 -1
- exonware/xwsystem/io/common/lock.py +1 -1
- exonware/xwsystem/io/common/watcher.py +4 -4
- exonware/xwsystem/io/contracts.py +34 -39
- exonware/xwsystem/io/data_operations.py +480 -0
- exonware/xwsystem/io/defs.py +2 -2
- exonware/xwsystem/io/errors.py +32 -3
- exonware/xwsystem/io/facade.py +4 -4
- exonware/xwsystem/io/file/__init__.py +1 -1
- exonware/xwsystem/io/file/base.py +2 -2
- exonware/xwsystem/io/file/conversion.py +1 -1
- exonware/xwsystem/io/file/file.py +10 -8
- exonware/xwsystem/io/file/paged_source.py +8 -1
- exonware/xwsystem/io/file/paging/__init__.py +1 -1
- exonware/xwsystem/io/file/paging/byte_paging.py +1 -1
- exonware/xwsystem/io/file/paging/line_paging.py +1 -1
- exonware/xwsystem/io/file/paging/record_paging.py +1 -1
- exonware/xwsystem/io/file/paging/registry.py +5 -5
- exonware/xwsystem/io/file/source.py +22 -11
- exonware/xwsystem/io/filesystem/__init__.py +1 -1
- exonware/xwsystem/io/filesystem/base.py +1 -1
- exonware/xwsystem/io/filesystem/local.py +9 -1
- exonware/xwsystem/io/folder/__init__.py +1 -1
- exonware/xwsystem/io/folder/base.py +2 -2
- exonware/xwsystem/io/folder/folder.py +6 -6
- exonware/xwsystem/io/serialization/__init__.py +1 -1
- exonware/xwsystem/io/serialization/auto_serializer.py +53 -40
- exonware/xwsystem/io/serialization/base.py +248 -35
- exonware/xwsystem/io/serialization/contracts.py +93 -4
- exonware/xwsystem/io/serialization/defs.py +1 -1
- exonware/xwsystem/io/serialization/errors.py +1 -1
- exonware/xwsystem/io/serialization/flyweight.py +22 -22
- exonware/xwsystem/io/serialization/format_detector.py +18 -15
- exonware/xwsystem/io/serialization/formats/__init__.py +1 -1
- exonware/xwsystem/io/serialization/formats/binary/bson.py +1 -1
- exonware/xwsystem/io/serialization/formats/binary/cbor.py +1 -1
- exonware/xwsystem/io/serialization/formats/binary/marshal.py +1 -1
- exonware/xwsystem/io/serialization/formats/binary/msgpack.py +1 -1
- exonware/xwsystem/io/serialization/formats/binary/pickle.py +1 -1
- exonware/xwsystem/io/serialization/formats/binary/plistlib.py +1 -1
- exonware/xwsystem/io/serialization/formats/database/dbm.py +53 -1
- exonware/xwsystem/io/serialization/formats/database/shelve.py +48 -1
- exonware/xwsystem/io/serialization/formats/database/sqlite3.py +85 -1
- exonware/xwsystem/io/serialization/formats/text/configparser.py +2 -2
- exonware/xwsystem/io/serialization/formats/text/csv.py +2 -2
- exonware/xwsystem/io/serialization/formats/text/formdata.py +2 -2
- exonware/xwsystem/io/serialization/formats/text/json.py +23 -5
- exonware/xwsystem/io/serialization/formats/text/json5.py +98 -13
- exonware/xwsystem/io/serialization/formats/text/jsonlines.py +230 -20
- exonware/xwsystem/io/serialization/formats/text/multipart.py +2 -2
- exonware/xwsystem/io/serialization/formats/text/toml.py +65 -4
- exonware/xwsystem/io/serialization/formats/text/xml.py +451 -69
- exonware/xwsystem/io/serialization/formats/text/yaml.py +52 -2
- exonware/xwsystem/io/serialization/registry.py +5 -5
- exonware/xwsystem/io/serialization/serializer.py +184 -12
- exonware/xwsystem/io/serialization/utils/__init__.py +1 -1
- exonware/xwsystem/io/serialization/utils/path_ops.py +3 -3
- exonware/xwsystem/io/stream/__init__.py +1 -1
- exonware/xwsystem/io/stream/async_operations.py +3 -3
- exonware/xwsystem/io/stream/base.py +3 -7
- exonware/xwsystem/io/stream/codec_io.py +4 -7
- exonware/xwsystem/ipc/async_fabric.py +7 -8
- exonware/xwsystem/ipc/base.py +9 -9
- exonware/xwsystem/ipc/contracts.py +5 -5
- exonware/xwsystem/ipc/defs.py +1 -1
- exonware/xwsystem/ipc/errors.py +2 -2
- exonware/xwsystem/ipc/message_queue.py +4 -6
- exonware/xwsystem/ipc/pipes.py +2 -2
- exonware/xwsystem/ipc/process_manager.py +7 -7
- exonware/xwsystem/ipc/process_pool.py +8 -8
- exonware/xwsystem/ipc/shared_memory.py +7 -7
- exonware/xwsystem/monitoring/base.py +33 -33
- exonware/xwsystem/monitoring/contracts.py +27 -27
- exonware/xwsystem/monitoring/defs.py +1 -1
- exonware/xwsystem/monitoring/error_recovery.py +16 -16
- exonware/xwsystem/monitoring/errors.py +2 -2
- exonware/xwsystem/monitoring/memory_monitor.py +12 -12
- exonware/xwsystem/monitoring/metrics.py +8 -8
- exonware/xwsystem/monitoring/performance_manager_generic.py +20 -20
- exonware/xwsystem/monitoring/performance_monitor.py +11 -11
- exonware/xwsystem/monitoring/performance_validator.py +21 -21
- exonware/xwsystem/monitoring/system_monitor.py +17 -17
- exonware/xwsystem/monitoring/tracing.py +20 -20
- exonware/xwsystem/monitoring/tracker.py +7 -7
- exonware/xwsystem/operations/__init__.py +5 -5
- exonware/xwsystem/operations/base.py +3 -3
- exonware/xwsystem/operations/contracts.py +3 -3
- exonware/xwsystem/operations/defs.py +5 -5
- exonware/xwsystem/operations/diff.py +5 -5
- exonware/xwsystem/operations/merge.py +2 -2
- exonware/xwsystem/operations/patch.py +5 -5
- exonware/xwsystem/patterns/base.py +4 -4
- exonware/xwsystem/patterns/context_manager.py +7 -7
- exonware/xwsystem/patterns/contracts.py +29 -31
- exonware/xwsystem/patterns/defs.py +1 -1
- exonware/xwsystem/patterns/dynamic_facade.py +9 -9
- exonware/xwsystem/patterns/errors.py +10 -10
- exonware/xwsystem/patterns/handler_factory.py +15 -14
- exonware/xwsystem/patterns/import_registry.py +22 -22
- exonware/xwsystem/patterns/object_pool.py +14 -13
- exonware/xwsystem/patterns/registry.py +45 -32
- exonware/xwsystem/plugins/__init__.py +1 -1
- exonware/xwsystem/plugins/base.py +25 -25
- exonware/xwsystem/plugins/contracts.py +28 -28
- exonware/xwsystem/plugins/defs.py +1 -1
- exonware/xwsystem/plugins/errors.py +9 -9
- exonware/xwsystem/runtime/__init__.py +1 -1
- exonware/xwsystem/runtime/base.py +42 -42
- exonware/xwsystem/runtime/contracts.py +9 -9
- exonware/xwsystem/runtime/defs.py +1 -1
- exonware/xwsystem/runtime/env.py +9 -9
- exonware/xwsystem/runtime/errors.py +1 -1
- exonware/xwsystem/runtime/reflection.py +15 -15
- exonware/xwsystem/security/auth.py +47 -15
- exonware/xwsystem/security/base.py +17 -17
- exonware/xwsystem/security/contracts.py +30 -30
- exonware/xwsystem/security/crypto.py +8 -8
- exonware/xwsystem/security/defs.py +1 -1
- exonware/xwsystem/security/errors.py +2 -2
- exonware/xwsystem/security/hazmat.py +7 -7
- exonware/xwsystem/security/path_validator.py +1 -1
- exonware/xwsystem/shared/__init__.py +1 -1
- exonware/xwsystem/shared/base.py +14 -14
- exonware/xwsystem/shared/contracts.py +6 -6
- exonware/xwsystem/shared/defs.py +1 -1
- exonware/xwsystem/shared/errors.py +1 -1
- exonware/xwsystem/structures/__init__.py +1 -1
- exonware/xwsystem/structures/base.py +29 -29
- exonware/xwsystem/structures/circular_detector.py +15 -15
- exonware/xwsystem/structures/contracts.py +9 -9
- exonware/xwsystem/structures/defs.py +1 -1
- exonware/xwsystem/structures/errors.py +2 -2
- exonware/xwsystem/structures/tree_walker.py +8 -8
- exonware/xwsystem/threading/async_primitives.py +7 -7
- exonware/xwsystem/threading/base.py +19 -19
- exonware/xwsystem/threading/contracts.py +13 -13
- exonware/xwsystem/threading/defs.py +1 -1
- exonware/xwsystem/threading/errors.py +2 -2
- exonware/xwsystem/threading/safe_factory.py +13 -12
- exonware/xwsystem/utils/base.py +34 -34
- exonware/xwsystem/utils/contracts.py +9 -9
- exonware/xwsystem/utils/dt/__init__.py +1 -1
- exonware/xwsystem/utils/dt/base.py +6 -6
- exonware/xwsystem/utils/dt/contracts.py +2 -2
- exonware/xwsystem/utils/dt/defs.py +1 -1
- exonware/xwsystem/utils/dt/errors.py +2 -2
- exonware/xwsystem/utils/dt/formatting.py +3 -3
- exonware/xwsystem/utils/dt/humanize.py +2 -2
- exonware/xwsystem/utils/dt/parsing.py +2 -2
- exonware/xwsystem/utils/dt/timezone_utils.py +5 -5
- exonware/xwsystem/utils/errors.py +2 -2
- exonware/xwsystem/utils/test_runner.py +6 -6
- exonware/xwsystem/utils/utils_contracts.py +1 -1
- exonware/xwsystem/validation/__init__.py +1 -1
- exonware/xwsystem/validation/base.py +48 -48
- exonware/xwsystem/validation/contracts.py +8 -8
- exonware/xwsystem/validation/data_validator.py +10 -0
- exonware/xwsystem/validation/declarative.py +15 -15
- exonware/xwsystem/validation/defs.py +1 -1
- exonware/xwsystem/validation/errors.py +2 -2
- exonware/xwsystem/validation/fluent_validator.py +10 -10
- exonware/xwsystem/version.py +2 -2
- {exonware_xwsystem-0.0.1.409.dist-info → exonware_xwsystem-0.0.1.411.dist-info}/METADATA +9 -11
- exonware_xwsystem-0.0.1.411.dist-info/RECORD +274 -0
- {exonware_xwsystem-0.0.1.409.dist-info → exonware_xwsystem-0.0.1.411.dist-info}/WHEEL +1 -1
- exonware/xwsystem/lazy_bootstrap.py +0 -79
- exonware_xwsystem-0.0.1.409.dist-info/RECORD +0 -274
- {exonware_xwsystem-0.0.1.409.dist-info → exonware_xwsystem-0.0.1.411.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
#exonware/xwsystem/src/exonware/xwsystem/io/data_operations.py
|
|
4
|
+
|
|
5
|
+
Generic data-operations layer for large, file-backed datasets.
|
|
6
|
+
|
|
7
|
+
This module provides:
|
|
8
|
+
- A small indexing model for line-oriented files (e.g. NDJSON / JSONL)
|
|
9
|
+
- Streaming read / update helpers with atomic guarantees
|
|
10
|
+
- Paging helpers built on top of line offsets
|
|
11
|
+
|
|
12
|
+
The goal is to expose these capabilities in a format-agnostic way so that
|
|
13
|
+
higher-level libraries (xwdata, xwnode, xwentity, etc.) can build powerful
|
|
14
|
+
lazy, paged, and atomic access features without re-implementing I/O logic.
|
|
15
|
+
|
|
16
|
+
Company: eXonware.com
|
|
17
|
+
Author: Eng. Muhammad AlShehri
|
|
18
|
+
Email: connect@exonware.com
|
|
19
|
+
Version: 0.0.1.411
|
|
20
|
+
Generation Date: 15-Dec-2025
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any, Callable, Optional
|
|
28
|
+
from abc import ABC, abstractmethod
|
|
29
|
+
import json
|
|
30
|
+
import os
|
|
31
|
+
import tempfile
|
|
32
|
+
|
|
33
|
+
from .serialization.auto_serializer import AutoSerializer
|
|
34
|
+
from ..config.logging_setup import get_logger
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
logger = get_logger(__name__)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
JsonMatchFn = Callable[[Any], bool]
|
|
41
|
+
JsonUpdateFn = Callable[[Any], Any]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class JsonIndexMeta:
|
|
46
|
+
"""
|
|
47
|
+
Minimal metadata for a JSONL/NDJSON index.
|
|
48
|
+
|
|
49
|
+
This intentionally mirrors the capabilities used in the x5 examples
|
|
50
|
+
without pulling in any of the example code directly.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
path: str
|
|
54
|
+
size: int
|
|
55
|
+
mtime: float
|
|
56
|
+
version: int = 1
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class JsonIndex:
|
|
61
|
+
"""
|
|
62
|
+
Simple index for line-oriented JSON files.
|
|
63
|
+
|
|
64
|
+
- line_offsets: byte offset of each JSON line
|
|
65
|
+
- id_index: optional mapping id_value -> line_number
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
meta: JsonIndexMeta
|
|
69
|
+
line_offsets: list[int]
|
|
70
|
+
id_index: Optional[dict[str, int]] = None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ADataOperations(ABC):
|
|
74
|
+
"""
|
|
75
|
+
Abstract, format-agnostic interface for large, file-backed data operations.
|
|
76
|
+
|
|
77
|
+
Concrete implementations may target specific physical layouts
|
|
78
|
+
(NDJSON/JSONL, multi-document YAML, binary record stores, etc.), but MUST
|
|
79
|
+
conform to these semantics:
|
|
80
|
+
|
|
81
|
+
- Streaming, record-by-record read with a match predicate.
|
|
82
|
+
- Streaming, atomic update using a temp file + replace pattern.
|
|
83
|
+
- Optional indexing for random access and paging.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
@abstractmethod
|
|
87
|
+
def stream_read(
|
|
88
|
+
self,
|
|
89
|
+
file_path: str | Path,
|
|
90
|
+
match: JsonMatchFn,
|
|
91
|
+
path: Optional[list[object]] = None,
|
|
92
|
+
encoding: str = "utf-8",
|
|
93
|
+
) -> Any:
|
|
94
|
+
"""Return the first record (or sub-path) that matches the predicate."""
|
|
95
|
+
raise NotImplementedError
|
|
96
|
+
|
|
97
|
+
@abstractmethod
|
|
98
|
+
def stream_update(
|
|
99
|
+
self,
|
|
100
|
+
file_path: str | Path,
|
|
101
|
+
match: JsonMatchFn,
|
|
102
|
+
updater: JsonUpdateFn,
|
|
103
|
+
*,
|
|
104
|
+
encoding: str = "utf-8",
|
|
105
|
+
newline: str = "\n",
|
|
106
|
+
atomic: bool = True,
|
|
107
|
+
) -> int:
|
|
108
|
+
"""
|
|
109
|
+
Stream-copy the backing store, applying `updater` to matching records.
|
|
110
|
+
|
|
111
|
+
MUST use atomic replace semantics when `atomic=True`.
|
|
112
|
+
Returns number of updated records.
|
|
113
|
+
"""
|
|
114
|
+
raise NotImplementedError
|
|
115
|
+
|
|
116
|
+
@abstractmethod
|
|
117
|
+
def build_index(
|
|
118
|
+
self,
|
|
119
|
+
file_path: str | Path,
|
|
120
|
+
*,
|
|
121
|
+
encoding: str = "utf-8",
|
|
122
|
+
id_field: str | None = None,
|
|
123
|
+
max_id_index: int | None = None,
|
|
124
|
+
) -> JsonIndex:
|
|
125
|
+
"""Build an index structure suitable for random access and paging."""
|
|
126
|
+
raise NotImplementedError
|
|
127
|
+
|
|
128
|
+
@abstractmethod
|
|
129
|
+
def indexed_get_by_line(
|
|
130
|
+
self,
|
|
131
|
+
file_path: str | Path,
|
|
132
|
+
line_number: int,
|
|
133
|
+
*,
|
|
134
|
+
encoding: str = "utf-8",
|
|
135
|
+
index: Optional[JsonIndex] = None,
|
|
136
|
+
) -> Any:
|
|
137
|
+
"""Random-access a specific logical record by its index position."""
|
|
138
|
+
raise NotImplementedError
|
|
139
|
+
|
|
140
|
+
@abstractmethod
|
|
141
|
+
def indexed_get_by_id(
|
|
142
|
+
self,
|
|
143
|
+
file_path: str | Path,
|
|
144
|
+
id_value: Any,
|
|
145
|
+
*,
|
|
146
|
+
encoding: str = "utf-8",
|
|
147
|
+
id_field: str = "id",
|
|
148
|
+
index: Optional[JsonIndex] = None,
|
|
149
|
+
) -> Any:
|
|
150
|
+
"""Random-access a record by logical identifier, with optional index."""
|
|
151
|
+
raise NotImplementedError
|
|
152
|
+
|
|
153
|
+
@abstractmethod
|
|
154
|
+
def get_page(
|
|
155
|
+
self,
|
|
156
|
+
file_path: str | Path,
|
|
157
|
+
page_number: int,
|
|
158
|
+
page_size: int,
|
|
159
|
+
*,
|
|
160
|
+
encoding: str = "utf-8",
|
|
161
|
+
index: Optional[JsonIndex] = None,
|
|
162
|
+
) -> list[Any]:
|
|
163
|
+
"""Return a page of logical records using an index for efficiency."""
|
|
164
|
+
raise NotImplementedError
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class NDJSONDataOperations(ADataOperations):
|
|
168
|
+
"""
|
|
169
|
+
Generic data-operations helper for NDJSON / JSONL style files.
|
|
170
|
+
|
|
171
|
+
This class is deliberately low-level and works directly with paths and
|
|
172
|
+
native Python data. XWData and other libraries can wrap it to provide
|
|
173
|
+
higher-level, type-agnostic facades.
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
def __init__(self, serializer: Optional[AutoSerializer] = None):
|
|
177
|
+
# Reuse xwsystem's AutoSerializer so we do not re-implement parsing.
|
|
178
|
+
self._serializer = serializer or AutoSerializer(default_format="JSON")
|
|
179
|
+
|
|
180
|
+
# ------------------------------------------------------------------
|
|
181
|
+
# Streaming read
|
|
182
|
+
# ------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
def stream_read(
|
|
185
|
+
self,
|
|
186
|
+
file_path: str | Path,
|
|
187
|
+
match: JsonMatchFn,
|
|
188
|
+
path: Optional[list[object]] = None,
|
|
189
|
+
encoding: str = "utf-8",
|
|
190
|
+
) -> Any:
|
|
191
|
+
"""
|
|
192
|
+
Stream a huge NDJSON file and return the first record (or sub-path)
|
|
193
|
+
matching `match`.
|
|
194
|
+
|
|
195
|
+
This is intentionally simple and focused:
|
|
196
|
+
- Reads one line at a time
|
|
197
|
+
- Uses AutoSerializer(JSON) for parsing
|
|
198
|
+
- Optional path extraction
|
|
199
|
+
"""
|
|
200
|
+
target = Path(file_path)
|
|
201
|
+
if not target.exists():
|
|
202
|
+
raise FileNotFoundError(str(target))
|
|
203
|
+
|
|
204
|
+
with target.open("r", encoding=encoding) as f:
|
|
205
|
+
for line in f:
|
|
206
|
+
line = line.strip()
|
|
207
|
+
if not line:
|
|
208
|
+
continue
|
|
209
|
+
obj = self._serializer.detect_and_deserialize(
|
|
210
|
+
line, file_path=target, format_hint="JSON"
|
|
211
|
+
)
|
|
212
|
+
if match(obj):
|
|
213
|
+
return self._extract_path(obj, path)
|
|
214
|
+
|
|
215
|
+
raise KeyError("No matching record found")
|
|
216
|
+
|
|
217
|
+
# ------------------------------------------------------------------
|
|
218
|
+
# Streaming update with atomic replace
|
|
219
|
+
# ------------------------------------------------------------------
|
|
220
|
+
|
|
221
|
+
def stream_update(
|
|
222
|
+
self,
|
|
223
|
+
file_path: str | Path,
|
|
224
|
+
match: JsonMatchFn,
|
|
225
|
+
updater: JsonUpdateFn,
|
|
226
|
+
*,
|
|
227
|
+
encoding: str = "utf-8",
|
|
228
|
+
newline: str = "\n",
|
|
229
|
+
atomic: bool = True,
|
|
230
|
+
) -> int:
|
|
231
|
+
"""
|
|
232
|
+
Stream-copy a huge NDJSON file, applying `updater` to records
|
|
233
|
+
where `match(obj)` is True.
|
|
234
|
+
|
|
235
|
+
Only matching records are fully materialized. All writes go to a
|
|
236
|
+
temporary file, which is atomically replaced on success.
|
|
237
|
+
|
|
238
|
+
Returns the number of updated records.
|
|
239
|
+
"""
|
|
240
|
+
target = Path(file_path)
|
|
241
|
+
if not target.exists():
|
|
242
|
+
raise FileNotFoundError(str(target))
|
|
243
|
+
|
|
244
|
+
updated = 0
|
|
245
|
+
dir_path = target.parent
|
|
246
|
+
|
|
247
|
+
# Write to a temp file in the same directory for atomic replace.
|
|
248
|
+
fd, tmp_path_str = tempfile.mkstemp(
|
|
249
|
+
prefix=f".{target.name}.tmp.", dir=str(dir_path)
|
|
250
|
+
)
|
|
251
|
+
tmp_path = Path(tmp_path_str)
|
|
252
|
+
|
|
253
|
+
try:
|
|
254
|
+
with os.fdopen(fd, "w", encoding=encoding, newline=newline) as out_f, target.open(
|
|
255
|
+
"r", encoding=encoding
|
|
256
|
+
) as in_f:
|
|
257
|
+
for line in in_f:
|
|
258
|
+
raw = line.rstrip("\n")
|
|
259
|
+
if not raw:
|
|
260
|
+
out_f.write(line)
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
obj = self._serializer.detect_and_deserialize(
|
|
264
|
+
raw, file_path=target, format_hint="JSON"
|
|
265
|
+
)
|
|
266
|
+
if match(obj):
|
|
267
|
+
updated_obj = updater(obj)
|
|
268
|
+
updated_line = json.dumps(updated_obj, ensure_ascii=False)
|
|
269
|
+
out_f.write(updated_line + newline)
|
|
270
|
+
updated += 1
|
|
271
|
+
else:
|
|
272
|
+
out_f.write(line)
|
|
273
|
+
|
|
274
|
+
if atomic:
|
|
275
|
+
os.replace(tmp_path, target)
|
|
276
|
+
else:
|
|
277
|
+
tmp_path.replace(target)
|
|
278
|
+
|
|
279
|
+
return updated
|
|
280
|
+
finally:
|
|
281
|
+
# Ensure temp file is removed on error
|
|
282
|
+
if tmp_path.exists():
|
|
283
|
+
try:
|
|
284
|
+
tmp_path.unlink()
|
|
285
|
+
except OSError:
|
|
286
|
+
# Best-effort cleanup; do not mask original error.
|
|
287
|
+
logger.debug("Failed to cleanup temp file %s", tmp_path)
|
|
288
|
+
|
|
289
|
+
# ------------------------------------------------------------------
|
|
290
|
+
# Indexing and paging
|
|
291
|
+
# ------------------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
def build_index(
|
|
294
|
+
self,
|
|
295
|
+
file_path: str | Path,
|
|
296
|
+
*,
|
|
297
|
+
encoding: str = "utf-8",
|
|
298
|
+
id_field: str | None = None,
|
|
299
|
+
max_id_index: int | None = None,
|
|
300
|
+
) -> JsonIndex:
|
|
301
|
+
"""
|
|
302
|
+
One-time full scan to build an index:
|
|
303
|
+
- line_offsets: byte offset of each JSON line
|
|
304
|
+
- optional id_index: obj[id_field] -> line_number
|
|
305
|
+
"""
|
|
306
|
+
target = Path(file_path)
|
|
307
|
+
if not target.exists():
|
|
308
|
+
raise FileNotFoundError(str(target))
|
|
309
|
+
|
|
310
|
+
line_offsets: list[int] = []
|
|
311
|
+
id_index: dict[str, int] | None = {} if id_field else None
|
|
312
|
+
|
|
313
|
+
size = target.stat().st_size
|
|
314
|
+
mtime = target.stat().st_mtime
|
|
315
|
+
|
|
316
|
+
offset = 0
|
|
317
|
+
with target.open("rb") as f:
|
|
318
|
+
line_no = 0
|
|
319
|
+
while True:
|
|
320
|
+
line = f.readline()
|
|
321
|
+
if not line:
|
|
322
|
+
break
|
|
323
|
+
line_offsets.append(offset)
|
|
324
|
+
|
|
325
|
+
if id_index is not None:
|
|
326
|
+
try:
|
|
327
|
+
text = line.decode(encoding).strip()
|
|
328
|
+
if text:
|
|
329
|
+
obj = self._serializer.detect_and_deserialize(
|
|
330
|
+
text, file_path=target, format_hint="JSON"
|
|
331
|
+
)
|
|
332
|
+
if isinstance(obj, dict) and id_field in obj:
|
|
333
|
+
id_val = str(obj[id_field])
|
|
334
|
+
if max_id_index is None or len(id_index) < max_id_index:
|
|
335
|
+
id_index[id_val] = line_no
|
|
336
|
+
except Exception:
|
|
337
|
+
# Index should be best-effort and robust to bad lines.
|
|
338
|
+
logger.debug("Skipping line %s while building id index", line_no)
|
|
339
|
+
|
|
340
|
+
offset += len(line)
|
|
341
|
+
line_no += 1
|
|
342
|
+
|
|
343
|
+
meta = JsonIndexMeta(path=str(target), size=size, mtime=mtime, version=1)
|
|
344
|
+
return JsonIndex(meta=meta, line_offsets=line_offsets, id_index=id_index)
|
|
345
|
+
|
|
346
|
+
def indexed_get_by_line(
|
|
347
|
+
self,
|
|
348
|
+
file_path: str | Path,
|
|
349
|
+
line_number: int,
|
|
350
|
+
*,
|
|
351
|
+
encoding: str = "utf-8",
|
|
352
|
+
index: Optional[JsonIndex] = None,
|
|
353
|
+
) -> Any:
|
|
354
|
+
"""
|
|
355
|
+
Random-access a specific record by line_number (0-based) using index.
|
|
356
|
+
"""
|
|
357
|
+
target = Path(file_path)
|
|
358
|
+
if index is None:
|
|
359
|
+
index = self.build_index(target, encoding=encoding)
|
|
360
|
+
|
|
361
|
+
if line_number < 0 or line_number >= len(index.line_offsets):
|
|
362
|
+
raise IndexError("line_number out of range")
|
|
363
|
+
|
|
364
|
+
offset = index.line_offsets[line_number]
|
|
365
|
+
with target.open("rb") as f:
|
|
366
|
+
f.seek(offset)
|
|
367
|
+
line = f.readline()
|
|
368
|
+
text = line.decode(encoding).strip()
|
|
369
|
+
if not text:
|
|
370
|
+
raise ValueError("Empty line at indexed position")
|
|
371
|
+
return self._serializer.detect_and_deserialize(
|
|
372
|
+
text, file_path=target, format_hint="JSON"
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
def indexed_get_by_id(
|
|
376
|
+
self,
|
|
377
|
+
file_path: str | Path,
|
|
378
|
+
id_value: Any,
|
|
379
|
+
*,
|
|
380
|
+
encoding: str = "utf-8",
|
|
381
|
+
id_field: str = "id",
|
|
382
|
+
index: Optional[JsonIndex] = None,
|
|
383
|
+
) -> Any:
|
|
384
|
+
"""
|
|
385
|
+
Random-access a record by logical id using id_index if available.
|
|
386
|
+
Falls back to linear scan if id_index missing or incomplete.
|
|
387
|
+
"""
|
|
388
|
+
target = Path(file_path)
|
|
389
|
+
if index is None:
|
|
390
|
+
index = self.build_index(target, encoding=encoding, id_field=id_field)
|
|
391
|
+
|
|
392
|
+
id_index = index.id_index
|
|
393
|
+
if id_index is not None:
|
|
394
|
+
key = str(id_value)
|
|
395
|
+
if key in id_index:
|
|
396
|
+
return self.indexed_get_by_line(
|
|
397
|
+
target, id_index[key], encoding=encoding, index=index
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
# Fallback: linear scan using stream_read semantics
|
|
401
|
+
def _match(obj: Any) -> bool:
|
|
402
|
+
return isinstance(obj, dict) and obj.get(id_field) == id_value
|
|
403
|
+
|
|
404
|
+
return self.stream_read(target, _match, path=None, encoding=encoding)
|
|
405
|
+
|
|
406
|
+
def get_page(
|
|
407
|
+
self,
|
|
408
|
+
file_path: str | Path,
|
|
409
|
+
page_number: int,
|
|
410
|
+
page_size: int,
|
|
411
|
+
*,
|
|
412
|
+
encoding: str = "utf-8",
|
|
413
|
+
index: Optional[JsonIndex] = None,
|
|
414
|
+
) -> list[Any]:
|
|
415
|
+
"""
|
|
416
|
+
Paging helper using index:
|
|
417
|
+
- page_number: 1-based
|
|
418
|
+
- page_size: number of records per page
|
|
419
|
+
"""
|
|
420
|
+
target = Path(file_path)
|
|
421
|
+
if index is None:
|
|
422
|
+
index = self.build_index(target, encoding=encoding)
|
|
423
|
+
|
|
424
|
+
if page_number < 1 or page_size <= 0:
|
|
425
|
+
raise ValueError("Invalid page_number or page_size")
|
|
426
|
+
|
|
427
|
+
start = (page_number - 1) * page_size
|
|
428
|
+
end = start + page_size
|
|
429
|
+
|
|
430
|
+
if start >= len(index.line_offsets):
|
|
431
|
+
return []
|
|
432
|
+
|
|
433
|
+
end = min(end, len(index.line_offsets))
|
|
434
|
+
|
|
435
|
+
results: list[Any] = []
|
|
436
|
+
with target.open("rb") as f:
|
|
437
|
+
for line_no in range(start, end):
|
|
438
|
+
offset = index.line_offsets[line_no]
|
|
439
|
+
f.seek(offset)
|
|
440
|
+
line = f.readline()
|
|
441
|
+
text = line.decode(encoding).strip()
|
|
442
|
+
if not text:
|
|
443
|
+
continue
|
|
444
|
+
obj = self._serializer.detect_and_deserialize(
|
|
445
|
+
text, file_path=target, format_hint="JSON"
|
|
446
|
+
)
|
|
447
|
+
results.append(obj)
|
|
448
|
+
|
|
449
|
+
return results
|
|
450
|
+
|
|
451
|
+
# ------------------------------------------------------------------
|
|
452
|
+
# Helpers
|
|
453
|
+
# ------------------------------------------------------------------
|
|
454
|
+
|
|
455
|
+
def _extract_path(self, obj: Any, path: Optional[list[object]]) -> Any:
|
|
456
|
+
"""Extract a nested path like ['user', 'email'] or ['tags', 0]."""
|
|
457
|
+
if not path:
|
|
458
|
+
return obj
|
|
459
|
+
|
|
460
|
+
current = obj
|
|
461
|
+
for part in path:
|
|
462
|
+
if isinstance(current, dict) and isinstance(part, str):
|
|
463
|
+
if part not in current:
|
|
464
|
+
raise KeyError(part)
|
|
465
|
+
current = current[part]
|
|
466
|
+
elif isinstance(current, list) and isinstance(part, int):
|
|
467
|
+
current = current[part]
|
|
468
|
+
else:
|
|
469
|
+
raise KeyError(part)
|
|
470
|
+
return current
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
__all__ = [
|
|
474
|
+
"JsonIndexMeta",
|
|
475
|
+
"JsonIndex",
|
|
476
|
+
"ADataOperations",
|
|
477
|
+
"NDJSONDataOperations",
|
|
478
|
+
]
|
|
479
|
+
|
|
480
|
+
|
exonware/xwsystem/io/defs.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Company: eXonware.com
|
|
3
3
|
Author: Eng. Muhammad AlShehri
|
|
4
4
|
Email: connect@exonware.com
|
|
5
|
-
Version: 0.0.1.
|
|
5
|
+
Version: 0.0.1.411
|
|
6
6
|
Generation Date: 30-Oct-2025
|
|
7
7
|
|
|
8
8
|
IO module definitions - ALL enums and types in ONE place.
|
|
@@ -11,7 +11,7 @@ Consolidated from all submodules for maintainability.
|
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
from enum import Enum, IntEnum, Flag, IntFlag, auto
|
|
14
|
-
from typing import Any,
|
|
14
|
+
from typing import Any, Optional
|
|
15
15
|
from dataclasses import dataclass
|
|
16
16
|
|
|
17
17
|
|
exonware/xwsystem/io/errors.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Company: eXonware.com
|
|
3
3
|
Author: Eng. Muhammad AlShehri
|
|
4
4
|
Email: connect@exonware.com
|
|
5
|
-
Version: 0.0.1.
|
|
5
|
+
Version: 0.0.1.411
|
|
6
6
|
Generation Date: 30-Oct-2025
|
|
7
7
|
|
|
8
8
|
IO module errors - ALL exceptions in ONE place.
|
|
@@ -10,7 +10,7 @@ IO module errors - ALL exceptions in ONE place.
|
|
|
10
10
|
Consolidated from all submodules for maintainability.
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
-
from typing import Any,
|
|
13
|
+
from typing import Any, Optional, Union
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
|
|
16
16
|
|
|
@@ -359,8 +359,37 @@ class SerializationError(Exception):
|
|
|
359
359
|
|
|
360
360
|
Root cause fixed: Added missing SerializationError class that was being
|
|
361
361
|
imported by serialization/base.py but didn't exist.
|
|
362
|
+
|
|
363
|
+
Root cause fixed: Added __init__ to accept format_name and original_error
|
|
364
|
+
parameters that are used throughout the serialization codebase.
|
|
362
365
|
"""
|
|
363
|
-
|
|
366
|
+
|
|
367
|
+
def __init__(
|
|
368
|
+
self,
|
|
369
|
+
message: str,
|
|
370
|
+
format_name: Optional[str] = None,
|
|
371
|
+
original_error: Optional[Exception] = None
|
|
372
|
+
):
|
|
373
|
+
"""
|
|
374
|
+
Initialize SerializationError.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
message: Error message
|
|
378
|
+
format_name: Optional format name (e.g., "JSON", "XML")
|
|
379
|
+
original_error: Optional original exception that caused this error
|
|
380
|
+
"""
|
|
381
|
+
super().__init__(message)
|
|
382
|
+
self.format_name = format_name
|
|
383
|
+
self.original_error = original_error
|
|
384
|
+
|
|
385
|
+
def __str__(self) -> str:
|
|
386
|
+
"""Format error message with optional context."""
|
|
387
|
+
parts = [super().__str__()]
|
|
388
|
+
if self.format_name:
|
|
389
|
+
parts.append(f"[format: {self.format_name}]")
|
|
390
|
+
if self.original_error:
|
|
391
|
+
parts.append(f"[caused by: {type(self.original_error).__name__}]")
|
|
392
|
+
return " ".join(parts)
|
|
364
393
|
|
|
365
394
|
|
|
366
395
|
class EncodeError(CodecError):
|
exonware/xwsystem/io/facade.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Company: eXonware.com
|
|
3
3
|
Author: Eng. Muhammad AlShehri
|
|
4
4
|
Email: connect@exonware.com
|
|
5
|
-
Version: 0.0.1.
|
|
5
|
+
Version: 0.0.1.411
|
|
6
6
|
Generation Date: September 04, 2025
|
|
7
7
|
|
|
8
8
|
XWIO - Main facade for all I/O operations (MANDATORY facade pattern).
|
|
@@ -15,7 +15,7 @@ import shutil
|
|
|
15
15
|
import tempfile
|
|
16
16
|
import time
|
|
17
17
|
from pathlib import Path
|
|
18
|
-
from typing import Any,
|
|
18
|
+
from typing import Any, Optional, Union, BinaryIO, TextIO
|
|
19
19
|
|
|
20
20
|
from .base import AUnifiedIO
|
|
21
21
|
from .contracts import FileMode, FileType, PathType, OperationResult, LockType, IUnifiedIO
|
|
@@ -92,7 +92,7 @@ class XWIO(AUnifiedIO):
|
|
|
92
92
|
|
|
93
93
|
with performance_monitor("file_open"):
|
|
94
94
|
# Ensure parent directory exists
|
|
95
|
-
if self.auto_create_dirs and mode in [FileMode.WRITE, FileMode.APPEND, FileMode.
|
|
95
|
+
if self.auto_create_dirs and mode in [FileMode.WRITE, FileMode.APPEND, FileMode.WRITE_READ]:
|
|
96
96
|
target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
97
97
|
|
|
98
98
|
# Open file
|
|
@@ -873,7 +873,7 @@ class XWIO(AUnifiedIO):
|
|
|
873
873
|
# UTILITY METHODS
|
|
874
874
|
# ============================================================================
|
|
875
875
|
|
|
876
|
-
def get_info(self) ->
|
|
876
|
+
def get_info(self) -> dict[str, Any]:
|
|
877
877
|
"""Get comprehensive I/O information."""
|
|
878
878
|
return {
|
|
879
879
|
'file_path': str(self.file_path) if self.file_path else None,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
Company: eXonware.com
|
|
5
5
|
Author: Eng. Muhammad AlShehri
|
|
6
6
|
Email: connect@exonware.com
|
|
7
|
-
Version: 0.0.1.
|
|
7
|
+
Version: 0.0.1.411
|
|
8
8
|
Generation Date: 30-Oct-2025
|
|
9
9
|
|
|
10
10
|
Base classes for file operations.
|
|
@@ -20,7 +20,7 @@ Priority 5 (Extensibility): Ready for new file types
|
|
|
20
20
|
|
|
21
21
|
from abc import ABC, abstractmethod
|
|
22
22
|
from pathlib import Path
|
|
23
|
-
from typing import Union, Optional,
|
|
23
|
+
from typing import Union, Optional, Any
|
|
24
24
|
|
|
25
25
|
from ..contracts import IFileSource, IPagedSource, IPagingStrategy
|
|
26
26
|
from ..defs import PagingMode
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Company: eXonware.com
|
|
3
3
|
Author: Eng. Muhammad AlShehri
|
|
4
4
|
Email: connect@exonware.com
|
|
5
|
-
Version: 0.0.1.
|
|
5
|
+
Version: 0.0.1.411
|
|
6
6
|
Generation Date: September 04, 2025
|
|
7
7
|
|
|
8
8
|
XWFile - Concrete implementation of file operations.
|
|
@@ -10,7 +10,7 @@ XWFile - Concrete implementation of file operations.
|
|
|
10
10
|
|
|
11
11
|
import os
|
|
12
12
|
from pathlib import Path
|
|
13
|
-
from typing import Any,
|
|
13
|
+
from typing import Any, Optional, Union, BinaryIO, TextIO
|
|
14
14
|
|
|
15
15
|
from ..base import AFile
|
|
16
16
|
from ..contracts import FileMode, OperationResult, IFile
|
|
@@ -72,11 +72,12 @@ class XWFile(AFile):
|
|
|
72
72
|
def open(self, mode: FileMode = FileMode.READ) -> None:
|
|
73
73
|
"""Open file with validation and monitoring."""
|
|
74
74
|
if self.validate_paths:
|
|
75
|
-
|
|
75
|
+
for_writing = mode in [FileMode.WRITE, FileMode.APPEND, FileMode.WRITE_READ, FileMode.BINARY_WRITE, FileMode.BINARY_APPEND, FileMode.BINARY_WRITE_READ]
|
|
76
|
+
self._path_validator.validate_path(self.file_path, for_writing=for_writing, create_dirs=self.auto_create_dirs)
|
|
76
77
|
|
|
77
78
|
with performance_monitor("file_open"):
|
|
78
79
|
# Ensure parent directory exists
|
|
79
|
-
if self.auto_create_dirs and mode in [FileMode.WRITE, FileMode.APPEND, FileMode.
|
|
80
|
+
if self.auto_create_dirs and mode in [FileMode.WRITE, FileMode.APPEND, FileMode.WRITE_READ]:
|
|
80
81
|
self.file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
81
82
|
|
|
82
83
|
# Open file
|
|
@@ -110,7 +111,7 @@ class XWFile(AFile):
|
|
|
110
111
|
def save(self, data: Any, **kwargs) -> bool:
|
|
111
112
|
"""Save data to file with atomic operations."""
|
|
112
113
|
if self.validate_paths:
|
|
113
|
-
self._path_validator.validate_path(self.file_path)
|
|
114
|
+
self._path_validator.validate_path(self.file_path, for_writing=True, create_dirs=True)
|
|
114
115
|
|
|
115
116
|
if self.validate_data:
|
|
116
117
|
self._data_validator.validate_data(data)
|
|
@@ -119,9 +120,10 @@ class XWFile(AFile):
|
|
|
119
120
|
try:
|
|
120
121
|
if self.use_atomic_operations:
|
|
121
122
|
# Use atomic file writer
|
|
122
|
-
|
|
123
|
+
mode = 'wb' if isinstance(data, bytes) else 'w'
|
|
124
|
+
with AtomicFileWriter(self.file_path, mode=mode, backup=self.auto_backup) as writer:
|
|
123
125
|
if isinstance(data, str):
|
|
124
|
-
writer.write(data
|
|
126
|
+
writer.write(data)
|
|
125
127
|
else:
|
|
126
128
|
writer.write(data)
|
|
127
129
|
else:
|
|
@@ -208,7 +210,7 @@ class XWFile(AFile):
|
|
|
208
210
|
# UTILITY METHODS
|
|
209
211
|
# ============================================================================
|
|
210
212
|
|
|
211
|
-
def get_info(self) ->
|
|
213
|
+
def get_info(self) -> dict[str, Any]:
|
|
212
214
|
"""Get comprehensive file information."""
|
|
213
215
|
return {
|
|
214
216
|
'file_path': str(self.file_path),
|