exonware-xwsystem 0.1.0.1__py3-none-any.whl → 0.1.0.3__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 -1
- exonware/conf.py +2 -2
- exonware/xwsystem/__init__.py +115 -43
- exonware/xwsystem/base.py +30 -0
- exonware/xwsystem/caching/__init__.py +39 -13
- exonware/xwsystem/caching/base.py +24 -6
- exonware/xwsystem/caching/bloom_cache.py +2 -2
- exonware/xwsystem/caching/cache_manager.py +2 -1
- exonware/xwsystem/caching/conditional.py +2 -2
- exonware/xwsystem/caching/contracts.py +85 -139
- exonware/xwsystem/caching/decorators.py +6 -19
- exonware/xwsystem/caching/defs.py +2 -1
- exonware/xwsystem/caching/disk_cache.py +2 -1
- exonware/xwsystem/caching/distributed.py +2 -1
- exonware/xwsystem/caching/errors.py +2 -1
- exonware/xwsystem/caching/events.py +110 -27
- exonware/xwsystem/caching/eviction_strategies.py +2 -2
- exonware/xwsystem/caching/external_caching_python.py +701 -0
- exonware/xwsystem/caching/facade.py +253 -0
- exonware/xwsystem/caching/factory.py +300 -0
- exonware/xwsystem/caching/fluent.py +14 -12
- exonware/xwsystem/caching/integrity.py +21 -6
- exonware/xwsystem/caching/lfu_cache.py +2 -1
- exonware/xwsystem/caching/lfu_optimized.py +18 -6
- exonware/xwsystem/caching/lru_cache.py +7 -4
- exonware/xwsystem/caching/memory_bounded.py +2 -2
- exonware/xwsystem/caching/metrics_exporter.py +2 -2
- exonware/xwsystem/caching/observable_cache.py +2 -2
- exonware/xwsystem/caching/pluggable_cache.py +2 -2
- exonware/xwsystem/caching/rate_limiter.py +2 -2
- exonware/xwsystem/caching/read_through.py +2 -2
- exonware/xwsystem/caching/secure_cache.py +81 -28
- exonware/xwsystem/caching/serializable.py +9 -7
- exonware/xwsystem/caching/stats.py +2 -2
- exonware/xwsystem/caching/tagging.py +2 -2
- exonware/xwsystem/caching/ttl_cache.py +4 -3
- exonware/xwsystem/caching/two_tier_cache.py +6 -3
- exonware/xwsystem/caching/utils.py +30 -12
- exonware/xwsystem/caching/validation.py +2 -2
- exonware/xwsystem/caching/warming.py +6 -3
- exonware/xwsystem/caching/write_behind.py +15 -6
- exonware/xwsystem/config/__init__.py +11 -17
- exonware/xwsystem/config/base.py +5 -5
- exonware/xwsystem/config/contracts.py +93 -153
- exonware/xwsystem/config/defaults.py +3 -2
- exonware/xwsystem/config/defs.py +3 -2
- exonware/xwsystem/config/errors.py +2 -5
- exonware/xwsystem/config/logging.py +12 -8
- exonware/xwsystem/config/logging_setup.py +3 -2
- exonware/xwsystem/config/performance.py +1 -46
- exonware/xwsystem/config/performance_modes.py +9 -8
- exonware/xwsystem/config/version_manager.py +1 -0
- exonware/xwsystem/config.py +27 -0
- exonware/xwsystem/console/__init__.py +53 -0
- exonware/xwsystem/console/base.py +133 -0
- exonware/xwsystem/console/cli/__init__.py +61 -0
- exonware/xwsystem/{cli → console/cli}/args.py +27 -24
- exonware/xwsystem/{cli → console/cli}/base.py +18 -87
- exonware/xwsystem/{cli → console/cli}/colors.py +15 -13
- exonware/xwsystem/console/cli/console.py +98 -0
- exonware/xwsystem/{cli → console/cli}/contracts.py +51 -69
- exonware/xwsystem/console/cli/defs.py +87 -0
- exonware/xwsystem/console/cli/encoding.py +69 -0
- exonware/xwsystem/{cli → console/cli}/errors.py +8 -3
- exonware/xwsystem/console/cli/event_logger.py +166 -0
- exonware/xwsystem/{cli → console/cli}/progress.py +25 -21
- exonware/xwsystem/{cli → console/cli}/prompts.py +3 -2
- exonware/xwsystem/{cli → console/cli}/tables.py +27 -24
- exonware/xwsystem/console/contracts.py +113 -0
- exonware/xwsystem/console/defs.py +154 -0
- exonware/xwsystem/console/errors.py +34 -0
- exonware/xwsystem/console/event_logger.py +385 -0
- exonware/xwsystem/console/writer.py +132 -0
- exonware/xwsystem/contracts.py +28 -0
- exonware/xwsystem/data_structures/__init__.py +23 -0
- exonware/xwsystem/data_structures/trie.py +34 -0
- exonware/xwsystem/data_structures/union_find.py +144 -0
- exonware/xwsystem/defs.py +17 -0
- exonware/xwsystem/errors.py +23 -0
- exonware/xwsystem/facade.py +62 -0
- exonware/xwsystem/http_client/__init__.py +22 -1
- exonware/xwsystem/http_client/advanced_client.py +8 -5
- exonware/xwsystem/http_client/base.py +3 -2
- exonware/xwsystem/http_client/client.py +7 -4
- exonware/xwsystem/http_client/contracts.py +42 -56
- exonware/xwsystem/http_client/defs.py +2 -1
- exonware/xwsystem/http_client/errors.py +2 -1
- exonware/xwsystem/http_client/facade.py +156 -0
- exonware/xwsystem/io/__init__.py +22 -3
- exonware/xwsystem/io/archive/__init__.py +8 -2
- exonware/xwsystem/io/archive/archive.py +1 -1
- exonware/xwsystem/io/archive/archive_files.py +4 -7
- exonware/xwsystem/io/archive/archivers.py +120 -10
- exonware/xwsystem/io/archive/base.py +4 -5
- exonware/xwsystem/io/archive/codec_integration.py +1 -2
- exonware/xwsystem/io/archive/compression.py +1 -2
- exonware/xwsystem/io/archive/facade.py +263 -0
- exonware/xwsystem/io/archive/formats/__init__.py +2 -3
- exonware/xwsystem/io/archive/formats/brotli_format.py +20 -7
- exonware/xwsystem/io/archive/formats/lz4_format.py +20 -7
- exonware/xwsystem/io/archive/formats/rar.py +11 -5
- exonware/xwsystem/io/archive/formats/sevenzip.py +12 -6
- exonware/xwsystem/io/archive/formats/squashfs_format.py +1 -2
- exonware/xwsystem/io/archive/formats/tar.py +52 -7
- exonware/xwsystem/io/archive/formats/wim_format.py +11 -5
- exonware/xwsystem/io/archive/formats/zip.py +1 -2
- exonware/xwsystem/io/archive/formats/zpaq_format.py +1 -2
- exonware/xwsystem/io/archive/formats/zstandard.py +20 -7
- exonware/xwsystem/io/base.py +119 -115
- exonware/xwsystem/io/codec/__init__.py +4 -2
- exonware/xwsystem/io/codec/base.py +19 -13
- exonware/xwsystem/io/codec/contracts.py +59 -2
- exonware/xwsystem/io/codec/registry.py +67 -21
- exonware/xwsystem/io/common/__init__.py +1 -1
- exonware/xwsystem/io/common/atomic.py +29 -16
- exonware/xwsystem/io/common/base.py +11 -10
- exonware/xwsystem/io/common/lock.py +6 -5
- exonware/xwsystem/io/common/path_manager.py +2 -1
- exonware/xwsystem/io/common/watcher.py +1 -2
- exonware/xwsystem/io/contracts.py +301 -433
- exonware/xwsystem/io/contracts_1.py +1180 -0
- exonware/xwsystem/io/data_operations.py +19 -20
- exonware/xwsystem/io/defs.py +4 -3
- exonware/xwsystem/io/errors.py +3 -2
- exonware/xwsystem/io/facade.py +87 -61
- exonware/xwsystem/io/file/__init__.py +1 -1
- exonware/xwsystem/io/file/base.py +8 -9
- exonware/xwsystem/io/file/conversion.py +2 -3
- exonware/xwsystem/io/file/file.py +61 -18
- exonware/xwsystem/io/file/paged_source.py +8 -8
- exonware/xwsystem/io/file/paging/__init__.py +1 -2
- exonware/xwsystem/io/file/paging/byte_paging.py +4 -5
- exonware/xwsystem/io/file/paging/line_paging.py +2 -3
- exonware/xwsystem/io/file/paging/record_paging.py +2 -3
- exonware/xwsystem/io/file/paging/registry.py +1 -2
- exonware/xwsystem/io/file/source.py +13 -17
- exonware/xwsystem/io/filesystem/__init__.py +1 -1
- exonware/xwsystem/io/filesystem/base.py +1 -2
- exonware/xwsystem/io/filesystem/local.py +3 -4
- exonware/xwsystem/io/folder/__init__.py +1 -1
- exonware/xwsystem/io/folder/base.py +1 -2
- exonware/xwsystem/io/folder/folder.py +16 -7
- exonware/xwsystem/io/indexing/__init__.py +14 -0
- exonware/xwsystem/io/indexing/facade.py +443 -0
- exonware/xwsystem/io/path_parser.py +98 -0
- exonware/xwsystem/io/serialization/__init__.py +21 -3
- exonware/xwsystem/io/serialization/auto_serializer.py +146 -20
- exonware/xwsystem/io/serialization/base.py +84 -34
- exonware/xwsystem/io/serialization/contracts.py +50 -73
- exonware/xwsystem/io/serialization/defs.py +2 -1
- exonware/xwsystem/io/serialization/errors.py +2 -1
- exonware/xwsystem/io/serialization/flyweight.py +154 -7
- exonware/xwsystem/io/serialization/format_detector.py +15 -14
- exonware/xwsystem/io/serialization/formats/__init__.py +8 -5
- exonware/xwsystem/io/serialization/formats/binary/bson.py +15 -6
- exonware/xwsystem/io/serialization/formats/binary/cbor.py +5 -5
- exonware/xwsystem/io/serialization/formats/binary/marshal.py +5 -5
- exonware/xwsystem/io/serialization/formats/binary/msgpack.py +5 -5
- exonware/xwsystem/io/serialization/formats/binary/pickle.py +5 -5
- exonware/xwsystem/io/serialization/formats/binary/plistlib.py +5 -5
- exonware/xwsystem/io/serialization/formats/database/dbm.py +7 -7
- exonware/xwsystem/io/serialization/formats/database/shelve.py +7 -7
- exonware/xwsystem/io/serialization/formats/database/sqlite3.py +7 -7
- exonware/xwsystem/io/serialization/formats/tabular/__init__.py +27 -0
- exonware/xwsystem/io/serialization/formats/tabular/base.py +89 -0
- exonware/xwsystem/io/serialization/formats/tabular/csv.py +319 -0
- exonware/xwsystem/io/serialization/formats/tabular/df.py +249 -0
- exonware/xwsystem/io/serialization/formats/tabular/excel.py +291 -0
- exonware/xwsystem/io/serialization/formats/tabular/googlesheets.py +374 -0
- exonware/xwsystem/io/serialization/formats/text/__init__.py +1 -1
- exonware/xwsystem/io/serialization/formats/text/append_only_log.py +5 -7
- exonware/xwsystem/io/serialization/formats/text/configparser.py +5 -5
- exonware/xwsystem/io/serialization/formats/text/csv.py +7 -5
- exonware/xwsystem/io/serialization/formats/text/formdata.py +5 -5
- exonware/xwsystem/io/serialization/formats/text/json.py +27 -18
- exonware/xwsystem/io/serialization/formats/text/json5.py +8 -4
- exonware/xwsystem/io/serialization/formats/text/jsonlines.py +18 -14
- exonware/xwsystem/io/serialization/formats/text/multipart.py +5 -5
- exonware/xwsystem/io/serialization/formats/text/toml.py +8 -6
- exonware/xwsystem/io/serialization/formats/text/xml.py +25 -20
- exonware/xwsystem/io/serialization/formats/text/yaml.py +8 -6
- exonware/xwsystem/io/serialization/parsers/__init__.py +3 -2
- exonware/xwsystem/io/serialization/parsers/base.py +6 -5
- exonware/xwsystem/io/serialization/parsers/hybrid_parser.py +7 -6
- exonware/xwsystem/io/serialization/parsers/msgspec_parser.py +10 -7
- exonware/xwsystem/io/serialization/parsers/orjson_direct_parser.py +7 -6
- exonware/xwsystem/io/serialization/parsers/orjson_parser.py +11 -8
- exonware/xwsystem/io/serialization/parsers/pysimdjson_parser.py +13 -9
- exonware/xwsystem/io/serialization/parsers/rapidjson_parser.py +10 -7
- exonware/xwsystem/io/serialization/parsers/registry.py +11 -10
- exonware/xwsystem/io/serialization/parsers/standard.py +7 -6
- exonware/xwsystem/io/serialization/parsers/ujson_parser.py +10 -7
- exonware/xwsystem/io/serialization/registry.py +4 -4
- exonware/xwsystem/io/serialization/serializer.py +168 -79
- exonware/xwsystem/io/serialization/universal_options.py +367 -0
- exonware/xwsystem/io/serialization/utils/__init__.py +1 -2
- exonware/xwsystem/io/serialization/utils/path_ops.py +5 -6
- exonware/xwsystem/io/source_reader.py +223 -0
- exonware/xwsystem/io/stream/__init__.py +1 -1
- exonware/xwsystem/io/stream/async_operations.py +61 -14
- exonware/xwsystem/io/stream/base.py +1 -2
- exonware/xwsystem/io/stream/codec_io.py +6 -7
- exonware/xwsystem/ipc/__init__.py +1 -0
- exonware/xwsystem/ipc/async_fabric.py +4 -4
- exonware/xwsystem/ipc/base.py +6 -5
- exonware/xwsystem/ipc/contracts.py +41 -66
- exonware/xwsystem/ipc/defs.py +2 -1
- exonware/xwsystem/ipc/errors.py +2 -1
- exonware/xwsystem/ipc/message_queue.py +5 -2
- exonware/xwsystem/ipc/pipes.py +70 -34
- exonware/xwsystem/ipc/process_manager.py +7 -5
- exonware/xwsystem/ipc/process_pool.py +6 -5
- exonware/xwsystem/ipc/shared_memory.py +64 -11
- exonware/xwsystem/monitoring/__init__.py +7 -0
- exonware/xwsystem/monitoring/base.py +11 -8
- exonware/xwsystem/monitoring/contracts.py +86 -144
- exonware/xwsystem/monitoring/defs.py +2 -1
- exonware/xwsystem/monitoring/error_recovery.py +16 -3
- exonware/xwsystem/monitoring/errors.py +2 -1
- exonware/xwsystem/monitoring/facade.py +183 -0
- exonware/xwsystem/monitoring/memory_monitor.py +1 -0
- exonware/xwsystem/monitoring/metrics.py +1 -0
- exonware/xwsystem/monitoring/performance_manager_generic.py +7 -7
- exonware/xwsystem/monitoring/performance_monitor.py +1 -0
- exonware/xwsystem/monitoring/performance_validator.py +1 -0
- exonware/xwsystem/monitoring/system_monitor.py +6 -5
- exonware/xwsystem/monitoring/tracing.py +18 -16
- exonware/xwsystem/monitoring/tracker.py +2 -1
- exonware/xwsystem/operations/__init__.py +5 -50
- exonware/xwsystem/operations/base.py +3 -44
- exonware/xwsystem/operations/contracts.py +25 -15
- exonware/xwsystem/operations/defs.py +1 -1
- exonware/xwsystem/operations/diff.py +5 -4
- exonware/xwsystem/operations/errors.py +1 -1
- exonware/xwsystem/operations/merge.py +6 -4
- exonware/xwsystem/operations/patch.py +5 -4
- exonware/xwsystem/patterns/__init__.py +1 -0
- exonware/xwsystem/patterns/base.py +2 -1
- exonware/xwsystem/patterns/context_manager.py +2 -1
- exonware/xwsystem/patterns/contracts.py +215 -256
- exonware/xwsystem/patterns/defs.py +2 -1
- exonware/xwsystem/patterns/dynamic_facade.py +1 -0
- exonware/xwsystem/patterns/errors.py +2 -4
- exonware/xwsystem/patterns/handler_factory.py +2 -3
- exonware/xwsystem/patterns/import_registry.py +1 -0
- exonware/xwsystem/patterns/object_pool.py +1 -0
- exonware/xwsystem/patterns/registry.py +4 -43
- exonware/xwsystem/plugins/__init__.py +2 -1
- exonware/xwsystem/plugins/base.py +6 -5
- exonware/xwsystem/plugins/contracts.py +94 -158
- exonware/xwsystem/plugins/defs.py +2 -1
- exonware/xwsystem/plugins/errors.py +2 -1
- exonware/xwsystem/py.typed +3 -0
- exonware/xwsystem/query/__init__.py +36 -0
- exonware/xwsystem/query/contracts.py +56 -0
- exonware/xwsystem/query/errors.py +22 -0
- exonware/xwsystem/query/registry.py +128 -0
- exonware/xwsystem/runtime/__init__.py +2 -1
- exonware/xwsystem/runtime/base.py +4 -3
- exonware/xwsystem/runtime/contracts.py +39 -60
- exonware/xwsystem/runtime/defs.py +2 -1
- exonware/xwsystem/runtime/env.py +11 -9
- exonware/xwsystem/runtime/errors.py +2 -1
- exonware/xwsystem/runtime/reflection.py +3 -2
- exonware/xwsystem/security/__init__.py +68 -11
- exonware/xwsystem/security/audit.py +167 -0
- exonware/xwsystem/security/base.py +121 -24
- exonware/xwsystem/security/contracts.py +91 -146
- exonware/xwsystem/security/crypto.py +17 -16
- exonware/xwsystem/security/defs.py +2 -1
- exonware/xwsystem/security/errors.py +2 -1
- exonware/xwsystem/security/facade.py +321 -0
- exonware/xwsystem/security/file_security.py +330 -0
- exonware/xwsystem/security/hazmat.py +11 -8
- exonware/xwsystem/security/monitor.py +372 -0
- exonware/xwsystem/security/path_validator.py +140 -18
- exonware/xwsystem/security/policy.py +357 -0
- exonware/xwsystem/security/resource_limits.py +1 -0
- exonware/xwsystem/security/validator.py +455 -0
- exonware/xwsystem/shared/__init__.py +14 -1
- exonware/xwsystem/shared/base.py +285 -2
- exonware/xwsystem/shared/contracts.py +415 -126
- exonware/xwsystem/shared/defs.py +2 -1
- exonware/xwsystem/shared/errors.py +2 -2
- exonware/xwsystem/shared/xwobject.py +316 -0
- exonware/xwsystem/structures/__init__.py +1 -0
- exonware/xwsystem/structures/base.py +3 -2
- exonware/xwsystem/structures/circular_detector.py +15 -14
- exonware/xwsystem/structures/contracts.py +53 -76
- exonware/xwsystem/structures/defs.py +2 -1
- exonware/xwsystem/structures/errors.py +2 -1
- exonware/xwsystem/structures/tree_walker.py +2 -1
- exonware/xwsystem/threading/__init__.py +21 -4
- exonware/xwsystem/threading/async_primitives.py +6 -5
- exonware/xwsystem/threading/base.py +3 -2
- exonware/xwsystem/threading/contracts.py +87 -143
- exonware/xwsystem/threading/defs.py +2 -1
- exonware/xwsystem/threading/errors.py +2 -1
- exonware/xwsystem/threading/facade.py +175 -0
- exonware/xwsystem/threading/locks.py +1 -0
- exonware/xwsystem/threading/safe_factory.py +1 -0
- exonware/xwsystem/utils/__init__.py +40 -0
- exonware/xwsystem/utils/base.py +22 -21
- exonware/xwsystem/utils/contracts.py +50 -73
- exonware/xwsystem/utils/dt/__init__.py +19 -3
- exonware/xwsystem/utils/dt/base.py +5 -4
- exonware/xwsystem/utils/dt/contracts.py +22 -29
- exonware/xwsystem/utils/dt/defs.py +2 -1
- exonware/xwsystem/utils/dt/errors.py +2 -5
- exonware/xwsystem/utils/dt/formatting.py +88 -2
- exonware/xwsystem/utils/dt/humanize.py +10 -9
- exonware/xwsystem/utils/dt/parsing.py +56 -5
- exonware/xwsystem/utils/dt/timezone_utils.py +2 -24
- exonware/xwsystem/utils/errors.py +2 -4
- exonware/xwsystem/utils/paths.py +1 -0
- exonware/xwsystem/utils/string.py +49 -0
- exonware/xwsystem/utils/test_runner.py +185 -0
- exonware/xwsystem/utils/utils_contracts.py +2 -1
- exonware/xwsystem/utils/web.py +110 -0
- exonware/xwsystem/validation/__init__.py +25 -1
- exonware/xwsystem/validation/base.py +6 -5
- exonware/xwsystem/validation/contracts.py +29 -41
- exonware/xwsystem/validation/data_validator.py +1 -0
- exonware/xwsystem/validation/declarative.py +11 -8
- exonware/xwsystem/validation/defs.py +2 -1
- exonware/xwsystem/validation/errors.py +2 -1
- exonware/xwsystem/validation/facade.py +198 -0
- exonware/xwsystem/validation/fluent_validator.py +22 -19
- exonware/xwsystem/validation/schema_discovery.py +210 -0
- exonware/xwsystem/validation/type_safety.py +2 -1
- exonware/xwsystem/version.py +2 -2
- {exonware_xwsystem-0.1.0.1.dist-info → exonware_xwsystem-0.1.0.3.dist-info}/METADATA +71 -4
- exonware_xwsystem-0.1.0.3.dist-info/RECORD +337 -0
- exonware/xwsystem/cli/__init__.py +0 -43
- exonware/xwsystem/cli/console.py +0 -113
- exonware/xwsystem/cli/defs.py +0 -134
- exonware/xwsystem/conf.py +0 -44
- exonware/xwsystem/security/auth.py +0 -484
- exonware_xwsystem-0.1.0.1.dist-info/RECORD +0 -284
- {exonware_xwsystem-0.1.0.1.dist-info → exonware_xwsystem-0.1.0.3.dist-info}/WHEEL +0 -0
- {exonware_xwsystem-0.1.0.1.dist-info → exonware_xwsystem-0.1.0.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
#exonware/xwsystem/src/exonware/xwsystem/io/serialization/formats/tabular/excel.py
|
|
2
|
+
"""
|
|
3
|
+
Company: eXonware.com
|
|
4
|
+
Author: Eng. Muhammad AlShehri
|
|
5
|
+
Email: connect@exonware.com
|
|
6
|
+
Version: 0.1.0.3
|
|
7
|
+
Generation Date: January 2025
|
|
8
|
+
|
|
9
|
+
Excel serialization - .xlsx, .xls format.
|
|
10
|
+
|
|
11
|
+
Following I→A→XW pattern:
|
|
12
|
+
- I: ISerialization (interface)
|
|
13
|
+
- A: ASerialization (abstract base)
|
|
14
|
+
- ATabular: ATabularSerialization (tabular base)
|
|
15
|
+
- Concrete: ExcelSerializer
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import io
|
|
19
|
+
from typing import Any, Optional
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
import pandas as pd
|
|
23
|
+
|
|
24
|
+
from .base import ATabularSerialization
|
|
25
|
+
from ....contracts import EncodeOptions, DecodeOptions
|
|
26
|
+
from ....defs import CodecCapability
|
|
27
|
+
from ....errors import SerializationError
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ExcelSerializer(ATabularSerialization):
|
|
31
|
+
"""
|
|
32
|
+
Excel serializer - follows the I→A→ATabular pattern.
|
|
33
|
+
|
|
34
|
+
I: ISerialization (interface)
|
|
35
|
+
A: ASerialization (abstract base)
|
|
36
|
+
ATabular: ATabularSerialization (tabular base)
|
|
37
|
+
Concrete: ExcelSerializer
|
|
38
|
+
|
|
39
|
+
Supports .xlsx and .xls formats via pandas/openpyxl/xlrd.
|
|
40
|
+
|
|
41
|
+
Examples:
|
|
42
|
+
>>> serializer = ExcelSerializer()
|
|
43
|
+
>>>
|
|
44
|
+
>>> # Convert Excel bytes to DataFrame
|
|
45
|
+
>>> df = serializer.to_df(excel_bytes)
|
|
46
|
+
>>>
|
|
47
|
+
>>> # Convert DataFrame to Excel bytes
|
|
48
|
+
>>> excel_bytes = serializer.from_df(df)
|
|
49
|
+
>>>
|
|
50
|
+
>>> # Save to file
|
|
51
|
+
>>> serializer.save_file(df, "data.xlsx")
|
|
52
|
+
>>>
|
|
53
|
+
>>> # Load from file
|
|
54
|
+
>>> df = serializer.load_file("data.xlsx")
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
# ========================================================================
|
|
58
|
+
# CODEC METADATA
|
|
59
|
+
# ========================================================================
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def codec_id(self) -> str:
|
|
63
|
+
return "excel"
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def media_types(self) -> list[str]:
|
|
67
|
+
return [
|
|
68
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", # .xlsx
|
|
69
|
+
"application/vnd.ms-excel", # .xls
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def file_extensions(self) -> list[str]:
|
|
74
|
+
return [".xlsx", ".xls"]
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def format_name(self) -> str:
|
|
78
|
+
return "Excel"
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def mime_type(self) -> str:
|
|
82
|
+
return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def is_binary_format(self) -> bool:
|
|
86
|
+
return True # Excel is binary
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def supports_streaming(self) -> bool:
|
|
90
|
+
return False # Excel doesn't naturally support streaming
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def capabilities(self) -> CodecCapability:
|
|
94
|
+
return CodecCapability.BIDIRECTIONAL
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def aliases(self) -> list[str]:
|
|
98
|
+
return ["excel", "Excel", "xlsx", "XLSX", "xls", "XLS"]
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def codec_types(self) -> list[str]:
|
|
102
|
+
"""Excel is a data exchange format."""
|
|
103
|
+
return ["data", "tabular"]
|
|
104
|
+
|
|
105
|
+
# ========================================================================
|
|
106
|
+
# CORE ENCODE/DECODE (Using pandas)
|
|
107
|
+
# ========================================================================
|
|
108
|
+
|
|
109
|
+
def encode(self, value: Any, *, options: Optional[EncodeOptions] = None) -> bytes:
|
|
110
|
+
"""
|
|
111
|
+
Encode data to Excel bytes.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
value: DataFrame or dict of {sheet_name: DataFrame}
|
|
115
|
+
options: Excel options (engine, sheet_name, etc.)
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Excel file as bytes
|
|
119
|
+
|
|
120
|
+
Raises:
|
|
121
|
+
SerializationError: If encoding fails
|
|
122
|
+
"""
|
|
123
|
+
try:
|
|
124
|
+
# If value is already a DataFrame or dict, use from_df
|
|
125
|
+
if isinstance(value, (pd.DataFrame, dict)):
|
|
126
|
+
return self.from_df(value, **(options or {}))
|
|
127
|
+
|
|
128
|
+
# Otherwise, try to serialize as-is (fallback)
|
|
129
|
+
raise SerializationError(
|
|
130
|
+
f"Excel encoding requires DataFrame or dict of DataFrames, got {type(value)}",
|
|
131
|
+
format_name=self.format_name
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
except Exception as e:
|
|
135
|
+
raise SerializationError(
|
|
136
|
+
f"Failed to encode Excel: {e}",
|
|
137
|
+
format_name=self.format_name,
|
|
138
|
+
original_error=e
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def decode(self, repr: bytes | str, *, options: Optional[DecodeOptions] = None) -> Any:
|
|
142
|
+
"""
|
|
143
|
+
Decode Excel bytes to DataFrame(s).
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
repr: Excel file as bytes
|
|
147
|
+
options: Excel options (sheet_name, engine, etc.)
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
DataFrame or dict of {sheet_name: DataFrame}
|
|
151
|
+
|
|
152
|
+
Raises:
|
|
153
|
+
SerializationError: If decoding fails
|
|
154
|
+
"""
|
|
155
|
+
try:
|
|
156
|
+
if isinstance(repr, str):
|
|
157
|
+
# If string, treat as file path
|
|
158
|
+
return self.load_file(repr, **(options or {}))
|
|
159
|
+
|
|
160
|
+
# Use to_df for bytes
|
|
161
|
+
return self.to_df(repr, **(options or {}))
|
|
162
|
+
|
|
163
|
+
except Exception as e:
|
|
164
|
+
raise SerializationError(
|
|
165
|
+
f"Failed to decode Excel: {e}",
|
|
166
|
+
format_name=self.format_name,
|
|
167
|
+
original_error=e
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# ========================================================================
|
|
171
|
+
# TABULAR METHODS (to_df/from_df)
|
|
172
|
+
# ========================================================================
|
|
173
|
+
|
|
174
|
+
def to_df(
|
|
175
|
+
self,
|
|
176
|
+
excel_content: bytes | str | Path,
|
|
177
|
+
sheet_name: Optional[str | list[str]] = None,
|
|
178
|
+
**options
|
|
179
|
+
) -> pd.DataFrame | dict[str, pd.DataFrame]:
|
|
180
|
+
"""
|
|
181
|
+
Convert Excel content to DataFrame(s).
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
excel_content: Excel file as bytes, file path, or Path object
|
|
185
|
+
sheet_name: Specific sheet name(s) to load, or None for all sheets
|
|
186
|
+
**options: Additional pandas read_excel options (engine, header, etc.)
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Single DataFrame if one sheet, or dict of {sheet_name: DataFrame} if multiple
|
|
190
|
+
|
|
191
|
+
Examples:
|
|
192
|
+
>>> serializer = ExcelSerializer()
|
|
193
|
+
>>> df = serializer.to_df(excel_bytes) # Single sheet
|
|
194
|
+
>>> sheets = serializer.to_df(excel_bytes) # All sheets (dict)
|
|
195
|
+
>>> df = serializer.to_df(excel_bytes, sheet_name="Sheet1") # Specific sheet
|
|
196
|
+
"""
|
|
197
|
+
try:
|
|
198
|
+
# Handle file path
|
|
199
|
+
if isinstance(excel_content, (str, Path)):
|
|
200
|
+
file_path = str(excel_content)
|
|
201
|
+
if sheet_name is None:
|
|
202
|
+
# Read all sheets
|
|
203
|
+
return pd.read_excel(file_path, sheet_name=None, **options)
|
|
204
|
+
else:
|
|
205
|
+
# Read specific sheet(s)
|
|
206
|
+
return pd.read_excel(file_path, sheet_name=sheet_name, **options)
|
|
207
|
+
|
|
208
|
+
# Handle bytes
|
|
209
|
+
if not excel_content:
|
|
210
|
+
raise ValueError("Excel content cannot be empty")
|
|
211
|
+
|
|
212
|
+
excel_io = io.BytesIO(excel_content)
|
|
213
|
+
|
|
214
|
+
# Determine engine based on options or file extension
|
|
215
|
+
engine = options.get('engine', 'openpyxl') # Default to openpyxl for .xlsx
|
|
216
|
+
|
|
217
|
+
if sheet_name is None:
|
|
218
|
+
# Read all sheets
|
|
219
|
+
return pd.read_excel(excel_io, sheet_name=None, engine=engine, **{k: v for k, v in options.items() if k != 'engine'})
|
|
220
|
+
else:
|
|
221
|
+
# Read specific sheet(s)
|
|
222
|
+
result = pd.read_excel(excel_io, sheet_name=sheet_name, engine=engine, **{k: v for k, v in options.items() if k != 'engine'})
|
|
223
|
+
|
|
224
|
+
# If single sheet name, return DataFrame directly
|
|
225
|
+
if isinstance(sheet_name, str):
|
|
226
|
+
return result
|
|
227
|
+
# If list of sheet names, result is already a dict
|
|
228
|
+
return result
|
|
229
|
+
|
|
230
|
+
except Exception as e:
|
|
231
|
+
raise SerializationError(
|
|
232
|
+
f"Failed to convert Excel to DataFrame: {e}",
|
|
233
|
+
format_name=self.format_name,
|
|
234
|
+
original_error=e
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
def from_df(
|
|
238
|
+
self,
|
|
239
|
+
df: pd.DataFrame | dict[str, pd.DataFrame],
|
|
240
|
+
**options
|
|
241
|
+
) -> bytes:
|
|
242
|
+
"""
|
|
243
|
+
Convert DataFrame(s) to Excel bytes.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
df: Single DataFrame or dict of {sheet_name: DataFrame}
|
|
247
|
+
**options: Additional pandas to_excel options (engine, index, etc.)
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
Excel file as bytes
|
|
251
|
+
|
|
252
|
+
Examples:
|
|
253
|
+
>>> serializer = ExcelSerializer()
|
|
254
|
+
>>> excel_bytes = serializer.from_df(df) # Single sheet
|
|
255
|
+
>>> excel_bytes = serializer.from_df({"Sheet1": df1, "Sheet2": df2}) # Multiple sheets
|
|
256
|
+
"""
|
|
257
|
+
try:
|
|
258
|
+
excel_io = io.BytesIO()
|
|
259
|
+
|
|
260
|
+
# Determine engine
|
|
261
|
+
engine = options.get('engine', 'openpyxl') # Default to openpyxl
|
|
262
|
+
|
|
263
|
+
if isinstance(df, dict):
|
|
264
|
+
# Multiple sheets
|
|
265
|
+
with pd.ExcelWriter(excel_io, engine=engine) as writer:
|
|
266
|
+
for sheet_name, sheet_df in df.items():
|
|
267
|
+
sheet_df.to_excel(
|
|
268
|
+
writer,
|
|
269
|
+
sheet_name=sheet_name,
|
|
270
|
+
index=options.get('index', False),
|
|
271
|
+
**{k: v for k, v in options.items() if k not in ('engine', 'index')}
|
|
272
|
+
)
|
|
273
|
+
else:
|
|
274
|
+
# Single sheet
|
|
275
|
+
df.to_excel(
|
|
276
|
+
excel_io,
|
|
277
|
+
engine=engine,
|
|
278
|
+
index=options.get('index', False),
|
|
279
|
+
sheet_name=options.get('sheet_name', 'Sheet1'),
|
|
280
|
+
**{k: v for k, v in options.items() if k not in ('engine', 'index', 'sheet_name')}
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
excel_io.seek(0)
|
|
284
|
+
return excel_io.read()
|
|
285
|
+
|
|
286
|
+
except Exception as e:
|
|
287
|
+
raise SerializationError(
|
|
288
|
+
f"Failed to convert DataFrame to Excel: {e}",
|
|
289
|
+
format_name=self.format_name,
|
|
290
|
+
original_error=e
|
|
291
|
+
)
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
#exonware/xwsystem/src/exonware/xwsystem/io/serialization/formats/tabular/googlesheets.py
|
|
2
|
+
"""
|
|
3
|
+
Company: eXonware.com
|
|
4
|
+
Author: Eng. Muhammad AlShehri
|
|
5
|
+
Email: connect@exonware.com
|
|
6
|
+
Version: 0.1.0.3
|
|
7
|
+
Generation Date: January 2025
|
|
8
|
+
|
|
9
|
+
Google Sheets serialization.
|
|
10
|
+
|
|
11
|
+
Following I→A→ATabular pattern:
|
|
12
|
+
- I: ISerialization (interface)
|
|
13
|
+
- A: ASerialization (abstract base)
|
|
14
|
+
- ATabular: ATabularSerialization (tabular base)
|
|
15
|
+
- Concrete: GoogleSheetsSerializer
|
|
16
|
+
|
|
17
|
+
Note: For local saving, convert to Excel or CSV first.
|
|
18
|
+
For cloud saving, use xwstorage (avoids circular dependencies).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from typing import Any, Optional
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
import pandas as pd
|
|
25
|
+
|
|
26
|
+
from .base import ATabularSerialization
|
|
27
|
+
from ....contracts import EncodeOptions, DecodeOptions
|
|
28
|
+
from ....defs import CodecCapability
|
|
29
|
+
from ....errors import SerializationError
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class GoogleSheetsSerializer(ATabularSerialization):
|
|
33
|
+
"""
|
|
34
|
+
Google Sheets serializer - follows the I→A→ATabular pattern.
|
|
35
|
+
|
|
36
|
+
I: ISerialization (interface)
|
|
37
|
+
A: ASerialization (abstract base)
|
|
38
|
+
ATabular: ATabularSerialization (tabular base)
|
|
39
|
+
Concrete: GoogleSheetsSerializer
|
|
40
|
+
|
|
41
|
+
Uses gspread library for Google Sheets API access.
|
|
42
|
+
|
|
43
|
+
Local saving: Convert to Excel or CSV first, then save.
|
|
44
|
+
Cloud saving: Use xwstorage (avoids circular dependencies).
|
|
45
|
+
|
|
46
|
+
Examples:
|
|
47
|
+
>>> serializer = GoogleSheetsSerializer(credentials_path="path/to/credentials.json")
|
|
48
|
+
>>>
|
|
49
|
+
>>> # Read from Google Sheets
|
|
50
|
+
>>> df = serializer.to_df(spreadsheet_url, sheet_name="Sheet1")
|
|
51
|
+
>>> sheets = serializer.to_df(spreadsheet_url) # All sheets
|
|
52
|
+
>>>
|
|
53
|
+
>>> # Write to Google Sheets
|
|
54
|
+
>>> serializer.from_df(df, spreadsheet_url, sheet_name="Sheet1")
|
|
55
|
+
>>> serializer.from_df({"Sheet1": df1, "Sheet2": df2}, spreadsheet_url) # Multiple sheets
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
credentials_path: Optional[str | Path] = None,
|
|
61
|
+
max_depth: Optional[int] = None,
|
|
62
|
+
max_size_mb: Optional[float] = None
|
|
63
|
+
):
|
|
64
|
+
"""
|
|
65
|
+
Initialize Google Sheets serializer.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
credentials_path: Path to Google service account credentials JSON file
|
|
69
|
+
max_depth: Maximum nesting depth (inherited from ASerialization)
|
|
70
|
+
max_size_mb: Maximum size in MB (inherited from ASerialization)
|
|
71
|
+
"""
|
|
72
|
+
super().__init__(max_depth=max_depth, max_size_mb=max_size_mb)
|
|
73
|
+
self._credentials_path = str(credentials_path) if credentials_path else None
|
|
74
|
+
self._client = None
|
|
75
|
+
|
|
76
|
+
def _get_client(self):
|
|
77
|
+
"""Get or create gspread client (lazy initialization)."""
|
|
78
|
+
if self._client is None:
|
|
79
|
+
import importlib.util
|
|
80
|
+
_gspread_spec = importlib.util.find_spec('gspread')
|
|
81
|
+
_oauth2client_spec = importlib.util.find_spec('oauth2client')
|
|
82
|
+
|
|
83
|
+
if _gspread_spec is None or _oauth2client_spec is None:
|
|
84
|
+
raise ImportError(
|
|
85
|
+
"gspread and oauth2client are required for Google Sheets support. "
|
|
86
|
+
"Install with: pip install gspread oauth2client"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
import gspread
|
|
90
|
+
from oauth2client.service_account import ServiceAccountCredentials
|
|
91
|
+
|
|
92
|
+
if not self._credentials_path:
|
|
93
|
+
raise ValueError("credentials_path is required for Google Sheets access")
|
|
94
|
+
|
|
95
|
+
scope = 'https://www.googleapis.com/auth/spreadsheets'
|
|
96
|
+
creds = ServiceAccountCredentials.from_json_keyfile_name(
|
|
97
|
+
self._credentials_path, scope
|
|
98
|
+
)
|
|
99
|
+
self._client = gspread.authorize(creds)
|
|
100
|
+
|
|
101
|
+
return self._client
|
|
102
|
+
|
|
103
|
+
# ========================================================================
|
|
104
|
+
# CODEC METADATA
|
|
105
|
+
# ========================================================================
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def codec_id(self) -> str:
|
|
109
|
+
return "googlesheets"
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def media_types(self) -> list[str]:
|
|
113
|
+
return ["application/vnd.google-apps.spreadsheet"]
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def file_extensions(self) -> list[str]:
|
|
117
|
+
return [] # Google Sheets don't have local file extensions
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def format_name(self) -> str:
|
|
121
|
+
return "Google Sheets"
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def mime_type(self) -> str:
|
|
125
|
+
return "application/vnd.google-apps.spreadsheet"
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def is_binary_format(self) -> bool:
|
|
129
|
+
return False # Google Sheets API uses JSON
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def supports_streaming(self) -> bool:
|
|
133
|
+
return False # Google Sheets API doesn't support streaming
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def capabilities(self) -> CodecCapability:
|
|
137
|
+
return CodecCapability.BIDIRECTIONAL
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def aliases(self) -> list[str]:
|
|
141
|
+
return ["googlesheets", "GoogleSheets", "gsheet", "gsheets"]
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def codec_types(self) -> list[str]:
|
|
145
|
+
"""Google Sheets is a cloud data format."""
|
|
146
|
+
return ["data", "tabular", "cloud"]
|
|
147
|
+
|
|
148
|
+
# ========================================================================
|
|
149
|
+
# CORE ENCODE/DECODE
|
|
150
|
+
# ========================================================================
|
|
151
|
+
|
|
152
|
+
def encode(self, value: Any, *, options: Optional[EncodeOptions] = None) -> str:
|
|
153
|
+
"""
|
|
154
|
+
Encode data to Google Sheets format.
|
|
155
|
+
|
|
156
|
+
Note: This is a placeholder. Use from_df() with spreadsheet_url instead.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
value: DataFrame or dict of DataFrames
|
|
160
|
+
options: Options including spreadsheet_url and sheet_name
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Spreadsheet URL (as string representation)
|
|
164
|
+
|
|
165
|
+
Raises:
|
|
166
|
+
SerializationError: If encoding fails
|
|
167
|
+
"""
|
|
168
|
+
try:
|
|
169
|
+
opts = options or {}
|
|
170
|
+
spreadsheet_url = opts.get('spreadsheet_url')
|
|
171
|
+
if not spreadsheet_url:
|
|
172
|
+
raise ValueError("spreadsheet_url is required in options")
|
|
173
|
+
|
|
174
|
+
# Use from_df for actual encoding
|
|
175
|
+
if isinstance(value, (pd.DataFrame, dict)):
|
|
176
|
+
self.from_df(value, spreadsheet_url=spreadsheet_url, **(opts or {}))
|
|
177
|
+
return spreadsheet_url
|
|
178
|
+
|
|
179
|
+
raise ValueError(f"Google Sheets encoding requires DataFrame or dict, got {type(value)}")
|
|
180
|
+
|
|
181
|
+
except Exception as e:
|
|
182
|
+
raise SerializationError(
|
|
183
|
+
f"Failed to encode Google Sheets: {e}",
|
|
184
|
+
format_name=self.format_name,
|
|
185
|
+
original_error=e
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
def decode(self, repr: bytes | str, *, options: Optional[DecodeOptions] = None) -> Any:
|
|
189
|
+
"""
|
|
190
|
+
Decode Google Sheets to DataFrame(s).
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
repr: Spreadsheet URL (as string)
|
|
194
|
+
options: Options including sheet_name
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
DataFrame or dict of {sheet_name: DataFrame}
|
|
198
|
+
|
|
199
|
+
Raises:
|
|
200
|
+
SerializationError: If decoding fails
|
|
201
|
+
"""
|
|
202
|
+
try:
|
|
203
|
+
if isinstance(repr, bytes):
|
|
204
|
+
repr = repr.decode('utf-8')
|
|
205
|
+
|
|
206
|
+
opts = options or {}
|
|
207
|
+
sheet_name = opts.get('sheet_name')
|
|
208
|
+
|
|
209
|
+
return self.to_df(repr, sheet_name=sheet_name, **(opts or {}))
|
|
210
|
+
|
|
211
|
+
except Exception as e:
|
|
212
|
+
raise SerializationError(
|
|
213
|
+
f"Failed to decode Google Sheets: {e}",
|
|
214
|
+
format_name=self.format_name,
|
|
215
|
+
original_error=e
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# ========================================================================
|
|
219
|
+
# TABULAR METHODS (to_df/from_df)
|
|
220
|
+
# ========================================================================
|
|
221
|
+
|
|
222
|
+
def to_df(
|
|
223
|
+
self,
|
|
224
|
+
spreadsheet_url: str,
|
|
225
|
+
sheet_name: Optional[str | list[str]] = None,
|
|
226
|
+
header: int = 0,
|
|
227
|
+
**options
|
|
228
|
+
) -> pd.DataFrame | dict[str, pd.DataFrame]:
|
|
229
|
+
"""
|
|
230
|
+
Convert Google Sheet to DataFrame(s).
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
spreadsheet_url: Google Sheets URL
|
|
234
|
+
sheet_name: Specific sheet name(s) to load, or None for all sheets
|
|
235
|
+
header: Row index to use as header (default: 0)
|
|
236
|
+
**options: Additional options
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
Single DataFrame if one sheet, or dict of {sheet_name: DataFrame} if multiple
|
|
240
|
+
|
|
241
|
+
Examples:
|
|
242
|
+
>>> serializer = GoogleSheetsSerializer(credentials_path="creds.json")
|
|
243
|
+
>>> df = serializer.to_df(spreadsheet_url, sheet_name="Sheet1")
|
|
244
|
+
>>> sheets = serializer.to_df(spreadsheet_url) # All sheets
|
|
245
|
+
"""
|
|
246
|
+
try:
|
|
247
|
+
from gspread_dataframe import get_as_dataframe
|
|
248
|
+
|
|
249
|
+
client = self._get_client()
|
|
250
|
+
spreadsheet = client.open_by_url(spreadsheet_url)
|
|
251
|
+
|
|
252
|
+
if sheet_name is None:
|
|
253
|
+
# Read all sheets
|
|
254
|
+
result = {}
|
|
255
|
+
for worksheet in spreadsheet.worksheets():
|
|
256
|
+
try:
|
|
257
|
+
df = get_as_dataframe(worksheet, header=header, **options)
|
|
258
|
+
result[worksheet.title] = df
|
|
259
|
+
except Exception as e:
|
|
260
|
+
# Skip sheets that can't be read
|
|
261
|
+
continue
|
|
262
|
+
return result
|
|
263
|
+
|
|
264
|
+
elif isinstance(sheet_name, str):
|
|
265
|
+
# Read single sheet
|
|
266
|
+
try:
|
|
267
|
+
worksheet = spreadsheet.worksheet(sheet_name)
|
|
268
|
+
return get_as_dataframe(worksheet, header=header, **options)
|
|
269
|
+
except Exception as e:
|
|
270
|
+
raise SerializationError(
|
|
271
|
+
f"Failed to read sheet '{sheet_name}': {e}",
|
|
272
|
+
format_name=self.format_name,
|
|
273
|
+
original_error=e
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
else:
|
|
277
|
+
# Read multiple specific sheets
|
|
278
|
+
result = {}
|
|
279
|
+
for name in sheet_name:
|
|
280
|
+
try:
|
|
281
|
+
worksheet = spreadsheet.worksheet(name)
|
|
282
|
+
df = get_as_dataframe(worksheet, header=header, **options)
|
|
283
|
+
result[name] = df
|
|
284
|
+
except Exception as e:
|
|
285
|
+
# Skip sheets that can't be read
|
|
286
|
+
continue
|
|
287
|
+
return result
|
|
288
|
+
|
|
289
|
+
except Exception as e:
|
|
290
|
+
raise SerializationError(
|
|
291
|
+
f"Failed to convert Google Sheet to DataFrame: {e}",
|
|
292
|
+
format_name=self.format_name,
|
|
293
|
+
original_error=e
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
def from_df(
|
|
297
|
+
self,
|
|
298
|
+
df: pd.DataFrame | dict[str, pd.DataFrame],
|
|
299
|
+
spreadsheet_url: str,
|
|
300
|
+
sheet_name: Optional[str] = None,
|
|
301
|
+
**options
|
|
302
|
+
) -> None:
|
|
303
|
+
"""
|
|
304
|
+
Convert DataFrame(s) to Google Sheet.
|
|
305
|
+
|
|
306
|
+
Note: This writes directly to Google Sheets (cloud).
|
|
307
|
+
For local saving, convert to Excel or CSV first.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
df: Single DataFrame or dict of {sheet_name: DataFrame}
|
|
311
|
+
spreadsheet_url: Google Sheets URL
|
|
312
|
+
sheet_name: Sheet name (required if df is single DataFrame)
|
|
313
|
+
**options: Additional options
|
|
314
|
+
|
|
315
|
+
Examples:
|
|
316
|
+
>>> serializer = GoogleSheetsSerializer(credentials_path="creds.json")
|
|
317
|
+
>>> serializer.from_df(df, spreadsheet_url, sheet_name="Sheet1")
|
|
318
|
+
>>> serializer.from_df({"Sheet1": df1, "Sheet2": df2}, spreadsheet_url)
|
|
319
|
+
"""
|
|
320
|
+
try:
|
|
321
|
+
from gspread_dataframe import set_with_dataframe
|
|
322
|
+
|
|
323
|
+
client = self._get_client()
|
|
324
|
+
spreadsheet = client.open_by_url(spreadsheet_url)
|
|
325
|
+
|
|
326
|
+
if isinstance(df, dict):
|
|
327
|
+
# Multiple sheets
|
|
328
|
+
for name, sheet_df in df.items():
|
|
329
|
+
try:
|
|
330
|
+
try:
|
|
331
|
+
worksheet = spreadsheet.worksheet(name)
|
|
332
|
+
worksheet.clear()
|
|
333
|
+
except Exception:
|
|
334
|
+
# Create worksheet if it doesn't exist
|
|
335
|
+
worksheet = spreadsheet.add_worksheet(
|
|
336
|
+
title=name, rows="2000", cols="30"
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
set_with_dataframe(worksheet, sheet_df, **options)
|
|
340
|
+
except Exception as e:
|
|
341
|
+
raise SerializationError(
|
|
342
|
+
f"Failed to write sheet '{name}': {e}",
|
|
343
|
+
format_name=self.format_name,
|
|
344
|
+
original_error=e
|
|
345
|
+
)
|
|
346
|
+
else:
|
|
347
|
+
# Single sheet
|
|
348
|
+
if not sheet_name:
|
|
349
|
+
raise ValueError("sheet_name is required when writing a single DataFrame")
|
|
350
|
+
|
|
351
|
+
try:
|
|
352
|
+
try:
|
|
353
|
+
worksheet = spreadsheet.worksheet(sheet_name)
|
|
354
|
+
worksheet.clear()
|
|
355
|
+
except Exception:
|
|
356
|
+
# Create worksheet if it doesn't exist
|
|
357
|
+
worksheet = spreadsheet.add_worksheet(
|
|
358
|
+
title=sheet_name, rows="2000", cols="30"
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
set_with_dataframe(worksheet, df, **options)
|
|
362
|
+
except Exception as e:
|
|
363
|
+
raise SerializationError(
|
|
364
|
+
f"Failed to write sheet '{sheet_name}': {e}",
|
|
365
|
+
format_name=self.format_name,
|
|
366
|
+
original_error=e
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
except Exception as e:
|
|
370
|
+
raise SerializationError(
|
|
371
|
+
f"Failed to convert DataFrame to Google Sheet: {e}",
|
|
372
|
+
format_name=self.format_name,
|
|
373
|
+
original_error=e
|
|
374
|
+
)
|