From 317c95507969d7ba8f3d068a81dca3de9cdbf848 Mon Sep 17 00:00:00 2001 From: David Tulga <3924980+dtulga@users.noreply.github.com> Date: Wed, 10 Jul 2024 14:41:51 -0700 Subject: [PATCH] Initial DataChain Commit (#1) This adds the DataChain code to this repository, and includes an additional top ten contributors from the source iterative/dvcx repository. --------- Co-authored-by: Ronan Lamy Co-authored-by: Ivan Longin Co-authored-by: skshetry <18718008+skshetry@users.noreply.github.com> Co-authored-by: Jon Burdo Co-authored-by: Dmitry Petrov Co-authored-by: Domas Monkus Co-authored-by: Vladimir Rudnykh Co-authored-by: Dave Berenbaum Co-authored-by: Matt Seddon <37993418+mattseddon@users.noreply.github.com> Co-authored-by: Ivan Shcheklein --- .cruft.json | 23 + .gitattributes | 1 + .github/ISSUE_TEMPLATE/bug_report.yml | 27 + .github/ISSUE_TEMPLATE/empty_issue.md | 4 + .github/ISSUE_TEMPLATE/feature_request.yml | 12 + .github/codecov.yaml | 16 + .github/dependabot.yml | 16 + .github/workflows/benchmarks.yml | 34 + .github/workflows/release.yml | 40 + .github/workflows/tests.yml | 127 + .github/workflows/update-template.yaml | 19 + .gitignore | 142 + .pre-commit-config.yaml | 50 + .reuse/dep5 | 8 + CODE_OF_CONDUCT.rst | 105 + CONTRIBUTING.rst | 129 + LICENSE | 201 + LICENSES/Apache-2.0.txt | 73 + LICENSES/BSD-3-Clause.txt | 11 + LICENSES/Python-2.0.txt | 72 + README.rst | 293 ++ docs/assets/datachain.png | Bin 0 -> 2315 bytes docs/index.md | 3 + docs/references/catalog.md | 3 + docs/references/datachain.md | 3 + docs/tutorials/cv_intro.md | 217 + docs/tutorials/udfs.md | 94 + examples/blip2_image_desc_lib.py | 35 + examples/clip.py | 50 + examples/common_sql_functions.py | 78 + .../fashion_product_images/.gitignore | 4 + .../1-quick-start.ipynb | 2651 ++++++++++++ .../2-working-with-image-datachains.ipynb | 3589 ++++++++++++++++ .../fashion_product_images/README.md | 60 + .../fashion_product_images/requirements.txt | 6 + .../scripts/1-quick-start.py | 91 + .../scripts/2-basic-operations.py | 51 + .../scripts/2-embeddings.py | 44 + .../scripts/3-split-train-test.py | 46 + .../fashion_product_images/src/clustering.py | 41 + .../static/images/basic-operations.png | Bin 0 -> 311513 bytes .../static/images/core-concepts.png | Bin 0 -> 132975 bytes .../static/images/datachain-logo.png | Bin 0 -> 142179 bytes .../static/images/datachain-overview.png | Bin 0 -> 158727 bytes .../static/images/dataset-1.png | Bin 0 -> 180966 bytes .../static/images/dataset-2.png | Bin 0 -> 183612 bytes .../static/images/dataset-3.png | Bin 0 -> 154158 bytes .../static/images/studio.png | Bin 0 -> 238674 bytes examples/hf_pipeline.py | 98 + examples/iptc_exif_xmp_lib.py | 15 + examples/json-csv-reader.py | 119 + examples/llava2_image_desc_lib.py | 43 + examples/llm-claude-aggregate-query.py | 40 + examples/llm-claude-simple-query.py | 47 + examples/llm-claude.py | 21 + examples/loader.py | 31 + examples/multimodal/clip_fine_tuning.ipynb | 1700 ++++++++ examples/neurips/README | 18 + examples/neurips/distance_to_query.py | 29 + examples/neurips/llm_chat.py | 46 + examples/neurips/requirements.txt | 9 + examples/neurips/single_query.py | 119 + examples/neurips/text_loaders.py | 80 + examples/openai_image_desc_lib.py | 29 + examples/openimage-detect.py | 72 + examples/pose_detection.py | 220 + examples/torch-loader.py | 79 + examples/udfs/batching.py | 34 + examples/udfs/image_transformation.py | 45 + examples/udfs/parallel.py | 55 + examples/udfs/simple.py | 42 + examples/udfs/stateful.py | 44 + examples/udfs/stateful_similarity.py | 79 + examples/unstructured-text.py | 54 + examples/wds.py | 36 + examples/wds_filtered.py | 55 + examples/zalando/zalando_clip.py | 44 + examples/zalando/zalando_dir_as_class.py | 31 + .../zalando/zalando_splits_and_classes_ds.py | 9 + .../zalando_splits_and_classes_output.py | 17 + mkdocs.yml | 136 + noxfile.py | 76 + pyproject.toml | 254 ++ src/datachain/__init__.py | 0 src/datachain/__main__.py | 6 + src/datachain/asyn.py | 226 + src/datachain/cache.py | 146 + src/datachain/catalog/__init__.py | 17 + src/datachain/catalog/catalog.py | 2346 +++++++++++ src/datachain/catalog/datasource.py | 45 + src/datachain/catalog/loader.py | 173 + src/datachain/catalog/subclass.py | 60 + src/datachain/cli.py | 1112 +++++ src/datachain/cli_utils.py | 72 + src/datachain/client/__init__.py | 4 + src/datachain/client/azure.py | 66 + src/datachain/client/fileslice.py | 106 + src/datachain/client/fsspec.py | 407 ++ src/datachain/client/gcs.py | 132 + src/datachain/client/local.py | 166 + src/datachain/client/s3.py | 173 + src/datachain/config.py | 62 + src/datachain/data_storage/__init__.py | 14 + src/datachain/data_storage/db_engine.py | 108 + src/datachain/data_storage/id_generator.py | 122 + src/datachain/data_storage/job.py | 22 + src/datachain/data_storage/metastore.py | 1578 +++++++ src/datachain/data_storage/schema.py | 266 ++ src/datachain/data_storage/serializer.py | 29 + src/datachain/data_storage/sqlite.py | 710 ++++ src/datachain/data_storage/warehouse.py | 960 +++++ src/datachain/dataset.py | 487 +++ src/datachain/error.py | 61 + src/datachain/lib/__init__.py | 0 src/datachain/lib/arrow.py | 79 + src/datachain/lib/cached_stream.py | 38 + src/datachain/lib/claude.py | 69 + src/datachain/lib/clip.py | 151 + src/datachain/lib/dc.py | 925 +++++ src/datachain/lib/feature.py | 407 ++ src/datachain/lib/feature_registry.py | 51 + src/datachain/lib/feature_utils.py | 136 + src/datachain/lib/file.py | 288 ++ src/datachain/lib/gpt4_vision.py | 105 + src/datachain/lib/hf_image_to_text.py | 105 + src/datachain/lib/hf_pipeline.py | 98 + src/datachain/lib/image.py | 89 + src/datachain/lib/image_transform.py | 104 + src/datachain/lib/iptc_exif_xmp.py | 83 + src/datachain/lib/meta_formats.py | 196 + src/datachain/lib/pytorch.py | 152 + src/datachain/lib/settings.py | 84 + src/datachain/lib/signal_schema.py | 331 ++ src/datachain/lib/text.py | 49 + src/datachain/lib/udf.py | 214 + src/datachain/lib/udf_signature.py | 196 + src/datachain/lib/unstructured.py | 41 + src/datachain/lib/utils.py | 25 + src/datachain/lib/vfile.py | 0 src/datachain/lib/webdataset.py | 264 ++ src/datachain/lib/webdataset_laion.py | 65 + src/datachain/listing.py | 248 ++ src/datachain/node.py | 210 + src/datachain/nodes_fetcher.py | 30 + src/datachain/nodes_thread_pool.py | 115 + src/datachain/progress.py | 149 + src/datachain/py.typed | 0 src/datachain/query/__init__.py | 17 + src/datachain/query/batch.py | 121 + src/datachain/query/builtins.py | 117 + src/datachain/query/dataset.py | 1906 +++++++++ src/datachain/query/dispatch.py | 394 ++ src/datachain/query/metrics.py | 19 + src/datachain/query/params.py | 27 + src/datachain/query/schema.py | 289 ++ src/datachain/query/session.py | 107 + src/datachain/query/udf.py | 237 ++ src/datachain/remote/__init__.py | 0 src/datachain/remote/studio.py | 227 + src/datachain/sql/__init__.py | 16 + src/datachain/sql/default/__init__.py | 3 + src/datachain/sql/default/base.py | 22 + src/datachain/sql/functions/__init__.py | 25 + src/datachain/sql/functions/array.py | 38 + src/datachain/sql/functions/conditional.py | 9 + src/datachain/sql/functions/path.py | 61 + src/datachain/sql/functions/random.py | 12 + src/datachain/sql/functions/string.py | 22 + src/datachain/sql/selectable.py | 50 + src/datachain/sql/sqlite/__init__.py | 7 + src/datachain/sql/sqlite/base.py | 364 ++ src/datachain/sql/sqlite/types.py | 74 + src/datachain/sql/sqlite/vector.py | 23 + src/datachain/sql/types.py | 454 ++ src/datachain/sql/utils.py | 23 + src/datachain/storage.py | 136 + src/datachain/utils.py | 390 ++ tests/__init__.py | 1 + tests/benchmarks/__init__.py | 0 tests/benchmarks/conftest.py | 131 + tests/benchmarks/test_ls.py | 2 + tests/benchmarks/test_version.py | 2 + tests/conftest.py | 551 +++ tests/data.py | 126 + tests/examples/__init__.py | 0 tests/examples/test_wds_e2e.py | 130 + tests/examples/wds_data.py | 153 + tests/func/__init__.py | 0 tests/func/test_catalog.py | 1149 ++++++ tests/func/test_client.py | 93 + tests/func/test_datachain.py | 40 + tests/func/test_dataset_query.py | 3646 +++++++++++++++++ tests/func/test_datasets.py | 945 +++++ tests/func/test_ls.py | 384 ++ tests/func/test_pull.py | 339 ++ tests/func/test_pytorch.py | 69 + tests/func/test_query.py | 391 ++ tests/scripts/feature_class.py | 17 + tests/scripts/feature_class_parallel.py | 29 + tests/scripts/name_len_normal.py | 27 + tests/scripts/name_len_slow.py | 46 + tests/test_cli_e2e.py | 219 + tests/test_query_e2e.py | 222 + tests/unit/__init__.py | 0 tests/unit/lib/__init__.py | 0 tests/unit/lib/test_arrow.py | 108 + tests/unit/lib/test_clip.py | 61 + tests/unit/lib/test_datachain.py | 786 ++++ tests/unit/lib/test_datachain_bootstrap.py | 92 + tests/unit/lib/test_datachain_merge.py | 198 + tests/unit/lib/test_feature.py | 374 ++ tests/unit/lib/test_feature_utils.py | 106 + tests/unit/lib/test_file.py | 162 + tests/unit/lib/test_image.py | 65 + tests/unit/lib/test_signal_schema.py | 299 ++ tests/unit/lib/test_text.py | 51 + tests/unit/lib/test_udf_signature.py | 191 + tests/unit/lib/test_utils.py | 58 + tests/unit/lib/test_webdataset.py | 152 + tests/unit/sql/__init__.py | 0 tests/unit/sql/sqlite/__init__.py | 0 tests/unit/sql/sqlite/test_utils.py | 15 + tests/unit/sql/test_array.py | 20 + tests/unit/sql/test_conditional.py | 51 + tests/unit/sql/test_path.py | 74 + tests/unit/sql/test_random.py | 8 + tests/unit/sql/test_selectable.py | 27 + tests/unit/sql/test_string.py | 23 + tests/unit/test_asyn.py | 163 + tests/unit/test_cache.py | 56 + tests/unit/test_catalog.py | 170 + tests/unit/test_catalog_loader.py | 187 + tests/unit/test_cli_parsing.py | 125 + tests/unit/test_client.py | 136 + tests/unit/test_client_s3.py | 81 + tests/unit/test_data_storage.py | 171 + tests/unit/test_database_engine.py | 80 + tests/unit/test_dataset.py | 88 + tests/unit/test_dispatch.py | 53 + tests/unit/test_fileslice.py | 64 + tests/unit/test_id_generator.py | 185 + tests/unit/test_listing.py | 140 + tests/unit/test_metastore.py | 55 + tests/unit/test_query_metrics.py | 29 + tests/unit/test_query_params.py | 39 + tests/unit/test_serializer.py | 92 + tests/unit/test_session.py | 62 + tests/unit/test_storage.py | 222 + tests/unit/test_udf.py | 148 + tests/unit/test_utils.py | 180 + tests/unit/test_warehouse.py | 63 + tests/utils.py | 270 ++ 252 files changed, 46750 insertions(+) create mode 100644 .cruft.json create mode 100644 .gitattributes create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/empty_issue.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/codecov.yaml create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/benchmarks.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/tests.yml create mode 100644 .github/workflows/update-template.yaml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 .reuse/dep5 create mode 100644 CODE_OF_CONDUCT.rst create mode 100644 CONTRIBUTING.rst create mode 100644 LICENSE create mode 100644 LICENSES/Apache-2.0.txt create mode 100644 LICENSES/BSD-3-Clause.txt create mode 100644 LICENSES/Python-2.0.txt create mode 100644 README.rst create mode 100644 docs/assets/datachain.png create mode 100644 docs/index.md create mode 100644 docs/references/catalog.md create mode 100644 docs/references/datachain.md create mode 100644 docs/tutorials/cv_intro.md create mode 100644 docs/tutorials/udfs.md create mode 100644 examples/blip2_image_desc_lib.py create mode 100644 examples/clip.py create mode 100644 examples/common_sql_functions.py create mode 100644 examples/computer_vision/fashion_product_images/.gitignore create mode 100644 examples/computer_vision/fashion_product_images/1-quick-start.ipynb create mode 100644 examples/computer_vision/fashion_product_images/2-working-with-image-datachains.ipynb create mode 100644 examples/computer_vision/fashion_product_images/README.md create mode 100644 examples/computer_vision/fashion_product_images/requirements.txt create mode 100644 examples/computer_vision/fashion_product_images/scripts/1-quick-start.py create mode 100644 examples/computer_vision/fashion_product_images/scripts/2-basic-operations.py create mode 100644 examples/computer_vision/fashion_product_images/scripts/2-embeddings.py create mode 100644 examples/computer_vision/fashion_product_images/scripts/3-split-train-test.py create mode 100644 examples/computer_vision/fashion_product_images/src/clustering.py create mode 100644 examples/computer_vision/fashion_product_images/static/images/basic-operations.png create mode 100644 examples/computer_vision/fashion_product_images/static/images/core-concepts.png create mode 100644 examples/computer_vision/fashion_product_images/static/images/datachain-logo.png create mode 100644 examples/computer_vision/fashion_product_images/static/images/datachain-overview.png create mode 100644 examples/computer_vision/fashion_product_images/static/images/dataset-1.png create mode 100644 examples/computer_vision/fashion_product_images/static/images/dataset-2.png create mode 100644 examples/computer_vision/fashion_product_images/static/images/dataset-3.png create mode 100644 examples/computer_vision/fashion_product_images/static/images/studio.png create mode 100644 examples/hf_pipeline.py create mode 100644 examples/iptc_exif_xmp_lib.py create mode 100644 examples/json-csv-reader.py create mode 100644 examples/llava2_image_desc_lib.py create mode 100644 examples/llm-claude-aggregate-query.py create mode 100644 examples/llm-claude-simple-query.py create mode 100644 examples/llm-claude.py create mode 100644 examples/loader.py create mode 100644 examples/multimodal/clip_fine_tuning.ipynb create mode 100644 examples/neurips/README create mode 100644 examples/neurips/distance_to_query.py create mode 100644 examples/neurips/llm_chat.py create mode 100644 examples/neurips/requirements.txt create mode 100644 examples/neurips/single_query.py create mode 100644 examples/neurips/text_loaders.py create mode 100644 examples/openai_image_desc_lib.py create mode 100644 examples/openimage-detect.py create mode 100644 examples/pose_detection.py create mode 100644 examples/torch-loader.py create mode 100644 examples/udfs/batching.py create mode 100644 examples/udfs/image_transformation.py create mode 100644 examples/udfs/parallel.py create mode 100644 examples/udfs/simple.py create mode 100644 examples/udfs/stateful.py create mode 100644 examples/udfs/stateful_similarity.py create mode 100644 examples/unstructured-text.py create mode 100644 examples/wds.py create mode 100644 examples/wds_filtered.py create mode 100644 examples/zalando/zalando_clip.py create mode 100644 examples/zalando/zalando_dir_as_class.py create mode 100644 examples/zalando/zalando_splits_and_classes_ds.py create mode 100644 examples/zalando/zalando_splits_and_classes_output.py create mode 100644 mkdocs.yml create mode 100644 noxfile.py create mode 100644 pyproject.toml create mode 100644 src/datachain/__init__.py create mode 100644 src/datachain/__main__.py create mode 100644 src/datachain/asyn.py create mode 100644 src/datachain/cache.py create mode 100644 src/datachain/catalog/__init__.py create mode 100644 src/datachain/catalog/catalog.py create mode 100644 src/datachain/catalog/datasource.py create mode 100644 src/datachain/catalog/loader.py create mode 100644 src/datachain/catalog/subclass.py create mode 100644 src/datachain/cli.py create mode 100644 src/datachain/cli_utils.py create mode 100644 src/datachain/client/__init__.py create mode 100644 src/datachain/client/azure.py create mode 100644 src/datachain/client/fileslice.py create mode 100644 src/datachain/client/fsspec.py create mode 100644 src/datachain/client/gcs.py create mode 100644 src/datachain/client/local.py create mode 100644 src/datachain/client/s3.py create mode 100644 src/datachain/config.py create mode 100644 src/datachain/data_storage/__init__.py create mode 100644 src/datachain/data_storage/db_engine.py create mode 100644 src/datachain/data_storage/id_generator.py create mode 100644 src/datachain/data_storage/job.py create mode 100644 src/datachain/data_storage/metastore.py create mode 100644 src/datachain/data_storage/schema.py create mode 100644 src/datachain/data_storage/serializer.py create mode 100644 src/datachain/data_storage/sqlite.py create mode 100644 src/datachain/data_storage/warehouse.py create mode 100644 src/datachain/dataset.py create mode 100644 src/datachain/error.py create mode 100644 src/datachain/lib/__init__.py create mode 100644 src/datachain/lib/arrow.py create mode 100644 src/datachain/lib/cached_stream.py create mode 100644 src/datachain/lib/claude.py create mode 100644 src/datachain/lib/clip.py create mode 100644 src/datachain/lib/dc.py create mode 100644 src/datachain/lib/feature.py create mode 100644 src/datachain/lib/feature_registry.py create mode 100644 src/datachain/lib/feature_utils.py create mode 100644 src/datachain/lib/file.py create mode 100644 src/datachain/lib/gpt4_vision.py create mode 100644 src/datachain/lib/hf_image_to_text.py create mode 100644 src/datachain/lib/hf_pipeline.py create mode 100644 src/datachain/lib/image.py create mode 100644 src/datachain/lib/image_transform.py create mode 100644 src/datachain/lib/iptc_exif_xmp.py create mode 100644 src/datachain/lib/meta_formats.py create mode 100644 src/datachain/lib/pytorch.py create mode 100644 src/datachain/lib/settings.py create mode 100644 src/datachain/lib/signal_schema.py create mode 100644 src/datachain/lib/text.py create mode 100644 src/datachain/lib/udf.py create mode 100644 src/datachain/lib/udf_signature.py create mode 100644 src/datachain/lib/unstructured.py create mode 100644 src/datachain/lib/utils.py create mode 100644 src/datachain/lib/vfile.py create mode 100644 src/datachain/lib/webdataset.py create mode 100644 src/datachain/lib/webdataset_laion.py create mode 100644 src/datachain/listing.py create mode 100644 src/datachain/node.py create mode 100644 src/datachain/nodes_fetcher.py create mode 100644 src/datachain/nodes_thread_pool.py create mode 100644 src/datachain/progress.py create mode 100644 src/datachain/py.typed create mode 100644 src/datachain/query/__init__.py create mode 100644 src/datachain/query/batch.py create mode 100644 src/datachain/query/builtins.py create mode 100644 src/datachain/query/dataset.py create mode 100644 src/datachain/query/dispatch.py create mode 100644 src/datachain/query/metrics.py create mode 100644 src/datachain/query/params.py create mode 100644 src/datachain/query/schema.py create mode 100644 src/datachain/query/session.py create mode 100644 src/datachain/query/udf.py create mode 100644 src/datachain/remote/__init__.py create mode 100644 src/datachain/remote/studio.py create mode 100644 src/datachain/sql/__init__.py create mode 100644 src/datachain/sql/default/__init__.py create mode 100644 src/datachain/sql/default/base.py create mode 100644 src/datachain/sql/functions/__init__.py create mode 100644 src/datachain/sql/functions/array.py create mode 100644 src/datachain/sql/functions/conditional.py create mode 100644 src/datachain/sql/functions/path.py create mode 100644 src/datachain/sql/functions/random.py create mode 100644 src/datachain/sql/functions/string.py create mode 100644 src/datachain/sql/selectable.py create mode 100644 src/datachain/sql/sqlite/__init__.py create mode 100644 src/datachain/sql/sqlite/base.py create mode 100644 src/datachain/sql/sqlite/types.py create mode 100644 src/datachain/sql/sqlite/vector.py create mode 100644 src/datachain/sql/types.py create mode 100644 src/datachain/sql/utils.py create mode 100644 src/datachain/storage.py create mode 100644 src/datachain/utils.py create mode 100644 tests/__init__.py create mode 100644 tests/benchmarks/__init__.py create mode 100644 tests/benchmarks/conftest.py create mode 100644 tests/benchmarks/test_ls.py create mode 100644 tests/benchmarks/test_version.py create mode 100644 tests/conftest.py create mode 100644 tests/data.py create mode 100644 tests/examples/__init__.py create mode 100644 tests/examples/test_wds_e2e.py create mode 100644 tests/examples/wds_data.py create mode 100644 tests/func/__init__.py create mode 100644 tests/func/test_catalog.py create mode 100644 tests/func/test_client.py create mode 100644 tests/func/test_datachain.py create mode 100644 tests/func/test_dataset_query.py create mode 100644 tests/func/test_datasets.py create mode 100644 tests/func/test_ls.py create mode 100644 tests/func/test_pull.py create mode 100644 tests/func/test_pytorch.py create mode 100644 tests/func/test_query.py create mode 100644 tests/scripts/feature_class.py create mode 100644 tests/scripts/feature_class_parallel.py create mode 100644 tests/scripts/name_len_normal.py create mode 100644 tests/scripts/name_len_slow.py create mode 100644 tests/test_cli_e2e.py create mode 100644 tests/test_query_e2e.py create mode 100644 tests/unit/__init__.py create mode 100644 tests/unit/lib/__init__.py create mode 100644 tests/unit/lib/test_arrow.py create mode 100644 tests/unit/lib/test_clip.py create mode 100644 tests/unit/lib/test_datachain.py create mode 100644 tests/unit/lib/test_datachain_bootstrap.py create mode 100644 tests/unit/lib/test_datachain_merge.py create mode 100644 tests/unit/lib/test_feature.py create mode 100644 tests/unit/lib/test_feature_utils.py create mode 100644 tests/unit/lib/test_file.py create mode 100644 tests/unit/lib/test_image.py create mode 100644 tests/unit/lib/test_signal_schema.py create mode 100644 tests/unit/lib/test_text.py create mode 100644 tests/unit/lib/test_udf_signature.py create mode 100644 tests/unit/lib/test_utils.py create mode 100644 tests/unit/lib/test_webdataset.py create mode 100644 tests/unit/sql/__init__.py create mode 100644 tests/unit/sql/sqlite/__init__.py create mode 100644 tests/unit/sql/sqlite/test_utils.py create mode 100644 tests/unit/sql/test_array.py create mode 100644 tests/unit/sql/test_conditional.py create mode 100644 tests/unit/sql/test_path.py create mode 100644 tests/unit/sql/test_random.py create mode 100644 tests/unit/sql/test_selectable.py create mode 100644 tests/unit/sql/test_string.py create mode 100644 tests/unit/test_asyn.py create mode 100644 tests/unit/test_cache.py create mode 100644 tests/unit/test_catalog.py create mode 100644 tests/unit/test_catalog_loader.py create mode 100644 tests/unit/test_cli_parsing.py create mode 100644 tests/unit/test_client.py create mode 100644 tests/unit/test_client_s3.py create mode 100644 tests/unit/test_data_storage.py create mode 100644 tests/unit/test_database_engine.py create mode 100644 tests/unit/test_dataset.py create mode 100644 tests/unit/test_dispatch.py create mode 100644 tests/unit/test_fileslice.py create mode 100644 tests/unit/test_id_generator.py create mode 100644 tests/unit/test_listing.py create mode 100644 tests/unit/test_metastore.py create mode 100644 tests/unit/test_query_metrics.py create mode 100644 tests/unit/test_query_params.py create mode 100644 tests/unit/test_serializer.py create mode 100644 tests/unit/test_session.py create mode 100644 tests/unit/test_storage.py create mode 100644 tests/unit/test_udf.py create mode 100644 tests/unit/test_utils.py create mode 100644 tests/unit/test_warehouse.py create mode 100644 tests/utils.py diff --git a/.cruft.json b/.cruft.json new file mode 100644 index 000000000..7c6259e7b --- /dev/null +++ b/.cruft.json @@ -0,0 +1,23 @@ +{ + "template": "https://github.com/iterative/py-template", + "commit": "867297aa15a0deaf5302edd01a2bc7ab87039627", + "checkout": null, + "context": { + "cookiecutter": { + "project_name": "datachain", + "package_name": "datachain", + "friendly_name": "DataChain", + "author": "Dmitry Petrov", + "email": "support@dvc.org", + "github_user": "iterative", + "version": "0.0.0", + "copyright_year": "2022", + "license": "Apache-2.0", + "docs": true, + "short_description": "Wrangle unstructured AI data at scale", + "development_status": "Development Status :: 2 - Pre-Alpha", + "_template": "https://github.com/iterative/py-template" + } + }, + "directory": null +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..6313b56c5 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 000000000..0350ecf6a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,27 @@ +name: 🐛 Bug Report +description: Report a bug to help us improve +labels: bug + +body: + - type: textarea + id: description + attributes: + label: Description + description: + validations: + required: true + + - type: textarea + id: version + attributes: + label: Version Info + description: | + Please run the following command and copy the output below: + + ```bash + datachain -V; python -V + ``` + + render: Text + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/empty_issue.md b/.github/ISSUE_TEMPLATE/empty_issue.md new file mode 100644 index 000000000..37f1bac83 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/empty_issue.md @@ -0,0 +1,4 @@ +--- +name: Empty Issue +about: A minimal template for ordinary issues or sub-tasks +--- diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 000000000..a7f505b4f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,12 @@ +name: 💡 Feature Request +description: Suggest a new feature or share ideas +labels: enhancement + +body: + - type: textarea + id: description + attributes: + label: Description + description: + validations: + required: true diff --git a/.github/codecov.yaml b/.github/codecov.yaml new file mode 100644 index 000000000..b04164df0 --- /dev/null +++ b/.github/codecov.yaml @@ -0,0 +1,16 @@ +coverage: + status: + project: + default: + # auto compares coverage to the previous base commit + target: auto + # adjust accordingly based on how flaky your tests are + # this allows a 10% drop from the previous base commit coverage + threshold: 10% + # non-blocking status checks + informational: true + +flags: + datachain: + paths: + - src/datachain diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..393a9449d --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,16 @@ +version: 2 + +updates: + - directory: "/" + package-ecosystem: "pip" + schedule: + interval: "weekly" + labels: + - "maintenance" + + - directory: "/" + package-ecosystem: "github-actions" + schedule: + interval: "weekly" + labels: + - "maintenance" diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml new file mode 100644 index 000000000..564fb54fc --- /dev/null +++ b/.github/workflows/benchmarks.yml @@ -0,0 +1,34 @@ +name: Benchmarks + +on: + schedule: + - cron: '0 0 * * *' + pull_request: + types: [opened, reopened, labeled, synchronize] + workflow_dispatch: {} + +env: + FORCE_COLOR: "1" + +jobs: + build: + if: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-benchmarks') }} + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: '3.10' + cache: 'pip' + + - name: Upgrade nox and uv + run: | + python -m pip install --upgrade 'nox[uv]' + nox --version + uv --version + + - name: Run benchmarks + run: nox -s bench diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..366d3033d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,40 @@ +name: Release + +on: + release: + types: [published] + workflow_dispatch: + +env: + FORCE_COLOR: "1" + +jobs: + release: + environment: pypi + permissions: + contents: read + id-token: write + runs-on: ubuntu-latest + steps: + - name: Check out the repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Upgrade nox and uv + run: | + python -m pip install --upgrade 'nox[uv]' + nox --version + uv --version + + - name: Build package + run: nox -s build + + - name: Upload package + if: github.event_name == 'release' + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 000000000..bab0cd232 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,127 @@ +name: Tests + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +env: + FORCE_COLOR: "1" + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + lint: + runs-on: ubuntu-latest + steps: + + - name: Check out the repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python 3.9 + uses: actions/setup-python@v5 + with: + python-version: '3.9' + cache: 'pip' + + - name: Upgrade nox and uv + run: | + python -m pip install --upgrade 'nox[uv]' + nox --version + uv --version + + - name: Cache mypy + uses: actions/cache@v4 + with: + path: .mypy_cache + key: mypy-${{ runner.os }}-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }} + + - name: Cache pre-commit hooks + uses: actions/cache@v4 + with: + path: ~/.cache/pre-commit + key: pre-commit-3|${{ env.pythonLocation }}|${{ hashFiles('.pre-commit-config.yaml') }} + + - name: Lint code + run: nox -s lint + + tests: + timeout-minutes: 25 + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest-8-cores] + pyv: ['3.9', '3.10', '3.11', '3.12'] + include: + - os: macos-latest + pyv: '3.9' + - os: macos-latest + pyv: '3.12' + - os: windows-latest-8-cores + pyv: '3.9' + - os: windows-latest-8-cores + pyv: '3.12' + + steps: + + # https://github.com/iterative/pytest-servers/pull/122 + # https://github.com/abiosoft/colima/issues/468 + # https://github.com/abiosoft/colima/blob/main/docs/FAQ.md#cannot-connect-to-the-docker-daemon-at-unixvarrundockersock-is-the-docker-daemon-running + # colima v0.5.6 seems to run more stable than the latest - that has occasional network failures (ports are not open) + # see: https://github.com/abiosoft/colima/issues/962 + - name: Use colima as default docker host on MacOS + if: runner.os == 'macOS' + run: | + brew install docker lima || true # avoid non-zero exit code if brew link fails + sudo curl -L -o /usr/local/bin/colima https://github.com/abiosoft/colima/releases/download/v0.5.6/colima-Darwin-x86_64 + sudo chmod +x /usr/local/bin/colima + colima start + sudo ln -vsf "${HOME}"/.colima/default/docker.sock /var/run/docker.sock + env: + HOMEBREW_NO_AUTO_UPDATE: true + HOMEBREW_NO_INSTALL_CLEANUP: true + HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: true + HOMEBREW_NO_INSTALL_UPGRADE: true + + - name: Check out the repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python ${{ matrix.pyv }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.pyv }} + cache: 'pip' + + - name: Upgrade nox and uv + run: | + python -m pip install --upgrade 'nox[uv]' + nox --version + uv --version + + - name: Skip flaky azure, gs remotes if unavailable on macos + if: runner.os == 'macOS' + run: echo 'DATACHAIN_TEST_SKIP_MISSING_REMOTES=azure,gs' >> "$GITHUB_ENV" + + - name: Run tests + run: nox -s tests-${{ matrix.pyv }} + + - name: Upload coverage report + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: coverage.xml + flags: datachain + + - name: Build package + run: nox -s build + + - name: Build docs + run: nox -s docs diff --git a/.github/workflows/update-template.yaml b/.github/workflows/update-template.yaml new file mode 100644 index 000000000..ce3b40c16 --- /dev/null +++ b/.github/workflows/update-template.yaml @@ -0,0 +1,19 @@ +name: Update template + +on: + schedule: + - cron: '5 1 * * *' # every day at 01:05 + + workflow_dispatch: + +jobs: + update: + runs-on: ubuntu-latest + steps: + - name: Check out the repository + uses: actions/checkout@v4 + + - name: Update template + uses: iterative/py-template@main + with: + token: ${{ secrets.UPDATE_TEMPLATE_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..bf1d7d73a --- /dev/null +++ b/.gitignore @@ -0,0 +1,142 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +.idea/ +.vscode/ +.datachain/ +.dvcx/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..d37fcd017 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,50 @@ +default_language_version: + python: python3 +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: check-added-large-files + exclude: '^tests/examples/data/' + - id: check-case-conflict + - id: check-docstring-first + exclude: '^examples/' + - id: check-executables-have-shebangs + - id: check-json + - id: check-merge-conflict + args: ['--assume-in-merge'] + - id: check-toml + - id: check-yaml + - id: end-of-file-fixer + - id: mixed-line-ending + args: ['--fix=lf'] + - id: sort-simple-yaml + - id: trailing-whitespace + exclude: '^LICENSES/' + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: 'v0.5.0' + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + - id: ruff-format + - repo: https://github.com/codespell-project/codespell + rev: v2.3.0 + hooks: + - id: codespell + additional_dependencies: ["tomli"] + - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks + rev: v2.13.0 + hooks: + - id: pretty-format-toml + args: [--autofix, --no-sort] + - id: pretty-format-yaml + args: [--autofix, --indent, '2', '--offset', '2', --preserve-quotes] + - repo: local + hooks: + - id: mypy + name: mypy + entry: mypy + files: '^src/|^tests/' + language: system + types: [python] + require_serial: true diff --git a/.reuse/dep5 b/.reuse/dep5 new file mode 100644 index 000000000..20dca9d20 --- /dev/null +++ b/.reuse/dep5 @@ -0,0 +1,8 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: dvcx +Upstream-Contact: Iterative, Inc. +Source: https://github.com/iterative/dvcx + +Files: * +Copyright: 2017-2021 Iterative, Inc. +License: Apache-2.0 diff --git a/CODE_OF_CONDUCT.rst b/CODE_OF_CONDUCT.rst new file mode 100644 index 000000000..bc50a04c8 --- /dev/null +++ b/CODE_OF_CONDUCT.rst @@ -0,0 +1,105 @@ +Contributor Covenant Code of Conduct +==================================== + +Our Pledge +---------- + +We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. + + +Our Standards +------------- + +Examples of behavior that contributes to a positive environment for our community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or + advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email + address, without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +Enforcement Responsibilities +---------------------------- + +Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. + + +Scope +----- + +This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. + + +Enforcement +----------- + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at support@dvc.org. All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the reporter of any incident. + + +Enforcement Guidelines +---------------------- + +Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: + + +1. Correction +~~~~~~~~~~~~~ + +**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. + + +2. Warning +~~~~~~~~~~ + +**Community Impact**: A violation through a single incident or series of actions. + +**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. + + +3. Temporary Ban +~~~~~~~~~~~~~~~~ + +**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. + + +4. Permanent Ban +~~~~~~~~~~~~~~~~ + +**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the community. + + +Attribution +----------- + +This Code of Conduct is adapted from the `Contributor Covenant `__, version 2.0, +available at https://www.contributor-covenant.org/version/2/0/code_of_conduct/. + +Community Impact Guidelines were inspired by `Mozilla’s code of conduct enforcement ladder `__. + +.. _homepage: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 000000000..22f680f6b --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,129 @@ +Contributor Guide +================= + +Thank you for your interest in improving this project. +This project is open-source under the `Apache 2.0 license`_ and +welcomes contributions in the form of bug reports, feature requests, and pull requests. + +Here is a list of important resources for contributors: + +- `Source Code`_ +- `Documentation`_ +- `Issue Tracker`_ +- `Code of Conduct`_ + +.. _Apache 2.0 license: https://opensource.org/licenses/Apache-2.0 +.. _Source Code: https://github.com/iterative/dvcx +.. _Documentation: https://docs.dvc.ai/datachain +.. _Issue Tracker: https://github.com/iterative/dvcx/issues + +How to report a bug +------------------- + +Report bugs on the `Issue Tracker`_. + +When filing an issue, make sure to answer these questions: + +- Which operating system and Python version are you using? +- Which version of this project are you using? +- What did you do? +- What did you expect to see? +- What did you see instead? + +The best way to get your bug fixed is to provide a test case, +and/or steps to reproduce the issue. + + +How to request a feature +------------------------ + +Request features on the `Issue Tracker`_. + + +How to set up your development environment +------------------------------------------ + +You need Python 3.8+ and the following tools: + +- Nox_ + +Install the package with development requirements: + +.. code:: console + + $ pip install nox + +.. _Nox: https://nox.thea.codes/ + + +How to test the project +----------------------- + +Run the full test suite: + +.. code:: console + + $ nox + +List the available Nox sessions: + +.. code:: console + + $ nox --list-sessions + +You can also run a specific Nox session. +For example, invoke the unit test suite like this: + +.. code:: console + + $ nox --session=tests + +Unit tests are located in the ``tests`` directory, +and are written using the pytest_ testing framework. + +.. _pytest: https://pytest.readthedocs.io/ + + +Build documentation +------------------- + +If you've made any changes to the documentation (including changes to function signatures, +class definitions, or docstrings that will appear in the API documentation), +make sure it builds successfully. + +.. code:: console + + $ nox -s docs + +In order to run this locally with hot reload on changes: + +.. code:: console + + $ mkdocs serve + + +How to submit changes +--------------------- + +Open a `pull request`_ to submit changes to this project. + +Your pull request needs to meet the following guidelines for acceptance: + +- The Nox test suite must pass without errors and warnings. +- Include unit tests. This project maintains 100% code coverage. +- If your changes add functionality, update the documentation accordingly. + +Feel free to submit early, though—we can always iterate on this. + +To run linting and code formatting checks, you can invoke a `lint` session in nox: + +.. code:: console + + $ nox -s lint + +It is recommended to open an issue before starting work on anything. +This will allow a chance to talk it over with the owners and validate your approach. + +.. _pull request: https://github.com/iterative/dvcx/pulls +.. github-only +.. _Code of Conduct: CODE_OF_CONDUCT.rst diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..eea7a3bf5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2022 Dmitry Petrov. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/LICENSES/Apache-2.0.txt b/LICENSES/Apache-2.0.txt new file mode 100644 index 000000000..137069b82 --- /dev/null +++ b/LICENSES/Apache-2.0.txt @@ -0,0 +1,73 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/LICENSES/BSD-3-Clause.txt b/LICENSES/BSD-3-Clause.txt new file mode 100644 index 000000000..ea890afbc --- /dev/null +++ b/LICENSES/BSD-3-Clause.txt @@ -0,0 +1,11 @@ +Copyright (c) . + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/LICENSES/Python-2.0.txt b/LICENSES/Python-2.0.txt new file mode 100644 index 000000000..b212cb2cb --- /dev/null +++ b/LICENSES/Python-2.0.txt @@ -0,0 +1,72 @@ +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 + + 1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and the Individual or Organization ("Licensee") accessing and otherwise using this software ("Python") in source or binary form and its associated documentation. + + 2. Subject to the terms and conditions of this License Agreement, PSF hereby grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative works, distribute, and otherwise use Python alone or in any derivative version, provided, however, that PSF's License Agreement and PSF's notice of copyright, i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006 Python Software Foundation; All Rights Reserved" are retained in Python alone or in any derivative version prepared by Licensee. + + 3. In the event Licensee prepares a derivative work that is based on or incorporates Python or any part thereof, and wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in any such work a brief summary of the changes made to Python. + + 4. PSF is making Python available to Licensee on an "AS IS" basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. + + 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + + 6. This License Agreement will automatically terminate upon a material breach of its terms and conditions. + + 7. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint venture between PSF and Licensee. This License Agreement does not grant permission to use PSF trademarks or trade name in a trademark sense to endorse or promote products or services of Licensee, or any third party. + + 8. By copying, installing or otherwise using Python, Licensee agrees to be bound by the terms and conditions of this License Agreement. + + +BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 + +BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 + + 1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the Individual or Organization ("Licensee") accessing and otherwise using this software in source or binary form and its associated documentation ("the Software"). + + 2. Subject to the terms and conditions of this BeOpen Python License Agreement, BeOpen hereby grants Licensee a non-exclusive, royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative works, distribute, and otherwise use the Software alone or in any derivative version, provided, however, that the BeOpen Python License is retained in the Software, alone or in any derivative version prepared by Licensee. + + 3. BeOpen is making the Software available to Licensee on an "AS IS" basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. + + 4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + + 5. This License Agreement will automatically terminate upon a material breach of its terms and conditions. + + 6. This License Agreement shall be governed by and interpreted in all respects by the law of the State of California, excluding conflict of law provisions. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint venture between BeOpen and Licensee. This License Agreement does not grant permission to use BeOpen trademarks or trade names in a trademark sense to endorse or promote products or services of Licensee, or any third party. As an exception, the "BeOpen Python" logos available at http://www.pythonlabs.com/logos.html may be used according to the permissions granted on that web page. + + 7. By copying, installing or otherwise using the software, Licensee agrees to be bound by the terms and conditions of this License Agreement. + + +CNRI OPEN SOURCE LICENSE AGREEMENT (for Python 1.6b1) + +IMPORTANT: PLEASE READ THE FOLLOWING AGREEMENT CAREFULLY. + +BY CLICKING ON "ACCEPT" WHERE INDICATED BELOW, OR BY COPYING, INSTALLING OR OTHERWISE USING PYTHON 1.6, beta 1 SOFTWARE, YOU ARE DEEMED TO HAVE AGREED TO THE TERMS AND CONDITIONS OF THIS LICENSE AGREEMENT. + + 1. This LICENSE AGREEMENT is between the Corporation for National Research Initiatives, having an office at 1895 Preston White Drive, Reston, VA 20191 ("CNRI"), and the Individual or Organization ("Licensee") accessing and otherwise using Python 1.6, beta 1 software in source or binary form and its associated documentation, as released at the www.python.org Internet site on August 4, 2000 ("Python 1.6b1"). + + 2. Subject to the terms and conditions of this License Agreement, CNRI hereby grants Licensee a non-exclusive, royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative works, distribute, and otherwise use Python 1.6b1 alone or in any derivative version, provided, however, that CNRIs License Agreement is retained in Python 1.6b1, alone or in any derivative version prepared by Licensee. + + Alternately, in lieu of CNRIs License Agreement, Licensee may substitute the following text (omitting the quotes): "Python 1.6, beta 1, is made available subject to the terms and conditions in CNRIs License Agreement. This Agreement may be located on the Internet using the following unique, persistent identifier (known as a handle): 1895.22/1011. This Agreement may also be obtained from a proxy server on the Internet using the URL:http://hdl.handle.net/1895.22/1011". + + 3. In the event Licensee prepares a derivative work that is based on or incorporates Python 1.6b1 or any part thereof, and wants to make the derivative work available to the public as provided herein, then Licensee hereby agrees to indicate in any such work the nature of the modifications made to Python 1.6b1. + + 4. CNRI is making Python 1.6b1 available to Licensee on an "AS IS" basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6b1 WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. + + 5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF USING, MODIFYING OR DISTRIBUTING PYTHON 1.6b1, OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + + 6. This License Agreement will automatically terminate upon a material breach of its terms and conditions. + + 7. This License Agreement shall be governed by and interpreted in all respects by the law of the State of Virginia, excluding conflict of law provisions. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint venture between CNRI and Licensee. This License Agreement does not grant permission to use CNRI trademarks or trade name in a trademark sense to endorse or promote products or services of Licensee, or any third party. + + 8. By clicking on the "ACCEPT" button where indicated, or by copying, installing or otherwise using Python 1.6b1, Licensee agrees to be bound by the terms and conditions of this License Agreement. + +ACCEPT + + +CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 + +Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, The Netherlands. All rights reserved. + + Permission to use, copy, modify, and distribute this software and its documentation for any purpose and without fee is hereby granted, provided that the above copyright notice appear in all copies and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of Stichting Mathematisch Centrum or CWI not be used in advertising or publicity pertaining to distribution of the software without specific, written prior permission. + + STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/README.rst b/README.rst new file mode 100644 index 000000000..cdf37d696 --- /dev/null +++ b/README.rst @@ -0,0 +1,293 @@ +|PyPI| |Python Version| |Codecov| |Tests| |License| + +.. |PyPI| image:: https://img.shields.io/pypi/v/datachain.svg + :target: https://pypi.org/project/datachain/ + :alt: PyPI +.. |Python Version| image:: https://img.shields.io/pypi/pyversions/datachain + :target: https://pypi.org/project/datachain + :alt: Python Version +.. |Codecov| image:: https://codecov.io/gh/iterative/dvcx/branch/main/graph/badge.svg?token=VSCP2T9R5X + :target: https://app.codecov.io/gh/iterative/dvcx + :alt: Codecov +.. |Tests| image:: https://github.com/iterative/dvcx/workflows/Tests/badge.svg + :target: https://github.com/iterative/dvcx/actions?workflow=Tests + :alt: Tests +.. |License| image:: https://img.shields.io/pypi/l/datachain + :target: https://opensource.org/licenses/Apache-2.0 + :alt: License + +AI 🔗 DataChain +---------------- + +DataChain is an open-source Python data processing library for wrangling unstructured AI data at scale. + +It enables batch LLM API calls and local language and vision AI model inferences to run in parallel over many samples as chained operations resolving to table-like datasets. These datasets can be saved, versioned, and sent directly to PyTorch and TensorFlow for training. DataChain employs rigorous `Pydantic`_ data structures, promoting better data processing practices and enabling vectorized analytical operations normally found in databases. + +The DataChain fills the gap between dataframe libraries, data warehouses, and Python-based multimodal AI applications. Our primary use cases include massive data curation, LLM analytics and validation, batch image segmentation and pose detection, GenAI data alignment, etc. + +.. code:: console + + $ pip install datachain + +Basic operation +--------------- + +DataChain is built by composing wrangling operations. + +For example, it can be instructed to read files from the cloud, map them onto a modern AI service returning a Python object, parallelize API calls, save the result as a dataset, and export a column: + +.. code:: py + + import os + import datachain as dc + + from anthropic.types.message import Message + ClaudeModel = dc.pydantic_to_feature(Message) + PROMPT = "summarize this book in less than 200 words" + service = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY")) + source = "gs://datachain-demo/mybooks/" + + chain = dc.DataChain(source) \ + .filter(File.name.glob("*.txt")) \ + .settings(parallel=4) \ + .map( \ + claude = lambda file: \ + ClaudeModel(**service.messages.create( \ + model="claude-3-haiku-20240307", \ + system=PROMPT, \ + messages=[{"role": "user", \ + "content": file.get_value()}] \ + ), \ + ).model_dump() \ + ) \ + .save("mydataset") + + dc.DataChain("mydataset").export("./", "claude.response") # export summaries + +Dataset persistence +------------------- + +In the example above, the chain resolves to a saved dataset “mydataset”. DataChain datasets are immutable and versioned. A saved dataset version can be used as a data source: + +.. code:: py + + ds = dc.DataChain("mydataset", version = 1) + +Note that DataChain represents file samples as pointers into their respective storage locations. This means a newly created dataset version does not duplicate files in storage, and storage remains the single source of truth for the original samples + +Vectorized analytics +--------------------- +Since datasets are internally represented as tables, analytical queries can be vectorized: + +.. code:: py + + rate = ds.filter(chain.response == "Success").count() / chain.count() # ?? + print(f"API class success rate: {100*rate:.2f}%") + >> 74.68% + + price_input = 0.25 + price_output = 1.25 + price=(ds.sum(C.claude.usage.input_tokens)*price_input \ + + ds.sum(C.claude.usage.output_tokens)*price_output)/1_000_000 + print(f"Cost of API calls: ${price:.2f}") + >> Cost of API calls: $1.42 + + +Importing metadata +------------------------ + +It is common for AI data to come together with metadata (annotations, classes, etc). +DataChain understands many metadata formats, and can connect data samples in storage with external metadata (e.g. CSV columns) to form a single dataset: + +.. code:: py + + from dc import parse_csv + + files = dc.DataChain("gs://datachain-demo/myimages/") + metadata = dc.DataChain("gs://datachain-demo/myimagesmetadata.csv") \ + .gen(meta=parse_csv) # TBD, also dependent on dropping file + dataset = chain1.merge(chain2, on = "file.name", right_on="name"]) + + print(dataset.select("file.name", "class", "prob").limit(5).to_pandas()) + .... + .... + .... + .... + .... + +Nested annotations (like JSON) can be unrolled into rows and columns in the way that best fits the application. For example, the MS COCO dataset includes JSON annotations detailing segmentations. To build a dataset consisting of all segmented objects in all COCO images: + +.. code:: py + + image_files = dc.DataChain("gs://datachain-demo/coco/images/") + image_meta = dc.DataChain("gs://datachain-demo/coco.json") \ + .gen(meta=parse_json, key="images") # list of images + images = image_files.merge(image_meta, on = "file.name", right_on="file_name") + objects_meta = dc.DataChain("gs://datachain-demo/coco.json") \ + .gen(meta=parse_json, key="annotations") # annotated objects + + objects = image.full_merge(objects_meta, on = "id", right_on = "image_id") + +Generating metadata +--------------------- + +A typical step in data curation is to create features from data samples for future selection. DataChain represents the newly created metadata as columns, which makes it easy to create new features and filter on them: + +.. code:: py + + from fashion_clip.fashion_clip import FashionCLIP + from sqlalchemy import JSON + from tabulate import tabulate + + from datachain.lib.param import Image + from datachain.query import C, DatasetQuery, udf + + + @udf( + params=(Image(),), + output={"fclip": JSON}, + method="fashion_clip", + batch=10, + ) + class MyFashionClip: + def __init__(self): + self.fclip = FashionCLIP("fashion-clip") + + def fashion_clip(self, inputs): + embeddings = self.fclip.encode_images( + [input[0] for input in inputs], batch_size=1 + ) + return [(json.dumps(emb),) for emb in embeddings.tolist()] + + chain = dc.DataChain("gs://datachain-demo/zalando/images/").filter( + C.name.glob("*.jpg") + ).limit(5).add_signals(MyFashionClip).save("zalando_hd_emb") + + test_image = "cs://datachain-demo/zalando/test/banner.jpg" + test_embedding = MyFashionClip.fashion_clip.encode_images(Image(test_image)) + + best_matches = chain.filter(similarity_search(test_embeding)).limit(5) + + print best_matches.to_result() + + +Delta updates +------------- + +DataChain is capable of “delta updates” – that is, batch-processing only the newly added data samples. For example, let us copy some images into a local folder and run a chain to generate captions with a locally served captioning model from HuggingFace: + +.. code:: console + + > mkdir demo-images/ + > datachain cp gs://datachain-demo/images/ /tmp/demo-images + + +.. code:: py + + import torch + + from datachain.lib.hf_image_to_text import LLaVAdescribe + from datachain.query import C, DatasetQuery + + source = "/tmp/demo-images" + + if torch.cuda.is_available(): + device = "cuda" + else: + device = "cpu" + + if __name__ == "__main__": + results = ( + DatasetQuery( + source, + anon=True, + ) + .filter(C.name.glob("*.jpg")) + .add_signals( + LLaVAdescribe( + device=device, + model=model, + ), + parallel=False, + ) + .save("annotated-images") + ) + +Now let us add few more more images to the same folder: + +.. code:: console + + > datachain cp gs://datachain-demo/extra-images/ /tmp/demo-images + +and calculate updates only for the delta: + +.. code:: py + + processed = dc.DataChain("annotated-images") + delta = dc.dataChain("/tmp/demo-images").subtract(processed) + +Passing data to training +------------------------ + +Datasets can be exported to CSV or webdataset formats. However, a much better way to pass data to training which avoids data copies and re-sharding is to wrap a DataChain dataset into a PyTorch class, and let the library take care of file downloads and caching under the hood: + +.. code:: py + + ds = dc.DataChain("gs://datachain-demo/name-labeled/images/") + .filter(C.name.glob("*.jpg")) + .map(lambda name: (name[:3],), output={"label": str}, parallel=4) + ) + + train_loader = DataLoader( + ds.to_pytorch( + ImageReader(), + LabelReader("label", classes=CLASSES), + transform=transform, + ), + batch_size=16, + parallel=2, + ) + +Tutorials +------------------ + +* `Computer Vision `_ (try in `Colab `__) +* `Multimodal `_ (try in `Colab `__) + +💻  More examples +------------------ + +* Curating images to train a custom CLIP model without re-sharding the Webdataset files +* Batch-transforming and indexing images to create a searchable merchandise catalog +* Evaluating an LLM application at scale +* Ranking the LLM retrieval strategies +* Delta updates in batch processing + +Contributions +-------------------- + +Contributions are very welcome. +To learn more, see the `Contributor Guide`_. + + +License +------- + +Distributed under the terms of the `Apache 2.0 license`_, +*DataChain* is free and open source software. + + +Issues +------ + +If you encounter any problems, +please `file an issue`_ along with a detailed description. + + +.. _Apache 2.0 license: https://opensource.org/licenses/Apache-2.0 +.. _PyPI: https://pypi.org/ +.. _file an issue: https://github.com/iterative/dvcx/issues +.. _pip: https://pip.pypa.io/ +.. github-only +.. _Contributor Guide: CONTRIBUTING.rst +.. _Pydantic: https://github.com/pydantic/pydantic diff --git a/docs/assets/datachain.png b/docs/assets/datachain.png new file mode 100644 index 0000000000000000000000000000000000000000..10fc222218900d1e13ffbbc57b58dde5db0486e7 GIT binary patch literal 2315 zcmV+m3H0`fP)8nII%-D5(ox)rphZy% zwx|?n-Rg|2I+ewRqJm{Fge7F(Lzao0V~LY91anw6zaxUf6qH9DT_2Oo=3r3Bx##4gvn&?W^p)+oSir*EP(_DqX{%> z14v{FP)H?RdX@NKn3Lty+O=!-Z`DL`aWU@36JvT{fX~+G5O0)5J_HRFRnXG(6DVX7 zK%+6hU~z!$831O6GiX$bVx6dM^8vxep|@gU+ZTCUpmOq-EeMWrvqzxh+R<=^l37guX6fy}#B$L2m zv4T{tu};Xu=R%3OzPb|b-MIq{9=9g@SoYD0XzUjX9UZw&UOWdDSginBqp@RL zCIs1MIH`iu+q%W*%d&wUTEnf|pfQ=iN}uYL^x2`JbvtuHtyb%}DoqaTn4r)w5}Ql` zgVh2U4AS`8>e!NXVS~#w&x^~WgWl&-%~NY@szD{{2Q-2Jjps@Z^x^xWQIPuewL0fH z;fw1VYra>%N-Irp#MzDSRT$*PqScgr4|3~B&*JdNEw9XkP*`a9_fyxtPFg%aCt>bX zR>KdMpx|^K^bZVyR;vXni2wsX-vug41M3nOAi=YI*CPaM*(VvfQ^wM(sHh+~aNM(F z!>03E?>>OG?ryMSQcu0NVS{!|6PwacxvCgbFK=8vKZ$M_gahgOpz`*QU>q@u)G8(D zOlDhYp+Gv^-*)5158A4mHz8rZFU*)37-U7rS+)z2=k)}FVq8hXP;PvL|Kbsu*w)$$ z0AbPUmVdZr|I_bZGO z!)C0Bi;FY7WMbdZVz!sxtZ!1|=lD0(R>8Tvd_c&!))BRM!TF1&#|jDxhKP9lw^jlk zhUF@tlIdY;6FQnjBPKPqb}HFK6nIVdK+t5yM^E)W{L8@^13lKO$?+j8oT+Fyv{L|` zl76|16MK#D%Yw(x?)}Zfj7ixnOpXnT>$u+tMK%Ly3a(kF5GS1Z>f%q&&a7Cmyjg2B zua;|dB%DTz;?EAA*VXk44ND|z1AM(*@FWVfw2PQruRyY!JO5P5k}!9)Mh1s>37}Uc z8Rl~AR|)gJy7BzIzna*7qI_k1RPbJcUJj>^2*F6ev>23%CHY0g?av3)*4E0Mrc6;g zIJ+(s4Gv&%S}odpR)BAxsNZGIoLNAz4W?ev5Ye8Bou9BU1poL}HRNU;1U1_5gv+8P zo;Y*93SMCJ0hO1Rla%DC88diPIGb}3^mvStY|;NQr>Lj{#^Cz(t0$tPqX`ridsm-Y zPS-Tl1Bp%tv#nJiD64Hg7(vqW5RUCA2hqSUK(QlNJG1Rm59j1}jiong;#RBV-F2x^ z?xG5VutjSf1!0W8uU`0;in z5GlmFJXg-qlX-<_;r}pd!g(4e!i7nKhDQTHry+7@{){Xb2c@N@_iZ1sYgVoDa1W0z zW|8qT?>u-6!`&^?DbBpuyu5RDrZnz(Wd(uGxhW05e?q--E zqb6*vxU{0;KNe3%JaBnDs@Q+$x{HuaiZ{P1rWAZPrS`zV&MJ9r*9Xmm!RY-VF z?A3>Ia{6F`j5e#}SI=~8@|L(oI%B!kphjVjtvA&T> zf0{izs32+4T>j9{58=Q+_rQ=&A-AU!lXK6UuY^hP>mrhtmNr*y7`guGmQ8d=z7Jfz z{RAF9Y!R!~@){x*(}6~C_FN~Y$go-7{F$x{sJ?m)&YjK&D<0Luv}2^^e3@SfljJws zP-JAp1`309U~zmLB{n7o5DF8dY9knJquY*31P0CwZI7Cv{M%Bvd;dNV@aPA4EGGH< zrSBfX8}QOfwsh&zm|oF9Hp{`0@9F6QJT4coSOg>zDRj2C!{ERGU=fs&M!_GOGk4zZ zty{Nh;0+mbFHB5~ri%uR31+kT0|bM3napMu4u`Y-XS6nxNw`~Fo=efmlP6o?ZDD<3 loR4KxAP^wqU1TT2{{RephXEXBV2uC(002ovPDHLkV1n2KQicEk literal 0 HcmV?d00001 diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 000000000..98579e069 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,3 @@ +# Welcome to DataChain + +- [API Reference](./references/datachain.md) diff --git a/docs/references/catalog.md b/docs/references/catalog.md new file mode 100644 index 000000000..53418ba79 --- /dev/null +++ b/docs/references/catalog.md @@ -0,0 +1,3 @@ +# `Catalog` + +::: datachain.catalog.Catalog diff --git a/docs/references/datachain.md b/docs/references/datachain.md new file mode 100644 index 000000000..850838f24 --- /dev/null +++ b/docs/references/datachain.md @@ -0,0 +1,3 @@ +# `DataChain` + +::: datachain.lib.dc.DataChain diff --git a/docs/tutorials/cv_intro.md b/docs/tutorials/cv_intro.md new file mode 100644 index 000000000..5b5edce3c --- /dev/null +++ b/docs/tutorials/cv_intro.md @@ -0,0 +1,217 @@ +# Get started for computer vision + +Learn how to use DataChain to: +- Generate a dataset from cloud storage or local directory +- Apply transformations to add metadata to a dataset +- Consume that dataset into PyTorch data loaders for model training + +## Creating datasets from files + +We will start with a collection of cat and dog images stored in s3: + +```python +from datachain.lib.dc import DataChain + +ds = DataChain("s3://dvc-public/data/dvcx/cats-and-dogs/train/*.jpg") +``` + +DataChain works lazily so as not to waste compute. To force it to run, let's convert it to a pandas dataframe: + +``` +ds.to_pandas() +``` + +DataChain will scan for matching files and create a dataset from them. The output looks like: + +``` + id vtype dir_type parent name ... +0 3 0 dogs-and-cats cat.1.jpg ... +1 5 0 dogs-and-cats cat.10.jpg ... +2 7 0 dogs-and-cats cat.100.jpg ... +3 9 0 dogs-and-cats cat.1000.jpg ... +4 11 0 dogs-and-cats cat.1001.jpg ... +.. ... ... ... ... ... +195 393 0 dogs-and-cats dog.1084.jpg ... +196 395 0 dogs-and-cats dog.1085.jpg ... +197 397 0 dogs-and-cats dog.1086.jpg ... +198 399 0 dogs-and-cats dog.1087.jpg ... +199 401 0 dogs-and-cats dog.1088.jpg ... +``` + +DataChain automatically captures these file attributes for each file. A collection of columns is called a feature, and these columns are all part of the `File` feature. We will take a look +below at how to work with these columns and the files themselves. + +## Map new feature onto the dataset + +Let's add some labels to our dataset so we can train on it. Use `ds.map()` to +add new features to the dataset: + +```python +ds = ds.map(lambda name: (name[:3],), output={"label": str}) +``` + +The first argument can be any Python function (we call this a user-defined function or +UDF) to apply to each row in the dataset. By using `name` as the input to the function, +DataChain knows to pass the value from the `name` column to the function (or you can use the +`params` argument to pass the column names explicitly). The first 3 letters of each +filename in this case represent the label (cat or dog). The UDF must return a tuple of +values, each corresponding to a column/feature in the output. + +The second argument defines the column names and types for the output. In this case, it +adds a single `label` column with string values. + +Let's check the output again: + +``` +ds.select("name", "label").to_pandas() +``` + +Now the output looks like: + +``` + name label +0 cat.1.jpg cat +1 cat.10.jpg cat +2 cat.100.jpg cat +3 cat.1000.jpg cat +4 cat.1001.jpg cat +.. ... ... +195 dog.1084.jpg dog +196 dog.1085.jpg dog +197 dog.1086.jpg dog +198 dog.1087.jpg dog +199 dog.1088.jpg dog +``` + +## Model training + +Getting a dataframe is great, but for model training, we need to: +- Read the images themselves +- Convert those images to vectors and apply other transforms to them +- Convert the labels from strings to encoded integers (like cat=0, dog=1) +- In a more realistic scenario, iterate over a large collection without killing performance or exploding memory + +`ds.to_pytorch()` creates a PyTorch +[IterableDataset](https://pytorch.org/docs/stable/data.html#torch.utils.data.IterableDataset) +that can be passed to the standard PyTorch +[DataLoader](https://pytorch.org/docs/stable/data.html#torch.utils.data.DataLoader), and +it can even apply `torchvision` +[transforms](https://pytorch.org/vision/stable/transforms.html). + +Either features or feature reader objects can be passed to `ds.to_pytorch()`. Each +reader can transform the feature values as needed: + +```python +from datachain.lib.image import ImageReader +from datachain.lib.reader import LabelReader +from torchvision.transforms import v2 +from torch.utils.data import DataLoader + +# Define transformation for data preprocessing +transform = v2.Compose( + [ + v2.ToTensor(), + v2.Resize((64, 64)), + v2.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)), + ] +) + +# Create a pytorch dataset +pytorch_ds = ds.to_pytorch( + ImageReader(), + LabelReader(feature="label", classes=["cat", "dog"]), + transform=transform, +) + +# Pass to standard pytorch dataloader +train_loader = DataLoader( + pytorch_dataset, + batch_size=16, +) + +# Train the model +train(train_loader) +``` + +
+ +Get example model code + + +To run this example, you can use this simple Pytorch model code: + +```python +import torch +from torch import nn, optim + + +# Define torch model +class CNN(nn.Module): + def __init__(self): + super().__init__() + self.conv1 = nn.Conv2d(3, 16, kernel_size=3, stride=2, padding=1) + self.conv2 = nn.Conv2d(16, 32, kernel_size=3, stride=2, padding=1) + self.conv3 = nn.Conv2d(32, 64, kernel_size=3, stride=2, padding=1) + self.fc1 = nn.Linear(64 * 8 * 8, 512) + self.fc2 = nn.Linear(512, len(CLASSES)) + + def forward(self, x): + x = torch.relu(self.conv1(x)) + x = torch.relu(self.conv2(x)) + x = torch.relu(self.conv3(x)) + x = x.view(-1, 64 * 8 * 8) + x = torch.relu(self.fc1(x)) + x = self.fc2(x) + return x + + +# Define training loop +def train(train_loader): + model = CNN() + criterion = nn.CrossEntropyLoss() + optimizer = optim.Adam(model.parameters(), lr=0.001) + + # Train the model + num_epochs = 10 + for epoch in range(num_epochs): + for i, data in enumerate(train_loader): + inputs, labels = data + optimizer.zero_grad() + + # Forward pass + outputs = model(inputs) + loss = criterion(outputs, labels) + + # Backward pass and optimize + loss.backward() + optimizer.step() + + print("[%d, %5d] loss: %.3f" % (epoch + 1, i + 1, loss.item())) + + print("Finished Training") +``` + +
+ +The only DataChain code in the above block is here: + +```python +# Create a pytorch dataset +pytorch_ds = ds.to_pytorch( + ImageReader(), + LabelReader(feature="label", classes=["cat", "dog"]), + transform=transform, +) +``` + +Let's take a closer look at what this code does. It's reading and returning pairs of +values for two different features: +- `ImageReader()` reads in each file and returns it as a [PIL + Image](https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.Image) + that can be passed to PyTorch +- `LabelReader(feature="label", classes=["cat", "dog"])` reads the `label` feature + and converts it to integer-encoded labels for the classes cat and dog + +`ds.to_pytorch()` will wrap these values into a PyTorch dataset, optimize streaming the +data to PyTorch, and apply any transforms so you don't have to wrangle your data into a +special format for training. diff --git a/docs/tutorials/udfs.md b/docs/tutorials/udfs.md new file mode 100644 index 000000000..ff4daa460 --- /dev/null +++ b/docs/tutorials/udfs.md @@ -0,0 +1,94 @@ +# Writing DataChain UDFs + +UDFs are created by applying the `@udf` decorator to functions or classes: + +``` python +@udf( + params=("name",), # Columns consumed by the UDF. + output={"path_len": Integer}, # Signals being returned by the UDF, with the signal name and type. +) +def name_len(name): + return (len(name),) +``` + +The decorator takes several parameters: + - `params` - a sequence which will be passed to the UDF as parameters + - `output` - a dictionary containing a signal name and the sqlalchemy type for the signal + - `method` - an optional parameter specifying the method of a class UDF to call + - `batch` - an optional number of batches of inputs to process with each UDF call + +## Specifying parameters + +UDF parameters refer to the columns in an index or a dataset or the actual file object in storage. +Columns are specified as `"name"`, `C.name`, `"parent"`, `C.parent`, `etc.`. + +Objects are specified as `Object(loader)` where the `loader` is a function to load the object file. +For example to load a file as a PIL image, the loader can be defined as: + +``` python +def load_image(raw): + img = Image.open(raw) + img.load() + return img +``` + +and then used as `Object(load_image)`. + +## UDF return values + +UDFs need to bundle their return values in tuples (or lists of tuples for batched UDFs). +So a UDF returning a single value should return `(value,)`, for multiple value returns - +`(value1, value2)`. + +## Batching + +In some instances it makes sense to call UDFs for sets of rows, not once for each row. +This is done by specifying the `batch=n` parameter in the `@udf` decorator. In this case +inputs are bundled in sequences of no more than `n` tuples containing values of the input +parameters. + +Batching may be useful in stateful UDFs (see below) to load models or other resources +in `__init__` method and then use them for multiple rows. + +``` python +@udf( + output=(("path_len", Integer),), + parameters=( + C.parent, + C.name, + ), + batch=10, +) +def name_len(names): + return [(len(parent + name),) for (parent, name) in names] +``` + +Batched UDF functions take a single parameter. + +## Stateful (class) UDFs + +In some cases UDFs require instantiation (e.g. to load models). In such cases it makes +sense to write the UDF as a class and decorate it with the same `@udf` decorator. + +``` python +@udf( + output=(("path_len", Integer),), + parameters=( + C.parent, + C.name, + ), + method="name_len", +) +class NameLen: + def __init__(self, multiplier=1): + self.multiplier = multiplier + + def name_len(self, parent, name): + return (len(parent + name)*self.multiplier,) +``` + +The UDF can then be passed to the `add_signals` call: + +``` python +DatasetQuery(name).add_signals(NameLen(multiplier=2)) +``` diff --git a/examples/blip2_image_desc_lib.py b/examples/blip2_image_desc_lib.py new file mode 100644 index 000000000..bd3d2f74a --- /dev/null +++ b/examples/blip2_image_desc_lib.py @@ -0,0 +1,35 @@ +# pip install torch +import torch + +from datachain.lib.hf_image_to_text import BLIP2describe +from datachain.query import C, DatasetQuery + +source = "gs://dvcx-datalakes/dogs-and-cats/" + +if torch.backends.mps.is_available(): + device = "mps" +elif torch.cuda.is_available(): + device = "cuda" +else: + device = "cpu" + + +if __name__ == "__main__": + results = ( + DatasetQuery( + source, + anon=True, + ) + .filter(C.name.glob("cat*.jpg")) + .limit(5) + .add_signals( + BLIP2describe( + # device=device, + device="cpu", + ), + parallel=False, + ) + .select("source", "parent", "name", "description", "error") + .results() + ) + print(*results, sep="\n") diff --git a/examples/clip.py b/examples/clip.py new file mode 100644 index 000000000..0503ce9cd --- /dev/null +++ b/examples/clip.py @@ -0,0 +1,50 @@ +import open_clip +import torch +from torch.nn.functional import cosine_similarity +from torch.utils.data import DataLoader + +from datachain.lib.dc import C, DataChain + +source = "gs://dvcx-50k-laion-files/000000/00000000*" + + +def create_dataset(): + imgs = ( + DataChain.from_storage(source, type="image") + .filter(C.name.glob("*.jpg")) + .map(stem=lambda name: name.split(".")[0], output=str) + ) + captions = ( + DataChain.from_storage(source, type="text") + .filter(C.name.glob("*.txt")) + .map(stem=lambda name: name.split(".")[0], output=str) + ) + return imgs.merge(captions, on="stem") + + +if __name__ == "__main__": + q = create_dataset() + + model, _, preprocess = open_clip.create_model_and_transforms( + "ViT-B-32", pretrained="laion2b_s34b_b79k" + ) + tokenizer = open_clip.get_tokenizer("ViT-B-32") + + ds = q.select("file", "right_file").to_pytorch( + transform=preprocess, + tokenizer=tokenizer, + ) + loader = DataLoader(ds, batch_size=16) + + similarity_sum = 0 + row_count = 0 + with torch.no_grad(), torch.cuda.amp.autocast(): + for image, text in loader: + image_features = model.encode_image(image) + text_features = model.encode_text(text) + similarity_sum += ( + cosine_similarity(image_features, text_features).sum().item() + ) + row_count += len(image_features) + + print("Average cosine similarity:", similarity_sum / row_count) diff --git a/examples/common_sql_functions.py b/examples/common_sql_functions.py new file mode 100644 index 000000000..45e8f8bf8 --- /dev/null +++ b/examples/common_sql_functions.py @@ -0,0 +1,78 @@ +from datachain.query import C, DatasetQuery, udf +from datachain.sql import literal +from datachain.sql.functions import array, greatest, least, path, string +from datachain.sql.types import Array, String + + +@udf( + params=(C.name,), + output={"num_chars": Array(String)}, +) +def num_chars_udf(name): + parts = name.split(".") + if len(parts) > 1: + return (list(parts[1]),) + return ([],) + + +def show(dataset): + print(*dataset.results(), sep="\n", end="\n\n") + + +ds = DatasetQuery("gs://dvcx-datalakes/dogs-and-cats/", anon=True) +show(ds.limit(5).add_signals(num_chars_udf).select(C.name, C.num_chars)) +show( + ds.limit(5).select( + C.name, + string.length(C.name), + string.split(C.name, literal(".")), + ) +) +show( + ds.limit(5).select( + C.name, + path.file_stem(C.name), + path.file_ext(C.name), + ) +) +show( + ds.limit(10) + .mutate( + a=array.length(string.split(C.parent, literal("/"))), + b=array.length(string.split(C.name, literal("0"))), + ) + .select(C.a, C.b, greatest(C.a, C.b), least(C.a, C.b)) +) + + +""" +Expected output: +('', []) +('cat.1.jpg', ['1']) +('cat.1.json', ['1']) +('cat.10.jpg', ['1', '0']) +('cat.10.json', ['1', '0']) + +('', 0, ['']) +('cat.1.jpg', 9, ['cat', '1', 'jpg']) +('cat.1.json', 10, ['cat', '1', 'json']) +('cat.10.jpg', 10, ['cat', '10', 'jpg']) +('cat.10.json', 11, ['cat', '10', 'json']) + +('', '', '') +('cat.1.jpg', 'cat.1', 'jpg') +('cat.1.json', 'cat.1', 'json') +('cat.10.jpg', 'cat.10', 'jpg') +('cat.10.json', 'cat.10', 'json') + +(3, 1, 3, 1) +(3, 1, 3, 1) +(3, 1, 3, 1) +(3, 2, 3, 2) +(3, 2, 3, 2) +(3, 3, 3, 3) +(3, 3, 3, 3) +(3, 4, 4, 3) +(3, 4, 4, 3) +(3, 3, 3, 3) +""" diff --git a/examples/computer_vision/fashion_product_images/.gitignore b/examples/computer_vision/fashion_product_images/.gitignore new file mode 100644 index 000000000..eb1e8b1c2 --- /dev/null +++ b/examples/computer_vision/fashion_product_images/.gitignore @@ -0,0 +1,4 @@ +.datachain +data +dev +test-notebooks.sh diff --git a/examples/computer_vision/fashion_product_images/1-quick-start.ipynb b/examples/computer_vision/fashion_product_images/1-quick-start.ipynb new file mode 100644 index 000000000..1e381bd94 --- /dev/null +++ b/examples/computer_vision/fashion_product_images/1-quick-start.ipynb @@ -0,0 +1,2651 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "cd9bdbec", + "metadata": {}, + "source": [ + "# 🚀 Getting Started with DataChain\n", + "\n", + "\"DataChain\n", + "\n", + "DataChain is a powerful tool for managing datasets and ML workflows. This tutorial explores how **DataChain** helps Computer Vision projects:\n", + "- 🗂️ **Manage and version datasets and annotations** effectively.\n", + "- 🔍 **Handle large-scale operations**, applying complex filters and transformations to millions of data entries.\n", + "- ⏰ **Save valuable time and resources** by avoiding redundant computations for previously processed samples.\n", + "- 🌊 **Directly stream curated data into PyTorch**, eliminating the need for intermediate resharing.\n", + "\n", + "## 📋 Agenda\n", + "\n", + "- 🖼️ Create a `fashion-product-images` dataset from an image directory\n", + "- 📂 Load the dataset\n", + "- 🔍 Explore filtering techniques\n", + " \n", + "## 🛠 Prerequisites\n", + "\n", + "Before you begin, ensure you have:\n", + "- ⚙️ DataChain installed in your environment (follow the instructions in `examples/fashion-product-images/README.md`)" + ] + }, + { + "cell_type": "markdown", + "id": "8463ea57-80a1-45a8-9611-2f43a4c84c1a", + "metadata": {}, + "source": [ + "## Imports" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "74f03f34-ae13-4289-951d-555f7d14d3f3", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2\n", + "\n", + "# Import datachain \n", + "from datachain.lib.dc import DataChain, C\n", + "\n", + "import pandas as pd" + ] + }, + { + "cell_type": "markdown", + "id": "3de7923c-5fc8-4fb9-a62f-3c6990ad4d3a", + "metadata": {}, + "source": [ + "# 🆕 Create a DataChain\n", + "\n", + "There are multiple ways of creating of DataChain and persisting it as a dataset. First, import the necessary modules and load your dataset using a `DataChain` class. \n", + "\n", + "- From cloud storages (AWS S3, GCP, Azure...) or local directory\n", + "- From previously saved dataset version\n", + "- From values \n", + "\n", + "Here are a few examples: \n", + "\n", + "```python\n", + "# from cloud storages as S3, gs or Azure: \n", + "DataChain.from_storage(\"s3://my-bucket/my-dir/\")\n", + "\n", + "# from previously saved dataset: \n", + "DataChain.from_dataset(\"name\", version=1)\n", + "\n", + "# from values: \n", + "DataChain.from_features(fib=[1, 2, 3, 5, 8])\n", + "```\n", + "\n", + "Data in DataChain is presented as Python classes with an arbitrary set of fields,\n", + "including nested classes. The data classes have to inherit from `Feature` class based on `Pydantic`\n", + "\n", + "\"Dataset\"\n", + "\n", + "**Note:** The DataChain represents file samples as pointers to their respective storage locations. This means a newly created dataset version does not duplicate files in storage, and storage remains the single source of truth for the original samples" + ] + }, + { + "cell_type": "markdown", + "id": "2b863f3d-4fe7-42a0-ac17-3f37c4e70529", + "metadata": {}, + "source": [ + "## Create a DataChain from a GCP bucket" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "34c53875-a886-41b3-9611-4c800d009d1e", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Processed: 44441 rows [00:02, 19862.67 rows/s]\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idrandomvtypedir_typeparentnameetagversionis_latestlast_modified...file.sourcefile.parentfile.namefile.sizefile.versionfile.etagfile.is_latestfile.last_modifiedfile.locationfile.vtype
0115553670851378180950fashion-product-images/images10000.jpgCPzf74/e+4YDEAE=171948965337087612024-06-27 12:00:53.421000+00:00...gs://datachain-demofashion-product-images/images10000.jpg10301719489653370876CPzf74/e+4YDEAE=11970-01-01 00:00:00+00:00None
1221275616075779383520fashion-product-images/images10001.jpgCKaGwIne+4YDEAE=171948964000643812024-06-27 12:00:40.056000+00:00...gs://datachain-demofashion-product-images/images10001.jpg12101719489640006438CKaGwIne+4YDEAE=11970-01-01 00:00:00+00:00None
2354411519685022130980fashion-product-images/images10002.jpgCKTW55fe+4YDEAE=171948967001578012024-06-27 12:01:10.067000+00:00...gs://datachain-demofashion-product-images/images10002.jpg8071719489670015780CKTW55fe+4YDEAE=11970-01-01 00:00:00+00:00None
\n", + "
" + ], + "text/plain": [ + " id random vtype dir_type parent \\\n", + "0 1 1555367085137818095 0 fashion-product-images/images \n", + "1 2 2127561607577938352 0 fashion-product-images/images \n", + "2 3 5441151968502213098 0 fashion-product-images/images \n", + "\n", + " name etag version is_latest \\\n", + "0 10000.jpg CPzf74/e+4YDEAE= 1719489653370876 1 \n", + "1 10001.jpg CKaGwIne+4YDEAE= 1719489640006438 1 \n", + "2 10002.jpg CKTW55fe+4YDEAE= 1719489670015780 1 \n", + "\n", + " last_modified ... file.source \\\n", + "0 2024-06-27 12:00:53.421000+00:00 ... gs://datachain-demo \n", + "1 2024-06-27 12:00:40.056000+00:00 ... gs://datachain-demo \n", + "2 2024-06-27 12:01:10.067000+00:00 ... gs://datachain-demo \n", + "\n", + " file.parent file.name file.size file.version \\\n", + "0 fashion-product-images/images 10000.jpg 1030 1719489653370876 \n", + "1 fashion-product-images/images 10001.jpg 1210 1719489640006438 \n", + "2 fashion-product-images/images 10002.jpg 807 1719489670015780 \n", + "\n", + " file.etag file.is_latest file.last_modified file.location \\\n", + "0 CPzf74/e+4YDEAE= 1 1970-01-01 00:00:00+00:00 None \n", + "1 CKaGwIne+4YDEAE= 1 1970-01-01 00:00:00+00:00 None \n", + "2 CKTW55fe+4YDEAE= 1 1970-01-01 00:00:00+00:00 None \n", + "\n", + " file.vtype \n", + "0 \n", + "1 \n", + "2 " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[limited by 3 objects]\n" + ] + } + ], + "source": [ + "# Create a DataChain\n", + "\n", + "ds = (\n", + " DataChain.from_storage(\"gs://datachain-demo/fashion-product-images\", type=\"image\")\n", + " .filter(C(\"file.name\").glob(\"*.jpg\"))\n", + " .save()\n", + ")\n", + "ds.show(3)" + ] + }, + { + "cell_type": "markdown", + "id": "a7fcde1f-54bf-438b-bc11-6e3ab6fda937", + "metadata": {}, + "source": [ + "## Create a DataChain from a local directory of images" + ] + }, + { + "cell_type": "markdown", + "id": "c9e60cca-2400-4676-9396-31c930b04549", + "metadata": {}, + "source": [ + "**(OPTIONAL) You may skip this and work with data in our public dataset.**\n", + "\n", + "You may create a DataChain from a directory if images are stored locally. Download data from Kaggle to follow the example. \n", + "\n", + "**Manually**\n", + "- Download the Fashion Product Images (Small) dataset from kaggle.com: [Fashion Product Images (Small) dataset from Kaggle.com](https://www.kaggle.com/datasets/paramaggarwal/fashion-product-images-small/data) dataset contributed by Param Aggarwal.\n", + "- Unzip data into the (`data`) directory in `examples/fashion-product-images`\n", + "\n", + "**Using a script below:**\n", + "1. Obtain your Kaggle credentials file (`kaggle.json`) and save it to the (`~/.kaggle`) directory so that it's available at (`~/.kaggle/kaggle.json`).\n", + "2. Download the desired dataset from Kaggle.\n", + "3. Unzip the downloaded data into the (`data`) directory." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "2306fec4-cc54-4355-9bd9-668ca6eef980", + "metadata": {}, + "outputs": [], + "source": [ + "## Prepare credentials \n", + "# !mkdir -p ~/.kaggle\n", + "# !cp kaggle.json ~/.kaggle/\n", + "# !chmod 600 ~/.kaggle/kaggle.json\n", + "\n", + "## Download data \n", + "# !pip install -q kaggle\n", + "# !kaggle datasets download -d paramaggarwal/fashion-product-images-small\n", + "\n", + "## Unzip files \n", + "# unzip fashion-product-images-small.zip \"images/*\" -d data2\n", + "# unzip fashion-product-images-small.zip \"styles.csv\" -d data2\n", + "\n", + "## (optional) Remove unnecessary redundant directory in the source data \n", + "# ![ -d \"data/myntradataset\" ] && rm -r \"data/myntradataset\" " + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "09dc9b12-9a02-40be-b158-0da7b2552b8a", + "metadata": {}, + "outputs": [], + "source": [ + "# Create a DataChain\n", + "\n", + "# DATA_PATH = \"data/images\"\n", + "\n", + "# ds = (\n", + "# DataChain.from_storage(DATA_PATH, type=\"image\")\n", + "# .filter(C(\"file.name\").glob(\"*.jpg\"))\n", + "# )" + ] + }, + { + "cell_type": "markdown", + "id": "487cc386-5c70-4662-b56e-9f6c7016fd14", + "metadata": {}, + "source": [ + "## Preview DataChain content" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "a5235e62-cba6-409f-ab3e-8ef469895b74", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idrandomvtypedir_typeparentnameetagversionis_latestlast_modified...file.sourcefile.parentfile.namefile.sizefile.versionfile.etagfile.is_latestfile.last_modifiedfile.locationfile.vtype
0115553670851378180950fashion-product-images/images10000.jpgCPzf74/e+4YDEAE=171948965337087612024-06-27 12:00:53.421000+00:00...gs://datachain-demofashion-product-images/images10000.jpg10301719489653370876CPzf74/e+4YDEAE=11970-01-01 00:00:00+00:00None
1221275616075779383520fashion-product-images/images10001.jpgCKaGwIne+4YDEAE=171948964000643812024-06-27 12:00:40.056000+00:00...gs://datachain-demofashion-product-images/images10001.jpg12101719489640006438CKaGwIne+4YDEAE=11970-01-01 00:00:00+00:00None
2354411519685022130980fashion-product-images/images10002.jpgCKTW55fe+4YDEAE=171948967001578012024-06-27 12:01:10.067000+00:00...gs://datachain-demofashion-product-images/images10002.jpg8071719489670015780CKTW55fe+4YDEAE=11970-01-01 00:00:00+00:00None
\n", + "
" + ], + "text/plain": [ + " id random vtype dir_type parent \\\n", + "0 1 1555367085137818095 0 fashion-product-images/images \n", + "1 2 2127561607577938352 0 fashion-product-images/images \n", + "2 3 5441151968502213098 0 fashion-product-images/images \n", + "\n", + " name etag version is_latest \\\n", + "0 10000.jpg CPzf74/e+4YDEAE= 1719489653370876 1 \n", + "1 10001.jpg CKaGwIne+4YDEAE= 1719489640006438 1 \n", + "2 10002.jpg CKTW55fe+4YDEAE= 1719489670015780 1 \n", + "\n", + " last_modified ... file.source \\\n", + "0 2024-06-27 12:00:53.421000+00:00 ... gs://datachain-demo \n", + "1 2024-06-27 12:00:40.056000+00:00 ... gs://datachain-demo \n", + "2 2024-06-27 12:01:10.067000+00:00 ... gs://datachain-demo \n", + "\n", + " file.parent file.name file.size file.version \\\n", + "0 fashion-product-images/images 10000.jpg 1030 1719489653370876 \n", + "1 fashion-product-images/images 10001.jpg 1210 1719489640006438 \n", + "2 fashion-product-images/images 10002.jpg 807 1719489670015780 \n", + "\n", + " file.etag file.is_latest file.last_modified file.location \\\n", + "0 CPzf74/e+4YDEAE= 1 1970-01-01 00:00:00+00:00 None \n", + "1 CKaGwIne+4YDEAE= 1 1970-01-01 00:00:00+00:00 None \n", + "2 CKTW55fe+4YDEAE= 1 1970-01-01 00:00:00+00:00 None \n", + "\n", + " file.vtype \n", + "0 \n", + "1 \n", + "2 " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[limited by 3 objects]\n" + ] + } + ], + "source": [ + "# Preview with `.show()`\n", + "\n", + "ds.show(3)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "1e083a61-fa92-4f47-8d17-4d303faf7622", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(44439, 25)\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idrandomvtypedir_typeparentnameetagversionis_latestlast_modified...file.sourcefile.parentfile.namefile.sizefile.versionfile.etagfile.is_latestfile.last_modifiedfile.locationfile.vtype
0115553670851378180950fashion-product-images/images10000.jpgCPzf74/e+4YDEAE=171948965337087612024-06-27 12:00:53.421000+00:00...gs://datachain-demofashion-product-images/images10000.jpg10301719489653370876CPzf74/e+4YDEAE=11970-01-01 00:00:00+00:00None
1221275616075779383520fashion-product-images/images10001.jpgCKaGwIne+4YDEAE=171948964000643812024-06-27 12:00:40.056000+00:00...gs://datachain-demofashion-product-images/images10001.jpg12101719489640006438CKaGwIne+4YDEAE=11970-01-01 00:00:00+00:00None
2354411519685022130980fashion-product-images/images10002.jpgCKTW55fe+4YDEAE=171948967001578012024-06-27 12:01:10.067000+00:00...gs://datachain-demofashion-product-images/images10002.jpg8071719489670015780CKTW55fe+4YDEAE=11970-01-01 00:00:00+00:00None
3438104804169419450530fashion-product-images/images10003.jpgCO/fpJ7e+4YDEAE=171948968359934312024-06-27 12:01:23.651000+00:00...gs://datachain-demofashion-product-images/images10003.jpg115641719489683599343CO/fpJ7e+4YDEAE=11970-01-01 00:00:00+00:00None
456741549482353911470fashion-product-images/images10004.jpgCMDWmrbe+4YDEAE=171948973376595212024-06-27 12:02:13.818000+00:00...gs://datachain-demofashion-product-images/images10004.jpg206471719489733765952CMDWmrbe+4YDEAE=11970-01-01 00:00:00+00:00None
\n", + "

5 rows × 25 columns

\n", + "
" + ], + "text/plain": [ + " id random vtype dir_type parent \\\n", + "0 1 1555367085137818095 0 fashion-product-images/images \n", + "1 2 2127561607577938352 0 fashion-product-images/images \n", + "2 3 5441151968502213098 0 fashion-product-images/images \n", + "3 4 3810480416941945053 0 fashion-product-images/images \n", + "4 5 674154948235391147 0 fashion-product-images/images \n", + "\n", + " name etag version is_latest \\\n", + "0 10000.jpg CPzf74/e+4YDEAE= 1719489653370876 1 \n", + "1 10001.jpg CKaGwIne+4YDEAE= 1719489640006438 1 \n", + "2 10002.jpg CKTW55fe+4YDEAE= 1719489670015780 1 \n", + "3 10003.jpg CO/fpJ7e+4YDEAE= 1719489683599343 1 \n", + "4 10004.jpg CMDWmrbe+4YDEAE= 1719489733765952 1 \n", + "\n", + " last_modified ... file.source \\\n", + "0 2024-06-27 12:00:53.421000+00:00 ... gs://datachain-demo \n", + "1 2024-06-27 12:00:40.056000+00:00 ... gs://datachain-demo \n", + "2 2024-06-27 12:01:10.067000+00:00 ... gs://datachain-demo \n", + "3 2024-06-27 12:01:23.651000+00:00 ... gs://datachain-demo \n", + "4 2024-06-27 12:02:13.818000+00:00 ... gs://datachain-demo \n", + "\n", + " file.parent file.name file.size file.version \\\n", + "0 fashion-product-images/images 10000.jpg 1030 1719489653370876 \n", + "1 fashion-product-images/images 10001.jpg 1210 1719489640006438 \n", + "2 fashion-product-images/images 10002.jpg 807 1719489670015780 \n", + "3 fashion-product-images/images 10003.jpg 11564 1719489683599343 \n", + "4 fashion-product-images/images 10004.jpg 20647 1719489733765952 \n", + "\n", + " file.etag file.is_latest file.last_modified file.location \\\n", + "0 CPzf74/e+4YDEAE= 1 1970-01-01 00:00:00+00:00 None \n", + "1 CKaGwIne+4YDEAE= 1 1970-01-01 00:00:00+00:00 None \n", + "2 CKTW55fe+4YDEAE= 1 1970-01-01 00:00:00+00:00 None \n", + "3 CO/fpJ7e+4YDEAE= 1 1970-01-01 00:00:00+00:00 None \n", + "4 CMDWmrbe+4YDEAE= 1 1970-01-01 00:00:00+00:00 None \n", + "\n", + " file.vtype \n", + "0 \n", + "1 \n", + "2 \n", + "3 \n", + "4 \n", + "\n", + "[5 rows x 25 columns]" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Preview with Pandas\n", + "\n", + "df = ds.to_pandas()\n", + "\n", + "print(df.shape)\n", + "df.head()" + ] + }, + { + "cell_type": "markdown", + "id": "db3f982f-ea1a-4ad9-8b59-9b1f586136e3", + "metadata": {}, + "source": [ + "# 🏷️ Add Metadata\n", + "\n", + "In DataChain, you can add annotations and attributes to files. In the following steps, you'll add metadata from a CSV file. Here's how you can do it:\n", + "1. Load/prepare annotations\n", + "2. Define a mapping function or UDF\n", + "3. Apply the function to generate new columns\n", + "4. Save an annotated dataset\n", + "\n", + "\"Dataset\"" + ] + }, + { + "cell_type": "markdown", + "id": "bcfc530f-dd71-404d-9b1e-d8fc8d05bbf2", + "metadata": {}, + "source": [ + "## Load metadata from CSV in GCP\n", + "\n", + "- With Datachain, you can create a chain from a single CSV, JSON, or Parquet file or parse multiple files at once\n", + "The example below shows how to parse a single CSV file of metadata using `parse_csv()` method" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "02adaac4-1bdc-4360-812e-fd46ee0b452c", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Processed: 1 rows [00:00, 1488.40 rows/s]\n", + "Processed: 1 rows [00:00, 582.06 rows/s]\n", + "Processed: 0 rows [00:00, ? rows/s]\n", + "Generated: 0 rows [00:00, ? rows/s]\u001b[A\n", + "Generated: 10001 rows [00:01, 9923.37 rows/s]\u001b[A\n", + "Generated: 20001 rows [00:01, 10477.99 rows/s]\u001b[A\n", + "Generated: 30001 rows [00:02, 10544.49 rows/s]\u001b[A\n", + "Processed: 1 rows [00:04, 4.21s/ rows]rows/s]\u001b[A\n", + "Generated: 44446 rows [00:04, 10564.21 rows/s]\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idrandomsource.file.sourcesource.file.parentsource.file.namesource.file.sizesource.file.versionsource.file.etagsource.file.is_latestsource.file.last_modified...c0gendermastercategorysubcategoryarticletypebasecolourseasonyearusageproductdisplayname
01-1558329545615551488gs://datachain-demofashion-product-imagesstyles_clean.csv46750181719830629903847COfbk67UhYcDEAE=11970-01-01 00:00:00+00:00...12904MenApparelTopwearTshirtsBlueSummer2011.0SportsNike Sahara Team India Fanwear Round Neck Jersey
12-8342757993656255458gs://datachain-demofashion-product-imagesstyles_clean.csv46750181719830629903847COfbk67UhYcDEAE=11970-01-01 00:00:00+00:00...12627MenApparelTopwearTshirtsBlueWinter2015.0SportsNike Men Blue T20 Indian Cricket Jersey
23-1778859411961393851gs://datachain-demofashion-product-imagesstyles_clean.csv46750181719830629903847COfbk67UhYcDEAE=11970-01-01 00:00:00+00:00...16357MenApparelTopwearTshirtsBlueSummer2013.0SportsNike Mean Team India Cricket Jersey
\n", + "
" + ], + "text/plain": [ + " id random source.file.source source.file.parent \\\n", + "0 1 -1558329545615551488 gs://datachain-demo fashion-product-images \n", + "1 2 -8342757993656255458 gs://datachain-demo fashion-product-images \n", + "2 3 -1778859411961393851 gs://datachain-demo fashion-product-images \n", + "\n", + " source.file.name source.file.size source.file.version source.file.etag \\\n", + "0 styles_clean.csv 4675018 1719830629903847 COfbk67UhYcDEAE= \n", + "1 styles_clean.csv 4675018 1719830629903847 COfbk67UhYcDEAE= \n", + "2 styles_clean.csv 4675018 1719830629903847 COfbk67UhYcDEAE= \n", + "\n", + " source.file.is_latest source.file.last_modified ... c0 gender \\\n", + "0 1 1970-01-01 00:00:00+00:00 ... 12904 Men \n", + "1 1 1970-01-01 00:00:00+00:00 ... 12627 Men \n", + "2 1 1970-01-01 00:00:00+00:00 ... 16357 Men \n", + "\n", + " mastercategory subcategory articletype basecolour season year usage \\\n", + "0 Apparel Topwear Tshirts Blue Summer 2011.0 Sports \n", + "1 Apparel Topwear Tshirts Blue Winter 2015.0 Sports \n", + "2 Apparel Topwear Tshirts Blue Summer 2013.0 Sports \n", + "\n", + " productdisplayname \n", + "0 Nike Sahara Team India Fanwear Round Neck Jersey \n", + "1 Nike Men Blue T20 Indian Cricket Jersey \n", + "2 Nike Mean Team India Cricket Jersey " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[limited by 3 objects]\n" + ] + } + ], + "source": [ + "# Load metadata from CSV \n", + "ds_meta = (\n", + " DataChain.from_storage(\"gs://datachain-demo/fashion-product-images/styles_clean.csv\")\n", + " .parse_csv()\n", + " .save()\n", + ")\n", + "\n", + "ds_meta.show(3)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "91cc1fab-e519-4409-a7f6-c212da4d7e33", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Processed: 44446 rows [00:01, 24529.86 rows/s]\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idrandomsource.file.sourcesource.file.parentsource.file.namesource.file.sizesource.file.versionsource.file.etagsource.file.is_latestsource.file.last_modified...gendermastercategorysubcategoryarticletypebasecolourseasonyearusageproductdisplaynamefilename
01-1558329545615551488gs://datachain-demofashion-product-imagesstyles_clean.csv46750181719830629903847COfbk67UhYcDEAE=11970-01-01 00:00:00+00:00...MenApparelTopwearTshirtsBlueSummer2011.0SportsNike Sahara Team India Fanwear Round Neck Jersey12904.jpg
12-8342757993656255458gs://datachain-demofashion-product-imagesstyles_clean.csv46750181719830629903847COfbk67UhYcDEAE=11970-01-01 00:00:00+00:00...MenApparelTopwearTshirtsBlueWinter2015.0SportsNike Men Blue T20 Indian Cricket Jersey12627.jpg
23-1778859411961393851gs://datachain-demofashion-product-imagesstyles_clean.csv46750181719830629903847COfbk67UhYcDEAE=11970-01-01 00:00:00+00:00...MenApparelTopwearTshirtsBlueSummer2013.0SportsNike Mean Team India Cricket Jersey16357.jpg
\n", + "
" + ], + "text/plain": [ + " id random source.file.source source.file.parent \\\n", + "0 1 -1558329545615551488 gs://datachain-demo fashion-product-images \n", + "1 2 -8342757993656255458 gs://datachain-demo fashion-product-images \n", + "2 3 -1778859411961393851 gs://datachain-demo fashion-product-images \n", + "\n", + " source.file.name source.file.size source.file.version source.file.etag \\\n", + "0 styles_clean.csv 4675018 1719830629903847 COfbk67UhYcDEAE= \n", + "1 styles_clean.csv 4675018 1719830629903847 COfbk67UhYcDEAE= \n", + "2 styles_clean.csv 4675018 1719830629903847 COfbk67UhYcDEAE= \n", + "\n", + " source.file.is_latest source.file.last_modified ... gender mastercategory \\\n", + "0 1 1970-01-01 00:00:00+00:00 ... Men Apparel \n", + "1 1 1970-01-01 00:00:00+00:00 ... Men Apparel \n", + "2 1 1970-01-01 00:00:00+00:00 ... Men Apparel \n", + "\n", + " subcategory articletype basecolour season year usage \\\n", + "0 Topwear Tshirts Blue Summer 2011.0 Sports \n", + "1 Topwear Tshirts Blue Winter 2015.0 Sports \n", + "2 Topwear Tshirts Blue Summer 2013.0 Sports \n", + "\n", + " productdisplayname filename \n", + "0 Nike Sahara Team India Fanwear Round Neck Jersey 12904.jpg \n", + "1 Nike Men Blue T20 Indian Cricket Jersey 12627.jpg \n", + "2 Nike Mean Team India Cricket Jersey 16357.jpg " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[limited by 3 objects]\n" + ] + } + ], + "source": [ + "# Add a \"filename\" column to map each image file to its corresponding metadata\n", + "\n", + "ds_meta = ds_meta.map(filename=lambda c0: str(c0) + '.jpg', output=str)\n", + "ds_meta.show(3)" + ] + }, + { + "cell_type": "markdown", + "id": "a68d7a64-a1ea-4a81-89f5-2ab365a32aeb", + "metadata": {}, + "source": [ + "## Load metadata from a local CSV file\n", + "\n", + "**(OPTIONAL) You may skip this and work with data in our public dataset.**\n", + "\n", + "- In this example, you load the metadata from a CSV file and prepare a annotations \n", + "- Use an image `filename` to map each image file to its corresponding metadata" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "5d113b14-42a4-4120-8ef3-e4fe42b80c50", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "# # Load Annotations from 'data/styles.csv'\n", + "\n", + "# ANNOTATIONS_PATH = \"data/styles.csv\"\n", + "\n", + "# annotations = pd.read_csv(\n", + "# ANNOTATIONS_PATH,\n", + "# usecols=[\"id\", \"gender\", \"masterCategory\", \"subCategory\", \"articleType\", \"baseColour\", \"season\", \"year\", \"usage\", \"productDisplayName\"],\n", + "# )\n", + "\n", + "# annotations.head(3)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "147ebcc6-0089-40dd-9e8b-6677ac9617a6", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "# # Preprocess columns\n", + "# annotations[\"baseColour\"] = annotations[\"baseColour\"].fillna('')\n", + "# annotations[\"season\"] = annotations[\"season\"].fillna('')\n", + "# annotations[\"usage\"] = annotations[\"usage\"].fillna('')\n", + "# annotations[\"productDisplayName\"] = annotations[\"productDisplayName\"].fillna('')\n", + "\n", + "# # Add 'filename' column for each image\n", + "# annotations[\"filename\"] = annotations[\"id\"].apply(lambda s: str(s) + \".jpg\")\n", + "# annotations = annotations.drop(\"id\", axis=1)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "ac6468d7-0a7c-4653-9cde-cc60dfd02c4a", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "# ### Create a metadata Datachain allows to generate a chain from Pandas DataFrame \n", + "\n", + "# ds_meta = DataChain.from_pandas(annotations)\n", + "# ds_meta.show(3)" + ] + }, + { + "cell_type": "markdown", + "id": "a45dfe51-064f-46d9-a347-ef2b01691a62", + "metadata": {}, + "source": [ + "## Merge the original image and metadata datachains\n", + "\n", + "- The `merge` method merges two chains based on the specified criteria\n", + "- Parameters:\n", + " - `right_ds`: Chain to join with.\n", + " - `on`: Predicate or list of Predicates to join on. If both chains have the same predicates then this predicate is enough for the join. Otherwise, `right_on` parameter has to specify the predicates for the other chain.\n", + " - `right_on`: Optional predicate or list of Predicates for the `right_ds` to join.\n", + " - `inner`: Whether to run inner join or outer join. Default is False.\n", + " - `rname`: name prefix for conflicting signal names. Default: \"{name}_right\"" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "7f164604-eb7a-4f6a-b1a4-f8270af4fe36", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Processed: 44446 rows [00:01, 25949.11 rows/s]\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idrandomvtypedir_typeparentnameetagversionis_latestlast_modified...gendermastercategorysubcategoryarticletypebasecolourseasonyearusageproductdisplaynamefilename
0115553670851378180950fashion-product-images/images10000.jpgCPzf74/e+4YDEAE=171948965337087612024-06-27 12:00:53.421000+00:00...UnisexFootwearFlip FlopsFlip FlopsNavy BlueWinter2012.0CasualDisney Unisex Kids Basic Navy Blue Flip Flops10000.jpg
1221275616075779383520fashion-product-images/images10001.jpgCKaGwIne+4YDEAE=171948964000643812024-06-27 12:00:40.056000+00:00...MenFootwearShoesCasual ShoesBlackSummer2013.0CasualClarks Men Black Leather Loafers10001.jpg
2354411519685022130980fashion-product-images/images10002.jpgCKTW55fe+4YDEAE=171948967001578012024-06-27 12:01:10.067000+00:00...UnisexAccessoriesSocksSocksWhiteSummer2012.0CasualADIDAS Unisex White Pack of 3 Socks10002.jpg
\n", + "
" + ], + "text/plain": [ + " id random vtype dir_type parent \\\n", + "0 1 1555367085137818095 0 fashion-product-images/images \n", + "1 2 2127561607577938352 0 fashion-product-images/images \n", + "2 3 5441151968502213098 0 fashion-product-images/images \n", + "\n", + " name etag version is_latest \\\n", + "0 10000.jpg CPzf74/e+4YDEAE= 1719489653370876 1 \n", + "1 10001.jpg CKaGwIne+4YDEAE= 1719489640006438 1 \n", + "2 10002.jpg CKTW55fe+4YDEAE= 1719489670015780 1 \n", + "\n", + " last_modified ... gender mastercategory subcategory \\\n", + "0 2024-06-27 12:00:53.421000+00:00 ... Unisex Footwear Flip Flops \n", + "1 2024-06-27 12:00:40.056000+00:00 ... Men Footwear Shoes \n", + "2 2024-06-27 12:01:10.067000+00:00 ... Unisex Accessories Socks \n", + "\n", + " articletype basecolour season year usage \\\n", + "0 Flip Flops Navy Blue Winter 2012.0 Casual \n", + "1 Casual Shoes Black Summer 2013.0 Casual \n", + "2 Socks White Summer 2012.0 Casual \n", + "\n", + " productdisplayname filename \n", + "0 Disney Unisex Kids Basic Navy Blue Flip Flops 10000.jpg \n", + "1 Clarks Men Black Leather Loafers 10001.jpg \n", + "2 ADIDAS Unisex White Pack of 3 Socks 10002.jpg " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[limited by 3 objects]\n" + ] + } + ], + "source": [ + "ds_annotated = ds.merge(ds_meta, on=\"name\", right_on=\"filename\")\n", + "ds_annotated.show(3)" + ] + }, + { + "cell_type": "markdown", + "id": "2ea1ec3a-7e35-4b19-a241-65fc8723770e", + "metadata": {}, + "source": [ + "# 💾 Save Dataset\n", + "\n", + "Saving datasets in DataChain allows you to:\n", + "\n", + "- Persist the dataset and its metadata for future use\n", + "- Version the dataset to track changes over time\n", + "- Share the dataset with others in your team or organization\n", + "- Easily load the dataset in other DataChain workflows or notebooks\n", + "\n", + "By saving the annotated dataset, you ensure the metadata is stored alongside the image data, making it convenient to access and use the enriched dataset in your DataChain projects.\n", + "\n", + "To save the annotated dataset in DataChain, you can use the `.save()` method on the ds_annotated dataset object. \n", + "\n", + "\"Dataset\"" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "93c328f7-5a67-4207-a9fb-c8c35d7904c0", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Processed: 44446 rows [00:01, 25216.53 rows/s]\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ds_annotated.save(\"fashion-product-images\")" + ] + }, + { + "cell_type": "markdown", + "id": "77221bd7-43aa-40a3-96b9-db09f65b5182", + "metadata": { + "execution": { + "iopub.execute_input": "2024-06-14T12:11:45.499458Z", + "iopub.status.busy": "2024-06-14T12:11:45.499239Z", + "iopub.status.idle": "2024-06-14T12:11:45.515159Z", + "shell.execute_reply": "2024-06-14T12:11:45.514534Z", + "shell.execute_reply.started": "2024-06-14T12:11:45.499444Z" + } + }, + "source": [ + "This line of code saves the `ds_annotated` dataset as a new dataset named \"fashion-product-images\" in DataChain.\n", + "\n", + "The `.save()` method takes the name of the dataset as a parameter and creates a new dataset with that name in DataChain. The saved dataset will include all the data and metadata from the original dataset, as well as the newly added metadata signals from the `ImageMetadata` UDF.\n", + "\n", + "After executing this code, you will have a new dataset named \"fashion-product-images\" in your DataChain workspace, which contains the annotated image data. You can later load this dataset using `DataChain.from_dataset(\"fashion-product-images\")` to access the annotated data in your DataChain workflows." + ] + }, + { + "cell_type": "markdown", + "id": "b439c177-646f-4f32-9d3c-8af033d1648a", + "metadata": {}, + "source": [ + "# 🔍 Explore Data" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "32a56f12-61a5-49c1-b74b-5dadf9491c37", + "metadata": {}, + "source": [ + "The dataset contains metadata about the images. We can view this metadata in two ways: \n", + "- using method `.show()` \n", + "- using the `.to_pandas()` method to review as a Pandas DataFrame " + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "002bba93-992a-43ff-8983-db58169d0238", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "# Load Image Catalog\n", + "\n", + "ds = DataChain.from_dataset(name=\"fashion-product-images\")" + ] + }, + { + "cell_type": "markdown", + "id": "4cbf10f1-6c0a-4277-b4db-bdfcecd705a1", + "metadata": { + "execution": { + "iopub.execute_input": "2024-06-04T11:37:19.946871Z", + "iopub.status.busy": "2024-06-04T11:37:19.946216Z", + "iopub.status.idle": "2024-06-04T11:37:19.976279Z", + "shell.execute_reply": "2024-06-04T11:37:19.975561Z", + "shell.execute_reply.started": "2024-06-04T11:37:19.946820Z" + } + }, + "source": [ + "This line creates a DataChain object named `ds` that refers to previously saved dataset named `fashion-product-images`." + ] + }, + { + "cell_type": "markdown", + "id": "e60a11b9-d0e9-424c-a264-29ee61a91f92", + "metadata": {}, + "source": [ + "## Use DataChain API `show()`" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "0e5915e6-7562-49c8-99aa-c002d9320388", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idrandomvtypedir_typeparentnameetagversionis_latestlast_modified...gendermastercategorysubcategoryarticletypebasecolourseasonyearusageproductdisplaynamefilename
0115553670851378180950fashion-product-images/images10000.jpgCPzf74/e+4YDEAE=171948965337087612024-06-27 12:00:53.421000+00:00...UnisexFootwearFlip FlopsFlip FlopsNavy BlueWinter2012.0CasualDisney Unisex Kids Basic Navy Blue Flip Flops10000.jpg
1221275616075779383520fashion-product-images/images10001.jpgCKaGwIne+4YDEAE=171948964000643812024-06-27 12:00:40.056000+00:00...MenFootwearShoesCasual ShoesBlackSummer2013.0CasualClarks Men Black Leather Loafers10001.jpg
2354411519685022130980fashion-product-images/images10002.jpgCKTW55fe+4YDEAE=171948967001578012024-06-27 12:01:10.067000+00:00...UnisexAccessoriesSocksSocksWhiteSummer2012.0CasualADIDAS Unisex White Pack of 3 Socks10002.jpg
\n", + "
" + ], + "text/plain": [ + " id random vtype dir_type parent \\\n", + "0 1 1555367085137818095 0 fashion-product-images/images \n", + "1 2 2127561607577938352 0 fashion-product-images/images \n", + "2 3 5441151968502213098 0 fashion-product-images/images \n", + "\n", + " name etag version is_latest \\\n", + "0 10000.jpg CPzf74/e+4YDEAE= 1719489653370876 1 \n", + "1 10001.jpg CKaGwIne+4YDEAE= 1719489640006438 1 \n", + "2 10002.jpg CKTW55fe+4YDEAE= 1719489670015780 1 \n", + "\n", + " last_modified ... gender mastercategory subcategory \\\n", + "0 2024-06-27 12:00:53.421000+00:00 ... Unisex Footwear Flip Flops \n", + "1 2024-06-27 12:00:40.056000+00:00 ... Men Footwear Shoes \n", + "2 2024-06-27 12:01:10.067000+00:00 ... Unisex Accessories Socks \n", + "\n", + " articletype basecolour season year usage \\\n", + "0 Flip Flops Navy Blue Winter 2012.0 Casual \n", + "1 Casual Shoes Black Summer 2013.0 Casual \n", + "2 Socks White Summer 2012.0 Casual \n", + "\n", + " productdisplayname filename \n", + "0 Disney Unisex Kids Basic Navy Blue Flip Flops 10000.jpg \n", + "1 Clarks Men Black Leather Loafers 10001.jpg \n", + "2 ADIDAS Unisex White Pack of 3 Socks 10002.jpg " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[limited by 3 objects]\n" + ] + } + ], + "source": [ + "ds.show(3)" + ] + }, + { + "cell_type": "markdown", + "id": "a8f7bac1-9dc3-423a-bd7c-6bdd80ee8523", + "metadata": {}, + "source": [ + "## Convert to Pandas DataFrame\n", + "\n", + "This line converts the DataChain dataset (`ds`) into a pandas DataFrame (`df`), making it easier to explore the data using familiar pandas functionalities.\n", + "- For example, review of the distribution of values in these columns" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "631cb803-695a-4312-8da4-55b854fef85b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(44439, 49)\n", + "Index(['id', 'random', 'vtype', 'dir_type', 'parent', 'name', 'etag',\n", + " 'version', 'is_latest', 'last_modified', 'size', 'owner_name',\n", + " 'owner_id', 'location', 'source', 'file.source', 'file.parent',\n", + " 'file.name', 'file.size', 'file.version', 'file.etag', 'file.is_latest',\n", + " 'file.last_modified', 'file.location', 'file.vtype', 'right_id',\n", + " 'right_random', 'source.file.source', 'source.file.parent',\n", + " 'source.file.name', 'source.file.size', 'source.file.version',\n", + " 'source.file.etag', 'source.file.is_latest',\n", + " 'source.file.last_modified', 'source.file.location',\n", + " 'source.file.vtype', 'source.index', 'c0', 'gender', 'mastercategory',\n", + " 'subcategory', 'articletype', 'basecolour', 'season', 'year', 'usage',\n", + " 'productdisplayname', 'filename'],\n", + " dtype='object')\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idrandomvtypedir_typeparentnameetagversionis_latestlast_modified...gendermastercategorysubcategoryarticletypebasecolourseasonyearusageproductdisplaynamefilename
0115553670851378180950fashion-product-images/images10000.jpgCPzf74/e+4YDEAE=171948965337087612024-06-27 12:00:53.421000+00:00...UnisexFootwearFlip FlopsFlip FlopsNavy BlueWinter2012.0CasualDisney Unisex Kids Basic Navy Blue Flip Flops10000.jpg
1221275616075779383520fashion-product-images/images10001.jpgCKaGwIne+4YDEAE=171948964000643812024-06-27 12:00:40.056000+00:00...MenFootwearShoesCasual ShoesBlackSummer2013.0CasualClarks Men Black Leather Loafers10001.jpg
2354411519685022130980fashion-product-images/images10002.jpgCKTW55fe+4YDEAE=171948967001578012024-06-27 12:01:10.067000+00:00...UnisexAccessoriesSocksSocksWhiteSummer2012.0CasualADIDAS Unisex White Pack of 3 Socks10002.jpg
\n", + "

3 rows × 49 columns

\n", + "
" + ], + "text/plain": [ + " id random vtype dir_type parent \\\n", + "0 1 1555367085137818095 0 fashion-product-images/images \n", + "1 2 2127561607577938352 0 fashion-product-images/images \n", + "2 3 5441151968502213098 0 fashion-product-images/images \n", + "\n", + " name etag version is_latest \\\n", + "0 10000.jpg CPzf74/e+4YDEAE= 1719489653370876 1 \n", + "1 10001.jpg CKaGwIne+4YDEAE= 1719489640006438 1 \n", + "2 10002.jpg CKTW55fe+4YDEAE= 1719489670015780 1 \n", + "\n", + " last_modified ... gender mastercategory subcategory \\\n", + "0 2024-06-27 12:00:53.421000+00:00 ... Unisex Footwear Flip Flops \n", + "1 2024-06-27 12:00:40.056000+00:00 ... Men Footwear Shoes \n", + "2 2024-06-27 12:01:10.067000+00:00 ... Unisex Accessories Socks \n", + "\n", + " articletype basecolour season year usage \\\n", + "0 Flip Flops Navy Blue Winter 2012.0 Casual \n", + "1 Casual Shoes Black Summer 2013.0 Casual \n", + "2 Socks White Summer 2012.0 Casual \n", + "\n", + " productdisplayname filename \n", + "0 Disney Unisex Kids Basic Navy Blue Flip Flops 10000.jpg \n", + "1 Clarks Men Black Leather Loafers 10001.jpg \n", + "2 ADIDAS Unisex White Pack of 3 Socks 10002.jpg \n", + "\n", + "[3 rows x 49 columns]" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df = ds.to_pandas()\n", + "\n", + "print(df.shape)\n", + "print(df.columns)\n", + "df.head(3)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "cada1e20-6dc5-42c8-b8fd-555fd046e473", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "mastercategory\n", + "Apparel 16026\n", + "Accessories 8374\n", + "Footwear 6907\n", + "Personal Care 1765\n", + "Free Items 80\n", + "Sporting Goods 21\n", + "Name: count, dtype: int64\n", + "subcategory\n", + "Topwear 11538\n", + "Shoes 5530\n", + "Bags 2260\n", + "Bottomwear 2008\n", + "Watches 1883\n", + "Innerwear 1364\n", + "Eyewear 820\n", + "Jewellery 792\n", + "Fragrance 739\n", + "Sandal 717\n", + "Wallets 686\n", + "Flip Flops 660\n", + "Belts 602\n", + "Socks 518\n", + "Lips 382\n", + "Dress 355\n", + "Loungewear and Nightwear 340\n", + "Saree 328\n", + "Nails 241\n", + "Makeup 232\n", + "Headwear 222\n", + "Ties 191\n", + "Accessories 100\n", + "Scarves 87\n", + "Apparel Set 83\n", + "Cufflinks 81\n", + "Free Gifts 79\n", + "Stoles 71\n", + "Skin Care 55\n", + "Skin 51\n", + "Eyes 35\n", + "Mufflers 23\n", + "Shoe Accessories 19\n", + "Sports Equipment 18\n", + "Gloves 16\n", + "Hair 13\n", + "Bath and Body 10\n", + "Water Bottle 6\n", + "Perfumes 6\n", + "Umbrellas 4\n", + "Wristbands 3\n", + "Sports Accessories 2\n", + "Beauty Accessories 2\n", + "Vouchers 1\n", + "Name: count, dtype: int64\n" + ] + } + ], + "source": [ + "print(df.mastercategory.value_counts())\n", + "print(df.subcategory.value_counts())" + ] + }, + { + "cell_type": "markdown", + "id": "53029906-c9dd-4e89-bce0-80a4c6c98bca", + "metadata": { + "execution": { + "iopub.execute_input": "2024-06-04T11:40:37.618594Z", + "iopub.status.busy": "2024-06-04T11:40:37.618110Z", + "iopub.status.idle": "2024-06-04T11:40:37.647602Z", + "shell.execute_reply": "2024-06-04T11:40:37.647056Z", + "shell.execute_reply.started": "2024-06-04T11:40:37.618566Z" + } + }, + "source": [ + "This code snippet demonstrates how to leverage DataChain to load and get a basic understanding of your dataset using `pandas`.\n", + "\n", + "**Note**: DataChain offers functionalities beyond pandas conversion. Explore the documentation for more advanced data manipulation techniques!" + ] + }, + { + "cell_type": "markdown", + "id": "7740441a-c97b-4457-ba68-1f9763d677cf", + "metadata": {}, + "source": [ + "# 🕵️‍♀️ Filtering Data\n", + "\n", + "DataChain allows you to filter the dataset based on specific conditions.\n", + "- `.filter()` method applies querying expressions to columns \n", + "- use a `C` object to refer to the dataset column by names like `C(\"NAME\")` (e.g. `C(\"mastercategory\")`)" + ] + }, + { + "cell_type": "markdown", + "id": "17e09b5a-ce01-429e-aaa5-700ea86c6f3e", + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-21T12:33:59.874849Z", + "iopub.status.busy": "2024-05-21T12:33:59.874075Z", + "iopub.status.idle": "2024-05-21T12:33:59.888412Z", + "shell.execute_reply": "2024-05-21T12:33:59.887852Z", + "shell.execute_reply.started": "2024-05-21T12:33:59.874797Z" + } + }, + "source": [ + "## Show only images with `Apparel` category" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "0957ec7f-ea03-42bf-bed5-933d21f91077", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idrandomvtypedir_typeparentnameetagversionis_latestlast_modified...gendermastercategorysubcategoryarticletypebasecolourseasonyearusageproductdisplaynamefilename
0438104804169419450530fashion-product-images/images10003.jpgCO/fpJ7e+4YDEAE=171948968359934312024-06-27 12:01:23.651000+00:00...MenApparelBottomwearTrousersKhakiSpring2013.0Smart CasualAllen Solly Men Khaki Chino Trousers10003.jpg
156741549482353911470fashion-product-images/images10004.jpgCMDWmrbe+4YDEAE=171948973376595212024-06-27 12:02:13.818000+00:00...WomenApparelTopwearKurtasMultiSummer2012.0EthnicDiva Women Multi Coloured Kurta10004.jpg
2636432616489997967630fashion-product-images/images10005.jpgCKSeprve+4YDEAE=171948974444112412024-06-27 12:02:24.500000+00:00...BoysApparelTopwearTshirtsGreyFall2011.0CasualChhota Bheem Kids Boys Lets Rock Grey T-shirt10005.jpg
\n", + "
" + ], + "text/plain": [ + " id random vtype dir_type parent \\\n", + "0 4 3810480416941945053 0 fashion-product-images/images \n", + "1 5 674154948235391147 0 fashion-product-images/images \n", + "2 6 3643261648999796763 0 fashion-product-images/images \n", + "\n", + " name etag version is_latest \\\n", + "0 10003.jpg CO/fpJ7e+4YDEAE= 1719489683599343 1 \n", + "1 10004.jpg CMDWmrbe+4YDEAE= 1719489733765952 1 \n", + "2 10005.jpg CKSeprve+4YDEAE= 1719489744441124 1 \n", + "\n", + " last_modified ... gender mastercategory subcategory \\\n", + "0 2024-06-27 12:01:23.651000+00:00 ... Men Apparel Bottomwear \n", + "1 2024-06-27 12:02:13.818000+00:00 ... Women Apparel Topwear \n", + "2 2024-06-27 12:02:24.500000+00:00 ... Boys Apparel Topwear \n", + "\n", + " articletype basecolour season year usage \\\n", + "0 Trousers Khaki Spring 2013.0 Smart Casual \n", + "1 Kurtas Multi Summer 2012.0 Ethnic \n", + "2 Tshirts Grey Fall 2011.0 Casual \n", + "\n", + " productdisplayname filename \n", + "0 Allen Solly Men Khaki Chino Trousers 10003.jpg \n", + "1 Diva Women Multi Coloured Kurta 10004.jpg \n", + "2 Chhota Bheem Kids Boys Lets Rock Grey T-shirt 10005.jpg " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[limited by 3 objects]\n" + ] + } + ], + "source": [ + "(\n", + " ds.filter(C(\"mastercategory\") == \"Apparel\").show(3)\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "id": "494a4005-dcec-4b6c-896b-4adf549179b1", + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-21T12:34:01.475478Z", + "iopub.status.busy": "2024-05-21T12:34:01.474991Z", + "iopub.status.idle": "2024-05-21T12:34:01.479369Z", + "shell.execute_reply": "2024-05-21T12:34:01.478754Z", + "shell.execute_reply.started": "2024-05-21T12:34:01.475450Z" + } + }, + "source": [ + "## Show only `Topwear` products" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "38f45264-1394-4585-a748-abdcc2233b55", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idrandomvtypedir_typeparentnameetagversionis_latestlast_modified...gendermastercategorysubcategoryarticletypebasecolourseasonyearusageproductdisplaynamefilename
056741549482353911470fashion-product-images/images10004.jpgCMDWmrbe+4YDEAE=171948973376595212024-06-27 12:02:13.818000+00:00...WomenApparelTopwearKurtasMultiSummer2012.0EthnicDiva Women Multi Coloured Kurta10004.jpg
1636432616489997967630fashion-product-images/images10005.jpgCKSeprve+4YDEAE=171948974444112412024-06-27 12:02:24.500000+00:00...BoysApparelTopwearTshirtsGreyFall2011.0CasualChhota Bheem Kids Boys Lets Rock Grey T-shirt10005.jpg
2848081679847881170fashion-product-images/images10007.jpgCMK/06be+4YDEAE=171948970114246612024-06-27 12:01:41.201000+00:00...MenApparelTopwearTshirtsBlackSummer2011.0SportsADIDAS Men's Pune Warriors Graphic Black T-shirt10007.jpg
\n", + "
" + ], + "text/plain": [ + " id random vtype dir_type parent \\\n", + "0 5 674154948235391147 0 fashion-product-images/images \n", + "1 6 3643261648999796763 0 fashion-product-images/images \n", + "2 8 4808167984788117 0 fashion-product-images/images \n", + "\n", + " name etag version is_latest \\\n", + "0 10004.jpg CMDWmrbe+4YDEAE= 1719489733765952 1 \n", + "1 10005.jpg CKSeprve+4YDEAE= 1719489744441124 1 \n", + "2 10007.jpg CMK/06be+4YDEAE= 1719489701142466 1 \n", + "\n", + " last_modified ... gender mastercategory subcategory \\\n", + "0 2024-06-27 12:02:13.818000+00:00 ... Women Apparel Topwear \n", + "1 2024-06-27 12:02:24.500000+00:00 ... Boys Apparel Topwear \n", + "2 2024-06-27 12:01:41.201000+00:00 ... Men Apparel Topwear \n", + "\n", + " articletype basecolour season year usage \\\n", + "0 Kurtas Multi Summer 2012.0 Ethnic \n", + "1 Tshirts Grey Fall 2011.0 Casual \n", + "2 Tshirts Black Summer 2011.0 Sports \n", + "\n", + " productdisplayname filename \n", + "0 Diva Women Multi Coloured Kurta 10004.jpg \n", + "1 Chhota Bheem Kids Boys Lets Rock Grey T-shirt 10005.jpg \n", + "2 ADIDAS Men's Pune Warriors Graphic Black T-shirt 10007.jpg " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[limited by 3 objects]\n" + ] + } + ], + "source": [ + "(\n", + " ds.filter(C(\"subcategory\") == \"Topwear\").show(3)\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "647a0473-dc40-4afe-8359-f6663cc68c9e", + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-09T10:22:13.226793Z", + "iopub.status.busy": "2024-05-09T10:22:13.226290Z", + "iopub.status.idle": "2024-05-09T10:22:13.231593Z", + "shell.execute_reply": "2024-05-09T10:22:13.230415Z", + "shell.execute_reply.started": "2024-05-09T10:22:13.226764Z" + } + }, + "source": [ + "## Chain multiple filters together\n", + "\n", + "Show only 'Topwear' apparel products for a 'Summer' season" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "35f0a631-5081-4616-813c-c258ed397b6b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(6612, 49)" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "(\n", + " ds\n", + " .filter(C(\"mastercategory\") == \"Apparel\")\n", + " .filter(C(\"subcategory\") == \"Topwear\")\n", + " .filter(C(\"season\") == \"Summer\")\n", + " .to_pandas().shape\n", + " # .show(3)\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "6c0b99fa-8820-405b-b2fd-70527992c3c3", + "metadata": { + "execution": { + "iopub.execute_input": "2024-06-26T12:44:11.299947Z", + "iopub.status.busy": "2024-06-26T12:44:11.299464Z", + "iopub.status.idle": "2024-06-26T12:44:11.325798Z", + "shell.execute_reply": "2024-06-26T12:44:11.325135Z", + "shell.execute_reply.started": "2024-06-26T12:44:11.299918Z" + } + }, + "source": [ + "You may use one line filter with multiple expressions joined with logical operators like `&` (AND) and `|` (OR)" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "9b0f8a23-3de8-4ba4-a986-8a7a08bb3342", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(6612, 49)" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "(\n", + " ds\n", + " .filter((C(\"mastercategory\") == \"Apparel\") & (C(\"subcategory\") == \"Topwear\") & (C(\"season\") == \"Summer\"))\n", + " .to_pandas().shape\n", + " # .show(3)\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "ae12c167-b9d7-4751-9c36-4d8eb45a5a1a", + "metadata": {}, + "source": [ + "## Save Dataset\n", + "\n", + "Let's save \"fashion-topwear\" to make it version and reusable" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "2d782294-28fd-4b96-9f8a-91fe6dfa35cc", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(11538, 49)" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "(\n", + " DataChain(name=\"fashion-product-images\")\n", + " .filter(C(\"mastercategory\") == \"Apparel\")\n", + " .filter(C(\"subcategory\") == \"Topwear\")\n", + " .save(\"fashion-topwear\")\n", + " .to_pandas().shape\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "eb74d319-10db-4fde-980b-5d6bfd7560e3", + "metadata": {}, + "source": [ + "# ☁️ Run in Studio (SaaS)\n", + "\n", + "\n", + " \"DataChain\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "530dd9f7-7dc8-4365-b93b-6d7bd18c5e8b", + "metadata": {}, + "source": [ + "To run these examples in Studio, follow the quide\n", + "\n", + "1. Open Studio / YOUR_TEAM / `datasets` workspace\n", + "2. Create a new Python Script\n", + "3. Copy/past a script from `scripts/1-quick-start.py`\n", + "4. Click the Run button\n" + ] + }, + { + "cell_type": "markdown", + "id": "94fd11d6", + "metadata": {}, + "source": [ + "# 🎉 Summary \n", + "\n", + "👏 **Congratulations on completing this tutorial! You're a DataChain superstar! 🌟** You've taken the first steps in harnessing the power of DataChain for your computer vision projects. In this tutorial, we covered:\n", + "- Creating the `fashion-product-images` dataset from existing images\n", + "- Filtering the dataset based on specific conditions\n", + "- Essential DataChain methods:\n", + " - `.show()` for displaying dataset samples\n", + " - `.to_pandas()` for converting datasets to Pandas DataFrames\n", + " - `.filter()` for applying custom filters to datasets\n", + " - `.gen()` for generated metadata\n", + " - `.merge()` for attaching metadata to images\n", + "\n", + "But this is just the beginning! DataChain offers many features for streamlining your ML workflows, including data transformations, versioning, and much more. 🚀\n", + "\n", + "## What's Next?\n", + "\n", + "Excited to learn more? Check out the next parts of our tutorial series:\n", + "- 📂 Saving and Versioning Datasets \n", + "- 🧩 Splitting Datasets for Training, Validation, and Testing\n", + "- 🎨 Generating and Managing Embeddings\n", + "- 🔍 Performing Similarity Search\n", + "- 🧹 Finding and Removing Redundant Images\n", + "- 🧠 Training Models\n", + "- 🔮 Running Inference and Saving Predictions\n", + "- 📊 Analyzing Predictions\n", + "\n", + "By mastering these techniques, you'll be well on your way to building powerful and efficient computer vision pipelines with DataChain.\n", + "\n", + "## 🤝 Get Involved\n", + "\n", + "We'd love to have you join our growing community of DataChain users and contributors! Here's how you can get involved:\n", + "\n", + "- ⭐ Give us a star on [GitHub](https://github.com/iterative/dvcx) to show your support\n", + "- 🌐 Visit the [dvc.ai website](https://dvc.ai/) to learn more about our products and services\n", + "- 📞 Contact us to discuss on scaling 🚀 DataChain for your project!\n", + "- 🙌 Follow us on [LinkedIn](https://www.linkedin.com/company/dvc-ai/) and [Twitter](https://x.com/DVCorg) for the latest updates and insights\n", + "\n", + "Thanks for choosing DataChain, and happy coding! 😄" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/computer_vision/fashion_product_images/2-working-with-image-datachains.ipynb b/examples/computer_vision/fashion_product_images/2-working-with-image-datachains.ipynb new file mode 100644 index 000000000..3b4496447 --- /dev/null +++ b/examples/computer_vision/fashion_product_images/2-working-with-image-datachains.ipynb @@ -0,0 +1,3589 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d47ef272-a467-451d-8e34-a1b5fbb29d49", + "metadata": {}, + "source": [ + "\"Dataset\"\n", + "\n", + "# 🖼 Working with Image DataChains\n", + "\n", + "Welcome to our second tutorial, in which we dive into managing and optimizing image datasets with DataChain! Enhance your skills in handling complex image data, from filtering to minimizing redundancy.\n", + "\n", + "## 📋 Agenda\n", + "1. **Filtering & Sorting** - Refine your datasets to get exactly what you need.\n", + "2. **Adding Annotations (Signals)** - Enrich your data with meaningful attributes.\n", + "3. **Creating and Versioning Datasets** - Manage changes and maintain historical versions of your datasets.\n", + "4. **Similarity Search** - Discover and analyze similar images within your dataset.\n", + "5. **Minimizing Redundant Images** - Optimize your storage and processing by reducing duplicates.\n", + "\n", + "## 🛠 Prerequisites\n", + "Before diving in, make sure you’re set up:\n", + "- **DataChain Installation:** Ensure DataChain is up and running in your environment.\n", + "- **Dataset Preparation:** The `fashion-product-images` should already be created. If this is your first time, please start with `1-quick-start.ipynb` to get up to speed.\n", + "\n", + "This tutorial is designed to be straightforward and practical, allowing you to apply these concepts directly to your projects. Let’s optimize how you manage image datasets and push the boundaries of what you can achieve with DataChain!" + ] + }, + { + "cell_type": "markdown", + "id": "f93e0212-b46b-41a8-b5b7-07513db1a413", + "metadata": {}, + "source": [ + "# 📚 Core Concepts & Execution Workflow\n", + "\n", + "**AI 🔗 DataChain - a data structure for batch data processing and evaluation**\n", + "- It represents a sequence of data manipulation steps such as reading data from\n", + "storages, running AI or LLM models or calling external services API to validate or\n", + "enrich data.\n", + "\n", + "\"Dataset\"\n", + "\n", + "| Concept | Description |\n", + "|--------------------------|-------------|\n", + "| **DataChain SaaS / Studio** | Highly performant Unstructured Intelligence Platform for enterprises to automate ETL pipelines and boost collaboration and scalability. Provides UI to manage datasets, edit scripts, and execute compute jobs. |\n", + "| **DataChain Library** | DataChain Library provides a data frame-like interface that can automatically reference data stored as files (text, images, video) locally or in the cloud. |\n", + "| **DataChain** | DataChain object is a data structure for batch data processing and evaluation.
  • **Object** represents a file in a simple case but can be a virtual “file” or a part of a file.
  • **Signal** or **Column** is an object’s attribute.
  • **Row** is a set of signals for a given object.
  • **Dataset version** is an immutable set of objects with signals / columns
  • **Dataset operations**: updating dataset and querying dataset.
|\n", + "| **Query** | Query is a way to get a dataset from another dataset as a subset of its objects and signals. Query can create new signals but only trivial one that can be computed in DB. |\n", + "| **Processor** | The processor creates a dataset from another dataset by extending the set of signals or generating new objects. |\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "74f03f34-ae13-4289-951d-555f7d14d3f3", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2\n", + "\n", + "import os\n", + "import numpy as np\n", + "\n", + "from datachain.lib.dc import DataChain, C\n", + "\n", + "from src.clustering import select_diverse_elements" + ] + }, + { + "cell_type": "markdown", + "id": "b439c177-646f-4f32-9d3c-8af033d1648a", + "metadata": {}, + "source": [ + "# 🔍 Basic Operations: Selecting, Filtering, Ordering, Grouping\n", + "\n", + "\"Dataset\"\n", + "\n", + "Explore essential operations to manipulate and manage your datasets effectively. This section will guide you through the fundamental techniques of selecting, filtering, ordering, grouping, annotating, and version management.\n", + "\n", + "📋 Key Operations\n", + "1. Connecting to a Dataset\n", + "2. Filtering & Sorting\n", + "3. Adding Annotations (Signals)\n", + "4. Saving and Versioning\n", + "5. Deleting Dataset Versions" + ] + }, + { + "cell_type": "markdown", + "id": "38fb449c-5842-4c2c-99d8-1ebd23595ea8", + "metadata": {}, + "source": [ + "## Create a DataChain" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "002bba93-992a-43ff-8983-db58169d0238", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idrandomvtypedir_typeparentnameetagversionis_latestlast_modified...gendermastercategorysubcategoryarticletypebasecolourseasonyearusageproductdisplaynamefilename
0115553670851378180950fashion-product-images/images10000.jpgCPzf74/e+4YDEAE=171948965337087612024-06-27 12:00:53.421000+00:00...UnisexFootwearFlip FlopsFlip FlopsNavy BlueWinter2012.0CasualDisney Unisex Kids Basic Navy Blue Flip Flops10000.jpg
1221275616075779383520fashion-product-images/images10001.jpgCKaGwIne+4YDEAE=171948964000643812024-06-27 12:00:40.056000+00:00...MenFootwearShoesCasual ShoesBlackSummer2013.0CasualClarks Men Black Leather Loafers10001.jpg
2354411519685022130980fashion-product-images/images10002.jpgCKTW55fe+4YDEAE=171948967001578012024-06-27 12:01:10.067000+00:00...UnisexAccessoriesSocksSocksWhiteSummer2012.0CasualADIDAS Unisex White Pack of 3 Socks10002.jpg
\n", + "
" + ], + "text/plain": [ + " id random vtype dir_type parent \\\n", + "0 1 1555367085137818095 0 fashion-product-images/images \n", + "1 2 2127561607577938352 0 fashion-product-images/images \n", + "2 3 5441151968502213098 0 fashion-product-images/images \n", + "\n", + " name etag version is_latest \\\n", + "0 10000.jpg CPzf74/e+4YDEAE= 1719489653370876 1 \n", + "1 10001.jpg CKaGwIne+4YDEAE= 1719489640006438 1 \n", + "2 10002.jpg CKTW55fe+4YDEAE= 1719489670015780 1 \n", + "\n", + " last_modified ... gender mastercategory subcategory \\\n", + "0 2024-06-27 12:00:53.421000+00:00 ... Unisex Footwear Flip Flops \n", + "1 2024-06-27 12:00:40.056000+00:00 ... Men Footwear Shoes \n", + "2 2024-06-27 12:01:10.067000+00:00 ... Unisex Accessories Socks \n", + "\n", + " articletype basecolour season year usage \\\n", + "0 Flip Flops Navy Blue Winter 2012.0 Casual \n", + "1 Casual Shoes Black Summer 2013.0 Casual \n", + "2 Socks White Summer 2012.0 Casual \n", + "\n", + " productdisplayname filename \n", + "0 Disney Unisex Kids Basic Navy Blue Flip Flops 10000.jpg \n", + "1 Clarks Men Black Leather Loafers 10001.jpg \n", + "2 ADIDAS Unisex White Pack of 3 Socks 10002.jpg " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[limited by 3 objects]\n" + ] + } + ], + "source": [ + "# Create a DataChain from previously save dataset\n", + "\n", + "ds = (\n", + " DataChain.from_dataset(\"fashion-product-images\")\n", + ")\n", + "\n", + "ds.show(3)" + ] + }, + { + "cell_type": "markdown", + "id": "1c920bf9-e3fa-4b40-8c45-b53f8105ecbd", + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-23T18:23:37.839668Z", + "iopub.status.busy": "2024-05-23T18:23:37.839204Z", + "iopub.status.idle": "2024-05-23T18:23:37.842849Z", + "shell.execute_reply": "2024-05-23T18:23:37.842299Z", + "shell.execute_reply.started": "2024-05-23T18:23:37.839639Z" + }, + "scrolled": true + }, + "source": [ + "## Filtering & Sorting" + ] + }, + { + "cell_type": "markdown", + "id": "3dc2a69c-ddbb-4e99-8bd8-182aa4463d32", + "metadata": {}, + "source": [ + "Assume you want to track the evolution of product images over time, or to compare the front-facing images for different products. \n", + "\n", + "- Select specific columns: `gender`, `masterCategory`, `subCategory`, `articleType`, `baseColour`, `season`, `year`, `usage`, `productDisplayName`\n", + "- Filter to keep only `Topwear` products for a `Summer` season\n", + "- Sort the data by `year` in ascending order: `order_by(\"year)`\n", + "- Group the data by `gender`: `group_by('gender')`\n", + "- Convert the grouped data to a Pandas DataFrame: `to_pandas()`\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "e8f1d24b-55b0-4394-848d-89b7ec8332a7", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
parentnameusageseasonyeargender
0fashion-product-images/images10005.jpgCasualFall2011.0Boys
1fashion-product-images/images10009.jpgCasualSummer2011.0Women
2fashion-product-images/images10054.jpgCasualSummer2012.0Girls
3fashion-product-images/images10000.jpgCasualWinter2012.0Unisex
4fashion-product-images/images10001.jpgCasualSummer2013.0Men
\n", + "
" + ], + "text/plain": [ + " parent name usage season year gender\n", + "0 fashion-product-images/images 10005.jpg Casual Fall 2011.0 Boys\n", + "1 fashion-product-images/images 10009.jpg Casual Summer 2011.0 Women\n", + "2 fashion-product-images/images 10054.jpg Casual Summer 2012.0 Girls\n", + "3 fashion-product-images/images 10000.jpg Casual Winter 2012.0 Unisex\n", + "4 fashion-product-images/images 10001.jpg Casual Summer 2013.0 Men" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "(\n", + " DataChain(name=\"fashion-product-images\")\n", + " .select(\"parent\", \"name\", \"usage\",\"season\", \"year\", \"gender\",)\n", + " .filter(C(\"usage\") == \"Casual\" and C(\"season\") == \"Summer\")\n", + " .order_by(\"year\")\n", + " .group_by(\"gender\")\n", + " .to_pandas()\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "id": "6828302f-21b7-48fc-b484-9b58fc4d6e8e", + "metadata": {}, + "source": [ + "The example demonstrates how to leverage basic DataChain operations to process and extract meaningful insights from a dataset.\n", + "\n", + "- **Select Fields**: The `.select()` method specifies which columns to include in the output. Here, it selects columns like `parent`, `name`, `usage`, `season`, `year`, and `gender`.\n", + " \n", + "- **Apply Filters**: The `.filter()` method refines the dataset to include only records where both `usage` is 'Casual' and `season` is 'Summer'. This focuses the analysis on casual clothing items used during the summer.\n", + "- **Ordering**: The `.order_by(\"year\")` method sorts the data chronologically by the `year` column, organizing the entries by their temporal context.\n", + "- **Grouping**: The `.group_by('gender')` method groups the data by the `gender` column. This aggregation is crucial for analyzing trends and differences in clothing preferences across genders.\n", + "- **Convert to DataFrame**: Finally, the `.to_pandas()` function converts the dataset into a Pandas DataFrame. This transformation facilitates further analysis with Python's extensive data manipulation tools.\n", + "\n", + "This workflow efficiently handles data extraction and preprocessing, essential for machine learning tasks, allowing quick exploration of images and associated metadata in structured fashion workflows." + ] + }, + { + "cell_type": "markdown", + "id": "3f40b688-cd6e-4805-9e38-1c224751256a", + "metadata": {}, + "source": [ + "## Add signals (columns) with `map()` method\n", + "\n", + "- The `map()` method applies a function to each row to create new signals. It returns a chain itself with new signals.\n", + "- The mapping function should return a new object for each row.\n", + "\n", + "An example below demonstrates how to create `prod_name_lengh` column using `.map()` and a lambda function applied to the `name` column" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "df95dd93-3db6-4d11-81df-bd282a6b7a9e", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Processed: 44439 rows [00:00, 44576.68 rows/s]\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idrandomvtypedir_typeparentnameetagversionis_latestlast_modified...mastercategorysubcategoryarticletypebasecolourseasonyearusageproductdisplaynamefilenameprod_name_length
0115553670851378180950fashion-product-images/images10000.jpgCPzf74/e+4YDEAE=171948965337087612024-06-27 12:00:53.421000+00:00...FootwearFlip FlopsFlip FlopsNavy BlueWinter2012.0CasualDisney Unisex Kids Basic Navy Blue Flip Flops10000.jpg9
1221275616075779383520fashion-product-images/images10001.jpgCKaGwIne+4YDEAE=171948964000643812024-06-27 12:00:40.056000+00:00...FootwearShoesCasual ShoesBlackSummer2013.0CasualClarks Men Black Leather Loafers10001.jpg9
2354411519685022130980fashion-product-images/images10002.jpgCKTW55fe+4YDEAE=171948967001578012024-06-27 12:01:10.067000+00:00...AccessoriesSocksSocksWhiteSummer2012.0CasualADIDAS Unisex White Pack of 3 Socks10002.jpg9
\n", + "
" + ], + "text/plain": [ + " id random vtype dir_type parent \\\n", + "0 1 1555367085137818095 0 fashion-product-images/images \n", + "1 2 2127561607577938352 0 fashion-product-images/images \n", + "2 3 5441151968502213098 0 fashion-product-images/images \n", + "\n", + " name etag version is_latest \\\n", + "0 10000.jpg CPzf74/e+4YDEAE= 1719489653370876 1 \n", + "1 10001.jpg CKaGwIne+4YDEAE= 1719489640006438 1 \n", + "2 10002.jpg CKTW55fe+4YDEAE= 1719489670015780 1 \n", + "\n", + " last_modified ... mastercategory subcategory \\\n", + "0 2024-06-27 12:00:53.421000+00:00 ... Footwear Flip Flops \n", + "1 2024-06-27 12:00:40.056000+00:00 ... Footwear Shoes \n", + "2 2024-06-27 12:01:10.067000+00:00 ... Accessories Socks \n", + "\n", + " articletype basecolour season year usage \\\n", + "0 Flip Flops Navy Blue Winter 2012.0 Casual \n", + "1 Casual Shoes Black Summer 2013.0 Casual \n", + "2 Socks White Summer 2012.0 Casual \n", + "\n", + " productdisplayname filename prod_name_length \n", + "0 Disney Unisex Kids Basic Navy Blue Flip Flops 10000.jpg 9 \n", + "1 Clarks Men Black Leather Loafers 10001.jpg 9 \n", + "2 ADIDAS Unisex White Pack of 3 Socks 10002.jpg 9 " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[limited by 3 objects]\n" + ] + } + ], + "source": [ + "# Add signals (columns) with map() method\n", + "\n", + "(\n", + " DataChain.from_dataset(\"fashion-product-images\")\n", + " .map(prod_name_length=lambda name: len(name), output=int)\n", + " .show(3)\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "id": "0e49e1ef-a179-401e-b141-0540d322b144", + "metadata": {}, + "source": [ + "## Saving and Versioning Datasets\n", + "\n", + "DataChain supports versioning of datasets. You can save a dataset as a new version and load specific versions:\n", + "\n", + "You can load a specific version by specifying the `version` parameter." + ] + }, + { + "cell_type": "markdown", + "id": "22a6a7a2-7e67-4f16-be89-213f119c13b1", + "metadata": {}, + "source": [ + "### Save a dataset (version)\n", + "\n", + "- Let's add a column and save a new version of the dataset" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "adb5beda-4a5d-4bad-b5a5-6e4fe9cbc8a4", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Processed: 11538 rows [00:00, 34872.74 rows/s]\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Save a dataset (version)\n", + "\n", + "(\n", + " DataChain.from_dataset(name=\"fashion-topwear\")\n", + " .map(prod_name_length=lambda name: len(name), output=int)\n", + " .save(\"fashion-tmp\")\n", + ")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "83d95b66-f8c0-4886-998a-95236b8bd236", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idrandomvtypedir_typeparentnameetagversionis_latestlast_modified...mastercategorysubcategoryarticletypebasecolourseasonyearusageproductdisplaynamefilenameprod_name_length
016741549482353911470fashion-product-images/images10004.jpgCMDWmrbe+4YDEAE=171948973376595212024-06-27 12:02:13.818000+00:00...ApparelTopwearKurtasMultiSummer2012.0EthnicDiva Women Multi Coloured Kurta10004.jpg9
1236432616489997967630fashion-product-images/images10005.jpgCKSeprve+4YDEAE=171948974444112412024-06-27 12:02:24.500000+00:00...ApparelTopwearTshirtsGreyFall2011.0CasualChhota Bheem Kids Boys Lets Rock Grey T-shirt10005.jpg9
2348081679847881170fashion-product-images/images10007.jpgCMK/06be+4YDEAE=171948970114246612024-06-27 12:01:41.201000+00:00...ApparelTopwearTshirtsBlackSummer2011.0SportsADIDAS Men's Pune Warriors Graphic Black T-shirt10007.jpg9
\n", + "
" + ], + "text/plain": [ + " id random vtype dir_type parent \\\n", + "0 1 674154948235391147 0 fashion-product-images/images \n", + "1 2 3643261648999796763 0 fashion-product-images/images \n", + "2 3 4808167984788117 0 fashion-product-images/images \n", + "\n", + " name etag version is_latest \\\n", + "0 10004.jpg CMDWmrbe+4YDEAE= 1719489733765952 1 \n", + "1 10005.jpg CKSeprve+4YDEAE= 1719489744441124 1 \n", + "2 10007.jpg CMK/06be+4YDEAE= 1719489701142466 1 \n", + "\n", + " last_modified ... mastercategory subcategory \\\n", + "0 2024-06-27 12:02:13.818000+00:00 ... Apparel Topwear \n", + "1 2024-06-27 12:02:24.500000+00:00 ... Apparel Topwear \n", + "2 2024-06-27 12:01:41.201000+00:00 ... Apparel Topwear \n", + "\n", + " articletype basecolour season year usage \\\n", + "0 Kurtas Multi Summer 2012.0 Ethnic \n", + "1 Tshirts Grey Fall 2011.0 Casual \n", + "2 Tshirts Black Summer 2011.0 Sports \n", + "\n", + " productdisplayname filename \\\n", + "0 Diva Women Multi Coloured Kurta 10004.jpg \n", + "1 Chhota Bheem Kids Boys Lets Rock Grey T-shirt 10005.jpg \n", + "2 ADIDAS Men's Pune Warriors Graphic Black T-shirt 10007.jpg \n", + "\n", + " prod_name_length \n", + "0 9 \n", + "1 9 \n", + "2 9 " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[limited by 3 objects]\n" + ] + } + ], + "source": [ + "# Load dataset\n", + "\n", + "DataChain.from_dataset(\"fashion-tmp\").show(3)" + ] + }, + { + "cell_type": "markdown", + "id": "7150c58a-dd3f-4e1c-a1cf-c496b65c67f2", + "metadata": {}, + "source": [ + "### Save a new version " + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "93638895-85df-4252-beb8-1e5e71b269c8", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Processed: 11538 rows [00:00, 45202.92 rows/s]\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Save a new version (with \"prod_name_length_2\" column)\n", + "(\n", + " DataChain(name=\"fashion-topwear\")\n", + " .map(prod_name_length_2=lambda name: len(name), output=int)\n", + " .save(\"fashion-tmp\")\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "558525be-bea3-44ff-86e5-3895c65deaf7", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idrandomvtypedir_typeparentnameetagversionis_latestlast_modified...mastercategorysubcategoryarticletypebasecolourseasonyearusageproductdisplaynamefilenameprod_name_length_2
016741549482353911470fashion-product-images/images10004.jpgCMDWmrbe+4YDEAE=171948973376595212024-06-27 12:02:13.818000+00:00...ApparelTopwearKurtasMultiSummer2012.0EthnicDiva Women Multi Coloured Kurta10004.jpg9
1236432616489997967630fashion-product-images/images10005.jpgCKSeprve+4YDEAE=171948974444112412024-06-27 12:02:24.500000+00:00...ApparelTopwearTshirtsGreyFall2011.0CasualChhota Bheem Kids Boys Lets Rock Grey T-shirt10005.jpg9
2348081679847881170fashion-product-images/images10007.jpgCMK/06be+4YDEAE=171948970114246612024-06-27 12:01:41.201000+00:00...ApparelTopwearTshirtsBlackSummer2011.0SportsADIDAS Men's Pune Warriors Graphic Black T-shirt10007.jpg9
\n", + "
" + ], + "text/plain": [ + " id random vtype dir_type parent \\\n", + "0 1 674154948235391147 0 fashion-product-images/images \n", + "1 2 3643261648999796763 0 fashion-product-images/images \n", + "2 3 4808167984788117 0 fashion-product-images/images \n", + "\n", + " name etag version is_latest \\\n", + "0 10004.jpg CMDWmrbe+4YDEAE= 1719489733765952 1 \n", + "1 10005.jpg CKSeprve+4YDEAE= 1719489744441124 1 \n", + "2 10007.jpg CMK/06be+4YDEAE= 1719489701142466 1 \n", + "\n", + " last_modified ... mastercategory subcategory \\\n", + "0 2024-06-27 12:02:13.818000+00:00 ... Apparel Topwear \n", + "1 2024-06-27 12:02:24.500000+00:00 ... Apparel Topwear \n", + "2 2024-06-27 12:01:41.201000+00:00 ... Apparel Topwear \n", + "\n", + " articletype basecolour season year usage \\\n", + "0 Kurtas Multi Summer 2012.0 Ethnic \n", + "1 Tshirts Grey Fall 2011.0 Casual \n", + "2 Tshirts Black Summer 2011.0 Sports \n", + "\n", + " productdisplayname filename \\\n", + "0 Diva Women Multi Coloured Kurta 10004.jpg \n", + "1 Chhota Bheem Kids Boys Lets Rock Grey T-shirt 10005.jpg \n", + "2 ADIDAS Men's Pune Warriors Graphic Black T-shirt 10007.jpg \n", + "\n", + " prod_name_length_2 \n", + "0 9 \n", + "1 9 \n", + "2 9 " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[limited by 3 objects]\n" + ] + } + ], + "source": [ + "# Load the latest version\n", + "\n", + "DataChain(name=\"fashion-tmp\").show(3)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "53a41df0-a187-43ca-a41e-1bd0e969320f", + "metadata": {}, + "outputs": [], + "source": [ + "ds = DataChain(name=\"fashion-tmp\")\n", + "ds.version" + ] + }, + { + "cell_type": "markdown", + "id": "c3969f7b-4d4e-47ce-8d5e-500f22848dcf", + "metadata": {}, + "source": [ + "### Load a specific version of the dataset" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "0a183369-8ba4-425d-a2a2-f546bd1e27f8", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "# Load a specific version of the dataset\n", + "\n", + "# DataChain(name=\"fashion-tmp\", version=1).show(3)" + ] + }, + { + "cell_type": "markdown", + "id": "156fe72c-aabb-4e3c-99c7-f74c0a4382fb", + "metadata": {}, + "source": [ + "### Delete the dataset version " + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "57284102-1160-4d43-8f57-8ef9d37520e4", + "metadata": {}, + "outputs": [], + "source": [ + "# Delete the dataset version\n", + "\n", + "# DataChain.delete(\"fashion-tmp\", version=1)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "0ff9ae0d-d3b5-4baa-b360-a65b1be804eb", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "# Expected error: Dataset fashion-tmp does not have version 2\n", + "\n", + "# DataChain(name=\"fashion-tmp\", version=2).show(3)" + ] + }, + { + "cell_type": "markdown", + "id": "3892c8ad-61ed-4ad6-8672-6a5d3fddf342", + "metadata": {}, + "source": [ + "# 🧩 Split train/test/val" + ] + }, + { + "cell_type": "markdown", + "id": "721f7eb5-9407-43a9-9760-81c112b4c097", + "metadata": {}, + "source": [ + "In this example, we define a `train_test_split` function to randomly split the dataset into the train, test, and validation sets. \n", + "\n", + "The workflow:\n", + "- Create a function that generates a random label (`\"train\"`, `\"test\"`, or `\"val\"`)\n", + "- Use the `map()` method to apply the function to each row\n", + "- Use the `filter()` and `save()` methods to save split datasets" + ] + }, + { + "cell_type": "markdown", + "id": "61b18166-ede5-4fa8-9ef2-812d90b06dd6", + "metadata": {}, + "source": [ + "## Define `train_test_split` function\n", + "\n", + "- The function randomly chooses one of the labels (`\"train\"`, `\"test\"`, or `\"val\"`) using `random.choices` with the specified weights (`0.7`, `0.2`, and `0.1`, respectively).\n", + "- The function a `str` value " + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "7c8bbbe0-af83-4193-a971-172218f331d0", + "metadata": {}, + "outputs": [], + "source": [ + "def train_test_split(name) -> str:\n", + " import random\n", + " labels = [\"train\", \"test\", \"val\"]\n", + " return random.choices(labels, weights = [0.7, 0.2, 0.1])[0]" + ] + }, + { + "cell_type": "markdown", + "id": "d20648cb-a626-4ba7-9db7-759e69352e82", + "metadata": {}, + "source": [ + "## Add a signal (`split`)\n", + "\n", + "- This code loads the `fashion-product-images` dataset.\n", + "- It then applies the batched UDF `train_test_split_batch` to the dataset, adding a new column `\"split\"` with the randomly assigned labels.\n", + "- Finally, it converts the dataset to a Pandas DataFrame and displays the first few rows using `head()`." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "127bd21b-5a79-4633-b70b-57bfe99eda31", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Processed: 11538 rows [00:00, 42082.00 rows/s]\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idrandomvtypedir_typeparentnameetagversionis_latestlast_modified...mastercategorysubcategoryarticletypebasecolourseasonyearusageproductdisplaynamefilenamesplit
016741549482353911470fashion-product-images/images10004.jpgCMDWmrbe+4YDEAE=171948973376595212024-06-27 12:02:13.818000+00:00...ApparelTopwearKurtasMultiSummer2012.0EthnicDiva Women Multi Coloured Kurta10004.jpgtest
1236432616489997967630fashion-product-images/images10005.jpgCKSeprve+4YDEAE=171948974444112412024-06-27 12:02:24.500000+00:00...ApparelTopwearTshirtsGreyFall2011.0CasualChhota Bheem Kids Boys Lets Rock Grey T-shirt10005.jpgtrain
2348081679847881170fashion-product-images/images10007.jpgCMK/06be+4YDEAE=171948970114246612024-06-27 12:01:41.201000+00:00...ApparelTopwearTshirtsBlackSummer2011.0SportsADIDAS Men's Pune Warriors Graphic Black T-shirt10007.jpgval
\n", + "

3 rows × 50 columns

\n", + "
" + ], + "text/plain": [ + " id random vtype dir_type parent \\\n", + "0 1 674154948235391147 0 fashion-product-images/images \n", + "1 2 3643261648999796763 0 fashion-product-images/images \n", + "2 3 4808167984788117 0 fashion-product-images/images \n", + "\n", + " name etag version is_latest \\\n", + "0 10004.jpg CMDWmrbe+4YDEAE= 1719489733765952 1 \n", + "1 10005.jpg CKSeprve+4YDEAE= 1719489744441124 1 \n", + "2 10007.jpg CMK/06be+4YDEAE= 1719489701142466 1 \n", + "\n", + " last_modified ... mastercategory subcategory \\\n", + "0 2024-06-27 12:02:13.818000+00:00 ... Apparel Topwear \n", + "1 2024-06-27 12:02:24.500000+00:00 ... Apparel Topwear \n", + "2 2024-06-27 12:01:41.201000+00:00 ... Apparel Topwear \n", + "\n", + " articletype basecolour season year usage \\\n", + "0 Kurtas Multi Summer 2012.0 Ethnic \n", + "1 Tshirts Grey Fall 2011.0 Casual \n", + "2 Tshirts Black Summer 2011.0 Sports \n", + "\n", + " productdisplayname filename split \n", + "0 Diva Women Multi Coloured Kurta 10004.jpg test \n", + "1 Chhota Bheem Kids Boys Lets Rock Grey T-shirt 10005.jpg train \n", + "2 ADIDAS Men's Pune Warriors Graphic Black T-shirt 10007.jpg val \n", + "\n", + "[3 rows x 50 columns]" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Add signal\n", + "\n", + "ds = (\n", + " DataChain.from_dataset(\"fashion-product-images\")\n", + " .filter((C(\"masterCategory\") == \"Apparel\") & (C(\"subCategory\") == \"Topwear\"))\n", + " .map(split=train_test_split, params=[\"name\"], output=str)\n", + " .save()\n", + ")\n", + "\n", + "ds.to_pandas().head(3)" + ] + }, + { + "cell_type": "markdown", + "id": "2e134303-5e77-4a78-8015-cf72abd105c7", + "metadata": {}, + "source": [ + "By running this code, you'll get a Pandas DataFrame with a new column `split` containing the labels `\"train\", \"test\", or \"val\"` randomly assigned to each row based on the specified weights." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "57767332-9aaf-4f45-846f-2eeab1ed1c9d", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "split\n", + "train 8131\n", + "test 2229\n", + "val 1178\n", + "Name: count, dtype: int64" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ds.to_pandas()[\"split\"].value_counts(normalize=False)" + ] + }, + { + "cell_type": "markdown", + "id": "a8604cca-f201-4f53-a6be-3fb34e82962d", + "metadata": { + "execution": { + "iopub.execute_input": "2024-06-04T14:27:23.611752Z", + "iopub.status.busy": "2024-06-04T14:27:23.611260Z", + "iopub.status.idle": "2024-06-04T14:27:23.647486Z", + "shell.execute_reply": "2024-06-04T14:27:23.646779Z", + "shell.execute_reply.started": "2024-06-04T14:27:23.611723Z" + } + }, + "source": [ + "## Save `train`, `test` and `val` datasets" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "c4e7f5b2-f89a-4c1c-8207-8a1a825b36dd", + "metadata": {}, + "outputs": [], + "source": [ + "# Save train, test, and val datasets\n", + "\n", + "ds_train = (ds.filter(C(\"split\") == \"train\").save(\"fashion-train\"))\n", + "ds_test = (ds.filter(C(\"split\") == \"test\").save(\"fashion-test\"))\n", + "ds_val = (ds.filter(C(\"split\") == \"val\").save(\"fashion-val\"))\n" + ] + }, + { + "cell_type": "markdown", + "id": "4d24233e-356e-4103-94a9-32bf98cfd512", + "metadata": {}, + "source": [ + "After running this code, you'll have three separate datasets:\n", + "\n", + "1. `fashion-train`: Contains the rows from the original dataset where `split` is `'train'`.\n", + "2. `fashion-test`: Contains the rows from the original dataset where `split` is `'test'`.\n", + "3. `fashion-val`: Contains the rows from the original dataset where `split` is `'val'`.\n", + "\n", + "You can now use these datasets for training, testing, and validating your machine learning models, respectively." + ] + }, + { + "cell_type": "markdown", + "id": "12b1d75c-ce36-4fbe-8698-c0093ba202d0", + "metadata": {}, + "source": [ + "# 🎨 Generating & Managing Embeddings" + ] + }, + { + "cell_type": "markdown", + "id": "6f7624ff-ec19-4876-97fa-7bed89c47932", + "metadata": {}, + "source": [ + "This section demonstrates how to compute and save image embeddings with the pre-trained `ResNet50` model in PyTorch. \n", + "\n", + "DataChain helps to compute and manage embeddings, it prepares the data for downstream machine learning models or comparative analysis, integrating complex data transformations seamlessly into the overall data management workflow." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "f7458a40-02e0-442f-b810-5e99cef24649", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "import torch\n", + "from torchvision import transforms\n", + "from torchvision.models import resnet50\n", + "from tqdm import tqdm\n", + "from typing import List\n", + "\n", + "from datachain.lib.image import convert_image" + ] + }, + { + "cell_type": "markdown", + "id": "10afb63c-7c72-4e83-b374-fa00b8c305b0", + "metadata": {}, + "source": [ + "## Define `embeddings_processor` function" + ] + }, + { + "cell_type": "markdown", + "id": "013a6fd7-6dc8-48d4-9f1d-5ce2c7fecdcd", + "metadata": { + "execution": { + "iopub.execute_input": "2024-06-26T13:21:57.275195Z", + "iopub.status.busy": "2024-06-26T13:21:57.274282Z", + "iopub.status.idle": "2024-06-26T13:21:57.312958Z", + "shell.execute_reply": "2024-06-26T13:21:57.312305Z", + "shell.execute_reply.started": "2024-06-26T13:21:57.275164Z" + } + }, + "source": [ + "The embeddings processor function works as following: \n", + "\n", + "- Reads the raw image data and applies the transformation \n", + "- Passes the image through the model to get embeddings\n", + "- Returns the embeddings as a list of floats" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "51f80a09-7dcc-456f-843e-e2157ab73ab0", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/dave/micromamba/envs/dvcx-py311/lib/python3.11/site-packages/torchvision/models/_utils.py:208: UserWarning: The parameter 'pretrained' is deprecated since 0.13 and may be removed in the future, please use 'weights' instead.\n", + " warnings.warn(\n", + "/Users/dave/micromamba/envs/dvcx-py311/lib/python3.11/site-packages/torchvision/models/_utils.py:223: UserWarning: Arguments other than a weight enum or `None` for 'weights' are deprecated since 0.13 and may be removed in the future. The current behavior is equivalent to passing `weights=ResNet50_Weights.IMAGENET1K_V1`. You can also use `weights=ResNet50_Weights.DEFAULT` to get the most up-to-date weights.\n", + " warnings.warn(msg)\n" + ] + } + ], + "source": [ + "# Helpers \n", + "transformer = transforms.Compose([\n", + " transforms.Resize((224, 224)),\n", + " transforms.ToTensor(),\n", + " transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n", + " ])\n", + "model = resnet50(pretrained=True).eval()\n", + "\n", + "# Embeddings processor function\n", + "def embeddings_processor(file) -> list[float]: \n", + "\n", + " img_raw = file.get_value()\n", + " img = convert_image(img_raw, transform=transformer).unsqueeze(0)\n", + " with torch.no_grad():\n", + " emb = model(img)\n", + "\n", + " return emb[0].tolist()\n", + " " + ] + }, + { + "cell_type": "markdown", + "id": "0d7945ed-6d3c-4e91-99c5-ff8a5b5828b5", + "metadata": {}, + "source": [ + "## Compute and Save Embeddings" + ] + }, + { + "cell_type": "markdown", + "id": "085f4856-4cb6-493f-b54e-d1f2bbe292d1", + "metadata": {}, + "source": [ + "Run calculation on `fashion-test` dataset and save a new `fashion-embeddings` dataset \n", + "\n", + "- Load the `fashion-test` dataset.\n", + "- Use `.map` method to apply the `embeddings_processor` function to calculate embeddings for each image.\n", + "- Save the resulting dataset with embeddings as `fashion-embeddings`" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "52ad8daf-d9e2-4eca-8582-050736cd7359", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Processed: 2229 rows [04:17, 8.67 rows/s]\n" + ] + } + ], + "source": [ + "# Compute and Save Embeddings\n", + "\n", + "ds_emb = (\n", + " DataChain(name=\"fashion-test\")\n", + " .map(embeddings=embeddings_processor)\n", + " .save(\"fashion-embeddings\")\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "1f4c217c-b1cf-4388-80c4-c2239976e28f", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idrandomvtypedir_typeparentnameetagversionis_latestlast_modified...subcategoryarticletypebasecolourseasonyearusageproductdisplaynamefilenamesplitembeddings
016741549482353911470fashion-product-images/images10004.jpgCMDWmrbe+4YDEAE=171948973376595212024-06-27 12:02:13.818000+00:00...TopwearKurtasMultiSummer2012.0EthnicDiva Women Multi Coloured Kurta10004.jpgtest[2.544675588607788, -0.2560328245162964, -1.24...
1274626531780021272330fashion-product-images/images10052.jpgCO/x4rrd+4YDEAE=171948947490225512024-06-27 11:57:54.950000+00:00...TopwearShirtsBlueSummer2012.0CasualFrench Connection Men Blue Shirt10052.jpgtest[1.3957180976867676, 0.7307451367378235, -1.31...
2340454900878702138930fashion-product-images/images10054.jpgCMT7t6Dd+4YDEAE=171948941967302812024-06-27 11:56:59.711000+00:00...TopwearTopsPinkSummer2012.0CasualHannah Montana Girls Pink Top10054.jpgtest[0.028559938073158264, -0.6460703611373901, -2...
\n", + "
" + ], + "text/plain": [ + " id random vtype dir_type parent \\\n", + "0 1 674154948235391147 0 fashion-product-images/images \n", + "1 2 7462653178002127233 0 fashion-product-images/images \n", + "2 3 4045490087870213893 0 fashion-product-images/images \n", + "\n", + " name etag version is_latest \\\n", + "0 10004.jpg CMDWmrbe+4YDEAE= 1719489733765952 1 \n", + "1 10052.jpg CO/x4rrd+4YDEAE= 1719489474902255 1 \n", + "2 10054.jpg CMT7t6Dd+4YDEAE= 1719489419673028 1 \n", + "\n", + " last_modified ... subcategory articletype basecolour \\\n", + "0 2024-06-27 12:02:13.818000+00:00 ... Topwear Kurtas Multi \n", + "1 2024-06-27 11:57:54.950000+00:00 ... Topwear Shirts Blue \n", + "2 2024-06-27 11:56:59.711000+00:00 ... Topwear Tops Pink \n", + "\n", + " season year usage productdisplayname filename split \\\n", + "0 Summer 2012.0 Ethnic Diva Women Multi Coloured Kurta 10004.jpg test \n", + "1 Summer 2012.0 Casual French Connection Men Blue Shirt 10052.jpg test \n", + "2 Summer 2012.0 Casual Hannah Montana Girls Pink Top 10054.jpg test \n", + "\n", + " embeddings \n", + "0 [2.544675588607788, -0.2560328245162964, -1.24... \n", + "1 [1.3957180976867676, 0.7307451367378235, -1.31... \n", + "2 [0.028559938073158264, -0.6460703611373901, -2... " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[limited by 3 objects]\n" + ] + } + ], + "source": [ + "ds_emb.show(3)" + ] + }, + { + "cell_type": "markdown", + "id": "21ad4b5a-b3eb-408a-b5c6-ed40cc2053e5", + "metadata": {}, + "source": [ + "🚀🖼️ By using DataChain's `.map()` method, you can efficiently process each image in the dataset and compute its embeddings. The resulting dataset will contain a pointer to the original image and the computed embeddings, which can be used for various downstream tasks such as similarity search, clustering, or as input features for other machine learning models.\n", + "\n", + "This approach leverages DataChain's powerful data processing capabilities along with PyTorch's pre-trained models to create a scalable and efficient pipeline for computing image embeddings. \n", + "\n", + "**Notes:**\n", + "- You can use these embeddings for various downstream tasks, such as image classification, clustering, or retrieval.\n", + "- Every time you run the cell above, it will calculate embeddings and save a new version of it (incremented by 1)" + ] + }, + { + "cell_type": "markdown", + "id": "c5e8add2-376b-4cfd-b8ea-ea225fc0b364", + "metadata": {}, + "source": [ + "# 🕵️‍♀️ Similarity Search \n", + "\n", + "This example demonstrates how to perform similarity search using the embeddings calculated in the previous step" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "0801c361-a63a-4f7b-bbeb-b3f5f44bdad8", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "from sqlalchemy import tuple_\n", + "\n", + "from datachain.sql.functions.array import cosine_distance, euclidean_distance\n" + ] + }, + { + "cell_type": "markdown", + "id": "b78bcb67-9eb5-466b-83a4-c33f255aced5", + "metadata": {}, + "source": [ + "## Select `target` image\n", + "\n", + "- Select the first image from the `fashion-embeddings` dataset as the target image for similarity search.\n", + "- Print the source, parent, name, and the first 5 elements of the embeddings for the target image" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "6aaaff90-d618-4520-9706-5627bdd7bd87", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "('10004.jpg',\n", + " [2.544675588607788,\n", + " -0.2560328245162964,\n", + " -1.2497756481170654,\n", + " -1.529966115951538,\n", + " -0.9801440238952637])" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Get a \"target\" image for a similarity search\n", + "\n", + "TARGET_NAME, TARGET_EMB = (\n", + " DataChain.from_dataset(\"fashion-embeddings\")\n", + " .select(\"name\", \"embeddings\" )\n", + " .results()[0] # <- Select the first item\n", + ")\n", + "\n", + "TARGET_NAME, TARGET_EMB[:5]" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "62ca6be5-de7d-40f5-9080-e079944929c4", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "image/jpeg": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCABQADwDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD3+iiigAoooyPWgAooyPWigAooooAK57xvrB0PwfqF6n+t2COPnB3OQox7jOfwroa8w+Nd6YtB0yyBwLi6Lt7hFP8AVhQB5Zb+JtZsFItta1GMcnAnYjj6mr6+PvFCYCa9dHIz84U/0rlpCdnA5Jozl8/hQI6Wbx54tlKqfENyu4ZGxFB/QV0Pw18W6gfGccGpajdXiXqtb/v5SwVh8ykAn2x+NecSOfOi54AIH41oaNdHTte067XjybqKT8Nwz/WgD6tooooGFeKfHC6Lavo9rniOCSX/AL6YD/2Wva68I+NuR4tsCehsV/8AQ3oA85kb7vp1piEbFpkh+U+mMVBLI6xyFMfIoY59CwXj8SKBFgtvAPoxFWbclrmEHnMif+hCqkXCn2ar1llr21Cj5jPGB9dwoA+tqKBRQMK8G+NUom8XW0S4zDZLn8XY17zXjXxM8F67f+IpdWsrVr22lRF2w8vHgYwV6n1yKAPIZCTH0rY0bQzqXh/xXfbRizsoCpPADecHP47UP50v/CLa9LIYo9E1FnzjH2Z/8K9X8I+A9Stfhtr2mahbpBfaoHMcZcEqAgCBiOByCfbNAjxBBjP1qVGZJYijEOrqwI7EEYq9caBrFjOYLrSr2KYHBUwMefYgYP4VteHfAPiDXrrbFYvaw9Tc3cbIq4OeB1Y+w/SgD6PtJvtFnDNkHzI1fI6HIzU1VtPsxp+nW1mrs4giWMM3U4GM1Zpytd2BXtqFFFFIYUUUUAGKKKKACiiigD//2Q==", + "image/png": "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", + "text/plain": [ + "" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Preview the target image \n", + "\n", + "sample = DataChain.from_dataset(\"fashion-embeddings\").filter(C(\"name\") == TARGET_NAME).save()\n", + "img = next(sample.iterate_one(\"file\")).get_value()\n", + "img" + ] + }, + { + "cell_type": "markdown", + "id": "3d17c4ff-f224-4f09-82bf-ed0bbda1414d", + "metadata": {}, + "source": [ + "## Calculate similarity\n", + "\n", + "- Calculate the cosine and Euclidean distances between the embeddings of each image and the target image's embeddings using built-in `cosine_distance` and `euclidean_distance` functions." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "1aaa40b1-576e-49cb-8643-3dfa5a6ea1b4", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Calculate similarity\n", + "\n", + "(\n", + " DataChain(name=\"fashion-embeddings\")\n", + " .mutate(\n", + " cos_dist=cosine_distance(C(\"embeddings\"), TARGET_EMB),\n", + " eucl_dist=euclidean_distance(C(\"embeddings\"), TARGET_EMB),\n", + " )\n", + " .save(\"fashion-similarity\")\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "2b5ce0b8-793e-4d6e-bd10-a93237eda73f", + "metadata": {}, + "source": [ + "Here's a breakdown of the code:\n", + "- Load the `fashion-embeddings` dataset.\n", + "- Filter out the target image.\n", + "- Calculate the cosine and Euclidean distances between the embeddings of each image and the target image's embeddings using `cosine_distance` and `euclidean_distance` functions.\n", + "- Exclude the `embeddings` column from the output.\n", + "- Save the resulting dataset as `fashion-similarity`." + ] + }, + { + "cell_type": "markdown", + "id": "be6dd4d3-3124-4d7a-ad95-54cb591a1574", + "metadata": {}, + "source": [ + "## Visualise Similarity distances " + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "57313514-6b57-40cf-8e5b-bdbe6ef8c8a1", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idrandomvtypedir_typeparentnameetagversionis_latestlast_modified...basecolourseasonyearusageproductdisplaynamefilenamesplitembeddingscos_disteucl_dist
016741549482353911470fashion-product-images/images10004.jpgCMDWmrbe+4YDEAE=171948973376595212024-06-27 12:02:13.818000+00:00...MultiSummer2012.0EthnicDiva Women Multi Coloured Kurta10004.jpgtest[2.544675588607788, -0.2560328245162964, -1.24...0.0000000.000000
1114289614893854430107600fashion-product-images/images30609.jpgCObf5ZXd+4YDEAE=171948939735447012024-06-27 11:56:37.414000+00:00...BeigeSummer2012.0CasualMother Earth Women Beige Top30609.jpgtest[2.1158652305603027, -0.5784574747085571, -2.0...0.02216825.301680
215772276846894225461950fashion-product-images/images38567.jpgCPvdj5ve+4YDEAE=171948967696357912024-06-27 12:01:17.014000+00:00...BlueFall2011.0FormalBelmonte Men Check Blue Shirts38567.jpgtest[2.389871597290039, -0.44502636790275574, -0.8...0.02979118.695064
3193842906698679626853670fashion-product-images/images5451.jpgCPqHh5Dd+4YDEAE=171948938531737012024-06-27 11:56:25.358000+00:00...Mushroom BrownSummer2012.0CasualPuma Men Mushroom Brown T-shirt5451.jpgtest[2.194873809814453, -0.19374412298202515, -1.6...0.03236426.560666
4207391222837337108428420fashion-product-images/images7533.jpgCOajw7Xd+4YDEAE=171948946389859812024-06-27 11:57:43.943000+00:00...Off WhiteSummer2012.0EthnicFabindia Off White Chanderi Dupatta7533.jpgtest[2.9931812286376953, -0.21968069672584534, -0....0.03272519.859621
543720896099291457578430fashion-product-images/images17930.jpgCKW+17Ld+4YDEAE=171948945793821312024-06-27 11:57:37.981000+00:00...BlueSummer2011.0CasualStatus Quo Men's Beach Sky Blue T-shirt17930.jpgtest[1.6447069644927979, -0.07466438412666321, -0....0.04956124.513606
6157839977776034629509840fashion-product-images/images38570.jpgCMyFv4ze+4YDEAE=171948964628142012024-06-27 12:00:46.328000+00:00...PinkFall2010.0SportsNike Womens Pink T-shirt38570.jpgtest[1.3173340559005737, -0.8487207293510437, -0.9...0.05499324.534202
718572104199288906582200fashion-product-images/images13258.jpgCLe7wtXd+4YDEAE=171948953099410312024-06-27 11:58:51.041000+00:00...BlueSummer2012.0EthnicFabindia Men Blue Kurta13258.jpgtest[1.6476695537567139, -0.4306546449661255, -0.5...0.05844626.455464
830466728570860712204410fashion-product-images/images15622.jpgCIWRm4/e+4YDEAE=171948965198451712024-06-27 12:00:52.027000+00:00...TealSummer2012.0CasualTonga Women Teal Top15622.jpgtest[2.44319224357605, -0.11512380838394165, -0.59...0.06916427.598884
998143526928069581548920fashion-product-images/images27419.jpgCIf0ptfe+4YDEAE=171948980317235912024-06-27 12:03:23.225000+00:00...OliveSummer2012.0CasualUnited Colors of Benetton Boys Check Olive Shirt27419.jpgtest[1.6270990371704102, -0.7970958948135376, -1.3...0.06994228.246160
\n", + "

10 rows × 53 columns

\n", + "
" + ], + "text/plain": [ + " id random vtype dir_type parent \\\n", + "0 1 674154948235391147 0 fashion-product-images/images \n", + "1 1142 8961489385443010760 0 fashion-product-images/images \n", + "2 1577 227684689422546195 0 fashion-product-images/images \n", + "3 1938 4290669867962685367 0 fashion-product-images/images \n", + "4 2073 9122283733710842842 0 fashion-product-images/images \n", + "5 437 2089609929145757843 0 fashion-product-images/images \n", + "6 1578 3997777603462950984 0 fashion-product-images/images \n", + "7 185 7210419928890658220 0 fashion-product-images/images \n", + "8 304 6672857086071220441 0 fashion-product-images/images \n", + "9 981 4352692806958154892 0 fashion-product-images/images \n", + "\n", + " name etag version is_latest \\\n", + "0 10004.jpg CMDWmrbe+4YDEAE= 1719489733765952 1 \n", + "1 30609.jpg CObf5ZXd+4YDEAE= 1719489397354470 1 \n", + "2 38567.jpg CPvdj5ve+4YDEAE= 1719489676963579 1 \n", + "3 5451.jpg CPqHh5Dd+4YDEAE= 1719489385317370 1 \n", + "4 7533.jpg COajw7Xd+4YDEAE= 1719489463898598 1 \n", + "5 17930.jpg CKW+17Ld+4YDEAE= 1719489457938213 1 \n", + "6 38570.jpg CMyFv4ze+4YDEAE= 1719489646281420 1 \n", + "7 13258.jpg CLe7wtXd+4YDEAE= 1719489530994103 1 \n", + "8 15622.jpg CIWRm4/e+4YDEAE= 1719489651984517 1 \n", + "9 27419.jpg CIf0ptfe+4YDEAE= 1719489803172359 1 \n", + "\n", + " last_modified ... basecolour season year \\\n", + "0 2024-06-27 12:02:13.818000+00:00 ... Multi Summer 2012.0 \n", + "1 2024-06-27 11:56:37.414000+00:00 ... Beige Summer 2012.0 \n", + "2 2024-06-27 12:01:17.014000+00:00 ... Blue Fall 2011.0 \n", + "3 2024-06-27 11:56:25.358000+00:00 ... Mushroom Brown Summer 2012.0 \n", + "4 2024-06-27 11:57:43.943000+00:00 ... Off White Summer 2012.0 \n", + "5 2024-06-27 11:57:37.981000+00:00 ... Blue Summer 2011.0 \n", + "6 2024-06-27 12:00:46.328000+00:00 ... Pink Fall 2010.0 \n", + "7 2024-06-27 11:58:51.041000+00:00 ... Blue Summer 2012.0 \n", + "8 2024-06-27 12:00:52.027000+00:00 ... Teal Summer 2012.0 \n", + "9 2024-06-27 12:03:23.225000+00:00 ... Olive Summer 2012.0 \n", + "\n", + " usage productdisplayname filename split \\\n", + "0 Ethnic Diva Women Multi Coloured Kurta 10004.jpg test \n", + "1 Casual Mother Earth Women Beige Top 30609.jpg test \n", + "2 Formal Belmonte Men Check Blue Shirts 38567.jpg test \n", + "3 Casual Puma Men Mushroom Brown T-shirt 5451.jpg test \n", + "4 Ethnic Fabindia Off White Chanderi Dupatta 7533.jpg test \n", + "5 Casual Status Quo Men's Beach Sky Blue T-shirt 17930.jpg test \n", + "6 Sports Nike Womens Pink T-shirt 38570.jpg test \n", + "7 Ethnic Fabindia Men Blue Kurta 13258.jpg test \n", + "8 Casual Tonga Women Teal Top 15622.jpg test \n", + "9 Casual United Colors of Benetton Boys Check Olive Shirt 27419.jpg test \n", + "\n", + " embeddings cos_dist eucl_dist \n", + "0 [2.544675588607788, -0.2560328245162964, -1.24... 0.000000 0.000000 \n", + "1 [2.1158652305603027, -0.5784574747085571, -2.0... 0.022168 25.301680 \n", + "2 [2.389871597290039, -0.44502636790275574, -0.8... 0.029791 18.695064 \n", + "3 [2.194873809814453, -0.19374412298202515, -1.6... 0.032364 26.560666 \n", + "4 [2.9931812286376953, -0.21968069672584534, -0.... 0.032725 19.859621 \n", + "5 [1.6447069644927979, -0.07466438412666321, -0.... 0.049561 24.513606 \n", + "6 [1.3173340559005737, -0.8487207293510437, -0.9... 0.054993 24.534202 \n", + "7 [1.6476695537567139, -0.4306546449661255, -0.5... 0.058446 26.455464 \n", + "8 [2.44319224357605, -0.11512380838394165, -0.59... 0.069164 27.598884 \n", + "9 [1.6270990371704102, -0.7970958948135376, -1.3... 0.069942 28.246160 \n", + "\n", + "[10 rows x 53 columns]" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Preview results\n", + "\n", + "dist = DataChain.from_dataset(\"fashion-similarity\").order_by(\"cos_dist\").to_pandas()\n", + "dist.head(10)" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "9c723005-8d3c-4afd-846e-e440b7aff10f", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "image/png": "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GMCvv/5K3759efHFF9m5cycdOnTINa5XX32VgIAApkyZokteNm/ezKVLlxg4cCCenp6Eh4czb948wsPD2bdvX7Yv6ddee42qVavy2WefsXbtWj799FNcXFz47rvvaNGiBdOmTWPp0qWMHj2aevXq0aRJk6eeq7lz51KtWjU6d+6MhYUFf/31F0OHDiUzM5OwsLAsZS9cuMArr7zCoEGD6N+/Pz/88AMDBgwgODiYatWqAeqXX/PmzUlPT+fDDz+kVKlSzJs3D1tb22f+u+VF3759mTdvHps2baJ169aA+oenffv2BAcHM378eMzMzHSJ1D///EP9+vWz1NGjRw/8/f2ZOnUqR44c4fvvv8fd3Z1p06YBasLUsWNHatSowcSJE7G2tubChQvZEpjH5faZ0mg09OnTh+nTpxMXF4eLi4tu319//UV8fHy2X7d58fDhQ65du0aZMmVyLZOcnEyzZs24cOECw4YNw9/fnxUrVjBgwADu3bvHe++9h5ubG3PnzmXIkCF07dqVbt26AVCjRo1c642JiaFhw4YkJSXx7rvvUqZMGRYvXkznzp1ZuXIlXbt2pUmTJvz000/07dtXd8ngabR/aPr27Zun979lyxbat29PhQoVmDBhAsnJycyaNYtGjRpx5MgRXQLxzjvvsHLlSoYNG0ZQUBB37txh165dREREUKdOnaceY9myZSQkJPD222+j0WiYPn063bp149KlS7pWkfDwcBo1akTZsmV1n/dff/2Vl19+md9++42uXbvm6f3kpHz58jRt2pTt27cTHx+Po6NjjuW6d+9OeHg4w4cPx8/Pj1u3brF582YiIyPx8/Nj5syZDB8+HHt7e10rkYeHB6Amjr///juvvvoq/v7+xMTE8N1339G0aVNOnz6Nt7d3lmN99tlnmJmZMXr0aO7fv8/06dPp3bs3+/fv15XZvHkzHTt2xMvLi/feew9PT08iIiJYs2YN7733nsHPm8lQhMlbuHChAjz1Vq1atSyv8fX1Vfr37697XrNmTaVDhw5PPU5YWJiS00fi999/VwDl008/zbL9lVdeUTQajXLhwgVFURTl8OHDCqCMGDEiS7kBAwYogDJ+/HjdtvHjxyuA8vrrr2c7XlJSUrZtP//8swIof//9d7Y6Bg8erNuWnp6ulCtXTtFoNMpnn32m23737l3F1tY2yznJTU7Hb9u2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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "import seaborn as sns\n", + "\n", + "# Histogram and Density Plot for Cosine Distance\n", + "plt.figure(figsize=(6, 4))\n", + "sns.histplot(dist[\"cos_dist\"], color=\"blue\", label=\"Cosine Distance\", kde=True)\n", + "plt.title(\"Histogram and Density Plot of Cosine Distance\")\n", + "plt.xlabel(\"Cosine Distance\")\n", + "plt.ylabel(\"Frequency\")\n", + "plt.legend()\n", + "plt.show()\n", + "\n", + "# Histogram and Density Plot for Euclidean Distance\n", + "plt.figure(figsize=(6, 4))\n", + "sns.histplot(dist[\"eucl_dist\"], color=\"red\", label=\"Euclidean Distance\", kde=True)\n", + "plt.title(\"Histogram and Density Plot of Euclidean Distance\")\n", + "plt.xlabel(\"Euclidean Distance\")\n", + "plt.ylabel(\"Frequency\")\n", + "plt.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "c70f492d-9837-485c-b8e2-affde7625cd0", + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-09T13:45:31.525826Z", + "iopub.status.busy": "2024-05-09T13:45:31.525510Z", + "iopub.status.idle": "2024-05-09T13:45:31.529986Z", + "shell.execute_reply": "2024-05-09T13:45:31.529527Z", + "shell.execute_reply.started": "2024-05-09T13:45:31.525804Z" + } + }, + "source": [ + "## Visualize the most and the least similar images " + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "fd5e7edb-a6ed-485b-9de6-0b76984a75c1", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idrandomvtypedir_typeparentnameetagversionis_latestlast_modified...basecolourseasonyearusageproductdisplaynamefilenamesplitembeddingscos_disteucl_dist
016741549482353911470fashion-product-images/images10004.jpgCMDWmrbe+4YDEAE=171948973376595212024-06-27 12:02:13.818000+00:00...MultiSummer2012.0EthnicDiva Women Multi Coloured Kurta10004.jpgtest[2.544675588607788, -0.2560328245162964, -1.24...0.0000000.000000
1114289614893854430107600fashion-product-images/images30609.jpgCObf5ZXd+4YDEAE=171948939735447012024-06-27 11:56:37.414000+00:00...BeigeSummer2012.0CasualMother Earth Women Beige Top30609.jpgtest[2.1158652305603027, -0.5784574747085571, -2.0...0.02216825.301680
215772276846894225461950fashion-product-images/images38567.jpgCPvdj5ve+4YDEAE=171948967696357912024-06-27 12:01:17.014000+00:00...BlueFall2011.0FormalBelmonte Men Check Blue Shirts38567.jpgtest[2.389871597290039, -0.44502636790275574, -0.8...0.02979118.695064
3193842906698679626853670fashion-product-images/images5451.jpgCPqHh5Dd+4YDEAE=171948938531737012024-06-27 11:56:25.358000+00:00...Mushroom BrownSummer2012.0CasualPuma Men Mushroom Brown T-shirt5451.jpgtest[2.194873809814453, -0.19374412298202515, -1.6...0.03236426.560666
4207391222837337108428420fashion-product-images/images7533.jpgCOajw7Xd+4YDEAE=171948946389859812024-06-27 11:57:43.943000+00:00...Off WhiteSummer2012.0EthnicFabindia Off White Chanderi Dupatta7533.jpgtest[2.9931812286376953, -0.21968069672584534, -0....0.03272519.859621
543720896099291457578430fashion-product-images/images17930.jpgCKW+17Ld+4YDEAE=171948945793821312024-06-27 11:57:37.981000+00:00...BlueSummer2011.0CasualStatus Quo Men's Beach Sky Blue T-shirt17930.jpgtest[1.6447069644927979, -0.07466438412666321, -0....0.04956124.513606
6157839977776034629509840fashion-product-images/images38570.jpgCMyFv4ze+4YDEAE=171948964628142012024-06-27 12:00:46.328000+00:00...PinkFall2010.0SportsNike Womens Pink T-shirt38570.jpgtest[1.3173340559005737, -0.8487207293510437, -0.9...0.05499324.534202
718572104199288906582200fashion-product-images/images13258.jpgCLe7wtXd+4YDEAE=171948953099410312024-06-27 11:58:51.041000+00:00...BlueSummer2012.0EthnicFabindia Men Blue Kurta13258.jpgtest[1.6476695537567139, -0.4306546449661255, -0.5...0.05844626.455464
830466728570860712204410fashion-product-images/images15622.jpgCIWRm4/e+4YDEAE=171948965198451712024-06-27 12:00:52.027000+00:00...TealSummer2012.0CasualTonga Women Teal Top15622.jpgtest[2.44319224357605, -0.11512380838394165, -0.59...0.06916427.598884
998143526928069581548920fashion-product-images/images27419.jpgCIf0ptfe+4YDEAE=171948980317235912024-06-27 12:03:23.225000+00:00...OliveSummer2012.0CasualUnited Colors of Benetton Boys Check Olive Shirt27419.jpgtest[1.6270990371704102, -0.7970958948135376, -1.3...0.06994228.246160
\n", + "

10 rows × 53 columns

\n", + "
" + ], + "text/plain": [ + " id random vtype dir_type parent \\\n", + "0 1 674154948235391147 0 fashion-product-images/images \n", + "1 1142 8961489385443010760 0 fashion-product-images/images \n", + "2 1577 227684689422546195 0 fashion-product-images/images \n", + "3 1938 4290669867962685367 0 fashion-product-images/images \n", + "4 2073 9122283733710842842 0 fashion-product-images/images \n", + "5 437 2089609929145757843 0 fashion-product-images/images \n", + "6 1578 3997777603462950984 0 fashion-product-images/images \n", + "7 185 7210419928890658220 0 fashion-product-images/images \n", + "8 304 6672857086071220441 0 fashion-product-images/images \n", + "9 981 4352692806958154892 0 fashion-product-images/images \n", + "\n", + " name etag version is_latest \\\n", + "0 10004.jpg CMDWmrbe+4YDEAE= 1719489733765952 1 \n", + "1 30609.jpg CObf5ZXd+4YDEAE= 1719489397354470 1 \n", + "2 38567.jpg CPvdj5ve+4YDEAE= 1719489676963579 1 \n", + "3 5451.jpg CPqHh5Dd+4YDEAE= 1719489385317370 1 \n", + "4 7533.jpg COajw7Xd+4YDEAE= 1719489463898598 1 \n", + "5 17930.jpg CKW+17Ld+4YDEAE= 1719489457938213 1 \n", + "6 38570.jpg CMyFv4ze+4YDEAE= 1719489646281420 1 \n", + "7 13258.jpg CLe7wtXd+4YDEAE= 1719489530994103 1 \n", + "8 15622.jpg CIWRm4/e+4YDEAE= 1719489651984517 1 \n", + "9 27419.jpg CIf0ptfe+4YDEAE= 1719489803172359 1 \n", + "\n", + " last_modified ... basecolour season year \\\n", + "0 2024-06-27 12:02:13.818000+00:00 ... Multi Summer 2012.0 \n", + "1 2024-06-27 11:56:37.414000+00:00 ... Beige Summer 2012.0 \n", + "2 2024-06-27 12:01:17.014000+00:00 ... Blue Fall 2011.0 \n", + "3 2024-06-27 11:56:25.358000+00:00 ... Mushroom Brown Summer 2012.0 \n", + "4 2024-06-27 11:57:43.943000+00:00 ... Off White Summer 2012.0 \n", + "5 2024-06-27 11:57:37.981000+00:00 ... Blue Summer 2011.0 \n", + "6 2024-06-27 12:00:46.328000+00:00 ... Pink Fall 2010.0 \n", + "7 2024-06-27 11:58:51.041000+00:00 ... Blue Summer 2012.0 \n", + "8 2024-06-27 12:00:52.027000+00:00 ... Teal Summer 2012.0 \n", + "9 2024-06-27 12:03:23.225000+00:00 ... Olive Summer 2012.0 \n", + "\n", + " usage productdisplayname filename split \\\n", + "0 Ethnic Diva Women Multi Coloured Kurta 10004.jpg test \n", + "1 Casual Mother Earth Women Beige Top 30609.jpg test \n", + "2 Formal Belmonte Men Check Blue Shirts 38567.jpg test \n", + "3 Casual Puma Men Mushroom Brown T-shirt 5451.jpg test \n", + "4 Ethnic Fabindia Off White Chanderi Dupatta 7533.jpg test \n", + "5 Casual Status Quo Men's Beach Sky Blue T-shirt 17930.jpg test \n", + "6 Sports Nike Womens Pink T-shirt 38570.jpg test \n", + "7 Ethnic Fabindia Men Blue Kurta 13258.jpg test \n", + "8 Casual Tonga Women Teal Top 15622.jpg test \n", + "9 Casual United Colors of Benetton Boys Check Olive Shirt 27419.jpg test \n", + "\n", + " embeddings cos_dist eucl_dist \n", + "0 [2.544675588607788, -0.2560328245162964, -1.24... 0.000000 0.000000 \n", + "1 [2.1158652305603027, -0.5784574747085571, -2.0... 0.022168 25.301680 \n", + "2 [2.389871597290039, -0.44502636790275574, -0.8... 0.029791 18.695064 \n", + "3 [2.194873809814453, -0.19374412298202515, -1.6... 0.032364 26.560666 \n", + "4 [2.9931812286376953, -0.21968069672584534, -0.... 0.032725 19.859621 \n", + "5 [1.6447069644927979, -0.07466438412666321, -0.... 0.049561 24.513606 \n", + "6 [1.3173340559005737, -0.8487207293510437, -0.9... 0.054993 24.534202 \n", + "7 [1.6476695537567139, -0.4306546449661255, -0.5... 0.058446 26.455464 \n", + "8 [2.44319224357605, -0.11512380838394165, -0.59... 0.069164 27.598884 \n", + "9 [1.6270990371704102, -0.7970958948135376, -1.3... 0.069942 28.246160 \n", + "\n", + "[10 rows x 53 columns]" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sim_ds = DataChain.from_dataset(\"fashion-similarity\").order_by(C(\"cos_dist\"))\n", + "sim = sim_ds.to_pandas()\n", + "sim.head(10)" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "3a7732f1-1689-4dce-bd4e-5672c01143cc", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "10004.jpg\n" + ] + }, + { + "data": { + "image/jpeg": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCABQADwDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD3+iiigAoooyPWgAooyPWigAooooAK57xvrB0PwfqF6n+t2COPnB3OQox7jOfwroa8w+Nd6YtB0yyBwLi6Lt7hFP8AVhQB5Zb+JtZsFItta1GMcnAnYjj6mr6+PvFCYCa9dHIz84U/0rlpCdnA5Jozl8/hQI6Wbx54tlKqfENyu4ZGxFB/QV0Pw18W6gfGccGpajdXiXqtb/v5SwVh8ykAn2x+NecSOfOi54AIH41oaNdHTte067XjybqKT8Nwz/WgD6tooooGFeKfHC6Lavo9rniOCSX/AL6YD/2Wva68I+NuR4tsCehsV/8AQ3oA85kb7vp1piEbFpkh+U+mMVBLI6xyFMfIoY59CwXj8SKBFgtvAPoxFWbclrmEHnMif+hCqkXCn2ar1llr21Cj5jPGB9dwoA+tqKBRQMK8G+NUom8XW0S4zDZLn8XY17zXjXxM8F67f+IpdWsrVr22lRF2w8vHgYwV6n1yKAPIZCTH0rY0bQzqXh/xXfbRizsoCpPADecHP47UP50v/CLa9LIYo9E1FnzjH2Z/8K9X8I+A9Stfhtr2mahbpBfaoHMcZcEqAgCBiOByCfbNAjxBBjP1qVGZJYijEOrqwI7EEYq9caBrFjOYLrSr2KYHBUwMefYgYP4VteHfAPiDXrrbFYvaw9Tc3cbIq4OeB1Y+w/SgD6PtJvtFnDNkHzI1fI6HIzU1VtPsxp+nW1mrs4giWMM3U4GM1Zpytd2BXtqFFFFIYUUUUAGKKKKACiiigD//2Q==", + "image/png": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "30609.jpg\n" + ] + }, + { + "data": { + "image/jpeg": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/wAALCABQADwBAREA/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/9oACAEBAAA/APf6KKKKKKKKKQMD0IP0NLRTdyk4BGfTNOoorzf4xa4bHwyulQ7/AD75ssynGyNCCSfqcD8TXgKSyxsDHI6k9Crlf5GrMWuavBxFqV8n+7dOOPzqKbVdRuBia+u3B7PcOR+pq/4S1c6L4u0zUZXfy4bhfNw55Q8H9DmvrIEEZBzS0V84/FrVTqPju6hDZiskW3X643N+rY/CuDJIINDNukP0FMc5ZRSMfvDuen5V9c+GbxdQ8L6Xdq24S2sbZ9TtGa1aO1fKvjmZJ/HGtyR42m8kAx3xwf1Brnj1pgbMrflSn76n60wn96T7V9NfCa4Nx8ONMBOTH5kf5Oa7akPAz2r5B1Ob7TqV5ODnzZ5HB9csT/WqnGAaqM2JwB3qcnoaiY/NX0X8EpxL4DaPPMN5IuPqFb+tekVh+MNR/srwdq97kho7V9p/2iNo/UivlPHygegqInAqbTtNGoWur3J3ZsLT7QAPUyon8mJqqT8oqLtn3r3D4B32YNb08nlXjnUfUFT/AOgivZq434naZquseC57HSbY3E0ksZeNWAOwHJxnryBxXztd6HrGmsRe6Ze2+OvmwMB+eMVmOHZgiIzOT91Rk13ng/wxri+EvF8kuiXwF1pyx25MJDSMHzhVPJ7Hp2rgpoZrclJoZImB5EilT+oqTTFD3ke6wkv4xndBEzAscccqCRzzXrXwk0TxJZeLX1C40aSy02W2aJ/MQxAchl2qfmbkdT6nJr3GijFMEUasWWNQ3qF5p9MeKOUYdFYejDNJFBFACsUSRg9QigVJRRRRRRRRRX//2Q==", + "image/png": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "4690.jpg\n" + ] + }, + { + "data": { + "image/jpeg": "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", + "image/png": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Select images to display: target image, the most similar and the least similar \n", + "img_names = [\n", + " TARGET_NAME,\n", + " sim.name[1], # Most Similar\n", + " sim.name.iloc[-1] # Least Similar\n", + "]\n", + "\n", + "# Save images in local temporary files\n", + "images = []\n", + "for name in img_names:\n", + " print(name)\n", + " try: \n", + " sample = sim_ds.filter(C(\"name\") == name).save()\n", + " img = next(sample.iterate_one(\"file\")).get_value()\n", + " images.append({name: img})\n", + " display(img)\n", + " except: \n", + " print(\"Stop: \", name)\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "3262ad65-1b89-4bff-914e-75ce450c6327", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAgAAAAFICAYAAAAmm0ZmAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAAD3nElEQVR4nOy9d6AlRZk3/KvqcMLNE5lhmCHJjBIFQVFYEFgcERUJKiZwDSsOIMprwFVRFFjdZdFVZAXjuxhWV3G/Nb6CSBCUnDMyw+S58dwTO1Q93x/V1af7nD43TLzDrR+cOfd0V1dXd1fXk5+HERHBwMDAwMDAYFaB7+oBGBgYGBgYGOx8GAbAwMDAwMBgFsIwAAYGBgYGBrMQhgEwMDAwMDCYhTAMgIGBgYGBwSyEYQAMDAwMDAxmIQwDYGBgYGBgMAthGAADAwMDA4NZCMMAGBgYGBgYzEIYBmCWYO+99wZjLP5wztHT04MlS5bgta99Lf7P//k/uPvuuyfs4/jjjwdjDH/60592zqBn2Plnyhgmw+rVq8EYw957772rh2KwG0GvEatXr97VQzHYSbB39QAMdi5e85rXYP/99wcA1Ot1DA0N4YEHHsCf/vQnXHXVVTjuuOPw3e9+F/vuu+8uHqmBgcFUsPfee2PNmjV4/vnnDdNnMC0YBmCW4f3vfz/OPffc1DYiwm9/+1tcdNFFuPXWW/HqV78ad911F/bZZ59Uu//7f/8varUali5duhNHPLOwO9yDPffcE0888QQcx9nVQzHYjXDzzTcjCALsueeeu3ooBjsJhgEwAGMMp5xyCl796lfjqKOOwjPPPIP3v//9uPnmm1PtZjLR21nYHe6B4zhYsWLFrh6GwW6G/fbbb1cPwWAnw/gAGMTo7+/HV7/6VQDAH//4R9x3332p/Z3s357n4V/+5V9wxBFHoKenB67rYo899sCRRx6JT3ziExgZGUm1134IAHD99dfjiCOOQFdXF/r7+3HKKafgL3/5y7TGPTg4iH//93/HKaecgn322QeFQgG9vb14xStegS9/+ctoNBqp9s899xwsy8LAwABqtVrHfg888EAwxvCb3/xm0ntw7rnngjGG73//+3j++efx7ne/G3vssQdyuRz2228/fOYzn4HneZnnCcMQV111FQ466CDk83ksWLAAZ511Fh5//HF8//vfB2OsTWszESbyAUje+xtuuAFHHXUUuru7MX/+fJx99tl44YUXACit0De+8Q0cdthh6Orqwrx583Duuediy5YtbX0GQYAbbrgB73znO7FixQr09vaiUChg+fLluPDCC7Fhw4aOYx0eHsaFF16IpUuXIpfLYdmyZbjoooswNjaWuqdZuPnmm3H66adj0aJFcF0XCxYswFve8hbcddddme2feeYZ/MM//AP22Wcf5HI5dHd3Y9myZXjDG96A733ve5Pc1Sb+9Kc/gTGG448/Hp7n4Qtf+AIOOOAA5PN5LF26FJ/85CfjOVcqlfB//s//wb777ot8Po+9994bn//85xGGYVu/053Hem6sWbMGALDPPvuk/Hz0HE2Ot1ar4XOf+xxe+tKXolgspuZIlg/Af//3f4Mxhvnz52PdunVtY/79738Py7LQ19eHZ555Zsr30GCGgAxmBZYtW0YA6Hvf+96E7aSUNGfOHAJAV155ZWrfcccdRwDolltuibcJIejEE08kANTb20uvf/3r6eyzz6aTTjopPucDDzyQ6gcAAaCPfvSjxBijY445hs4++2w66KCDCADZtk2/+MUv2saWdX4iov/8z/8kALTnnnvScccdR29/+9vpxBNPpO7ubgJARx99NDUajdQxb3zjGwkAXXfddZn34Y9//CMBoP3224+klJOO4ZxzziEA9JGPfIR6e3tp2bJl9Na3vpVOOukkKhQKBIBOO+20tvMIIejUU08lAOS6Lp188sn0tre9jfbdd18qFot0/vnnEwA655xzMseZheeff54A0LJly9r26Xv/qU99imzbphNOOIHOPPNMWrp0KQGgvfbai0ZGRuitb30r5fN5WrlyJb3lLW+hBQsWEAA65JBDyPO8VJ9r164lANTX10evetWr6KyzzqJTTjmFFi9eTABo/vz59Mwzz7SNZcOGDbTffvsRAJozZw6dfvrpdNppp9HAwAAtX76cTjvttI5z9uKLLyYAxDmno446is466yx65StfSYwxsiyLvvvd76baP/LII9Tb20sAaPny5XT66afTWWedRUcffTR1d3fToYceOuX7e8stt8Tz6rjjjqPe3l5605veRKeeeir19fURADr11FNpeHiYli9fTvPnz6czzjiDTj75ZMrn8wSAPvShD7X1O915fPvtt9M555xDXV1dBIDOOOMMOuecc+LPE088kRrvK1/5SjryyCOpq6uLXv/619Pb3vY2Oumkk+L+9Pv6/PPPp8Z1wQUXEAA65phjKAiCePu6deto/vz5BID+67/+a8r3z2DmwDAAswRTZQCIiE466SQCQO9617tS27OI36233koA6OUvfzmNj4+39XXPPffQ0NBQapsmQoVCgW6++ebUvq985SsxMdm8efOk5ycievzxx+muu+5qO/fIyAidfPLJBIC+8pWvpPb94Q9/IAAdF/4zzjiDANBVV101pTFoBgAA/dM//ROFYRjve+SRR+JF+s4770wd97WvfY0A0KJFi+jJJ5+Mt4dhSB/5yEfiPrc3AzB37lx68MEH4+21Wo2OOeYYAkAHH3ww7bfffrR69ep4/+DgIO2///4EgG644YZUn+Pj4/Q///M/bYyB7/t0ySWXEAA65ZRT2sbylre8hQDQ8ccfT6VSKd4+OjoajyVrzl533XUEgPbff3966KGHUvtuvfVW6unpIdd16emnn463v/e97yUA9KUvfaltHLVajW699da27Z2gCSoAOuqoo1Lze/Xq1TQwMBDfxze+8Y1UrVbj/ffccw/Ztk2cc1qzZk2q362Zx0SdCXfWeA855BDauHFjZrtO/XieR0cddRQBoE9+8pNERBQEQfyMVq1aldmfwcyHYQBmCabDALz97W8nAPT6178+tT2L+P30pz8lAHThhRdOeSx6Mbrooosy97/iFa8gAHT55ZdPev7J8NRTTxEAOvLII9v2HXjggQSAbr/99tT2tWvXkm3bVCwWaXR0dEpj0AzAEUcckdIYaHzoQx8iAHTZZZeltu+7774EgL71rW+1HeN5Hu255547hAG45ppr2vb94he/iPf/+te/btt/1VVXEQB673vfO+WxEBEtXryYOOcpBnH16tXEGCPOeSypJvHII48QY6xtzgohYs3Cvffem3k+zURefPHF8bZTTjmFAND9998/rbFnQRNUxhg98sgjbfsvvPBCAkDd3d1tTCxRU/v0gx/8YMrnnGgeT4cBuO222zqeY6J+nn/+eRoYGCDGGP3617+mT3ziE/F8b9WuGew+ME6ABm2QUgJAbCueCIcffjgsy8J3v/tdHHDAAbFNdio455xzMre/5z3vwb333os//elP+PSnPz2lvoQQ+NOf/oQ777wTGzduRL1eBykGFwDw1FNPtR1z4YUX4h//8R/xjW98A8ccc0y8/Vvf+hbCMMR73/te9Pf3T+n8GqeeemrmfXvpS18KAFi/fn28bd26dfjb3/4GAHjHO97RdozrujjzzDPxta99bVpjmApOOeWUtm0veclLAAC2bePkk0/uuL+TTf+hhx7CzTffjOeffx7VajWeR2EYQkqJZ599Fi9/+csBALfffjuICEcccUSmw+JBBx2EQw45BA899FBq+wMPPIANGzZgv/32wxFHHJE5juOPPx4AcOedd8bbjjrqKPzmN7/Beeedhy984Qs47rjjkM/nM4+fKpYuXYqDDjqobbu+T0cccQQWLFjQcX/WfdyaeTxVLFiwAMcee+xWHbv33nvj+9//Pk477TScffbZKJfL6Ovrw09/+lPkcrmtHpPBroVhAAzaMDQ0BACYM2fOpG33228/XH311fj4xz+O888/H+effz6WLVuGo48+GqeeeirOOussuK6beWxrmGHr9iynoyw888wzeMtb3oLHHnusY5vx8fG2be9617vwqU99Cr/4xS+wceNGLFq0CL7v4/rrrwcAnH/++VM6fxKdogR6e3sBIOXIpa9v3rx56O7uzjxuR8V1Z41Tj2HRokWw7faloaenBwDanNGq1Sre/e5348Ybb5zwnMlnoK99ouvbe++92xgAzTA999xzkzKog4OD8d8f//jHcccdd+Cmm27CypUr4TgODj30UPzd3/0d3v72t+PII4+csK8sdHrW+j522t/pPm7tPJ4qtnUuvelNb8L73//++P247rrrTL6Q3RwmCsAgBSLCAw88AAA4+OCDp3TMBRdcgDVr1uC6667De97zHliWhZ/85Cd417vehZe97GXYuHHjVo9lKjjzzDPx2GOP4dRTT8Vtt92GoaEh+L4PIuroeQ8AxWIRH/jABxAEAa677joAwM9//nNs3rwZxx57LA455JBpj5nz6b9SExGyqWhhtgYTjXO613DJJZfgxhtvxIoVK/DLX/4S69evh+d5seR69NFHA8h+ntO9dq1V2GOPPXDOOedM+Hnzm98cH1csFvGHP/wBd999Ny677DKceOKJePrpp/Fv//ZvOOqoo7Bq1appXTMw+X2a7n3c2nk8VRQKhW06fnh4GL/97W/j39ON1jGYeTAaAIMUfvOb32B0dBQAMtXAnbBw4UJ84AMfwAc+8AEAwJNPPol/+Id/wF133YVPfepT+MEPftB2zPPPP4/DDjusbbsOQ1qyZMmk533yySfx8MMPY8GCBbjxxhvbJNfJQpNWrVqFq666Ctdddx0+/elP4xvf+AaArZP+pwudcGVwcBDVahVdXV1tbXaHtKw//elPAQD/9V//lck0ZT0Dfe0TXV/Wvr322gsAMHfu3I7hgRPhyCOPjKX9MAzxy1/+Eu95z3vwzW9+E2eeeSZe+9rXTrvP7YFtncc7GkSEd7/73Vi3bh1OO+003Hbbbbj66qtx/PHH401vetMuHZvB1sNoAAxilEolfPSjHwUA/P3f/30mcZ4qVqxYgU9+8pMAgAcffDCzzX/+539OuF3bcieCzjGwePHiTLX1DTfcMOHxS5cuxWmnnYYNGzbgc5/7HO68804sXrwYp59++qTn3lbstddesVr2xz/+cdt+3/fx85//fIePY1uhn8GyZcva9v3+97+PTUpJHHvssWCM4b777sPTTz/dtv/xxx9vU/8DioDPmzcPjz/++ISq8qnAtm2ceeaZeN3rXgeg8zzdGdiWeaxNbFm5BbYX/vmf/xm//e1v8dKXvhQ33HADfvCDH8T5KXQeAoPdD4YBMIhTAessgIsWLYrtfJPhj3/8I37zm98gCIK2Pn/1q18ByCYMAHDttde2JdS5+uqrcffdd6Onpwfve9/7Jj3/AQccAMuy8Mgjj7T19b//+7+4+uqrJ+3jIx/5CAC1yAHAP/7jP2YuwjsCF154IQDg0ksvTRFCKSUuueQSrF27dqeMY1ugHRy//vWvp7Y/9dRT+NCHPpR5zN577403vvGNkFLivPPOQ7lcjveVSiWcd955mSYDx3Fw6aWXgojwlre8BXfccUdbGyEE/vjHP6ZU1N/85jczHeg2bdqEe++9F0DnebozsC3zWGvKtpUh6oTbbrsNn/3sZ1EsFvGzn/0MXV1dOPXUU3HxxRdjdHQUb33rW9vef4PdA8YEMMvw7W9/O15gPM/D0NAQ7r///lgCOf744/Hd7353yovhww8/jI9+9KPo7e3F4YcfjsWLF6Ner+P+++/HmjVr0NfXh8suuyzz2H/8x3/ECSecgGOPPRZ77rknHn30UTzyyCNxVMEee+wx6fnnzZuH888/H1/72tdw4okn4thjj8XixYvx1FNP4f7778dnPvMZfOlLX5qwj2OPPRYvf/nL8cADD8BxHHzwgx+c0rVvD1x44YX4wx/+gN/+9rc45JBD8NrXvhb9/f245557sGHDBnz4wx/GN7/5zY6OlDMBl156Kc4880x89rOfxU9/+lMceOCB2LJlC26//fb4eSQ98jWuvfZaPPzww/jjH/+IffbZB8cddxyICLfeeivmzp2LN73pTfj//r//r+3azz//fLzwwgv4l3/5Fxx77LE48MADsf/++6NQKGDTpk148MEHMTY2hmuvvRavetWrACiHtVWrVmGfffbBQQcdhN7eXgwODuL2229HvV7HCSecsEtV2dsyj8844wzccssteNe73oWTTz4ZAwMDAJTj4/Lly7dpXIODgzj77LMhhMA111yDAw88MN53xRVX4I477sBf/vIXfOITn5gSs20ww7Cz4w4Ndg10jG/y09XVRYsXL6bjjjuOLr74Yrr77rsn7CMrBv7ZZ5+lz3/+83TiiSfS0qVLKZ/P08DAAB1yyCH0qU99itauXdvWjz4/EdG1115Lhx12GBUKBert7aWVK1fSn//85ymfn0hlL/zOd75DRxxxBHV3d1NfXx8dc8wx9JOf/KTtfJ3wyU9+kgDQ2WefPe17QNTMA9Apz8L3vve9jvH8vu/TV77yFXrZy15GuVyO5s2bR295y1vokUceocsuu4wA0CWXXDLhuJKYSh6A6R5H1IwnP+6449r23XbbbXTiiSfSvHnzqFgs0kEHHUSXX345eZ43Yf6GLVu20KpVq2jJkiXkui7ttddetGrVKhoeHqYTTjiBANDvf//7zPH8+c9/pne+8520bNkyyuVy1NPTQwcccACddtpp9O1vf5tGRkbitr/61a/ovPPOo5e//OU0f/58cl2XlixZQscffzz94Ac/IN/3M88x3ftANPGzJiK69NJLCQBdeumlqe1bO4+FEHTllVfSgQceGGcaTN7vycar0ZoHQAgRJyDqdC1r1qyJM4feeOONE/ZvMPPAiKboam1gsJ2gvbtnytQTQmC//fbDmjVrcOedd8Ze6zMBJ5xwAm655Rb8/Oc/3yl+CTMFY2Nj2HfffVEqlbB582bMmzdvVw/JwOBFB+MDYDDrcd1112HNmjU4+uijdwnxf/DBB+H7fmqb7/v4/Oc/j1tuuQULFizITNzzYsDdd9/dtm1wcBDnnHMORkdHceqppxrib2Cwg2A0AAY7HTNBA/DUU0/hX/7lX7Bp0yb87ne/AxHh9ttvx6tf/eqdPpbjjz8eDz74IA499FAsWrQIo6OjeOSRR7Bx40bk83nceOONWLly5U4f184AYwxLlizBS1/6UsydOxfr16/HAw88gEqlgqVLl+KOO+6IQ/8MDAy2LwwDYLDTMRMYgD/96U947WtfC9d1sWLFCnz+85/HW97yll0ylh/+8If44Q9/iIcffhjDw8MgIixevBivfe1rcfHFF+NlL3vZLhnXzsBnP/tZ3HzzzXjuuecwOjoK13Wx33774dRTT8XHPvYxzJ07d1cP0cDgRQvDABgYGBgYGMxCGB8AAwMDAwODWQjDABgYGBgYGMxCGAbAwMDAwMBgFsIwAAYGBgYGBrMQhgEwMDAwMDCYhTAMgIGBgYGBwSyEYQAMDAwMDAxmIQwDYGBgYGBgMAthGAADAwMDA4NZCMMAGBgYGBgYzEIYBsDAwMDAwGAWwjAABgYGBgYGsxCGATAwMDAwMJiFMAyAgYGBgYHBLIRhAAwMDAwMDGYhDANgYGBgYGAwC2EYAAMDAwMDg1kIwwAYGBgYGBjMQhgGwMDAwMDAYBbCMAAGBgYGBgazEIYBMDAwMDAwmIUwDICBgYGBgcEshGEADAwMDAwMZiEMA2BgYGBgYDALYRgAAwMDAwODWQjDABgYGBgYGMxCGAbAwMDAwMBgFsIwAAYGBgYGBrMQhgEwMDAwMDCYhTAMgIGBgYGBwSyEYQAMDAwMDAxmIQwDYGBgYGBgMAthGAADAwMDA4NZCMMAGBgYGBgYzEIYBsDAwMDAwGAWwjAABgYGBgYGsxCGATAwMDAwMJiFMAyAgYGBgYHBLIRhAAwMDAwMDGYhDANgYGBgYGAwC2EYAAMDAwMDg1kIwwAYGBgYGBjMQhgGwMDAwMDAYBbCMAAGBgYGBgazEIYBMDAwMDAwmIUwDICBgYGBgcEshGEADAwMDAwMZiEMA2BgYGBgYDALYRgAAwMDAwODWQjDABgYGBgYGMxCGAbAwMDAwMBgFsIwAAYGBgYGBrMQhgEwMDAwMDCYhTAMgIGBgYGBwSyEYQAMDAwMDAxmIQwDYGBgYGBgMAthGAADAwMDA4NZCMMAGBgYGBgYzEIYBsDAwMDAwGAWwjAAswif//znwRjb1cMwMDBz0WCXYfXq1WCM4fvf//6uHsouh2EAIlQqFVx66aVYuXIl5syZM+EEeeKJJ7By5Up0d3djzpw5ePe7343BwcG2dlJKfOUrX8E+++yDfD6PQw45BD/+8Y+3qc8kfvjDH4Ixhu7u7mlfr8HMxWOPPYazzjoL++67L4rFIubNm4e/+7u/w//+7/+2tZ3OvHnuuefwjne8AwsWLEChUMBLXvIS/NM//dNW9/nss8/izDPPxMDAAIrFIo455hjccsst234DDHZLXH755WCM4aCDDmrb5/s+rrjiCqxYsQL5fB4LFy7EG97wBqxbty7V7r777sPKlSvR29uLnp4enHzyyXjwwQczz3fnnXfimGOOQbFYxB577IELL7wQlUplR1zaixdkQEREzz//PAGgpUuX0vHHH08A6Hvf+15bu7Vr19K8efNov/32o6997Wt0+eWX08DAAB166KHkeV6q7ac+9SkCQB/4wAfouuuuoze84Q0EgH784x9vdZ8a5XKZFi9eTF1dXdTV1TWlawyCgOr1+tRuiMEuw69//Wt63eteR5///Ofpuuuuo69+9at07LHHEgD61re+Fbebzrx54IEHqK+vj172spfRP//zP9P1119Pn/3sZ+ncc89NtZtqny+88ALNmzePFi5cSJdffjl99atfpUMPPZRs26Zbb7110ms0c/HFhbVr11KxWKSuri468MADU/t836eTTjqJisUifeQjH6HvfOc79K//+q901lln0aOPPhq3u++++yifz9NLXvIS+td//Vf6yle+QnvvvTf19vbSk08+merzgQceoHw+Ty9/+cvp2muvpX/6p3+iXC5HK1eunHSsUkqq1+sUhuH2ufjdGIYBiNBoNGjjxo1ERHTPPfd0ZADOO+88KhQKtGbNmnjbH/7wh7bFed26deQ4Dq1atSreJqWkY489lpYsWZKafFPtM4lPfvKTtHz5cnrnO985ZQbAYPdFGIZ06KGH0vLly+NtU503Qgg66KCD6JWvfCXVarUJzzPVPj/84Q+TbduphblardJee+1Fhx9++DZdq8Huh7e97W10wgkn0HHHHdfGAHz5y18mx3Hor3/964R9nHLKKTQwMEBDQ0Pxtg0bNlB3dzedfvrpqbavf/3radGiRVQqleJt119/PQGg3//+99vhimYHDAOQgYkYgAULFtBZZ53Vtv2AAw6gE088Mf59zTXXEAB67LHHUu1+9KMfEQC6/fbbp92nxtNPP02u69Kvf/1rOuecc6bMAFx66aXUqvQBQKtWraIbbriBDjjgAMrlcnT44YdnSnG33HILHXHEEZTL5Wjfffel//iP/8js02DH4NRTT6WFCxfGv6c6b377298SAPrNb35DRIpQd5J+ptrnwQcfTEceeWRbu1WrVhEAevrppye8FjMXXzy49dZbybIsevjhh9sYACEELV68mN761rcSkdL8VKvVzH56enoy594b3vAGcl2XyuUyERGVSiWybZs+/vGPp9p5nkfd3d30vve9b8Lxam1vcn3X6+hzzz1HJ598MhWLRVq0aBF94QtfICll6vihoSF617veRT09PdTX10fvec976MEHH+xIM2YyjA/ANLB+/Xps2bIFr3jFK9r2HXXUUXjggQfi3w888AC6urrw0pe+tK2d3j/dPjUuuugivPa1r8Upp5yyTdejceutt+Kiiy7Cu971Llx22WUYHh7GypUr8eijj6auZ+XKlRgeHsYXvvAFvO9978Nll12GX/7yl9tlDAbtqFarGBoawnPPPYerr74av/3tb3HiiScCmN68uemmmwAAuVwOr3jFK9DV1YVisYi3v/3tGBkZidtNp0/P81AoFNraFYtFAMqWuzUwc3H3ghACF1xwAd7//vfj4IMPbtv/+OOPY8OGDTjkkEPwwQ9+EF1dXejq6sIhhxzS5i8y0ZzyfT+eA4888gjCMGybp67r4rDDDstcM6d6LStXrsTChQvxla98BUcccQQuvfRSXHrppXEbKSXe+MY34sc//jHOOeccXH755di4cSPOOeecrTrnroa9qwewO2Hjxo0AgEWLFrXtW7RoEUZGRuB5HnK5HDZu3IiFCxe2eTrrYzds2DDtPgHg17/+Nf7f//t/eOihh7bbdT366KO49957ccQRRwAA3v72t2P58uX43Oc+h1/84hcAgEsvvRSWZeHPf/4zFi9eDAB461vf2sbgGGw/XHzxxfjWt74FAOCc4/TTT8c3vvENANObN8888wwA9bxWrlyJSy65BA899BCuvPJKrF27FnfccQcYY9Pqc/ny5bj99ttRLpfR09MTt7vjjjsAKGZia2Dm4u6F//iP/8CaNWtiJrMVeu5dffXVmDNnTjyfr7jiCqxcuRL33HMPDjnkEADA8uXL8Ze//AVCCFiWBUA5D/71r38F0JxTk83T22+/fauupdFoYOXKlfj3f/93AMCHP/xhvPGNb8SXv/xlXHjhhZg3bx5++ctf4q677sJXv/pVfOQjHwEAnHfeefj7v//7rTrnrobRAEwD9XodAGJinEQ+n0+1qdfrU2431T5938dHP/pRfOhDH8LLXvaybbqWJI4++uh4wQWApUuX4s1vfjN+//vfQwgBIQRuuukmnHbaafGCCwD7778/Xv/612+3cRikcdFFF+EPf/gDfvCDH+D1r389hBDwfR/A9OaN9ow+8sgjccMNN+CMM87AZZddhi9+8Yu48847cfPNN0+7z/POOw9jY2N429vehgceeABPP/00LrroItx7772pdtOFmYu7D4aHh/G5z30On/3sZzF//vzMNnrulctl3HzzzTj33HNx7rnn4qabbgIR4Stf+Urc9sMf/jCefvppvO9978Pjjz+ORx99FO95z3tigj/VNXNr5x4AnH/++fHfjDGcf/758H0/ZnB+97vfwXEcfOADH4jbcc6xatWqrT7nroRhAKYBrZ7yPK9tX6PRSLUpFApTbjfVPq+++moMDQ3hC1/4wjZdRyte8pKXtG074IADUKvVMDg4iC1btqBer2P//fdva5e1zWD7YMWKFTjppJPwnve8B7/61a9QqVTwxje+EUQ07bkIAGeffXaq3Tve8Q4AKpwq2W4qfb7+9a/H17/+ddx22204/PDDsXz5cvz617/G5ZdfDgBbHZpq5uLug8985jOYM2cOLrjggo5t9Hx5zWteg7322ivevnTpUhxzzDHx3AOAD33oQ/j0pz+NH/3oRzjwwANx8MEH47nnnsMnPvEJAM05Ndk8zTIjTAWcc+y7776pbQcccAAAlTsAANasWYNFixbFpi6N3XXuGQZgGtAqJ82RJrFx40bMmTMn5koXLVqETZs2gYja2gGIpZep9lkqlfClL30JH/jABzA+Po7Vq1dj9erVqFQqICKsXr0aW7Zs2X4XazDjcOaZZ+Kee+7B008/Pa25qOfawoULU+0WLFgAABgdHQUwvfkNKGlp8+bNuPPOO3HvvffiySefRF9fH4Dmwmnw4sQzzzyD6667DhdeeCE2bNgQr0eNRgNBEGD16tUYGRnpOPcANf/03NO4/PLLsXnzZtx+++14+OGHcc8990BKCaA5pyabp0nNkMHEMAzANLDnnnti/vz5sZozibvvvhuHHXZY/Puwww5DrVbDE088kWqn7Vm67VT7HB0dRaVSiRML6c/Pf/5z1Go17LPPPvjgBz+4Vdel7XRJPP300ygWi5g/fz4WLFiAfD6PZ599tq1d1jaDHQOt2iyVStOai1ql3mqX134oWn07nT41urq6YrW9ZVm46aabUCgU8JrXvGarrtHMxd0D69evh5QSF154YWo9+utf/4qnn34a++yzDy677DIcfPDBcBwn0ydkw4YNmaaDgYEBHHPMMbFT4U033YQlS5ZgxYoVAICDDjoItm23zVPf9/Hggw9mztOpQEqJv/3tb6ltTz/9NABg7733BgAsW7YMGzduRK1WS7Xbbeferg1CmJmYKAzwQx/6EBUKBXrhhRfibTfddBMBoGuvvTbetnbt2o55APbcc89UGNZU+qxWq3TjjTe2fV772tdSPp+nG2+8kf7yl7/Ex2/YsIGeeOIJ8n0/3tYp9AoA3XffffG2F154gfL5PJ122mnxtlNPPZWKxSKtX78+3vbMM8+Qbdsm9Go7Y/PmzW3bfN+nww8/nAqFQhwONdW5uHHjRsrlcnTMMceQECLefskllxAAuvvuu+NtU+0zC3/+85/Jsiw6//zzU9vNXHzxYXBwMHM9OvDAA2np0qV044030sMPP0xERG9+85vJsix64okn4uMff/xxsiyLPvzhD094np/85CcEgP71X/81tX3lypW0aNEiGh8fj7d9+9vfJgD029/+Nt5WrVbpiSeeoMHBwXhbpzBAAHTBBRfE26SU9IY3vIEcx6EtW7YQEdF///d/EwD66le/GrcTQtAJJ5ywW4YBmrclga9//ev0xS9+kc477zwCQKeffjp98YtfpC9+8Ys0NjZGRGpBmjt3Lu2333707//+73TFFVfQwMAAHXzwwdRoNFL9ffzjHycA9MEPfpCuv/76OBPgD3/4w1S76fTZik55APSEfv755+NtnRbdgw46iObNm0eXXXYZffnLX6Zly5ZRPp+nhx56KG537733kuu6tPfee9OXv/xluuKKK2jx4sV02GGHmUV3O+O0006jE044gT7/+c/T9ddfT1/84hdpxYoVBICuuuqquN105s1ll11GAOjv//7v6ZprrqEPfvCDxBijs88+O9Vuqn2uXr2ajjrqKPrSl75E3/72t+mjH/0oFQoFevnLX55alInMXJxNyEoE9Nhjj1F3dzctWrSIrrzySrryyitp0aJFNH/+fFq3bl3c7tZbb6UTTzyRvvzlL9O3v/1tev/730+WZdHKlSspCIJUn/fddx/lcrlUJsB8Pk8nn3xyqt0tt9xCAOjSSy+Nt3ViAHQWwve85z10zTXX0KmnnkoA6NOf/nTcLgxDOuqoo2JG9xvf+AadfPLJ8dz7/ve/vx3u4s6DeVsSWLZsWSyFtH6Si9ejjz4aJ4vo7++nd77znbRp06a2/oQQdMUVV9CyZcvIdV068MAD6YYbbsg891T7bMX2YAB08pWXvOQl8Ut1yy23tPV5880308tf/nJyXZf2228/+va3v00XX3wx5fP5ScdpMHX8+Mc/ppNOOokWLlxItm3TwMAAnXTSSfQ///M/bW2nOm+klPT1r3+dDjjgAHIch/baay/6zGc+k5LKp9PnyMgIvfnNb6Y99tiDXNelffbZhz75yU+2EX8iMxdnE7IYACJFsE866STq6uqinp4eevOb39yWLOrZZ5+lk08+mebNm0e5XI5WrFhBV155Zcd06Lfffju9+tWvpnw+T/Pnz6dVq1a1zb/pMACtiYAWLlxIl156aUprRqS0H+94xzviREDnnnsu/fnPfyYA9JOf/GSad2zXwjAAswif+cxnyLKs1Da96G4t3vzmN9P++++/rUMzmGUwc9FgV+HZZ58lAPSf//mf8bbpZFTNwo033kgA6I477tgeQ9xpME6AswgbN27EvHnztvr41vjaZ555Br/5zW9w/PHHb+PIDGYbzFw02FXQ0QNbO/9a554QAl//+tfR29uLww8/fJvHtzNhMgHOAvztb3/DjTfeiJ/97Gc49dRTt7qffffdF+eeey723XdfrFmzBtdeey1c143jdA0MJoOZiwa7Et/97nfx3e9+F8ViEa961au2qo8LLrgA9XodRx99NDzPwy9+8QvceeeduOKKK7Y6B8GugmEAZgFuu+02fOELX8Dxxx+Pf/u3f9vqflauXIkf//jH2LRpE3K5HI4++mhcccUVmclbDAyyYOaiwa7EBz/4QRxwwAH42c9+hv7+/q3q44QTTsBVV12FX/3qV2g0Gth///3x9a9/PZVFcHcBI2rJVGNgYGBgYGDwoscO8wG45pprsPfeeyOfz+OVr3wl7r777h11KgODSWHmo8FMgpmPBjMBO4QB+K//+i987GMfw6WXXor7778fhx56KF73uteZVLUGuwRmPhrMJJj5aDBTsENMAK985Stx5JFHxqVLpZTYa6+9cMEFF+BTn/rUhMdKKbFh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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Create subplots\n", + "\n", + "f, ax_arr = plt.subplots(1, len(img_names), figsize=(6, 4))\n", + "title = \"Displaying images matrix\"\n", + "f.suptitle(title, fontsize=16)\n", + "\n", + "# Plot images\n", + "for i, ax in enumerate(ax_arr):\n", + " name = list(images[i].keys())[0]\n", + " img = images[i][name]\n", + " ax.imshow(img) # Read & show image from a temporary file\n", + " ax.set_title(name)\n", + "plt.show()\n" + ] + }, + { + "cell_type": "markdown", + "id": "f0a6a7f7-82d7-4b2a-9fa2-c691326ac08f", + "metadata": {}, + "source": [ + "# 🧹 Finding and Removing Redundant Images\n", + "\n", + "In this tutorial, you'll learn how to use DataChain to minimize redundant images in a dataset by selecting a diverse subset of images. \n", + "\n", + "- This is particularly useful when working with multiple product images captured from different angles\n", + "- It helps speed up computations and cut the costs \n", + "\n", + "Following these steps, you can effectively minimize redundant images in your dataset by selecting a diverse subset of images using DataChain and the `select_diverse_elements()` function.\n", + "\n", + "**Note:**\n", + "- Make sure to implement (or import) the `select_diverse_elements()` function in the `src.clustering` module for this code to work." + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "364fbd27-e33a-48d7-bb23-06d990a08ea4", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Requirement already satisfied: umap-learn in /Users/dave/micromamba/envs/dvcx-py311/lib/python3.11/site-packages (0.5.6)\n", + "Requirement already satisfied: numpy>=1.17 in /Users/dave/micromamba/envs/dvcx-py311/lib/python3.11/site-packages (from umap-learn) (1.26.4)\n", + "Requirement already satisfied: scipy>=1.3.1 in /Users/dave/micromamba/envs/dvcx-py311/lib/python3.11/site-packages (from umap-learn) (1.12.0)\n", + "Requirement already satisfied: scikit-learn>=0.22 in /Users/dave/micromamba/envs/dvcx-py311/lib/python3.11/site-packages (from umap-learn) (1.4.2)\n", + "Requirement already satisfied: numba>=0.51.2 in /Users/dave/micromamba/envs/dvcx-py311/lib/python3.11/site-packages (from umap-learn) (0.60.0)\n", + "Requirement already satisfied: pynndescent>=0.5 in /Users/dave/micromamba/envs/dvcx-py311/lib/python3.11/site-packages (from umap-learn) (0.5.13)\n", + "Requirement already satisfied: tqdm in /Users/dave/micromamba/envs/dvcx-py311/lib/python3.11/site-packages (from umap-learn) (4.66.2)\n", + "Requirement already satisfied: llvmlite<0.44,>=0.43.0dev0 in /Users/dave/micromamba/envs/dvcx-py311/lib/python3.11/site-packages (from numba>=0.51.2->umap-learn) (0.43.0)\n", + "Requirement already satisfied: joblib>=0.11 in /Users/dave/micromamba/envs/dvcx-py311/lib/python3.11/site-packages (from pynndescent>=0.5->umap-learn) (1.4.2)\n", + "Requirement already satisfied: threadpoolctl>=2.0.0 in /Users/dave/micromamba/envs/dvcx-py311/lib/python3.11/site-packages (from scikit-learn>=0.22->umap-learn) (3.5.0)\n" + ] + } + ], + "source": [ + "!pip install umap-learn" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "74ae0cb5-1fd0-4a84-8ccf-6aee411769a2", + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "import os\n", + "import random\n", + "\n", + "from matplotlib.offsetbox import AnnotationBbox, OffsetImage\n", + "from PIL import Image\n", + "from umap import UMAP" + ] + }, + { + "cell_type": "markdown", + "id": "63b66761-b696-46bd-bf0e-694291e8406e", + "metadata": {}, + "source": [ + "## Select groups to remove redundant images " + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "00874c0b-880e-4144-80f2-71f76e47c1e1", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idrandomvtypedir_typeparentnameetagversionis_latestlast_modified...subcategoryarticletypebasecolourseasonyearusageproductdisplaynamefilenamesplitembeddings
016741549482353911470fashion-product-images/images10004.jpgCMDWmrbe+4YDEAE=171948973376595212024-06-27 12:02:13.818000+00:00...TopwearKurtasMultiSummer2012.0EthnicDiva Women Multi Coloured Kurta10004.jpgtest[2.544675588607788, -0.2560328245162964, -1.24...
1274626531780021272330fashion-product-images/images10052.jpgCO/x4rrd+4YDEAE=171948947490225512024-06-27 11:57:54.950000+00:00...TopwearShirtsBlueSummer2012.0CasualFrench Connection Men Blue Shirt10052.jpgtest[1.3957180976867676, 0.7307451367378235, -1.31...
2340454900878702138930fashion-product-images/images10054.jpgCMT7t6Dd+4YDEAE=171948941967302812024-06-27 11:56:59.711000+00:00...TopwearTopsPinkSummer2012.0CasualHannah Montana Girls Pink Top10054.jpgtest[0.028559938073158264, -0.6460703611373901, -2...
\n", + "
" + ], + "text/plain": [ + " id random vtype dir_type parent \\\n", + "0 1 674154948235391147 0 fashion-product-images/images \n", + "1 2 7462653178002127233 0 fashion-product-images/images \n", + "2 3 4045490087870213893 0 fashion-product-images/images \n", + "\n", + " name etag version is_latest \\\n", + "0 10004.jpg CMDWmrbe+4YDEAE= 1719489733765952 1 \n", + "1 10052.jpg CO/x4rrd+4YDEAE= 1719489474902255 1 \n", + "2 10054.jpg CMT7t6Dd+4YDEAE= 1719489419673028 1 \n", + "\n", + " last_modified ... subcategory articletype basecolour \\\n", + "0 2024-06-27 12:02:13.818000+00:00 ... Topwear Kurtas Multi \n", + "1 2024-06-27 11:57:54.950000+00:00 ... Topwear Shirts Blue \n", + "2 2024-06-27 11:56:59.711000+00:00 ... Topwear Tops Pink \n", + "\n", + " season year usage productdisplayname filename split \\\n", + "0 Summer 2012.0 Ethnic Diva Women Multi Coloured Kurta 10004.jpg test \n", + "1 Summer 2012.0 Casual French Connection Men Blue Shirt 10052.jpg test \n", + "2 Summer 2012.0 Casual Hannah Montana Girls Pink Top 10054.jpg test \n", + "\n", + " embeddings \n", + "0 [2.544675588607788, -0.2560328245162964, -1.24... \n", + "1 [1.3957180976867676, 0.7307451367378235, -1.31... \n", + "2 [0.028559938073158264, -0.6460703611373901, -2... " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[limited by 3 objects]\n" + ] + } + ], + "source": [ + "# Select GROUPS to remove redundant images\n", + "\n", + "ds_source = DataChain.from_dataset(\"fashion-embeddings\")\n", + "ds_source.show(3)" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "id": "14c8f342-1563-43ac-b7f8-be1a14c34c42", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "articletype\n", + "Tshirts 1021\n", + "Shirts 479\n", + "Tops 268\n", + "Kurtas 260\n", + "Name: count, dtype: int64" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Select groups with large number of images (>100)\n", + "\n", + "df = ds_source.to_pandas()\n", + "grouped = df.groupby(\"articletype\").filter(lambda x: len(x) > 100)\n", + "grouped.articletype.value_counts()" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "id": "74977f0e-9cd2-4367-aa6f-a9cdb8f66f3d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['Kurtas', 'Shirts', 'Tops', 'Tshirts']" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Get the list of groups to remove redundant images\n", + "\n", + "GROUPS = grouped[\"articletype\"].unique().tolist()\n", + "GROUPS" + ] + }, + { + "cell_type": "markdown", + "id": "392c4f24-d661-437b-9821-95cae3232de9", + "metadata": {}, + "source": [ + "## Set the parameters for selecting diverse images" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "d9eb58ce-8623-4285-beba-9404bbe25e10", + "metadata": {}, + "outputs": [], + "source": [ + "num_clusters = 6 # Expected number of clusters\n", + "top_d = 30 # Number of images from each cluster\n", + "tot_num = num_clusters * top_d # Expected max number for diverse images" + ] + }, + { + "cell_type": "markdown", + "id": "ee01adb0-a8c6-4d1b-bfa6-4abcbdf9c2ce", + "metadata": {}, + "source": [ + "## Select `top_d` diverse images for each value in `UPC`" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "777a8e0e-ba13-4de9-beab-4fb1e0d63096", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Kurtas\n", + "Indices of the most diverse elements: [221, 73, 77, 232, 59, 211, 143, 148, 247, 147, 76, 238, 67, 146, 6, 120, 142, 89, 236, 134, 228, 80, 24, 219, 39, 7, 82, 3, 30, 160, 133, 243, 257, 53, 109, 126, 29, 121, 1, 176, 231, 110, 188, 65, 204, 234, 155, 179, 252, 141, 208, 81, 182, 175, 197, 159, 241, 222, 105, 206, 223, 191, 2, 170, 45, 240, 27, 93, 192, 92, 151, 64, 128, 200, 140, 185, 184, 137, 100, 186, 215, 162, 98, 248, 10, 189, 163, 213, 74, 108, 75, 123, 87, 166, 180, 101, 167, 187, 214, 114, 171, 115, 250, 201, 61, 220, 47, 50, 158, 150, 135, 35, 202, 229, 153, 212, 113, 55, 26, 66, 72, 157, 20, 173, 112, 4, 8, 165, 235, 12, 16, 48, 195, 91, 9, 84, 5, 38, 130, 116, 83, 161, 90, 63, 11, 258, 99]\n", + "Shirts\n", + "Indices of the most diverse elements: [44, 344, 243, 412, 223, 74, 370, 414, 101, 174, 156, 314, 339, 272, 342, 368, 55, 181, 323, 157, 132, 141, 160, 260, 110, 400, 158, 51, 197, 459, 222, 56, 338, 188, 35, 369, 148, 340, 191, 407, 289, 337, 105, 269, 217, 67, 100, 152, 245, 379, 264, 121, 447, 18, 32, 103, 438, 465, 252, 11, 366, 94, 206, 353, 341, 362, 213, 286, 360, 7, 207, 81, 253, 242, 112, 404, 333, 171, 308, 270, 184, 3, 463, 440, 126, 352, 349, 361, 130, 346, 265, 292, 395, 431, 386, 408, 284, 104, 304, 320, 403, 39, 288, 185, 363, 419, 432, 356, 15, 378, 256, 166, 393, 199, 57, 164, 183, 16, 240, 255, 354, 401, 473, 331, 249, 193, 137, 234, 151, 233, 332, 178, 422, 76, 75, 6, 179, 28, 232, 229, 231, 68, 176, 374, 402, 258, 224, 315, 173, 84, 70, 430, 228, 129, 319, 420, 189, 128, 469, 429, 299, 98, 357, 263, 425, 60, 297, 142, 36, 34, 150, 411, 448, 250, 467]\n", + "Tops\n", + "Indices of the most diverse elements: [175, 223, 216, 181, 222, 147, 5, 87, 197, 245, 134, 61, 148, 49, 192, 83, 102, 205, 125, 231, 261, 170, 101, 191, 249, 183, 120, 123, 177, 44, 2, 1, 266, 130, 232, 63, 179, 262, 140, 169, 131, 32, 24, 55, 133, 161, 106, 136, 20, 159, 160, 13, 158, 157, 215, 38, 26, 246, 77, 67, 151, 110, 66, 96, 21, 100, 88, 14, 124, 95, 162, 193, 68, 117, 165, 200, 240, 25, 59, 62, 81, 256, 19, 194, 252, 227, 156, 233, 221, 113, 244, 98, 203, 132, 73, 190, 92, 45, 70, 186, 30, 220, 142, 166, 91, 16, 23, 121, 241, 141, 15, 267, 22, 12, 109, 60, 212, 265, 105, 118, 9, 56, 234, 204, 17, 74, 239, 85, 180, 11, 250, 144, 238, 78, 108, 155, 104, 206, 184, 58, 255, 103, 137, 210, 116, 107, 69, 196, 185, 254, 178, 50, 164, 75, 146, 51, 189, 6, 76, 251, 237, 138, 188, 213]\n", + "Tshirts\n", + "Indices of the most diverse elements: [893, 1014, 827, 108, 269, 901, 86, 475, 300, 535, 69, 1011, 406, 52, 413, 572, 217, 58, 449, 27, 529, 859, 445, 558, 965, 714, 296, 982, 980, 1006, 314, 488, 150, 139, 399, 958, 924, 803, 725, 625, 768, 925, 906, 704, 585, 1013, 79, 739, 50, 994, 842, 795, 770, 189, 1016, 148, 339, 781, 2, 701, 390, 687, 221, 452, 451, 462, 161, 380, 410, 173, 800, 959, 137, 470, 578, 177, 650, 477, 626, 457, 666, 826, 11, 166, 619, 160, 144, 402, 460, 212, 758, 807, 946, 510, 974, 967, 147, 549, 850, 22, 194, 288, 135, 248, 253, 493, 200, 653, 735, 771, 676, 127, 567, 671, 125, 129, 640, 1018, 265, 142, 526, 295, 509, 237, 102, 259, 321, 927, 996, 615, 472, 351, 381, 565, 947, 360, 525, 712, 522, 568, 814, 117, 15, 103, 235, 861, 403, 566, 43, 741, 182, 353, 337, 747, 1, 336, 675, 33, 34, 755, 548, 500, 688, 494, 179, 37, 180, 485, 530, 846, 962, 746, 592, 12, 178, 593, 664, 591, 963, 674]\n" + ] + } + ], + "source": [ + "# Select top_d diverse images for each DUP group\n", + "\n", + "diversed_ids = [] # IDs in DataChain dataset\n", + "\n", + "for IMAGE_GROUP in GROUPS:\n", + " print(IMAGE_GROUP)\n", + "\n", + " # Load Dataset\n", + " ds_dup = (\n", + " ds_source\n", + " .filter(C(\"articleType\") == IMAGE_GROUP)\n", + " )\n", + "\n", + " # Get embeddings\n", + " emb_all = ds_dup.select(\"embeddings\").results() # -> list(tuple)\n", + "\n", + " # Select diverse images\n", + " diverse_elements_indices = select_diverse_elements(emb_all, num_clusters, top_d)\n", + " diversed_ids.extend(diverse_elements_indices)\n", + " print(\"Indices of the most diverse elements:\", diverse_elements_indices)\n" + ] + }, + { + "cell_type": "markdown", + "id": "7d7d4ca7-7c4c-4d77-a5e0-d070bbd84538", + "metadata": {}, + "source": [ + "## Create a new dataset with the selected diverse images\n", + "\n", + "- The `fashion-embeddings` dataset is filtered to include only the selected diverse images\n", + "- The resulting dataset is saved as `fashion-curated`" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "id": "eefb85b5-9692-4af0-8b3c-f1b90e356527", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Select diverse items\n", + "\n", + "(\n", + " ds_source\n", + " .filter(C(\"id\").in_(diversed_ids))\n", + " .save(\"fashion-curated\")\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "id": "a7792ea4-c1bf-493f-9e0b-4592350f4a8a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(2229, 51)\n", + "(439, 51)\n" + ] + } + ], + "source": [ + "print(DataChain.from_dataset(\"fashion-embeddings\").to_pandas().shape)\n", + "print(DataChain.from_dataset(\"fashion-curated\").to_pandas().shape)" + ] + }, + { + "cell_type": "markdown", + "id": "b87ae7cc-2efc-461a-a6c4-0d722c6a2322", + "metadata": {}, + "source": [ + "## Visualize Clusters\n", + "\n", + "This section demonstrates how to visualize image clusters using UMAP (Uniform Manifold Approximation and Projection) and matplotlib. \n", + "Let's break it down:\n", + "- 🔍 UMAP reduces high-dimensional embeddings to 2D for visualization.\n", + "- 🎭 Random sampling (k_samples) ensures a diverse representation of images.\n", + "- 🖼️ Image thumbnails are added to each point in the scatter plot.\n", + "- 🎨 The plot uses a colorbar to differentiate clusters." + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "id": "56e9a51e-cfe6-40f3-9be5-668ca46c66d3", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "# Extract image paths and embeddings from the \"fashion-curated\" dataset\n", + "\n", + "image_paths = []\n", + "image_embeddings = []\n", + "\n", + "for row in (\n", + " DataChain.from_dataset(\"fashion-curated\")\n", + " .select(\"source\", \"parent\", \"name\", \"embeddings\")\n", + " .iterate()\n", + "):\n", + " image_paths.append(os.path.join(row[0], row[1], row[2]))\n", + " image_embeddings.append(row[3])\n" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "id": "8679d878-a80a-43f4-b74a-98f8a8078f63", + "metadata": {}, + "outputs": [], + "source": [ + "def reduce_dimensions_umap(features, n_neighbors=15, n_components=2, metric=\"euclidean\"):\n", + " # This function uses UMAP to reduce the high-dimensional embeddings to 2D for visualization\n", + " reducer = UMAP(n_neighbors, n_components, metric)\n", + " embedding = reducer.fit_transform(features)\n", + " return embedding\n", + "\n", + "def plot_embeddings(ds: DataChain, embeddings: list, image_paths: list, k_samples=30):\n", + " # This function creates a scatter plot of the reduced embeddings and adds image thumbnails at each point.\n", + "\n", + " # Ensure n_display does not exceed the number of image paths\n", + " k_samples = min(k_samples, len(image_paths))\n", + "\n", + " # Generate random indexes\n", + " indexes = np.random.choice(len(image_paths), k_samples, replace=False)\n", + " selected_images = [image_paths[i] for i in indexes]\n", + " selected_embs = [embeddings[i] for i in indexes]\n", + " filenames = [os.path.basename(img_path) for img_path in selected_images]\n", + "\n", + " # Extract (X,Y) values\n", + " x_values = [emb[0] for emb in selected_embs]\n", + " y_values = [emb[1] for emb in selected_embs]\n", + "\n", + " # Configure the plot\n", + " plt.figure(figsize=(16, 8))\n", + " plt.scatter(x_values, y_values, s=5)\n", + " plt.gca().set_aspect(\"equal\", \"datalim\")\n", + " plt.colorbar(boundaries=np.arange(11)-0.5).set_ticks(np.arange(10))\n", + " plt.title(\"UMAP projection of the Image Dataset\", fontsize=24)\n", + " ax = plt.gca()\n", + "\n", + " # Add image thumbnails at the points \n", + " for i, name in enumerate(filenames):\n", + "\n", + " # Extract image from DataChain\n", + " sample = ds.filter(C(\"name\") == name).save()\n", + " img = next(sample.iterate_one(\"file\")).get_value()\n", + "\n", + " # Attach thumbnail to the point\n", + " img.thumbnail((50, 50), Image.Resampling.LANCZOS) # Updated line here\n", + " imagebox = OffsetImage(img, zoom=0.5)\n", + " ab = AnnotationBbox(imagebox, (x_values[i], y_values[i]), frameon=False)\n", + " ax.add_artist(ab)\n", + "\n", + " plt.show()\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "id": "46ae853a-41c4-47a6-a256-a30c2fc783f6", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[8.71247 , 7.308159 ],\n", + " [9.54952 , 6.4284997],\n", + " [8.3555565, 8.240379 ]], dtype=float32)" + ] + }, + "execution_count": 41, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Apply UMAP to reduce embeddings dimensions \n", + "\n", + "umap_v = reduce_dimensions_umap(\n", + " image_embeddings,\n", + " n_neighbors=15,\n", + " n_components=2,\n", + " metric=\"euclidean\",\n", + ")\n", + "\n", + "umap_v[:3]" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "id": "14966fe5-ff81-4458-8aee-73f6d265ce60", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABH4AAAK2CAYAAAA43lv6AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOzdd5xU1f3/8de5d8r2yrLL0ruIUkQBxcTeYjeWqBHUVDUaozEmfmNJ+xlTTNGoKQZRE1uiaBK7YkNBEcECgvS6lIXtu1PuPb8/Znfcha2wsMvyfvoYmXLuvefeOzM785nP+RxjrbWIiIiIiIiIiEiP43R1B0REREREREREZM9Q4EdEREREREREpIdS4EdEREREREREpIdS4EdEREREREREpIdS4EdEREREREREpIdS4EdEREREREREpIdS4EdEREREREREpIdS4EdEREREREREpIdS4EdEREREREREpIdS4EdEpId74IEHMMZgjOHSSy/t6u7sFa+99lpyn48++uiu7k6P8O677zJt2jSGDRtGenp68vgaY/ZaH7pimyIiIiL7OgV+RPagVatWNfmismrVqg4tf/TRRyeXve2225pt0/hLfcPl7LPP7tB2Pvnkk53WMWjQoA6tA+DHP/5xk3VcccUVHV5H431u7pKZmcmgQYM488wzueuuuygvL+/wNkSkY+69914OP/xwHnzwQZYvX05NTU1Xd6lHuO222xSg7CGa+1vccHFdl9zcXAYNGsS4ceO46KKL+O1vf8vs2bOx1nZ110VEZD+gwI9ID/Tss89SWlra7vYzZszY7W1aa3nooYea3PfYY48RiUR2e92NVVVVsXr1ap555hmuueYaBgwYwIMPPtip25DuRdk7XWv16tVcc801+L4PwJAhQ7jwwgu56qqrkpddMWjQoF0OiovsS3zfp6ysjNWrV7Nw4UIeeeQRvv/973PkkUcyYsQIfv3rX3dZMHV/zAhtr8Y/3u3Kj2EiIt1JoKs7ICKdLxqN8uijj7brC5nv+/zjH//Y7W3OmjWLNWvWNLlv+/btPPPMM5x33nm7tM7DDjuMiRMnJm9baykrK+O9997js88+A6CiooJp06ZRV1fHN7/5zV3fARFp1iOPPEI8HgfgxBNP5H//+x+BgD4+iLQkMzOTqVOnNrmvurqasrIy1q1bx8KFC4nFYgAsW7aMH/zg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uNV17e75b3wu23MREdkXqEKjSA80derUZL2bBx98sEnGRIPVq1cnf8k/8cQTKSws3KVtPfTQQ8lfzSZNmsSIESMAKCoq4oQTTuD5558HEr8kNgz/6kyNf/nLzs7utPVeeumlPP300xhjmDhxIgceeCCRSIS33347OfX0O++8w3HHHcfs2bPJyspqc52//vWv+clPfgLA0KFDmThxImlpaaxatYpgMJhsV1NTw7HHHsu7776bvK+4uJgvfOELZGRksGzZMt566y08z2Pjxo2cccYZPPLII5x77rm7tc/V1dWcdNJJTbJQhg4dyoQJE8jNzWXbtm3Mnj2bDRs2UFtby9VXX01FRQU33XRTi+t89NFHmTp1apNsmREjRjB+/Hiys7OpqKjgk08+4ZNPPsH3ferq6pLtJk6cyFVXXcX69euTQxKLi4s5++yzd9pOQ5ZbR23atIkpU6awfPnyJvs8adIkwuEwixYtYu7cuQB89tlnHHPMMTz//PPtKr66YcMGjj/+eDZu3EhOTg5f+MIXKCoqYuvWrbz66qvJwuQ//elPOfDAA7ngggt2aR8a832fL3/5yzzzzDPJ+/Ly8jj66KPJy8tj7dq1zJo1i2g0SllZGZdeeillZWV897vfbbKe448/noyMDCBRSL0hw+uss86ib9++TdrueLs1WVlZyaLzDz74IJWVlUDiPSszM3On9q2t+4EHHuCKK64AYOTIkRx66KGkpqby6aefMnv2bKy1lJaWcsYZZ7B48eJW3x9+97vfcf311yffy7Kysjj88MPp168fnufxySefMG/ePKy1/Pe//+Xoo49m9uzZpKWltXvfO8uf//xnbrzxRgDGjBnDuHHjcF2XuXPnJovzv/DCC1xzzTX8+c9/5sorr+Qvf/kLjuNw2GGHMWrUqGT2zsqVK4HE63Ts2LH88Ic/bHabnuexfv16ADIyMhg9ejRDhgwhKyuLWCzGunXrmDNnDhUVFVRXV3PJJZcQDAZbfU6XlpZy7LHHJmdohKavvcWLFzN37lyefvppLr/88nYfnyeeeIKLL744+Z6TmprK5MmTGTRoEI7jsHTpUt555x3i8Thz5szh8MMP57333tvlv4Gd5ZBDDmHIkCHJzNU333yT733vezu127BhA57nATBkyBBGjRpFQUEBKSkplJWV8dFHH/HJJ58A8Oqrr3L88cczZ84cwuFwk/U0vMY//fRTXnnlFQAOOOAAjjvuuJ22OXz48Ca3q6urkxmDubm5jB49moEDB5KRkUE0GmXlypXMmTOHuro6SktL+dKXvsTrr7/eJEO3sT/84Q/Jv40AvXr1YvLkyfTp0wdjDNu2bePTTz9l8eLFyX1vSWec/1GjRnHVVVdRWVmZHDLf3ExtIiL7lK6NO4n0bF2V8WOttSNHjrSAzczMtNXV1Tst97Of/Sy53COPPGKttbuU8TN8+PDkMnfffXeTx/7xj38kH+vdu7eNxWKdss+NjRkzJtn+sMMOa1efm9M4QyQUClnADh48uNlsgb/+9a82GAwm23/zm99scb2Nz38gELDZ2dn2qaee2qldw6/z1tomv867rmt///vfW8/zmrRfunSpnTBhQrJdVlZWi8+v9mb8TJ06NdluxIgRzf6qHo/H7T333JPMsnBd17799tvNrm/+/Pk2JSUluc7x48fbOXPmNNt248aN9te//rW94447dnqsI9k7HV3mlFNOSbZLT09PvhYae++99+yQIUOS7fr372+3b9/e7Poa/4recIxuvPHGnV6DpaWl9thjj022HTJkiPV9v1371po77rijyXPuhz/8oY1EIk3abNy40Z544olNnpctnRdrbYcyLTpi4MCBHX5vbLxv4XDYFhQU2Oeee26ndq+//rrNyspKtv3JT37S4jpffvll6zhO8rX/y1/+stn3zA8++MAeeOCByXVeccUV7d7XluxKxk84HLZFRUXNnovf/OY3Tc7rnXfeaQE7atQou2DBgiZt4/G4vfbaa5PtMzIybFVVVbPbj0Qi9rLLLrOzZs2y0Wi02TZ1dXX2V7/6lQ0EAhYSGWWVlZUt7tNXv/rV5LZTUlLsww8/vFOb+fPn22HDhjV5PbX2PPz4449tamqqBawxxn7/+99v9rW6fPlye+SRRybXd8opp7TYz/ba3Ywfa6298MILk+vo06dPs23uv/9+e9ddd7WaqbRw4UJ76KGHJtf1s5/9rF39bisjtMGqVavs1VdfbefOnbvT36YG5eXl9vrrr2/yN6W5trFYzPbq1SvZ7vbbb2/xOVZaWmr//ve/N/t3wtrOP/+NP8Pt6jkVEekuFPgR2YO6MvDzi1/8Inl/cx+oG4beZGdn29raWmttxwM/s2fPTrYPBoN2y5YtTR6vrq62GRkZyTbPPPNMp+xzg//+9787fcndVY0DBQ1BgGXLlrXY/m9/+1uyrTGmxbaN1+k4jn399ddb7ceyZcuSX0KbC6Y1tm3bNjto0KBk28suu6zZdu35YP/GG28k2wwdOnSnc9naOk8++eRm20yZMiXZ5tBDD231S2Br9lTg59VXX21yfv773/+2uL6VK1fa7OzsNgMJOw6f+NGPftTiOktKSmx6enqybWvBl/YoLy9v8nr7/ve/32Lburo6e9hhhyXbHnPMMS227c6Bn4ULF7bY9u677062PeCAA5pt43lek+D1k08+2er2N27caAsLC5PveWvXrm1Xv1uyK4GflJQU+/HHH7fY9vjjj2/Svnfv3i0OSY3H48kfCQD72GOP7db+WGvtL3/5y+T67rnnnmbbLFq0qEkfmwu4Nli1alWTIF5rz8PGwdSWhko1qKqqahLI293XX2cEfm677bYmgbvdUVZWlhy62KdPnxaH8u1K4Kcjvv3tbyfX39yw5I8++ij5+JQpU3ZrW519/hX4EZGeRMWdRXqohiLLsPPsXnPmzGHp0qVAoqhkSkrKLm2jcdHmU045hV69ejV5PC0tjS9/+cvNtt9dM2fO5Ktf/Wrydjgc5sorr+y09V933XUMHTq0xce/9rWvMWHCBACstfztb39rc53nnnsuX/ziF1tt89e//jVZNHrcuHGt7lNubi533HFH8vY///nP5PChjrrzzjuT13/729/udC53dOmll3LAAQcAiWElOxYKnjt3bnLImDGGGTNmJIcOdRd//vOfk9fPOOMMTj311BbbDho0qMmQtvvuu6/NwrAFBQXccsstLT5eWFjYZJuNh/btin/+85/JQqqFhYX89Kc/bbFtOBzm7rvvTt6eNWsWS5Ys2a3t723f/OY3GTNmTIuPT506lUAgMaJ9yZIlVFRU7NTmP//5T3Ko0VlnndXsMMLGioqKuPbaa4FE8djHH398F3u/6771rW8xevToFh+/8MILm9y+6aab6N27d7NtXdfl/PPPT97e3ecg0KRYcEszLf79739PXj/iiCP4yle+0uL6Bg4cyPXXX9/mdhcuXJgsvj1+/PjkeWpJeno6N998c/L2npp9siMaD0eMx+PNPmc7sq6G5/PGjRuTwwD3traeD433saCgYJe30xPOv4jInqQaPyI91IABAzj66KOZNWsWr7zyChs3bqRPnz5A0wDMro5Zr6ura/Klp/EsXo1NnTo1ub3//Oc/bNu2jby8vHZt49lnn91phpiysjLefffdJnUhIBG46N+/f0d2oVXtOS5Tp07l/fffBxJfnNvS2pebBo1nDWqYdaU1Z599Nnl5eWzbto1IJMI777zDySef3OZ2GovH47z00ktAorbJaaed1q7ljjnmGD799FOstcyePZszzjgj+VhDbSeA4447jgMPPLBDfdobGp+z9tQQueyyy/jRj36E7/ts3LiRJUuWJINfzTn99NPbDKqOHz8++TpqqB21qxo/dy688EJSU1NbbT9x4kQOPvhgPvroIyBxPEaOHLlbfdibzjvvvFYfz8zMZOjQoSxZsgRrLatXr+bggw9u0ubZZ59NXm88q1Jrjj322OT1t956i+uuu64Dvd59bdXy2nEf22p/0EEHJa831Pxpje/7vP/++yxYsIB169ZRUVHR4ox3CxYsaPb+xrNyNQ7gt+SrX/0qt956a6ttGp/LCy+8sF2zLe14LrvajsHxysrKVuvHbd68mTlz5rB48WK2b99OdXV1k4D0vHnzktcXLFiw03OjM8RiMebOncvChQspKSmhsrKyyaxyDXW8Gvqwo8Z/t2fNmsXSpUuTtQI7oiecfxGRPUmBH5EebOrUqcyaNQvP83j44Ye54YYbiEajPPbYY0BiCuUjjzxyl9b99NNPU1ZWBkBOTg6nn356s+2OPvpo+vXrx7p164hGozz66KPtzsx57733eO+991ptk5mZyR/+8Icmvyrurl69ejFs2LA22x1++OHJ6wsWLMBa2+qHzYYMoZZYa5t8MG6pEGZjwWCQiRMnJgMt8+fP73Dg58MPP0xOCx8MBncq9NuSxudm7dq1TR6bM2dO8npzxcW72vr169m8eXPydnuOdUFBASNGjODTTz8FEse6tcBPe75kNS5KvTu/7gN88MEHyevt2R+AKVOmJAM/8+fP363t722dcXzfeeed5PV///vfTaaub0njrLodn/d7Q+NATXNyc3OT17Ozs9ssvt04EN/aczAej/PHP/6R3/3ud6xbt65dfW1uandrLR9++GHy9qRJk9pcz5AhQ+jVq1erU8U3PpezZs1qdTr0xn1p0BXnckeNgyRAi0GfRYsWceONN/Lcc8+1Wey4QWvHblfU1tby//7f/+O+++5r97qba9e/f38mT57MnDlzKC8vZ8KECVxy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Lbfcwg9/+MN2FWo+8MADueiii5K3p02b1iTjpcHChQs5/vjjKS8vT76XtOauu+5KBj9feumlVt9zAJYuXcrPfvYzBg0atFeHh9XV1fG///2PE044gUsvvZTa2logkX359NNPN5vtA4ls3Qa33357s5lQ8+fP56ijjmLt2rXtOheNX4dz585tsz7Ysccem3w/XrZsGdOmTUu+NzaoqKjgm9/8Jvfdd1+bfXj66acZMWIEv/nNb5rUOGosEolw9913869//St5X+P37gadff7D4TDDhw8HEj98zZw5s9V9ERHpzjSrl8he8Ktf/YoPP/yQWbNmYa3l5z//Ob/97W+ZPHkyAwYMIBwOs3XrVubOndvkF7ecnByeeOKJXc7i6GyNh2d98YtfpH///h1aPjU1lbPPPjuZ+j1jxowmv/x2F4cffjh5eXk89dRTHHrooUyePJlRo0YRiUR45513WLFiRbLtqFGj2vVrdEf85je/Yd68ebz33nvE43G+853v8Mtf/pIjjzySjIwMli9fzhtvvJH8pTUQCHD//fczaNCg3drun//8ZzZu3MiLL75INBrlpptu4uc//zmTJk1KPk/LyspYvnw5H3/8cXJK4ZbqTx1yyCHcf//9XHrppcTjcT744AMmT57MyJEjGT9+PNnZ2ZSXl7No0SI+/vhjfN/nu9/97k7rcV2Xs846K5ltdvTRR3PyySczYMCA5Bf2vLw8brrppg7v8/Tp05kyZQrLly+nqqqK888/n+HDhzNp0iRCoRCLFi1i7ty5ySyQ9PR0HnnkkV0eKrmnff/73+ett95K1r/4+c9/zr333ssxxxxDbm4ua9euZdasWclzB4ksjcb1MfaWL3/5y/z5z38GEsML33//fQ455JAm03tfccUVuzXMtT2OP/547r33Xq644go8z+O5557j+eef58ADD2TMmDFkZWVRU1PDxo0bWbhwYZu1Znoax3H42c9+lnyvfumllxgxYgRHHHEEAwcOpLS0lNdeey2ZMfGXv/xlp7pyzfnDH/7AnDlzWLFiBbW1tVx00UXccsstTJ48mVAoxKeffso777yDtZZzzz2XLVu28Prrryf71JyDDjqIRx55hAsuuICamhrmzp3L5MmTGTp0KIcccgh5eXnU1dWxefNmPvzwwz2WabZt27YmP/hA4seOsrIy1q1bx4IFC3bKHhk9ejQPP/xwkzpvO5o2bRq//e1vWbp0KZFIhEsuuYT/9//+H2PHjiUlJYWPP/44WUtw7NixnHTSSfzqV79qta9FRUUcccQRvP3229TV1TF27FhOPvlk+vTpkzzOQ4cO5YorrgAgNzeX73//+/z0pz8FEkP1nnvuOSZNmkTfvn3ZuHEjr732GtXV1QQCAe65554mGU3NWb58OTfccAM33HADAwYMYMyYMcmMnpKSEubMmcO2bduS7S+++GKOOOKIndazJ87/l7/8Zf7f//t/ye0+8MADDBs2rEmB8s7+DCAiskdYEdkrotGove6662woFLJAm5fJkyfbpUuXtrne6dOnJ5eZNGnSbvXxueeeS65r4MCBTR7bunVrk77/9a9/3aVtvPjii8l1GGPsihUrmjx+1FFHJR+/9dZbd3FPOm7WrFnJ7R511FG2vLzcnnbaaa2eo0mTJtmNGze2ut7G7TuisrLSnn/++W0+T/r06WOfffbZVtfV+Dkybdq0VtvG43F7880327S0tHY9T4PBoL3qqqtaXecrr7xiBw8e3K71/d///V+z61i1apUtKipqcbkdn687ns/WlJSU2GOPPbbNvg0bNsy+++67ra5r2rRpyfbTp09vta21HTs37RWLxex3vvMd67puq/uTnZ3drj42fk3OmjWrU/rY4MILL2y1jztur6Ovp470/dVXX7XDhw9v1/MUsKNHj7br16/fxT1PuPXWW9v1PO3Ifq9cubLF10Vz2vtauemmm1o9HikpKfa+++7rUH9Xr15tx40b1+p6zzzzTFtRUWGPOOKI5H0ffPBBq+tdsGCBnTBhQrvP5aBBg9pcZ1sav5Y7chkxYoT97W9/a2tra9u1nSVLltghQ4a0us4pU6bYdevWNXl+tfb39L333rOZmZktrm/H50U8HrdTp05ttQ85OTn2qaeeavP5+MQTT1hjTLuOleM49sorr7TRaLTVY9SZ57+srMwecMABrS4vIrIvUMaPyF4SDAb57W9/y3XXXcdDDz3Ea6+9xuLFiyktLSUajZKbm0u/fv2YMmUKZ511VpNCrd3BI488khwyEg6HOffcc3dpPcceeyx9+vRh48aNWGuZMWMGt912Wyf2tHNkZWXxzDPP8K9//YsZM2bw4YcfsmnTJnJychgzZgwXX3wxU6dObfGX592VkZHBY489xrXXXpt8vmzYsIHa2lp6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6UtfetjjG40GExMTh9xEREREHu+QLVrG4Lzn05/+NGvXrqXZbM5U8Ry8nStJEqy1fOITn+DrX//6zDGq+BEREZHTxTEPfv7wD/+QN7zhDaxZs4Yoijj//PP5vd/7Pa655prDHn/dddfR0dExc1u8ePGxXpKIiIicAqarfqy1BFO3rq4uvva1r3HBBRccEuocHAI550jTlP/4j/+Y+b6qf0REROR0ccyDn3/913/l85//PF/4whe4++67ufHGG/mrv/orbrzxxsMef+211zI+Pj5z6+/vP9ZLEhERkVPYvHnz+MY3vsHFF188U/lz8FQwaAVA27ZtO6GBz+ObST/h5jzuKBtN7/jMV2lOTuIP6mMkIiIicjSOeXPn3//935+p+gE4++yz2bFjB9dddx1vfvObn3B8Npslm80e62WIiIjIacIYQ3d3N1/96ld505vexDe/+U3y+TzW2pkmz9ZaOjo6TugWr6e8luGoB68H5TKp80SoWklERESenmMe/FSrVaw9tJAoOGjShoiIiMixcHCwEgQBHR0d/Pu//ztf+cpX+N73vscjjzzCzp07ATjzzDP54Ac/CLSqZR7/XmW2mxyfpH1skkxbEYM99iXbIiIicso65sHPK17xCj784Q+zZMkS1q9fzz333MNf//Vf85a3vOVYX0pEREROcweHP9Nbu173utfx+te/Hu899XodgFwuN/P9Z2LFjHMp6dgEdvECALwH9acWERGRo3HMg5+Pf/zj/Mmf/Alve9vbGBgYYMGCBfz2b/8273vf+471pUREREQOMR0EOefw3pPL5Wa+nq7yeSZO9DLRCOV9++k460y8AfsMfA4iIiJychzz4KetrY3rr7+e66+//lifWkREROSIDg50Dm7qPN3j5+CvD278/Eww96XPoW3BeXgL1hv80+gPJCIiIqe3Yx78iIiIiJwsjw9zDh7x/kzWfe5zcVjMVNzzzH42IiIiciKpN6CIiIjILOe9wXjAeVKlPiIiIvI0qOJHRERE5CQ6uNX0kTIda4MDfz+uqxEREZFTjYIfERERkZNIBTwiIiJyPOmXRiIiIiIiIiIipygFPyIiIiIiIiIipygFPyIiIiIiIiIipygFPyIiIiIiIiIipygFPyIiIiIiIiIipygFPyIiIiIiIiIipygFPyIiIiIiIiIipygFPyIiIiIiIiIipygFPyIi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BT8iIiIiIiIiInOUgh8RERERERERkTlKwY+IiIiIiIiIyByl4EdEREREREREZI5S8CMiIiIiIiIiMkcp+BERERERERERmaMU/IiIiIiIiIiIzFEKfkRERERERERE5igFPyIiIiIiIiIic5SCHxERERERERGROUrBj4iIiIiIiIjIHKXgR0RERERERETkLHTnnXfyute9joULF2KM4ctf/vKPfAwFPyIiIiIiIiIiZ6FyuczGjRv52Mc+9h8+RngK1yMiIiIiIiIiIqfILbfcwi233PKCjqGKHxERERERERGROUoVPyIiIiIiIiIip9H09PQRn2ezWbLZ7ItyLlX8iIiIiIiIiIicRkuWLKGzs3P247bbbnvRzqWKHxERERERERGR02hwcJCOjo7Zz1+sah9Q8CMiIiIiIiIiclp1dHQcEfy8mBT8iIiIiIiIiIichUqlEtu2bZv9fMeOHTzyyCP09PSwdOnSkzqGgh8RERERERERkbPQAw88wPXXXz/7+a233grAW9/6Vj796U+f1DEU/IiIiIiIiIiInIWuu+46vPcv6Bja1UtEREREREREZI5S8CMiIiIiIiIiMkcp+BERERERERERmaMU/IiIiIiIiIiIzFEKfkRERERERERE5igFPyIiIiIiIiIic5SCHxERERERERGROUrBj4iIiIiIiIjIHKXgR0RERERERERkjlLwIyIiIiIiIiIyRyn4ERERERERERGZoxT8iIiIiIiIiIjMUQp+RERERERERETmKAU/IiIiIiIiIiJzlIIfEREREREREZE5SsGPiIiIiIiIiMgcpeBHRERERERERGSOUvAjIiIiIiIiIjJHKfgREREREREREZmjFPyIiIiIiIiIiMxRCn5EREREREREROYoBT8iIiIiIiIiInOUgh8RERERERERkTlKwY+IiIiIiIiIyByl4EdEREREREREZI4Kz/QCREREREREREROt59YexGhiU7rORMfA9tP6zlV8SMiIiIiIiIiMkcp+BERERERERERmaMU/IiIiIiIiIiIzFEKfkRERERERERE5igFPyIiIiIiIiIic5SCHxERERERERGROUrBj4iIiIiIiIjIHKXgR0RERERERERkjlLwIyIiIiIiIiIyRyn4ERERERERERGZoxT8iIiIiIiIiIjMUQp+RERERERERETmKAU/IiIiIiIiIiJzlIIfEREREREREZE5SsGPiIiIiIiIiMgc9aIFPx/72MdYvnw5uVyOTZs2cd99971YpxIRERERERERkWN4UYKfz3/+89x666188IMf5KGHHmLjxo3cdNNNjIyMvBinExERERERERGRY3hRgp8Pf/jDvPOd7+Ttb387559/Pp/4xCcoFAp88pOffDFOJyIiIiIiIiIix3DKg59Go8GDDz7IDTfccOgk1nLDDTdw9913H/X8er3O9PT0ER8iIiIiIiIiIvLCnfLgZ3R0lDRNmTdv3hGPz5s3j6GhoaOef9ttt9HZ2Tn7sWTJklO9JBERERERERGRc9IZ39Xr/e9/P1NTU7Mfg4ODZ3pJIiIiIiIiIiJzQniqD9jX10cQBAwPDx/x+PDwMPPnzz/q+dlslmw2e6qXISIiIiIiIiJyzjvlFT+ZTIbLLruM22+/ffYx5xy33347V1999ak+nYiIiIiIiIiIHMcpr/gBuPXWW3nrW9/K5ZdfzpVXXslHPvIRyuUyb3/721+M04mIiIiIiIiIyDG8KMHPz/zMz3DgwAE+8IEPMDQ0xMUXX8w3vvGNowY+i4iIiIiIiIjIi+dFG+787ne/m127dlGv17n33nvZtGnTi3UqkbOSB7zzOOfBg/fNn3t/6NdbzzpjaxQREREREZGz38c+9jGWL19OLpdj06ZN3HfffSf92jO+q5fIS4X3Hu/87I8udc0fvTvsOc2wJ049cRITpwn1aoVKtUy9WiFOGsRpTJoks8dyzuOcO8GZRURERERE5Fz1+c9/nltvvZUPfvCDPPTQQ2zcuJGbbrqJkZGRk3q9gh+Rk+XBG8B4PB6smX388Cc540jrZZ78/vf58kf/lIe+8y3GhwbZs/Up7v6H/81D3/wme4eG8cYB5vRfh4iIiIiIiLxkfPjDH+ad73wnb3/72zn//PP5xCc+QaFQ4JOf/ORJvf5FmfEjMicZmoGPS3GJw9gQ0wp/PJ6DIY7xhmfu+g5f+PM/5dc++Y/0zl/AwV9esXwROz7+brY/upixV7yZxatX09HZSZTN4r3HGAVBIiIiIiIic9309PQRn2ezWbLZ7FHPazQaPPjgg7z//e+ffcxayw033MDdd999UudSxY/ICXh/qJzHAxbD9NgET95/H4NPbSVxKd47XOoOa9fyrLn2lfzGZ/4v7T09OJfiabWGdfRTueodPLh5O3/2O7/Nb/3yL/G//+ovAUjV7iUiIiIiInJOWLJkCZ2dnbMft9122zGfNzo6SpqmR22WNW/ePIaGhk7qXKr4ETmBIypwDhb1GFh70Toe/os/4Ml7VvPKn38nuXyzYmd0aB9f+pu/Zvfjj1ApzVBo76Fn6TJe+6v/iTUr1rLrqcf42Pv/G0u75rPUGEokTE5MYFAKKyIiIiIicq4YHByko6Nj9vNjVfucKgp+RE7g8IofaO7I1dnbh3eOhTe+BvvNr/PVD/8PVl71KmIMn/jgb1KcrtKVzdARRfipGcb27OBvNj/Kj7/9l7jvW//OxgVLMB76iwUyff0EB5NbdXmJiIiIiIicEzo6Oo4Ifo6nr6+PIAgYHh4+4vHh4WHmz59/UudS8CNyEpIkIQyb3y7eg8ey+PJX0bHkQup/88ds++bn+Pz3NvPqzm7qxQLWB2AM1lpMFDI6M0M8OYRNKrS35UmiiLaOPPneIv3r1+BQxY+IiIiIiIgcKZPJcNlll3H77bfzxje+EQDnHLfffjvvfve7T+oYCn5ETkKj0SCKIgCMaYY/AJ3981j/S7/B8D9/ht5wC5neDjJJFXyACSCwnjAoUi2E3PjO9zL67FaKlRJpLsu8+YvYUy5xyeq1+DSBQN+OIiIiIiIicqRbb72Vt771rVx++eVceeWVfOQjH6FcLvP2t7/9pF6vd5oiJ6FQKBz1mPfNaep98/r5iV9+L1N79tFVGceattYkaENUKJDWZrhy0xtwzrH2xjew7Tv/Qj5fYK8LufpnfpF5S1ZgjYfDdgYTERERERERAfiZn/kZDhw4wAc+8AGGhoa4+OKL+cY3vnHUwOfjUfAj8jyOt826MQZDs53LOceFN7+Gr/3lh3nddVdjjAdrmCg1GB/Ncv3NP47zMZe8/AbK1Srl6TI3vu4N5NracQfLh7SVu4iIiIiIiBzDu9/97pNu7XouBT8iJ2CMOWboc+gJgIcgtFzxsuuxNmT/009Rnp4ml8+x8qoNXLPxctra2zDe4wLLj/34G8GYZqAEBObggUREREREREROLQU/Iv8Bh4dBxhjwFms9V778Onj5dbO/1qwWAjAYa7Hez1b2HB0qKfwRERERERGRU0vBj8gpYlpTnz3gUoexBmvs0SHR0a88bWsUERERERGRc4uCH5FTyZhm+1YQABrXLCIiIiIiImeWgh+RU+CoSh5zxA8iIiIiIiIiZ4Q90wsQEREREREREZEXh4IfEREREREREZE5SsGPiIiIiIiIiMgcpeBHRERERERERGSOUvAjIiIiIiIiIjJHKfgREREREREREZmjFPyIiIiIiIiIiMxRCn5EREREREREROYoBT8iIiIiIiIiInOUgh8RERERERERkTlKwY+IiIiIiIiIyByl4EdEREREREREZI5S8CMiIiIiIiIiMkcp+BERERERERERmaMU/IiIiIiIiIiIzFEKfkRERERERERE5igFPyIiIiIiIiIic5SCHxERERERERGROSo80wt4Lu89ANPT02d4JSIiIiIiIiJz18H33Qffh59rEmI4zZeeEJ/eE3IWBj8zMzMALFmy5AyvRERERERERGTum5mZobOz80wv47TJZDLMnz+fu4b+9Yycf/78+WQymdN2PuPPsmjPOce+fftob2/HGHPC505PT7NkyRIGBwfp6Og4TSsUmXv0vSTywun7SOTU0PeSyAun7yM5Wd57ZmZmWLhwIdaeW5NgarUajUbjjJw7k8mQy+VO2/nOuoofay2LFy/+kV7T0dGh39BETgF9L4m8cPo+Ejk19L0k8sLp+0hOxrlU6XO4XC53WsOXM+ncivRERERERERERM4hCn5EREREREREROaol3Twk81m+eAHP0g2mz3TSxF5SdP3ksgLp+8jkVND30siL5y+j0TkcGfdcGcRERERERERETk1XtIVPyIiIiIiIiIicnwKfkRERERERERE5igFPyIiIiIiIiIic5SCHxERERERERGROWpOBT/Lly/HGHPExx/+4R+e6WWJnNU+9rGPsXz5cnK5HJs2beK+++4700sSeUn5nd/5naP+7Fm/fv2ZXpbIWe/OO+/kda97HQsXLsQYw5e//OUjft17zwc+8AEWLFhAPp/nhhtu4JlnnjkzixU5Sz3f99Hb3va2o/6Muvnmm8/MYkXkjJlTwQ/A7/7u77J///7Zj/e85z1nekkiZ63Pf/7z3HrrrXzwgx/koYceYuPGjdx0002MjIyc6aWJvKRccMEFR/zZc9ddd53pJYmc9crlMhs3buRjH/vYMX/9j//4j/noRz/KJz7xCe69916KxSI33XQTtVrtNK9U5Oz1fN9HADfffPMRf0Z97nOfO40rFJGzQXimF3Cqtbe3M3/+/DO9DJGXhA9/+MO8853v5O1vfzsAn/jEJ/j617/OJz/5SX7jN37jDK9O5KUjDEP92SPyI7rlllu45ZZbjvlr3ns+8pGP8Fu/9Vu84Q1vAOAzn/kM8+bN48tf/jI/+7M/ezqXKnLWOtH30UHZbFZ/Romc4+Zcxc8f/uEf0tvbyyWXXMKf/MmfkCTJmV6SyFmp0Wjw4IMPcsMNN8w+Zq3lhhtu4O677z6DKxN56XnmmWdYuHAhK1eu5Od//ufZvXv3mV6SyEvajh07GBoaOuLPqM7OTjZt2qQ/o0R+RHfccQcDAwOsW7eOX/mVX2FsbOxML0lETrM5VfHzX/7Lf+HSSy+lp6eHH/7wh7z//e9n//79fPjDHz7TSxM564yOjpKmKfPmzTvi8Xnz5rFly5YztCqRl55Nmzbx6U9/mnXr1rF//34+9KEP8fKXv5wnnniC9vb2M708kZekoaEhgGP+GXXw10Tk+d1888286U1vYsWKFTz77LP85m/+Jrfccgt33303QRCc6eWJyGly1gc/v/Ebv8Ef/dEfnfA5Tz31FOvXr+fWW2+dfWzDhg1kMhn+03/6T9x2221ks9kXe6kiInIOOrzEfsOGDWzatIlly5bxhS98gXe84x1ncGUiInKuO7wt8qKLLmLDhg2sWrWKO+64g1e96lVncGUicjqd9cHPr/3ar/G2t73thM9ZuXLlMR/ftGkTSZKwc+dO1q1b9yKsTuSlq6+vjyAIGB4ePuLx4eFh9YGLvABdXV2sXbuWbdu2nemliLxkHfxzaHh4mAULFsw+Pjw8zMUXX3yGViXy0rdy5Ur6+vrYtm2bgh+Rc8hZH/z09/fT39//H3rtI488grWWgYGBU7wqkZe+TCbDZZddxu23384b3/hGAJxz3H777bz73e8+s4sTeQkrlUo8++yzvOUtbznTSxF5yVqxYgXz58/n9ttvnw16pqenuffee/mVX/mVM7s4kZewPXv2MDY2dkSgKiJz31kf/Jysu+++m3vvvZfrr7+e9vZ27r77bt73vvfxC7/wC3R3d5/p5YmclW699Vbe+ta3cvnll3PllVfykY98hHK5PLvLl4g8v//23/4br3vd61i2bBn79u3jgx/8IEEQ8HM/93NnemkiZ7VSqXREZdyOHTt45JFH6OnpYenSpbz3ve/l93//91mzZg0rVqzgt3/7t1m4cOHsP1aIyIm/j3p6evjQhz7ET/7kTzJ//nyeffZZfv3Xf53Vq1dz0003ncFVi8jpZrz3/kwv4lR46KGH+NVf/VW2bNlCvV5nxYoVvOUtb+HWW2/VfB+RE/jLv/xL/uRP/oShoSEuvvhiPvrRj7Jp06YzvSyRl4yf/dmf5c4772RsbIz+/n5e9rKX8Qd/8AesWrXqTC9N5Kx2xx13cP311x/1+Fvf+lY+/elP473ngx/8IH/zN3/D5OQkL3vZy/irv/or1q5dewZWK3J2OtH30cc//nHe+MY38vDDDzM5OcnChQu58cYb+b3f+72jBqeLyNw2Z4IfERERERERERE5kj3TCxARERERERERkReHgh8RERERERERkTlKwY+IiIiIiIiIyByl4EdEREREREREZI5S8CMiIiIiIiIiMkcp+BERERERERERmaMU/IiIiIiIiIiIzFEKfkRERERERERE5igFPyIiIiIiIiIic5SCHxERERERERGROUrBj4iIiIiIiIjIHKXgR0RERERERERkjvr/AGdo2CbHvu0FAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Plot embeddings projection with image thumbnails\n", + "\n", + "plot_embeddings(\n", + " DataChain.from_dataset(\"fashion-curated\"), \n", + " umap_v, \n", + " image_paths, \n", + " k_samples=100)" + ] + }, + { + "cell_type": "markdown", + "id": "3228a4e8-cd03-4e1b-98b5-94caaa87850e", + "metadata": {}, + "source": [ + "# ☁️ Run in Studio (SaaS)\n", + "\n", + "\n", + " \"DataChain\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "409740b0-1104-479f-b28b-06644cc5ffd0", + "metadata": {}, + "source": [ + "To run these examples in Studio, follow the guide\n", + "\n", + "1. Open Studio / YOUR_TEAM / `datasets` workspace\n", + "2. Create a new Python Script\n", + "3. Copy/past a script from (split into few smaller scripts for convenience only)\n", + " - `scripts/2-basic-operations.py`\n", + " - `scripts/2-embeddings.py`\n", + " - `scripts/2-similarity-search.py`\n", + " - `scripts/2-remove-redundant-images.py`\n", + "5. Click the Run button\n" + ] + }, + { + "cell_type": "markdown", + "id": "0e4c4c97-e1c4-4d34-af2b-c563305eb678", + "metadata": { + "execution": { + "iopub.execute_input": "2024-06-14T15:49:22.107073Z", + "iopub.status.busy": "2024-06-14T15:49:22.106747Z", + "iopub.status.idle": "2024-06-14T15:49:22.175240Z", + "shell.execute_reply": "2024-06-14T15:49:22.174751Z", + "shell.execute_reply.started": "2024-06-14T15:49:22.107050Z" + } + }, + "source": [ + "# 🎉 Summary \n", + "\n", + "**🌟 Congratulations! You've Mastered Advanced Image DataChain Techniques! 🌟**\n", + "\n", + "In this tutorial, you've gained a wealth of knowledge and skills that will elevate your computer vision projects to new heights. Let's recap the key topics covered:\n", + "\n", + "🔍 **Basic Operations:** Connecting to image catalogs, filtering, sorting, annotating, and versioning datasets.\n", + "\n", + "🧩 **Splitting Datasets:** Dividing datasets into train, test, and validation subsets using the `train_test_split` UDF.\n", + "\n", + "🎨 **Generating & Managing Embeddings:** Calculating and saving image embeddings for advanced analysis.\n", + "\n", + "🔍 **Similarity Search:** Finding visually similar images and visualizing similarity distances.\n", + "\n", + "🧹 **Minimizing Redundant Images:** Identifying and removing redundant images for dataset optimization.\n", + "\n", + "🔍 **Visualizing Clusters:** Gaining insights into dataset structure through cluster visualization.\n", + "\n", + "## What's Next?\n", + "\n", + "Keep exploring, experimenting, and pushing the boundaries of what's possible in computer vision. Check out the next parts of our tutorial series:\n", + "- 🧠 Training Models\n", + "- 🔮 Running Inference and Saving Predictions\n", + "- 📊 Analyzing Predictions\n", + "\n", + "By mastering these techniques, you'll be well on your way to building powerful and efficient computer vision pipelines with DataChain.\n", + "\n", + "## Get Involved\n", + "\n", + "We'd love to have you join our growing community of DataChain users and contributors! Here's how you can get involved:\n", + "- ⭐ Give us a star on [GitHub](https://github.com/iterative/dvcx) to show your support\n", + "- 🌐 Visit the [dvc.ai website](https://dvc.ai/) to learn more about our products and services\n", + "- 📞 Contact us to discuss how DataChain can help streamline your company's ML workflows\n", + "- 🙌 Follow us on social media for the latest updates and insights\n", + "\n", + "Thanks for choosing DataChain, and happy coding! 😄" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/computer_vision/fashion_product_images/README.md b/examples/computer_vision/fashion_product_images/README.md new file mode 100644 index 000000000..687163169 --- /dev/null +++ b/examples/computer_vision/fashion_product_images/README.md @@ -0,0 +1,60 @@ +# Fashion Products Images 👗👔 + +DataChain is a powerful tool for managing datasets and ML workflows. This tutorial explores how DataChain helps Computer Vision projects: + +- 🗂️ Manage and version datasets and annotations effectively. +- 🔍 Handle large-scale operations, applying complex filters and transformations to millions of data entries. +- 🎨 Generating and managing embeddings +- ⏰ Save time and resources by avoiding redundant computations for previously processed samples. +- 🌊 Directly stream curated data into PyTorch for training and inference. + +Dataset + +## 📥 Download data + +The Fashion Product Images (Small) dataset for this example was loaded from [kaggle.com](https://www.kaggle.com/datasets/paramaggarwal/fashion-product-images-small/data) contributed by Param Aggarwal. + +Download data from [kaggle](https://www.kaggle.com/datasets/paramaggarwal/fashion-product-images-small) to the `data` directory. + +```bash +data/ +├──images +├── styles.csv +``` + +## 🛠️ Install + +```bash +python -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt +``` + +## 🚀 Run Jupyter Notebooks + +The tutorial is available in Jupyter Notebooks. Start Jupyter Notebook server and follow the instructions. + +```bash +jupyter notebook +``` + +## 🏃‍♂️ Run scripts (optional) + +DataChain provides a CLI tool to run Python scripts. There are a few example scripts in the `scripts` directory similar to examples in Jupyter Notebooks. + +Use `datachain query` CLI command to run scripts. + +```bash +datachain query scripts/1-quick-start.py +``` + +## 🤝 Get Involved + +We'd love to have you join our growing community of DataChain users and contributors! Here's how you can get involved: + +- ⭐ Give us a star on [GitHub](https://github.com/iterative/dvcx) to show your support +- 🌐 Visit the [dvc.ai website](https://dvc.ai/) to learn more about our products and services +- 📞 Contact us to discuss on scaling 🚀 DataChain for your project! +- 🙌 Follow us on [LinkedIn](https://www.linkedin.com/company/dvc-ai/) and [Twitter](https://x.com/DVCorg) for the latest updates and insights + +Thanks for choosing DataChain, and happy coding! 😄 diff --git a/examples/computer_vision/fashion_product_images/requirements.txt b/examples/computer_vision/fashion_product_images/requirements.txt new file mode 100644 index 000000000..8c761726f --- /dev/null +++ b/examples/computer_vision/fashion_product_images/requirements.txt @@ -0,0 +1,6 @@ +datachain +numpy +pandas +torch +torchvision +tqdm diff --git a/examples/computer_vision/fashion_product_images/scripts/1-quick-start.py b/examples/computer_vision/fashion_product_images/scripts/1-quick-start.py new file mode 100644 index 000000000..8ec466a5c --- /dev/null +++ b/examples/computer_vision/fashion_product_images/scripts/1-quick-start.py @@ -0,0 +1,91 @@ +""" +# Getting Started with DataChain + +Before you begin, ensure you have +- DataChain installed in your environment. +- Download the Fashion Product Images (Small) dataset (see README.md) +- Save data in the `data` directory + - `data/images` + - `data/styles.csv` +""" + +import pandas as pd + +from datachain.lib.dc import C, DataChain + +DATA_PATH = "data/images" +ANNOTATIONS_PATH = "data/styles.csv" + + +# Create a Dataset + +print("\n# Create a Dataset:") +ds = DataChain.from_storage(DATA_PATH, type="image").filter(C.name.glob("*.jpg")) +print(ds.show(3)) + +# Preview as a Pandas DataFrame + +print("\n# Preview as a Pandas DataFrame:") +df = ds.to_pandas() +print(df.shape) +print(df.head(3)) + + +# Create a Metadata DataChain + +print("\n# Add Metadata:") +annotations = pd.read_csv( + ANNOTATIONS_PATH, + usecols=[ + "id", + "gender", + "masterCategory", + "subCategory", + "articleType", + "baseColour", + "season", + "year", + "usage", + "productDisplayName", + ], +) + +# Preprocess columns + +annotations["baseColour"] = annotations["baseColour"].fillna("") +annotations["season"] = annotations["season"].fillna("") +annotations["usage"] = annotations["usage"].fillna("") +annotations["productDisplayName"] = annotations["productDisplayName"].fillna("") +annotations["filename"] = annotations["id"].apply(lambda s: str(s) + ".jpg") +annotations = annotations.drop("id", axis=1) + +# Create a metadata DataChain + +ds_meta = DataChain.from_pandas(annotations) +ds_meta.show(3) + +# Merge the original image and metadata datachains + +print("\n# Merge the original image and metadata datachains:") +ds_annotated = ds.merge(ds_meta, on="name", right_on="filename") + +# Save dataset + +print("\n# Save dataset:") +ds_annotated.save("fashion-product-images") + + +# Filtering Data + +print("\n# Filtering Data:") +ds = ( + DataChain.from_dataset(name="fashion-product-images") + .filter(C.mastercategory == "Apparel") + .filter(C.subcategory == "Topwear") + .filter(C.season == "Summer") +) +print(ds.to_pandas().shape) + + +# NOTE: DataChain requires the Last line to be an instance of DatasetQuery +ds.limit(3) diff --git a/examples/computer_vision/fashion_product_images/scripts/2-basic-operations.py b/examples/computer_vision/fashion_product_images/scripts/2-basic-operations.py new file mode 100644 index 000000000..4cabb1cd9 --- /dev/null +++ b/examples/computer_vision/fashion_product_images/scripts/2-basic-operations.py @@ -0,0 +1,51 @@ +from datachain.lib.dc import C, DataChain + +# Create a dataset + +print("\n# Connect to a dataset:") +ds = DataChain.from_dataset("fashion-product-images") + +# Filtering & Sorting + +print("\n# Filtering & Sorting:") +( + DataChain.from_dataset("fashion-product-images") + .select("parent", "name", "usage", "season", "year", "gender") + .filter(C.usage == "Casual" and C.season == "Summer") + .order_by("year") + .group_by("gender") + .to_pandas() +) + +# Add signals (columns) with map() method + +print("\n# Add signals (columns) with map() method:") +( + DataChain.from_dataset("fashion-product-images") + .map(prod_name_length=lambda name: len(name), output=int) + .show(3) +) + + +# Save a dataset (version) + +print("\n# Save a dataset (version):") +( + DataChain.from_dataset(name="fashion-topwear") + .map(prod_name_length=lambda name: len(name), output=int) + .save("fashion-tmp") +) + +# Save a new version (with "prod_name_length_2" column) + +print("\n# Save a new version (with prod_name_length_2 column):") +( + DataChain(name="fashion-topwear") + .map(prod_name_length_2=lambda name: len(name), output=int) + .save("fashion-tmp") +) + +# Load the latest version and show the first 3 rows + +print("\n# Load the latest version and show the first 3 rows:") +DataChain(name="fashion-tmp").limit(3) diff --git a/examples/computer_vision/fashion_product_images/scripts/2-embeddings.py b/examples/computer_vision/fashion_product_images/scripts/2-embeddings.py new file mode 100644 index 000000000..3f825581f --- /dev/null +++ b/examples/computer_vision/fashion_product_images/scripts/2-embeddings.py @@ -0,0 +1,44 @@ +import torch +from torchvision import transforms +from torchvision.models import resnet50 + +from datachain.lib.dc import DataChain +from datachain.lib.image import ImageReader + +# Helpers + +print("\n# Helpers:") +transformer = transforms.Compose( + [ + transforms.Resize((224, 224)), + transforms.ToTensor(), + transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), + ] +) +reader = ImageReader(transform=transformer) +model = resnet50(pretrained=True).eval() + +# Embeddings processor function + +print("\n# Embeddings processor function:") + + +def embeddings_processor(file) -> list[float]: + img_raw = file.get_value() + img = reader(img_raw).unsqueeze(0) + with torch.no_grad(): + emb = model(img) + + return emb[0].tolist() + + +# Compute and Save Embeddings + +print("\n# Compute and Save Embeddings:") +ds_emb = ( + DataChain(name="fashion-test") + .limit(1000) + .map(embeddings=embeddings_processor) + .save("fashion-embeddings") +) +ds_emb.limit(3) diff --git a/examples/computer_vision/fashion_product_images/scripts/3-split-train-test.py b/examples/computer_vision/fashion_product_images/scripts/3-split-train-test.py new file mode 100644 index 000000000..d64634d72 --- /dev/null +++ b/examples/computer_vision/fashion_product_images/scripts/3-split-train-test.py @@ -0,0 +1,46 @@ +from datachain.lib.dc import C, DataChain + +# Define train_test_split function + +print("\n# Define train_test_split function:") + + +def train_test_split(name) -> str: + import random + + labels = ["train", "test", "val"] + return random.choices(labels, weights=[0.7, 0.2, 0.1])[0] # noqa: S311 + + +# Add a signal (split) + +print("\n# Add a signal (split):") +ds = ( + DataChain.from_dataset("fashion-product-images") + .filter((C.masterCategory == "Apparel") & (C.subCategory == "Topwear")) + .map(split=train_test_split, params=["name"], output=str) + .save() +) + +# Print splitting details + +print("\n# Print splitting details:") +df = ds.to_pandas() +print(df.head(5)) +print(df["split"].value_counts()) + + +# Save train, test and val datasets + +print("\n# Save train, test and val datasets:") +ds_train = ds.filter(C.split == "train").save("fashion-train") +ds_test = ds.filter(C.split == "test").save("fashion-test") +ds_val = ds.filter(C.split == "val").save("fashion-val") + +# Print splitting details + +print("Train dataset size: ", ds_train.to_pandas().shape) +print("Test dataset size: ", ds_test.to_pandas().shape) +print("Val dataset size: ", ds_val.to_pandas().shape) + +ds_train.limit(3) diff --git a/examples/computer_vision/fashion_product_images/src/clustering.py b/examples/computer_vision/fashion_product_images/src/clustering.py new file mode 100644 index 000000000..f2ab740bc --- /dev/null +++ b/examples/computer_vision/fashion_product_images/src/clustering.py @@ -0,0 +1,41 @@ +import random + +import numpy as np +from scipy.spatial.distance import cdist +from sklearn.cluster import KMeans + + +def select_diverse_elements(embeddings: list, num_clusters: int, top_d: int): + diverse_elements = [] + + # Prepare the embeddings (convert list(tuples) to list(list)) + embeddings = [res[0] for res in embeddings] + + # Step 1: Cluster the embeddings + kmeans = KMeans(n_clusters=num_clusters, random_state=0).fit(embeddings) + labels = kmeans.labels_ + centroids = kmeans.cluster_centers_ + + # Step 2: Calculate distances from centroids + distances = cdist(embeddings, centroids, "euclidean") + + # Step 3: Select cluster cnetroids + for i in range(num_clusters): + cluster_indices = np.where(labels == i)[0] + cluster_distances = distances[cluster_indices, i] + + # Get the index of the centroid (closest to the cluster center) + centroid_index = cluster_indices[np.argmin(cluster_distances)] + diverse_elements.append(centroid_index) + + # Select other elements randomly from the cluster, excluding the centroid + other_indices = list(set(cluster_indices) - {centroid_index}) + random.shuffle(other_indices) + number_to_select = top_d - 1 # Since we already have the centroid + + if number_to_select > len(other_indices): + diverse_elements.extend(other_indices) + else: + diverse_elements.extend(other_indices[:number_to_select]) + + return diverse_elements diff --git a/examples/computer_vision/fashion_product_images/static/images/basic-operations.png b/examples/computer_vision/fashion_product_images/static/images/basic-operations.png new file mode 100644 index 0000000000000000000000000000000000000000..1ebfdc0192e41c32e2c90ca71bffeb582dfac109 GIT binary patch literal 311513 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 XFrpT`HUM(? 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%0A9(vBWIz9@PE)}x4Y$#380YVHPO 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 b%0a@o=|fJ8^dqh`Q1E)X`njxgN@xNAU6$&* literal 0 HcmV?d00001 diff --git a/examples/computer_vision/fashion_product_images/static/images/core-concepts.png b/examples/computer_vision/fashion_product_images/static/images/core-concepts.png new file mode 100644 index 0000000000000000000000000000000000000000..ec6ad03fc891605ad4e5156d2a1fed277a42c936 GIT binary patch literal 132975 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

*7w?#KvajS|$b$BWC!j1+via5oy2!kf;R$ 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%0ANG0 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 bF5D#9j%*|MtIta&-bVSf#;F`d^ML;WuhX3Y literal 0 HcmV?d00001 diff --git a/examples/computer_vision/fashion_product_images/static/images/dataset-1.png b/examples/computer_vision/fashion_product_images/static/images/dataset-1.png new file mode 100644 index 0000000000000000000000000000000000000000..7bfbb3aa51916eb43cacd5be67d1f9b3c6be4778 GIT binary patch literal 180966 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

    ItFZF>m8s8npIdmz>DgJS+s^o< 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 z%0D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

    vp6wOCMDFrQs^d;Zt?kzcX4 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 KA@YIK-~SI@q*J{B literal 0 HcmV?d00001 diff --git a/examples/hf_pipeline.py b/examples/hf_pipeline.py new file mode 100644 index 000000000..48a19251f --- /dev/null +++ b/examples/hf_pipeline.py @@ -0,0 +1,98 @@ +# pip install torch +import torch + +from datachain.lib.hf_pipeline import ImageHelper, RawHelper, TextHelper +from datachain.query import C, DatasetQuery + +image_source = "gs://dvcx-datalakes/dogs-and-cats/" +audio_source = "gs://dvcx-datalakes/speech-emotion-recognition-dataset/" +text_source = "gs://dvcx-datalakes/NLP/cnn/stories" + +if torch.cuda.is_available(): + device = "cuda" +else: + device = "cpu" + + +if __name__ == "__main__": + print("** HuggingFace pipeline helper model zoo demo **") + print("\nZero-shot object detection and classification:") + results = ( + DatasetQuery( + image_source, + anon=True, + ) + .filter(C.name.glob("*.jpg")) + .limit(1) + .add_signals( + ImageHelper( + model="google/owlv2-base-patch16", + device=device, + candidate_labels=["cat", "dog", "squirrel", "unknown"], + ), + parallel=False, + ) + .select("source", "parent", "name", "model_output", "error") + .results() + ) + print(*results, sep="\n") + + print("\nNot-safe-for-work image detection:") + results = ( + DatasetQuery( + image_source, + anon=True, + ) + .filter(C.name.glob("*.jpg")) + .limit(1) + .add_signals( + ImageHelper( + model="Falconsai/nsfw_image_detection", + device=device, + ), + parallel=False, + ) + .select("source", "parent", "name", "model_output", "error") + .results() + ) + print(*results, sep="\n") + + print("\nAudio emotion classification:") + results = ( + DatasetQuery( + audio_source, + anon=True, + ) + .filter(C.name.glob("*.wav")) + .limit(1) + .add_signals( + RawHelper( + model="Krithika-p/my_awesome_emotions_model", + device=device, + ), + parallel=False, + ) + .select("source", "parent", "name", "model_output", "error") + .results() + ) + print(*results, sep="\n") + print("\nLong text summarization:") + results = ( + DatasetQuery( + text_source, + anon=True, + ) + .filter(C.name.glob("*.story")) + .limit(1) + .add_signals( + TextHelper( + model="pszemraj/led-large-book-summary", + device=device, + max_length=150, + ), + parallel=False, + ) + .select("source", "parent", "name", "model_output", "error") + .results() + ) + print(*results, sep="\n") diff --git a/examples/iptc_exif_xmp_lib.py b/examples/iptc_exif_xmp_lib.py new file mode 100644 index 000000000..528f42034 --- /dev/null +++ b/examples/iptc_exif_xmp_lib.py @@ -0,0 +1,15 @@ +from datachain.lib.iptc_exif_xmp import GetMetadata +from datachain.query import C, DatasetQuery + +source = "gs://dvcx-datalakes/open-images-v6/" + +if __name__ == "__main__": + results = ( + DatasetQuery(source) + .filter(C.name.glob("*.jpg")) + .limit(10000) + .add_signals(GetMetadata, parallel=True) + .select("source", "xmp", "exif", "iptc", "error") + .results() + ) + print(*results, sep="\n") diff --git a/examples/json-csv-reader.py b/examples/json-csv-reader.py new file mode 100644 index 000000000..459fd99e5 --- /dev/null +++ b/examples/json-csv-reader.py @@ -0,0 +1,119 @@ +# +# TODO: +# refactor lib/meta_formats/read_scema into a Datachain method +# +# ER: add support for Optional fields in read_schema() +# ER: add support for headless CSV within static schema only +# ER: fix the bug in datamodel-codegen failing to recognize csv float and int columns +# +# Open issues: +# 1. A single filename cannot be passed as schema source (#1563) +# 2. Need syntax like "file.open(encoding='utf-8')" to avoid "type=text" (#1614) +# 3. Need syntax like "datachain.collate(func -> Any)" (#1615) +# 4. "Feature" does not tolerate creating a class twice (#1617) +# 5. Unsure how to deal with 'folder' pseudo-files in cloud systems(#1618) +# 6. There should be exec() method to force-run the existing chain (#1616) +# 7. data-model-codegenerator: datamodel-codegen reports all CSV fields as 'str'. +# 8. from_json and from_csv methods do not filter empty files from AWS +# dependencies: +# pip install datamodel-code-generator +# pip install jmespath + +from typing import Optional + +from pydantic import BaseModel + +from datachain.lib.dc import C, DataChain +from datachain.lib.feature_utils import pydantic_to_feature + + +# Sample model for static JSON model +class LicenseModel(BaseModel): + url: str + id: int + name: str + + +LicenseFeature = pydantic_to_feature(LicenseModel) + + +# Sample model for static CSV model +class ChatDialog(BaseModel): + id: Optional[int] = None + count: Optional[int] = None + sender: Optional[str] = None + text: Optional[str] = None + + +ChatFeature = pydantic_to_feature(ChatDialog) + + +def main(): + print() + print("========================================================================") + print("Dynamic JSONl schema from 2 objects") + print("========================================================================") + uri = "gs://datachain-demo/jsonl/object.jsonl" + jsonl_ds = DataChain.from_json(uri, meta_type="jsonl", show_schema=True) + print(jsonl_ds.to_pandas()) + + print() + print("========================================================================") + print("Dynamic JSON schema from 200 OpenImage json-pairs with validation errors") + print("========================================================================") + uri = "gs://datachain-demo/openimages-v6-test-jsonpairs/*json" + schema_uri = ( + "gs://datachain-demo/openimages-v6-test-jsonpairs/08392c290ecc9d2a.json" + ) + json_pairs_ds = DataChain.from_json( + uri, schema_from=schema_uri, jmespath="@", model_name="OpenImage" + ) + print(json_pairs_ds.to_pandas()) + # print(json_pairs_ds.collect()[0]) + + uri = "gs://datachain-demo/coco2017/annotations_captions/" + + print() + print("========================================================================") + print("Reading JSON schema from main COCO annotation") + print("========================================================================") + chain = ( + DataChain.from_storage(uri) + .filter(C.name.glob("*.json")) + .show_json_schema(jmespath="@", model_name="Coco") + ) + chain.save() + + print() + print("========================================================================") + print("static JSON schema test parsing 7 objects") + print("========================================================================") + static_json_ds = DataChain.from_json(uri, jmespath="licenses", spec=LicenseFeature) + print(static_json_ds.to_pandas()) + + print() + print("========================================================================") + print("dynamic JSON schema test parsing 5K objects") + print("========================================================================") + dynamic_json_ds = DataChain.from_json(uri, jmespath="images", show_schema=True) + print(dynamic_json_ds.to_pandas()) + + uri = "gs://datachain-demo/chatbot-csv/" + print() + print("========================================================================") + print("static CSV with header schema test parsing 3.5K objects") + print("========================================================================") + static_csv_ds = DataChain.from_csv(uri, spec=ChatFeature) + print(static_csv_ds.to_pandas()) + + uri = "gs://datachain-demo/laion-aesthetics-csv" + print() + print("========================================================================") + print("dynamic CSV with header schema test parsing 3M objects") + print("========================================================================") + dynamic_csv_ds = DataChain.from_csv(uri, object_name="laion", show_schema=True) + print(dynamic_csv_ds.to_pandas()) + + +if __name__ == "__main__": + main() diff --git a/examples/llava2_image_desc_lib.py b/examples/llava2_image_desc_lib.py new file mode 100644 index 000000000..62ae1f5d3 --- /dev/null +++ b/examples/llava2_image_desc_lib.py @@ -0,0 +1,43 @@ +# pip install torch +import torch + +from datachain.lib.hf_image_to_text import LLaVAdescribe +from datachain.query import C, DatasetQuery + +model = "llava-hf/llava-1.5-7b-hf" + +# HuggingFace supports the following base models: +# +# "llava-hf/llava-1.5-7b-hf" +# "llava-hf/llava-1.5-13b-hf" +# "llava-hf/bakLlava-v1-hf" +# +# https://huggingface.co/llava-hf + +source = "gs://dvcx-datalakes/dogs-and-cats/" + +# device='mps' not supported +if torch.cuda.is_available(): + device = "cuda" +else: + device = "cpu" + +if __name__ == "__main__": + results = ( + DatasetQuery( + source, + anon=True, + ) + .filter(C.name.glob("cat*.jpg")) + .limit(2) + .add_signals( + LLaVAdescribe( + device=device, + model=model, + ), + parallel=False, + ) + .select("source", "parent", "name", "description", "error") + .results() + ) + print(*results, sep="\n") diff --git a/examples/llm-claude-aggregate-query.py b/examples/llm-claude-aggregate-query.py new file mode 100644 index 000000000..aadcf73ea --- /dev/null +++ b/examples/llm-claude-aggregate-query.py @@ -0,0 +1,40 @@ +import pandas as pd + +from datachain.lib.claude import claude_processor +from datachain.lib.dc import C, DataChain +from datachain.sql.functions import path + +SOURCE = "gs://dvcx-datalakes/chatbot-public" +MODEL = "claude-3-opus-20240229" +PROMPT = """Consider the following dialogues between the 'user' and the 'bot' separated\ + by '===='. The 'user' is a human trying to find the best mobile plan. The 'bot' is a \ +chatbot designed to query the user and offer the best solution. The dialog is \ +successful if the 'bot' is able to gather the information and offer a plan, or inform \ +the user that such plan does not exist. The dialog is not successful if the \ +conversation ends early or the 'user' requests additional functions the 'bot' \ +cannot perform. Read the dialogues and classify them into a fixed number of concise \ +failure reasons covering most failure cases. Present output as JSON list of reason \ +strings and nothing else. +""" + + +chain = ( + DataChain.from_storage(SOURCE, is_text=True) + .filter(C.name.glob("*.txt")) + .limit(5) + .agg( + dialogues=lambda file: ["\n=====\n".join(f.get_value() for f in file)], + output=str, + partition_by=path.file_ext(C.name), + ) + .map(claude=claude_processor(prompt=PROMPT, model=MODEL), params="dialogues") + .map( + res=lambda claude: [str(claude.content[0].text) if claude.content else ""], + output=str, + ) +) + +df = chain.to_pandas() + +with pd.option_context("display.max_columns", None): + print(df) diff --git a/examples/llm-claude-simple-query.py b/examples/llm-claude-simple-query.py new file mode 100644 index 000000000..5a59e7816 --- /dev/null +++ b/examples/llm-claude-simple-query.py @@ -0,0 +1,47 @@ +import json + +import pandas as pd + +from datachain.lib.claude import claude_processor +from datachain.lib.dc import C, DataChain +from datachain.lib.feature import Feature + +SOURCE = "gs://dvcx-datalakes/chatbot-public" +MODEL = "claude-3-opus-20240229" +PROMPT = """Consider the dialogue between the 'user' and the 'bot'. \ +The 'user' is a human trying to find the best mobile plan. \ +The 'bot' is a chatbot designed to query the user and offer the \ +best solution. The dialog is successful if the 'bot' is able to \ +gather the information and offer a plan, or inform the user that \ +such plan does not exist. The dialog is not successful if the \ +conversation ends early or the 'user' requests additional functions \ +the 'bot' cannot perform. Read the dialogue below and rate it 'Success' \ +if it is successful, and 'Failure' if not. After that, provide \ +one-sentence explanation of the reasons for this rating. Use only \ +JSON object as output with the keys 'status', and 'explanation'. +""" + + +class Rating(Feature): + status: str = "" + explanation: str = "" + + +chain = ( + DataChain.from_storage(SOURCE, type="text") + .filter(C.name.glob("*.txt")) + .settings(parallel=3) + .limit(5) + .map(claude=claude_processor(prompt=PROMPT, model=MODEL)) + .map( + rating=lambda claude: Rating( + **(json.loads(claude.content[0].text) if claude.content else {}) + ), + output=Rating, + ) +) + +df = chain.to_pandas() + +with pd.option_context("display.max_columns", None): + print(df) diff --git a/examples/llm-claude.py b/examples/llm-claude.py new file mode 100644 index 000000000..7818c4c19 --- /dev/null +++ b/examples/llm-claude.py @@ -0,0 +1,21 @@ +import pandas as pd + +from datachain.lib.claude import claude_processor +from datachain.lib.dc import C, DataChain + +SOURCE = "gs://dvcx-datalakes/chatbot-public" +MODEL = "claude-3-opus-20240229" +PROMPT = """Summarise the dialog in a sentence""" + + +chain = ( + DataChain.from_storage(SOURCE, is_text=True) + .filter(C.name.glob("*.txt")) + .limit(5) + .map(claude=claude_processor(prompt=PROMPT, model=MODEL)) +) + +df = chain.to_pandas() + +with pd.option_context("display.max_columns", None): + print(df) diff --git a/examples/loader.py b/examples/loader.py new file mode 100644 index 000000000..5ab25b421 --- /dev/null +++ b/examples/loader.py @@ -0,0 +1,31 @@ +""" +A simple data loader example. + +This downloads and displays the first 5 images of the dataset. +""" + +from contextlib import closing + +from datachain.catalog import get_catalog +from datachain.error import DatasetNotFoundError +from datachain.lib.param import Image +from datachain.query import C, DatasetQuery + +catalog = get_catalog() +try: + ds = catalog.get_dataset("cats") +except DatasetNotFoundError: + ds = ( + DatasetQuery( + path="gs://dvcx-datalakes/dogs-and-cats/", + catalog=catalog, + ) + .filter(C.name.glob("*cat*.jpg")) + .save("cats") + ) + + +images = ds.limit(5).extract(Image(), cache=False) +with closing(images): + for (img,) in images: + img.show() diff --git a/examples/multimodal/clip_fine_tuning.ipynb b/examples/multimodal/clip_fine_tuning.ipynb new file mode 100644 index 000000000..e49cb1cd0 --- /dev/null +++ b/examples/multimodal/clip_fine_tuning.ipynb @@ -0,0 +1,1700 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "c565c64c-c915-4911-a397-39f84765bae5", + "metadata": {}, + "source": [ + "# Fine tuning CLIP for image captions\n", + "\n", + "This notebook will show how to use DataChain for multimodal AI, including how to:\n", + "- [Ingest multimodal data like images and text.](#ingesting)\n", + "- [Join those data into a coherent dataset.](#joining)\n", + "- [Filter and inspect the data.](#filtering)\n", + "- [Calculate similarities between images and text.](#similarities)\n", + "- [Save the chain results as a dataset](#saving)\n", + "- [Apply/map functions to transform the data and chain transformations.](#mapping)\n", + "- [Train models from the chained data.](#training)\n", + "- [Perform inference and calculate evaluation metrics.](#eval)" + ] + }, + { + "cell_type": "markdown", + "id": "4de8b887-760e-4a20-995b-f84f2aa4699e", + "metadata": {}, + "source": [ + "\n", + "\n", + "## Setup\n", + "\n", + "To start, install the dependencies and import the packages needed." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "d3cbfcbe-4a9c-4c9f-93f6-cd3c6e899c26", + "metadata": {}, + "outputs": [], + "source": [ + "!pip install -q ipywidgets datachain git+https://github.com/openai/CLIP.git" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "d75242f5-7514-4ce0-b6ad-4b2defefd73d", + "metadata": {}, + "outputs": [], + "source": [ + "import io\n", + "import os\n", + "import string\n", + "\n", + "import clip\n", + "import torch\n", + "from datachain.lib.clip import similarity_scores\n", + "from datachain.lib.dc import C, DataChain\n", + "from torch.utils.data import DataLoader" + ] + }, + { + "cell_type": "markdown", + "id": "e1edc95a-a155-440b-82ab-2a0bb116f39d", + "metadata": {}, + "source": [ + "\n", + "\n", + "## Get data and create DataChain\n", + "\n", + "We will use a dataset from https://www.capcon.dev/, which is based on the New Yorker Cartoon Caption Contest. We use the \"matching\" dataset, which pairs a cartoon image with several possible jokes as caption choices. By the end of this notebook, we will try to predict the correct joke that matches the cartoon from each set of multiple choices.\n", + "\n", + "To start, we have two data sources in `gs://datachain-demo/newyorker_caption_contest`:\n", + "1. `images`: A folder of JPEG files, each representing a cartoon image.\n", + "2. `new_yorker_meta.parquet`: A parquet file with metadata about the images, including multiple choices of captions for the image and the correct caption choice." + ] + }, + { + "cell_type": "markdown", + "id": "6b59ee66-79af-4a12-9552-1822e704c24f", + "metadata": {}, + "source": [ + "We can create a DataChain from each of the two data sources.\n", + "1. DataChain can create a single dataset from many files in a storage bucket, folder, or other path. DataChain will generate one record per file, with each record including a `file` signal that saves metadata needed to read the file. In that way, DataChain keeps a link to the original storage without having to copy the files or load all file contents in memory.\n", + "2. DataChain can also create a dataset from other traditional data sources like parquet files without copying those files or loading them entirely in memory. This provides an efficient way to connect data from a variety of sources.\n", + "\n", + "

    \n", + "Note about the dataset format\n", + "We have slightly modified the dataset from its original form in Hugging Face. The original dataset keeps the entire contents of each image embedded as a column in parquet files. That will not scale easily to large datasets, which is why DataChain keeps the image files in their original locations and reads from them as needed. We have dropped the image contents from the parquet files and only kept the metadata. The images are instead saved as regular JPEG files.\n", + "
    " + ] + }, + { + "cell_type": "markdown", + "id": "b329a883-c1d5-4552-9ee1-3e00833f0bee", + "metadata": {}, + "source": [ + "### From storage\n", + "\n", + "To create a chain from a directory of files, use `DataChain.from_storage()` and point to the location of the directory. Use the `type` argument to specify a type like `binary`, `text`, or `image`, which will tell DataChain how to open these files later in the chain:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "9d84cfe3-9aaf-4a0e-8449-89983b412ade", + "metadata": {}, + "outputs": [], + "source": [ + "img_dc = DataChain.from_storage(\"gs://datachain-demo/newyorker_caption_contest/images\", type=\"image\")" + ] + }, + { + "cell_type": "markdown", + "id": "a93e734b-a491-474a-9884-580adccb6d50", + "metadata": {}, + "source": [ + "You may be wondering why you don't see any results. DataChain tries to compute the entire chain lazily, waiting until you explicitly ask for a result before executing. To preview the results, use `DataChain.show()`:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "c40feba6-929a-4a0f-af8e-028c048a644c", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Processed: 422 rows [00:00, 23553.16 rows/s]\n" + ] + }, + { + "data": { + "text/html": [ + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    idrandomvtypedir_typeparentnameetagversionis_latestlast_modified...file.sourcefile.parentfile.namefile.sizefile.versionfile.etagfile.is_latestfile.last_modifiedfile.locationfile.vtype
    0172408970257939842620newyorker_caption_contest/images101.jpegCMeLzNiXhocDEAE=171984870408339912024-07-01 15:45:04.128000+00:00...gs://datachain-demonewyorker_caption_contest/images101.jpeg260231719848704083399CMeLzNiXhocDEAE=11970-01-01 00:00:00+00:00None
    1222825539128402706460newyorker_caption_contest/images102.jpegCIWl09iXhocDEAE=171984870420134912024-07-01 15:45:04.231000+00:00...gs://datachain-demonewyorker_caption_contest/images102.jpeg101141719848704201349CIWl09iXhocDEAE=11970-01-01 00:00:00+00:00None
    2377560635072470683660newyorker_caption_contest/images104.jpegCJv+5NiXhocDEAE=171984870449129112024-07-01 15:45:04.538000+00:00...gs://datachain-demonewyorker_caption_contest/images104.jpeg182541719848704491291CJv+5NiXhocDEAE=11970-01-01 00:00:00+00:00None
    \n", + "
    " + ], + "text/plain": [ + " id random vtype dir_type parent \\\n", + "0 1 7240897025793984262 0 newyorker_caption_contest/images \n", + "1 2 2282553912840270646 0 newyorker_caption_contest/images \n", + "2 3 7756063507247068366 0 newyorker_caption_contest/images \n", + "\n", + " name etag version is_latest \\\n", + "0 101.jpeg CMeLzNiXhocDEAE= 1719848704083399 1 \n", + "1 102.jpeg CIWl09iXhocDEAE= 1719848704201349 1 \n", + "2 104.jpeg CJv+5NiXhocDEAE= 1719848704491291 1 \n", + "\n", + " last_modified ... file.source \\\n", + "0 2024-07-01 15:45:04.128000+00:00 ... gs://datachain-demo \n", + "1 2024-07-01 15:45:04.231000+00:00 ... gs://datachain-demo \n", + "2 2024-07-01 15:45:04.538000+00:00 ... gs://datachain-demo \n", + "\n", + " file.parent file.name file.size file.version \\\n", + "0 newyorker_caption_contest/images 101.jpeg 26023 1719848704083399 \n", + "1 newyorker_caption_contest/images 102.jpeg 10114 1719848704201349 \n", + "2 newyorker_caption_contest/images 104.jpeg 18254 1719848704491291 \n", + "\n", + " file.etag file.is_latest file.last_modified file.location \\\n", + "0 CMeLzNiXhocDEAE= 1 1970-01-01 00:00:00+00:00 None \n", + "1 CIWl09iXhocDEAE= 1 1970-01-01 00:00:00+00:00 None \n", + "2 CJv+5NiXhocDEAE= 1 1970-01-01 00:00:00+00:00 None \n", + "\n", + " file.vtype \n", + "0 \n", + "1 \n", + "2 " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[limited by 3 objects]\n" + ] + } + ], + "source": [ + "img_dc.show(3)" + ] + }, + { + "cell_type": "markdown", + "id": "da18e23e-5626-4730-915b-519b18cb0cf4", + "metadata": {}, + "source": [ + "DataChain created a record for each file in the directory, generating a `file` signal for each file. The file signal contains subsignals with metadata about each file, like `file.name` and `file.size`. Aggregate signals like `file` that contain multiple subsignals are called features.\n", + "\n", + "You can use the `file` feature to not only get metadata about each file, but also to open and read the file as needed. We will come back to this later in the notebook." + ] + }, + { + "cell_type": "markdown", + "id": "1a487749-e492-4cdf-ae39-de729f7e1926", + "metadata": {}, + "source": [ + "### From parquet\n", + "\n", + "Use `DataChain.parse_parquet()` to load data from a dataset of any number of parquet files (you can also read the data into another library like pandas and use `DataChain.from_dataframe()`). Here we use it to load the metadata about the cartoons, including the text for all caption choices." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "1ae89eb2-153e-4a06-a24d-187d3baa8b21", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Processed: 1 rows [00:00, 1515.28 rows/s]\n" + ] + } + ], + "source": [ + "meta_dc = DataChain.from_storage(\"gs://datachain-demo/newyorker_caption_contest/new_yorker_meta.parquet\").parse_parquet()" + ] + }, + { + "cell_type": "markdown", + "id": "86ba6dff-ff4b-4cbf-ba57-8cd19b342176", + "metadata": {}, + "source": [ + "Let's preview the parquet data:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "1cddbffb-77f2-4c76-96ce-d9ab8a9c3e20", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Processed: 1 rows [00:00, 1312.36 rows/s]\n", + "Processed: 0 rows [00:00, ? rows/s]\n", + "Generated: 0 rows [00:00, ? rows/s]\u001b[A\n", + "Processed: 1 rows [00:00, 2.34 rows/s]ows/s]\u001b[A\n", + "Generated: 9792 rows [00:00, 22919.32 rows/s]\n" + ] + }, + { + "data": { + "text/html": [ + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    idrandomsource.file.sourcesource.file.parentsource.file.namesource.file.sizesource.file.versionsource.file.etagsource.file.is_latestsource.file.last_modified...image_descriptionimage_uncanny_descriptionentitiesquestionscaption_choicesfrom_descriptionlabeln_tokens_labelinstance_idfilename
    013891531844767248658gs://datachain-demonewyorker_caption_contestnew_yorker_meta.parquet37656801719847348473242CJqbmNKShocDEAE=11970-01-01 00:00:00+00:00...Two priests and a rabbi are walking into a bar...The scene depicts a very stereotypical \"bar jo...[https://en.wikipedia.org/wiki/Rule_of_three_(...[What is the bartender saying on the phone in ...[Tell me about your childhood very quickly., B...scene: a bar description: Two priests and a ra...C121125bb8787b4e7e82aa3b0a1cba157149.jpeg
    124531363468689655038gs://datachain-demonewyorker_caption_contestnew_yorker_meta.parquet37656801719847348473242CJqbmNKShocDEAE=11970-01-01 00:00:00+00:00...Two men are standing on clouds, one is dressed...Seeing two men standing on clouds and holding/...[https://en.wikipedia.org/wiki/God, https://en...[Why are these men standing on clouds holding/...[Business school changed you, Son., I'm a thro...scene: clouds description: Two men are standin...A187056539d887b9b38cb33ee8101c2c99393.jpeg
    23842984054593741747gs://datachain-demonewyorker_caption_contestnew_yorker_meta.parquet37656801719847348473242CJqbmNKShocDEAE=11970-01-01 00:00:00+00:00...An alligator is coming out of the floor. Two p...There is an alligator coming out of the floor.[https://en.wikipedia.org/wiki/Alligator, http...[Why is an alligator in a restaurant?][Where were you between the hours of beddy-bye...scene: restaurant description: An alligator is...D13ef377604288fc61e83dd2341f1567de703.jpeg
    \n", + "
    " + ], + "text/plain": [ + " id random source.file.source source.file.parent \\\n", + "0 1 3891531844767248658 gs://datachain-demo newyorker_caption_contest \n", + "1 2 4531363468689655038 gs://datachain-demo newyorker_caption_contest \n", + "2 3 842984054593741747 gs://datachain-demo newyorker_caption_contest \n", + "\n", + " source.file.name source.file.size source.file.version \\\n", + "0 new_yorker_meta.parquet 3765680 1719847348473242 \n", + "1 new_yorker_meta.parquet 3765680 1719847348473242 \n", + "2 new_yorker_meta.parquet 3765680 1719847348473242 \n", + "\n", + " source.file.etag source.file.is_latest source.file.last_modified ... \\\n", + "0 CJqbmNKShocDEAE= 1 1970-01-01 00:00:00+00:00 ... \n", + "1 CJqbmNKShocDEAE= 1 1970-01-01 00:00:00+00:00 ... \n", + "2 CJqbmNKShocDEAE= 1 1970-01-01 00:00:00+00:00 ... \n", + "\n", + " image_description \\\n", + "0 Two priests and a rabbi are walking into a bar... \n", + "1 Two men are standing on clouds, one is dressed... \n", + "2 An alligator is coming out of the floor. Two p... \n", + "\n", + " image_uncanny_description \\\n", + "0 The scene depicts a very stereotypical \"bar jo... \n", + "1 Seeing two men standing on clouds and holding/... \n", + "2 There is an alligator coming out of the floor. \n", + "\n", + " entities \\\n", + "0 [https://en.wikipedia.org/wiki/Rule_of_three_(... \n", + "1 [https://en.wikipedia.org/wiki/God, https://en... \n", + "2 [https://en.wikipedia.org/wiki/Alligator, http... \n", + "\n", + " questions \\\n", + "0 [What is the bartender saying on the phone in ... \n", + "1 [Why are these men standing on clouds holding/... \n", + "2 [Why is an alligator in a restaurant?] \n", + "\n", + " caption_choices \\\n", + "0 [Tell me about your childhood very quickly., B... \n", + "1 [Business school changed you, Son., I'm a thro... \n", + "2 [Where were you between the hours of beddy-bye... \n", + "\n", + " from_description label n_tokens_label \\\n", + "0 scene: a bar description: Two priests and a ra... C 1 \n", + "1 scene: clouds description: Two men are standin... A 1 \n", + "2 scene: restaurant description: An alligator is... D 1 \n", + "\n", + " instance_id filename \n", + "0 21125bb8787b4e7e82aa3b0a1cba1571 49.jpeg \n", + "1 87056539d887b9b38cb33ee8101c2c99 393.jpeg \n", + "2 3ef377604288fc61e83dd2341f1567de 703.jpeg " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[limited by 3 objects]\n" + ] + } + ], + "source": [ + "meta_dc.show(3)" + ] + }, + { + "cell_type": "markdown", + "id": "0a21b5c8-6474-41b9-b4ef-1a462a6d39db", + "metadata": {}, + "source": [ + "DataChain created a record for each row in the parquet dataset, including a `source` signal similar to the `file` signal above that stores information about the source parquet file, as well as signals for all of the columns in the parquet file.\n", + "\n", + "Since we are not interested in the `source` at the moment, we can select only the other columns to preview using `DataChain.select()` or `DataChain.select_except()`. For aggregate features like `source`, you can select a signal for either the entire feature (like `source`) or any individual subsignal (like `source.index`)." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "217c1abe-637e-4f8d-a4e6-49cd8d1240df", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Processed: 1 rows [00:00, 1562.12 rows/s]\n", + "Processed: 0 rows [00:00, ? rows/s]\n", + "Processed: 1 rows [00:00, 2.62 rows/s]\n", + "Generated: 9792 rows [00:00, 25776.10 rows/s]\n" + ] + }, + { + "data": { + "text/html": [ + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    contest_numberimage_locationimage_descriptionimage_uncanny_descriptionentitiesquestionscaption_choicesfrom_descriptionlabeln_tokens_labelinstance_idfilename
    049a barTwo priests and a rabbi are walking into a bar...The scene depicts a very stereotypical \"bar jo...[https://en.wikipedia.org/wiki/Rule_of_three_(...[What is the bartender saying on the phone in ...[Tell me about your childhood very quickly., B...scene: a bar description: Two priests and a ra...C121125bb8787b4e7e82aa3b0a1cba157149.jpeg
    1393cloudsTwo men are standing on clouds, one is dressed...Seeing two men standing on clouds and holding/...[https://en.wikipedia.org/wiki/God, https://en...[Why are these men standing on clouds holding/...[Business school changed you, Son., I'm a thro...scene: clouds description: Two men are standin...A187056539d887b9b38cb33ee8101c2c99393.jpeg
    2703restaurantAn alligator is coming out of the floor. Two p...There is an alligator coming out of the floor.[https://en.wikipedia.org/wiki/Alligator, http...[Why is an alligator in a restaurant?][Where were you between the hours of beddy-bye...scene: restaurant description: An alligator is...D13ef377604288fc61e83dd2341f1567de703.jpeg
    \n", + "
    " + ], + "text/plain": [ + " contest_number image_location \\\n", + "0 49 a bar \n", + "1 393 clouds \n", + "2 703 restaurant \n", + "\n", + " image_description \\\n", + "0 Two priests and a rabbi are walking into a bar... \n", + "1 Two men are standing on clouds, one is dressed... \n", + "2 An alligator is coming out of the floor. Two p... \n", + "\n", + " image_uncanny_description \\\n", + "0 The scene depicts a very stereotypical \"bar jo... \n", + "1 Seeing two men standing on clouds and holding/... \n", + "2 There is an alligator coming out of the floor. \n", + "\n", + " entities \\\n", + "0 [https://en.wikipedia.org/wiki/Rule_of_three_(... \n", + "1 [https://en.wikipedia.org/wiki/God, https://en... \n", + "2 [https://en.wikipedia.org/wiki/Alligator, http... \n", + "\n", + " questions \\\n", + "0 [What is the bartender saying on the phone in ... \n", + "1 [Why are these men standing on clouds holding/... \n", + "2 [Why is an alligator in a restaurant?] \n", + "\n", + " caption_choices \\\n", + "0 [Tell me about your childhood very quickly., B... \n", + "1 [Business school changed you, Son., I'm a thro... \n", + "2 [Where were you between the hours of beddy-bye... \n", + "\n", + " from_description label n_tokens_label \\\n", + "0 scene: a bar description: Two priests and a ra... C 1 \n", + "1 scene: clouds description: Two men are standin... A 1 \n", + "2 scene: restaurant description: An alligator is... D 1 \n", + "\n", + " instance_id filename \n", + "0 21125bb8787b4e7e82aa3b0a1cba1571 49.jpeg \n", + "1 87056539d887b9b38cb33ee8101c2c99 393.jpeg \n", + "2 3ef377604288fc61e83dd2341f1567de 703.jpeg " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[limited by 3 objects]\n" + ] + } + ], + "source": [ + "meta_dc.select_except(\"source\").show(3)" + ] + }, + { + "cell_type": "markdown", + "id": "d5d64b2c-60b2-4f54-ba6e-3c5b1e865c84", + "metadata": {}, + "source": [ + "\n", + "\n", + "### Joining\n", + "\n", + "To compare the images to the text caption choices, we need to join both chains by matching the rows using the name of the image file. For `img_dc`, this field is captured automatically in `file.name`. For `meta_dc`, the `filename` is a column in the parquet file identifying the corresponding JPEG file." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "750455a9-62b8-46a0-b438-c07561a598d9", + "metadata": {}, + "outputs": [], + "source": [ + "dc = img_dc.merge(meta_dc, on=\"file.name\", right_on=\"filename\")" + ] + }, + { + "cell_type": "markdown", + "id": "24f639bf-ecc8-457a-b515-dd7e340bfda5", + "metadata": {}, + "source": [ + "The resulting chain includes the union of all columns from both of the initial chains. Since this is a lot of columns, let's take a look at just a subset of columns of interest:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "3cff46f4-74a5-48b7-884d-533100669b1b", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Processed: 422 rows [00:00, 21900.74 rows/s]\n", + "Processed: 1 rows [00:00, 921.83 rows/s]\n", + "Processed: 0 rows [00:00, ? rows/s]\n", + "Processed: 1 rows [00:00, 2.58 rows/s]\n", + "Generated: 9792 rows [00:00, 25325.90 rows/s]\n" + ] + }, + { + "data": { + "text/html": [ + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    file.namecaption_choiceslabel
    0101.jpeg[And, if you open the window, the view will ta...C
    1101.jpeg[And, if you open the window, the view will ta...C
    2101.jpeg[And, if you open the window, the view will ta...C
    \n", + "
    " + ], + "text/plain": [ + " file.name caption_choices label\n", + "0 101.jpeg [And, if you open the window, the view will ta... C\n", + "1 101.jpeg [And, if you open the window, the view will ta... C\n", + "2 101.jpeg [And, if you open the window, the view will ta... C" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[limited by 3 objects]\n" + ] + } + ], + "source": [ + "dc.select(\"file.name\", \"caption_choices\", \"label\").show(3)" + ] + }, + { + "cell_type": "markdown", + "id": "34bb7924-4495-45fe-84f7-1417ebff0ad5", + "metadata": {}, + "source": [ + "\n", + "\n", + "## Filtering and inspecting a sample\n", + "\n", + "Let's take a look at one sample cartoon from the dataset to see how it looks.\n", + "\n", + "`DataChain.filter()` allows for filtering of the dataset. In this case, we will filter to a single JPEG image using `file.name`. There are multiple rows of metadata for each image, since there can be different combinations of image and caption choices or other metadata, so we will also limit to one output row:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "72ea9e4b-572d-4f5c-b58a-de38429d28ec", + "metadata": {}, + "outputs": [], + "source": [ + "sample = dc.filter(C(\"file.name\") == \"371.jpeg\").limit(1)" + ] + }, + { + "cell_type": "markdown", + "id": "04aa9418-87eb-45b6-9b5e-d86779d52c06", + "metadata": {}, + "source": [ + "We can use `DataChain.collect()` to extract the values from the sample. Here's an example of collecting a subset of column signals from the sample:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "8f306dee-0c5a-4112-9fb3-885755b06961", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Processed: 422 rows [00:00, 18992.19 rows/s]\n", + "Processed: 1 rows [00:00, 580.45 rows/s]\n", + "Processed: 0 rows [00:00, ? rows/s]\n", + "Generated: 0 rows [00:00, ? rows/s]\u001b[A\n", + "Processed: 1 rows [00:00, 2.53 rows/s]ows/s]\u001b[A\n", + "Generated: 9792 rows [00:00, 24786.87 rows/s]\n" + ] + }, + { + "data": { + "text/plain": [ + "[[ImageFile(source='gs://datachain-demo', parent='newyorker_caption_contest/images', name='371.jpeg', size=25555, version='1719848719616822', etag='CLaWgOCXhocDEAE=', is_latest=True, last_modified=datetime.datetime(1970, 1, 1, 0, 0, tzinfo=datetime.timezone.utc), location=None, vtype=''),\n", + " ['Can you please identify which hand was mistakenly amputated?',\n", + " \"Frank called to say he'll be late, he's stuck at the office.\",\n", + " 'Your overhead is going to kill you.',\n", + " 'Just the worm, hold the Tequila.',\n", + " 'Calm down, I just came to get my things.'],\n", + " 'E']]" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sample_results = sample.collect(\"file\", \"caption_choices\", \"label\")\n", + "sample_results" + ] + }, + { + "cell_type": "markdown", + "id": "ee6d0ba9-a5ec-4f28-97a0-c7756fc8deac", + "metadata": {}, + "source": [ + "The example has an output for each signal:\n", + "- `file` returns a special `ImageFile` object (see below).\n", + "- `caption_choices` returns a list with the text for each of the 5 caption choices.\n", + "- `label` return the correct caption choice as a letter (A-E).\n", + "\n", + "DataChain knows to treat `file` as an `ImageFile` because we created the chain for the image files with `DataChain.from_storage(..., type=\"image\")`. `ImageFile` is called a \"feature,\" and you can use `.get_value()` to get the feature value, which for `ImageFile` returns the image itself." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "072c1e5d-4396-4390-bf1e-5c9b1fdfe0c1", + "metadata": {}, + "outputs": [ + { + "data": { + "image/jpeg": "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", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAmkAAAHQCAAAAADfN6IuAAEAAElEQVR4Aex9B4AdZdX29H779k0P6SQQeke6oogiiqIoil1/UD8r9l6wi72AIAoqAlKk9x46JKTX7bu33+ntf85sNiSBkLZsIu4Q9rYp77zvmVOfcw4bM+Pb+AyMwQxwY3CN8UuMzwDDjFPaOBWMzQyMU9rYzPP4VcYpbZwGxmYGxiltbOZ5/CrjlDZOA2MzA+OUNjbzPH4V4X99Crb2J7L/6xPySt3/OE97pWZ2/LxbzsA4pW05H+OfXqkZGKe0V2pmx8+75QyMU9qW8zH+6ZWagXFKe6Vmdvy8W87AOKVtOR/jn16pGRintFdqZsfPu+UMsFv7k7b8efzT+AyM0gyM87RRmsjx02xnBsYpbTsTNP7zKM3AOKWN0kSOn2Y7MzBOaduZoPGfR2kGxiltlCZy/DTbmYFxStvOBI3/PEozME5pozSR46fZzgyMU9p2Jmj851GagXFKG6WJHD/NdmZgnNK2M0HjP4/SDIxT2ihN5PhptjMD45S2nQka/3mUZmCc0kZpIsdPs50ZGKe07UzQ+M+jNAPjlDZKEzl+mu3MwDilbWeCxn8epRkYp7RRmsjx02xnBsYpbTsTNP7zKM3AOKWN0kSOn2Y7MzBOaduZoPGfR2kGxiltlCZy/DTbmYFxStvOBI3/PEozME5pozSR46fZzgyMU9p2Jmj851GagXFKG6WJHD/NdmZgnNK2M0HjP4/SDIxT2ihN5PhptjMD45S2nQka/3mUZmCc0kZpIsdPs50ZGKe07UzQ+M+jNAPjlDZKEzl+mu3MwDilbWeCxn8epRkYp7RRmsjx02xnBsYpbTsTNP7zKM3AOKWN0kSOn2Y7MzBOaduZoPGfR2kGxiltlCZy/DTbmYFxStvOBI3/PEozME5pozSR46fZzgyMU9p2Jmj851GagXFKG6WJHD/NdmZgnNK2M0HjP4/SDIxT2ihN5PhptjMD45S2nQka/3mUZmCc0kZpIsdPs50ZGKe07UzQ+M+jNAPjlDZKEzl+mu3MwDilbWeCXi0/x1EUxXHo15j+yGWYcMzva7yT7JhP+Z65YIyWwSwTRaLHcy4n4v0Yb+M8bYwnfE9fzrqTD4UoHHNCY8YpbU8v/Rhf/9nL+zxBHHtCG6e0MV7oPX4598lVMcNGYz+OcZ429nO+R684W1whMOE4pe3RRfifuHiL+iTukxt7UhP+J6Z3/CY3zYBvLw4kYVxP2zQh429GewZYFuTFCcL0VZrj7wF/2rieNtorupefL1YaPlxrY7+NU9rYz/kevWKslh24bcdefI5T2h5d97G/eCyEDjvu5Rj7if/fu6LHhBwbjz1PG7c9/9dorZph+HHp+b+26nvifsutPL8nrjuup+2JWd+T12xkuD3B0sbjnnty0ffEtd1SPuNGbDDm1x7naWM+5Xv2gpLveeTDHfNtnNLGfMr37AVFLww4oDnGfBTjlDbmU75nLxh4KifE0dgbBeOUtmcXfsyv3ghy8jhqaMyn/X/wgo0oO/b8jOZ5nKf9j1GbEykBAz/HmG/jlDbmU75nLxjFrBdz7Nhn4b1qKS3euCVpji+zuEiCZJg4jjzGcUOmVmdCz/PG3tv0MiMcnZ9G5kNvvkuPvD2QfPmqpbQt1mcHAFmOF8h2pDhWwEtCZDtbHP8q+iCwccTye8Cj9r9AacS0tk0rMTgafk+7LjNUkfA29CNRVbZ9wH/3L4bMsdyesAle9VgOclICI7NNmMzGH+KykWr8IXr/RJHxfTZiI/m/m6C2OXpdBnNBMvuYE9urntLAz16GzrAgCSmClYnefb8z2861OIaVxdB7tVIaEwUBaO1lmPw2iXT3fnjVU9ownb1s+CWZdZWpPDXILNdZLnJDkdd3b1r33qNtNw74eA8Ah179etrLa2l4uJMd4kaw8llDfa7PYTg1aNj23ksruzcylhMAhNwDcc9XPU9LpOcOLE7zYxddO7fpga/MWnhg3mDisc+83YExjsYuiibLSCMYe+k5ajxtxF+z9etozM6unCOZyjgKA97l+S6oJoHtM7HnhFEUej6meuMW8KzDxTXeX3c72350cPXnrjNQXcxHubEwCIJw7D2cu3KzO3AM0j2TzbLtCiOigtpYb6NGaWM98B29XmjpzPoPv+l+RlBFt+qhoFMIf9ILty1R3Touqjq/bghy/OaZ7b+8lyl6lSB56vcIOHVH72zX9uOMUGbG9bRdm7yXPUrUf5q76YH/vPPBpRU/YgUB4YAwcF/wzMLU5PxAFR54lg8zsjYwibn4mYKc3+gE2APxwZe9m93/kU1ZMhj97p9oZ88w6nraiAKwt6xRLe6Svq/VGkce/57XtoiuKbJJeHnT8CJODhuCEd02sM+hkx5/olCZcZ15rV6StWFVZtN+Ozuxe+3+Qp0JODZ6gauP0UjH/IJjdF+bLsM2f6x5rXTYb+c8cO7Z/wp1EbVePQcx5pEdwOMEXmKWXpV96/5re2fOmDUtc/vvTTFF4dBX4xZ5tsfwr4a6HCOa9t6ySoXHPvYOLXdgfMH7591x5imXVnhJVlnXskbGx/sWY0ilv3a/tu3f69oXhlODDPOlrxkJnZEKPbLfq+U1dpRA4PaA7TlqQf0RFjDiHhhhGntqqSK6MMi+UZ8TqNynNYErPXHfoDHt85NmNTFxwIyoDaxnZWP26bPN16x05lmiOGPd0+4zuafzMpy9ryZKG1kf5wj7MQPyc2R9xuwJGvMLjtmdbbxQVjxnqhLe0xlzhf2OnyusOPuzVyxxANDaNA5RZCy3siq3YclxbKGglbh8XShfXB1ZmU37vUresHFeYKJw7Nd9t3natlYEPAXxRgQd43jsnx9QRRI5B08L3MYblv70pEc3hHoPFzceeJxhZrznY1k7FkOFqYuxZBs1g3vztQf5+qQOplLLpZ0NQ0sq/zizJqq1IG+rrxIKG1knd/KcuyvpaISlj93djT1tj929JVfy5PbjNafzqFPVxRWeVQ674E37rPnGG/9Q08Sw4SoKtGM3iu1yttY6pzkWmiZw5X4rdVTm40tMdUDPm68WQts067abDySOH1FyNn3/ir8ZA0p7QVC94nfzUhdg2SPtDUzTxA+f29nLpgrT9znyGP2+C8779zrB4H2zhGiBwC9e29LUlnbqvC7ostEqzTm+/xdaWaRCY6+2rWROAk5lD+Swv+JclEQothHmPeZrJ4VWu1UO6s03/XSNMFmfaXrGzAmL1t9447vffYTCqh2+kw6ia9cdFzC+E9lBXWblpvLgxIMufd0pbC02gld8gsaYlPuDiRzvSWN8VVxuDHja2N/UFlfkGIlTNJHLZWrL71v0fDU3Ue446vjJ6mVnXdwTIDLjcX7lan6e5UpZud6AgA1T7c4Q73yWjzV5D+gzWwx+9D/UGRkBuD1wX684pQ17pEa8bGPvDw14rcK7jFRecPXgfad0X3/1+qasok9fMGv/6mfffT8zoBoM37d0/r7zMm4gSqLEZERH0xxu8rKLZE70R3+p9/AZVaaX89kXPNdjNpxXnNL2BBRqi9nzmfVizYpTYnntgVf+/dD1t173XKx2LpzcfMCCB077Sso1Gsp6Zi7TIjZqbFqLTZetFLnU5M7M95Yxofiqy5Lq5J7xfW0PlB8dA0pL1n2TnrYFFYzBB8lhBrNiLPNxrl1V3vqnM6vXP8lLtd79Dpo/qRD+/rWDTJHtZrQ1TwxKushxXCpQU3pz++Qm1v+lUoOL91W2tYor3Ijjxh4N9YpRGkdik2WprA1catAHY+S0wmG6vfzLUVtYEtiE01CZAfNAPQgFX4gif/plH2pZ/edl0rSG17rg8KMbDx/3m6nxMrl3tS4LsiJyzGCIcTJmmUmH98QcMEZAqQX4btTGtYdOhLzWBMYi5kKeD/bA/bxilLaHJvRFlwWiMWZSNi8QqBmAIadWOHPwgWelWoHpmj3/k9NWfe38Ls59tKEg8a5RbgS8xTqGJ3L61LaVP1T5Vw1PS0Jr+DMUTlUElhVfNFGv9BevekoLY7bKZNyN98my2llTlx00MHhD8wYhV2tdc/DxjV+cc5XWxqt86ADlIGt+HOiRJOdnzQh+W3r1+NNGKG29PQeOpyQq/ErT1pbnf9VTGoALazmdjyABGZbnhSdPuLF18IJ7urtbmXlKqWPGPgvbH1xrzOGDWMzktNg2BE8AqkaOtNTErmutVw1PI48m6RMDZkcYxmOvpr36/WmRGC2XVA1Neof/W1gduG3/G3+x/OpH6/cExjzHn/PeE5oGHlljaFJoWmKLJNuSKThsyXNSxu+qWz6X/8WfoKSRqgZdQnT3SAWYV5sL/EW0wDOr+yZm+JCncD8U/Jhp6fnzJz72psV39DQ1px9XF1TFw4Whxzl14iQl9DnR4mLOxmKIuiJ3PNW98EUn/C/9gjJeyawxGJMR+DEP1TCbYFr/pfO3/WFL/sNxtiXZL8k/KWk9HeGPCz8+Y+WD79X6ZK4xwPHzO7Xlj6w5cHberRVTscFoMe80C02Bs+zfC14tj2Jy87CqDaEiwCEw9turXk8T/OcULefBXwHLnuOFVG8HU+O/cslT9QV/vjnWegbM5u5qcc6p7bWbr3ygrOWCMMOkYznUKmKTpv2ld+yX5JW5IjxMLIf/RbYhsP+VXo4Rm2Zkfkb8ZeS+oQ2fk22jX20THm7411cuOjVSFy1ii0wT7yMFCuWckH/HT/KDdOCeclVa//AT93an2VyjY1JRm/GGmXHvg49V2zmrHkTper6sx0PdUf89jcDkhFqE0naujSD8yG3+171ivjFm9CbzjQY8amOfHDXmPG2Yi4/dQoVSxDQrWjDyRAw/2Sw742Zp6eeeuLpJyineQBi7ubd96lD/uYfvLk5P+46j+fkUO6N9tveszBsDVk7od3yWZ8Q9kZI7unPV4HSF2QOO27G3PYcBuNGYIfE80ff04Qc6WTKiOEgRzuPuMB/4XPZbRbvmWZmoX+AnHH0w88zt6ytFJesjr73a1T7twPDyXpcvaCHTqiL4bkb8ntBwRpXU6lxKgmAZs/nfNPgx52lJxD0pu7JpDK/oG4jNUKl48ghPo4uRILEk9/Z513yo7Z9PR52Kkp8edHW3nHRis732WT/lsYro6zMP2m9GtrZYtZw4tDykuksuzLZXdLBjcPKQF5DCzv1PUNoYTOdmlzBCTW5TkXe2cSP9kFyY2brMf+/cbx9fX/xsmfGLax2VH3KnHzxtw9122C/nWwoxU+vsnMf9CitTlzTJrftMWgr3QEGLkZGPzisAeVS7e+x58x54RnGTY9jJlLWaeIQ0K5nNFgojiO10jc1+4djPHzUw0NcqambOcfC4z51zz2PrZqaGlgly38DKGV7VuuWu9hk8YwVpAQuE1Lz/9k2PTBeLPvZhtjGnNMSEsFhjl9pqinODDfELVdFxdbJCOS8S4vuyTde/1bEfKXTMLiARqqA4Yt5c92BZWGHKflPjKSX0NPlNjbbDDzh40uBEtcYa7H99BoscohMeg3jbWG9jTmlIy0vMz7G6US8zP1ox2DKclE40ToQOMFPcb2jHKK9/44W/vb2ns4VfoqYN3/XN5ye/95rn+CmHddTVpcK01mqkPbPg7ruu1rzPv3O27Mf//TntLFw9TMhu0ibGah12Q8OFugPOlKRVbj7aiFyEw7beFg6NEbmT3CI+IDaCg7HrRk4+8jtCJljObbfQwjxRWAVoBGybX3ir98l1sG+qtu+02/IoKErXwQFkDeCFVVXB++gTd1599nvLtz4+8O6mqKgq3cakvticXh2w04euk/QgePzkVvfo3OvFm35WW9HGyDWDYchdgkvtAXj0Vje4kx/p1nEI269PDKkux5gztVGjbdBNsm1cflqNLTb4Fog2tv5+E4FtsfMofoCVlap1vdjUckS/9sv7//imv55SO55d/eB+R+bWV7Juv7FmKD0n3XP3/YwvsObsVYNMJDS+/9OB/X+a7Q2amZBiBhjzKz7sUZyBLU9VjwwJPo6t12HLnV6JT6NOaTRIiuaObAl5DdMYyGxLShuO+b6yywbmM9/vfrGtlWqghB9/2tU3zHzy7jC76C73iIPsRipaY/jc7CMyy67OtYhx5+yc32/bfXdU5IFLVudNJvI6EqY4cnP/ha/VKCezPqAtY72NGqWNDBy3kHDpkc8jryM8b+QV39Pd4uOucYiX4I8jl9riFY0FThDLL6a03l/9UQbUnDvh6l++tVCp/v2h4r4nTeOUVoNzhNlHdg7eUnFq6hK9tO9kidvvyJbVX53+pScecbkG3Rz5Sba4yH/RBytM7ZnBj5pFMEKyw8V6X/zIgGGD1W3B7WjN6H+wthfvv2OLt/n5XvqIWDYPYnpf3Oght+KSn805e16juuDm/l/d/hv9qmffPovjMo21PFfSJyi9/2hp1mpdutJfkyb/WGPdey/94w8PvMdJ0YOxkRe/9AX38m+DUPUZntvlGd/l2xs1ShsZAXEP3MYm0hl5+EdeR/ajW8W+RCq7tHLJBXD8pguNnHir15hxmphngxc1slAunPftu27PD4ntBysfP/pb1z/zhN7vqf46QferbKtulLxY9w8PZunpYj3ji/Jrj+k7//E1+zao2tXW97LVJffqj16shvx/ted20+zD8Nx89Ue+J8IaFpgbF2JkL6KzzQ/Y0XXawWNCxogLT73YItgw8ROHFu9b9+Tyoeunv2bFg0cWbn0kP7UR5TxkQ2pMKBwp9umpZp8v6p7cyHHWvNrPBr6wr6lsROCP3NaOjnav2c9hdEz4Hhj+qPG0kbEPo+xeIIOR7xMyw5+Rz8PURVS5S3QGsqVrjJxt2wsZxDLT1sWGW9/oxIp6ePV1pf8sLd3X9fw+pyyys0N3HL4/W9wQpFqlcqi6Yqy3LVvQN52V9ls1IDb1+rcPZNfntn/BbQ9lr/jFYRR+TySs7AaWA4gI8CgiLDIoUSRteBv+SDmTlNoZAbKOd3BxbGYJDM84IdcoAzQ5+oWHjIAWOGCb7p6R8yVWbDKGl19AHQNs9moqKBoqZASji4iFZc00F2b4xuq/DX73ovzK21/3ToGpLS8WnXSb7vEZjVGmHD2xrnnzB7uZ9S2zZja3P2gxn5t8/h2WKHiczVfR1+CFQb/8EPaKX0fWydPMQAz+qzy3e8X8bX8QPNoPTH20J6eN7EqPQOAH9+dkRZkw9ePn/OjSAz97y103tDbN63FMeADEkA04QU7LDmrHh0VRRMEUlBsTpp5eUjbceemXT52XYtiVk31EG/CcJJx15NT/Da8s+n5oIiZhrLethcpYX/8Vv56AAOeBv1u+zwilDTstw+BtNtPiudObJs3WL58xa92SJdqs2ZbsqIISBRFi7XnWZHMa58majH4FbKQcOlNRw+ev+OY3bzl5oGWfBhh6wnxf8RsY5Quw6IWHyNDYxwj2GKWRDKNt5Oka+Tz87Qvfj3ze5VegHhcwKxFHGt6g0XPI4BaueGaR43SteY5JKYPVRwf00O3SVFFpWitxARvyisGyUlrxY05WGV7goq6MJte5fT99W88MM1NMGR40h0Tyj5z4v+Q14tBgBsHnMR/uHqO0sbrTgOXjCcxKNCMe3qAwogCKyB/95sDTmGhJ6fnVT4fLeVOeNknmlWgQ1IP22FpelWKOdz0RmqYgoF5HjhMY1WOkxU53qklwREwcHpLt+/PG6j539Doer4pRiBsa622PUdrIyr/SPM3HI5yauNIfuVEU50hMA33VfRsaPaXKhorBDPlG+7w4qMpxIb/YiVnZyDdbvMhELqdDdMoSx0lFSeVZLTBnLfvM4ecuiIgpwHUS7wZEYayXevh6Vow28+EeUNRGFmDP3PYYXJWNQkGb/+S66RuvBYORKsGE+m1fKjY5Djdpoh2oeZ3NVqLO7NCAGwWsksqoqBgDYuKUDBMGnAjiyzGMFzmOeun/PbrkF1///AgQf+RBGYM7GZ1LAKdOD/mLgyajc/ptn+VVT2miHwri9Du7RygN0hSpBREz+M75q8JYF5ruu7lDq9an1Vsip2gFfaoiZlrQGgKZewEhkwyT5fiQk4ZESZJct2Xf64f+9stvRQcfD0/J1oiBbU/z3vOLG/qhxO8BSttleT3iH0uynPBkJ0md5CKjSU1KSQ372ZgYbgb4yMBKAEYDg4jR9jDxv+EzMn3JYY0lw//wx+E8I+cded3lNYK/DmeLIsV1mEPZPourVlnH46UQVeo4Hx2kJk4sdEoD3/3yYj0l5K3A40p8qj1neWpO19QwKDZERdQMIZ/2GoISZg2Zi6WWmBfaLlh51nevcq2I5wKznjQBxX3v8jjH6EAsNMXRIp33FSzC2Fe12m2elnhQX95UJDoCASb/XpA2CUnSNG+MFmzMLXhhh9FZgohRQ2f/6Koz3bQV+EJgM4pf46Jso3dtUag9/uDS+XNTnsaLOrxnsbTOaDFSjBkrMnxqPM/yfiSRIN1kIjNsVZakn3dddcpbuZrZugfQN7s8Lck8xwofBkjC26ZnfJdPv70Dd5vShi9AlDSy4e3mH5Ovk6gT/ZD8QtSUBNfpKPyPOSDQN30a9U1w5Mic03GPxaRQdo8PU7YXGKJjPh+HQ089s7q437GZrtAIvJQpiJZmtk3m4NiN4tDnJVEU4koKlugLmVVQ1prYYM2kU+7/Zv4wP8399xSRh4zA3KLhDRoyQ3YMP96jPtsvc8LdpjQSePh/EysaFiO4leQrKN/JK24Ttwp5SfvRIURf9C65fXpJPoFEN53oZQa9Mz/xdZyRO+qap49ELxVB8AIVWlr90ef85x9bYTOT58xTTXTwRU6o7jqc19Gq+k6sCIGVVXVVZBwLXg5xc54mubXcVObNv1xz1rc+ytRT/zX11TZSGmOHtAa7rDTtzNRvue9uU1pCMQk1bXnihABHyAY+LCp2S2SU6Gv0ARv9SR4vvBl+v/U5RuGzFIWGc9w/7z/KlhFs91mh/8rbi91F3yscMlWMhKGe9taSnxb0dG/daEyXKvCUsVEcGIrI8oqeApaLpOemzZOVwB+Yfuf3//Cxp38rleQXwZE27bl3voH2IHnRHiC13aY0knnDFDQ8s1vysmHSwh4JaSUsDPsnC0ektZGr4Xj8Prycw19v3Gf4lLv1N9ACx3D305+INMmP7WLsL7npVrFpnyPEQrqy1kUxmDh2Fa7Otoh6ejDlBblUEPJwnoVuwCgp0cK4knFvHIUlqII92ZnyjXdc+LvjTkv915SRx+yCmeGvKCd9ZMecq+02pUHgbSXx8HGYaJK1SSgHPI1MT9wq7Uu/4muireFf6TVR07Y4crcIbNPBEWJJjDDlrP9cfaZXa9Q2LI+V445UIn1oaDUXOoraFHI1SQuCQTbIcEo9rWdEG82jIo+XJTQ4FrIOMBub3U+WCcrmhJqWPuCyn7/jtO90pDddae9+Q/OO6WURWMOUjDmdjYLAHlkEcK1ke+n5JjWN7nRYaib7EK0lhIZPI6/09mXP89Jnf7lvpTBS7TB//uorzIHu5cueW/b4KiFnr3jOigSR9WtDFUVR23VuEm9z6UAZVNrEWi2SOSvkUy0FGREryiTabIQO6uFOqLaYCjP9B4df/63yy1177/otWSmWi3yfEfZAI8xd5mmQmsSUwNEoQyCk/MeEI40QDbrigodhp5AVYpSC8BTULjM9zdUZn4sD1MQPGKja8L+RaydmHejlDAoSBHDGCRJqyW08nwt1nShxhKJ3dPXI/CAOGqIdV8TX7267+6KTV5diJZ8qm+gQHS3Nhi7qvliM43fapjNZRidQttEJbx8/NRD9FiC9yxmR5braI0WyZUuLHUBxqdI878UZRmMs/cZHmq47+WyRC7lis4VdQZIJem5Hhzim+xFWGA8xM5CL7Dzjjr16ucuU9pLTtJmYoTlHswsQWQx3QezLmleWVIXnLFPMMh6iiVg4Gw2DIzNGXjUaTfieJEYmI/LIpwiDxLWI80Ebx6+7jtzja3KDk376g0PW9qxatzbVamYCKxCCPnj/JctUMxxjVv3A7EVIc4gTszmU5hTg5WAlVZTxUPipkihZPKuDIztcKFmqq8rWqsHinYMDS9Wh7I9PNjJCmGUahQSrtrPPw0vO4iv6pRXJEpjD2G+jSWmgixecYsTy6CECah3MQjWGFEFXWBAYvFpB4PPZelEXYosvikaK1qemqIxXFvQA2GOG4wTUMU/Oh8AJYgq7zisCQbayz/+68SGLCRpxvJZFLAZPQIPzQkmghPQU7w6xjJyH9S9l2vImz0g+J6B/niAJgH3ITJ9gWFeDEVeDygDv1/oqld4qhTdijykUKz/9RLYvq5RbPBotEMZ7QAPaKaqxQ0lEj4adOmZUdh41Skv0e5rsjcOiqgZ0P4OazPCRDSR1JDENV3a1uLfq8ca61s7q3dctY/3chI6WibOn5AeYZiGCzIUeAaBPYJC9gH8JagJUt6t362lexF7S+0mvuaunHMWD2T4/z1YEQ2qAccm8DTemZadUozsFuJAioocxK3kia8QiBiKISqlluvDcw18qR0wgo6RVtsJoHprmmREzpVlqeXrlxe0f8FAsEggRqh2xG8x3V29wJ48Tw5BBEdadPGoUdt91x/xmeho0FHpKkn8jjJnYBXS4WHNsHijpoJqPiwMrFveuFruXunzIoEiUGnvp9ulWeWDQk9NiOOPMMyYytgiiguSkQGJyPo/jyfu203NDlI5TYf17UgOHq99aI/YybjkX6l5dD6vo98nKdklIhYxWZIR2h13bKUtK80zVF+NcXeIKkRSyGsegV3vqgbfUMVw1LjQXJongZf1rytWS5qAMQ/1rqy/TrzvBqba6FLTdmwmN9DSSMSc+ed80QRDGnquNHk8bZkGbmA+oBfkhUWRGuuQPBYq27p9/WgG5Jcde6tRj9MrixuCGvtaTTrT05kzjmYeWd8eL7r/4zHcsiGLPjmS0rtt0PpDsCPnuwqMVNZqkXw2+q4hK/BZXSEdehvFCI+04fhCyqlwB+oyoJ85mBQ4mmSCB90J7ZAUJmKHQcwVdaH1npk2d2BkInU3+o/9vfYUTOk6YqcptyobSecGUb3yROVxlqA4RUdrYr+BOzklr2CDrbezHOWqUNqxVvSDmKApF9qg22Ft6+q4VYbdbZ456/YLC8qENUac0WM3mZ6i87y/V17lqS+eH056/+tY7fnrngR9oacqQD5t0CZoOAbwChLbL6o8dSX1XMc3WBlmr1NvisB+1RRHZDKKyB7w2bElRjRsClxeVIPIdi8sililJKK7GkXIIp0BmtfC9VCTU0oS9L/YVZ536pn0wa3zgapWcK32J/+oHvv9Wz5VQagaRa4x2795y8BQKfqLYjO1AR016YtiJl2OkIxGULXLaxKv//NcudM1s5irG0Qf5PcjVtwOrP2hukLjBDZdQG0MqTOkIhtSJQzfd32BOeOcxLSAALGZyPmgUZBHstGIxIj0Zj7n6XdMOWmDDXdEXMu19DNsaD4hSo9qW39DIA7DRG4btUlOke6KSna6xbJC3lTiLVLVI5Rl1YIIpC0W0A+JslbFhL6MklCPwlp9tBNla2rIL771U/8vhOeSFE08b29XbiauNSM9P//amIyULvsSdOHZUdt1lnoaRU2HHjewGNOUjoShGIIdRaqpjxL7GNOp9jy26q1hWTjo65w+JUWOx73t1OKk5wy+CbYh2ndGUuofwzuq+oVjvKpx80PJ777jDOOHdC6eCwFwobL5WRr/qALXlaBV3anpQg40FPtvne5jJ09cKKZ/LWGFFRrEqUIpkpdkBP6MEgxGbQ/CyKqjNAhpeKGGkIAgvuCwjhSHH22pZDJkcmFUsBfgKT2aAhJdYlX1ZjdORrJi/mPiDj//4sGbVUQJAefGk7fxQR2UpX/4kydThyWZFaKBCKL383q/Ar7tMaVuPJWT5yEHijRN6dVljA37t03fdtoQtsO0HTmkvLjHJUYUlCiUFXntGlAw42hSYpaEQ8CLPek0c6C2tt//gyTVLbrl9yms+MAVhIWANbZ3xEPROXEDEgHeY2GBHhDxC5cLq30xo0/skDw40k1EIwCj58NqaUVOqLrSqQNZGNiemYLYAoxkIcDDzDP4hGRrZT4Bnkh6d2JV0y4l+Q6SEzBd8iqOa5n9jwcfPuugdHb6CZ6e6KQlr6wnaSz6HjkmPythvo0ZpXMTC79wodTiRLtfXPH3LM0viKa+dlMtFjlupirOLMdVe5xhfVeMA6yWbtUBPsV4EJT2IfVNTZMFsCPz9+dee2LXsmSsvP+N9CwVbS1sSU3MlQSQ3By3zjlIaSTKyX53clatPOrhLEF14ycDmNHBdvuGH6ZwSMHl0j2UVDjBU2Qs8j5MNHVJfEEhag8SSFHmY0PC0cCPNIoaHAFrbSGlpwWFOvfZbn7n/55NQMAqH7uVb6FuOwu6BYe72JUccXTxjm4KsSkMpzb7uDw8Vmdlv0hEdsAxR0w2mvKHFdWJ4y+SaT+El2HZA3+gZtAnWKEEOXhKHlQsi00jLpjB1+n7df//7qo+fqZlFYKoViQG3HGYnENjD2/YoDrp5wnq4gWsnHpt7IoXQsg9gtyt7bCiGkR9oXIURPbPByVzKVbmq7zkSr4D8iI0loTSeLAJQFCgN3JTORv9tJHZ8jU8sJxYLRemwv77v6o6PzGdKqjqip+6tBMfB0N4NU37Xb2u3KW3k0n2GGkGv4ZtuuuWe3uKC06Z4dc9IeaUNahDocsorhjHlSfCcD/EF94cL0ePUgroFl5XIgZwqliLKnFO1hAkZrjDnoMvuG1p13MRUmonroSrZUCwIlztyue2/UuAT9qB4b/dB+zwto2YYC2gW77GmGhmyAb9aIHFDQaHNkQMIWVeXOSaw6+Cd5KtAc4hhe5dchZS4QvSV0NmI1g9LE9/iGI1Ji2b6yvP/9PRl00UFkfe9e+M1Q94jFUV2m9JGeEwb4yLSFNZu+uNd0nHNLXKtFmf4PpuDCYqgtRg3EEkEhD1I+TEU9ZjxeXSZbLiuAcBhFHihzsl6yMl80S0O9DhZZdoh+uMXMlM7PnzIzHSPl6Fxblzp4ZXcHs2RJQg6cGoPR9OXDAhuhEg4UlAQCRdcIRYB83YaMsBBuunWdS8DzBb4F75ScBz+wz9wxcRgA4kDQEAtMYevuZHckxtnOVc1GV+vpb+l/OKNP3rtHqi+vpOETQFdZ08w3t2mtJEb7dcUobz8rkevZ6bsO98Oa9CsBU5JZStxtS5Kok5lVWC9hei8BHYhQSeFqeopDBuEIIggXsU1y7YtyaVsihNc+8lFCw6d39Xb8/T/O+b/jskTA0lIbeNij1z0ZV6hk4HY3PrqOztbVytOqIWRJJEkFiQXcfw4ao1MPiuBqfJeCKOkEcsiF7hUQz3ZiNYQtiFtjEyCcFhpw6dhNx8sAhpLHClOlBri+Fr+IvEHXy2/cae9MS9zC6/ITz4pr+D1Y76NGqW1BktuuGW5w7y2bZpZ6UrpFqUXyUFv6NhaNvQiIIb8lCAKUI6wPpLe53Maj+oDjhVr8NJzBjgisnh5vdKXE2q5+eXqU3rnYen48od+5k7QWpMl3nE6wzySoRq51uAj73I7Nwh2oRFLkhcIjgsT05NioWzLhus1VbVcJJfNSIjAaz1oyxsPBZURhWHDF/hDFJjwyUS4gr/R+RE04/yU3+QrJSb8kvKND0n7TcW3e/Pm+z6yEPfACJP52pXrYvKhp8CYc0I9bojss9+6tXDsvIENfKBrcK3hbnII5NbYACgIOeIzYq+Yangaa8toxQadW/TqjpaNamGgWj4PX5TiSo20z3mC64L/GXVgJSIlY0T1TlaYcGIh7bkshKut2jHKaYA1kjAjvkMEACoAFIN4zPAX9NaF06gUdl10/SeW81K+JqhhudMPg75Ovs7JjiUyWosyZOfDuIrOqimMEPARsXmywjSV0kA3QZjCGQsgCo9wPzgaQla424iREhxaUjcumTaiRFi5sFg45t/nb3jvJ+cINVlGAIFhS/mIG/uaPttYTDwyeDjC8PO/vG++JP434tPqnG5HRl39vyWnnTrwrNPi+LzrImgoeV4EqclakoC1EeQ6PsYi69VSQmz6WsplVFkIXAS0BVkURcXlHMRJeb0sQzsSoyAPsBFQjOUO1b/7yabGW3nfEOIqr3vopwN05aYnhEiNtk0kN0JtWP/AixrL5SqMgUhlAkFl0ERFdpDOjMinq7qrDd0I2MARIVFjDIsPq14spNKizHqxTBBPcnQMsza4OhIqTqg6+TN82YR10mCEuC6dyv3qX9ecfsLCfZmKEJdkAdbEyPA27r3nX1hZRIHMPcDUdvOSUGD0siOyq0586NTXrO7PxyW0AxMEDbXgkDVJ0kUQgQcLvcTuC2BtAowTxb4Xmg20lnBDwn5SKjtve3xKC+qNhhOysq7wDcsBHMSrr1/2PLdQ/vG+n7qjiAhQqj9wzJqN1IAtNqznsJjbyOBAFkiPiz3X6Vs3W0hpouXFNTu2zMDLsYD8VutsNpcSpZQh2ZYnGHDbRq6DmIJTNX0E2eG8wOFJQn5CaETIke/ByYv5AvMEy9y0Ednhc5VVHOmN3zivctW5C89Y3AQnXcwH3p4A7G8a2Uu+8Ql1twcSi3erVA5p6bGApnLx3z8TfmDC2qVNWUWraMDVqqwHmDYbOTZHEkcQQodVIwe6P1ewID/j2OKIklxOpWosceCBSfGy6ACLC3+95Dl8JAhAecconRdpExvlvit+N/Xdb+vk22nRwfzIC5FsGEOy7mA/CTeDjMN/+IcYfRTaKysHZ2KfqweZOgWn0iQCUXcvZq2QzSp1G44zNZ/1IouB8NQzQSSLscvxKjodD294TXR/NgzgnhkGOm5iqBv3IeoDYtwfajpk/3f/4t6ea4wvTKpDS0AsPxj7sM+mQb3kmypQ9THY98j8veROr8SXu8HTsKLYOM6ULnpn8/unPlqd61qTFR8Kt2/Vy/g1MGsu5zl2pOfTQh0Y7phTmkToX34UIPEt8H3XtsEVQ7isgpwS2i6Xbs7Lckq2qg7yeRFd8CNXTdnryxNee94HpO9cZBnuQAWMBXjZkcmgah5EahSwB1WHNCb6Dc4VrL+1hJsuho5qSDx8feCvUgWQXzbbpDUCsn29yMAmw/hVM+hsjGimaVq26zomTpGcF3U3IEUp5ZOiBETNybbxhb4ZlqVZy9SNuh/t96N/fzZzw82SGzeAy2P84d33nr8V22Zg/488SGM3sK3E0I5fOHmusahSue//bvt/c+/lmLRVNYJSDpzD84FDCxnfYzkd1VYkVQrQ8RedcsNA9xUJtCdJVsDL+EIMfC5AYlhowm3PoQa7xAoamJlioakmks7hVc1KarW8jHlj7idXznh/S5NVFRVliwcEq54QQTJ4IrTEWuA9FAYwl7brlchjVDZQ5Uh0a3VGwYCEGKkrQp3CYREkOxdoGitEgN9yKNnn80BAeqT5g3RJ1eIE0NywKAUzTL4nr+3GjdzJJE/hHhRYkS/ps7/S+f4v73ccuxZ3Gukju+0tr0HgsASdH/NtlymNRkpcjbns6zO+P3SPxilDTovV0yqEMYKUPAs4DrLAAxEwfQ5BIAvle0LWcWVWgnxUZaaBcBTWR7Rhp0L2RCaqL/KSWbHychBq0JMUFD5L87bpyo7DFkLl4dSFz375Hz86xGAcBLw3RbIT6Ua0RquNf4QSI3bkS4j2u+aG1OqGJHpxHOh8Q2dsN40yG3zoMJwHR54Lgei5DT9QjUGfFUU3SoVs3RNVPvIR+ST+SNEoOj0oDcMM4QAe/mpr6eOqgosSII46+MyaxbnyN7XDdNsFv9vbNkOAYxOlU8d8XLtDaUl6yo+/P/PtxdqEqtiA6xYeKxteDjkWANhAbCmsw0MGIEZiYQO6wcO5EEFRyzA1A6UJbV8QMqLAxwLPpbi67WsQaLJcgtZm6jwc8xyyp2wBzEgXMkx3X+eb7jnh7e88Vakgv2dkpojSiBIAvCAByibiChTnQ16GrlOpPJKaIntAkyu+D3EmeI4sCqipptlSs9BdSSEjLWpYCFPYos5X42zK7B+sNKVFiWLosCwoPJVsoDQwthDskH4YuTxehxdNjqwo5T3y0PVPmJqttd79nT83226KM/c2ppaS8ZySVT3W265QGuYeelFPK/n5z7h3zoKVvjKIhr/IMmrw8lAOyDM/TPGRh0XKgNJk3wKl8G7kVxkxUEyumbPhCoFuhGqelRSjOm5a93pFLQj7kJNUVHmVAQK2lvJZK8MwrcjJY7pMXsrx+kzx+X9cefKXjmQ8RCmBGUOFFrZoeoAVOZqraKykMc0Bx9qxSqaGJacqjJs9Zr0aFp7Ox/oUL/TgjPMCWeVMOQoaUjbF9POOqKtKTcnxVk1PC1VWbHEHyq19UxSRtxGqBe6BDFlKEYSTEOWWCYqLzyBt/BpJbIPTXC/li7p5/Z9uVNl93tyeKvR/5Y4bzwyzxcJIyfCxXtcXXY/MGoRn+JTJuwKwdC/a4ZX+Yhcobfh5dicwvtj7yQeK69sVeMQAlECFfogLxWczcs0ORVmEis3C0oGrCTFElFZkIdA4D6yOavm3hGA0iEoZNcbgg0gCzjZE8F3sh6MUpBzpZiPKxnwGNgGjKrFusE7dUlrz0xbd9tgNh/vcQL2/a7BcqrWEmgZMj5Up9VkTD833FftLAd989HGuBoy23XB1zVbr5WBCxnFi2LHlFBJgQCgwbWEPYDwITjlsCtmmBJqDC0b3ELYyRB3Jelga2CugK6Is+oMXMIThLRHV2GGg2bBNXa5k6+t+cJV35mmTKlfeXnlmpmL/5g3GUG4PSKntUIzMADe/0/k/2znpjvy885QGQiNaq2QaSvj25weBj4BWDTRhHFhsLPFZO+JV22ZZNJbwIzYEExAZKhULtxnw+wily7hV8AkP8CFBkvkygutOYFDNOxYWoy4w5IiLh8RMrHTX9CxCBlDCpHrIGqJbqsjvNn/+raNWbngycDyYDwDmMgDxqgzylTn3CiPs4iK49Pf70EdqDVF0fFTbQCyio1of1FS/zxMBuGSQqoJUGoW1fI4Hy4IrTUQ4IogUBfKRD5ERGYQ1hMZ8KmiHQEfivYVkJnIjKk3uP+laBEpsMRnJrarZhvy5mz7zscsvvzuYbvcUVjTHDwymoOvtdQXWUqA0YLZ2hDZGd5+dvuQwocWMxjY1fr1sUDqgtRVeCzsWYLwB8cBpsuc3fEkDohCOKzZG4f44QmoxoCqgGCja0Mc5DVZoyGvoKsO6wHZAqYsC6D5k1smAFGEtI7dZrweuR2nAHHR3obXUiHzLjsybfOWmtW3CqVhyKFOBY/tmXW5SDxwSoofur+5zhsh3KPc9/NUfpuYdv6AUaGYrZw7wJZUpO7Gm41ToN4CZFj04mGPOa+AhyaAUDAdrAJmpyIXkVE5S0ZIACl8coqwyMTMwWQKqJYIz4XQgb3zEHrElxDBg/LrmH3v3DT9lJ3wtfnaZA6jI9DWL8hkHD9hetuURgsE9jf2odprSkica85eCW8arMid2CjLcGGFgkR7DQ1pmoGAzmuqQFJRFRrW8ELo4svGwvCIvKwA+SkhmV1wA06LACdEtJ1ZjIHngbAX8A9YAnAph3CFVuztagBgHn4RXVzRYFYi3VMfg2uwnytr87qdgRQDnyxjIsGrwUBG7GnrTiVp2qtMzkMtPCiZ1/fuvrD5fyJcHxahWbrGCOID3JLCQD8WiilCEWAWHrBQz0DJyXdHxRYAYJ9/PpwANpnA7dYYiCgM54R+RGzG3ZJHwFmuVLJfGOIHgBoanfGi/t+TflP5b96Rjn38y6vz4p+49OhP6Y68PbYeGcjx0DeLd29lv1H/eaUqjh5T4GkonLt3gnDu95jjAqgox/GToEiOwdkEmDgcMP1ZJlULTh9qlKaoFlQhmJhIjHdhv4EcISVGOHQrHBaEiOkB4QJ2LOAUNTqggDiMxgiNJtqIgfAjhvCESFKSHNLyD1R7v4bhXgdsOGhb8djgSEq+/YPS5E42BRdOn+G7hyFwGca16n1BtVQ0mNiqmr6t1BF0lU2YgdTGY0EWsiFcDyhICwglWMeNKshRAGbBjJ+pUPQGyNUaQI5l0MjgJ7U0cjuaANpKlDUaOHYMZ9FKZQ//2gd/Cxu27IlvRr5/0f10ZV28M77gX/YWJguggfLdjve00pWGARGiMl+v7xaWvbzElNMRkHAdOUNnmFZW1Q19QbIvCjmAFXh2dJ+BSUAUUdgEzwUIR52BYgGBpxbkIESgPLT8ULKnnMSJoKozA2sR6lHesOgutHf3rUAhZb1joGhabKc9Vpw45M55DYWMBCchwq2DJfcdI8/0loV1Oh0UPLXicKaGTbm1xs+mpncs22FZRsepMLm32SBSrgKKGKBhlXyH0GaGzFAyWFHKhWEmZjIQnqJVZOGEEEXUe4LklCgN9QfwjjElLlPxPfyBZjfWTxD6j6yP9NzdSp/38E71BqlfkDzz32uNzdcPEcwUC3qs2BOmQR70HQuy7QmnJVEfMTy49tUUWEQdg0F4tagIs1jdSQgMwQ9Y1CRsUxLY/2CHFiDi5piXJCIQS9AbxTXgxHD4A64vgl8VaAqsGSiOa4hshyl5oYtwy1OAULsMNIFivKaCNTBpdeFSpJHm1DblK1VBVNvIbjuRKihg5jX2XWq2ttsX6CNTbKDPaHRsqVx6qnLLkUWdqnvGGdD0OrUgVNSkIUC7AZkXJd4EuSSHenE5XBEDqbFGWkF0c8Cm5ySNhKeDM4GmQmqhbA9JCgCBxryeydJjSasfPu6otnnBuNsvZ4tuO/O4DS3Lamd8pTzi6ULTFhrEnvPEvT9l4cCA9R0zol993NH/dYUrDPINH4dKAL7IeH+tX/HPmbMNrtIcVCehVxa6GmWxYL4h1k0PkEkXLqPdvkJEcMA44PwQF4ByrIYYWymLHgpIpeVzoy7LXzpjgbw3Rgd4lamzoAHLk+voG0Ve82NJrYZxWyqZczXqpelwxUjUurVpstiSg6gGNXkmlvUpq4lNqEx/JUVifnBmsoHyWmberPFtkVq17/BS2PwoLTUIF8hBFEnxTYc0ak0uFNrhs4LYyNfBR3a8z1LYHFZXTCnieHigiqrmRRGdYuDpgg8KWASvEDJD/w1UZKP6D7dLHX6N4UvkNclezumZq+8VrDi5efqKlPKW9w9ERtyeQ096xkVYJ/lzCRAdjrqTh2jtMaSPTBcwYYzCW9tSnpPOqvYWKYoY8D8g9wEG6gMo9nMeA0cVyKgoDk5dl20e/TPA3wNQgfWRUTXOg7UhKxUZ8kxD65IjDyskcqvPFDuoVAvnB2FaWbFdExeF5CCzLl+stQDY2sXVRgIELL9jkOFJdFyawW6yJcTjYApkMi1Co23yMHlyOXipnhe5e5lnJL6Qd33UaXB0NyGMDhrFkMW0DDQceNdttDXxNCj2xqqiEdPJiUUqj3ItK5EQdpuBOh/uJwprweJDrNjEEYkZxON+P2+PKezOuzaSEaEKjPvFK55332W96fX8BY336/Zan7HUhAqaMkAuiHIk1M7KmY/K681zUN+KlYuau1zedVjLlrpSNpeEj1wx4RYa6ZEKn1yRO0AJO03h0joGBR9pNpCuoryJqAHjLKsB4cLsLPBCQvEZdWuHjcomlo/ILeu+wrCKhDAta0MEYyGZ18BbFiDZ0uWz/IGq2M6bFqgqLOD3PihnNrXiiGAKlZFVL5bqdCmoeB5h2T63W7045ZGHsyq0pQMWgBjO4toaghaExtoMmxpIqazmAgcBJI6hkdHG74YspHcYvzFP4V5CUjpgYxkGWAFw2ZHMmvAEevpBLpdgB6H6urPHCbac/4qTdnzwi/s16Z/326Cf/6Q72jeWqDiVy79q6OBW5XntAqO80T+NLeWECc9//60N7XytdcFAfCjAcD22AgYDm4ZdXuBil2JkQrk2AIRFpB2uAn0Ll0CtTYJR6DEirG9gpLgYOUuYAtUX422kgH5PhJAk2J0w/Ge1cwUvgxggNqBUgVbkZhWmFGtdUBHoN3i7T9uF6wwlZRMUzVVQngl8OfhIx5QJKGQBLonp+CARafsPqJ0x2/44qMugh/wTWiHybiSswcfF8KPl6DuYI7E9R0QWfko9VHTUhybFLMA5SGFCCiAxPPDMJ2xwmtdi2HKO/9IUPnELJLghxKZOaqpm/Tr/s1qMP/+Qfs0uLS5UZkW4zdmrvIjSmD3YnUqnHflQ7TWlsc9Vtue/T3U1ybWq9MgmPPBRnpHqyse8iMTfV0OIGkk+8jG9j7VEigQJVYAeIf8Pmi4dvEfoexCzKsMv4C4xRYNk5ML7A8S0wQuSxsIzihTHyR6lOAtR2+GebeBNNAziEN0UbTel4cNJYimuBKAZVjwFOCadEaSkMCLwoCIWSXZB6Gb3CBc9Vs7NhPWCcqCbUGlVMSZJVvYELcKJSNjRwuYgw3yJctmoJ7BLxU7j9hku4EfyNGD8H3gc3HHzNJOsBZZMY3tCndrj5x396/BnyMQe1Dip9U+vf1U9In690/Pb5oycd5Nv5vU96+h4Q7C+Gcr7ylLfTlIao+VD3fx5trr0m6m7v7897IB4IvLgpoF4lkEE8wk9qsjYKUkEQ0YRpilxiF60L8Y7RAlgUsSQ3XKg9gQM4JBKLUVoKIWsmdmCMMgzswwg96CzY42ByMPwkznEsXg/FBkqYwuNhIi/BgcXJy57nF6JylBYbdYS8fTOSIpR4iQCNW5uGvIRZbEkSQOVmrAKxjVx4PjAdTfP5jFWvouUKU1czYkOCmqmnFJQTRZFn8GBUTYAiTxwNKc9gaJRaDAQUuBx1xoT4xE91RnQU7VcMU+csLlVXU3Ez48ZXLz/to9q0b2W7zi1+XI99C2rdXra1kW9gD5ieO28RMD2ZQr1JLb6zwefkXHWwGZBZFGrxA14lDIcLNwXihzHK4lFGiukmqC5EtF2y3fzI0xJkNrzzcCD6geMwcKNC54E/lUETHR6ikRNlKNSmE2HR4yCQZE5CUpQMoKRclmTU0AZDUw1k8cFbAomKHE444QCsQAQzDEV0GSat3su3ug6CQYMzq70RZ0UtAXCWiLgyVU7Xdde1QjnFqQDF8jqAkhavZHXZAchcQBpEqGg6asEiYEaODrJacHLQIHKkKEqQGOExpKLHunJJ5BaeOR8QzrXnT/1207Nf1T9f+drC06/74uK3vqeRFfs79jI6Y5jZTANPzh4wPneap7GiJ/irwg+2hBP61mQhcaC+6EYoFg3o71gqfijukO1AgwCELSkJJoCHqAWBYKMgCx5KFvjwk6IBmBySBxVxAvQBgzEkKj6PQu3YKfCCwA48Bw1dJR99TxPxJaIgQYTLSUNwkIiax8VI2FM5MEZ1CN1RBi0VpE65zACfBQ1wL7HdHJLRty7bYPJpd9VQKg0ACaKtAdsU1dCsrGEqKVxHZCdKqL3TUDIYIskUNq1LAaernA26AiwJFjC5bGMuEBFVQMoo+FtCPR6ykaP61a1HKtee95kpsVFadbjceDb44sJ7f/W5v3y//5SvdzLVXEfD2NtIbTLngtJ2etl3/za2eUlyndFGqBm8bDKLfVcRLv7DJ09Y3rVUb7LthuLailyOdc10UDpNaTATI6cq865hofYg3AJNYgmFqRwZzVmhqhtiKfaRAQfVDOWGEH0E97E8DmINLARZ7byCTgCDgaBoJvBloZoNLU5ETCuyBMnW/DxwZXXREE1fMkW3R1B50xWc3qyBklRIwkC/TcQged0aELXBuhNoKXhaS1PmP7nYDipOxotTnJgv4uqrOFQZAulBUIpWkFFB6JrPGXwF+wPNBMimC2PVAy17eCwojYAPoYoC9QTjE5ojMCc9n1rxuTMuufjkhcpxD86qLpk5YzEzKH6Uee2zJz1+wf3pn16AsouZGP6gvWXb+IAA3N6jVbLIWdtqYMkCk8JAHA8yn9ARNEHQlWEVjRy91UE783HrC2732EZOv/prb9u/xsqeFfOoFaBAv0bmAMdisdwA2eqw8UJW93yX1VH7rJxryph1TjFRJA1QDtiTfEpkKnUFdfAExBgbWsGzAlbIMTbw7ZbViuAQfLKc4gITZAkKanjHQLJJqM2ioV2Gj7weHlKMQioAQjoWSnh4xUhDmT3PZSzgK8mxKrB+QyrEtqdnxbrcLj+8cjoA5RyQmgIEJDKUkcqEeAB1HhPgWgY+lxADMa9JhgT0CLzOYezBXAZwGBYoPWtw2A5H16m+KJmiy7qfeuTMC46dkvEzE6KHz3ikUBuc9P/47P6pjz5yf/PvTsCDNBrLs93l2OkdUmgOIaDu4LYOBF3B2gc7B8gFJjlSxDcxmW0dsmPf7zSlCfrK8/gTtH7AbZxIRaxWdmpoYAEPA7D7dhA0Rah9YUW6GrmwAFHopc4bgGlZg5qOPGNJhJ4FSC0y3lDlCiFRX/VqoZSCliYjNAqtSKRmwOhXwMG4gHJHJa/g8hJklNJAlDRgdZQIomSlJGpqW65b4bR8WFnZQSNg0GbDB5BERqieir4jmoRE03IWTDdurxlKSRJNjCkG2gh5UjIonZOAKOFlzcjXgRyCdESiJuCakJgeMpHhb6GZhl4PQTq8gZ/hYY/Dw/5Vn8ibM3KI7U+I33BXxxdO/d3zVbOuxb9m9v/pAamavmPzP+Z7ZYBnkV8mNxDOAXqWEIH24NgBmn20RrjTJ0rVv1n9QVuP5bicFiAcScmckCuodR3VUAItVjgAZxUsno0yA3XAOaq2jiLYdRXhAWA+kM7uGpKq844k40YQGwCbQocwb72iOJavGhFQlEhjlxyERwPUbDHhNuUAOGp16rgOh+Zzlm2h6ytipToYlOg15YXIzMB1BU0NaprTIN0P3YlJPZRFqZrJr0Qx5JtnZ22GtXgTQXOfVVlfS/QySQL/lJEAkSETGJlZMngYYv2wWqlMCs05nBywQAllQ7wNsoVsHDz3LSnRZ3OVLKMNhcbQT+7+GfyJshn+oD06cCbvppNyqcMq3Wit1aicR4mYUHGSuOJLng9IKXjEITc5CSE4TvTJpUgP8e5uO01p7O+uvfCIcsQqDSHl1fkCQBQYUogG0wg+ZfnBYgZJ4gaL+o5wutb4HKuqUT0wmlpBiyKPuGTaSIWxy5RgWaJPM+emm/Aaci06YkShrts+AldRKJuGikg359pQG1ANC+lsyDZG5SuAFSleRShyEfYovF7IbQ8NeBlcC84KDbq8hHKmvKmn/GqYVm2+UDWOW/3kyvm4iKTXOAEME3lMIEREMGHRuDjYDcUBAWVRwXN9oiYCpsksSJ5HcUFsG0XnFtqK7+FRYdiaW3/kh4tdqf1Dv+IvWvudX79bc+HGBSZl99dmd9f2JY9nCaz1MmNDfgWeJqChkOKG+iiAoiSJSbsvQnea0qqX1rh+iwB+SCMPBaOMiA0wkFgWhJ3UiFG0yPTQCIrcAkB3E4YWVX5gF+IAWkI4mayi2MAzg2gldC0AtBEzRcqJVTcD0bZkZE2CrXAQtg6vBDaFSwXEQ2MWdbNFiS06LPLorCiLJk7wwkUdYZ0Fpi2M5Ayyz83KZNHmZMRGBcEFvjcYSvf3qM2d88y+RjpmsjmxQvwItkogAvjGikLaRDYWHL4wClBXDFwZqDRwZXicSZGE/4WMUpigYGjJ+kAqJxqyhQQv0WEyl157neKc/ljPZccEP/79ykPepdU8r6OYp5SQl1nPlySCsfkyMKkw+jbHRj2zKBaSoJ2A0dmmQrezo91pSlsxJC+aDIC8hfw0eCCiDAALQDoLpg9+U4+4LHrhxA2Pz6QQoBLZOqN6UONcJCqpYBccH9TLnJVHBAicCmcAJteMDQNhIuhW6HboeZIKQoO6DluSCsmoASINIMpaRidDMJjq84YIP3cNkXpeF4PB1olOrQr1TYO31QR8CILPYWS12TLZnFQdyiqlfJETcyvXzAWutu6CUmSZ4lKyAkNSQt065Hcqop0BGo3qhasSuJwkoVJ3jAYYMD3QmA+JD8TWsDqUs0+Sh+VyXq3S89jVjzXe/L3/3Mt0//iY3g8M/a31SmPZLIYpFZi9BsKxNUEIcDQHwrYhtx6cQajLH1JNdTx1AjnU94z0XNIWH4CUNxehISmAep+Hy9ZAuXfWjFNBXdAqkDuyL2SB+WnEqdjsmp0L7AaXdhAJQJUXaYi3M1lUJIKqALaHCqSK5jhoERamoOgBPxsKAhJJUEcGpItGjXraBmuDw7QkyGD7ELQBytVGvCTKSAdAjzFOMOO05gC55jmiBhcaHP9epKZ8hItQt9bonRi0o9TjlMcqSmuItHgVhgVcy5DAiKALkobnoho2cY0sorW8BtZLCHVkqmqVUKMIK3ltwaEJEU3MjFQ3mni2FK360bXawT84ZErh0DOvO+O9TFv7Dz+5T52ZVneEvCvX97Z45wjFicDi+DA+Rz5v/YqamLGPfO4HqsyEqU1Ggk7Hbb+C0nOEvdLM4uJgqYgXiU6p+NTH9yuZjVaWqw9m9qmt7MqkuQoiSyhMjIL/MlLPoT6Be7E1tKVmyszcMipaF62gZtbVwDWMNKNrUoVH8MpFjB1heQvyEY6r/lV6Ft44WecayBKNFB+JuhJQQbaFJOABJp8S6v1MzuyaAGUVzgfVczUUeOfQaFOikkVqFSXnBcYNVZ1pKAZwaHmxVBHlJrhha/pEe2L7kjmtJW5Ctz4k6Jn0oJfWGTPUDZ7tsvIFLuBRipQIGPSLYryI8MNLiFA/6sPAb0IgdHA02C8xzFaGMdOMdeMVt036/BtnpSKudviP730PrNk4nRVR3gsITagTe53pOfKkqE0RHESAnG4kMXjQSB+AvR9AMw0Mdqi5ds1NT/T7rG14rYe8b/+OGipau2jzRYSwG3GsHZaescnCruRWX5Ey1g9NEkt6KGeEqp+XqXGdxyu5KqNQzRUbHS6ASbH1boPnpQ7RHFzXX6q6yOBVZI7XUlro6+1NbSg4CzAu1K+WkiWiQLzr5pRqwzBQHAaeU45HCjKvgWMpoFdPLZgIEcQ67EJUM6BJgpoHr3Ds8xIaiCJXxnZkWYhRT0ZPwCNgWBIAlkaWs3AeRCKUjkPMx5lpTJRzeVQDl9KyjgJpSE4B7cSQ1ygOgsAUQOVQytDzRUUOFeafDNAkcDCsFWOq4Uepu61p/09/fcTlmFtutsP99/+EdMY5LiXtCRwyRzEHtNteu0mwj6n+yNYDXDVdKKa16qDQ/Nhn70KQ2GFQsrPnxue+fPDMODYNMpR27652mNIEB0scckueff3cej0tBiHKuPg1Pk3ZmRwvQwdCGBMrAx8BOoyhcacalrjW3vtWAE6ULhjcfqg527DL3SlUUABgW9E6Og0tl5EzYF4QkP4k21dQcEyvw6eBiDfkHFRyi2HSKmyHDNeQHK0Z+hnTB3WOOupwVNcD7EdEBgKEG5DxcOlCX6yHoCQEkXVFhRcFKQ3wwCDT3Dio96FottAHux2RV15HzMKHLw0gXVAlZDe4F1zjcGfA7oVzhFyXJCkpKLBRS6GJhnVSzzGVuy+901Q0vlJCYa51f7tVrhk9nd7HTkM7JiKy5M/WC7nXfJbSZZRMfLGeP70/Vai4Be2mK/9tKakiMBFcjeGlZeec8Mt0CgWA5d0ltR2mNGQQO65Te8SfCgY2ZDeZ0Jsptm5CS0ds3NUVlIGEpQb1GchW3y+hwtDq3y/nOrITJrelJX6tBC8uXx00mEotqvXV1/Q+4stzZ2YOdVAcEoWwtFotnWnYbAWZogHqWcFRAaka+WUpTXAkrgm+iahiuU2OBTCIKDsG/Coa5xZhMtY81RBcT0QXCqS+MDJjBZwjo6gQ6/kq8p8ihDOy83p64ylPmEZadeA4iRCjAMQWrdhRAAQC3KeuHKT3k85PkLrE1CSf7cZnOVFsYMHEfUuuuLoK/BHonUF5RctY5Lu5pkfMBzvP/WgOmVdEmLv5+L+iZJkrI0KA1jNbX0TVGCU78KefDMxMLy5qjqA5MdN+3K09tx9dOO1CFmm7u8mpX3TBrQcw8hnyxRdqD9zRxq5Lx4NRRkYYXCA1WkExUTWu5iDxXOBgRVtEgpQvlDpvvpObsN+0qUIu7/bbMtzuUIvgohG4bBqwRSceWl+LF/X/i5t24CQt8AcN1o1zbM2AmgPzDisP81MN46IgIiYvw0vGUhawm6XQJAN9SubrbkZyUT/SF6CdDQVAqinwpzTgBkHLIKiVqC5TLbbZSID3LbMw3f/z0oXNhikoCM/6KLunA4LmsE5ap4gWvBLQRcHeUJmTnhb4AUhuJtUREgZAJAd2GrNfvC6oMqqZ7ZzcnEFdyf2N/tmFdWv/cZk7WEYSNSWI02GQvHvnFmS7QTMvEp7MYHNtcPqG71xRn3DgBhuRGm/2ckR/JxoTjKXO+gygDLTRre3qtsOUhpxKXijf0n1WAQJIVT34COCSCkyJB5YWkU4EroEKA3MCw8AaCen1XYe9PjRnGLXAa9hiE++W6pYVNlpEQ8iool5N8U6j/+mny6WBR5hJRy4AzDtMuWInUK8mKzFKBehKCdDdDMpsayhLGsio1ZjLpIvUsBqQHomx6wGw4qgeClRRUAJWCVECAa2CMCuKrjtw8aISOPzDvodmD7EiHnTDvVPbDEGV+FzWikwF7lUfZa0MXmTSiivAC4ssK+iZSCdA4WT4+BNleevJLVsHv8UYmjpDBxMPeQXwlbnsfRff4Bx/9rGGwSA1ItHptj5qV5dn9I+zm2x4pyjjdsut2bLanvniDepC++baNHldqtjyPOf3PT/AHb6qtriBx313+fQOUxp0ILWx4sF58xyN9eP0AKq+IreXldWQyyM6k2oAtghvcmSiLxSiRmGl47VYNm7dDN9hudZ0voeRMlJWgzwCdroSZTYo6D+RPXROry+Un1rbfdtbrVrTdC+a0B/Bewtx6irI9UQ4tYCwEzxaQbtM/Td9EzFQRNcRfbRCg5U5JY1cc15E/SoFQCOVlVGnwY/xKLgwSWFKFerk0UAVN8Ga/s6/3HGuio4uciYVAjuCblSoy2Eo8AOKqFcTMagkglwailIlHn6iFvC34TWhhgSJfTDtEjj4gOaWoOEBZO6p4aLvXJ858c3vs2HjAE4CmYoD915K4yegaySiMFtztQGhbe3F90hKV0eqXK+5M4rwg/PMY85kz2fCO95AHqbdu6sdpjRC0Q8sXvlBffVEVGk3KnXALdhATql1Phv1m3DChigmik4QGQWlWTgbSb2ThmoTDHQAE1QkETR7BrIrea4euUiaUnJpXYn8WBH2GWKk2c8s3vBnxuRft580QJl4GXh/mwop3waEH+iV0MSp++HV9RtWo5kiUHCkoehfC+wSSee9miXpaQMRfQOQM0s3fNP16q0o4qzDbtdUEdYskmC8GR9fvrhJ4hpwYiAMkbOR2AxUsBwKKeh1MoBnSMKjzhzwByTxKBifeC7wKCe0Rugp0uNCJfBVppgUUmFiRX/kr3/0z3nXYWmkUzEWK5P+kxgFW3KMveeT1iqxwDbA977lFras/cE/lHlS9XnP4OpqHpUz45RNzSo7B5a8/6aD7d19eLZJaSPsFc904m8ZRH/CNT+b+eb7pqmWI3U5KGDGakZsoTBPnzaFbwS2HaWyqA+bzocNvBhm2C9MgBkoaM1a1cmVdWhwUaURplK+Dy8/BJimRbUhj9G9aNbEDfeAQd+y8qgjGnVFQ9HIsq3ymeZiBVFUQo8rmgUeonlM2gNMUYSMziFXrgrEgYsSamk9tPgWpYJGFjaDtHlWE3kLWQOwOfkavP6sb8HVv7blO11ce7lQstEd3gr1LFgxuBn4rKvEPCwH6GekrMGKxgtAJLAKED3buCBk7KB0B87YSIW1gl1ms3Cn8+/6OzPv7DOmMp6jRo0oA6Qd9EUUGeQ910Dv2Qhh+mEWl/C6PcjtCCdE0IBA2lBr5iAYNt4X8MpCXWnk2pi//a12ZBhp3qTD/l7wdAlZrK6rW4bfrcj2b+YqJtK1kU60JX3uxKdtUtrW50Dxp9WXqUcu6wBMG5oN+vBQ8iOcA4ykp9RGHSxChoOvLeNVHBvpAilQgCzDTYAgUSQ4JR1KJosumo4mJTWIdKTooPoyiq/HUg7J7/uvKwVLl0pHGJlqQ+O8prQB/6KgSWWAaHGTwKmBUyJg6cIbAaUdv0lYVs4F5ZFXRK1ZgMKEtUYWaTPgPXEG5T+Qyww0WgS4JSBpqEoU6VpL4Eh6LnKFFEX4EfeEjQu+BQkLRoa7oT43L0kP5LFFaBfOa9GXc66XK1L3iJ9d0fG+D0yKrDhOOxWgxss4gUh6JF9ui6qZh6QD9zZpCgd0CMa7yWCBBwFPMuMs/dp1DPdMPVIKRx1xT8fKDbGiRhqQesUJjJseuP+6d6BjBCWfbU0XO/x5hynNcJQ7/37SgWvzSFEHK0B4ENgIIOU8X+QQHLdSITxTrIwqfVU4wCQd1QiQGswAI2ZVdECJ4FJAkQIkjSDYTl4Qg9aURwUhoBcztt+iTBCVVf9Z9b0TZuVismVbOcsUUXoNiSduxOmaBbQ1OtHAakKOL5Lpauh9DExbGMvwgAFKFduBxCMBRqLQEWhFRRllTQByA6XiBeglGRSsFw1VzA4B2lR3UyloYDBlAJkDL0N8D44N8C+Kn9MGP0BCccOSk76BVMVXIEU3U2mkylmmWvDsuy+c972FnW5DExoNo82rkuKIjnqcGKHC0k8e63ryodWT6Tw46a4zA7r66G1g2aiVPnyXdFbkTiMGKEX9t0sAOqT7Qjlo9LdKOTmbLqKRF1QShZ3Q2HDvGYCmIhVo17cdpjRlwP2nciS08ggeBqBpiyLikFCT0dsOejGfZ3nHzmQYZ9BFXjB2q4sowGdi4gGYVNALBwFKUCeSNMlewA3CRkWmsQDU2iBSURhFzbvBnNwtd1/defQs0EClF+oVvGSBbAOxjb4EUNIpNwmpVggNQcYFvO/6GhBJwwiRGFEjVUQ8SkVBmQgZTo6HJDnoXmisE1ClmLonezAySimthvw7LU1aF0AksGzJPwscL1E++FpCalS5YquN6ruB9xHpAUOZk4emWdqys2f/4ghMgo6Ae4OJTGh8Q7UMGsSgGFfLwcs46+1eC060V+lteJqgB2x0EuIek8n0NXax2drvtRsbhOKaOQzXQMFrFQVXfTaseAi02Q91T0dCBcrf7TJT22FKc4OrH3pn01oxBaQc6ujFqLoOrwLAqBaoHmXeh0TkSWpByYGmH0RWQwOYFea0aApSoRmOCx5gScSFUAUthHcMDBzPFQUSgZuo+SoPzdxyA+2off/61PJTTxSKOTuQoW2FNbR2BeA6REkNsENKpILVBFwbT2F45NmFDuw9UQH7jAOgf4BJxoZmVVEVIXYYyUw/oqSyLoQyICEISUpiIei3QOAUOQO0COBdIh7ygxGTQ9Ads0+eV/wjstr09CP+QaSO0BWYOhOX5f7W/ousE4/wUAoL2YWi4buInsU5vt5bGeypMRdqH/7t/j9sxlCGKe1FpLsVJY/VR0R0Ej/OCMXAu4NaPOnot8wJHL96MW4RD36WrwJcFMkGkO5DkdDwmeeem04OxheZrDs+7B2mNMf6p3faCkw2aqFLslXMgk5IH6rX2UwuGhpk2jIW5L/YlGJrDvRvza+zUhpWKiEggaVOLDqEkXwXnip4eUEE4CoQpKi7gpt1S7m8F/mdM7ieK/7ReTSS8+C78DyvpGby6GaAIqZgPQKqlYLzIDWPnkUoapFpouoBC+QjeogCaMki2wnhTPTgpoyFIHZxJNKBJYX3dYYqs0Fjy0sCynQTxA8ZBxCccJ5B16eYABkAIC6iCRAdvgXr3URp+JhgbVErJAK6RAFy5YnLj/tUFYZaHfatHWlGAH+v/88rrqfZb27w17Nf5/lSei/kaXiE8HQNb5wPrArDPLJeunVI52sZVKfuc+VmGR5KqkEQxqaowXRrPPwG4A1fBgMycr5tvu4wpbn3rszVg7SJGp0saonCHQ/FSI58dChJq4EepFtU1qQOAqhsAD8buguAtpCXpwleg0eDHxtKGxGFgzAO8gvABrAuhP1ypQK8ECjWF9qSEYVHMuFf/9MytYKTScDAiYguICgALyzoE04tFJghzsJxOqnzNiOo+JZ4Gyt4LmIIyHgaRi1qaMEuoh4fUAtB4HK+jlz6AEjdFGtMwBmQb4rQKSgNpA4xDihnMvt4dJKp2rQQmyYOxhpkNuqC281Ie1oRdqTXXtHxg1avkdE9x5dS9dAKCswj11yk7nf4HGZ970nfXZ49NlUtjETcSTbvHRsFOjYbSYTeSpp5KX9c2Dlf/mbfzIYKxD16Uw6hGY3l2XnBN1FGgnnEQvwYj/Hmh252lu2/3WFKc66LpftnWbGjq2YZ5QRU4K6pGAbozO51pZxulcwY1G8hSMnaFhcjhg05JgSo4MeKfgOZoJLgwkJAIQQRTIm8sSSHw7qWBtiW40uBqjkoBl8/yfvH9R8vDKHYQqAUeBueWdUI/TLxF8SLYGQQwQoK2vGQXLQIfkEAbI5KAFLVGSi8kLd+4PkCPKxhCmolkvTEVBrFtVBV0hIAuvL5QTBUCE8wabwBmSW1H6HBoIJlIjW3njpo0mC9AmeUECZd/9G7f3vSt6/4+uyy04TmZzBKfMNHqaW7f3XDkR97S1zLI5F/4ZvXPrefUQUkEouDq2x9wj30GfeBiXphOHgSkGU7dL3pr+i9r7l0/OH38JFUqWhuGRIAUL9cuttCg3N5BXxUgQSTf1fHvc0DE/mBx5hD7TtkEpl95eKc2ai7j+IXbEGObXfijErfUGZChzMwJE1QnBrSwNGDjJUMvxJmUepMF3kGPlWhpmWDQSnDl2pQ7NFDNh01UK4IbjMJuXsAEaOogA4XrmdJWd6U0kOqfkzv/eY5qtycZeoDcPpmvBpjK6JfaeGsgsihyzXw/XEJAAxKnGpCnincG+D0sH6ZIKXVESjzXJQhElHUURXQ4hX0xYcoIIQa4gHAmxFCULFPJgE4K1L5UFEEswcPMT4DkALfeITIOR/bnAEHiWXEASSt7xrMU/cG/8/5dc+puT/+Oj3xU2bmgrcaTB0Fk2wkwcSsm4r+9m3792egUgT67bnCvm+6+K+HWxkTSaLoJC8HKdL+CIu05QY2PEzZpE2gFO8OFMYlEQ8Gnwj6rU+35ck3/4Q96TAebVKRJ73Jn8YhDVxlltkql9uv9aI4VKzmdVLr4xq0BUT8Umj7ZSGFNvBK95zKWbLoJklw0MC39vxufqmXfL9NShvZ24eGhdxyO6xn5wlsCekdPBDbgiVHZbT2BMMYqqKSLJOHjx3qOCpymx4lCcLJBfGJiKWDQrWSBsgD4NxA74MYgJWG5IMaSoWtQBGUxQnMfjNT5rSo3Dzgth5fW/mTCwriQL6ZlSwLBqRo1kQ0CWAE39HAP4AxEzNkwuLZRBNQeMaQAQhNF0IQoVcGycGYUAMaRl410yAlGY0IYAxAy0h4WLI6iUeDHBtbbXDVCKiS4stG2m/YQpZ3RfHBB+aeJPR9+sY4zPyya/AnQvTxTyj3S+0z86j5DbpPo6Z8erB58GeXif88opoYEazQN+WUi/vQNAspr8yqW783cP3cNh+9ZLa5QBZ5a+AhfKW3qBChnwdCwhsvBEmhhNHS6jeOyaQem9zF+BZKe7I80NHAlZLtgxmCvBXC5a/zAXffLr1sc/zbPRIQbDwB6E1RnXYY8kqA3PBC1XOsJg/9YHOit7KSmpEeqoe6h0QnjA5iEtW0UYkFdAc3howUtxgA7jKrgeWIQOeDGMEWoVY58LczvoqSHvCEojopGqeoYc2TG43cicITv3rDyVxvNq4AKgYzCXUkgfxEqqdVT2k67KUg7XqJWYdyCnAqA0cEngDwDyppo+opvKcohoZ6o/VaE6iLKA1eXhiboOzEhoTBMSzQNvGYEZJDw08xRVVrTPhK9MjWvEV//63/5YP+9QWhwvPvT11Qvbbt/HfUs6dzyLslDEAEKGbGazQPXvzd1L9mMHAMKG5ZykxZKaqV6uDdxdSUxy+SilkOUTGAlF6M1tm4NACbwypHKsU2l2p0fvA6gyqyhzd1XQTkkbeU5+MHftWLEiWFAH7IquqjOjaICl4NxBcxV5A97kMfg2JNbfR2cdsBSgNzF1hvYHABusoJfKYO9wVahjGex2syej5wildnUhrV6Ia2FHkcCpGhtgZ4NPnnpTQkAscpGfBrIGIVZKgDIJ0wIhnsDGYklGzPkHmnxBnoiqMWDaHITzmxcNu/ph3ml+ES0xC9CpC/LjP1QIPZgCJnSCynwt1I5Ee71sR2hMbmo7SzKEoNxLhRMlJoLkmdGdPUUXUeNwD/G0xJ7A/JQZiz5EuIsuEA+uYiyJbQuqAuZ5A8yLpgzbdcdattc3/7xzMLPhh8wVv4jTf0fnCKQV2Jwhq6AjGOYieOEYf54u/nXzlTGGpCNzax4Fflljui5TMGDQea31wl+sWBeQbdroC82sZCyQBLEdRpGz+P2tdxU1TfaPYk54TIdt3MQPqOKW/suHoor6aDsF+tskE45AsyynWSEY7/Vf/JOjo0vajIwo6Pa1s3vukMSGTmMT3V9fr8ICoaTjEF1KynI4EIrigfvUna+Br4DGEEsebok6lD3BrIXUGNJ6S2p0SAZ1kEphzo7FhlLLEIby6DPDegJ6CMh2iSV7UNJV0EZsAKFcHkJoc145DS418/5wCuOAUPF3U5A7oDTRqR7TnBs1yBelTAFQfpiSRzVDYg+Y6LI5dFSDkeldYL15UQPw9FpQSqgmVJmFpyqoCzYRAgNZislI0HXzIxN7CnjXfMuryYyzHMctbIaLU//frxfEuqFBUbF33YNzKpY9Qo3R5UojzTUJHtAOnr+Eh3cNSmL1zadl2bLRZ6WwC2g8cw8G9oBPPffdgA0/vLZZlT36whbQGENiK0Ns3vyJs6xDYUkFdcfOI5w6g3A6jBQ84xZfX7+zbbz+rtWmwFDZSkDvmSB4g9dCA0m0IoKBX0d8+PIf9HxrvTr9s9UkhacoVDi6bM8bVqCt3EVKe9EA/B7c+4jJA1m9S6Hzp1VOCklQfCI6hFKhI/4JoANDFVC6D1I0wAh5eBPsIQqfB+gcMgPOV7HGpjIBzaxNoDOdWuSAqaN8L7ARFdOWXify5pPa43XfbCQFKgTKOwNofyi2QjijKwCND7KTWUCAcWLFD8AriZz+Rg4OIR8HPpyDfzUqiCccJnjN4ZAggLKjlZmsTTSAyQ7CRxij8jG0KC6On47NI/Lllw+srL7NmnfvbQm/6Ye+/MDivHnMPUbWTJV9pcyuaDvIb1Ws+zNb/A3P/9Q/7SBFXQa19jTsjajhIsumG/X82vy6rVVPv5wX/pwZOFaBlCGdvY4HdjgJbXRzyq29htt79mi+R8pzDN8OZDd1TdHtZZv77aMxUdTDweDdwEcW5/P++a8CGhDhSwVNBFnzoQPZh3fdsupdGgBMbuue0YdY1KQUegNzSuWoZTCvJK0mVYJ4EjZBFahrkP8w7RmhhDAjYYMgxsjALqKD2G7F0RYGsmcUpoDONQGWwwI7XYSDXxRQeWB1p9wv+leRVJz1bdA+1brm5vrQH2hkAp+jnWEUd367KLXE5J8KGnQwEEZaMHC8xveM1AwWQ6+giAERWh9YtXQeTBgH8XIHR6iqGagcCHiQyQdJgKCblh8jZpa+iHlbaff/ihR1fL7kNP6W8+a1anFp1xegNF1vhalIqzjWjD5Laq35RiIjhv4OoFvt5UmMe+Mv3qVkaInlm69LE1M886OW9rn5lz+XxbQgVw++h/rjLbXaqNSUS+rW3AzgH6tIkCtrXbbn/fJTfDn0jQjmRDz1KeWdmf+ULQEva//tE0CgLAEvC5iW7JQ/E7YG3B+BnRsJn73gu7YNcvv11Ki8EsorjeX5qX8mKdl1yhpaW6MmxB3wl010H1RTc1ja266PPrIhyLkJMHCACAHTYqfKbhpFVjVNEwOEeyEbpCX00qfIGKpMi7VHTqPB0gWd11uUm1iFeAZwQ6g0+7pXhquTS/+MQtZ2cHdPSdpvoLVRvhdEvJRJ6HptlIPICrA5nlNQhJYB9R5QOsC5ZDA5X94B1DlruUVVDEKoSVC0YGz12MFHiATRNKQ44TlDYocGQYbEZnDJO743c3R1Mzk9eJb3jPvm16HQ0aBQjKcovLZJ2w1Fzbpywa/FBTiUUZcmAuhUGpnVl22V1rWqKG8buLtDnnVH/0ufBs/6tL75y/YaL6fKuvvulr61h0CraRw7DtDIPbniwvPLoFtf92fS136Mh4UMyS4jqysxDCr7Ohsc9nzAnXXVUYXIBckaonO7Cs4GkzUkU8hmDy0JKZZ1FGip7WXdy2SWnk06b/4elCEL/nLgHZZ89MZZsNI1rjZBDARqluNYOaLqy3Blq7VCVPF7kFkZ5rIY+gAKUIzgZiaAaK/tvoqWNCigHkw/uIXuKkITAaQJrB3RTHdiXtsshTRnOeip9tDmoWqzOznrp532P0oCagQkcotkOsSgXo814DqS8kQ0MXJalyCA3AGEdBPXAtZAdQvSFy/iMW6wo52UWSqQDHLTWugHjFgHBXIC3YomQnQHfzBE+pxXC+BVGaMa/6Vr9y7DtPq1zffcQROdd3dT8EOoNlm4FUAaWqseaTRzrj50JUQoJzJZBRjiv7i7c11/P50o9yT900iSuc+eQZl/3sdTPMiY44vayZZafhanZK1STfzI6sE+WRYcVD1Le0APX94Q8HDOuMK20o5FAlMULkNOOW0GI0NBC5tUiqjgj55BWkQou+4z578tfhyZJ7WwxHEm1k9iSfA/Apo9c55LB608P+QB0Ql4zQXJaZynqksEHip5Afy6oVZLOZ9QzUXbogrr3zvHeblDYyI1iUUDDdIX+Kb6YEtpJFgjpNhWtkABGm1gNQhEQKTSGhnEKw5KOSRRHqExl5Idz/pLpTvBJ6EvBGsE9j8CSkZyqAgQQK4gVUSK2K+BsC6T7CurKPTACETHOD9/7+P59CphI0tTS0IoQnUX3Zd6iDO4qvkE8jhHcRWA2cFOmaFFAn7zdsBAFZeCgUTq2gycUJ4qLaVKQNY6ISHpa8Id0YCuDqaRAYClJKf/Dbhv/u8+dYzMT3uWnIWj0BdST7j8zH1q+8Xajy7zE+olcrfOO8i7991tq+T+77mssvDD9faFQ7gOLsm7y+50RxUCvXpjFMdpOXI3GdQr8UKVckeu77/qQLn7xyLdrBE6I88kIDzhP4GaqZkKkJmuElvI4GvZsbmoF7MEs2UUpYz4RDZeGBXmfm4yLgKA1gsHy01FJQHowJTABSUfUO6Uam36hlUUVh04E7O44doDRInYbdM31BLW7JpYFoRQaXouCpdn1Dj100CyDbALV7wE6g00EXQyEiIPuxtAh7UptFfA+HPT2qtMhgKmQpotgGOWwD3UGmOKgBwH6oaTF8ZrxL2ZeuwEx9g/VU/w/fMr+izEaNUfitIAU58sfBcAhECroLsmYh74RXYxsQM0QgsAOmkYgdOVCE9UAABbyZEpFJk4NiNjJTSQQdbMME+LeZKalqUVj0oScPe+/7kaUAs9jJQhGDZWy/4LOnh3mLlU7kcE0ytWeeaeswJV00zr3r91HHJZ2faXxIuvY/J4kf/9Zgs84OXmF/1ohTVlNUctmOzRdoWIRlrLT5kyLz/g9dWP3xD9nketD/Kij1A1iKHNYArUKvbxyHq285gM3PtaPvXb9ipal6z8ZNRpkOadDtekxfuwRKrFTEcgFABd0XliaeZQPThrkSHdYcmuXRbO7itl1Kg2RG2LJeOzVVQ8Aa2XNg6xKAY5laDbqPUzNQpwV+FnhhKcoNfQj+M/AclEwmdxXWnsxkrDmkmgNID6QYBg7XCYKUIAtMp4CCyFCx1IYTIl4JanAdAU6UvNsw1f3jJxfljKn6AJsZgDQE5AzITwUWK8vBQxx56KAIyxbRJmh6gE6RKwPlHsFSAUfUiSqhyhGzT1oJQFrhdwyE/qM1w9AQbnJTFiSEWlz8h8uzP3vNApR9Y3mXT1WQBogBIpA/vP8wjUHkbJxowqrRey2MxWt6jQK6/VlK+6X3yvH7p6/8xrRp7zzmSzf8Mvo/cfG8+m9mtzlybztTaYJNPDLhgI2AxXJMpTnumXLnv4555I3MwcgDU11IAMDm0Ocbtiie6UhVYEkAcIWrYcz4t3sbr1iNDPwJm86SxcO2Xjj9ZH3ou2VFjYeg/yOYglwRAGJQrhB2DGQYNCHGWn9UCLDVrm4jN77N4ylli2V6mVkb2FbbcAwFq0POjJ5UJ4pIuYgZIr6JEtqgHmgVniVChIJcwP3hhCS3DZ5P6viKmp9JeUUcDS8YqBGxQvKv4UcUSABlAiEGlocgowiPBBthiQeGmuY/VdhvH9lthHE9k0VITAE6DBYtDFwXcDTQEsoTxVTjjIp+U21mpM+FoorW77l8DiEXXB/5BGTV4y/RPDJgtrxVva9NHVRSd950sfu2704sMmpQRc3mIMzg0fAjqt635f4vfCJKixkknP7t0v1fWyij34sVTjy+0Va890PRWx/50mvyky+86CP1JuZHzjvmm377mvta58pNm2w+Og/J9eZL/ihN+hezlrlyw/3V/X1TTlbEt5GSiq4vni0rTCVuYWHfjgadoc1WLrawpptuw0TP6eqG7D7ZTOhZ/RxaOEC1RqMIDTDDOLKQSAUBj3qw6N7wHBymmx256RQ79mb7lEZt1OPnhYc3zGyXlEBBtBJ9ogI+i+bEYEWcCkdVANXVAZ0JoVNLQ2OiMj4AjQEoBu8VgkMQbAD6gH6wNIhsJT1akS8KksTIURMbJWYRfUti3X4pB3CIyfH9IMSgwGfcAxY8a3UGRdREg+RDfSuQDhF7Lk6hb1TkooAp8kulTMohIxmd3IFPAwotyKapkw7YFqrnQk0Dp0oeR9K5EtrBHyJAttTaQK22332ze9/vHmW4bTX0OUBVCiSsoDoNMMI+9KON+5PgeIGn4QNxtcjKL/lw7T/zi4UGzwx1dqWy/pQHT/35pwfmyOvQv2r9kf6t/wj+UZx06pPfX+6875dxNbVxXfB4gs6CUP5Gt3GX+pG+7j/8prPwvq8d+Ia2jnzWQH+ijT5cbkgrQDsh4Cj9Gx7Mjq3tS+6FnEmXHpHkprCHXk1nVleLv25rnekIg369jMQxMeY0pGHg0SVstwCNFyWgwvA5gP82Sd2XPPnLfbkDlIYaGc5T8T/fngdm2oVzFMlJkqplPVR9hEtVQawJa0kMDUXfEbeCBUBpSUB10MKgoACAPuCLEK3gZhzCLYk8BaNEAB6lN/AKqxFZnNTFBCJJYuA5AFqSZx0p61utTz7TIUsVJD0B4Yb7DHQgJHkJ7dKBm3QhaoBjQSiCQbUiFHUGBBtyBmRNlT2Ih5HhQF+T54wSU8irMbzRNwiExqV8Qyz8+ye5z7ytrcEGZT0NgHYOeWpg2zJKXG02szhgCy0lUdOY1vgX7FlTA6nGCnaO12HErjv77KEfPvjumfpK7R/79+pf7HdTvxR/l3sr/7USkk23WAwM8W/6r57+6zfeKPXOvk0Y6nniyauFlJydPCn9lkIzvN1SCl56JDzClBgdlsY4iBVSBcJN45AsrVyOo2W9WfRZnaoDNYPl44QhUyZXAUpOoC8TZk70mRVmCyAumw7cyTfbpTQ8upHlrgqZQ3PQTEUXPioNlXr09Rowsb6Gxq7ohQcmh5LvDppVpAl/DUEfIT0F0ApoFxCs5FiFqQiiACOjOCV0Ow+yE1oazEcy5snzEEIF5ISJNiKnyBKIwxIKTnrzntHEBkWNBNRrQTqBm4GrVoW2ZXqod4vizTVWpcp9gIfAm0ZpPhzKvIeorIUr49nFV2SYIyJF1gpE/IjcIIrDBDKFIB2t+KL6qJ3yjfWFdNVKGx7apIGVhYENjY88PS+3eesuZc8XkeipPX9Tp/QGi1EnDfntQ/fceWP6K3NEXf/p0n1/Lw29Zvkv3nXCrIvK+kZOhbMToUPN+Ou0O//GTDW5aXe8ZsPhUX6N1n3PNQ8tZdQfpWcsPHj+lIybK/9h7XH7Via8wIZebjjb/Q2mE8mSTVvDKFsDvVMu7eyWHn+qJgygWwgqYaNMHSw4lEM37bpvY+XQh4nvrrc4kDy7uG2itBFaxUNPTJrY9PArVYRd55v7p8W03z+9F1UUAcERihnblVPUQAcBb9i+CtyvQObAa4GAE/zKGnLcob7HLBraIHKMUFA9AprLhA4PlgMDEq000b0HyUTQ55Dri5YYfIaaPiHnPERDKSI/pq8E1Mp1p/a31FVkP7nwMUoS6ueBa6KDmAycAdLOW1yPWuyg5IunpNBfKix3xLVUJmr3G8ReWR2cDUYDRbEgy4nPDW+Sx6O8X1TKrJvuffj5uwPGjfyOgFoiMzIYKyVC4RnZFA6FrEtmBM5amDf4E6oNdDe20vJNTqpZ3tDi8H/4c1GdO//N0/ZVmgaNzFkfYLrzQu36T8/9zULGvPjjf/t579/2sSRnhNSI/DFPXO7WFCOtzzw/e+5AsEGvmEzH+9+77unlpe5lTz1+madNmn7g/hsu/1W648jM8XNTWdyHCT0D3L2c5YqFsNzkYaygHEBhyPIHvGdrQiB0VQAjB4oqiIiXzWBeXM0SJI9GwIpBbnCt+iZ/IDS7J6w8hXE09CbS7VbUUpH0GN7G5oH0CpV3aymuunQayvVsnIetL7Pdzy8a2NZHQH5APjUPLmyFm0qXckgrEjwTFIQsS5ilDJVwoyqdBDuDQcyhrSEYfVIgCvcoMBrWFqwFoVBwNRWGDfxe0NmAKcLy03OKcBVkIMKIsB3xBhMH/gVvBy/B89+UVp65bX/WsA24SUnjQ6QRyWKQjdDsobEDly0AXgVBjDJ7cGWrEvpadKZ5NDwD/8Q3xDk23tLI6/BHpBCCb0aZ+nTm0gc/vgAAlQbitIh7YVzgdiMESR+22CQudhjRq6D5NWfq6cZPvjL7ookDE+Iy82zpEvbJdT/MT3vXvnBMZdFyVFh58c+mXHCQ39N0cOcT6lVHOEI9tTWPfGbSHz7Q/djSyYtb0hGfVaOyZKRaXvcWPVpTzqo9Tyx/6i+/8urNg06p8kd5+htP3r+B4r2oacPkK0Iu7OsMoYDQPSL9Hib2ZuMeGTT0erTvilgIQQmea6HayLBsOlKhxBJTxzybzbH/16tnFvYLWgZLRXhFrRZGkVeHGdT7F1Ioq59LowevkJJQzxW1znd12y6lgR5CPTd/9SzZq3KqKyoSokcgJCutQlbBF4O/aH2HhrCk8+Mb8o6Rk5RHAE2WQrAvMucpQIj8A1iY1OQJ8tbEGyI1GI4IUoE2qf0PdWkE68YPocRmG4KSlg+4f8nxDtKEObQFBZXReKFDoXgc6BHMBRBKSqxgOKQUALOB/u2ZRg4mm5FE0kkrw2VJZcMfvCE2PbyBHXMC2LD6n8d/NfdLaQeECueIU8+R/pbs/9JTGhKeThAUpLtGbPX+S2993bcX9jNsaTrzsJjPTXe91Ys+/JMjmZ5yoa3/J3cvPmDqjc+8+Riut3vOx14Ll8aLA+wf/NR1K63ulrCpZvl8OTInecBI8YYxkZ+UiToP9kVz4JnPrGA+nC4NPvPsU19g3nvAgon5DDOIwGBo55mhVvi+UFwQ5iLd2abHatPg8SzCHkLmc0NlBvm8mfHxCIiDCKURnaFONTIjnnQWKL1OvtoEkFAAGJ1kS15f3FKuR6wGGCF5wlElQg03+LuRHLVDlCaoE8W0y7iKYomi5aGAj4veTFBu1ciBYpQk3YILIJIIcUMuWqj+yHmEUQATgriWABeqjAALqibCMQTeQ7YB0QB1XaHVR/YbCCWBzaK5CRWuUpC5ybEOM21RIyyKDLqtSCo4DmwC9GmE7YAcY/CxAGUyKHGAZR3UtcUbJlIr3U1+VE3L8FDQWGBbQD0j3pSo8MO0hlXBEcQau99Vyl3dVEsXC3DVojoWRDv53DZtm71NvjMVoGgHhHQN3PSp796Y++ap+9TzqMlQ4c67vLPY0pjQ++Elx53/hRah8advFn3js0dc+e1gYXBt4RMf7M2JjLxJeo5c4NCWn8XO3Uq9s9CcL2QRhRxcvaGCetRdhf2n5fdrC3x9avOMOvv/pqNw+7pl96655J9q2/7nHNKMGqguHBGtcBBhvDQfdId4bLfaqCcMeCDwEINNzTGjw7JqdDA5uJzoeyYu6qmeWvpzUy2r8k9oLFU3LQEA6npGXUTvG19uxExxgDNQ50uK1kYorLrV+Xf44/YpDVgvJH+EGpRtWTUR4Yaww4oiTRhgDQLa8kjYJuUFIhPMgBRO3DCA+JCsoDbibvByANoJ/k7wMDLvsfTwnSR6BVX2g0BAIAlkA16JIALMATA8G1FQAU0soLSlvJzMSGhMDHpENRhwPrhUcTwvoloV2ulBJkc5hYcpisoRju5rmHoYBAhZURlLWgJUONwkEIdtSFgnbIiEhh822lpbbcVGP4NIY9Bbloa/NXltNp1Zpz9OtzCeJt3yjQebP/X+2WGVtRRFUkpv/8MfP1APemdF+17x3fyF3crjxjvumdFSbqo+nWFu404PCmJv22Yn2vh2v1t/19Xfs4pZvx5fGJMnPQOUQboNKY1c1zrrgUnVSfOm6X5fUwtbUeQ5844PC9dfNXj9ZdrJb9l3fy1CPo8vsA4EAAguucMXjRxN4agYE7t4HtNViKvt9Qwe63IOIZiEpTH5kFnzpMeiR1bvkKRl+uM2Dhk9YnbWc+j6o/ppIIuBbECsD7DPAdQMePEd7OA326U0iDeAIapp8CNJZLRMHl2ZTCjfcJfB4wEJCX3ThYMBfVGI0vARNwt9S4T4goqHWqAIRUKBgDpOxAVvR6I90fDJr4vuO1AeiERBwORUIF4PKceKdRQrUVApi+mptpXZAjI40ZwKCEMwTZKi5FTDKdEODzWDUAwZVVmBOgKqv1zgkbgoImyBoYChJnM/TDzEPvEPUwM9UEZJBki9K7LVvjfOf09HMyM0oO1o5ObdfO5o78233tZWFE73+h7+8dNHXHTMvhqevbCP04S6OuvXH8y8viMeMtym8y/InZ35qvqv732pZf1l+nnlK0vnFmr5ejuKim+tp5VmfNPPMs8M9q1ZN7R62eIV0LrkQnuX0mhGVUGpXF12u7jPQGryGuvmfjZunTRhwWlvKN43eM/S8+Z+6kzkMpMQgbcI6jJmDRzgRZ7VZDZBhPO60Wo6ku1UKnARcqhnN84F4gBe95yYLzalU6jIX4wLvSxa+bbAF46OlBUhEwkd+QEfzlKFG0hc8ZvPxk683y6lQdPnrMZqsZhD68tY010L7YRRDITsTBel0FAYCtwDPn5SN7G2xMRwyxFICv090SubiqpAJUMQj4ogwxrA79CDwBqxpCj3D6aGCDgMC3jw0aMHMXR8T5hFWLkoLiQxzz56jFBDHAxuER5JdYAaUWwBwSf0u0CDdYhohPM5VJdGMAtJ8iK79v73aaS94aKJZkbEkhDZ5iSEHZA4792kNNhzl/3kl+3HnnJ8a1iPkPgOUfwyelo7sOj8iqv+vbjjM2+fqHIlxal0dEY9bbGdf+Ndv2vfv17YkJpon/f5g9uzOhtf0ju09ENH/eGLp//My9eclJl/0dK09Ba4SjDXUauS8NAf7/P2r3b3dnWhM6/JT5o/K9UxoVZtSG7qiYcvQ3kpRzTK6f2/dFTzmdrSj7+bOQcZ/IoDzjPEoMghbBWoIVtfAHITz7rVqJ9c+tVpwNI1s0bFDpkW6Cj02KGiftq01/0yyB+o2p6rtK5IrQDjikWLSaOQCTpYQuTA5hMob2gQjQm3Pv8Of94upQHhG5R6n3UGJiARytcU1kLFAzI7wbhsB3UwMF5qcwf+hFg6YP1woJH+BNc9BQV4a1g7ggMQPgZAG7Ab9o7RvhUbeCBMUYhOCFI8L+CeUO6pEA7P2z7QsiGK4k9av/qghcUALjJkDwFKSR45eIoRGgRLZbgOQEWowXZDRVUa0bLljPfMH97VgpoSKJUMmkVwLCHpjcrXJmITAzOTErqvaUWThNRxc/puvz5c8N7T8g5KCWNgm0Tti5YO9VVXXP7n+tw/H9UW8r6VH8jmglK+FTzFq/xx4gXvPRw+mSX5JQc88R526dtW//105zP2EfVZXxerIduysp0BImHLrdHW34TiV5KrKaWHbqzse4YiOMVVz6woM+6y5VFm8vTWNi0TtwaPuh+40Hvg4ZVWdO/bTvl93tn/hvNuvnbJqSe8riqo9aeKuTkKypIAsLfl2TGrCDWLYa3/uEz1nG9+FCmFyqTua8rTj4KHPNnXWJdeKxiNVb48SxUqTJY3YgMJksBqFRQsRd3k4lIJMF2UVZFt9AjZmidvfb1tfk7AK/Trpodh43ORLA+NRkG7VfTkTOXNeMOsEoc6jWh/DR2oEUg6IkNgX7gdHA+PBiQfpB3kLazNOM35gKe4MeJmwCbCeEDdH0QSZBhvYEGchxQN5LJoaJeCbissqjGDtyGWjKBDw6EWBS1w4codTlfIH9MWA4UZRkgkBcfhqXUW1L3IE5QUCuJCfsO34bXC7vBceUptqPWQmuVlHXjCIcTJp00zihdgwKEzwviFIhbFjbCJsVN3XvOJ6/a9b+DC/dYd5mQe+N2Pph02a98pDESxmVgSONCBQQ3YJIdKAXAFu0Jp0SX/1t95xuSZiN2ie1UI30RcAMeWRQjBLx969U+WMjO9eObP59vlScrnz1p/5TfXFs57zww/g7NMQRVdmutkowkHy+XdHLmT0XqBWbtcD577cKZzSmfhtac+XKm5qwq1p1bV55z1tH9ydXV8Xe/sk88Ww+VNT1619ucHeuKp7z923x/fXD321pselYKV7CVvdeFPhPqF/4iVI8cR1SJQOlpCwUPtq5Ujzv7HF+e8NsWcXPxQo+DdfITp5hG0NRvtzKL06afJ2bXrESzR8lItRr+4Njblan6KZ3r0lq4n4rg4BSEeN+qeZ8EAxHP4IoLeeFMv8/LCnW+1UyJBwI8QGmdVRZsyz1C9Zk1C+2l4mfHg16D7g08QxgAKGTwVdDxGgFvFGpAHkYQifHGIbMI0Ap+iGQAnTDZ08UHsEhKMDApS0SFfKfhMRWS8EA0CrNgCUsPnNYUDy0KhF+hxADwCsQvDFg1SOJUyO3EEfBUIRgGuS8U5MAox3dsmPDzLVxMoNYnu4Y1OTiOkWcKV4pxTUdyz7svc3LG69r4pv7mG//PR5zN33nf3Fc8z5318AadZwzvHRoi2HgAJe+hhpVduvvlq64CrTo2CPKQ91pOWFMKW9oVTPeJPWrhEevqupg/NFzZM5OT7b/p3d2XuD980jYo4JJcfHsvw3+HVwgLAAkfNpnK08Of19U/cXOq+Hd2IoDnkJ03KNfz7lIGvqU3ZRX7bvOY/XIQudge8/mPLPn/sPw/LT2PzP2m/5Ozpq978c+bn1fMOVggoQioxrUNcy6JEpcqU8n4FHZF+dvf+v51w+NkfXVMSfv0NdkbLsguvQvoztGKoIFGPtlC23dwNwrz1zrNT4gpKnTRKJrzgpTzuDgCuUCZENaw/7Lz5HezU+5ehNNK4IdpAaehkPX+CrXGz+3mUrWbNKBP2eMC1YPEReILWtFl0kFYR3IPEKDliIwAsIN/gYgWR0coMa9wAwGLJ4drHgiUEQ5os2Q2MCQCDg0aIKHas+VUm09HepQNeAccKKAyLidaKdJq4DF5KMhf4KYRPAtSeBGQNnjZPL2kHXndeeRKsJOxB60uEQO/pLYlFGkpUTAf8v24NmyZU+t941NU35L458clHph1/yKf7i9dfes3bz50LkxUWLnpQgc9CJWTSpWeefPCBUst733tgVIEfK43zEa1gCHRaxgCloS5pW3N01McafRVh4s2L7rlD7Tz1461TuD4nm0pce7TvyIaD6XiyikBpCDuorJBt2udsLiove/ihlcX62qfZjvaOM9vZH+lH1P48t3LKm9/whP3w80vExlt+/aUvXHTwIafeVv7Soof73/HNgS+uPvv8ZsB8MH/DpIBmbKjLiSYRGmrhS0HXb2ufSDHT3AP65M9fLnz7uODYFY+frFbhr0FN6PoqdV+RNzNr/XwpOz2fcqcFqPjr6iBbgbwyiBgKBkKKSNxCL9+R4e/06zYpDfOQmJEw4ni+XJ/JWFomNRjDy8oX5Dp1JB+un5LoNRTUBuWAyCh0i8eAFHIoqMnjlWjuNKugSZpbrBCWGnmf2J8T0FKdjFAcgaOThSP6hkEloY2eDKwjIgUGlbKCHYuJJH4JtZ+NbSA2UCWH3LEItYOzho5LQ6i0tdXO/OoGPNu0AcpEsoTGkdAEPfEEUkMgLMv0fjvS2dLqha+//4p9PxOHi/95ykJR7px28Ad/9PPfnXCKIOuplCaZcr5JMkuN2255BHD0ae/6enFlBlWe02Tk4G5IjiQDt6kpCVABLGpFGO26ff6vzgmPPup1R4NSOTg3qFEBdqX7H96GP9BnDA6bDi0LnpwCVNbc5KMbTlf1uVWLlz3CGI1JfOVN/JSD79Sel86Iz7PX/vHifd984612zfj07e+/888/fvORP/5b9qeHtEKFJZMpWTQaEmqZW4MoQwyQVXX1YPawWvwvZfaUD/6e+fxH6vmDH37meMHRkKBvChsGmgZkLe+jlpD3cP+pfevnNNBoyeHCfBYaN5q4wr1J3k50oEEyUDLYXfmzTUobnknUX0CuG9ewYLLobQ6wn1K/NiFYEU8DcUCKUegVe+KpTKQjVhoWH4YDaQoyS6QrWB6+xpfYkZ4ImN1Qa5CRA2qkQ3EfiQ+CmBoOaWlU4xal2AWxySASyixawottXA2XQeYeQG7EyWhtWkGPtEp1TAIck5hjG1VBoPHZCnvi9256u0MePkQNED5JlpJGQAswMklNpvKP9UzH1FtPO/GRy9Uj8sba3ziT72nSp9ad3I8+eNnf78SuRPMmp2ZEqxy0Fk457ohWI13PF8CE4GPZeKKEjLGvgkKoCnJYhGY0YnO5e3972M9TIeKcCtoNUUHwrQiNJiL5RwMiknVdpNqzfp38hKyYNjqkU8LKivWPr7t6fTufurh09/JLlslzW45yZ7/xse/NO+vq3/5m1aPMo/865sInPnrDWz67D2sFQG/BYQ4lNrnPAM2f9ZZo3Zre5yqVHr/ygW8u+eWEY067Yd/rCsgzajdqKI6O++PQzLKFu1fpcOLndEUOy0GDb5M4Pa+Uu+OgjK4PJp5yJExB+8Y9AzQ3MoE7+7pNSsP9J8RD6RoC2rgirTbd5QOrPQFZBVk0X4d9CSc8Rg0rOhGZw5emCD0WFL5TioFg6eHwSJgc8R+wNyJDejBgVCBtD1yKGH7CnAivi8I3HMwbmA4umiwC0Gr+Jzxn37Sp6j46ugAbB8yYiA1Jxgg7oFN4UvAPvrNYQUkOFDCKM56V0k+4/hwElbFkCUcjtWXTvGAZksWV+yf9SW45/L7Dznj8Ny1vWKCWv+BeeOxzQX4iKj4yU85/P7r1WSayvWYteXQNM2HupAWTkaYPTZFzIfJjKIojFJycGXeEwksE0Cy12htm5m97S+ZCiwM7ZmzwCvB5hO6GaWtkJDQH+D/hvfQeV0Ugl88jjxCOVrhiURqstTVgfsMIpfNya/niYdJU9vlHbkb7Ln3fv2xQlv5jzoQTb/zpFPmCwV+/Xr3nKFTipGRaDGvjZsVp5tkrr18ZQAKk52jLT5IbhU/wB/yl7SFxnz5rYBIeDjgzYmBpG4OXVNLaoetnZ3uj3nX9Pp+OHHNCtb9RtjzDAkQCjzkui8nlNhVZGLnKjr9uk9KwIkQVmATRY9uEFaejFTpanodCk9/DTZGHoJhDCaZlI0sSClKynuActKz4Fmh8SlUBIIjEL0gILGuYdokUwStBokRg5BbDUUSgdCxTzhTMehDlqwAesb5TXTXrM1NRCjkD6wyWAdowgpWiUjJnYXywAJCwgJA+6ikAigHDFsUaUCaGefunnz0CQA4Y/eSNSWaDiI7Oj4+IF6CMX+H2x4T2DWvetfjy/FsXTBz4ovu9I5eKbo+bbskLSjtD7fyIArjD3w/BbYcZCDiUR4nQ6dvmMvQz6QvJ+Yhk4gF41uG8VgY2NM9cdeUl0SWnOmgIAxYPTkAubJwKO9MZN23J55FvIsAWDIRBXEBDOdSESMeAGLPdl4Rac+mwxweceLY8G/qxWBqyp+yTCoTuR+ekOqXFv3gy+OOJa6ccAjmHeEqEghokIKDMlFW5+z9XPmZOnHvkfKOtRbjtI6UMN0G/+eEvX338H66/Zs4RUogsSJX424yDzrEzSvr2uiuHU9JmWmzKKoIHzBSWNaiwaUQRUWYYrA2TCSTqLm7bpjRyrUJ6wHthCnluBTTDGtrfNdSSradiS0LxE6hsiKNj1cgVi+sn/2MxE1YCRy4kHVI74YZILMBkSkFSGD9t8HPBWEB2HJwmyerjd5wFfljy/CPCDqcZKr7X2MKccpARXCSwRzKbRiIvNmhsDtKvUFsLrhFUq5XRIRHORVS3dSK1sCxzVOqB46DDYiTwlxAdJGtM48Ph5EhGs8f0N7UJ8l3fGro9/r8J1tDZ838qLw71KL1qNm8HKL+NmwBxRnDSoGIph8rhvIfGa4IQ1FlSj6miFgnQEW7JtqL8JLoMKU3O/X/9i3zc9XOWzEVjXRNIKrAq1I1KtmTww29HPpM4oP/QzyBmHZPNeCbKW0rIe5ORZGk/8vnfrpuUv8QuWI/AA9k0xOhze3pS/mPsQZPWTXnTY08/6h125prZPD+UtlxkUAWgBLIv4rh1+RW/69vn8yfMBxgQ/VjZ1/z8e7O+ok/42wcC5i32F9q+NB/uAVjvkeHUn5s+paZoPa5+0qQ/2egh2Ks4bP0ZAN3ToutbYrqKRg8WaqkgR2nb1R42u6uXfoscgeROaTGSbeSVgIQ0kZB3mjN16v2XnZPuCtzyBDNP6DBklgOUClYNmxLAC1AbnK54mFBVlFgTHnYIRxT5BxIdGSY4C+6davWTrQVuCE5H2CJobqi3ARgZsqTISQYoUSyjkQpip3BfBbwrdViFtWa+m3w4EpokwqXtUs4QZb1SFRywTKhiyEjguCxF3nnZsNw1E2sT3vXnT3spGfl5dqKJE+9BbhsIFKMCOKhYKBfvuXdW670fFv5R/DLTE3563ielAhrIrmWb+ivz2j0PUGLfQW6qi0LlVPUMyiH8apRYitAZ+RA5D1kKgD0A+1YVdFf2JK6e5pzuf/znduXcDy5grWlonhDpaEmLpwdgi2Re8WcLYsPn5HtQvoAALYCXsAoSYA7qksfcYDPjDg62nLXscc2xF1phtt4srX8yaPYX96cO7OnoL5y1eh1z+NBMT3IyftZHAJ+eeBRLN1ctW/7g89Kf3uLkEaDyxJAPW97wBtGX7rgwED520KnRuaejMg5SVUP0+po11a0U7GrRbg7b3GznOlVbG0wdKrWiM/nEdayTn3FHWl49AaWVUWIKXeEQAYKbnqTDzm0gp+ReX3QUaCOZFgzUs+qtq655zyJrWlkH5pacFsDcYHbw/GCNQWpYCCqaNlJ/nEYBdkAv4OXEUpJxJbwFp4VSj5wkCDFiYtAwiQ6wFhQ1BbQTZyIjQbI4FI/nMpO6eqZrIFGEoFA/L0ngCzwgILUgEaYwU3F5SNIqJDLNKqWCso05G5493EQOkGBQhZfhLbnPOLBytVKLee3X1+mlDR86+CelD6CHyy3tx3D+Y08xJx36nD6x75lKprOUQi0ReGFGDt54juQlOVE9hSAuBJ7jWW1MrTJJqi7pMey77li84GunzEelGvKQ0H3Tf4lGsfkZXuI97YWvN10PBQzRg4HVr+DbJlx3QPdKYU0FDqam9GsW9Vu3rVamlTtTtebON9906HEZ5NTCa/7cvt0PHuQ/5a14YmA5VwIz/+Q79kNWrWNxBqqIIemDQfn3bw0xrz/3vJ4Tv0YPz/AlDWtdmrXruRy6QMeh6ti2GHBZjRnMihvYqav7YTi7kWDFZuwUAKHfNMCXuIeX/Yp8ysktvngv/EDThKLXLHPAQ2/OtC5fOrvAw9cH7Ai0Vxop1haPOM3osEsNSw3SpcFg2fEX74ZtFTwICT0NqzY4LjFtSXcCX6Sb5oDDALtD9QQy08EmC4EVuraSM6pLO1DwBkYlZC0ydBDew17ou0eFYOBmg7YGyoUoJiGO8aCKX8BZB8pX7oesezipaAzJ7JBYpzWv5Ppmupd9xhQ6SydM++Pytzqa9ovFHzqEX/GX2vtb+7Lqfya0mF5LlsP0IoS2dfAoeSzoD3WqbUU2ZpjT+5ZFC9ZcddVTjG6KB904azoTVeCvoAcHt0v0RhL7BVX9xTOdfJM8MDhm40YtwTgmf9y/tFbrzVLFeHpDuX8oGKg+wmjm89oZndk52io+ddhdJx3E2GhJ76X2/dHPOHtAhPf2oANPmTzbbxzNVc1sTwcmuQ7wey4NlON9D8m5no8+ccjFcg3aEGYjjuupajghW4fjyg+qoQmLP06HTF4RUbJpILPfKgZ169B5BQuoxXomThJPR8a4U68UjRr+t9VhmCYsIOZHCkRPna/NMpujgbbYAn9HFSHiVzSVVFqN6AZiDAKNJGPCCvELeSywIiQ3iZboIviDqSeJi7cEJIS/A0obGbm0M2QpEK/wz5HLDAhR13fATnucfkelPBTojEhHDmKU+raR/EeV3hEcgB8NVwc/Q4E+jDdCH0XUN4hbXn/1wSc1ewin4HrJv+R+MN6hqaWZ3qPfNSV1+ZEn/+vBt06evvTa/jOP5ZZ9P/PBhZ6bvv3r32sv8otmNsnAfCEKRQ/M5htORhvb3Rq3Mn4qdMtPX3VV2LacOWPhtdb/O28a0Ecxn00kLs0BYlVEcXT7L7/hvLTPJlKjljW20Pn+Fc/efd871rD7N8uT/BK3tqez1tOrvPaU3jxXZ8tsNW4gRo6i03HXdd9wMuX23tece/hMX3TQwwi4ywxTGmJVtxQFQ/FUiUs9hN7Qjysf+E5TndwuWI0YpVZ8iXv8sezhgwjvROhIDmT+kCcJ6HboMV57zObwnHApQZZSsqohYWxXt2Hp+RLzQASC+yaaAeOSpEyme8KUCmpUQFJByQcPISZBXIlGjNEMsyfCZBDlAABJvw4PCxMN6Yj3oAj6Gv+g7OEDXYN8GyC2JGpKiKLkdJxApUjRZ6B/w4Qcwg1Em6BG5F8Bw4HWBE4A2FISfIDiRb+hNRD0Wzo1Ol0DA/OBSz63/oJQ28hYaRwJT4ujbMSZXT/uEYXqhMP+8+Cxs/XiX4ZOOyl/w/XsG8/qXzth6LL0AazYZPYzKKyK6MDWhEZki41lpiKLAfWSLOmmPz8qy8wn3zfV/Zgxi6mgRQcqaCK/f+O8JLvv0h+A+zz1mEW/vmXJpZq/LNeVk9sPe82E1OAaV5uUEpqKYrZkPFRcWfdUhEcE4bka0xb3Tvv6oaIDQHZmIKiUYzv77Apx/mxUn7CzG1pK994YT3GnLcs0QQEcvjHUfwutctfSJfmTV7CdLf1orFQDD4S1LAMyhGWMJMlCnUtUmEKQhgqh79Kt0EFJhJ2oZSNRbDrRJkEeoiqmNJA9clmer3pqpk6CEToY0QxRTVJrkXJBsMg4DQUx8S2sO3rBXxJtCTNLrkCkRoQGawGsaKOKjt2QYEmMEvtD24MchdYt1c0ww1vuEXM0gIpxavAs6HJgeSgxifgrBgF/GjDaOAy/6eixCKUw8bKxbO/hb/3Hr0+diRqTROA4LYZFp0dB9Eq69M9r077/moVL7zrgdFv+ZOH/CuHNf8p8p7U3bvJ/v+bLzcuaBlBjtb0JhI2QOt3mZhtRPNH28tYmu9rGLL+A++6RPSmtwmvqrKCEEDH0WgYOSPIA0WEJO6Mp2M5GA6SpSQ7CvuSdU9GESvrYWeGiNXdadzFVZs0D8fR3zTgqLDOrDK7aqdW5ZxstEZwAaF7dN8gwK7yZXzio8ZzQtAGSpsxlfX/1p8vM6edlkJxef7ItvlFp/8WR5cmZng5Av5LxsIyqSxMmv+UUTe1XWjPPe5xXkaMGJ2sCClnJUR1zZqGhNlcNgg1B3QQGaVc3mkgitBcdD9lGGhgBwFw2OzDluW/9gK21hiZIEyKFeBmp/NDdEw0k4W3JetJKgO7gTyNjAZRG9Aa+hRfoUfSBzgp9jpaf5hVfEe2BsnG+RIGDERExGdYGGVdW8/u34Xck+GIlSNKSvIUJSawMshreM7zQ+aGTQSoj/w42AjJQma+k/7jkAJAi2C9dCUuYEHJcXd6zz094a0r3Ec/dc8TJ+eJV+lk5ruev+XM7s2j99sRN7zqdKeQBpXSK6C9LgdhtbDP9oNHmf/uK/X8wtYzCha21GosGkIFSiwBxQXly2Mlg7XQntDjbPM0LZycf7uaXQ+MOObKEgZZ2/82Dr2XmLezrTzecRm8jRDn0u/tj4fXTUwND4iE6ipyxDrfi6ne+/R1T7pJ7V1wz71DN9DLGINoSpjJIlYhRuT9KzzL5mcufH1RWxVw70NKYF9pYtO0KysKk0FsVA0QF9RPd76rwXYluhqQXik9YvAKEASOui21nN+pyDE/BiwmNuAhWGKvrAcwg1zsW1dY6ExuofkHxZrI3QRTYJ6FGAIGQOpKQQXI+Ih6QBBEVTpD83WgBwPtBSBB8T7odUlwIxAi6ge+APpBzEP4fUAzSNoy2grf8IaYJlRTBvRD6SkgfpbCA7khsUXKGIMyAkaB3mQ30EbkvPfQTVeTmYO4HmUdqiSeE5hP/0z3itfTpcw7jO5rWf4x/4MC3iv23PHVmu1W+OvpK6yAf7LPqwsPONYtcyVBszS42kEn4Yotg45kavX58x1t/+9m/T23kpKjVR7sDH0Inljm34aDW9PAUkE+bHolkBBjAtrfhqdpsP3iZMEfQzCvVvuyc5eoph2hqTuq98keP6MrA48/e88BarmV9RWqnwji6JP4hd4JyYnD2quXFbqvGrbxjtSrrxjr1++d99PQ2lAsc6J/av+7IVm2lLWZjVM6nNlj0kDM5V2yTzXoZ6Sy4a76JdUWr5JfKYmyg/ZcAwGvD8aA161oFWbybWO62b2RbvxBlgCiITUH0kFs1kVG0evC9w65DReC02Gh78s/Tj7LKHvooovWgoiqigepa9MwSlaAZtSYqVL4BYgxvAMaFZ02GlwD9B7AbPMCodUHqO3DFhODGdcCbcCOoCAo3F/xcAupRAC8EaAH1PraQ0zuURqn33qW5o92UKqOoEmiP+BqaL0BeWDEaKpKcpdnCkNB3mNKUfU5liDoiphTvf9Q13YIChBbKGBG/RK3TAPXnvnrfRHNA7ftwyzc7z17uLL/tuPnp4HeLP8YzM+vZx77e9F7RDpRsXI1tNc0g98Fr4JmGwd3HwnGAsnCw4lx6WiJ58bumv3bpncchqQMlD0LUVYC5Ahe6iPK4cLLBgQacCoZIXh48bvRcjmy0yJs2etJog4QgPw+48/BN+SgxwotaiDYGTRmohPbrBqJP9np8Q3m+mqkOHMhK69ihB3n+Mq23rKGqcGvDeviOWmA89OUPviEfuU8OrFBFt7ORO/dtrej+xzRbz7ZP9fYP1/CqHRQ8VOhMnnWWNfN+lzq1zBYtszwoUWN6I81ptzXQXhedaFQ0erP4oo7YydR8kONRQAEqOMmjnd5eUEMSVYLmAP8Te9p4KhB/pAuZvqHXR41JVUuDNwKxSpoOzCEinsNMkfQxkAF9i+kiQYA7ITjQyGnoK9oSU5S+hV5P4o6+IAoiXpZcM4XsGLTfQXABAYBchTmwA3WDhAh0muyG2rpIskJTUzpqk/oDgQx8b7JK4JeJj5znD3x0/ZxnZ4kV5DkiplHPB7W0H/z1av75vLT809Hvm15fmv3cP5qPS/de7n7hIHvfp6cGP+758kKgHZF6hT6qKDuDyUHwMF3K88/PifNw1aoQ1rYUm2rpkd8sMrX3fmRCakeNsZGJGJ6GHf8Ls9t1VaukPvH01A8+cktFaDyz5ECBXZL2Fl+xVD3jjt99dprbnxOtlP/XN9an3luZUbD6Lj/hQKlFimrwTMYNz44kzgV01s5lXH8GZ/AOxCKWINEmUGxdLnjLrln1NdicChrYI5qMSDXq1SHfEqhXHhUxEEvg1ApWk7LQdnzkW+35AqUldILLJ69YdLyhfeGr9xk/X4GKa1TR0ZaIKDFAyDWbxNaHz5icCEeQ4Un+NhgHiXJGXxFN0MmIlBIixHvyrwG0Sg0y8IlMDIQOcO0YDaVQgiRy0y6aXpeUKQipw3YYdvOCuiQIi1CSnUSskfo1vIRUzQpXJa2IaDBm+tpO/8VTx85FxBHVgxQ0tCoLfNeEpR9j0RXJ/aT2p67/WzDQczn3cbX0+8FPHNzIlaZ132B95qwiXMcA5UG7oZAXHiSTiTIMN6cUZ1HbqOo0lQDPrP/tlsrpX/3jmw6XlKEMRr8j2wil0TzszBZSgR0ET2xHDzVUaXvjrQPdx6e47thYszb46NybTkIFaSCwtPeUL1nJr/xM6/3Tcw8/MV2s2k1NKEGFjhBI9QGyxnNaBbH8bHA4mxGRFw5sczJLcZxF8MvMHTdXcOCeNFFREZPtooNEAwiD0Kw2ZETFEIFG4AITvBuENmx70p1DAhMHouuD2Gkbng/UCwHuZfZhj/GhXIdsIh2LyJCSPIksKZ/zhb1BPYneT/YC7UfUmhBDohnTccOUBh0f2jysZsrJTCgQTxlYHGgPJAQCjGx487W+rvS0hEYTE5lGBJK1ARMClnd4fIlNQSPFuOAKiUAfyQEsWzz07b/9cDXMo2EaUH2xkCtmW8Mv5KNKK2LKX0t9ecIG5+fSJzjnYvM786vtVm323+/8wDkmMhJEGBrIdyT8G+7RwGNSNTskxAuQL77q9osOOOS5f835/OEGc5DrKiv3GeFpw8N5gY6GPyfigYZNc5FsI3uMvG78epsvScoZL03lbis1eap/xruesep6gdPlhpQRfx9z7+jsMvLIxZv6yw99/S7uxynhhNykL9v3/vuw2ULGQ+fKPnSMQ60KJmfnJz3xFDMbPR2g6iaAQgwriu0o3bqG2z+wbB31s4DAR4a2g4rYspFSkQmOShhQwGOEZTAdGOSODvvF97MZT0s8EyR5iLGRtZScFeSPhhsHXvCxjOM0xRZqONMPifOMdCRy7I8QWjKZybOCPxCH4G7EizDJdCaabPw3MlTo90SpsLAgRBONj17QoxYO0zrBB4FYj+9b3XyE4tRtQR9uAI7rlXREU32q8LvZllyZIvOInOBiNCAzy512zaH/noZJk7JIGo3llHfTUw+dfktm/ZuP+4p/fsZddK/6sc7Bf5c/eWCxKVQ6f33za453etUc4EKgy9hDEyI05Gajai7MwC+A067496MrljJPX3rW1XMFvb+5mJUb+2w5iI0MdrMv8RZfjpDZlj/s0Cc86dTFat5+i1LFS1o++JFni0x/OSuqBzwlMYc9tlT561l5dOcU+tTMfn9xq1d/lum75E9z3nfUskM6WqC5alpZU2NKYlM7pKuurJ3eWUksOjTbBfuH+oOa5Eyq2NMWSEE+xdfReJeFng1lpepaNaZRQdMaGwE9Dy00ZejYm5Zvh8a+xU4Js6BvQN649EaGQKuF7xKKg4hsyOxrCotOTVks+roS2eAfnmXYEAhNkejDFwAGJUSUcBQcDVsS9EMycnijB4Le0wWT/XGtxCFL5gJ0O+hZUIvROIhDUzvK/fKkfOk+t30SfgeXSZwFOJ4VE10JVDh82uSFuCbhM3BFAsDhmmB6fvz2Hz32/r9OgNdJRyHvhlj/+5fqhz3jcMcefKv/urbl6iODP8qt4O75wok1pC1ol91w4Lly2UD3MroPanCrqrDpeS/HFKsTO2z70X/d0PK1/a9fP+X4iVaG2TDRzJFiWUwPjyO5t2RiNn4eeRmeyZFZGPn2pfZ84bfN31FIG09W2/kf1Ab+7zUHmf8uGnwpxbgVL+p97R+f++oti46s+42CpJldk430+469YNm97UcwG14/mWtg5uyeFGs7sZaRUvbd33U+8n6EEVETDLhRSgIhQ0kNgRGRdadhT4YjDQUjUHuDqjUY+YwepXTZhQEOgoQTGwsE3Xrzse3U+814WjIn8CfQBiIDOdAKknHoaUMBd/X5HV1CqpIgDTBx9EjgZ2JhJHChfA6/p+HTcBAmx17gUhuHA0pLRgkSgdjF7+S2xTEgDphyyW+4WMQ7sGzQ0QzeMV+7b0n+UA05hgDgJXAVUDWTCp9omgV0aULZNEZaY5xn+A/BhJIRxFmhbNz14f8cettczuIdPXCMP3+jqlU3cAu/du0NFxS8+T9vnN+yhrvoiCOlaaXBWc/+59R362s42aii8zEKivAtrCKjREVoMuvu/cukTw58s3T0rfsy7gfwaKmGy0wkWAsj2fqOznZy8zu682b7gR4QFIuFd/7iCc6+6b5/Qs2fJ62qMk+ilNw986fMD288mEXWUq6LmcWsai1ED1756Z4Hhelt4O/wKvqe5dhcVhf9FVfeJP/kbcagAe4ENplYT1gS1N3nc3l0Bg7ctBx7OurGopkXmpU3XMtkLBOd8zCzyK8kTwQe5Y1uuM0GuKNvX6C0ZCqIbhJCw+mTZYS2ybFSZmUwWK01CpVKCt2Esbgb1xdKFk5AH8BLcAIcAgojkiVipTMlxgE+Q8rSuekn/IWMVmBooKIHUQgBWEGsdDz+BChvDepm+GL6GXf6wrzroa6aBpxZsjHh6uumTkLzIJwHO4EvDptQuB5tRPGYGvIneOklc7/ff+eXf58DblruVX/5naqafia34B2Lfn3eQTX+H0+dsGBl9iH7jemhDROVx/WLRXtdC7+6oIkwPrWWAjp1oWazE/zt1lVrbfbafc45eW5Qj1GKDzFmW+LrXgHNywGP3dGJ3tX9gMJyWT2I3L9d/BuUtl8iMtWZ+mJbyRzxcPjc8rmz773tO7qQ5ewsj3QTh8n7p5/0mUv7f+01VAF9jURDpVb35eeWXKlZZs1M5bgq+kYD84XSomS8xawM68tLZ/vZFDqKpI0ywjMuKh+S4kvrCD8mojCign5dwHlhgnd12+QaIakDOkjA+XQ6KMBgJvCJ8rGQtVvcb1/w9P7MxIEyumiSlCL2klw1sZcxYpKl5JBLLAWsNaBS5EQalqoYMVEZ7QMLIslJRFBz+Azw3FFVOEpdp8cHpV98qVXqdbiM9XzHB98A9zRayIGlQSUsa3JRn/30kvdamg/FQqrkY8SWE09gUhYTx+MqdB0B9TmU6ZXOH37+X0dfgMmqPHbBmtlSr5qZ9476V/c/titS/QXHOi3CEbMPd4Qml5vY+ujscHrDnZdz9ZZGH1tftFZ8e279c7fen/3wxEtuOfCcBe0Ao4koFYjoFwdMNACbqHWPmveoZElcwEthDohZAwmAiBp5bGGrYTAQPQQaCAzMBRYqyU6kPVE9izyLw8KenjJoqRBtaLKFF3q30WeMgIeOAJwQT/vKebfeU+4a8ifBw8q96cF+plt5amGnu+bHJy6oapUW6F+WaquDbcE3nnvsD+8XqqTboi8fOhoM/OcW67Bp7Ss/e/Wv9pGY3D2f1mmxwajg14QirQ+K5qT1iI3qiAL0+u2x7hYQ4mowqOsNxGotRM1roDBXeE2OAuctkeAubC/wtI0HJ+SD9+RxpAkA9QPdy8vvu2Zdilkxow/hxoQnYTmxF2n+CYUQm6JDQVHDv9BuxHHwMdlAnPQNGYi0F9TChCORE4w44DAIBEuAGSeUh1LN1+7utd7RAnJNxsIxdT1XlQPF/fTJvzu/aE6qeVkjRsBm5AJbvQJVhOZV8/7v7q+d2MlHX77EPPtrd11YXHjqndcc/Jrl+3WsnHzU+5xHba29YahV9ZD84DvKi1aE9cqRB8pDiy5/MJr5SKpHu0l72wdniOG07yAHF7WNiKwIZ4LxQrXB9UAbGD3MZ3QbgqVEwoDK9eKuOVTtIuKBLMIecG8D+ERf4ElKYnSwr6GG2Gh/hO9BbbQMZOShzibF2mLPV0DSRLkceolTyiyaAU0//zPlc+9kZhTqXXGP+dC8Gc/c8YbDD3z8R7+/4E1urqcNxSNYYajNrrfcdOSN0w6uO7oMt/lQLhNH89oKR0yJ2h98/WndTNuXKlmvwQ0qznMPVhYeM4WpN1BorHcfvlT03AklIAQtJ3RNNGJlBDQDQ7MkxKsAOgdPokr8W03zjn8cprTkeEwfFn1kI8lHPrFkWvElunEyXVMRQMO+wzRFvxMDSWZ45G3yBVaCJNswyZE2hy8oBE/H0WmTBQLl4ZkD5nGYl4EX0jkwtWg4y8kpoeg+UWrmB5rw2JPNUxIrZ1+fWZ+xVh18wu/foRXQliJCIbdh0t74d2Ts9IqkNpb3xRMvf+vnbgzecEuKuW1tTzG7+sKcv/hB5aD+cpF5aPrzxWKqdNiJHe7KoXu9xx728379owd8+7f29Ms7rlvE/Kvpnee2IIWARbI7ODCy5VEhBiozZgCPtUUjJWUd5gw0GAQnktGjZVZiW4XItcQegHWCK4B44LmBSo1EWLLHwfPAIWMFPjvKhcJXAZgzyBA3KiPTHsKLCqIl0g08Bz9EQblFDUud1dslZga7/tm4K88eeew/bxqoHfl8z9DH7vri1ImlUodkOflSg1s386S11xzM1EH1SDaMUVNxTizY63smHvS5P328o75s8b+C5UDf+XX0IWTPe+OhTTzfZj8Rzj10iTuh0JXA7dHkmUNWvYIan2T3AMiFsaII0O5gOWjZ6f9hGgPPp2VK6IcCAcjvTMKUyDua5f2Le9P6FrIEiPqwJ3alSaeNnmP6KlnyjaegvbAHnQsENvKaXCq5YsKt6BA6GWWRbNwJSj0wDC47LWr/hliAf4xoL5L8dmXhnZNK2r7sl4///g9Loiw1AJ2j9j00kOSayZU2/uFDS5bR3/O4r/zkc417OqrM4BAK8XW0eG2ZJuMo+attvXc8MsiHae+6GwzeaoDfREpnx4O/uj7/vv/nZv/0VXnqx89E/xcP8H7QBE4PwoVPl5QDSkRgVTyEUNKohAIKG1EWYvIkgcXRcPADzR5AmvCrgxviSYVpDbQyyhOTxgmPNQDuQB0xfYC3ujZKPVB5eA9cDvorgnRUMB0V6uK4gfQc5EsjYp9louIizHG7Wamo3Cd/d5th1285IWe9PXfjLVcvPLg/mN4c1Jn0413dNueLNb+BK3B+pk7QEqef5TJdS1cM/lFs3XdGevrrmpvq/cw+6o2XfPeqM3qm2uV4AqPlXDSco3Y4opg1ItEB9ttCEgSMM9xSaFlUrSdILMLNJ3pH34/wtI2URgweM0RkkxAATkPzBlqKTvzEPz/0nbNIAcek4nMyn4k/jchp+HiiOdo2/kavST4KvibZmTydpA6O0B/ekUqNw+lXHE0oEWre6NuDmfiJzNsAisL3tIpG1/9v7zsA5aiq/mdndmZne329prz0QkILvYQmTQQRFBGw42f5q6CiqIioWPD7BEFFUAGlWOgg0oIEMAkkIb2Xl5e8um/7zk6f/+/M7rwkL3kkL4KEuEN4szNz586955577rmnRu898uJfTUhHpcP/34PHnQVqogTg7lpGZPuru/2B0Tf0w9mi77Mv3K2PXR18eTDfGnf51UbGDOUC7GJrYrNHb/jzHRqnMNOKXf7UuZ+a/a3fiR+Y9nkoFIxNwu2X8ANeAc5DiH8EI/8SUArsoqoHQXmoxeCo0WbwW6TdBNtKQKF/4MsAQnCoQEzqNeI7EEwt2BEZSAwPPg61gWYhNaoPVutZdZMaqnEn+4swKYZIXl1J3BsmV6MKlTHAmkVGQYr8mxnMbnD7VytWLI6gAjxzR2bHzTNcfSsi9Y8f97nXX1uy2M88zni5Aq8FPJkxqe2dXmhGTF7vDkTlnC5EAoInO7CZOf5HtY3uDBI28HC9CjBHX7bw97cpzYInFWUMTjZlL4KuQEkDG3leRXwSpBqnIAk0jlKJBaUbiVnZDfZ7vQCmAWAYLiIs9BuV0qjT2OMXpAbAavBpQurIOU+Ff3EVB+9VFCGUoo8SdKkczVL6Vb6kqoi/tZ9V/tr1oTytx/QJ/MQA4I+NZkTOKOYKnIcRG8c0FGXWmucGrwy6UsgJTiOLTW3e+68zLr7swigT+J/nLlx6mIkgwmwJCmiCw56HirjD4Jpc3iULzdOOnfe5mXA+8TJmrhTrySaS0tfqfIW0J7R+XO350+omq0v7g7O9S1Yf/9XJ7Z6tiah59Q3RjKtGkYMgDG6fWuSDFCLVQvaFkt1F4vxpucMqh5UPy4rdAKyLlkiwQOcQ3RPbARoZGzPNbCFTQPBpd0YppFOpnBxN7kghxzKXlrC6MSXJT5w2HDGm+hADQ9Fdm7G+YZuEwGC0biDRfWJM69jsa4lknchuRRblUuLcjZffqCf6e5nnxn/mInn9Eqs2u3D8+5c9MUFQ3Y3diMEJ7gGKf55FIDKfaEjbtInXrQ+k80vagwl3OoLIZEkpOnvW3N4nbrnyA9ds9XsTAVnG3PUVgOmISYSIbH4b9jRQiFdk8D5nu7InuPd9p0zTaORp4cOJ+HT7h/2uPYo2bNtSn5wwhss1lAARPCeUockKIuRgGEEbULFfpvrsS9SK4lSG1s+dFVNJUDeqn75n70kJfQFU2sEKoUX/+2rkUuSJwHJkbyZKDebWlgc+/7OffObM2WP/fNxHf3VkAOlFEaKeSGQFm+mHc8AHGBk4sdg1hwenfPfruun3S0VeFwKlRjBPIYQY5/0e1xUXGUFWKnqP0wXZfcL9NYGkq9TMdEWmqvm4lvQijjVTzEGEqxf9wCJw7IwXf6nVCGlOnBs4fHhZyjA7gQ0m7RfyIFaQcCJUOfQ4wJJUKZseHExLOfh8ERUEs+cLgNANzprr2THgS8zqaOhbmmyZjJSGsMzhETEMS62KMI4KsmnKiOFC0iAICUOamq9/8YVeARYr85nWOo4Jb7w/Hxysad1Rc+eaL/jPl10zel5eO/24R5JjQxt8gWAkAyM0lxl0pzKcVTQDRk1A4o+TVkYzGSsca0lQ5OCCqblDE07s+OlduYubevSIzveRiRhWA/Av0LbyIIroNPQ5LMJ0YEsEpDvQgzANsCMcKx82phFGEDgx7Ai8gtHX1NKv1r5vc/emiWCoUJ5eAdwgTgIzTEUJaYgsOhSL6qNahg6b4y+PEsrZJIz+0AKEFRP4UqFNWFmw8AT9P3jc9/7DTQgHwBwB9VwZE8n32p9acv0PfzH24o89cskvfzRWT7b6R/TeJ48TAEh18Zfed9sC04jBry/u74Wuyxrb0tCQDUB5XucK9JdeCJ/Il8wBl9wW7zOXyz2tY4JGf85sBlZ29YTyW+MBIFAx0+sORtgcwsBiiwjGi2vF8KWLMB32+8x8Afk8AAUCgR/54QWfly+hr25ocOK8L9wW9vMhMRyGAA7mw6wXuwfVA9pNcw0bz5qZACb2mBQGD/weOkvMEI/NBA+NGBYBDC/SpblrteUZCzJo3zrv+yY89vK21nxtW9N291EfenrhRf4xWeu3Yca8i3Vv354I5hPIMwSxGa+XvBoTtLSY5OovCUZ6yyy2T+TdmxQj4Su4AwziCRncpy++4dfP9R0RayiE/EW4aIO1RMQGbDkRogCjDT6ZR3hOIhj28A6N6ah+gPMkHh8O03CehS6dTcM+hyIyQqOsULDPYt1AlNUkYfXNodqg9urZRjzJWVBO0pINMy1NQ+YcCJg0GJZBAEJSD0JR8DF0JtyrHJRADOuMrVbCF0lVQGNDfA2hGVZNvI0bBZP3Iyvei09bpavTTBh8GtXkQgZtLQwxwLQnH75j3oq7T/3mtWeOZVulZCvpIOi58x3nB7QItByx3fU3vrFw+SeLhT7GWhVA7uBS5p/esT1p8NsUrs3d3p//P0MPNm4vCvXuvKehb7tZ703pkaJm+aLIce/pRiBdXyQ4hShWKA6eyR0OGf39G0pCYzvs3fh0zt/R1BAL8T4fIhi6EvDnwWYZEVstxDS3WQy7abSMllEIaAM8Im6UJiP9JcE7LPpI1ghVK1mpwdsQujmEhiAoGcguyHqTCPucRxQJdnx/oPSoYB23/MwZt8g1ZrpwxWde/turncjUq/LeZlE6wrNRWFSSM7zW1573Q8khICLX1v42P1/0FsZuN2Lp/lgg+6Y7BoUwB7rOptno9afdaDz8mRsRMEn2BLdYvmAsV9vNxFr6Ve9mFhG3ZnSr/YjOwRUDtE8k+kDtHtUB2TshBII9GCXYYnkYH4YHtlmFpA7+NNefZBfM7c1aMaNTObrOjLx485fT6TbbKaNM9ezlDu/TomkjfHmk7b90XV7bqEmgY/a3CAFtAggEwW36Y7cdNUEWADccV68/OHhrD3Pk1GhOgjM5/qMilWPgvDkP37XivnnKt2dOL3GtXXBHtA8qtOuBKYQh1Y1APvbVPz1f983zW6/vYHKhXMnr31EKF4wtiH1BnPeMrGQiD1jEV3RHWckdlgaMhJA3AhheREGEeowEXWRgQnQcxAUMMtY3rRRC1g7S/vK6zJGVA1I0YhtMNZIAEPybnSCBaNToDsADDUenbejYWAmpB/auCGbU/yTXtGNaYYcZKghf8KVeWrW90WOovgJ35MlC//IVeuvYhGh1enWJu3ZRNtaeEtVAQYS4DLoOBQyhH3kGYQVm+DvCmUxLckM+1AKgxxsjPJM4dv5n//jbZXpAKyJACHncGGKokO5TAghiySFGAIy7BDbqBuof6IEgWWWqyMI7XOrfkstmu7b2JJNIR15ieB+E4aFnZgTa/T8pTe3oy5oLmM+LUT7tQ2x2YqAADlK8YpTL3moEpgodo19l1LMRDrCzAUh/6IM2+lCj0XTCWUI+TGDsy5VsDcf834vq4VeGB4N22juUolfoJctV66q57LyVf36M6f/KE6GsVYMK7WM4ptFNQrbwoHZ2i/Xr329k+n7rqrNUhFWvU3wRbSyEYfbhJ88ZJIn0QRsBXZMnZvnYALIngMmClg9rBwn9Zezw0T4sJbQhBObxcA6DIRccP7gAcFEzbUEInpMChXoH/Qrg4rSu/Kk9/qLRw0tU+kEnmqWkciPA6Cz2wQNLmF7hvPY3pQ9Jf75lfI8FT7u+QnF8ADZkTN0Jx5J3jsnGLNnrjbp6a5q2G7mWYp2eM5CeRlTkAgZMZZS4P6+k1doiG/WI1mDeXdClgFeMsnPGvfF4hBWzHuSegSkq0hUjj6RCXSAqi8jX8Pah9uzRif28YdM06lQGwcELb/xlpcyJNVMmNniFQoGN1kU9nto8TK6/WDrniPgmcWwyMNgqI48QpRUoI0tFnkWYVqY++DAgRA0iVKOFmQ60lVpp4x/xb/TTLo/xotKwvLZvImeUKFu33mL+8LC5bCmuK7b8plIHVVxys6H4mLnfe+FnKz/+K0GKOPZh9ld2+YOV1t7YZiKm5+iv3flMaOKzx/4xwScYqYR4IDDuQzAiBN6FVgviO+AVFlO4k8JqIohKCG0Ir1AFroBsLjuzGjphYSeACYEQGtDmUNIJJLXCMi3iOc0XUDJ0higR7Uzxg/4f6djtebmLKGq/YUPOfg8yOHxWVUMhV2EVNCTm2M7+4MYNgcI2TyDkC0UGEY4H1qODQljEzhr28j6Ykhqyu02QeTHoE0vwbUOUB082HBTTOQ00Pq+3xAv+rYiuLAnxlpXytm21bSGX/9QJawovdU+XODVlIa2o21C8Xl8u7wOQVEVGKAVThfnYgR+QR9EII4ZgMc2Lx80QYgAs7HmLMZtjZRgpGZbV6/7Y1ugdZOrHLV69tb5kRs1UzF4A8SrmkU2QqBYCE9VHULN/ObfsAcMu1V5jiTIA62yIolx5TcV4QqlDVLLIG7f9tHTrF1IsE2fyCKCC6ugPvYBKfUoJNre+sZGPTn3u99ew22rtvlcqGw4HfMmLmM6+OeN/Zgjfu/War16c8/oxuQzOV66VZRGHGKwlWHQgHwUbUcGg0g2iSIQvxHuSnMLGePoM/sdd00etQkeJooG/QtARaijUxUSl8SZeQ8VU+i0OPCdY7XoARHTQcoHTThKCnVJhuVuP12stp/19cYnxppm+wYzfnfUd5sP63YBmqAw8qYnTE3W/Omn9hnom2LctyWpwylAyBVj1wVdbFFXklYPNt4eLxHlhsB8bXER7tRJsGzP2mwu2dCLwbob114KFkhBRBFJkzB0IawwBUpE4qaQO9KDMm4AIsuMwfqzGAVEijTBYkBR2TnDitjyeQuJPdzWeKXKqJzp5ztInD29Mp8KeEqYs9hLANGyeUAFp5nEA/BVEs89lcFUekSzDBh1NWBoC+z7eoAu8Rf9cLrWk/vn7xe9/QS9BMe31iA7tLFeCgEdCmDHSljeW/OeXbmSvCVKV9qAMgwD0jVjmdCOoKV5GE32y/wfnnvXhtV9BSq4CUkNiXSThAVkEYu9b5q3AnYM4Qb3njC+NG9pMBzRI6C24L4RsoM0g0BEneKV4sXuHyAeGPTR96DcVt/eUdC5zFXRrjwPdpvLD79u4V35Gj7CGo3/kChFtnHnaMwMDyyT2i+v/ZXXXJ/ytxZOyXVaTBn/XNDAeoaZVeEkiYGipw92zoPNSF7d1WzbsFRm2mDZzatAXyDCZYmZNj1SKFbSWSUdMdPng0ipYa3qPlgK9obnj1sfRZ+TVTGQVONwgD14ITYBWl3dLWYOvQZ5Be8YNb/P+XJPWAwhCIizItaHWCxuyVIBZWa0C/RtYFLEQmX9TRKxJ5wvaZteEZY/NnuvL+HwpagKxEEBSqIIxHgQymud02mXoiWbhLugeLZ4oTJuHyng4ZW3iQNOY1qXAhs9GPnp9ZzDhUSSF3Dzt4SCdBCGVFdKgz4szzI6m/Kf/fp18PaVF3ct4UdU2+spMAv0Ky9pgzUl9H7tl6XVzNCmObR5pvoFsLMxR4XMOjpvskqAvIvsG6gG9jzuADuSymHJ+2iVSCwmLoAOzZwsC4RfLe3WoPrF04jmhMBUkunggh41o9pfKv6glwHE3FKLc+4To4+IPjz/viMnnIaeS6Kvf0rZ8oxJIeX3ukO6lvb0FuQpGxjrSO29zfEZCkrzY2cOTr4BGaYpoJfufk12BRLumj+3sWfvM1ONPC+jI+ZWSB8Z4Oa14zv+uJntbZP9JQogMZZjJl2B8isRI0EYZnB/iwAPpk/1OGdMIGXiTQmQXeeTuxBMN0xXJtwtKLN71l4366YgeFRUGM3XC1hvWfq1BBcLQdLNHmcaf0Mg+oEsmSNODoXuVJzR8JJotYxqgZx+og15BYRwQqru3nBqb+9N0s5o0I1E4YlHFuw5aOgqbHUPKtKajZ931jRsOOwO14Hnl85VKcQKfhpoRP6sgwsWFyXqsvBl6+js3uW8Yj3BXoF2kMcKbXkqBjaR6XtJKQnhHTrVYMmF1DjQksg1pDhRRZD1MPCdiYoFPgwYK3YAEjEUgHyRKhlDHXj3xwza5o0/T3LJBsbNNw37t2q2hR5hOAIndIxvVgPaIPAJaGshtPuzMI47/fXrpK2fMnKkLhVRdtE995mW9o04pCW7sfSmDDbLda2aAGyu/6j11qrSqLxRNF4PeEjwHsdtJdy7cxNafPSscBAsSiCy5f8HarosRmDdR52EL6Zig3/Dk+jDMBaGBSpKWwBfskylGHswFtVAaXCkQfqiho/0Bdhg9o/UAvlmwIYcUDQJveJ76Rcp6FQzmct4/+MZjW9MXttxNmTF96//xidaiKwZdAWgZ/NWIUwOvU5Z6YSXGNp+8irAm2ZAmGTNtx6AFoLa5oPfOeNQIKD4uAFKDUb1MFml70lFk18T69Z3kxXdG8slEPSwYkJ6BVjAaMToTPoWBojwTDhvhLH/Z5BvPu/NKvugpBABmoxh0gbeoHPgkDthfBiFo4PW4uyBqsvDZ1L3TbnBlIoaZictIOGpQGiU7XQsi25AckcQaNHExuM6YoxtQ3pKcA+TMvkkAxwJq61fIVgcEzBaDoADNi/IEtNGMCCdhDkXwpHfg4IZr0okSWaRkMYTR0DbosNaz5wsyoEJwij2AvdWCjxymBCE7ADuu6A5cedZTf1n27LMoDI0875bzzMcREQkRzXwwNUICVHA/Sj2jT5m2Rj4v41M7pyLkG5hMPxo7ZcXGV8KfnynAi7GAtKhK/9gvLfnHQwsuP47L9CUisQzHFdj6TuEStTCG9/IC8EGiYIGqp9SHKJ1IIFyoV8qBEgkUoz4q2qid7xFjAfoCyJTpiZh9WJ7QzudEzo/QBPGO4potcCA0U4hySDO2jONEeOjAHaJk9ty3hSBEKml0gMVlGzoXX2L9CDCvk2ocw4CECgVN8Ih6tuhKjjeZf9712Ow7IqVCw77ItNsKF4uzv5764qLfFn0RKRsmQgCzzUpLyu3Z5S+srbBfbvzYopte+eoRGitHBzwhKxNF1mkM5S7lnJ9l5Qmt9pgklZso6nR46DuER/ScntE/+kMo47yjkBwIOAXkBTE3TAUac9uHGlGraJ4RlQC3D6sJOuCTBOUvFnfgNv6Dti+P5N8UgQqxQEFcLLb1E5f2vLlgqxJcnUI4x6lnfjQcgl8KBODICg1pCzY29ZInWyyqc2LBRc8v2DE3Y/ECMvoOznjzXvHUExoi1DLTLGWDCKs5K7jq6U1jvEqNe96sJi1VV+wvzogHhD6kj1KQOhr1RbfBBBqBhrCQ6NB7Qg1vt/MA/tCA01EGEw29DSLabQFpcFdce6+noxWJH1ysKnH+xPtcW5I1fACiJAI2TXICLb1uv4hho/WRBg8AxmqCxtE+3d7CEegE+FJaeT+Mp/EeXlODCQR4VwtBlh/PsB/8W+CEbyWKbEN+iDjZzdvzTzGA5JPM1P/72V1L/1Yo1SJBI7h81be3+F52IyHBwL7Wf9R9L/7fOS0f/sh0tqbY6WpikgF0GCNdJoF7foY6R2sZNdX+h4I0B2m8Kgd+0XM8pRI4dkU0SP6JDNKqCgyC9wTtpBDhBrS6LNRFhly8gcQx9qvgF7EscCzsVFAhlPdMCI9JqiLYRkoMl+OiiQnnITFiygzpmUAMbxSx7zCCEnTAoiiWXCVBaryjx7hk/Cp4gNbnB2HtluFU/8rHC8fPPT43QHJC1IylX+PraiefwGLXxfHSa2fGkAxEis9m8xoTNlIiDIy5XDoGao69KdL6lpiGEPbcB45pu8DNBh9ajT4ScIEuAKn46rKxzSJ0RhYSUsh64eleY/XkXIEJluFKgLeBS0gKZAOiEbISZcOmDv8h9h6qA2tJAKMwfBzmNMKNl1+CSEUvgh0Sov1RVXnw+sK5Z086IhlNCcF90TQe7qBMTp39+w9d1f4/xz1+l4jUzyCbeyNPNiIwnOzxFTKh8e3nG0/97PdjPvFRv8fdrzeW8HUM9R6YVuYGCIMwUYjpoo5SSequjWtl1KBb1F/7AWGcjWg2UOwCiKFEQm4KWYNVGbEwCW0ABqwZWKBpJtpiERsgVDUpoCAPh9QOB208oaiCpoqc0IgkukKw9vEgsSdTy+hiyEi7eLiek/IZUV5h2qMFVMlrbbgtd2nb0hfWuyOxAAKj+gCq5PPahy9wFWViMd0wmCETA3PA7ZndjwR9sqT532DnSuHAFjhaCnkxryPdYAGhEsEi8bKuS7KkMxEKdWl36kD+2IsE4GMDi8BEkML/BAmbvklPai0hRKP3whErzBZyO3zi+lhRBYDKMC2/SINJiyH9BVEDBMHtEF2DFTvEU3QX5aleeIa53FD5YuLbr0Ij7RfVUiHAc9+91XXcbe2l/jYtnkrsqzNwc3aDMctzZ73+xYf+mr/0EUStBmmnduztYCk+Kx8sap6MGL/o8oWndY47qRiuVYv0ip1neW9v4R64TpA8MguyEY3+4sBpj6M8CBD62LBwhgSideAV+k2RNwhRybCWIjJT78GjQVYFL0NdhZTCPpD5CRpTkGjbjcOFPPZoH7ycbEYV62oRlrrAPZgewFqyKEZBnuzFllVNSlgH0+B4V8vvu2ddWNy0hs/JA7A00LMKy75auBxCg1QBdqaQjllGmoXPOkamqz7TGw8VxeAGpbljfYGdkVNdzYfH5EIILlSUnZeB1ruUNQoME0Wgfgr7e2CHvXoCA5wKbPTCdGNBdAAd1rVkUbiGR4hUHwinz0IkRkt69GP1SM1lvwR8wZv2ywRHHIRkhGpEGSn8MDgOm1OjFZQmt5RgkVABEnkMCL2heGI8BX3x9X/4RebO9/NqtsZKNiQGYnZtI//RA/ARhZiLF8f+7pafMWsWHQvTMJo3eznoO5rXC0GY6MqxUYYNHPuNm79w6SfCRaRVQOtBs/Z4i6oinMKmBHhFLDkRbQffdgWYjVr2rLQ7ZC8HsECo1AhxMCq3zDzWTMTTw08VsWIo3jRBDY4QIPAwY0Joe3ob6If5CQKGvFfALSyI0NKDkZMNOB7CpYNFZDYmb0BEVlDdQSO3zdigSmVClc4Zbk847FLdSf8fcpOwB+5SQq0eMQj21V8s9XYcw/YiHq6bZAyIFu2J8Hndpw1kESrOrbuas4HAsgkP9Z44fWlX8ogLY3lLCQdgvyG5GB8SNpkMVvngWwgHK719ixMlEis/pnkKYNqJvfC7PCct5kkVQWYwFMhTAyrFx4NJpnNZPfZSAJMNGrsCQIgGwj7RqkCrDQEYzi5YMbAK0PYNO1WY6uhaSNcKbITs43EoHmVQCDH9P/9daPaNx4ZkdjChh3sasDi/9VEMI+ZQRmo0u+P1Xzzs2p47m/whU0A6gj0PG6WJCEvI45dqZHrZQumbh993070XXT4tFcDIgsiMuCxgtcGOFD4oNnQIN/ABdG3nQVAAwCozDbMMkw0ilEoBUAWSp1hBEgoBgnbAXrDalE8MvJkG4g7higAJNaEo2gKWDvawWVO2oAKW2R0ggIqsIiMcUqcjdRkiqPUU1VQ/Mkym+EixT4jDyBvBr06CGVtxUHJtCQ22rmGmnJtNvrDVP/uYlCIWvVnPhi72CHZ7AKL+EGJhI2qSHvNLfUmXme9fFWhL50rZSf78loWveN63yZ1gii5YZ8q1ATMH200zQKATEBAFYoYKruzs/P7/sqcuahqCHbGswDkTzjyURZiZF20IK253AAQEwcr40DmPysqz4xIVBrZC08pv23MeCEVMD8EbfBkMmqEDAaDLbChEIM3PPhGdeqKXUhbjLdhyMi5fyLj/+9tiV105TpFc1pRcUK0tBPbFp4WLrFD0R4owApZbPnjKuQ9+pD0MLwtM+b0fIBqQdTNae4ENelm3eOHpj/zp5z/9xNXTMDFIXjMcRR0+DU5B9JgmDk0kktzSB4i8OQc6QvhXhqGNLygPYbB9eLRCUUXAoPVyPp8vSGohVBvnM10DC/MIFy2W8oLAGZoQCSMuIeYzWC1YUQSt/p5epGLWsOzZShxeEAYRKcPK64Li9web2sYqbNzqd7XVucY3kvMOArdYyAWYUtwRYfvrVw+cNfbJLfPhG3tc21Ykmi5EtWRhVnBbTYkBAiuGF1acKw0Zmu148+RUYceWiHd5yAoVb39tzIVvRj3xmtp0AcmgLbngrumG8YQCv1e45cGUH8Byej3aM5ZqdA+zTZA4eMZbYMAQshrpSnLRUkDSwxs6tfPerLeY+kEdshXsjifMWbX9oexN6QQE+EFk1CT4ENxppAjWtNdCfdC+wQ6RPKmKsuBBcDGvUlR4ryEN+EoPMuMOm3BBOMSLPpeKzVJo4//eU4ze/FFzEKFrwcUhAKQX3D2ZRcArkdIWq1hkqJE4SLZPcikDhvleNp9qS8C+NVh78zmf2f7oyci1k25QRDDA5YxgaMoQBmFm4hA1Dzz3TETo1y+4ctUdf73rkg+cya7esLWnXoRhDHiyPEyKlKK5OSv7EIYkMV6sicyMpL1yoSjnoBMVG7AEil7OgB0ZkrThA3IJESwQrBG0SZNlTRJr2tujlAbKpReT/dkXWHWwL+/yILe1zkIGBpc3+PoK7MS6gFkoBWI5uCeoUidyziNxDL7foKWTrpp47ekIXCj6g17wyN5INMAPACBYWk2f1490qbCytLtDEhI64HKI9TQcKYnZmto7k0ef9VLpn4IobVt4fNSDwN/Z7Xpz0AiXVF/UKrlEs6c/mykJUbCtxY2JWFu/HujaIJz82D+YM8Z09Znx19WcS673u/nWnl7GrF9t1ISFwpG9WlHUQYAw3pSBgoyjQFYcLmEIzpV27XFyGkyUiEaW9cp5t1fQVC9nFRBI6c3u9mBTwJXPQk5MlkKuzSHrAnPNNqnYEMln/H4xi/doubAHkmY9pjdNeCw5NnKAAQaHh3B8ZKiKKH9qx9cu71u36KWfe3zB2oaE1/ANLFzpPuP9c5sRGd7IU/4lWKYCkjY+wc6FtYisMApNeKqyQj1cOudWJVe9qudD/t/f06tvbc9ecdRXOW+aFeHoKaLYXg9ABLW4GDnOFMb85PJX/3p1OqCqHaf0d28c4KOC1pTzK4Fgc0iqCShyfsvjWXijgdfuq6co3C7ZZEuekJLk6wYgbyXb1KCCWN3AuTYNKXMRpS+V6ssz6Cmtnghh6P1wIACRoiDEYUEi+rEUSHmVD0V85DkMlgLAQ0EIXbHdRNPAECERsisA2kasLkTJZHQC9ooJYuaRST0sUUjWTpPZPuh1OqyiHmBhV1ez49ol/mtWGX/e0XHHz5cvTNQrOZHNZhU+GC9C4ZY28BubNEQmhQ25aKYPT2U5syC2bLGWP6wxr8z6SVvPyYOMqjVK/R4ti2xdnI83fIiJwwfqvYwoaUinilZCM6LbsQfKX9+Pv+DTCC+Qwx6kER59XAZZQ3WYmuRhaur2Mlut5qKJmIBKQPS4kA6AqU/tmFrTva6hzSiwflaGIRd5PRLPaK8uQHVMPfzGVoPuUahkRGRG5h/gIogfr2sJOLNe0LV4+/rODSCBSP/BHH31+RGpoPiCSFSNuHyIoORjC3CWsHkf7LAYcCg2P040zf4MRtGUDW8EkYA046EbV4dy0WOeucz96qJ7kEnZLLohyhyh8za64hn8XQDEOTPOe/Glbf29vc+df8HJjUwR0kmVy7qZEEogRZjgMrKqa1uumevp4L0Q8RnyMkmMFDrVeB1rStmiwY6Hg6I3FBTDCOhpm0SqvckC9FN+wcNxYsgvY+bAuNcyILegAOWmGED0Jpi3kR8roG+3E3IHW56BvUlB5HyQNMEsgPxXEMwN8bToJ4CBnSesx3EBpBsiIQ6mqW7s13iv4r3rudR3IxtfL4aumfTF97u6g+EQZJ+CNUPMmz6zkE0hbhcUOthpwEhKZOTlUa7kDhm5TS8zzJWnLnv6fRceN3cH15hPpMVIsGHDVoMNCZLfUto8fQtvmdXRQdQcKfd00eNFFkoeO6r9PBy7BcS0zpp8SM/loXItmR4hgfxnWW77635fJxLOC0Ie2dJV7H3MhrHPC8ICsZ1k3SQBqGRbsdk7YmRoshFG0JYTYDCAaUQvyV8WSMgF+yS35Z/ecWF/T8+GpWuSBQkoXtq2ZnKcYZAJEBgvAphGATkB7ekO7IQtAqgE6sRBNM1miAwOyWUA/e2Lbn8xcXGof0rbwOtm798av97UlQgxeeSH3/vhjEwM+VsQp9o35rOf1dLrnp13+y8mXHlFI5ODMTQ81pV8oMQjdFqRicreunSUGS8TTGWOOZZkV0dqiGcLdp4k8xh4ONcB9WmBBz/DtbYSjUK6bEgjLCaMRzCcpB0WmZZoPngR4NAg3iBVG8kZ7c0T3cThsm1DIByjVZEgAPE+EBI0nSBb7j4gvHMX6PQHAfsQVsIjb3gk+bkp23cszX7uxPhZn/vtq/2za3SrsSWNaF2s2pNV8uSZToFRc0zYz7DRIqcZ0eKC14o1sSsuyFz06d899OjXDytK3h5PPIrsEL4CxGiZgMV0evLbfuw55vz3RYLoF8gZPoxpUGm80wq7D3v/4yqiPzR8Kjx1c4JXVYoDSRUyFyFfyA56kw93t4w7Vg/loDnTEfM04F/vU5duyUrHzTr++KgBhUUR9IwUTwQY+5894eAXAHgQsqiskjcEVsWOB/pz2ORlAqFcEVncmUDMnR4oLe3tWbsWL/Mz586Z3OotiSYSC/CiG5oEbPoM3a3BGAZab/Idx5BAzkcCfTAsbiRtSj3z2LJ+bc5ZUVe9pBcWrarvXHb991e3wOEcdmJEQnfh0yr9p4lQhgsZKzCFgKwQGhvp3947mAiHop8KNXqaUSYfZIwijGqk/kZWEUte1avwmoLYt7SoAc0kEgIBcmTPBukKttaoF3cg66SDoW0QsSMQnUFBD74VU480EXAuJniV9bh4x54+eBVLEnUPXAxmL0VmtdEKUhb0Gq/RLCVCZiGYGQ7crBzOD75fCGsWJ333l4d9q2fHkxvO+HbMivZ8/VmlpTZc2/7Phcd8vmawZyOkJEXShyFoENylACbsR9Rs5/JNrDn9G6dtuX379CusX9w3+/O+HxZ1q/aNOcsnr5WueXTtyfoi0Sh+8bR1f11RmnPeiR0IeAuHGFaE7zL4ARvOZZg6rdrLmTANt7Gn8nCpzq7lS96A/RYEfYYbAiAmwg/UJpsu0QO5MNTQiHjsckfT2Tqz6/WBZOj0TxxrlsRABdMQEa88gjZEIK0HYAASpCc0ZI0ATJIOgEJRPD7wLRxXhE8IFoT6Pt2X7+zJr3tznc40T0xMnzB7DIstgB1JDsPk1bHsFkwEIaFxhO0rYRp4GB67o0duXR1LdVx0VEkTpVzrK38++ayXfzumaf71H29NQ1dfhgCN0q4HKJPdUtjHQxVKGmyKdgD5VjHf+fTfNotZX9x3yuzpwYVdxZYZY+J+o0CLjAE1kuqWcwl0EzQpj9Rj9owyDVAyCPUgpSdSa38M7CzhGpg422iPhD62hS8kKTxpo6BXR+NkyNbsKnZt3C6/gYEEPkCNcAzCWSqMfmPtwAmS30pZZ/UqYmfq9+TmXZB4MJm/f17ND46b0tXC5B++fVMq0ti2umfSVyIbBoy45MciQobWSKMqQYKejxnKwlcKTOKcW0PYU2S/t2jSj4xPLj9xZezU0rp5U7Z88O/Jr//jzRPzG1p70l+/LpRjH3n17z2Hf/jsdhMd1xXbKAFDvU88A80vS8EYK+/u+s1tpRkzL2xt5E2wrrAccQUDrHjDMyKnuYPxAXCi5MggBQrZyKWH3Rre9Mqco+JePkvwACZBL1de1irfteEETg3rqF8D02zBWxYSSVL35jhVtjOlQHwAO2S5FJw4Kdzd73ZvfHPz8oeEcTMOn4bdm+DBEsEhlDbJFbA9gacRQdjpk8Q98Z11wQmpyz7he5NzDdRp/QPbF046Mf2XHPOj1T+cmAnsMmq7/7QRjSK4wCkNKQUDLgU2I0V/UGw45itrNm4prXi284/uAuWZNpgTzvvgGAQzAL9u+NMBrGMqouS6uSCWHyATjCbdBS8HMSH2J4Q+sGcNQnCDvbEhAV5w/QEmwswPkxZMHnZr2OtA1YToSgYyMdkDRNINtMiGVBl0IHgoSepJuI3iTRu3AFsb00jgUpF1lnvlwAPhn6FneP6XzCf9K5Zs9V51eg3TsjK5aUlzTyq/MT3oltYKkgj1lYlwvQiaa3jRF/oeu3zDMm1CrvcjK1sDcET9pXjDB899cu0fXuy5rCa4WdPmvpn0RsHMCCetWLA5DUnfGR/TH7n7x7/r+GhtcxzGcCPyw7vDHFewg8dAYv0vRt07esK1yGaKEDiIQo84UxoUi97P3dv6/kKtHpdhDAs7O6G7ODG2vElKPg0PgzmndXBBUtGBX8P2iGqyBR6YijClAR5hkUNUbUErgiaVFAaeE9ji55SAmmbqoGzQkFmACcMuVi/w3rQboIgYxflPrITBusSHwwi86fdfCl90f00IFsE+EZll1SCtPZjbKx64v9s3ZvrEhu25RCML8Z/R/ugdU29PL/kpl2WO/vz7QXns9gynaRi68uDokEq6NCVY0jwQ7rHZuEuCvK2/lln/fMBrJesj2IV3vbaw4Vph0dYkNp0xacaU+tlGLrMpOf5YfyqITNbpnMrEIkBAD9wLaFUD7smkXYImWoF1Dn6WI8qCQCng7LEN18m8FwAHzcYZKGiH+MJAEBNGOEe0kgQ5EJrgr71g0FKJOUq4jRcpshGK4559OGeumImL/7rxmauvvHf5yvT/3OR79b5tW+VuP/YW/kHEH5RPbJ0Y6hVAkbGZLxmIn4PZAwqw/u9g2GaI8vy28z8zFYZuUmLw5/POuPK237anz2vpuu9vd7x00ytPHdElf3TVC8fc2SKogqZ5XfJzLzziHTf3rElGOkbzxSY2lQaNdHIVnBGxu0LWPdQj0gZLQbfkyl8+7/SjVb5xWyqCMF5gk6TarQE915H1rnljC1M/sfXsSHsCQkWXJLhhGwNY5sDOGGrMCEKJQRtFePwD8C6tiDUZq4glQ54jWWKG4aK+XAGeMRqnlXyJHJQ0XmjJMTayJGULG+NSsZjesSyHsATQNGgtQlMi15eFCvmkC+s3LNr0r/zYlu05qejyF7yloz7Qzi+aGPvc4NUnDTz0enF8r37fhSkXki4iny0jI9mVAlM0RtGDLrBVxPJgclQgYvN8RI9VAUK7QoCR4PKhhrPhnppsXOPnXzIQ65jmO8odD+U29vRs6Q10tCXGNXUvXmNsLHgDjf7w3OPDmhu8GrTgZLsDFADKADMqcHW+49Ae576N8TQzK+1wMMYWjdLSSZoLFVHas1GkOcAeFLbB2JIi9ymcbE3e0xdzKR5e0zKNYFAwUN7+ENd14/1nX8U+8nvP1z+18q93C4kzrrxK2GD6oyDD7n5x0pwmBaqJNZY8mdejYIUEn1spDSzLD/QxTRc+3+fi7rho/YQU+InULa8KR37418/EzfRD9zz+o7ZzZ7bM/+Sap2ffNR3hnez5wFiltff/mb3so1MwJYA02L/AAB3bQxpyu/9Ov4YQbwjTQL9pIhHhpjOoue5RLH967sYzpmpmXR5oi9kkuAdDgz5GiqZdXk9h8+ou7BwbGhKtbf6ZkFiIXD7jW9rnGT/OE2rAC4C+G7ZzsHXiRZeSh/5XTyo+FiJzJQdGCimGVQHSD1EvULp6E0ICxOIh01dLVWr9ECIXlZ5nuDBMDaTMcy4EXa1t8m9btxKy3JZGD/f5c82ehc+MkfuZpQuFL89i+fQTjxxz8+Zf9Hd/5JhruP/36Zb+BPLeGKY3E8G4mRnE1oCUAphGPcVSVz4AHshTTM2zvZHNwHAO1sYGYv2YSURuK4qW5/7PTP1JeNuOzlelrblJx0+pmbBx6doeye09qr1+yhgo01LxMPyxbdML4IWeib9NmAZwYluBpUUJaKaQE0SojQ2keYaREA96WgyBWrphch0Z0OopPbq07f6nCus7ftX8yHXMpZ9+6UcClxOn3vLprdDOwwzDo/iLTLzV6E3D47DmjGmI7ZBKMT42l+kJcMvzDYXcV70Pd0pX/ADx9+NKf+Kle198furLtz5Tunb1U3d7PzF7/cBHVi6f8XBdQMZabo+szPqUX/+0ePx17fUICoyJitDy8MsHt1DGtOFrCVbPytwDHtkYRqSb9jbQ9LFpT6RzonB2W8D0yPBw9wvwtpUwfcGfljJCJIHZr/dt27I1CRrBCQZ8cChzkic34RSzcwBJmZPckcfXDKqNk+uZ0oB/IpdW2KTgKkLJDUfSYLHH02Rsh9AAMllbgAnbXETCxLehlDY1Tg7yilePIiiJDHdM7BQ5bAWLgb7FvW0ToYLJ+vDejv41blcqu/lB68pz/vlUuuszs5gHl2QjU98sipM605/62niwrXIgE8lI9eh6wfJArYsPkIEY5gF1nYWvJn7APNs7wEcGn/CkU3NOKHVPgM+QP5kATf7ezdOLGffksyZMbOfW/eGpVM1hx3TEW6K01Z///WXxQEEVw5CniSHOe9ipIYLzW9K08ldptSTKNzJNQ4xdjAeH7Fl8iRdAaGxjRxXJKLAyYHIgpgH8GwToo1mkhU4vvi6ZS0XVG2ct/cW2S2/9xQ9CeZefP3/wCZ8MZheW3WlYAakK40s0dTQzXLhP7Un4updu1PzmmFrfBe976vsDJ1674Jbcad88MrDe0wBHqId+2H6/NP/B5wuJT8y4TfekL3/itbN+FYxQfi+0GhBUSgG3fN9989s+/PGOghS0ZTfYrJM3JfVsaCpXZvSumAZGAIOANQ9Yi72dysMowPOvk9pO8jUhfoM7g9wPksp7YNHJBos8W1TFILbgXo135Qq51GpeHtQDbiWbsRUk4GrZhJJlwnnQSGR55eas9TT4jzglpMsw0dJEuPaJYUiVYMqM4GU+waTdO/RX2MlTgArsfXnZwxrIlmkgajlUBb4843HzRpbxp80ON7KiK27E/y30L9zcMVXa9MBT47788w2M+yfZ5t++icBqn/ni7f9XO+3FyLdmppKnjPFiF8YUsPB4KScjAIWVyZYuAGoUIY9YIIbJuMJPf2uVVuPK8t+9PFyKFksJIxuTXz7zp59C1B1I+zQxu3rrCY0gjdhGMxC1bbqfPTaaHizppWxhUNDdM05ulbEj2A3TCLN2PUBe7WNfmIYU9WgWyD7Mvhkrh3jbWE8hfbJD3lT2rrCLNVk/1vvNt9164pgd5//vrEu939kw5o7DXvp6rPFvjR97+V8uxa1H2q2Uzm1vG2N190rG55sMS4luy2X6tyAcwsTWDx/VUDBDpXVn8aH/mXRZ78m/nNqXkLvqI73Sl9Z94+NrX//LfCE24exn1t7w52f+dAE2TrZVFPqoenXkSGWkrod+l77oi4cxafi1YDbQjg1dpBhVw44hmgZeCs8hTARHSguMUOLEgig99ZmOY9R43Cc37kDEDsUdYqVIEgnBg0moiAxYY6YDTXFdlrI1ct4ICv1bf8P4zSmnCOlYpFVc/tJg4nCWKQ2mDWFwrejfYLZEzHzJYPzjxp3ijoYDiPmogkvjgrxWIuUCtmngo7GvszRfmOQZAQl5XQQsqXCwh3Dd8HDJmIw8lsVwTmRefnPMMc0YD8TUX/rNhXOfDsY6jz0zeVvsjMNnRI/EnoJ75lsrm7vZk29pC6tKsIA8BlnWq5CMC5iFyQSMAC3VZdgQqqV8KMRsu2/rpfX+Yu7X97b89ni/lvUJcEP8yt1b2gfgKm97OynwqTSSZoIy+kLiICPbK4VPdOn5KHgoH4Jlk26E5vQu/NhuIKfdJB37wjQF+dgRLhuZN0pYGyEAw9JfQjohuItBpww5BcQuiBQDUxiD6b362Sk/eOlv57wvWPjZ63U/Mtf0Rdc8GdECfcyxY541G+v6tgm1h41pMvp2dPac3ajy27vf6FM0z4yzz5gaRixGtNbNrPr2c9Y5112+buZDY1dPQrB+OI8tvFa79pTBDS890DtpM3vH408/Pimg1gBLqPUkdGZLOXeEy6SW3ftk3RnfrvNpEo+og9R9HPvENGj3yqsnLOu8Op97/uPhs+IC75fqBksehBgJFc1IVvPmveGiZHIenw+h/q0S0gzJ0KL7+ULGsz04JlHoFgIZKx40Xd4UhPkUdz1QCG6/e164jWmrB4swMNg/84rjBTGA7GGqZHoFbPztoE+WDzCEjZ8mRhVMEDFTC1W1ivhMqivA4cNCic1zTV5YtXh+/WWZiV71qXaZDZWSz3+cnzptMPk6w5z6rVMZPZVt60+zrq1/f6H1DUU/fs4V7QO8S+bqIWimEFHANOzhCNNYmeRjyx98/hjunFNdRS1S9CZrmUe/1/fTC71autZkf/ydo59GMIKuOLIn8SbrQxR7UBREVg7JsuBGBu4wLyMMH8XrgHrSXpp3wzQbrcqYZf+0x6l8jREeefUE7bI3meCcWeSlJT4ZWggzlYClW4hV+vuXLd1Rf/7swvqVG4sN9/ZNWJ+YcvbGAXHdqnDJLHlLTDAfKiTyM7t7NCYUisc9YY8gxiJqWlKWdvUwbWMnTzx6UphM1VgVVFEwZOXFX69peeKqNw5/jtkwltvmD3fVmY8+3/XVs6Vtr8zf8kooKy6bAKIFPo0QDUmPLAqVmYfBYkkZWLr08XHHnHik19qRQN/2gWnkgEM8EsGfxBRIjgBp28arX5lzHHTOOQ/yViLQjRdRAiQdqg9XQQtE3TISv/jdstuHKIIUQyMQSonBPBOGcRXn0kqIdp2GU19eFsODZpjvTkJ52NLsB/+aW3B99nOni+2T8piqJNoFJ0YW5FoIyngY1RtkXILYT7LKBDxw/pGCUALCdKovFnIpYsjKusIvrTxlyw3bPddfnhMizIaZ/u8dw0149vexw1YOJAebtyJlYG2N1XDUJYXXnrlXHnfsPVd/tw4hyfIwQoSZLfEHIOIkE8N+zG107Xjsl6UPXn66N+8ZaLK2tWWu++3zx2UTzNr2ptDvTuqtUbCzgQRWhgUJxbcADySLssfKBWBEaEHH7ub82FZbLBIADaNpNnqVMcv+CdpEJfZJ03ioi3mPVoKMh8vqcYU8mkVt26ZnV64LHtm4+lUtz4fCY9hV3dh8x3PeXFP/BX1r+5u3xxo2qOO61Eg+lGaYlsDlry/exnhFDRt018TjWnvXzkfS3XOOPD4+FqwpyQo4N9JIMX5ZTPo8R2+88Jozt1//fSYnxyXEVt9a6/v+nZO+eEKkdMOvvvxy13Lsl/KYVDam8QWSdrABFyVMYvJ64c8Ppg4/d26TjMe0VLwVn0Z0EXIewItkh6Ug1Oy83/zdp3xn15p1xRC4OOQhQtruPAU8RfhbmBnLigXCx+reEOvR3T6zaGBA/AoiuPtKcDfzeNUcW9T8IejVdS8nS0gkWIIrvBcVj/nOX5FaYfqki+KtDaKBpDEGg/WiBKEdYlDCu5RTC1E3BC1FqKvNIBRX0O/7Xc/+Xr304rAUj1pGZ7NnMLz1qf8Xv/tsrqRsurSbj9X1+3IKa4jxojblsNMOR9g7hABD2oDiH+5ZOm5T45UXjkVCR8j/QNPIbd3GNBKrYIvHGV0P3aJ+5sqZCMuZCwETxtf9bkIuuG1SpP8HVzUU4ZRmqMhNbvhzHsxkGKyXQjnGbyE6kQIzWNQkkcEFhGD2VmOX1dNGLxqcyo/9xTQU59j8ps1PbOq18qd/b2zB3PbyvBXdmCnQAHm1QqAAE+ix45T8fDfTxB3LPVyEijI+KCrejjPX/aN+9sBrvvrzPtv79EtL1DCXFWI7fBA7qUV5/Pj2aXODTTBsRWOxy+9uhbK/6O5LeHRh85nJn6s/Sb04PdWQDLs3xyN9Ma3n9kfP+HTb/Ct7vvNLKR0ree3tE6ZTIUjGGXI+KFJ6QmzChdIrt78089KPl0naXjANEeXs9ZIgTwJDh7/AOgxOBth300/aL/C6eMIfHi4rGpK9UL4N2ACBbClSQ7oE2HOeRMkUobWBIFZBTleBh7AA8jTePeBFvCzRryKCPxbIkoSoJKApmhzvXr92xSaYoHLe6Rec2h5GGBwEsNMgG2dkya0layUspFD1S3D2IIGlhlAkQuq5J9dvveKXBaNxewsCErtB3h6+/H33gAufd5FU528I9te8Vi/1dmzxnJr/8ZTODYW+hp6jxm4ddPUVFj6XVZizPnfGJVOujoiUO5HLLZ5UJxshCgUD4qazuc0vf3nW3xpNTyqsejI//N/V4y3h4hduvN7z6nhYSTBwZRMRZNeLeA4WFH5Q+RHth4JXBzn2qNjC6FII+EH2UYRxFcza18nBQDJIIOaR4E6cJBbKRY/+YxvDNDfGpCMu2PzYuk3IyomoVPr4D4h/X8IzbSccc8bar2yUxx495/CJUXbTvNf6kvoRZ2ycfIQ3e+sVj914wrWBOYWTV7qhtPYiCVaW9+gyM276hY1jG5DsArhCalTMAOgWYMfDetSetuxlr9Y/MCc0byqMCmNEduDT7NWW/nLRWTPf+Mmnn+3lSEgEOsjAiMPpn9N+rqT7zaUPPLPQtmrCojG849AR4CaQjWw77b0AzUybf6B1wGICT3976ye8nDjY5O6REzA9gewJJu08pKmki9BLeUUQeX/QT5gGPKX84VDp2VI84oMUVrRKnIcEwkgLpGi81wMTAJcRQArPXH/eGpt67fkepqn5cye3InV4mDYuCixtVDZV5DjVdMsU1B+tLvg5KxBVB7JfXjDuhRiDZFBMxicoas/02s38jqfuWHbi5JWbraR1PnfkbJ/R8Nyq4ssDLiYXzltRSOfZtrww23y10wooOT/f1BI02v61Xv3mV81UGMnIEKyaKRhyLXffx8//26ZxTFqNZx7/RD6Q7P94/2Xzuu85vJBTCl392Vzr1ZDCIDUAoxe9WDGJpkcYeE4WYQVUoOBiUikMKBIG7rF6DId85doZKZriNKNgWO1n0t25hxe/qopzTpxydI1/8ZOvLE4fI0id/UxD2xkzTo7887nAcU2empJ72e/1L4ZifkxR2QvKAu1aWCqGBWZb62va2JbekPuN8Kb/s/pXM9O7DXgfnPexWW7TB0BizwVewEYGxJqEASb8Sv1iTttyQvTOC6KL6hTRSxJk4mWlYjPzl7vXlC5/Lj4vC141Brkn5gPU0uXDwSiEx4RzVqnghzDSJl3D++vK00DCEgqyXYfE25jmQKAo//DXF0xOFH1YAIkZNkxkVnMHfLDB9UHaCuNt8K5sIOSHzBkmDnA2QCxEsmRDvSRKQDpSd4l0LzCSciGDMMxqLbdXREgoTxiaDZc5JpBaN//ZFf31J557couaidI4GSXJC+9FsyhBpUCoj1EQWY0NICr+jl888PlvDdRjd8JkdXdp+Tnv/xPz4xsCvqxSe8bZs8d31j71Ss/ieuR02yYpddFx4/Pt47n1K9jOrF9JSYMSMipCKpTJMUJj7pI7rF4RhsOa4DaTdUxK9J2xYGPYs62Bl9gf/GpM3mUGljAdm85j3lR8WqCJ3Rr+kCoMJq26aYePQxxa3oDKKRUrmF5VgMIfXpoItw0aTGrOPVeP4ZCvXDtwBvgx9DCIF9jixhf/+i+mfs5ZJ47xGMW7fjTonX1h05dLRf+cC46vh50yo5LEx8JOxkzHySwd7nnIK4QfxPcoGE6PLjEh4isR82xtw4t/Pvy01x+yGk86A5M5Cq6e5HH25/AqpHPQybkEr2sgwj9+4ZzLPhdb0KoLiAkKqBPpA+FgBpcvn/KNjl/Wyv4SmGaSRSG2Z/lwaJsCJgpB3iB2JQkS9WXY4SrgPRBtsjjAd+1+25hmy01wL9nw8MejZx++Zcz2Upwf1MOwlwJeekQ4VftdEiNCnY+ENt5gCC1AbESst1juoM8hV2z02UTmZq8KtZAOQzxkyxVBOxFoBu5dMNeAXa07bhS5Gq+ir5z31Ebv9LlHjguF/bD5h2wGsdIQThOp1En7TDbBWLdlRQhJx6n3Hp6FXxPfB5HcbdfdeMX93w52hz7woY6eeW/klhy9lTtsRXLahoaxJ7Q1rF87UOIzal1NfS6n9fQObikYTbmOqFBSlk1oeaLx+Xpsb6FxBmRgwAvR8M03dNbDVkDuevNnxVWMt8QG8rV9tRORQjbsTXJNkZoilIXFAtvQ3FmyMMt8iSmXIMcNoxZEH2KrKjAiIs06yNowOO/zsjw64Dj053/6auvc8RdEE7A2cXGX/eOzp8WnJ5M/qRk7bVoYqjVk2QG25EIwZIIS0q9g1Mg6CViKxAPYG1oeWQpn4iUuF0MjtjcVgipU70ZRh2EcJKslEh6CBtAgQ+IC8ypgHQLAy6nGPuG2Gz+59p9fvoGREKPDpnmIgm+krBpsgwtHtP2hIQfnMvBEGNchN7ShmUIVguP1VvBoDwAAWWwWDp6tQ6i2G6ZJvoEf/nbiOZbPLXLFAheEkhzyaRey+MHrDaHJ8mi26Q4iImxFrQO7eJhgMQjrAZRjTDHPiXC0ZxmIKHgN/oi0SMP9HgIpKEoR550X4xHNCqvRgScfXZ1SPLVTzjqr0YVwvyCQ5LVWxn/KCGenMtX6E9ff+8RRGUQ1NIOKPHjGpgcW/EprCa84MvCK1j6tiX+T7V875eSZtUJ+7T0DHcdOaj8mpDOrl3W+uEQae2RdXIm8nE4ZcillRNMnPODmoiXE5VMZD5uBV07/T+5bMDbtfeOGBUr7k1cvKk7i3b50HxdsKvGXhLnmOnXj+te6u5ENAyt58+wZvmTa8J5pbCjCfcAd9h9VAw1kroJpe8zpfeAaaARmPASz1tYNkSNgxWevNNi7dNaF+pnaki2DR2IMDDEcGwGcLJYUbFDpPmDqgsVYmWeBmSNi9oGuulQ5xGh5SJQDzPY6TN1BJeQlx28sGpQGjigLll1W4NWi5fHvqBE2XLL0gkejL00ykKkAmEaLOUwHTJHpK7U/e0HdEw3xwTBRD2IoHUxyMI0FOXEsmWjInOdD3QamEWOBfaW9SGFm0NluRLmMnG9c/KXFR54haoJfzTIhvaRDUo1giMA2xE4RBfg+sYg4DC0SPJ+QtNQDiSOsUBFSGPbvLssNA6ESbIo8eYWcPGnDgs2fABkGcTIcDHBkxFUNNJeCoprr39b92OB6qW7GxVeSLye2hQWyHyPYoGXE64Ejzjz26QUTQqmagZDr1setV+oG/VPEJa4Mc86FMzfeO/hmzfvmHNl45xtPMPyMWRd1eGu2dr34TFIIzAzPHb/1lUXFl2e2dkQEpRT9+7bNzCnTrpytI2I/TPWhm7U82umBx1Ix9tKnTspYd85BwEJOGj84afZJZ7rZLQ+90LmhoTk9a9acFp71IcGv3L92RWf+SSQOj3trw5nXU1/5mpvx61g90LE9JZdDIN/9h4OR0FFjZAE0iuTiRhIB26xVV/wQkvt8PXGIOsLQKQJJVOijIMpF1HIZ7FIB2y4OYw8WSgB/4VYFSQ/lg2rRFSlIEciZQkQAc75eL2LTwzgsBEwh1CRMI0acfOzhBIpG3TH/heTpAytP/osadTMS0hmgaXCPE5D2D0+LU7ZFrr28RQL/osAtYvdO0BXVA9pCSyeh6B6YhjhIVKUt4SAmkRhT+6DS9A+sx5TrvruFnTBYhLRUU+A4hfA1wGusNWD90VcsazBhw3ihu9BiATFgy0wfsmcF4m/pKtlUoEsAPxxQyBreQkBpCbgqsOmoxx3k9Xxyi6c2UN/g+xy3/S/3PLc+/ckoaxY5IQBEs9uTE5F+HlaVlic039V9dKkGOHjbTSU92hc4vWPyyTPvWDFjyrZf/fOy/3eOVWjc+Lrxw+PGhbUAoy35zrza4z43Nd72ox8NSM1HHvGPbCC78bVVvS9Pntu25tXNYk0YUiLByL42zzr2jEXzz32NX75oQUA6/M47737hucbWc2c1NEMvL4e2J5ve33w6xp0yEaSf/vvyUkqKzZpzzJWCMDaqcoZ19x8a6tBSbHQAaXTwQA7ia4uGzweuGyZSiKLL+7aMyYi+dNRtRMCCYespQN2J3EYwn5PAhnAcAprDpBK+7tAfICKEJEI/iP27AKcjK0iObp1WewiZcpslJB0reUMwPwVpoTEAXEH4CMM9THFg4zeWWcdPe6T7Ix8Kl4pWwFfmpiDnhFRPzhlR7avpu2786d1nZr1upJUBxg87YMlbxi/wYXs+RWFXmsK0wuqpbAqFLDMVmuZIORBntL/j8j9e8pHG4uLeKJcUC2bYB08DFtGXaPulgGmjtVPHThRohzztRFuxOybbDASKzguiWvQFdF6VFLdoKXCU8mBPgOhx2CqThdRYTxohPDVPiC1aQY+mJvzc9tt+n6q55KppoJvkkESLCjxjNOK7B545tfm27/z1nGJ407iNk8zfr3xA64fsMnrcov4j5WJdx29Ul0fIgg00XYWgyRRC1vOhWcg97Mr90jyjA/7cpeufX10/dtr4mcenP/08FAT3Tm+HwLXY+cE1wYYzX11yyjxEpmdj2+eay/94eLSvqciJENcCeyALCKlWrkZD1B23umqx2tQcjYQ5BP60EOpazAq++ds3LJ5z1mxMKOwJyF5vvw5nyGDZj4UJTDUQhgyE8sjqLTD5/nZWpjxhEK+CB/IZiAOHFQg1C9AjekEIOGRZRpQWTqU9NCy3IOFjdRHiAUv2u3MeD7YAcsE/0NwNwRd4GMSMor0fzXUc4IDzXk7Nr3vyuS35m/w7fqNe+7Hxg2G0HVIawkTwyPCRBdesYPHTv/CU+4SfxyNEFcnW2D6cKQUmzeZOYZRHUh7aqOx+uJLYDWIogRqyhGDOLtmDgJoS+AFXLqZxerquaArb37+h4XtXrF2wbEyJHfRxCH6qIZNwpIS4lUEkVPKFICHFB2AijTxJNuZiRwB4QQVv9nEQ9iLQVwH8G62WsBQEmqElkMghK6sfGjJqAGYCKLpg6T7Y70fdi/7x8PLgRVedaHt6wuEbXEkhyhR8zJX3hb/97KLl0QCSev38OjZykf5ASRdnBmqORc4yt6Ao0SOmcMgRKtE2HXIumjn24Yz8tofXH3t2XEP+zGxvfttf/9Jx4snj6qNy/7Enj72tLvb1pj8sFU77wKynv3rWlDkTyerSofHO2dZrgtqTK5y9ShBxwLX2laWzG5rnNJNCnI79lXKUS+/8CxpjH0MYaMvR4aE67L4zwpXbO094gIGGhUQJayI2KcRuYXmBwBeedHAgglgQuGzxCtLbwRtDeeXvryyTO44+7NiNN278zCcTDfQ6yIXzfec7UoCRsyv/+JBw64cVkE8EG0awLexZR/LX2Nkgp+FZYtKAvWoYOxNkLBLMsO5hBnzZRuxX/AUz4CoIvl9+IX7dVeLav9REXbpbRYwQA9xaXFVZBCR3C5BXwaKMwqTAPkmAZSnoIXZNdr43JZwqcb6wlYYgHblpyccKaWQgefLAQhLJcRHCBGAA3cUCjzSWCHLpQ576kKuoL/r7P9INbTOOq5sUzyuIgiVrCbT5I0llkWp+7gTPgP7i0tAc7/8euabtSOTlCu6Q3sh4xvteT1zyqZkEAUARU8uwTSGor84ccxXkEC+ZPoR2UUTXhiU3debj7UHW9Wb45l91PTwJ22O4/IlML++FjBZWJQ7EK/Cy0Qq/y5YIaLO9DAHTCktj07GwDkXVcr7nvLe/Z2dknTN0dADPzr2e0x7n+R714gFhmhu7HFgGGj4SVcAcTy5HnqFY6Do0iJzs5rJvvrQivUBvOvmYuKo8skI57SNHRiBAx/vANKde5zsG1ieByW688clpPz81Z0Ug57Qx2P4cIafzwkhnF/JU2pyaXiiFo1oe6Yj6o3yKTzF/7VF7v3TcMtFYtOMi71fnXfWN6JYnB7kO+GdCPIYoLhak63CzBXPshieKC8woOFVNx5JFPLGVI5Uaq8oxmNoy2LaCXMFYjndDHgP2DDIQeHUiO4tINjj4Dx4NcHuyLa7cyFEfiOt63WPXbomree8ln50NAqh7+qOWKZ169FcufRMGunjB55LPvvJTiq6JXNtsvb7Xvw17+9Cq8aef1WFRjlbK/cIMRQB2IJb1gzpoMK8CAQaHprAvPN+Zk/x1szf/hrnqR5HBOLDT9GLJsuPVVJhEfM2BJNE2GgaQYRoOYnBtmqYq0P/Cqx6P7GNoqJwbozw7NNQeSfxxKnbqdfqzR7X2C+SlD6KOlZxEnIRsfkCfaAD53sMjKKctfnreWibR+pG5E1c9MX9Vf+MVHx8PZ2FUDy4LmObU75xhzQXJKOJm/eqX2+f88JS0KCA1K0ehxWx44Dv7OOAbVS5K8QjMghHiC8G1K9csSKxpO8K/XN9R2Mqzk34+adWnll909Zz7N9V6EXUJyWWQlKuEXAiQ57h8fqRJQ+gjsHuEarUa9hiAP7bZ0DBiv8h5zQIraDLvJnbLDu4E6S2GGkpQRGDggZfEQsLED+JjRNWGMadZMH059vDOXwz4u5RVSdE/8ZJz6wVFdvuzF7149OYCuF+R7Q9A4eAptIZP+UB959JV3X1rG4z8MbFJNTNmBVLbc9rYJlMWnLWM0Ll8SFhGITaWfWgNemzAqqoEuyGIgb98zz0fQ+B/JF1EF5H3CWIYSAMcfsQZYfSOiDAq3B3TGBYRskSyE/n3DgfDhuoBA2+Tgkq1zn2nP3t8DQ+IptHiTsLccs+xZtDGHf0tFFOZ7I4V69b0WLMuOKXVs+2ex5Kq99wrThOQ0A1oY6/exBsPqxj5w8BkIWcN8+qv/q5edVMomYuHFRVYixbh3z65BVfOps5ol5jOxGPq4JsrfifFY+8bU9jyr87e+OTzpmzvWdU7vyOVH+RvOHXNjqmI5wVpmamrBQ+MvbVADiHEaSdDomm36DJg7AMGyWLh7g7OzDQjJXCVIgINeuAzCwwkKk4rZRCcLPQGHj+iCBAsJQGWceDYsC57kCMAATs0qTEoRXoHu19/eDET/8hp55sr+K0v/5gR5SnfnNT4+jdLLb7zG2qiLYF7f76ZmRiY+fGIUF/kdiz9+/xeX234qsOm8xUBsA0zB3I0UnBWwfbZJklWKmbDKB8sef9yck0uZGJnByluhEaFdiPOXHVGGJhQxibIsDAeQ6sn4n3QCMMh7987hmMaPoGKscmqVOu0w7ne42t4QJimkiEBzDUQWxLN1PXO/q6e7t6MutKUwXn7hKMOP4pf/MLagpWd8OlT3cF2O92nZk8szPs9MY0YfYJFdzDIvHTd0jnXnCEo22NRYoPRJFhp7tGQYTdcGeI4QV49JSakPPPnVam6M8bG+h5Jrgic+I1436rFL27jJx0jP1SIw7tenTUXDt6YzZgyebA7rEtC4GXKxoc7kOfyECZCSwO5DqlRkXhUV3UkLYFqg5UkUAjsvkHMyjsQxM7gkHIJFBH4CJoISQj4NA75l6jtFnK/eVw+dTDR3qU0xgb/ev9C/4nyQkUSP3X6glevugI7QFeXN+jpq5N8qWu6vnRYjUvIwgR06+++H2mfdvJJ7eymBFkiI9T2sA7D5AlxZyB3AlNMOYFhiAnbJvgL6Vot01sj+5msK2QUvBoxK2XZmF3DriNrj/YwTCsh6LdgUNqtt/mo0DTn+079zvUeX6tgGqYDxB849GLvutcXbc4VSogIyIu1RckQoo01m5LdEhNNBI+55AQGqR+KBmxcLT9bseR0vmKjUeUTYHswC70wZPUav/5SzUcvn0AJtEiiQaWdHVel8J4nOM1jatKBtDQrHk9NOY1//P6VdRdNP89997rV25vqjh4TnvfP/u4Gd580pnZh6KLJHj8i1yNSlRKFtX8pQHosEmeThANBlxX48/jJaBBhLkUe5rMW4kVALobZAkUUSDO4N3KfsRD1AxbaCFcE8SzspJB7HbgJu7QM7xURRomNbW1j10faXGsTCTktxrnlP3ncfeJVsfZ6MR1GZAvYjUNbZfi21vKeEgJlMoE8X3IHZTXZ4NWzbAhTDCnR8SUHAs7IuKCctHel2BsDow2FhbkclBilkCnnIRAbJJN1lRVskgdxvLN6Dr1vwxBkZtjqqZI+AzNp93J7AnzUd4iLBZEZXq9zvUd9eEA0jeCJeVtcseLFhYMuxOY2PH6trw8paBFfRoLSuX7asUe2uVUYTGPzBj2mChsrHKT5oxocXHPOMPwFpmmSRwrByE/f8vVn/N//hKtUS/km6Z8D5z3a49xwFTHqEAByg0EXsrsbbLHw+uIp7/d0tTxxsy948bkPPfum6pkWnfFoT196esfyzaa/ftLkJgSIRLpgaGkpbySkvVhoEGQEdpIwdoYbshshDAMlhOTm4Ee8aeOAgaBIDeG6GHnTwpIDUINcC20rSEIYV9hZeBD1ApoYeBggTR0Mygwx0RXgtFCxrxVGl2KMHWy+84bUCV+ZUYdcuoYH/Dqb9hvQbOuclgmJ8EeQQFkVBemckXlV9IDBwm7DBk6lp87IlLwuZFqAgR2YMIwG3NJUctC0slrUB6qEbSeZAkDRDcEp0uU5cNr9jNEn0ksH+G57R2Ab+MNzycHMEd4sv/QWf512Ou87184rI913nhOi2HyaWx/csmT+is0wkudFpL+QXNFYKBLhaqZ2MIPu2nEe2D1hYQbxRowdy4u8OaRlgN0CeBnU4PTP+R7UV/BJsxDoLJeOi5qXmXfrvMO+NJfml42W+8a0HMgskIV8kcuITNGaIEdK1giK4Op844/nnAWn3pf+Z7UYCgQiifCLPWpo8tRJzW6/Dg8VrujHIBcEUeEzbEuzKyclGQbZgSVR3doJo0ST3bG9R4YhEedWIrOjA3ptotU1uTnBBLz9bn5Hxi9ikURKeUOOasWI7DNCOVswB10DnH9gXYRtLawvgRMxbdWDDw4EOy6M+MZ6x3hCsFi0IDANFyGu0HxQJsFZDSmgKc+U/c8ZIQdiQyMx7McefFHlufO+A+lhrw1dOuUwwjbt2Vf5oRf38wfxhdSjfR+0jsGzA6NGoULzCx57cQtFMAW5SUw8bOwMM9AYQw54p722hB4VD+ev3vJTZK9IBUCdIGtYfvNfTvr2qdBdE7FTsYLBygM17o6pQ+125W08I0xDFVQLMI10qCqrax6flgvKHlOJM+tkMeBDvAKX9OR9C7JCOHiSFW8PwEDAD30FwjPqXGMMnJmc1N98LR0yBxV1IE2d8oDIAPdchVgBjjzotR6QfG1HdRwvBv1yzpQGZud0eNgwAdaQQrIIjxusxtQVWpER+wV7KOgUkNkhE4sY2xa9tGIFbMiZ5rHjLpuZwNYDcIITHbhCOYhwYBSoqtKNPVaboR4P+/G2YdqwVWfYZw74cn8xDYgONt4w1KCeDHvXPPmLbAHpIyKeOW0zJ9fDckxDFh/sEIgptw8wMvaA05Dvegy/dp4RHhOGOZimSoK/69EHFn/koyfCJMyAqydCXDFK3vZ3tXHKebNytn2jaNJQsBz7K2gL7QUhigVFQcLFPKUdCghI9QNzE8rQrW+f//Tr/cjFVhsxQy3j81pxoMgYNZLeeuL4ja88oSDUrwxtJyzjeCEa93kaWsWBvpKk+Ty9ve1zTl237Pl1LktMTG6Lto2r9/O9ciKql9ARKSj7JPBrwDF0yLDlaxztziEIZriabJaLB6X0Alhlbl/3zw2pGbNOOs8VT/sopzSDcIWI5+eCGzPhqQ0Up4PDOjz88u3CNFpCKxAc/ol/63p/MQ3zzcY0JJfLv/KLfzBM7ZTDD5/SEpZ4it+GiBSkILLgxlVpDRFg+ufQOKeRznPn2jnb5XfBNAgOEGnqlWd+EPrsl+sZLW3VIX4JEhaUa6RVcthRjsuB2w6OEzrS10GHPVbBgIm/n88bIrwlKNIQrDAwb3JdOzJLu9dtzqEcCsOLhYEtF1gw/D/xsot9Lz/VV+sJ1jUkEs118OmEphAUPa0EtT49mulpfvG7MOBmIY0X/NNPPb0mKCP6PVjFkk8OSi6KeUg0VRMQYQyybcxBexCVgB+FYBtSkv0BmBYsfuLe/paJnz0PMu9BeDMLLNzTsKUsUTK70RwjQdq5v6/adilnD8Zovr0/ZfcX04g6UMgsnX/pt89nW878f8Fo0K4f2zAiZCoiuFIcNqe99Iz6tuu1c4/Oww97h1ReGG3aJgeYvBZjmM57f8zeeKXfh5B0vwAAHfRJREFUEDNqgi3IiCpODdkbphXpc/ifJJL2p1GMqBqoMSQokMVyRt5CpEoNW1r6hAQRP1NI5eFXnTO4wsA/t6W42KyTJ/PP/Mvws9HmswWX24uFlwIgUpQNjV/958Ucm+mOmBkhUeoa0xlzh66qCQZSXQPrX+xuPWriYWNqIgKzWSy5i6GSJdtSOKz9oKiw1KfdChoGIUrIk8eGM5wV/TBNQsNYY90fHt024adHNKJdKhiHIJl8w5aGOrT/x0iQdu7vq7Zdyr2rmGYPMO2qtvzsj9M/eUSgEfJJaNMAPXAYCL8Thl6aoOK0l860ljnXDsRG6m+ZQ7Px2cY0ODtA2qVpkcHOe38R+trFTeC3ERmNeCVUvDdMk8r4hQXK/oFPkwcVaoTtMva9yL1oIQiClPWTNpNoDYQCcgGhwBgEsYAUKoDImpCxuyRi2RgFlAn2BVA5UrQmRJKAXzCzZn2pmFTHJiRXY6OQb5VS3tz9Vj9SqylrlhXAGDa3TTzmiGhEtdLwx0IrSd9P5tGwWIaRJWRsFIBMgXwYgTvMrWKCC+g+UC/kRNj0P88zsyY2dzS1diApoQnDTPjKlTvkQHAkyDmQdc7Dyw+/dsoNP+8sR7/292vDaxn5en9pWrkGAFwsZEVB8EAJCB4EyGUgaN2QORnMGyqfsuvdS3tH6oETJcHh19zY6EOa5VFFq7jmziesq884FpyyhVA7gMJeMa007MvQJEGLQUYZoMXQVYYRGMgDk0eYpkHP5WbdMLAOQG/lh9QfS5y/gNFFFRJ6IxUhDIXwT3MbGiLNAl3hRGWZHi+5rDAZNeCFX5/Gpzz+9B+7MrLlTTR6Muvjge3z1sdm/6aBs5JBCeUpzw02rcAuippAEQLgd0pGNZA8SvnxBVauQZZogStoUXb56nm9y3pdStPJt0OWi5gLKF6eMTsxoNLBfZyGlx9+PdLrTrkytSAq8fYe+41pldUTmyMjyBqDtbZojPxDSMtQVk1hnRqSL2Iu73VijNT+MqahAiqAJQ/1IgybG57WXqgYky99R5r96RN0s6w02Dem0ceBaQi1Y3eQoifBwxgiZCRHB5bA5QRBRHi9CGeCMHHisDPGdJFVn7sIigNSyDJFL5RK4D5lXII9MFWyQ4CNBrTyYNxlNVY0Qz2wcOMU2Erwrmy4sOKpZ9YVmeVjfK4UgpmVKAYrpTJEfC0eu07YkiNum8tAxCtJ5bxCzh2XA1mfYcRhA5b3D8QlPdp50++s62pjbdPDOkWjtUHoYAC+ul+HU96B9PDrkSpxymEZKjMoIxU8wPv7i2k2sw4+TYRNgVzyeSBagspFh9ekaNujYC9gu/c57cXZnhbOtdM8p//OtXMGt2c/ojXDxjSXDu8DPZ9gZCgbLdeG5+9bNumys5og/rVJ2h71kIa9PBEhWCCCa+9hUBY9tA9sImhBphwjNiQxcvSrzKmjOgcS9CrpdUGdcRcvUa2Vd+jsjLi9jqNmR45T8vzxzqK5tdR6y/jSUWk+oHIFppT2IJ0Iqa2Q/ZFUVegk8om6YcnGyuVAMS5YJ9pAosllrfz13ZpfU6dd+Kk6Xnfng2ZfAxlxlGBzWunGe/bkYMIeIzesR2VgYJPuwHzY8329P6z4iJdOexz8IKd94uuxEq245cHaD3+yEQGpwV8TV7/bsRdMKyOlU1MZ04hookpc7MQ0G6GAaeUzjTv9j+flG3QiFKtgnINpztedFm9vgkemfssf1/jynnHauOuPHRB8G9XGXtUXgBSWc+dpNQUSI6Ii2NtKPlrUHUZFNHlUiLp5Lr0DoWXm3/uvY6+a1NayVWhwaTwcAeFdDgHbe/tw4ETQfKtjiNYQGbBhv3vpfb2/e+mRr5z2OPjhYBpChgWstc+/8d2aCHgtiuc77Cj7RtHNIZpGIwiZVqUgkMtGIRA2/MDQ0qX9q9wfhwMnyW/5KUrgHxFA+uuc6YoO5+zUjxZlRFGRVi1aWVi70fz+FWAwmGIx21YocgEPvHZsTDOwrwWTSAS8Qjv9QHocEHuYEqw/mB5PTHvlB8tS9b3Hnf2hWkGGeROMLZHsyP7qe/ePM7IO3EbqiT3jMSex40eR/d9TjlTfSPed9jjj52AabHKBXfkdsSAs+8haYngFcJmu3KpgGlEusoRzaio/pvrLPagUp9UQPyEprZzL0cvLT+mvzTkSXgD/7L+Vz1Tet2ujWxlkRIEEhUJjs8yiG14484M1sZkuxH+AiJgC/ffCBRLNwTYF5kX0UaAe9qaUR8NOCmM7M8DMABsY2Zu3ti5Kvf5X9yk/OBK+Wh5V8Q7Zu1c+/547OSPrwG2kDoBjt6ENGm5Pbuc9p/y+3nfK7evs1OucHSt3BcJ/HVnpS7CSRdyWoT3uUH1wc678djANl7Qq7Y5pZZbLRqpycTS8/M9ZPam80xt7wSxfYNmlH+UtS/nV3f9C68nBSL7IePNSA7P2jrsMeJnN/voHbA0mfNkRG5HCe8C5iieLKxywHgE/SLTKpqHI6EX1F7Gj53vr3FbKo/3jw0ffPcXVb4Z4nsKNvacPZ3gc2I7UGRIPERzeLUwjuyNwxuByYH0BmrYH1+KS0XS7N7th2k5Zsq0wQCewDpbJE8pTryv/2xiJ31SMqqH/6bJSonJZ/gLulZ/YPyp/yImsRAH9fTCTysdLK1e/0fXawNzPnAfHcAUqT1i4kfUrCBiWTqwOZJZDtYLzLVcGlyBsl2F4jL8ZbzGW5QJ9x3bG7j67VIxYtBN7bx/UTToIom91lH3Obe4cxWi8dj/29f7upUe+2qNeoAXtCMCmwC1EcCGiEImj9qzApdDA0esVTMPv3VdPYupxlLd6Nq0jkkUH/u5cPZ0WODTOJuFUWbms87jyqv1++Q+8QdFIKYjsUC4EWJYCErfiR4+Zh33gI+MNCTbvsO0AktGeFk1ANfZXYSJvtwBW4fA9wEbWGgwy2YCJXJdBxMJ9sOtbJ9zbLvnydu7WXT723vvpAK4MxpHbX8Y0YlrojT1L73ln5Lre6onTHqcM8e+EaQVEynZ5kLgAXI1NFZwCznlPTMMTwqyh1dPGNBLG2DhI93diGprvXCPAqX1/t2u8U5bC2N13vmmfnZ4rEAsj2A28Fb1sLpwJu1JmwpLX3nOHybWcct7sKA8OrfwZoqkQ9+F/+hBhGpqJv3jZpeajOHtyiEjFikV/KnLOc69MzTciVMZ7XczhjKwDr92AuMsFjHfKRSrjtMsj++e+3h9efqRrpz3OcwfTYHwBJIfRDUaFAk3tKeWg1ZMODKO9XbCpEEYQth30HwU9JuXU0F7CwSSn5c65XMvo/zotd870TaJeK59+ZmuqWNvx5ZPjjAJqrCBJrLsIpyVopygEhdMO4k/sNlgIEyiaspZtccHB6ebrb/4SwucFYcB3cB1OP0eS/hxcrd13a5z+OCWd6z33ng6m2WhVJhv4WbZXwwiChtFI7lz3ibDY2FCp2h5l5zMHcHZa5pwdTHNbUq5/yaPPcd++uNaPmNGIVII4eb6cFyI1smgnTr/cLkI0BF+C3Yeq8GJRkAQkrF181hHPefvqENf6ANr0Tr7i9PO/DtOGbDBpr1AmvGU4VzCIfLXLfFoF/DZG2mP7zoxH5btQU/nFhpnHv/b96753/LlnjZOQ4hCbS5mDyzKyF4HWllESPAI1BrwCJ+dcYUHneSGJMGwLKT4GVtg9tkDvTKurte4TArDnLx9Y5UG4sC0ok4rKXXD8NPlwYLUaOsq0pHxZwYyhZ6P94czxne8R/UQ0JVjA8FDeL1/00JsF5rLbYb1rSYISRjmywkKcLpu02ku+vVOAehWZe4qx7iZj7aesZfyv3uczlQhaf3AdTn8PVZrmQHsPuA9hGtDJZintvSOQzllnKUMtbTydN+k5/XaunbPzhdGeHcgPvUcV4ns2MZJMUe1LbPmfV5jzT50zlo3D3wm7K9tjtmIjbPNohGlcDlEQB3lkSkB0ujGdzKfvGODj25sPOm2U09//Pkxz+DSYnkEUX5EeYLBJqkBjjmiSGHQ4n1dQwebEK9hGt94uTNutHmAO0VoyvhxEpLK+u+/aEstOnnY7EyuvmkB2atwQzqO8imBGPi8zULP5cuuBKdqiaNsAsg+xtu03tfNgOQ51TNttHHcFOplz2QdhWRm5ytfEswEqZFpZFiyUbxNmlf9Vrt+RExZQP3JhmoZW8qfDXs8116x79oHFK6e3TmyPYkdAqQuwm8anK/sT/Ew3kOZp/u/FtmXsT4yTodBC5CE/Ub7qcVBAgGJBVo7yHm7oioYR/5G2AOvqrnyaTUqcufnvDuXe6yEi5UIGUCSERxYWBA9Whd5v3qfX9rOJ2rETJ04ch6wyZZKGxthzxJRSye4lT74uetPjNnlvuwRbVY8RJe/ag+tw+nuorp4j4kM56igeY5/mLEY2S06SWDow5niC87ABG7HGYeUO9NJpj2PPpiNny+C2rQNrtvTltjKt72/VtueO+lAdHEspYLTKrf7HI6tkwevtYc6buaLto40Jz2C8HxnaKLIedF1Im0qKekhvYJQK35qDTs52oHB6z7y3C6YBdwh9CN9Ay2jJBIaVF9R3BdPK7XH2vCTdQGAlLWBxXX/8TWeggPguNWcf5mZTueZZpX+8vJQn+xXTCOhI5R3OdZ7/41pKQDwQRahWODxQzyAa0eEKgbwBWH3t6+qf/xwEdmJaeWCHMM1uAt17t2ia054KLBC81YWQLUwW5o2rX1yrXbLyqX8qITYDJYwRMzOMJxiKj59a59riylmx1Esrop/US6WjJh2naBH4w/dETESEKNuCgKohxW8Z9f5zgP6v/9IQpjl7Sod/sEcCWEa2E+/G6um0Z+cIwVYIztIB8gdETHyt1L2xRzaNDyz55fymSN5zWEssvWFbXg9sR5y9uhYun1vSoEj5JjgnsG3TW088EaEQKRmthdBCCNhhk++ddVd/veMQ2AXTykTEwTQsLzZr9m7RtJ02IWUYwCYIDYS/My5lRFpWKcyLwbp3xPKy/MhdWxp6zNCRc2Y2RXTkqNi6Zk1XdpM12Bpsmp3mJLlnjXAsQoi7EJitccr0+pAfQVTfcdBWP7AbBIYwzSZdmOo2fwY+rYx3KPsu0bRKe0iTbh+U05BEyBpsvMkpy1tAECvEC3Iz0Ij2b0VqoBgs2uAWZtxzn8QOmhNmTBuvH6/tUFbP3yJnc1Yt8lWJfrdR6u5tuvRSLwS81eM/CoFdMc1GLgfTaMmkPQGshd6V1ROfRXtsqyQbIERjyYUAXlMGj+i+KeQRMErIlgTjcCTY9TBKjgsg4awcWrOqbmqMM4xSmCl88Z7gzPazj/EFM4gvTvsAWUE4N8PnsWmjXXH1z38GArtjGsbWwTRbrU7X7zKm0efpgNSDnI0NC0yWVOK9cD11+Tx6ifGxiMlgFZHTAI5QliJkQ1wqzKUjFHpQfJprmFH0Q3NAxI/yUEI4R8F2K2hcrrr69z8BARdFawMHNKRtImEtpLVD9mhOI2wC41y8C2dnBjjtAObZB5DGPhyMhIsBmdgNWUA69yvFsMNBh8sqXudW9fyfgACcd4l6/Sc+9fZ+w8Gw4bXaNpJD9gHDnxLbic5iZr0Hu7xnZ95Ld7AFsyH/Xmqz3VYH05wdg9MBeCPbyDScljnPca4i2S7A+I/9tDf7hGtvMTL/scYcyIeGow1WfxCunXd3/nJqR1f3vOk8rJ7fKQiQSW15RXmnvvAfrpdi12AfM5zU7doK2hBUj/80BNw0JsC19xxJcxq8B3ki2Qx2mA4ghz8v82lDj51i1fM7DQE39mp7Gmq80199R+sHbu1EtHf0S9XKRwEBZNIBAcAUHz73R1HHu1J0pPbCDxk0rWIdvpeWVVi4XTi5vRSq3nr7IeBy7L+cqokiENo568tII+qUd860Ar8nxSVOB6rndxYCbxtzbGPoO9vWau3vZQi8fSYNWLOIEO4vDXwvA63a9gOAwBCmvQ0YQvzR21DNAfSi+srBD4EhTPt3m2rzdSRf+Hcrqr5/aEJgCNMcDHF2AqPtrr3nG+1L1fL/PRB423YEsL0mqDkI+98DwWpP9w8Cbx+mvffUDPsHoWqptwcCsJhG4hwKv145KB42ESbnel+fgZDUPiSF1FpIHvAuHU47KK8LciocKBPwLrX+P/pZO/o5YoFm9ByTQxhheCeWSgrSAiPfoZJXi4hIkWcsTSJRa1LWmGwR2SGYdIGRBnUmpxiarGi6VETuJtUsIEJxjpEzjKIzWQlxbkfsyRCfNmKJ/XxQEAwPMl3vZ+lqsXcdAhrrdXsYL5I+I4cNMnMaCIWM2CwIhqzKcMhgPG4zrwUSQEOk7VEKoSiyNdUwA7zqgm8aMkR5FJNnXQGY2yPZdhhRxwJIeGgHEdp71+AngOA9u9nZ7L3gSHcd4pHj4OL2LjrsOu0gc3TkiqkyjCMNmG1QQRlKFDmYLYgR5H42kKwTrhkWz2ky8s5hSZN1n6m5REviSm5elEyYzss6XLQFTQM2QdfHy+4SGwACumVeYbxIM54PcbobobFHOt42mhbGF7B4vou4NlIXq/f3BgHFg0hMUTacV91uN8Wn5Un9zXOcUfKYQC7J70YwRFYUNSTY1JiMXxBUHlk2G5FGFTlUXEgPUdI9ngySLfqZvCvTzBvwVONGlnK9bcudourIalxdPvc2qgfjPUQ79KqpV7WgG+yYbsLDTDeQakTg3H5B1JnBgAnHIMalMRYnqywT4Y2c6Ta8Y4oIMhzwcnlh+yBCYGsRl6gpTFBoZvyoxUTewpGOt42mmbaz+8goPVIDqvffHQj4Smp44JUfLPkiHPsZMNhuregRGeRw4ExTUT112kAjlimWk/hwycN0855QRGf1AhPT8/AxE4NqXNSyPJMNM8lHH5MnXj5uLCNTsrgRe/O2YZoAZhDhDKqBZUcE9cH1AFmeGX9o7Ytf8Vn9KaTfCQbC+sZVXdKgMpBxdVwTF59dtuhfno5jPxNvNdmu76w+7Mpj3MX1f/v7dsVf42v6wMWCefsNzMcumTnwyRd9nro//enuBi9S5Qy52O2lsybi3kFCQM5rB3Qg1aJ9lFTkR8QCekCVvA0vOe0wdQ1ZlCnfT/UYAQKIbmIYaaukyVujY3OZZy+NIHLmta9a3V/kmSAj+APNF/VY6yYyE08+rZZhvqyq/fchAnrN17ODHwE1ExrGMcxHe7b/oKFpFjOz2M0EHjfVj7JfSFo5iElG+CRuj0zugPOUO6NidLbPZdHOW1TVEOxlLh8ct+wBxHBy5F8tW2IRqc9Zc4y45MkbVzcfxb4htz/Xf7h8ux6Lt7SZmycr2YZTnjb72u64qefxM3vHh2dr67o/eJu6btpJvZtq+vtbuHNfe//PIk2+VZkJEy7rWbb4jN/VSL6iYO0tj08ZAPuzeu4Tyw4OWFZbsT8QMD2m5BYZOQDhhrZV4bqu2f6bS8LM1pMWLI5a/Etjn1f6u0vT/nfqatMrMDVMw0BU2HanO/nsOFZ4avn3zhXi+VQNNy3/l9c//cv8A8VLEr/jNt6U6GGm9QoepGNiSshZvPdjPzDNZvKq1Grv8HvP3TVUC7H03Z7ukJjb9GgfUt3H2oSUtoyr0T3hbQ96Q4w7NOWsiZlJ/qxqPPanFyb/bPY/fzrzUzue3BTPr/9O3XEnLLj3Gxzj63DdPlDYNvdmdXHg6iPr//V/N//42PPmNNeE3iLY64gLKxZzsG9Y0cF87Qfbg4L7UWrEr1UfvMMQsBlXcNI5TSkY1ra///ams+Ned+Jj2WsZZtyUMTx/1bbkSZ4b8tv+cdNFJ92sWetqmJPDTP3XNvX3X8IXrAF56ZPPzWHOKqUZsdMqrbYePnfCOcsLVuEE/zO6ZvTc2iEy3Am3FnMjdmOffBpZ0pIOdF+T18ljsK9y1efvDgR28mmC+vjPFnFGvGb6+R21kQA/b2Gqf+yprb4w98RneyemBpjpR33klELwiy+3H/5BI9SSrHk49cnuxoIRzoaXetvltdbhO8aoQk9tgfdpvS13FT/uYs1AIdC/7ok1Y65tGbF3+4FptCl4C31WpWpHkLJPlByxKdUH7yQEHEzTczXMkzfLZ1/D5V21GKxMqORnoHkSUr50/OlnG5gxs6bk3S6dLwQ1qVZ3FwII1Zr0lzhI1zbXQB+lDtSr3n4hgp1FVwtTCOQCA3UQ2eddSH/pEkbm0/YL06BI3CcCVTHtncSTf79uB9MMwcqwYYQ65JE1HVtRPaIziP+b8yNYmI8pijmvKco+SURQMLdoFBif7t3hixb9xf46H2L1y3LAslTOy2QieV5y5VtLhTDiuvos2acEDJ1DxsIRjv3QRlUtcEaA3XvytpXKhMJSkfXIJYb1iYES7INcmhLijK2+dMFveEWvojAqmwwGVEYLh3lLbpAZhBUe4yowguSKqMg8kmGKIc1yxaKthiescb6groYRS50RXCMi2lupD8qAPNRolVHmOf/rJDfOODoGZA6J0WCfSCBx4tM508d5jlzIVGAoX4VTj3N2FrvhZ6eeofN+SDmGylZ/HAIQcDDC6UoZkXbe3fmrXALXKELH7k8cTHQwrlz6Lf5WMe0tgHMoPtodX8hOjXCN8Ij+YPM3rNND13aZnQ+d+1VM2wmT6q9dITAcQ0CbnFvkGz50MYRBhIJlLCv/rVTmrMLOy7t+Y6+/qzRtr2D577k5hClEzWxc273vO4ncboi2e6H9uapi2v5A6b+gTAXjhhDP6bKDX/TA+Z+eDS84RAOdF4efq5g2HCKH+PVeMKJ8y0YdO8DZbhDYBaN2+blbkf27qGLa/sHpkC21F8zbr7467+03+o2sI9iv71ULVSGwnxBwxCL7WbxarAqBA4RAFdMOEHDV10YJgSqmjRJg1eIHCIEqph0g4KqvjRICVUwbJcCqxQ8QAlVMO0DAVV8bJQSqmDZKgFWLHyAEqph2gICrvjZKCFQxbZQAqxY/QAhUMe0AAVd9bZQQqGLaKAFWLX6AEKhi2gECrvraKCFQxbRRAqxa/AAhUMW0AwRc9bVRQqCKaaMEWLX4AUKgimkHCLjqa6OEQBXTRgmwavEDhEAV0w4QcNXXRgmBKqaNEmDV4gcIgSqmHSDgqq+NEgJVTBslwKrFDxACVUw7QMBVXxslBKqYNkqAVYsfIASqnsUHCLh36zXEE4Yzr+PXu2fUAqddTn4Ip+Q+PYAr+Sd0ntFdbsZ4iyDczidGd65i2ujgdXCUpijX+2iJjY5As32VG14N4szuDPoy/OG/c13FtH8Heu/Ou8Afh1K9dQOo1KgxjUKqvRNHFdPeCai+k3US6mCl2x8M2p8yw5sKNKPYVgfy6vCqdr+uYtru8HhPXCG7xD7bScgCdBl93BWquopp+4Tvf0uBfWNaZYXdJz83HGImJc7cMwjp8GKjv67StNHD7F19A3SKSNo+UY1FKcqBKI6ytaaONCf7rH2UlVLxKqYdANDeC6+A2TKRycscNaaZCKv8TnSwimnvBFTfwTqJ96LqRyI7Q5GOQc/shLqVtuwTe2zZhsululiD2XdKndH3sIppo4fZO/KGgzn7xIj9/7oLiZxGTZ8EoBmQbtTv7bNZVUzbJ4jeqwVcEFg4+Lv/fbCTDoMcVnUE+w+z91ZJByfePprmokVw1LTp7fv+MPhXadowgBwyl5QvxUHfUXQKebXKkrhRvLNfRauYtl9geucLvf20BHgGmuYg2/7WbxKXNnol1r4B9M7ouPb93WqJdxgCNmYN4dn+fwyIRq86CLr/L+6rZJWm7QtC/6Hn+0tz9rs59tK5U8S7v/VzJETBjmC/v7O/BYeWcqfqCvUcNSe5vx+slts7BBz47y9G7L2WnXed+pw7b1e9Tn2jPldXz1GDrPrCAUGgimkHBLbqS6OGQBXTRg2y6gsHBIEqph0Q2KovjRoCVUwbNciqLxwQBKqYdkBgq740aghUMW3UIKu+cEAQ2EOe9q7LXQ6oG9WXDnoIVGnaQT9Eh0gDq5h2iAzkQd+NKqYd9EN0iDSwimmHyEAe9N2oYtpBP0SHSAOrmHaIDORB340qph30Q3SINHBInnaI9KfajYMVAlWadrCOzKHWriqmHWojerD2p4ppB+vIHGrtqmLaoTaiB2t/qph2sI7ModauKqYdaiN6sPanimkH68gcau2qYtqhNqIHa3+qmHawjsyh1q4qph1qI3qw9qeKaQfryBxq7api2qE2ogdrf6qYdrCOzKHWriqmHWojerD2p4ppB+vIHGrt+v+kJ1rHW0Yi0wAAAABJRU5ErkJggg==", + "text/plain": [ + "" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "example = sample_results[0]\n", + "example[0].get_value()" + ] + }, + { + "cell_type": "markdown", + "id": "70a4c8f0-2d83-4722-bdec-5a1076a97ce2", + "metadata": {}, + "source": [ + "\n", + "\n", + "## Predict caption similarities with CLIP\n", + "\n", + "Let's use the CLIP model (https://github.com/openai/CLIP) to predict similarity scores between the cartoon and the caption choices.\n", + "\n", + "To start, we need to load the model:" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "9dfbc57e-3024-4e5a-a8a6-864143293991", + "metadata": {}, + "outputs": [], + "source": [ + "device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n", + "model, preprocess = clip.load(\"ViT-B/32\", device=device)" + ] + }, + { + "cell_type": "markdown", + "id": "ae795c14-956f-4ceb-852a-5db6012d80c5", + "metadata": {}, + "source": [ + "To get similarity scores for each image/text pair, we need to:\n", + "1. Preprocess the image to transform it into a torch tensor with the expected dimensions\n", + "2. Tokenize the text to get the tokens for each word to pass to the model\n", + "3. Pass both of these values to the model to get the cosine similarities for each image-text combination.\n", + "\n", + "Here's how to do this for our current example:" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "3176721d-41fb-447c-8c27-f58108ca0a5d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "tensor([[20.2135, 23.0318, 21.6857, 23.2001, 22.4901]], grad_fn=)" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "image = example[0].get_value()\n", + "image = preprocess(image).unsqueeze(0)\n", + "text = clip.tokenize(example[1])\n", + "logits_per_image, logits_per_text = model(image, text)\n", + "logits_per_image" + ] + }, + { + "cell_type": "markdown", + "id": "275b4206-7a2e-42e6-9769-e6074926f943", + "metadata": {}, + "source": [ + "To predict the probability of each caption choice, we can perform a softmax operation across these scores:" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "298d3cfa-8995-450b-bef3-f5ceb3500fdf", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "tensor([0.0194, 0.3241, 0.0844, 0.3836, 0.1886], grad_fn=)" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "logits_per_image.softmax(dim=1)[0]" + ] + }, + { + "cell_type": "markdown", + "id": "43f33413-5e5f-4f0e-be5c-9b5799f5aadf", + "metadata": {}, + "source": [ + "We can use `DataChain.clip.similarity_scores()` to reduce this to a one-liner. It takes the following parameters:\n", + "- `images`: any number of images as inputs\n", + "- `text`: any number of text values as inputs\n", + "- `model`: CLIP model\n", + "- `preprocess`: image preprocessor\n", + "- `tokenizer`: text tokenizer\n", + "- `prob`: whether to return probabilities or raw cosine similarities" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "143bc601-c3d6-4905-a685-6ec2897f1ad3", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/dave/Code/dvcx/src/datachain/lib/text.py:49: UserWarning: To copy construct from a tensor, it is recommended to use sourceTensor.clone().detach() or sourceTensor.clone().detach().requires_grad_(True), rather than torch.tensor(sourceTensor).\n", + " return encoder(torch.tensor(tokens))\n" + ] + }, + { + "data": { + "text/plain": [ + "[0.019353175535798073,\n", + " 0.324138879776001,\n", + " 0.08436062932014465,\n", + " 0.3835700452327728,\n", + " 0.18857727944850922]" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "similarity_scores(example[0].get_value(), example[1], model, preprocess, clip.tokenize, prob=True)[0]" + ] + }, + { + "cell_type": "markdown", + "id": "0d91857a-566c-42a0-bcc1-7aa962f0d37c", + "metadata": {}, + "source": [ + "\n", + "\n", + "## Saving the chain\n", + "\n", + "We'll need to apply these similarities across our dataset for fine-tuning. To start, let's create a small random training sample using `DataChain.shuffle()`, which returns the records in a deterministic random order.\n", + "\n", + "The result is saved using `DataChain.save()`. This will save a persistent, versioned dataset that we can recover anytime in the future. It also will prevent DataChain from recomputing the same steps when running the chain again." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "a14316af-b409-4ec7-ac15-292a6726af4b", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Processed: 422 rows [00:00, 19869.07 rows/s]\n", + "Processed: 1 rows [00:00, 1349.95 rows/s]\n", + "Processed: 0 rows [00:00, ? rows/s]\n", + "Processed: 1 rows [00:00, 2.50 rows/s]\n", + "Generated: 9792 rows [00:00, 24507.27 rows/s]\n" + ] + } + ], + "source": [ + "train_dc = dc.shuffle().limit(10).save(\"newyorker_caption_contest_train\")" + ] + }, + { + "cell_type": "markdown", + "id": "9f0a40c6-af3f-44c1-a048-f16edb8a5a6d", + "metadata": {}, + "source": [ + "\n", + "\n", + "## Mapping the similarity function to the dataset and chaining operations\n", + "\n", + "How do we calculate similarity scores across the entire sample? You can use `DataChain.map()` to map this function onto each record in the sample. It takes the following parameters:\n", + "\n", + "- `func`: The function to map to each record.\n", + "- `params`: The signals names from the chain to use as input parameters to the function.\n", + "- `output`: The dict of output signal names and types in format `{column_name: column_type}`." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "f6e39bbd-fad0-4198-abe1-1876eadf3f4a", + "metadata": {}, + "outputs": [], + "source": [ + "train_dc = train_dc.map(\n", + " func=lambda img_file, txt: similarity_scores(img_file.get_value(), txt, model, preprocess, clip.tokenize, prob=True)[0],\n", + " params=[\"file\", \"caption_choices\"],\n", + " output={\"scores\": list[float]}\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "d8187d7a-5889-46b5-be24-7287084027a9", + "metadata": {}, + "source": [ + "We use `similarity_scores()` from above to make a one-line lambda function, but if you need to customize your mapper function, you can pass any function as long as it:\n", + "- takes the `params` signal types as input\n", + "- returns the types specified in `output`\n", + "\n", + "Next, we want to calculate the probability that each record predicted the correct caption.\n", + "Using `DataChain.map()`, we can write our own functions to add new signals to the data and chain them together.\n", + "\n", + "We need two functions:\n", + "- `label_ind()`: Takes the label column which contains a letter A-E and maps it to an integer 0-4.\n", + "- `label_prob()`: Returns the predicted probability of that label." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "f6386db4-8f60-4be7-9a81-f903b5b3ce54", + "metadata": {}, + "outputs": [], + "source": [ + "def label_ind(label):\n", + " return string.ascii_uppercase.index(label)\n", + "\n", + "def label_prob(scores, label_ind):\n", + " return scores[label_ind]" + ] + }, + { + "cell_type": "markdown", + "id": "2b0b4046-aba4-4a72-b37a-93f64b66021d", + "metadata": {}, + "source": [ + "To apply these to the sample, we can chain together multiple `map()` operations:" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "912d230d-b5cc-40e0-a5fe-5993bba789c8", + "metadata": {}, + "outputs": [], + "source": [ + "train_dc = (\n", + " train_dc.map(label_ind, params=[\"label\"], output={\"label_ind\": int})\n", + " .map(label_prob, params=[\"scores\", \"label_ind\"], output={\"label_prob\": float})\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "f17bba8b-a7fc-458d-b5c7-2881399d6ab3", + "metadata": {}, + "source": [ + "Like other DataChain operations, the entire chain of operations here is lazy until we invoke some greedy computation, so we have not actually performed any of these transformations yet. We can save again to force computation:" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "44fcd0a9-134c-4434-a8e0-3de6ec8bc632", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Processed: 10 rows [00:01, 6.29 rows/s]\n", + "Processed: 10 rows [00:00, 6455.75 rows/s]\n", + "Processed: 10 rows [00:00, 3949.81 rows/s]\n" + ] + } + ], + "source": [ + "train_dc = train_dc.save()" + ] + }, + { + "cell_type": "markdown", + "id": "4710f807-9a0d-4308-98be-ef77321848f7", + "metadata": {}, + "source": [ + "Let's take a look at the results:" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "4c120469-f024-4a01-902a-b3bf4c09b3ca", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    idrandomvtypedir_typeparentnameetagversionis_latestlast_modified...questionscaption_choicesfrom_descriptionlabeln_tokens_labelinstance_idfilenamescoreslabel_indlabel_prob
    0133118667281811830newyorker_caption_contest/images660.jpegCJCloeiXhocDEAE=171984873693659212024-07-01 15:45:36.967000+00:00...[Why is the king at a doctor's office?][Put it down slowly, the mothers are very prot...scene: doctor's office description: A king wea...B1cf85e2ed637a41cbd9de5d1ce2b91255660.jpeg[0.001104930299334228, 0.2960589528083801, 0.0...10.296059
    1233118667281811830newyorker_caption_contest/images660.jpegCJCloeiXhocDEAE=171984873693659212024-07-01 15:45:36.967000+00:00...[Why is the king at a doctor's office?][Under pre-existing conditions, why did you wr...scene: a doctor's office description: A king w...A1118ec0f16b3b0c67388659f32fb9d8b6660.jpeg[0.7271297574043274, 0.20022231340408325, 0.00...00.727130
    2333118667281811830newyorker_caption_contest/images660.jpegCJCloeiXhocDEAE=171984873693659212024-07-01 15:45:36.967000+00:00...[Why is the King visiting the doctor?][On second thought, it's more of a sandals day...scene: doctor's office description: A king sit...C1e25c28b3485994bd5968d0d5eafb5c3f660.jpeg[0.035876620560884476, 0.023404479026794434, 0...20.903180
    \n", + "
    " + ], + "text/plain": [ + " id random vtype dir_type parent \\\n", + "0 1 3311866728181183 0 newyorker_caption_contest/images \n", + "1 2 3311866728181183 0 newyorker_caption_contest/images \n", + "2 3 3311866728181183 0 newyorker_caption_contest/images \n", + "\n", + " name etag version is_latest \\\n", + "0 660.jpeg CJCloeiXhocDEAE= 1719848736936592 1 \n", + "1 660.jpeg CJCloeiXhocDEAE= 1719848736936592 1 \n", + "2 660.jpeg CJCloeiXhocDEAE= 1719848736936592 1 \n", + "\n", + " last_modified ... \\\n", + "0 2024-07-01 15:45:36.967000+00:00 ... \n", + "1 2024-07-01 15:45:36.967000+00:00 ... \n", + "2 2024-07-01 15:45:36.967000+00:00 ... \n", + "\n", + " questions \\\n", + "0 [Why is the king at a doctor's office?] \n", + "1 [Why is the king at a doctor's office?] \n", + "2 [Why is the King visiting the doctor?] \n", + "\n", + " caption_choices \\\n", + "0 [Put it down slowly, the mothers are very prot... \n", + "1 [Under pre-existing conditions, why did you wr... \n", + "2 [On second thought, it's more of a sandals day... \n", + "\n", + " from_description label n_tokens_label \\\n", + "0 scene: doctor's office description: A king wea... B 1 \n", + "1 scene: a doctor's office description: A king w... A 1 \n", + "2 scene: doctor's office description: A king sit... C 1 \n", + "\n", + " instance_id filename \\\n", + "0 cf85e2ed637a41cbd9de5d1ce2b91255 660.jpeg \n", + "1 118ec0f16b3b0c67388659f32fb9d8b6 660.jpeg \n", + "2 e25c28b3485994bd5968d0d5eafb5c3f 660.jpeg \n", + "\n", + " scores label_ind label_prob \n", + "0 [0.001104930299334228, 0.2960589528083801, 0.0... 1 0.296059 \n", + "1 [0.7271297574043274, 0.20022231340408325, 0.00... 0 0.727130 \n", + "2 [0.035876620560884476, 0.023404479026794434, 0... 2 0.903180 " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[limited by 3 objects]\n" + ] + } + ], + "source": [ + "train_dc.show(3)" + ] + }, + { + "cell_type": "markdown", + "id": "d16fe570-f34b-4aa3-b6f8-184e5f584f0b", + "metadata": {}, + "source": [ + "Now that we have computed the predicted probability of each label, we can find the average predicted probability of the correct caption. DataChain's aggregation functions can calculate summary metrics. `DataChain.avg()` can be used to calculate the average predicted probability:" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "e5c6b505-3619-4600-be86-b5210569902a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0.7005960181355476" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "train_dc.avg(\"label_prob\")" + ] + }, + { + "cell_type": "markdown", + "id": "56f30f71-1459-4d1e-8921-81618acaa3d4", + "metadata": {}, + "source": [ + "Let's see if we can improve the average predicted probability using fine tuning." + ] + }, + { + "cell_type": "markdown", + "id": "7f61a438-a48a-4e9b-a0ea-4dfd65076c2d", + "metadata": {}, + "source": [ + "\n", + "\n", + "## Fine tuning the CLIP model\n", + "\n", + "We can fine tune the CLIP model on our dataset so it can correctly predict the captions. Since our sample for this toy example is a single cartoon, it should be pretty simple for the model to learn which captions are correct.\n", + "\n", + "To fine tune the model, we need to convert the data into a [PyTorch Dataset](https://pytorch.org/tutorials/beginner/basics/data_tutorial.html). Luckily, `DataChain.to_pytorch()` does just that. We need to select 3 signals from the data to pass to the model:\n", + "1. Image files\n", + "2. Text caption choices\n", + "3. Label indices\n", + "\n", + "`DataChain.to_pytorch()` has a couple helpful arguments to transform our image and text data:\n", + "- `transform`: a [torchvision transform](https://pytorch.org/vision/stable/transforms.html) to process the image into the expected torch tensor dimensions.\n", + "- `tokenizer`: a tokenizer function (such as a [Hugging Face tokenizer](https://huggingface.co/docs/tokenizers/index)) to convert the raw text into tokens.\n", + "\n", + "The pre-trained CLIP model we loaded earlier includes both of these:\n", + "- `preprocess` is a torchvision transform.\n", + "- `clip.tokenize` is a tokenizer." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "096b639a-c624-4809-a47f-12ba0f39fd63", + "metadata": {}, + "outputs": [], + "source": [ + "ds = train_dc.select(\"file\", \"caption_choices\", \"label_ind\").to_pytorch(\n", + " transform=preprocess,\n", + " tokenizer=clip.tokenize,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "5fe92c7d-945a-4c5b-a268-389b3d7560d1", + "metadata": {}, + "source": [ + "Now we can write our PyTorch training loop as we normally would. The data is a normal PyTorch `Dataset` at this point, so there's nothing special about this code. For each image, it calculates the similarities to each text caption choice and applies the loss function based on the label of the correct choice.\n", + "\n", + "
    \n", + "Notes about how we fine-tuned the model\n", + "We won't go into details of CLIP fine-tuning, but take a look at https://github.com/openai/CLIP and especially https://github.com/openai/CLIP/issues/83 if you are interested. Note that the code below is slightly different from typical CLIP training, which normally takes a batch of pairs each containing a single image and text caption and then trains on all combinations of images and texts in that batch. Instead, here we take a single image and multiple caption choices and train on those combinations.\n", + "
    " + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "bcca91aa-71cb-4ccd-8dc2-299a43d837a6", + "metadata": {}, + "outputs": [], + "source": [ + "def train(loader, model, optimizer, epochs=5):\n", + " loss_func = torch.nn.CrossEntropyLoss()\n", + "\n", + " for epoch in range(epochs): \n", + " total_loss = 0\n", + " for images, texts, labels in loader:\n", + " optimizer.zero_grad()\n", + " batch_loss = 0\n", + " for image, text, label in zip(images, texts, labels):\n", + " image = image.to(device).unsqueeze(0)\n", + " text = text.to(device)\n", + " label = label.to(device).unsqueeze(0)\n", + " \n", + " \n", + " logits_per_image, logits_per_text = model(image, text)\n", + " \n", + " batch_loss += loss_func(logits_per_image, label)\n", + " batch_loss.backward()\n", + " optimizer.step()\n", + " batch_loss = batch_loss.item()\n", + " total_loss += batch_loss\n", + " print(f\"loss for epoch {epoch}: {total_loss}\")" + ] + }, + { + "cell_type": "markdown", + "id": "07ee6f9e-19e6-4bd5-973b-e358bbde17bf", + "metadata": {}, + "source": [ + "Now we are ready to train the model! All we need is to load the data into a PyTorch `DataLoader`, set up a PyTorch optimizer, and start training as usual. Again, there's nothing special about this code. It's all standard PyTorch." + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "267f7eef-373b-4a5e-9e52-d62f878d948b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "loss for epoch 0: 14.143621398194227\n", + "loss for epoch 1: 10.231055159121752\n", + "loss for epoch 2: 0.22259006446620333\n", + "loss for epoch 3: 6.318037593899817e-05\n", + "loss for epoch 4: 3.862364474116475e-05\n" + ] + } + ], + "source": [ + "loader = DataLoader(ds, batch_size=2)\n", + "optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)\n", + "train(loader, model, optimizer)" + ] + }, + { + "cell_type": "markdown", + "id": "fcae719a-ce7f-4f86-9f04-7a564dc43fdb", + "metadata": {}, + "source": [ + "\n", + "\n", + "## Inference and Evaluation\n", + "\n", + "Now that we have fine-tuned, how do we know if the model improved?\n", + "\n", + "Let's start by performing inference. We will reuse our training sample to confirm that the fine-tuning fit to the data in that sample (in this exercise, we are intentionally overfitting to this small sample; make sure to evaluate on a holdout sample in any real application).\n", + "\n", + "We can use `similarity_scores()` again to predict the probability of each caption using the fine-tuned model. This is the same map operation we performed above to get the initial predicted probabilities, except this time we use the fine-tuned model." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "7e7f3b22-9daa-4b75-808e-133036e64c31", + "metadata": {}, + "outputs": [], + "source": [ + "train_dc = train_dc.map(\n", + " func=lambda img_file, txt: similarity_scores(img_file.get_value(), txt, model, preprocess, clip.tokenize, prob=True)[0],\n", + " params=[\"file\", \"caption_choices\"],\n", + " output={\"scores_fine_tune\": list[float]}\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "1fd2319e-f7b8-4e6a-85b4-03adb153f9d7", + "metadata": {}, + "source": [ + "Now that we have the probability of each caption, let's reuse our `label_prob()` function from above to get the predicted probability of the correct caption:" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "bbf0f747-a767-4ea2-b654-9a5367665349", + "metadata": {}, + "outputs": [], + "source": [ + "train_dc = train_dc.map(label_prob, params=[\"scores_fine_tune\", \"label_ind\"], output={\"label_prob_fine_tune\": float})" + ] + }, + { + "cell_type": "markdown", + "id": "4e7fa17d-a9ed-4f7d-a452-614b93db84a7", + "metadata": {}, + "source": [ + "Finally, we can evaluate the average predicted probability of the correct caption using the fine-tuned model. As we showed above, `DataChain.avg()` will compute the average signal value across the dataset:" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "f92b7ca9-d54e-41ec-b815-5e6ba09844d2", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/dave/Code/dvcx/src/datachain/lib/text.py:49: UserWarning: To copy construct from a tensor, it is recommended to use sourceTensor.clone().detach() or sourceTensor.clone().detach().requires_grad_(True), rather than torch.tensor(sourceTensor).\n", + " return encoder(torch.tensor(tokens))\n", + "Processed: 10 rows [00:01, 7.56 rows/s]\n", + "Processed: 10 rows [00:00, 4635.61 rows/s]\n" + ] + }, + { + "data": { + "text/plain": [ + "0.9999965786933899" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "train_dc.avg(\"label_prob_fine_tune\")" + ] + }, + { + "cell_type": "markdown", + "id": "937c3d0e-d4d0-4490-8f72-7b934628c2ce", + "metadata": {}, + "source": [ + "Looks like the model was quickly able to fit to the sample provided and predict the correct caption!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fd42fa6c-d3e6-48c4-83ac-21910aa878fe", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/neurips/README b/examples/neurips/README new file mode 100644 index 000000000..110082845 --- /dev/null +++ b/examples/neurips/README @@ -0,0 +1,18 @@ +The Py files assume the wget of the website has been placed in an S3 bucket: s3://neurips-papers/ + +The command to get all the filters is: +wget --adjust-extension -e robots=off -r -np -l9 'https://papers.nips.cc/' + +The --adjust-extension is needed because there are directories and files that both have the year name with no extension. This creates errors without changing the pages to .html. +the -e robots=off is needed because the pdf files have a no robots meta-data to prevent commands like wget. +-r is to download everything recursive. +-np is good practice. +-l9 lets it go 9 levels deep which is enough for now. + +In 2023 this downloads about 73G of data. + +In studio, the commands are run in Python 3.9 and the requirements of requirements.txt. + +You must supply your own in OpenAI API key in the `process_pdf.py` file. The API key is not version controlled. + +To run the RAG-LLM demo, one must save the results to `pdf-bib` and then run `distance_to_query.py` and save the results to `rag-query`. The final results are generated by `llm_chat.py`. diff --git a/examples/neurips/distance_to_query.py b/examples/neurips/distance_to_query.py new file mode 100644 index 000000000..61a75144c --- /dev/null +++ b/examples/neurips/distance_to_query.py @@ -0,0 +1,29 @@ +import os + +from langchain_community.embeddings import OpenAIEmbeddings +from scipy.spatial import distance + +from datachain.query import C, DatasetQuery, udf +from datachain.sql.functions.array import cosine_distance +from datachain.sql.types import Float + +query = "What is the best way to do topic modeling?" + +openai_api_key = os.environ["OPENAI_API_KEY"] +assert openai_api_key.startswith("sk-") +openai = OpenAIEmbeddings(openai_api_key=openai_api_key) +del openai_api_key + +(embed_query,) = openai.embed_documents([query]) + + +@udf( + params=("embed",), + output={"distance": Float}, +) +def distance_to_example(embed, embed0=embed_query): + dist = distance.cosine(embed, embed0) + return (dist,) + + +DatasetQuery(name="pdf-bib").mutate(distance=cosine_distance(C.embed, embed_query)) diff --git a/examples/neurips/llm_chat.py b/examples/neurips/llm_chat.py new file mode 100644 index 000000000..060654573 --- /dev/null +++ b/examples/neurips/llm_chat.py @@ -0,0 +1,46 @@ +import os + +from openai import OpenAI + +from datachain.query import C, DatasetQuery + +query = "What is the best way to do topic modeling?" + +k_rag = 5 +source_name = "Community" +system_name = "System" + +ds = DatasetQuery(name="rag-query").order_by(C.distance).limit(k_rag) + +df = ds.to_pandas().sort_values(by="distance", ascending=True) +content = "\n\n".join([f"{source_name}: {page}" for page in df["page"].to_list()]) + +openai_api_key = os.environ["OPENAI_API_KEY"] +assert openai_api_key.startswith("sk-") +client = OpenAI(api_key=openai_api_key) +del openai_api_key + +msg = f""" +{system_name}: {query.strip('?')} according to the {source_name}? + +{content} +""" + +print(msg) + +chat_completion = client.chat.completions.create( + messages=[ + { + "role": "user", + "content": msg.strip(), + } + ], + model="gpt-4", + temperature=0, +) +response = chat_completion.choices[0].message.content + +print("\n\nLLM-RAG Response:\n") +print(response.strip()) + +ds.limit(k_rag) diff --git a/examples/neurips/requirements.txt b/examples/neurips/requirements.txt new file mode 100644 index 000000000..7263d87f1 --- /dev/null +++ b/examples/neurips/requirements.txt @@ -0,0 +1,9 @@ +pybtex +PyPDF2 +openai +langchain +tiktoken +parse +numpy +pandas +scipy diff --git a/examples/neurips/single_query.py b/examples/neurips/single_query.py new file mode 100644 index 000000000..21ac7f7df --- /dev/null +++ b/examples/neurips/single_query.py @@ -0,0 +1,119 @@ +import os + +from langchain_community.embeddings import OpenAIEmbeddings +from parse import parse +from text_loaders import load_bibtex, pdf_pages + +from datachain.query import C, DatasetQuery, Object, udf +from datachain.sql.types import Array, Float, String + +# This will only run on 1/512th of the data! Change to 1 to run on all the data. +PDF_SUBSET_MOD = 512 + +BASE_URL = "https://proceedings.neurips.cc/paper_files/paper/" + + +def chomp(ss, prefix): + assert ss.startswith(prefix) + return ss[len(prefix) :] + + +@udf( + params=(Object(load_bibtex),), + output={ + "bib_parent": String, + "bib_name": String, + "bib_title": String, + "bib_authors": Array(String), + }, +) +def get_bib_data(bib_data): + (entry,) = bib_data.entries.keys() # should only be one entry + data = bib_data.entries[entry] + + url = data.fields["url"] + url = chomp(url, BASE_URL) + parent, name = os.path.split(url) + + title = data.fields.get("title", None) + + authors = [str(author) for author in data.persons.get("author", [])] + return (parent, name, title, authors) + + +@udf( + params=("bib_parent", "bib_name"), + output={"hash_": String, "key": String}, +) +def parse_bib_filename(parent, paper_filename): + parse_result = parse("{}-{}.{}", paper_filename) + hash_, _, _ = parse_result + key_ = f"{parent}-{hash_}" + return (hash_, key_) + + +@udf( + params=("parent", "name"), + output={ + "hash_": String, + "object_type": String, + "object_subtype": String, + "ext": String, + "key": String, + }, +) +def parse_pdf_filename(parent, filename): + parse_result = parse("{}-{}.{}", filename) + hash_, object_type, ext = parse_result + + # Ensure consistent camel case + object_type = object_type.replace(" ", "").replace("_", "") + + split_list = object_type.split("-") + assert len(split_list) <= 2 + object_type = split_list[0] + object_subtype = split_list[1] if len(split_list) > 1 else None + + key_ = f"{parent}-{hash_}" + return hash_, object_type, object_subtype, ext, key_ + + +@udf( + params=("page",), + output={"embed": Array(Float)}, + batch=64, +) +def embed_page(pages): + openai_api_key = os.environ["OPENAI_API_KEY"] + assert openai_api_key.startswith("sk-") + openai = OpenAIEmbeddings(openai_api_key=openai_api_key) + del openai_api_key + + embed = openai.embed_documents([page[0] for page in pages]) + return [(ee,) for ee in embed] + + +# Remove last line to include post-2000 papers +subset_ds = ( + DatasetQuery("s3://neurips-papers/") + .filter(~C.name.glob("*.zip")) + .filter(~(C.parent.glob("20*/file") | C.parent.glob("20*/hash"))) +) + +pdf_ds = ( + subset_ds.filter(C.parent != "") + .filter(C.name.glob("*.pdf")) + .filter(C.random % PDF_SUBSET_MOD == 0) + .generate(pdf_pages) + .add_signals(parse_pdf_filename) + .add_signals(embed_page) +) + +bib_ds = ( + subset_ds.filter(C.parent != "") + .filter((C.name == "bibtex") | C.name.glob("*.bib")) + .add_signals(get_bib_data) + .add_signals(parse_bib_filename) +) + +pdf_ds.join(bib_ds, predicates="key") diff --git a/examples/neurips/text_loaders.py b/examples/neurips/text_loaders.py new file mode 100644 index 000000000..8486a55eb --- /dev/null +++ b/examples/neurips/text_loaders.py @@ -0,0 +1,80 @@ +import logging +import re + +import PyPDF2 +from pybtex.database.input import bibtex + +from datachain.query import DatasetRow, Object, udf +from datachain.sql.types import Int, String + +logger = logging.getLogger("datachain") + + +def sanitize_utf8(original): + """Clean up invalid UTF characters that may cause failures when writing to a DB.""" + return ( + original.encode("utf-8", "ignore").decode("utf-8", "ignore").replace("\x00", "") + ) + + +def sanitize_ascii(original): + """More restrictive than sanitize_utf8 when we want just basic keyboard chars. + This also puts all the text on a single line by eliminating all line breaks. + If we expect a large amount of non-English text, then it makes sense to first use + the Unidecode package. + """ + sanitized = ( + original.encode("ascii", "ignore").decode("ascii", "ignore").replace("\x00", "") + ) + sanitized = " ".join(sanitized.splitlines()) + sanitized = re.sub(r"[^\x20-\x7e]", r"", sanitized) + # These should always pass after the previous conversions + assert isinstance(sanitized, str) + assert sanitized.isascii() + assert sanitized.isprintable() + return sanitized + + +def compress_whitespace(original): + """PDF parsers often result in some weird whitespace.""" + return " ".join(original.split()) + + +def load_pdf_pages(raw): + raw_name = raw.info()["name"] + + pages = [] + try: + pdf_reader = PyPDF2.PdfReader(raw) + pages = [sanitize_utf8(page.extract_text()) for page in pdf_reader.pages] + except Exception as error: # noqa: BLE001 + error_type = type(error).__name__ + logger.warning( + "A pdf reader error occurred in %s: %s - %s", + raw_name, + error_type, + error, + ) + return pages + + +@udf( + params=(Object(load_pdf_pages), *tuple(DatasetRow.schema.keys())), + output={**DatasetRow.schema, "n_page": Int, "total_pages": Int, "page": String}, +) +def pdf_pages(pages, *args): + record = dict(zip(DatasetRow.schema.keys(), args)) + del record["random"] # random will be populated automatically + record["is_latest"] = record["is_latest"] > 0 # needs to be a bool + + total_pages = len(pages) + for n_page, page in enumerate(pages): + page = sanitize_ascii(page) + page = compress_whitespace(page) + yield (*DatasetRow.create(**record), n_page, total_pages, page) + + +def load_bibtex(raw): + bibtex_str = raw.read().decode("utf-8") + parser = bibtex.Parser() + return parser.parse_string(bibtex_str) diff --git a/examples/openai_image_desc_lib.py b/examples/openai_image_desc_lib.py new file mode 100644 index 000000000..93765d58c --- /dev/null +++ b/examples/openai_image_desc_lib.py @@ -0,0 +1,29 @@ +# pip install Pillow + +import os + +from datachain.lib.gpt4_vision import DescribeImage +from datachain.query import C, DatasetQuery + +OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "") + +source = "gs://dvcx-datalakes/dogs-and-cats/" + +if __name__ == "__main__": + results = ( + DatasetQuery( + source, + anon=True, + ) + .filter(C.name.glob("cat*.jpg")) + .limit(10) + .add_signals( + DescribeImage( + key=OPENAI_API_KEY, max_tokens=300, prompt="What is in this image?" + ), + parallel=-1, + ) + .select("source", "parent", "name", "description", "error") + .results() + ) + print(*results, sep="\n") diff --git a/examples/openimage-detect.py b/examples/openimage-detect.py new file mode 100644 index 000000000..f5b387c0e --- /dev/null +++ b/examples/openimage-detect.py @@ -0,0 +1,72 @@ +import json + +import pandas as pd +from PIL import Image + +from datachain.lib.dc import DataChain +from datachain.lib.feature import ShallowFeature +from datachain.lib.feature_udf import FeatureAggregator +from datachain.lib.file import File, FileInfo +from datachain.query.schema import C +from datachain.sql.functions import path + + +class BBox(ShallowFeature): + x_min: int + x_max: int + y_min: int + y_max: int + + +class OpenImageDetect(FeatureAggregator): + def __init__(self): + super().__init__(File, [FileInfo, BBox]) + + def process(self, args): + if len(args) != 2: + raise ValueError("Group jpg-json mismatch") + + stream_jpg = args[0] + stream_json = args[1] + if args[0].get_file_ext() != "jpg": + stream_jpg, stream_json = stream_json, stream_jpg + + with stream_jpg.open() as fd: + img = Image.open(fd) + + with stream_json.open() as stream_json: + detections = json.load(stream_json).get("detections", []) + + for i, detect in enumerate(detections): + bbox = BBox( + x_min=int(detect["XMin"] * img.width), + x_max=int(detect["XMax"] * img.width), + y_min=int(detect["YMin"] * img.height), + y_max=int(detect["YMax"] * img.height), + ) + + fstream = FileInfo( + name=f"detect_{i}", + source=source, + parent=f"{stream_jpg.parent}/{stream_jpg.name}", + version=stream_jpg.version, + etag=f"{stream_jpg.etag}_{stream_jpg.etag}", + ) + + yield fstream, bbox + + +source = "s3://ldb-public/remote/data-lakes/open-images-v6-test-200" + +ds = ( + DataChain(source, anon=True) + .filter(C.name.glob("*.jpg") | C.name.glob("*.json")) + .aggregate( + OpenImageDetect(), + partition_by=path.file_stem(C.name), + ) +) + +with pd.option_context("display.max_columns", None): + df = ds.limit(10).to_pandas() + print(df) diff --git a/examples/pose_detection.py b/examples/pose_detection.py new file mode 100644 index 000000000..d26099e0c --- /dev/null +++ b/examples/pose_detection.py @@ -0,0 +1,220 @@ +import json +import os + +import fsspec +import mediapipe as mp +import numpy as np +from google.protobuf.json_format import MessageToDict, ParseDict +from mediapipe import solutions +from mediapipe.framework.formats import landmark_pb2 +from mediapipe.tasks import python +from mediapipe.tasks.python import vision +from PIL import Image, ImageFile +from tabulate import tabulate + +from datachain.catalog import get_catalog +from datachain.query import C, DatasetQuery, DatasetRow, Stream, udf +from datachain.sql.types import JSON + + +def load_image(stream): + with stream: + img = Image.open(stream) + img.load() + format = img.format # typically, this will be JPEG + image = mp.Image(image_format=mp.ImageFormat.SRGB, data=np.asarray(img)) + return image, format + + +def landmarks_list_to_pb2(pose_landmarks): + pose_landmarks_proto = landmark_pb2.NormalizedLandmarkList() + pose_landmarks_proto.landmark.extend( + [ + landmark_pb2.NormalizedLandmark(x=landmark.x, y=landmark.y, z=landmark.z) + for landmark in pose_landmarks + ] + ) + return pose_landmarks_proto + + +def pb2_to_dict(pose_landmarks_proto): + pose_dict = MessageToDict(pose_landmarks_proto) + # Test round trip while we are at it + assert pose_landmarks_proto == ParseDict( + pose_dict, landmark_pb2.NormalizedLandmarkList() + ) + return pose_dict + + +def annotate_image(image, pose): + annotated_image = np.copy(image) + for pose_landmarks_proto in pose: + solutions.drawing_utils.draw_landmarks( + annotated_image, + pose_landmarks_proto, + solutions.pose.POSE_CONNECTIONS, + solutions.drawing_styles.get_default_pose_landmarks_style(), + ) + return annotated_image + + +def mask_image(detection_result): + # This only takes the most confident detection. + # We could union all the detections or do a generator over each. + segmentation_mask = detection_result.segmentation_masks[0].numpy_view() + visualized_mask = np.repeat(segmentation_mask[:, :, np.newaxis], 3, axis=2) * 255 + return visualized_mask.astype(np.uint8) + + +@udf( + params=(Stream(), *tuple(DatasetRow.schema.keys())), + output={**DatasetRow.schema, "pose": JSON}, +) +class PoseDetector: + annotated_folder = "annotated_images" + mask_folder = "mask_images" + + def __init__( + self, + *, + model_asset_path, + bucket_name, + prefix, + ): + base_options = python.BaseOptions(model_asset_path=model_asset_path) + options = vision.PoseLandmarkerOptions( + base_options=base_options, output_segmentation_masks=True + ) + self.detector = vision.PoseLandmarker.create_from_options(options) + + catalog = get_catalog() + self.client, _ = catalog.parse_url(os.path.join(prefix, bucket_name)) + + def save(self, image, source, folder, name, format): + # Do writeback + blob_name = os.path.join(folder, name) + urlpath = os.path.join(source, blob_name) + cloud_file = fsspec.open(urlpath=urlpath, mode="wb") + with cloud_file as fp: + image.save(fp, format=format) + + # Get the blob info + info_ = self.client.fs.info(urlpath) + entry = self.client.convert_info(info_, folder) + return DatasetRow.create( + name=name, + source=source, + parent=folder, + size=entry.size, + location=None, + vtype="", + dir_type=0, + owner_name=entry.owner_name, + owner_id=entry.owner_id, + is_latest=entry.is_latest, + last_modified=entry.last_modified, + version=entry.version, + etag=entry.etag, + ) + + def __call__( + self, + stream, + *args, + ): + # Build a dict from row contents + record = dict(zip(DatasetRow.schema.keys(), args)) + + # Don't re-apply analysis to output + if record["parent"] in (self.annotated_folder, self.mask_folder): + return + + # CLeanup to records + del record["random"] # random will be populated automatically + record["is_latest"] = record["is_latest"] > 0 # needs to be a bool + row = DatasetRow.create(**record) + + # Put into media pipe object + image, image_format = load_image(stream) + + # Do the detection + detection_result = self.detector.detect(image) + + # Move into protobuf list for next step + pose = [landmarks_list_to_pb2(lm) for lm in detection_result.pose_landmarks] + + # Turn into json + pose_json = [pb2_to_dict(lmp) for lmp in pose] + + # Yield same row back (with json pose info) + yield (*row, json.dumps(pose_json)) + + # No detections ==> we can stop here + if len(pose) == 0: + return + + # Annotate image + annotated_image = annotate_image(image.numpy_view(), pose) + annotated_image = Image.fromarray(annotated_image) # make PIL object + + # Save the image and get the cloud object info + row = self.save( + image=annotated_image, + source=record["source"], + folder=self.annotated_folder, + name=record["name"], + format=image_format, + ) + yield (*row, json.dumps(pose_json)) + + # Now do mask + visualized_mask = mask_image(detection_result) + visualized_mask = Image.fromarray(visualized_mask) # make PIL object + + # Save the image and get the cloud object info + row = self.save( + image=visualized_mask, + source=record["source"], + folder=self.mask_folder, + name=record["name"], + format=image_format, + ) + yield (*row, json.dumps(pose_json)) + + +cloud_prefix = "s3://" # for GCP just switch to "gs://" +bucket = "dvcx-50k-laion-files-writable" # which bucket to use for both read and write +bucket_region = "us-east-2" # no need to specify for GCP + +# Use: !wget -O pose_landmarker.task -q https://storage.googleapis.com/mediapipe-models/pose_landmarker/pose_landmarker_heavy/float16/1/pose_landmarker_heavy.task +model_asset_path = "pose_landmarker.task" + +file_type = "*.jpg" # which files to use +filter_mod = 512 # how much of a subset of the data to use, i.e., 1/512 +chunk_num = 1 + +# only needed for AWS (no effect if using GCP) +os.environ["AWS_REGION"] = bucket_region +os.environ["AWS_DEFAULT_REGION"] = bucket_region + +ImageFile.LOAD_TRUNCATED_IMAGES = True + +assert chunk_num < filter_mod + +pose_udf = PoseDetector( + model_asset_path=model_asset_path, + bucket_name=bucket, + prefix=cloud_prefix, +) + +if __name__ == "__main__": + data = ( + DatasetQuery(os.path.join(cloud_prefix, bucket)) + .filter(C.name.glob(file_type)) + .filter(C.random % filter_mod == chunk_num) + .generate(pose_udf) + .results() + ) + + # Output the contents of the new dataset. + print(tabulate(data)) diff --git a/examples/torch-loader.py b/examples/torch-loader.py new file mode 100644 index 000000000..d0335b457 --- /dev/null +++ b/examples/torch-loader.py @@ -0,0 +1,79 @@ +# pip install Pillow torchvision + +import torch +from torch import nn, optim +from torch.utils.data import DataLoader +from torchvision.transforms import v2 + +from datachain.lib.dc import C, DataChain +from datachain.lib.pytorch import label_to_int + +STORAGE = "gs://dvcx-datalakes/dogs-and-cats/" + +# Define transformation for data preprocessing +transform = v2.Compose( + [ + v2.ToTensor(), + v2.Resize((64, 64)), + v2.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)), + ] +) + + +CLASSES = ["cat", "dog"] + + +# Define torch model +class CNN(nn.Module): + def __init__(self): + super().__init__() + self.conv1 = nn.Conv2d(3, 16, kernel_size=3, stride=2, padding=1) + self.conv2 = nn.Conv2d(16, 32, kernel_size=3, stride=2, padding=1) + self.conv3 = nn.Conv2d(32, 64, kernel_size=3, stride=2, padding=1) + self.fc1 = nn.Linear(64 * 8 * 8, 512) + self.fc2 = nn.Linear(512, len(CLASSES)) + + def forward(self, x): + x = torch.relu(self.conv1(x)) + x = torch.relu(self.conv2(x)) + x = torch.relu(self.conv3(x)) + x = x.view(-1, 64 * 8 * 8) + x = torch.relu(self.fc1(x)) + return self.fc2(x) + + +if __name__ == "__main__": + ds = ( + DataChain.from_storage(STORAGE, type="image") + .filter(C.name.glob("*.jpg")) + .map(label=lambda name: label_to_int(name[:3], CLASSES), output=int) + ) + + train_loader = DataLoader( + ds.to_pytorch(transform=transform), + batch_size=16, + num_workers=2, + ) + + model = CNN() + criterion = nn.CrossEntropyLoss() + optimizer = optim.Adam(model.parameters(), lr=0.001) + + # Train the model + num_epochs = 10 + for epoch in range(num_epochs): + for i, data in enumerate(train_loader): + inputs, labels = data + optimizer.zero_grad() + + # Forward pass + outputs = model(inputs) + loss = criterion(outputs, labels) + + # Backward pass and optimize + loss.backward() + optimizer.step() + + print("[%d, %5d] loss: %.3f" % (epoch + 1, i + 1, loss.item())) + + print("Finished Training") diff --git a/examples/udfs/batching.py b/examples/udfs/batching.py new file mode 100644 index 000000000..315f3f928 --- /dev/null +++ b/examples/udfs/batching.py @@ -0,0 +1,34 @@ +# In some cases it is more expedient for a UDF to process several rows +# in one invocation. In such cases the UDFs need to be written in a +# slightly different way: +# * They need to accept a single function parameter, which will be a +# list of tuples of UDF inputs. +# * They need to return a list of tuples of result values, one tuple +# per row, in the same order as the input parameters. +# To install script dependencies: pip install tabulate +from sqlalchemy import Integer +from tabulate import tabulate + +from datachain.query import C, DatasetQuery, udf + + +# Define the UDF: +@udf( + ("parent", "name"), # Columns consumed by the UDF. + { + "path_len": Integer + }, # Signals being returned by the UDF, with the signal name and type. + batch=10, +) +def name_len(names): + return [(len(parent + name),) for (parent, name) in names] + + +if __name__ == "__main__": + # Save as a new dataset + DatasetQuery(path="gs://dvcx-datalakes/dogs-and-cats/").filter( + C.name.glob("*cat*") + ).add_signals(name_len).save("cats_with_signal") + + # Output the contents of the new dataset. + print(tabulate(DatasetQuery(name="cats_with_signal").results()[:10])) diff --git a/examples/udfs/image_transformation.py b/examples/udfs/image_transformation.py new file mode 100644 index 000000000..eedba29a7 --- /dev/null +++ b/examples/udfs/image_transformation.py @@ -0,0 +1,45 @@ +import os + +from PIL import ImageFile, ImageFilter +from tabulate import tabulate + +from datachain.lib.image_transform import ImageTransform +from datachain.query import C, DatasetQuery + +cloud_prefix = "s3://" # for GCP just switch to "gs://" +bucket = "dvcx-50k-laion-files-writable" # which bucket to use for both read and write +bucket_region = "us-east-2" # no need to specify for GCP + +file_type = "*.jpg" # which files to use +blur_radius = 3 # how much to blur +filter_mod = 512 # how much of a subset of the data to use, i.e., 1/512 + +# only needed for AWS (no effect if using GCP) +os.environ["AWS_REGION"] = bucket_region +os.environ["AWS_DEFAULT_REGION"] = bucket_region + +ImageFile.LOAD_TRUNCATED_IMAGES = True + +# Custom filters can be implemented with the ImageFilter abstract class +# https://pillow.readthedocs.io/en/stable/reference/ImageFilter.html#PIL.ImageFilter.Filter +image_filter = ImageFilter.GaussianBlur(radius=blur_radius) + +image_filter_udf = ImageTransform( + image_filter=image_filter, + bucket_name=bucket, + prefix=cloud_prefix, + output_folder="blur", + file_prefix="blur_", +) + +if __name__ == "__main__": + data = ( + DatasetQuery(os.path.join(cloud_prefix, bucket)) + .filter(C.name.glob(file_type)) + .filter(C.random % filter_mod == 0) + .generate(image_filter_udf) + .results() + ) + + # Output the contents of the new dataset. + print(tabulate(data)) diff --git a/examples/udfs/parallel.py b/examples/udfs/parallel.py new file mode 100644 index 000000000..e42cebe9f --- /dev/null +++ b/examples/udfs/parallel.py @@ -0,0 +1,55 @@ +""" +This is a simple UDF to demonstrate local parallel processing with multiprocessing. + +In add_signals specify either parallel=-1 to use processes equal to the number +of CPUs/cores on your current machine, or parallel=N for N processes. +The default if parallel is not specified is to run single-threaded. + +The UDF specified in add_signals will then be run in parallel across all these +worker processes, no other code changes are needed. + +Benchmark results showed an almost 8X speedup on a MacBook Pro, using this test, with +parallel processing reducing execution time from a median of 377s to 48s total. + +To install script dependencies: pip install tabulate +""" + +from tabulate import tabulate + +from datachain.query import C, DatasetQuery, udf +from datachain.sql.types import Int + + +# This is a simple single-threaded benchmark function to demonstrate the speedup +# that can be achieved with multiprocessing, by enabling parallel in add_signals. +def fibonacci(n): + if n <= 1: + return n + return fibonacci(n - 1) + fibonacci(n - 2) + + +# Define the UDF: +@udf( + ("name",), # Columns consumed by the UDF. + { + "path_len": Int + }, # Signals being returned by the UDF, with the signal name and type. +) +def name_len_benchmark(name): + # Run the fibonacci benchmark as an example of a single-threaded CPU-bound UDF + fibonacci(35) + if name.endswith(".json"): + return (-1,) + return (len(name),) + + +# Save as a new dataset +DatasetQuery( + path="gs://dvcx-datalakes/dogs-and-cats/", + anon=True, +).filter(C.name.glob("*cat*")).add_signals(name_len_benchmark, parallel=-1).save( + "cats_with_signal" +) + +# Output the contents of the new dataset. +print(tabulate(DatasetQuery(name="cats_with_signal").results()[:10])) diff --git a/examples/udfs/simple.py b/examples/udfs/simple.py new file mode 100644 index 000000000..d06854d58 --- /dev/null +++ b/examples/udfs/simple.py @@ -0,0 +1,42 @@ +import uuid + +from tabulate import tabulate + +from datachain.query import C, DatasetQuery, udf +from datachain.sql.types import Int + +# To install script dependencies: pip install tabulate + + +# Define the UDF: +@udf( + ("name",), # Columns consumed by the UDF. + { + "name_len": Int + }, # Signals being returned by the UDF, with the signal name and type. +) +def name_len(name): + if name.endswith(".json"): + return (-1,) + return (len(name),) + + +if __name__ == "__main__": + ds_name = uuid.uuid4().hex + print(f"Saving to dataset: {ds_name}") + # Save as a new dataset + DatasetQuery( + path="gs://dvcx-datalakes/dogs-and-cats/", + anon=True, + ).filter(C.name.glob("*cat*")).add_signals(name_len).save(ds_name) + + # Output the contents of the new dataset. + print( + tabulate( + DatasetQuery(name=ds_name) + .order_by(C.parent, C.name) + .limit(10) + .select(C.source, C.parent, C.name, C.size, C.name_len) + .results() + ) + ) diff --git a/examples/udfs/stateful.py b/examples/udfs/stateful.py new file mode 100644 index 000000000..d35f8ac30 --- /dev/null +++ b/examples/udfs/stateful.py @@ -0,0 +1,44 @@ +""" +To install dependencies: + + pip install open_clip_torch + +""" + +import uuid + +import open_clip + +from datachain.lib.dc import C, DataChain +from datachain.lib.image import ImageFile + +model, _, preprocess = open_clip.create_model_and_transforms( + "ViT-B-32", pretrained="laion2b_s34b_b79k" +) + + +def encode_image(file: ImageFile) -> list[float]: + img = file.get_value() + img = preprocess(img).unsqueeze(0) + emb = model.encode_image(img) + return emb[0].tolist() + + +if __name__ == "__main__": + ds_name = uuid.uuid4().hex + print(f"Saving to dataset: {ds_name}") + # Save as a new dataset + ( + DataChain.from_storage("gs://dvcx-datalakes/dogs-and-cats/", type="image") + .settings(parallel=2) + .filter(C.name.glob("*cat*.jpg")) + .limit(5) + .map(emb=encode_image) + .save(ds_name) + ) + + for row in DataChain.from_dataset(ds_name).results()[:2]: + print("default columns: ", row[:-1]) + print("embedding[:10]: ", row[-1][:10]) + print(f"type: {type(row[-1]).__name__}, len: {len(row[-1])}") + print() diff --git a/examples/udfs/stateful_similarity.py b/examples/udfs/stateful_similarity.py new file mode 100644 index 000000000..373aa6334 --- /dev/null +++ b/examples/udfs/stateful_similarity.py @@ -0,0 +1,79 @@ +""" +To install dependencies: + + pip install imgbeddings + +""" + +import uuid + +from imgbeddings import imgbeddings +from PIL import Image +from sqlalchemy import tuple_ + +from datachain.query import C, DatasetQuery, Object, udf +from datachain.sql.functions.array import cosine_distance, euclidean_distance +from datachain.sql.types import Array, Float32 + + +def load_image(raw): + img = Image.open(raw) + img.load() + return img + + +@udf( + params=(Object(load_image),), + output={"embedding": Array(Float32)}, + method="embedding", +) +class ImageEmbeddings: + def __init__(self): + self.emb = imgbeddings() + + def embedding(self, img): + emb = self.emb.to_embeddings(img) + return (emb[0].tolist(),) + + +ds1_name = uuid.uuid4().hex +print(f"Saving embeddings to dataset: {ds1_name}") +# Save as a new dataset +( + DatasetQuery(path="gs://dvcx-datalakes/dogs-and-cats/") + .filter(C.name.glob("*cat*.jpg")) + .add_signals(ImageEmbeddings) + .save(ds1_name) +) + +ds2_name = uuid.uuid4().hex +source, parent, name, embedding = ( + DatasetQuery(name=ds1_name) + .select(C.source, C.parent, C.name, C.embedding) + .order_by(C.source, C.parent, C.name) + .limit(1) + .results()[0] +) +( + DatasetQuery(name=ds1_name) + .filter(tuple_(C.source, C.parent, C.name) != (source, parent, name)) + .mutate( + cos_dist=cosine_distance(C.embedding, embedding), + eucl_dist=euclidean_distance(C.embedding, embedding), + ) + .order_by(C.cos_dist) + .limit(10) + .select_except(C.embedding) + .save(ds2_name) +) + +print("target:", source, parent, name, embedding[:3]) +print() +print("Top 10 by cosine distance:") +for row in ( + DatasetQuery(name=ds2_name) + .select(C.source, C.parent, C.name, C.cos_dist, C.eucl_dist) + .order_by(C.cos_dist) + .results() +): + print(*row) diff --git a/examples/unstructured-text.py b/examples/unstructured-text.py new file mode 100644 index 000000000..5ae4e73f4 --- /dev/null +++ b/examples/unstructured-text.py @@ -0,0 +1,54 @@ +# +# pip install unstructured[all-docs] +# libmagic +# +# partition_object supports via unstructured library: +# +# "csv", "doc", "docx", "epub", "image", "md", "msg", "odt", "org", +# "pdf", "ppt", "pptx", "rtf", "rst", "tsv", "xlsx" + +from transformers import pipeline + +from datachain.lib.dc import DataChain +from datachain.lib.unstructured import PartitionObject +from datachain.query import C +from datachain.sql.types import String + +device = "cpu" +model = "pszemraj/led-large-book-summary" +source = "gs://dvcx-datalakes/NLP/infobooks/" + + +def cleanse(text): + separator = "Listen to this story" + head, _sep, _tail = text.partition(separator) + return (head,) + + +def summarize(clean): + helper = pipeline(model=model, device=device) + summary = helper(clean, max_length=200)[0]["summary_text"] + return (summary,) + + +ds = ( + DataChain( + source, + anon=True, + ) + .filter(C.name.glob("*.pdf")) + .limit(1) + .map(PartitionObject(), parallel=False) +) + +ds = ds.map(cleanse, output={"clean": String}) +ds = ds.map(summarize, output={"summary": String}) +results = ds.select("text", "summary").results() + +for story in results: + print("\n *********** the original: ********** ") + print(story[0]) + + print("\n *********** the summary: *********** ") + print(story[1]) + print("\n") diff --git a/examples/wds.py b/examples/wds.py new file mode 100644 index 000000000..7bc2c7aa1 --- /dev/null +++ b/examples/wds.py @@ -0,0 +1,36 @@ +import pandas as pd + +from datachain.lib.dc import C, DataChain +from datachain.lib.webdataset import process_webdataset +from datachain.lib.webdataset_laion import WDSLaion, process_laion_meta + +wds = ( + DataChain.from_storage("gs://dvcx-datacomp-small/shards") + .filter(C.name.glob("00000000.tar")) + .settings(cache=True) + .gen(laion=process_webdataset(spec=WDSLaion), params="file") +) + +meta_emd = ( + DataChain.from_storage("gs://dvcx-datacomp-small/metadata") + .filter(C.name.glob("0020f*.npz")) + .gen(emd=process_laion_meta) + .map(stem=lambda file: file.get_file_stem(), params=["emd.file"], output=str) +) + +meta_pq = ( + DataChain.from_storage("gs://dvcx-datacomp-small/metadata") + .filter(C.name.glob("0020f*.parquet")) + .parse_parquet() + .map(stem=lambda file: file.get_file_stem(), params=["source.file"], output=str) +) + +meta = meta_emd.merge( + meta_pq, on=["stem", "emd.index"], right_on=["stem", "source.index"] +) + +res = wds.merge(meta, on="laion.json.uid", right_on="uid") + +df = res.limit(10).to_pandas() +with pd.option_context("display.max_columns", None): + print(df) diff --git a/examples/wds_filtered.py b/examples/wds_filtered.py new file mode 100644 index 000000000..d5c9b8e11 --- /dev/null +++ b/examples/wds_filtered.py @@ -0,0 +1,55 @@ +import pandas as pd + +import datachain.error +from datachain.lib.dc import DataChain +from datachain.lib.webdataset import WebDataset +from datachain.query.schema import C +from datachain.sql import literal +from datachain.sql.functions import array, greatest, least, string + +name = "wds" +wds = DataChain(name=name) +try: + df = wds.limit(3).to_pandas() +except datachain.error.DatasetNotFoundError: + ( + DataChain("gs://dvcx-datacomp-small/shards", anon=True) + .filter(C.name.glob("00000000.tar")) + .generate(WebDataset()) + .save(name) + ) + df = wds.limit(3).to_pandas() + +print(df.columns.tolist()) +columns = [ + "parent", + "name", + "vtype", + "dir_type", + "size", + "caption", + "url", + "width", + "height", + "original_width", + "original_height", +] +with pd.option_context("display.max_columns", None): + print(df[columns]) + +filtered = ( + wds.filter(string.length(C.caption) > 5) + .filter(array.length(string.split(C.caption, literal(" "))) > 2) + .filter(least(C.original_width, C.original_height) > 200) + .filter( + greatest(C.original_width, C.original_height) + / least(C.original_width, C.original_height) + < 3.0 + ) +) +filtered_df = filtered.limit(3).to_pandas()[columns] +with pd.option_context("display.max_columns", None): + print(filtered_df) + +print(f"wds count: {wds.count():>6}") +print(f"filtered count: {filtered.count():>6}") diff --git a/examples/zalando/zalando_clip.py b/examples/zalando/zalando_clip.py new file mode 100644 index 000000000..cf1514ed8 --- /dev/null +++ b/examples/zalando/zalando_clip.py @@ -0,0 +1,44 @@ +""" +fashion-clip UDF, using the +[fashion-clip](https://pypi.org/project/fashion-clip/#description) package. + +Generated embeddings are stored json-encoded. + +To install script dependencies: pip install tabulate fashion-clip +""" + +import json + +from fashion_clip.fashion_clip import FashionCLIP +from sqlalchemy import JSON +from tabulate import tabulate + +from datachain.lib.param import Image +from datachain.query import C, DatasetQuery, udf + + +@udf( + params=(Image(),), + output={"fclip": JSON}, + method="fashion_clip", + batch=10, +) +class MyFashionClip: + def __init__(self): + self.fclip = FashionCLIP("fashion-clip") + + def fashion_clip(self, inputs): + embeddings = self.fclip.encode_images( + [input[0] for input in inputs], batch_size=1 + ) + return [(json.dumps(emb),) for emb in embeddings.tolist()] + + +if __name__ == "__main__": + # This example processes 5 objects in the new dataset and generates the + # embeddings for them. + DatasetQuery(path="gs://dvcx-zalando-hd-resized/zalando-hd-resized/").filter( + C.name.glob("*.jpg") + ).limit(5).add_signals(MyFashionClip).save("zalando_hd_emb") + + print(tabulate(DatasetQuery(name="zalando_hd_emb").results()[:5])) diff --git a/examples/zalando/zalando_dir_as_class.py b/examples/zalando/zalando_dir_as_class.py new file mode 100644 index 000000000..62b93a384 --- /dev/null +++ b/examples/zalando/zalando_dir_as_class.py @@ -0,0 +1,31 @@ +""" +UDF to create 'class' and 'type' signals from the directory structure. + +To install script dependencies: pip install tabulate +""" + +from pathlib import Path + +from sqlalchemy import Text +from tabulate import tabulate + +from datachain.query import C, DatasetQuery, udf + + +@udf("parent", {"class": Text, "type": Text}) +def dir_as_class(parent): + try: + s_class, s_type = Path(parent).parts[-2:] + except ValueError: + return ("", "") + return (s_class, s_type) + + +if __name__ == "__main__": + # - save as a new dataset + DatasetQuery(path="gs://dvcx-zalando-hd-resized/zalando-hd-resized").filter( + C.name.glob("*.jpg") + ).add_signals(dir_as_class).save("zalando-with-signals") + + # Output the contents of the new dataset. + print(tabulate(DatasetQuery(name="zalando-with-signals").results()[:10])) diff --git a/examples/zalando/zalando_splits_and_classes_ds.py b/examples/zalando/zalando_splits_and_classes_ds.py new file mode 100644 index 000000000..438730b4c --- /dev/null +++ b/examples/zalando/zalando_splits_and_classes_ds.py @@ -0,0 +1,9 @@ +from datachain.query import C, DatasetQuery +from datachain.sql.functions import path + +source_path = "gs://dvcx-zalando-hd-resized/zalando-hd-resized/" +ds = ( + DatasetQuery(source_path) + .filter(C.name.glob("*.jpg")) + .mutate(**{"class": path.name(C.parent), "label": path.name(path.parent(C.parent))}) +) diff --git a/examples/zalando/zalando_splits_and_classes_output.py b/examples/zalando/zalando_splits_and_classes_output.py new file mode 100644 index 000000000..30bcbc8ab --- /dev/null +++ b/examples/zalando/zalando_splits_and_classes_output.py @@ -0,0 +1,17 @@ +import uuid + +from zalando_splits_and_classes_ds import ds + +from datachain.query import C, DatasetQuery + +ds_name = uuid.uuid4().hex +ds.save(ds_name) +print(ds_name) +for row in ( + DatasetQuery(name=ds_name) + .select(C.source, C.parent, C.name, C("class"), C("label")) + .order_by(C.random) + .limit(8) + .results() +): + print(row) diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 000000000..7c49ea4f5 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,136 @@ +site_name: DataChain +site_url: https://datachain.dvc.ai +site_description: Wrangle unstructured AI data at scale + +repo_url: "https://github.com/iterative/dvcx" +repo_name: "iterative/dvcx" +edit_uri: edit/main/docs/ + +strict: true + +validation: + omitted_files: warn + absolute_links: warn + unrecognized_links: warn + +theme: + name: material + logo: assets/datachain.png + favicon: assets/datachain.png + icon: + repo: fontawesome/brands/github + features: + - navigation.instant + - navigation.instant.progress + - navigation.tracking + - navigation.tabs + - navigation.path + - navigation.top + - navigation.prune + - navigation.footer + - toc.follow + - content.action.edit + - content.code.copy + - content.code.annotate + - content.tabs.link + - content.tooltips + - search.highlight + - search.suggest + - navigation.sections + + palette: + # Palette toggle for automatic mode + - media: "(prefers-color-scheme)" + toggle: + icon: material/brightness-auto + name: Switch to light mode + + # Palette toggle for light mode + - media: "(prefers-color-scheme: light)" + scheme: default + toggle: + icon: material/weather-sunny + name: Switch to dark mode + + # Palette toggle for dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: black + accent: lime + toggle: + icon: material/weather-night + name: Switch to system preference + +nav: + - Home: index.md + - Tutorials: + - Get started for computer vision: tutorials/cv_intro.md + - Writing DataChain UDFs: tutorials/udfs.md + - API reference: + - Datachain: references/datachain.md + - Catalog: references/catalog.md + +markdown_extensions: + - abbr + - admonition + - attr_list + - def_list + - footnotes + - md_in_html + - pymdownx.details + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences: + - pymdownx.tabbed: + alternate_style: true + - pymdownx.critic + - pymdownx.caret + - pymdownx.keys + - pymdownx.mark + - pymdownx.tilde + - tables + - toc: + permalink: true + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/iterative/dvcx + - icon: fontawesome/brands/twitter + link: https://twitter.com/DVCorg + - icon: fontawesome/brands/linkedin + link: https://www.linkedin.com/company/dvc-ai + +plugins: + - search + - section-index + - mkdocstrings: + handlers: + python: + rendering: + show_submodules: no + options: + docstring_options: + ignore_init_summary: true + docstring_section_style: list + merge_init_into_class: true + separate_signature: true + show_root_full_path: false + show_root_heading: true + show_signature_annotations: true + show_symbol_type_heading: true + signature_crossrefs: true + import: + - https://docs.python.org/3/objects.inv + - https://numpy.org/doc/stable/objects.inv + - https://pandas.pydata.org/docs/objects.inv + - https://arrow.apache.org/docs/objects.inv + - https://docs.sqlalchemy.org/objects.inv + - https://docs.pydantic.dev/latest/objects.inv + +watch: + - src/datachain diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 000000000..5c3650475 --- /dev/null +++ b/noxfile.py @@ -0,0 +1,76 @@ +"""Automation using nox.""" + +import glob +import os + +import nox + +nox.options.default_venv_backend = "uv|virtualenv" +nox.options.reuse_existing_virtualenvs = True +nox.options.sessions = "lint", "tests" +locations = "src", "tests" + + +@nox.session +def docs(session: nox.Session) -> None: + session.install(".[docs]") + session.run("mkdocs", "build") + + +@nox.session +def bench(session: nox.Session) -> None: + session.install(".[tests]") + session.run( + "pytest", + "-m", + "benchmark", + "--benchmark-group-by", + "func", + *session.posargs, + ) + + +@nox.session(python=["3.9", "3.10", "3.11", "3.12", "pypy3.9", "pypy3.10"]) +def tests(session: nox.Session) -> None: + session.install(".[tests]") + session.run( + "pytest", + "--cov", + "--cov-config=pyproject.toml", + "--cov-report=xml", + "--durations=10", + "--numprocesses=logical", + *session.posargs, + env={"COVERAGE_FILE": f".coverage.{session.python}"}, + ) + + +@nox.session +def lint(session: nox.Session) -> None: + session.install("pre-commit") + session.install("-e", ".[dev,vector]") + + args = *(session.posargs or ("--show-diff-on-failure",)), "--all-files" + session.run("pre-commit", "run", *args) + + +@nox.session +def build(session: nox.Session) -> None: + session.install("build", "twine", "uv") + session.run("python", "-m", "build", "--installer", "uv") + dists = glob.glob("dist/*") + session.run("twine", "check", *dists, silent=True) + + +@nox.session +def dev(session: nox.Session) -> None: + """Sets up a python development environment for the project.""" + args = session.posargs or ("venv",) + venv_dir = os.fsdecode(os.path.abspath(args[0])) + + session.log(f"Setting up virtual environment in {venv_dir}") + session.install("virtualenv") + session.run("virtualenv", venv_dir, silent=True) + + python = os.path.join(venv_dir, "bin/python") + session.run(python, "-m", "pip", "install", "-e", ".[dev]", external=True) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..73d07acee --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,254 @@ +[build-system] +requires = ["setuptools>=48", "setuptools_scm[toml]>=6.3.1"] +build-backend = "setuptools.build_meta" + +[project] +name = "datachain" +description = "Wrangle unstructured AI data at scale" +readme = "README.rst" +license = {text = "Apache-2.0"} +authors = [{name = "Dmitry Petrov", email = "support@dvc.org"}] +classifiers = [ + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Development Status :: 2 - Pre-Alpha" +] +requires-python = ">=3.9" +dynamic = ["version"] +dependencies = [ + "pyyaml", + "tomlkit", + "tqdm", + "numpy", + 'numpy>=1,<2; sys_platform == "win32"', + "pandas>=2.0.0", + "pyarrow", + "typing-extensions", + "python-dateutil>=2", + "attrs>=21.3.0", + "s3fs>=2024.2.0", + "gcsfs>=2024.2.0", + "adlfs>=2024.2.0", + "dvc-data>=3.10,<4", + "dvc-objects>=4,<6", + "shtab>=1.3.4,<2", + "sqlalchemy>=2", + "multiprocess==0.70.16", + "dill==0.3.8", + "ujson>=5.9.0", + "pydantic>=2,<3", + "jmespath>=1.0", + "datamodel-code-generator>=0.25" +] + +[project.optional-dependencies] +docs = [ + "mkdocs>=1.5.2", + "mkdocs-gen-files>=0.5.0", + "mkdocs-material>=9.3.1", + "mkdocs-section-index>=0.3.6", + "mkdocstrings-python>=1.6.3", + "mkdocs-literate-nav>=0.6.1" +] +cv = [ + "Pillow>=10.0.0,<11", + "torch>=2.1.0", + "torchvision", + "transformers>=4.36.0" +] +remote = [ + "datachain[pandas]", + "lz4", + "msgpack>=1.0.4,<2", + "requests>=2.22.0" +] +vector = [ + "usearch" +] +tests = [ + "datachain[cv,pandas,remote,vector]", + "pytest>=8,<9", + "pytest-sugar>=0.9.6", + "pytest-cov>=4.1.0", + "pytest-mock>=3.12.0", + "pytest-servers[all]>=0.5.4", + "pytest-benchmark[histogram]", + "pytest-asyncio>=0.23.2", + "pytest-xdist>=3.3.1", + "virtualenv", + "dulwich", + "hypothesis", + "open_clip_torch", + "aiotools>=1.7.0", + "requests-mock" +] +dev = [ + "datachain[docs,tests]", + "mypy==1.10.1", + "types-python-dateutil", + "types-PyYAML", + "types-requests", + "types-ujson" +] + +[project.urls] +Documentation = "https://datachain.dvc.ai" +Issues = "https://github.com/iterative/dvcx/issues" +Source = "https://github.com/iterative/dvcx" + +[project.scripts] +datachain = "datachain.cli:main" + +[tool.setuptools.packages.find] +exclude = ["tests", "tests.*"] +where = ["src"] +namespaces = false + +[tool.setuptools_scm] + +[tool.pytest.ini_options] +addopts = "-rfEs -m 'not benchmark'" +markers = [ + "benchmark: benchmarks.", + "e2e: End-to-end tests" +] +asyncio_mode = "auto" +filterwarnings = [ + "ignore:Field name .* shadows an attribute in parent:UserWarning" # datachain.lib.feature +] + +[tool.coverage.run] +branch = true +source = ["datachain", "tests"] + +[tool.coverage.paths] +source = ["src", "*/site-packages"] + +[tool.coverage.report] +show_missing = true +exclude_lines = [ + "pragma: no cover", + "if __name__ == .__main__.:", + "if typing.TYPE_CHECKING:", + "if TYPE_CHECKING:", + "raise NotImplementedError", + "raise AssertionError", + "@overload" +] + +[tool.mypy] +# Error output +show_column_numbers = true +show_error_codes = true +show_error_context = true +show_traceback = true +pretty = true +check_untyped_defs = false +# Warnings +warn_no_return = true +warn_redundant_casts = true +warn_unreachable = true +ignore_missing_imports = true +disable_error_code = "annotation-unchecked" +files = ["src", "tests"] + +[tool.codespell] +ignore-words-list = " " +skip = ["CODE_OF_CONDUCT.rst", "examples/**/*.ipynb", "tests/examples/wds_data.py"] + +[tool.ruff] +show-fixes = true + +[tool.ruff.lint] +preview = true +explicit-preview-rules = true +ignore = [ + "S101", # assert + "PLR2004", # magic-value-comparison + "PLW2901", # redefined-loop-name + "ISC001", # single-line-implicit-string-concatenation, incompatible with ruff format + "RET502", # implicit-return-value + "RET503", # implicit-return + "SIM105", # suppressible-exception + "SIM108", # if-else-block-instead-of-if-exp + "SIM117", # multiple-with-statements + "PERF203" # perflint - try-except-in-loop, irrelevant for Python>=3.11 +] +select = [ + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "C90", # mccabe + "W", # pycodestyle - Warning + "E", # pycodestyle - Error + "F", # pyflakes + "I", # isort + "T10", # flake8-debugger + "S", # flake8-bandit + "PL", # pylint + "TCH", # flake8-type-checking + "UP", # pyupgrade + "N", # pep8-naming + "YTT", # flake8-2020 + "ASYNC", # flake8-async + "EXE", # flake8-executable + "ISC", # flake8-implicit-str-concat + "ICN", # flake8-import-conventions + "PIE", # flake8-pie + "LOG", # flake8-logging + "G", # flake8-logging-format + "PYI", # flake8-pyi + "Q", # flake8-quotes + "SLOT", # flake8-slots + "PGH", # pygrep-hooks + "FLY", # flynt + "PERF", # perflint + "TID", # flake8-tidy-imports + "RSE", # flake8-raise + "INP", # flake8-no-pep420 + "RUF", # ruff rules + "BLE", # flake8-blind-except + "SIM", # flake8-simplify + "RSE", # flake8-raise + "RET", # flake8-return + "DTZ", # flake8-datetimez + "FURB", # refurb + "NPY", # numpy + "TRY004", # type-check-without-type-error + "TRY201", # verbose-raise + "TRY302", # useless-try-except + "TRY401", # verbose-log-message + "RUF022", # unsorted-dunder-all + "RUF023", # unsorted-dunder-slots + "RUF025", # unnecessary-dict-comprehension-for-iterable + "RUF027", # missing-f-string-syntax + "RUF030", # assert-with-print-message + "RUF101" # redirected-noqa +] + +[tool.ruff.lint.flake8-bugbear] +# Allow default arguments like, e.g., `data: List[str] = fastapi.Query(None)`. +extend-immutable-calls = ["datachain.storage.StorageURI"] + +[tool.ruff.lint.flake8-type-checking] +strict = true + +[tool.ruff.lint.isort] +known-first-party = ["datachain"] + +[tool.ruff.lint.pylint] +max-args = 16 +max-branches = 16 +max-public-methods = 32 +max-statements = 64 + +[tool.ruff.lint.mccabe] +max-complexity = 15 + +[tool.ruff.lint.per-file-ignores] +"examples/**" = ["INP001"] +"tests/scripts/**" = ["INP001"] +"tests/**" = ["DTZ"] +"tests/examples/wds_data.py" = ["E501"] diff --git a/src/datachain/__init__.py b/src/datachain/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/datachain/__main__.py b/src/datachain/__main__.py new file mode 100644 index 000000000..14108d3e3 --- /dev/null +++ b/src/datachain/__main__.py @@ -0,0 +1,6 @@ +if __name__ == "__main__": + import sys + + from .cli import main + + sys.exit(main()) diff --git a/src/datachain/asyn.py b/src/datachain/asyn.py new file mode 100644 index 000000000..637e18aad --- /dev/null +++ b/src/datachain/asyn.py @@ -0,0 +1,226 @@ +import asyncio +from collections.abc import Awaitable, Coroutine, Iterable +from concurrent.futures import ThreadPoolExecutor +from heapq import heappop, heappush +from typing import ( + Any, + Callable, + Generic, + Optional, + TypeVar, +) + +from fsspec.asyn import get_loop + +ASYNC_WORKERS = 20 + +InputT = TypeVar("InputT", contravariant=True) # noqa: PLC0105 +ResultT = TypeVar("ResultT", covariant=True) # noqa: PLC0105 + + +class AsyncMapper(Generic[InputT, ResultT]): + """ + Asynchronous unordered mapping iterable compatible with fsspec. + + `AsyncMapper(func, it)` is roughly equivalent to `map(func, it)`, except + that `func` is an async function, which is executed concurrently by up to + `workers` asyncio coroutines, and the results are yielded in arbitrary + order. + + If `func` needs to call synchronous functions that may themselves run coroutines + on the same loop, it must use `mapper.to_thread()`. + + Note that `loop`, which defaults to the fsspec loop, must be running on a different + thread than the one calling `mapper.iterate()`. + """ + + order_preserving = False + work_queue: "asyncio.Queue[InputT]" + loop: asyncio.AbstractEventLoop + + def __init__( + self, + func: Callable[[InputT], Awaitable[ResultT]], + iterable: Iterable[InputT], + *, + workers: int = ASYNC_WORKERS, + loop: Optional[asyncio.AbstractEventLoop] = None, + ): + self.func = func + self.iterable = iterable + self.workers = workers + self.loop = get_loop() if loop is None else loop + self.pool = ThreadPoolExecutor(workers) + self._tasks: set[asyncio.Task] = set() + + def start_task(self, coro: Coroutine) -> asyncio.Task: + task = self.loop.create_task(coro) + self._tasks.add(task) + task.add_done_callback(self._tasks.discard) + return task + + async def produce(self) -> None: + for item in self.iterable: + await self.work_queue.put(item) + + async def worker(self) -> None: + while (item := await self.work_queue.get()) is not None: + try: + result = await self.func(item) + await self.result_queue.put(result) + finally: + self.work_queue.task_done() + + async def init(self) -> None: + self.work_queue = asyncio.Queue(2 * self.workers) + self.result_queue: asyncio.Queue[Optional[ResultT]] = asyncio.Queue( + self.workers + ) + + async def run(self) -> None: + producer = self.start_task(self.produce()) + for _i in range(self.workers): + self.start_task(self.worker()) + try: + done, _pending = await asyncio.wait( + self._tasks, return_when=asyncio.FIRST_COMPLETED + ) + self.gather_exceptions(done) + assert producer.done() + join = self.start_task(self.work_queue.join()) + done, _pending = await asyncio.wait( + self._tasks, return_when=asyncio.FIRST_COMPLETED + ) + self.gather_exceptions(done) + assert join.done() + except: + await self.cancel_all() + await self._break_iteration() + raise + else: + await self.cancel_all() + await self._end_iteration() + + async def cancel_all(self) -> None: + if self._tasks: + for task in self._tasks: + task.cancel() + await asyncio.wait(self._tasks) + + def gather_exceptions(self, done_tasks): + # Check all exceptions to avoid "Task exception was never retrieved" warning + exceptions = [task.exception() for task in done_tasks] + # Raise the first exception found, if any. Additional ones are ignored. + for exc in exceptions: + if exc: + raise exc + + async def _pop_result(self) -> Optional[ResultT]: + return await self.result_queue.get() + + def next_result(self, timeout=None) -> Optional[ResultT]: + """ + Return the next available result. + + Blocks as long as the result queue is empty. + """ + future = asyncio.run_coroutine_threadsafe(self._pop_result(), self.loop) + return future.result(timeout=timeout) + + async def _end_iteration(self) -> None: + """Signal successful end of iteration.""" + await self.result_queue.put(None) + + async def _break_iteration(self) -> None: + """Signal that iteration must stop ASAP.""" + while not self.result_queue.empty(): + self.result_queue.get_nowait() + await self.result_queue.put(None) + + def iterate(self, timeout=None) -> Iterable[ResultT]: + init = asyncio.run_coroutine_threadsafe(self.init(), self.loop) + init.result(timeout=1) + async_run = asyncio.run_coroutine_threadsafe(self.run(), self.loop) + try: + while True: + if (result := self.next_result(timeout)) is not None: + yield result + else: + break + if exc := async_run.exception(): + raise exc + finally: + if not async_run.done(): + async_run.cancel() + + def __iter__(self): + return self.iterate() + + async def to_thread(self, func, *args): + return await self.loop.run_in_executor(self.pool, func, *args) + + +class OrderedMapper(AsyncMapper[InputT, ResultT]): + """ + Asynchronous ordered mapping iterable compatible with fsspec. + + See `AsyncMapper` for details. + """ + + order_preserving = True + + def __init__( + self, + func: Callable[[InputT], Awaitable[ResultT]], + iterable: Iterable[InputT], + *, + workers: int = ASYNC_WORKERS, + loop: Optional[asyncio.AbstractEventLoop] = None, + ): + super().__init__(func, iterable, workers=workers, loop=loop) + self._waiters: dict[int, Any] = {} + self._getters: dict[int, asyncio.Future[Optional[ResultT]]] = {} + self.heap: list[tuple[int, Optional[ResultT]]] = [] + self._next_yield = 0 + self._items_seen = 0 + self._window = 2 * workers + + def _push_result(self, i: int, result: Optional[ResultT]) -> None: + if i in self._getters: + future = self._getters.pop(i) + future.set_result(result) + else: + heappush(self.heap, (i, result)) + + async def worker(self) -> None: + while (item := await self.work_queue.get()) is not None: + i = self._items_seen + self._items_seen += 1 + if i >= self._next_yield + self._window: + event = self._waiters[i - self._window] = asyncio.Event() + await event.wait() + result = await self.func(item) + self._push_result(i, result) + self.work_queue.task_done() + + async def init(self) -> None: + self.work_queue = asyncio.Queue(2 * self.workers) + + async def _pop_result(self) -> Optional[ResultT]: + if self.heap and self.heap[0][0] == self._next_yield: + _i, out = heappop(self.heap) + else: + self._getters[self._next_yield] = get_value = self.loop.create_future() + out = await get_value + if self._next_yield in self._waiters: + event = self._waiters.pop(self._next_yield) + event.set() + self._next_yield += 1 + return out + + async def _end_iteration(self) -> None: + self._push_result(self._next_yield + len(self.heap), None) + + async def _break_iteration(self) -> None: + self.heap = [] + self._push_result(self._next_yield, None) diff --git a/src/datachain/cache.py b/src/datachain/cache.py new file mode 100644 index 000000000..ed40cc7ad --- /dev/null +++ b/src/datachain/cache.py @@ -0,0 +1,146 @@ +import hashlib +import json +import os +from functools import partial +from typing import TYPE_CHECKING, Optional + +import attrs +from dvc_data.hashfile.db.local import LocalHashFileDB +from dvc_objects.fs.local import LocalFileSystem +from fsspec.callbacks import Callback, TqdmCallback + +from .progress import Tqdm + +if TYPE_CHECKING: + from datachain.client import Client + from datachain.storage import StorageURI + +sha256 = partial(hashlib.sha256, usedforsecurity=False) + + +@attrs.frozen +class UniqueId: + storage: "StorageURI" + parent: str + name: str + etag: str + size: int + vtype: str + location: Optional[str] + + @property + def path(self) -> str: + return f"{self.parent}/{self.name}" if self.parent else self.name + + def get_parsed_location(self) -> Optional[dict]: + if not self.location: + return None + + loc_stack = ( + json.loads(self.location) + if isinstance(self.location, str) + else self.location + ) + if len(loc_stack) > 1: + raise NotImplementedError("Nested v-objects are not supported yet.") + + return loc_stack[0] + + def get_hash(self) -> str: + etag = f"{self.vtype}{self.location}" if self.vtype else self.etag + return sha256( + f"{self.storage}/{self.parent}/{self.name}/{etag}".encode() + ).hexdigest() + + +def try_scandir(path): + try: + with os.scandir(path) as it: + yield from it + except OSError: + pass + + +class DataChainCache: + def __init__(self, cache_dir: str, tmp_dir: str): + self.odb = LocalHashFileDB( + LocalFileSystem(), + cache_dir, + tmp_dir=tmp_dir, + ) + + @property + def cache_dir(self): + return self.odb.path + + @property + def tmp_dir(self): + return self.odb.tmp_dir + + def get_path(self, uid: UniqueId) -> Optional[str]: + if self.contains(uid): + return self.path_from_checksum(uid.get_hash()) + return None + + def contains(self, uid: UniqueId) -> bool: + return self.odb.exists(uid.get_hash()) + + def path_from_checksum(self, checksum: str) -> str: + assert checksum + return self.odb.oid_to_path(checksum) + + def remove(self, uid: UniqueId) -> None: + self.odb.delete(uid.get_hash()) + + async def download( + self, uid: UniqueId, client: "Client", callback: Optional[Callback] = None + ) -> None: + from_path = f"{uid.storage}/{uid.path}" + from dvc_objects.fs.utils import tmp_fname + + odb_fs = self.odb.fs + tmp_info = odb_fs.join(self.odb.tmp_dir, tmp_fname()) # type: ignore[arg-type] + size = uid.size + if size < 0: + size = await client.get_size(from_path) + cb = callback or TqdmCallback( + tqdm_kwargs={"desc": odb_fs.name(from_path), "bytes": True}, + tqdm_cls=Tqdm, + size=size, + ) + try: + await client.get_file(from_path, tmp_info, callback=cb) + finally: + if not callback: + cb.close() + + try: + oid = uid.get_hash() + self.odb.add(tmp_info, self.odb.fs, oid) + finally: + os.unlink(tmp_info) + + def store_data(self, uid: UniqueId, contents: bytes) -> None: + checksum = uid.get_hash() + dst = self.path_from_checksum(checksum) + if not os.path.exists(dst): + # Create the file only if it's not already in cache + os.makedirs(os.path.dirname(dst), exist_ok=True) + with open(dst, mode="wb") as f: + f.write(contents) + + def clear(self): + """ + Completely clear the cache. + """ + self.odb.clear() + + def get_total_size(self) -> int: + total = 0 + for subdir in try_scandir(self.odb.path): + for file in try_scandir(subdir): + try: + total += file.stat().st_size + except OSError: + pass + return total diff --git a/src/datachain/catalog/__init__.py b/src/datachain/catalog/__init__.py new file mode 100644 index 000000000..498142c1a --- /dev/null +++ b/src/datachain/catalog/__init__.py @@ -0,0 +1,17 @@ +from .catalog import ( + QUERY_DATASET_PREFIX, + QUERY_SCRIPT_CANCELED_EXIT_CODE, + QUERY_SCRIPT_INVALID_LAST_STATEMENT_EXIT_CODE, + Catalog, + parse_edatachain_file, +) +from .loader import get_catalog + +__all__ = [ + "QUERY_DATASET_PREFIX", + "QUERY_SCRIPT_CANCELED_EXIT_CODE", + "QUERY_SCRIPT_INVALID_LAST_STATEMENT_EXIT_CODE", + "Catalog", + "get_catalog", + "parse_edatachain_file", +] diff --git a/src/datachain/catalog/catalog.py b/src/datachain/catalog/catalog.py new file mode 100644 index 000000000..c5be5ed14 --- /dev/null +++ b/src/datachain/catalog/catalog.py @@ -0,0 +1,2346 @@ +import ast +import io +import json +import logging +import math +import os +import os.path +import posixpath +import subprocess +import sys +import tempfile +import time +import traceback +from collections.abc import Iterable, Iterator, Mapping, Sequence +from contextlib import contextmanager, nullcontext +from copy import copy +from dataclasses import dataclass +from functools import cached_property, reduce +from random import shuffle +from threading import Thread +from typing import ( + IO, + TYPE_CHECKING, + Any, + Callable, + NamedTuple, + NoReturn, + Optional, + Union, +) +from uuid import uuid4 + +import requests +import sqlalchemy as sa +import yaml +from sqlalchemy import Column +from tqdm import tqdm + +from datachain.cache import DataChainCache, UniqueId +from datachain.client import Client +from datachain.config import get_remote_config, read_config +from datachain.dataset import ( + DATASET_PREFIX, + QUERY_DATASET_PREFIX, + DatasetDependency, + DatasetRecord, + DatasetStats, + DatasetStatus, + RowDict, + create_dataset_uri, + parse_dataset_uri, +) +from datachain.error import ( + ClientError, + DataChainError, + DatasetInvalidVersionError, + DatasetNotFoundError, + PendingIndexingError, + QueryScriptCancelError, + QueryScriptCompileError, + QueryScriptDatasetNotFound, + QueryScriptRunError, +) +from datachain.listing import Listing +from datachain.node import DirType, Node, NodeWithPath +from datachain.nodes_thread_pool import NodesThreadPool +from datachain.remote.studio import StudioClient +from datachain.sql.types import JSON, Boolean, DateTime, Int, Int64, SQLType, String +from datachain.storage import Storage, StorageStatus, StorageURI +from datachain.utils import ( + DataChainDir, + batched, + datachain_paths_join, + import_object, + parse_params_string, +) + +from .datasource import DataSource +from .subclass import SubclassFinder + +if TYPE_CHECKING: + from datachain.data_storage import ( + AbstractIDGenerator, + AbstractMetastore, + AbstractWarehouse, + ) + +logger = logging.getLogger("datachain") + +DEFAULT_DATASET_DIR = "dataset" +DATASET_FILE_SUFFIX = ".edatachain" +FEATURE_CLASSES = ["Feature"] + +TTL_INT = 4 * 60 * 60 + +INDEX_INTERNAL_ERROR_MESSAGE = "Internal error on indexing" +DATASET_INTERNAL_ERROR_MESSAGE = "Internal error on creating dataset" +# exit code we use if last statement in query script is not instance of DatasetQuery +QUERY_SCRIPT_INVALID_LAST_STATEMENT_EXIT_CODE = 10 +# exit code we use if query script was canceled +QUERY_SCRIPT_CANCELED_EXIT_CODE = 11 + +# dataset pull +PULL_DATASET_MAX_THREADS = 10 +PULL_DATASET_CHUNK_TIMEOUT = 3600 +PULL_DATASET_SLEEP_INTERVAL = 0.1 # sleep time while waiting for chunk to be available +PULL_DATASET_CHECK_STATUS_INTERVAL = 20 # interval to check export status in Studio + + +def _raise_remote_error(error_message: str) -> NoReturn: + raise DataChainError(f"Error from server: {error_message}") + + +def noop(_: str): + pass + + +@contextmanager +def print_and_capture( + stream: "IO[str]", callback: Callable[[str], None] = noop +) -> "Iterator[list[str]]": + lines: list[str] = [] + append = lines.append + + def loop() -> None: + for line in iter(stream.readline, ""): + print(line, end="") + callback(line) + append(line) + + thread = Thread(target=loop, daemon=True) + thread.start() + + try: + yield lines + finally: + thread.join() + + +class QueryResult(NamedTuple): + dataset: Optional[DatasetRecord] + version: Optional[int] + output: str + preview: Optional[list[dict]] + metrics: dict[str, Any] + + +class DatasetRowsFetcher(NodesThreadPool): + def __init__( + self, + metastore: "AbstractMetastore", + warehouse: "AbstractWarehouse", + remote_config: dict[str, Any], + dataset_name: str, + dataset_version: int, + schema: dict[str, Union[SQLType, type[SQLType]]], + max_threads: int = PULL_DATASET_MAX_THREADS, + ): + super().__init__(max_threads) + self._check_dependencies() + self.metastore = metastore + self.warehouse = warehouse + self.dataset_name = dataset_name + self.dataset_version = dataset_version + self.schema = schema + self.last_status_check: Optional[float] = None + + self.studio_client = StudioClient( + remote_config["url"], remote_config["username"], remote_config["token"] + ) + + def done_task(self, done): + for task in done: + task.result() + + def _check_dependencies(self) -> None: + try: + import lz4.frame # noqa: F401 + import numpy as np # noqa: F401 + import pandas as pd # noqa: F401 + import pyarrow as pa # noqa: F401 + except ImportError as exc: + raise Exception( + f"Missing dependency: {exc.name}\n" + "To install run:\n" + "\tpip install 'datachain[remote]'" + ) from None + + def should_check_for_status(self) -> bool: + if not self.last_status_check: + return True + return time.time() - self.last_status_check > PULL_DATASET_CHECK_STATUS_INTERVAL + + def check_for_status(self) -> None: + """ + Method that checks export status in Studio and raises Exception if export + failed or was removed. + Checks are done every PULL_DATASET_CHECK_STATUS_INTERVAL seconds + """ + export_status_response = self.studio_client.dataset_export_status( + self.dataset_name, self.dataset_version + ) + if not export_status_response.ok: + _raise_remote_error(export_status_response.message) + + export_status = export_status_response.data["status"] # type: ignore [index] + + if export_status == "failed": + _raise_remote_error("Dataset export failed in Studio") + if export_status == "removed": + _raise_remote_error("Dataset export removed in Studio") + + self.last_status_check = time.time() + + def fix_columns(self, df) -> None: + import pandas as pd + + """ + Method that does various column decoding or parsing, depending on a type + before inserting into DB + """ + # we get dataframe from parquet export files where datetimes are serialized + # as timestamps so we need to parse it back to datetime objects + for c in [c for c, t in self.schema.items() if t == DateTime]: + df[c] = pd.to_datetime(df[c], unit="s") + + # strings are represented as binaries in parquet export so need to + # decode it back to strings + for c in [c for c, t in self.schema.items() if t == String]: + df[c] = df[c].str.decode("utf-8") + + def do_task(self, urls): + import lz4.frame + import pandas as pd + + metastore = self.metastore.clone() # metastore is not thread safe + warehouse = self.warehouse.clone() # warehouse is not thread safe + dataset = metastore.get_dataset(self.dataset_name) + + urls = list(urls) + while urls: + for url in urls: + if self.should_check_for_status(): + self.check_for_status() + + r = requests.get(url, timeout=PULL_DATASET_CHUNK_TIMEOUT) + if r.status_code == 404: + time.sleep(PULL_DATASET_SLEEP_INTERVAL) + # moving to the next url + continue + + r.raise_for_status() + + df = pd.read_parquet(io.BytesIO(lz4.frame.decompress(r.content))) + + self.fix_columns(df) + + # id will be autogenerated in DB + df = df.drop("id", axis=1) + + inserted = warehouse.insert_dataset_rows( + df, dataset, self.dataset_version + ) + self.increase_counter(inserted) # type: ignore [arg-type] + urls.remove(url) + + +@dataclass +class NodeGroup: + """Class for a group of nodes from the same source""" + + listing: Listing + sources: list[DataSource] + + # The source path within the bucket + # (not including the bucket name or s3:// prefix) + source_path: str = "" + is_edatachain: bool = False + dataset_name: Optional[str] = None + dataset_version: Optional[int] = None + instantiated_nodes: Optional[list[NodeWithPath]] = None + + @property + def is_dataset(self) -> bool: + return bool(self.dataset_name) + + def iternodes(self, recursive: bool = False): + for src in self.sources: + if recursive and src.is_container(): + for nwp in src.find(): + yield nwp.n + else: + yield src.node + + def download(self, recursive: bool = False, pbar=None) -> None: + """ + Download this node group to cache. + """ + if self.sources: + self.listing.client.fetch_nodes( + self.iternodes(recursive), shared_progress_bar=pbar + ) + + +def check_output_dataset_file( + output: str, + force: bool = False, + dataset_filename: Optional[str] = None, + skip_check_edatachain: bool = False, +) -> str: + """ + Checks the dataset filename for existence or if it should be force-overwritten. + """ + dataset_file = ( + dataset_filename if dataset_filename else output + DATASET_FILE_SUFFIX + ) + if not skip_check_edatachain and os.path.exists(dataset_file): + if force: + os.remove(dataset_file) + else: + raise RuntimeError(f"Output dataset file already exists: {dataset_file}") + return dataset_file + + +def parse_edatachain_file(filename: str) -> list[dict[str, Any]]: + with open(filename, encoding="utf-8") as f: + contents = yaml.safe_load(f) + + if not isinstance(contents, list): + contents = [contents] + + for entry in contents: + if not isinstance(entry, dict): + raise TypeError( + "Failed parsing EDataChain file, " + "each data source entry must be a dictionary" + ) + if "data-source" not in entry or "files" not in entry: + raise ValueError( + "Failed parsing EDataChain file, " + "each data source entry must contain the " + '"data-source" and "files" keys' + ) + + return contents + + +def prepare_output_for_cp( + node_groups: list[NodeGroup], + output: str, + force: bool = False, + edatachain_only: bool = False, + no_edatachain_file: bool = False, +) -> tuple[bool, Optional[str]]: + total_node_count = 0 + for node_group in node_groups: + if not node_group.sources: + raise FileNotFoundError( + f"No such file or directory: {node_group.source_path}" + ) + total_node_count += len(node_group.sources) + + always_copy_dir_contents = False + copy_to_filename = None + + if edatachain_only: + return always_copy_dir_contents, copy_to_filename + + if not os.path.isdir(output): + if all(n.is_dataset for n in node_groups): + os.mkdir(output) + elif total_node_count == 1: + first_source = node_groups[0].sources[0] + if first_source.is_container(): + if os.path.exists(output): + if force: + os.remove(output) + else: + raise FileExistsError(f"Path already exists: {output}") + always_copy_dir_contents = True + os.mkdir(output) + else: # Is a File + if os.path.exists(output): + if force: + os.remove(output) + else: + raise FileExistsError(f"Path already exists: {output}") + copy_to_filename = output + else: + raise FileNotFoundError(f"Is not a directory: {output}") + + if copy_to_filename and not no_edatachain_file: + raise RuntimeError("File to file cp not supported with .edatachain files!") + + return always_copy_dir_contents, copy_to_filename + + +def collect_nodes_for_cp( + node_groups: Iterable[NodeGroup], + recursive: bool = False, +) -> tuple[int, int]: + total_size: int = 0 + total_files: int = 0 + + # Collect all sources to process + for node_group in node_groups: + listing: Listing = node_group.listing + valid_sources: list[DataSource] = [] + for dsrc in node_group.sources: + if dsrc.is_single_object(): + total_size += dsrc.node.size + total_files += 1 + valid_sources.append(dsrc) + else: + node = dsrc.node + if not recursive: + print(f"{node.full_path} is a directory (not copied).") + continue + add_size, add_files = listing.du(node, count_files=True) + total_size += add_size + total_files += add_files + valid_sources.append(dsrc) + + node_group.sources = valid_sources + + return total_size, total_files + + +def get_download_bar(bar_format: str, total_size: int): + return tqdm( + desc="Downloading files: ", + unit="B", + bar_format=bar_format, + unit_scale=True, + unit_divisor=1000, + total=total_size, + ) + + +def instantiate_node_groups( + node_groups: Iterable[NodeGroup], + output: str, + bar_format: str, + total_files: int, + force: bool = False, + recursive: bool = False, + virtual_only: bool = False, + always_copy_dir_contents: bool = False, + copy_to_filename: Optional[str] = None, +) -> None: + instantiate_progress_bar = ( + None + if virtual_only + else tqdm( + desc=f"Instantiating {output}: ", + unit=" f", + bar_format=bar_format, + unit_scale=True, + unit_divisor=1000, + total=total_files, + ) + ) + + output_dir = output + if copy_to_filename: + output_dir = os.path.dirname(output) + if not output_dir: + output_dir = "." + + # Instantiate these nodes + for node_group in node_groups: + if not node_group.sources: + continue + listing: Listing = node_group.listing + source_path: str = node_group.source_path + + copy_dir_contents = always_copy_dir_contents or source_path.endswith("/") + instantiated_nodes = listing.collect_nodes_to_instantiate( + node_group.sources, + copy_to_filename, + recursive, + copy_dir_contents, + source_path, + node_group.is_edatachain, + node_group.is_dataset, + ) + if not virtual_only: + listing.instantiate_nodes( + instantiated_nodes, + output_dir, + total_files, + force=force, + shared_progress_bar=instantiate_progress_bar, + ) + node_group.instantiated_nodes = instantiated_nodes + if instantiate_progress_bar: + instantiate_progress_bar.close() + + +def compute_metafile_data(node_groups) -> list[dict[str, Any]]: + metafile_data = [] + for node_group in node_groups: + if not node_group.sources: + continue + listing: Listing = node_group.listing + source_path: str = node_group.source_path + if not node_group.is_dataset: + assert listing.storage + data_source = listing.storage.to_dict(source_path) + else: + data_source = {"uri": listing.metastore.uri} + + metafile_group = {"data-source": data_source, "files": []} + for node in node_group.instantiated_nodes: + if not node.n.is_dir: + metafile_group["files"].append(node.get_metafile_data()) + if metafile_group["files"]: + metafile_data.append(metafile_group) + + return metafile_data + + +def find_column_to_str( # noqa: PLR0911 + row: tuple[Any, ...], field_lookup: dict[str, int], src: DataSource, column: str +) -> str: + if column == "du": + return str( + src.listing.du( + { + f: row[field_lookup[f]] + for f in ["dir_type", "size", "parent", "name"] + } + )[0] + ) + if column == "name": + return row[field_lookup["name"]] or "" + if column == "owner": + return row[field_lookup["owner_name"]] or "" + if column == "path": + is_dir = row[field_lookup["dir_type"]] == DirType.DIR + parent = row[field_lookup["parent"]] + name = row[field_lookup["name"]] + path = f"{parent}/{name}" if parent else name + if is_dir and path: + full_path = path + "/" + else: + full_path = path + return src.get_node_full_path_from_path(full_path) + if column == "size": + return str(row[field_lookup["size"]]) + if column == "type": + dt = row[field_lookup["dir_type"]] + if dt == DirType.DIR: + return "d" + if dt == DirType.FILE: + return "f" + if dt == DirType.TAR_ARCHIVE: + return "t" + # Unknown - this only happens if a type was added elsewhere but not here + return "u" + return "" + + +def form_module_source(source_ast): + module = ast.Module(body=source_ast, type_ignores=[]) + module = ast.fix_missing_locations(module) + return ast.unparse(module) + + +class Catalog: + def __init__( + self, + id_generator: "AbstractIDGenerator", + metastore: "AbstractMetastore", + warehouse: "AbstractWarehouse", + cache_dir=None, + tmp_dir=None, + client_config: Optional[dict[str, Any]] = None, + warehouse_ready_callback: Optional[ + Callable[["AbstractWarehouse"], None] + ] = None, + ): + datachain_dir = DataChainDir(cache=cache_dir, tmp=tmp_dir) + datachain_dir.init() + self.id_generator = id_generator + self.metastore = metastore + self._warehouse = warehouse + self.cache = DataChainCache(datachain_dir.cache, datachain_dir.tmp) + self.client_config = client_config if client_config is not None else {} + self._init_params = { + "cache_dir": cache_dir, + "tmp_dir": tmp_dir, + } + self._warehouse_ready_callback = warehouse_ready_callback + + @cached_property + def warehouse(self) -> "AbstractWarehouse": + if self._warehouse_ready_callback: + self._warehouse_ready_callback(self._warehouse) + + return self._warehouse + + def get_init_params(self) -> dict[str, Any]: + return { + **self._init_params, + "client_config": self.client_config, + } + + def copy(self, cache=True, db=True): + result = copy(self) + if not db: + result.id_generator = None + result.metastore = None + result._warehouse = None + result.warehouse = None + return result + + @classmethod + def generate_query_dataset_name(cls) -> str: + return f"{QUERY_DATASET_PREFIX}_{uuid4().hex}" + + def attach_query_wrapper(self, code_ast): + if code_ast.body: + last_expr = code_ast.body[-1] + if isinstance(last_expr, ast.Expr): + new_expressions = [ + ast.Import( + names=[ast.alias(name="datachain.query.dataset", asname=None)] + ), + ast.Expr( + value=ast.Call( + func=ast.Attribute( + value=ast.Attribute( + value=ast.Attribute( + value=ast.Name(id="datachain", ctx=ast.Load()), + attr="query", + ctx=ast.Load(), + ), + attr="dataset", + ctx=ast.Load(), + ), + attr="query_wrapper", + ctx=ast.Load(), + ), + args=[last_expr], + keywords=[], + ) + ), + ] + code_ast.body[-1:] = new_expressions + else: + raise Exception("Last line in a script was not an expression") + return code_ast + + def compile_query_script( + self, script: str, feature_module_name: str + ) -> tuple[Union[str, None], str]: + code_ast = ast.parse(script) + code_ast = self.attach_query_wrapper(code_ast) + finder = SubclassFinder(FEATURE_CLASSES) + finder.visit(code_ast) + + if not finder.feature_class: + main_module = form_module_source([*finder.imports, *finder.main_body]) + return None, main_module + + feature_import = ast.ImportFrom( + module=feature_module_name, + names=[ast.alias(name="*", asname=None)], + level=0, + ) + feature_module = form_module_source([*finder.imports, *finder.feature_class]) + main_module = form_module_source( + [*finder.imports, feature_import, *finder.main_body] + ) + + return feature_module, main_module + + def parse_url(self, uri: str, **config: Any) -> tuple[Client, str]: + config = config or self.client_config + return Client.parse_url(uri, self.metastore, self.cache, **config) + + def get_client(self, uri: StorageURI, **config: Any) -> Client: + """ + Return the client corresponding to the given source `uri`. + """ + config = config or self.client_config + cls = Client.get_implementation(uri) + return cls.from_source(uri, self.cache, **config) + + def enlist_source( + self, + source: str, + ttl: int, + force_update=False, + skip_indexing=False, + client_config=None, + ) -> tuple[Listing, str]: + if force_update and skip_indexing: + raise ValueError( + "Both force_update and skip_indexing flags" + " cannot be True at the same time" + ) + + partial_id: Optional[int] + partial_path: Optional[str] + + client_config = client_config or self.client_config + client, path = self.parse_url(source, **client_config) + prefix = posixpath.dirname(path) + storage_dataset_name = Storage.dataset_name( + client.uri, posixpath.join(prefix, "") + ) + source_metastore = self.metastore.clone(client.uri) + source_warehouse = self.warehouse.clone() + + columns = [ + Column("vtype", String), + Column("dir_type", Int), + Column("parent", String), + Column("name", String), + Column("etag", String), + Column("version", String), + Column("is_latest", Boolean), + Column("last_modified", DateTime(timezone=True)), + Column("size", Int64), + Column("owner_name", String), + Column("owner_id", String), + Column("location", JSON), + Column("source", String), + ] + + if skip_indexing: + source_metastore.create_storage_if_not_registered(client.uri) + storage = source_metastore.get_storage(client.uri) + source_metastore.init_partial_id(client.uri) + partial_id = source_metastore.get_next_partial_id(client.uri) + + source_metastore = self.metastore.clone( + uri=client.uri, partial_id=partial_id + ) + source_metastore.init(client.uri) + + source_warehouse = self.warehouse.clone() + dataset = self.create_dataset( + storage_dataset_name, columns=columns, listing=True + ) + + return ( + Listing(storage, source_metastore, source_warehouse, client, dataset), + path, + ) + + ( + storage, + need_index, + in_progress, + partial_id, + partial_path, + ) = source_metastore.register_storage_for_indexing( + client.uri, force_update, prefix + ) + if in_progress: + raise PendingIndexingError(f"Pending indexing operation: uri={storage.uri}") + + if not need_index: + assert partial_id is not None + assert partial_path is not None + source_metastore = self.metastore.clone( + uri=client.uri, partial_id=partial_id + ) + source_warehouse = self.warehouse.clone() + dataset = self.get_dataset(Storage.dataset_name(client.uri, partial_path)) + lst = Listing(storage, source_metastore, source_warehouse, client, dataset) + logger.debug( + "Using cached listing %s. Valid till: %s", + storage.uri, + storage.expires_to_local, + ) + # Listing has to have correct version of data storage + # initialized with correct Storage + + self.update_dataset_version_with_warehouse_info( + dataset, + dataset.latest_version, + ) + + return lst, path + + source_metastore.init_partial_id(client.uri) + partial_id = source_metastore.get_next_partial_id(client.uri) + + source_metastore.init(client.uri) + source_metastore = self.metastore.clone(uri=client.uri, partial_id=partial_id) + + source_warehouse = self.warehouse.clone() + + dataset = self.create_dataset( + storage_dataset_name, columns=columns, listing=True + ) + + lst = Listing(storage, source_metastore, source_warehouse, client, dataset) + + try: + lst.fetch(prefix) + + source_metastore.mark_storage_indexed( + storage.uri, + StorageStatus.PARTIAL if prefix else StorageStatus.COMPLETE, + ttl, + prefix=prefix, + partial_id=partial_id, + dataset=dataset, + ) + + self.update_dataset_version_with_warehouse_info( + dataset, + dataset.latest_version, + ) + + except ClientError as e: + # for handling cloud errors + error_message = INDEX_INTERNAL_ERROR_MESSAGE + if e.error_code in ["InvalidAccessKeyId", "SignatureDoesNotMatch"]: + error_message = "Invalid cloud credentials" + + source_metastore.mark_storage_indexed( + storage.uri, + StorageStatus.FAILED, + ttl, + prefix=prefix, + error_message=error_message, + error_stack=traceback.format_exc(), + dataset=dataset, + ) + self._remove_dataset_rows_and_warehouse_info( + dataset, dataset.latest_version + ) + raise + except: + source_metastore.mark_storage_indexed( + storage.uri, + StorageStatus.FAILED, + ttl, + prefix=prefix, + error_message=INDEX_INTERNAL_ERROR_MESSAGE, + error_stack=traceback.format_exc(), + dataset=dataset, + ) + self._remove_dataset_rows_and_warehouse_info( + dataset, dataset.latest_version + ) + raise + + lst.storage = storage + + return lst, path + + def _remove_dataset_rows_and_warehouse_info( + self, dataset: DatasetRecord, version: int, **kwargs + ): + self.warehouse.drop_dataset_rows_table(dataset, version) + self.update_dataset_version_with_warehouse_info( + dataset, + version, + rows_dropped=True, + **kwargs, + ) + + def enlist_sources( + self, + sources: list[str], + ttl: int, + update: bool, + skip_indexing=False, + client_config=None, + only_index=False, + ) -> Optional[list["DataSource"]]: + enlisted_sources = [] + for src in sources: # Opt: parallel + listing, file_path = self.enlist_source( + src, + ttl, + update, + skip_indexing=skip_indexing, + client_config=client_config or self.client_config, + ) + enlisted_sources.append((listing, file_path)) + + if only_index: + # sometimes we don't really need listing result (e.g on indexing process) + # so this is to improve performance + return None + + dsrc_all: list[DataSource] = [] + for listing, file_path in enlisted_sources: + nodes = listing.expand_path(file_path) + dir_only = file_path.endswith("/") + dsrc_all.extend(DataSource(listing, node, dir_only) for node in nodes) + return dsrc_all + + def enlist_sources_grouped( + self, + sources: list[str], + ttl: int, + update: bool, + no_glob: bool = False, + client_config=None, + ) -> list[NodeGroup]: + from datachain.query import DatasetQuery + + def _row_to_node(d: dict[str, Any]) -> Node: + del d["source"] + return Node.from_dict(d) + + enlisted_sources: list[tuple[bool, bool, Any]] = [] + client_config = client_config or self.client_config + for src in sources: # Opt: parallel + if src.endswith(DATASET_FILE_SUFFIX) and os.path.isfile(src): + # TODO: Also allow using EDataChain files from cloud locations? + edatachain_data = parse_edatachain_file(src) + indexed_sources = [] + for ds in edatachain_data: + listing, source_path = self.enlist_source( + ds["data-source"]["uri"], + ttl, + update, + client_config=client_config, + ) + paths = datachain_paths_join( + source_path, (f["name"] for f in ds["files"]) + ) + indexed_sources.append((listing, source_path, paths)) + enlisted_sources.append((True, False, indexed_sources)) + elif src.startswith("ds://"): + ds_name, ds_version = parse_dataset_uri(src) + dataset = self.get_dataset(ds_name) + if not ds_version: + ds_version = dataset.latest_version + dataset_sources = self.warehouse.get_dataset_sources( + dataset, + ds_version, + ) + indexed_sources = [] + for source in dataset_sources: + client = self.get_client(source, **client_config) + uri = client.uri + ms = self.metastore.clone(uri, None) + st = self.warehouse.clone() + listing = Listing(None, ms, st, client, None) + rows = ( + DatasetQuery( + name=dataset.name, version=ds_version, catalog=self + ) + .select() + .to_records() + ) + indexed_sources.append( + ( + listing, + source, + [_row_to_node(r) for r in rows], + ds_name, + ds_version, + ) # type: ignore [arg-type] + ) + + enlisted_sources.append((False, True, indexed_sources)) + else: + listing, source_path = self.enlist_source( + src, ttl, update, client_config=client_config + ) + enlisted_sources.append((False, False, (listing, source_path))) + + node_groups = [] + for is_datachain, is_dataset, payload in enlisted_sources: # Opt: parallel + if is_dataset: + for ( + listing, + source_path, + nodes, + dataset_name, + dataset_version, + ) in payload: + dsrc = [DataSource(listing, node) for node in nodes] + node_groups.append( + NodeGroup( + listing, + dsrc, + source_path, + dataset_name=dataset_name, + dataset_version=dataset_version, + ) + ) + elif is_datachain: + for listing, source_path, paths in payload: + dsrc = [DataSource(listing, listing.resolve_path(p)) for p in paths] + node_groups.append( + NodeGroup(listing, dsrc, source_path, is_edatachain=True) + ) + else: + listing, source_path = payload + as_container = source_path.endswith("/") + dsrc = [ + DataSource(listing, n, as_container) + for n in listing.expand_path(source_path, use_glob=not no_glob) + ] + node_groups.append(NodeGroup(listing, dsrc, source_path)) + + return node_groups + + def unlist_source(self, uri: StorageURI) -> None: + self.metastore.clone(uri=uri).mark_storage_not_indexed(uri) + + def storage_stats(self, uri: StorageURI) -> Optional[DatasetStats]: + """ + Returns tuple with storage stats: total number of rows and total dataset size. + """ + partial_path = self.metastore.get_last_partial_path(uri) + if partial_path is None: + return None + dataset = self.get_dataset(Storage.dataset_name(uri, partial_path)) + + return self.dataset_stats(dataset.name, dataset.latest_version) + + def create_dataset( + self, + name: str, + version: Optional[int] = None, + *, + columns: Sequence[Column], + feature_schema: Optional[dict] = None, + query_script: str = "", + create_rows: Optional[bool] = True, + validate_version: Optional[bool] = True, + listing: Optional[bool] = False, + ) -> "DatasetRecord": + """ + Creates new dataset of a specific version. + If dataset is not yet created, it will create it with version 1 + If version is None, then next unused version is created. + If version is given, then it must be an unused version number. + """ + assert [c.name for c in columns if c.name != "id"], f"got {columns=}" + if not listing and Client.is_data_source_uri(name): + raise RuntimeError( + "Cannot create dataset that starts with source prefix, e.g s3://" + ) + default_version = 1 + try: + dataset = self.get_dataset(name) + default_version = dataset.next_version + except DatasetNotFoundError: + schema = { + c.name: c.type.to_dict() for c in columns if isinstance(c.type, SQLType) + } + dataset = self.metastore.create_dataset( + name, + feature_schema=feature_schema, + query_script=query_script, + schema=schema, + ignore_if_exists=True, + ) + + version = version or default_version + + if dataset.has_version(version): + raise DatasetInvalidVersionError( + f"Version {version} already exists in dataset {name}" + ) + + if validate_version and not dataset.is_valid_next_version(version): + raise DatasetInvalidVersionError( + f"Version {version} must be higher than the current latest one" + ) + + return self.create_new_dataset_version( + dataset, + version, + feature_schema=feature_schema, + query_script=query_script, + create_rows_table=create_rows, + columns=columns, + ) + + def create_new_dataset_version( + self, + dataset: DatasetRecord, + version: int, + *, + columns: Sequence[Column], + sources="", + feature_schema=None, + query_script="", + error_message="", + error_stack="", + script_output="", + create_rows_table=True, + job_id: Optional[str] = None, + is_job_result: bool = False, + ) -> DatasetRecord: + """ + Creates dataset version if it doesn't exist. + If create_rows is False, dataset rows table will not be created + """ + assert [c.name for c in columns if c.name != "id"], f"got {columns=}" + schema = { + c.name: c.type.to_dict() for c in columns if isinstance(c.type, SQLType) + } + dataset = self.metastore.create_dataset_version( + dataset, + version, + status=DatasetStatus.PENDING, + sources=sources, + feature_schema=feature_schema, + query_script=query_script, + error_message=error_message, + error_stack=error_stack, + script_output=script_output, + schema=schema, + job_id=job_id, + is_job_result=is_job_result, + ignore_if_exists=True, + ) + + if create_rows_table: + table_name = self.warehouse.dataset_table_name(dataset.name, version) + self.warehouse.create_dataset_rows_table(table_name, columns=columns) + self.update_dataset_version_with_warehouse_info(dataset, version) + + return dataset + + def update_dataset_version_with_warehouse_info( + self, dataset: DatasetRecord, version: int, rows_dropped=False, **kwargs + ) -> None: + from datachain.query import DatasetQuery + + dataset_version = dataset.get_version(version) + + values = {**kwargs} + + if rows_dropped: + values["num_objects"] = None + values["size"] = None + values["preview"] = None + self.metastore.update_dataset_version( + dataset, + version, + **values, + ) + return + + if not dataset_version.num_objects: + num_objects, size = self.warehouse.dataset_stats(dataset, version) + if num_objects != dataset_version.num_objects: + values["num_objects"] = num_objects + if size != dataset_version.size: + values["size"] = size + + if not dataset_version.preview: + values["preview"] = ( + DatasetQuery(name=dataset.name, version=version, catalog=self) + .select() + .limit(20) + .to_records() + ) + + if not values: + return + + self.metastore.update_dataset_version( + dataset, + version, + **values, + ) + + def update_dataset( + self, dataset: DatasetRecord, conn=None, **kwargs + ) -> DatasetRecord: + """Updates dataset fields.""" + old_name = None + new_name = None + if "name" in kwargs and kwargs["name"] != dataset.name: + old_name = dataset.name + new_name = kwargs["name"] + + dataset = self.metastore.update_dataset(dataset, conn=conn, **kwargs) + + if old_name and new_name: + # updating name must result in updating dataset table names as well + for version in [v.version for v in dataset.versions]: + self.warehouse.rename_dataset_table( + old_name, + new_name, + old_version=version, + new_version=version, + ) + + return dataset + + def remove_dataset_version( + self, dataset: DatasetRecord, version: int, drop_rows: Optional[bool] = True + ) -> None: + """ + Deletes one single dataset version. + If it was last version, it removes dataset completely + """ + if not dataset.has_version(version): + return + dataset = self.metastore.remove_dataset_version(dataset, version) + if drop_rows: + self.warehouse.drop_dataset_rows_table(dataset, version) + + def get_temp_table_names(self) -> list[str]: + return self.warehouse.get_temp_table_names() + + def cleanup_temp_tables(self, names: Iterable[str]) -> None: + """ + Drop tables created temporarily when processing datasets. + + This should be implemented even if temporary tables are used to + ensure that they are cleaned up as soon as they are no longer + needed. When running the same `DatasetQuery` multiple times we + may use the same temporary table names. + """ + self.warehouse.cleanup_temp_tables(names) + self.id_generator.delete_uris(names) + + def create_dataset_from_sources( + self, + name: str, + sources: list[str], + client_config=None, + recursive=False, + ) -> DatasetRecord: + if not sources: + raise ValueError("Sources needs to be non empty list") + + from datachain.query import DatasetQuery + + dataset_queries = [] + for source in sources: + if source.startswith(DATASET_PREFIX): + dq = DatasetQuery( + name=source[len(DATASET_PREFIX) :], + catalog=self, + client_config=client_config, + ) + else: + dq = DatasetQuery( + path=source, + catalog=self, + client_config=client_config, + recursive=recursive, + ) + + dataset_queries.append(dq) + + # create union of all dataset queries created from sources + dq = reduce(lambda ds1, ds2: ds1.union(ds2), dataset_queries) + try: + dq.save(name) + except Exception as e: # noqa: BLE001 + try: + ds = self.get_dataset(name) + self.metastore.update_dataset_status( + ds, + DatasetStatus.FAILED, + version=ds.latest_version, + error_message=DATASET_INTERNAL_ERROR_MESSAGE, + error_stack=traceback.format_exc(), + ) + self._remove_dataset_rows_and_warehouse_info( + ds, + ds.latest_version, + sources="\n".join(sources), + ) + raise + except DatasetNotFoundError: + raise e from None + + ds = self.get_dataset(name) + + self.update_dataset_version_with_warehouse_info( + ds, + ds.latest_version, + sources="\n".join(sources), + ) + + return self.get_dataset(name) + + def register_new_dataset( + self, + source_dataset: DatasetRecord, + source_version: int, + target_name: str, + ) -> DatasetRecord: + target_dataset = self.metastore.create_dataset( + target_name, + query_script=source_dataset.query_script, + schema=source_dataset.serialized_schema, + ) + return self.register_dataset(source_dataset, source_version, target_dataset, 1) + + def register_dataset( + self, + dataset: DatasetRecord, + version: int, + target_dataset: DatasetRecord, + target_version: Optional[int] = None, + ) -> DatasetRecord: + """ + Registers dataset version of one dataset as dataset version of another + one (it can be new version of existing one). + It also removes original dataset version + """ + target_version = target_version or target_dataset.next_version + + if not target_dataset.is_valid_next_version(target_version): + raise DatasetInvalidVersionError( + f"Version {target_version} must be higher than the current latest one" + ) + + dataset_version = dataset.get_version(version) + if not dataset_version: + raise ValueError(f"Dataset {dataset.name} does not have version {version}") + + if not dataset_version.is_final_status(): + raise ValueError("Cannot register dataset version in non final status") + + # copy dataset version + target_dataset = self.metastore.create_dataset_version( + target_dataset, + target_version, + sources=dataset_version.sources, + status=dataset_version.status, + query_script=dataset_version.query_script, + error_message=dataset_version.error_message, + error_stack=dataset_version.error_stack, + script_output=dataset_version.script_output, + created_at=dataset_version.created_at, + finished_at=dataset_version.finished_at, + schema=dataset_version.serialized_schema, + num_objects=dataset_version.num_objects, + size=dataset_version.size, + preview=dataset_version.preview, + job_id=dataset_version.job_id, + is_job_result=dataset_version.is_job_result, + ) + # to avoid re-creating rows table, we are just renaming it for a new version + # of target dataset + self.warehouse.rename_dataset_table( + dataset.name, + target_dataset.name, + old_version=version, + new_version=target_version, + ) + self.metastore.update_dataset_dependency_source( + dataset, + version, + new_source_dataset=target_dataset, + new_source_dataset_version=target_version, + ) + + if dataset.id == target_dataset.id: + # we are updating the same dataset so we need to refresh it to have newly + # added version in step before + dataset = self.get_dataset(dataset.name) + + self.remove_dataset_version(dataset, version, drop_rows=False) + + return self.get_dataset(target_dataset.name) + + def get_dataset(self, name: str) -> DatasetRecord: + return self.metastore.get_dataset(name) + + def get_remote_dataset(self, name: str, *, remote_config=None) -> DatasetRecord: + remote_config = remote_config or get_remote_config( + read_config(DataChainDir.find().root), remote="" + ) + studio_client = StudioClient( + remote_config["url"], remote_config["username"], remote_config["token"] + ) + + info_response = studio_client.dataset_info(name) + if not info_response.ok: + _raise_remote_error(info_response.message) + + dataset_info = info_response.data + assert isinstance(dataset_info, dict) + return DatasetRecord.from_dict(dataset_info) + + def get_dataset_dependencies( + self, name: str, version: int, indirect=False + ) -> list[Optional[DatasetDependency]]: + dataset = self.get_dataset(name) + + direct_dependencies = self.metastore.get_direct_dataset_dependencies( + dataset, version + ) + + if not indirect: + return direct_dependencies + + for d in direct_dependencies: + if not d: + # dependency has been removed + continue + if d.is_dataset: + # only datasets can have dependencies + d.dependencies = self.get_dataset_dependencies( + d.name, int(d.version), indirect=indirect + ) + + return direct_dependencies + + def ls_datasets(self) -> Iterator[DatasetRecord]: + datasets = self.metastore.list_datasets() + for d in datasets: + if not d.is_bucket_listing: + yield d + + def ls_dataset_rows( + self, name: str, version: int, offset=None, limit=None + ) -> list[dict]: + from datachain.query import DatasetQuery + + dataset = self.get_dataset(name) + + q = DatasetQuery(name=dataset.name, version=version, catalog=self).select() + if limit: + q = q.limit(limit) + if offset: + q = q.offset(offset) + + q = q.order_by("id") + + return q.to_records() + + def signed_url(self, source: str, path: str, client_config=None) -> str: + client_config = client_config or self.client_config + client, _ = self.parse_url(source, **client_config) + return client.url(path) + + def export_dataset_table( + self, + bucket_uri: str, + name: str, + version: int, + client_config=None, + ) -> list[str]: + dataset = self.get_dataset(name) + + return self.warehouse.export_dataset_table( + bucket_uri, dataset, version, client_config + ) + + def dataset_table_export_file_names(self, name: str, version: int) -> list[str]: + dataset = self.get_dataset(name) + return self.warehouse.dataset_table_export_file_names(dataset, version) + + def dataset_stats(self, name: str, version: int) -> DatasetStats: + """ + Returns tuple with dataset stats: total number of rows and total dataset size. + """ + dataset = self.get_dataset(name) + dataset_version = dataset.get_version(version) + return DatasetStats( + num_objects=dataset_version.num_objects, + size=dataset_version.size, + ) + + def remove_dataset( + self, + name: str, + version: Optional[int] = None, + force: Optional[bool] = False, + ): + dataset = self.get_dataset(name) + if not version and not force: + raise ValueError(f"Missing dataset version from input for dataset {name}") + if version and not dataset.has_version(version): + raise DatasetInvalidVersionError( + f"Dataset {name} doesn't have version {version}" + ) + + if version: + self.remove_dataset_version(dataset, version) + return + + while dataset.versions: + version = dataset.versions[0].version + self.remove_dataset_version( + dataset, + version, + ) + + def edit_dataset( + self, + name: str, + new_name: Optional[str] = None, + description: Optional[str] = None, + labels: Optional[list[str]] = None, + ) -> DatasetRecord: + update_data = {} + if new_name: + update_data["name"] = new_name + if description is not None: + update_data["description"] = description + if labels is not None: + update_data["labels"] = labels # type: ignore[assignment] + + dataset = self.get_dataset(name) + return self.update_dataset(dataset, **update_data) + + def merge_datasets( + self, + src: DatasetRecord, + dst: DatasetRecord, + src_version: int, + dst_version: Optional[int] = None, + ) -> DatasetRecord: + """ + Merges records from source to destination dataset. + It will create new version + of a dataset with records merged from old version and the source, unless + existing version is specified for destination in which case it must + be in non final status as datasets are immutable + """ + if ( + dst_version + and not dst.is_valid_next_version(dst_version) + and dst.get_version(dst_version).is_final_status() + ): + raise DatasetInvalidVersionError( + f"Version {dst_version} must be higher than the current latest one" + ) + + src_dep = self.get_dataset_dependencies(src.name, src_version) + dst_dep = self.get_dataset_dependencies( + dst.name, + dst.latest_version, # type: ignore[arg-type] + ) + + if dst.has_version(dst_version): # type: ignore[arg-type] + # case where we don't create new version, but append to the existing one + self.warehouse.merge_dataset_rows( + src, + dst, + src_version, + dst_version=dst_version, # type: ignore[arg-type] + ) + merged_schema = src.serialized_schema | dst.serialized_schema + self.update_dataset(dst, schema=merged_schema) + self.update_dataset_version_with_warehouse_info( + dst, + dst_version, # type: ignore[arg-type] + schema=merged_schema, + ) + for dep in src_dep: + if dep and dep not in dst_dep: + self.metastore.add_dependency( + dep, + dst.name, + dst_version, # type: ignore[arg-type] + ) + else: + # case where we create new version of merged results + src_dr = self.warehouse.dataset_rows(src, src_version) + dst_dr = self.warehouse.dataset_rows(dst) + + merge_result_columns = list( + { + c.name: c for c in list(src_dr.table.c) + list(dst_dr.table.c) + }.values() + ) + + dst_version = dst_version or dst.next_version + dst = self.create_new_dataset_version( + dst, + dst_version, + columns=merge_result_columns, + ) + self.warehouse.merge_dataset_rows( + src, + dst, + src_version, + dst_version, + ) + self.update_dataset_version_with_warehouse_info(dst, dst_version) + for dep in set(src_dep + dst_dep): + if dep: + self.metastore.add_dependency(dep, dst.name, dst_version) + + return dst + + def get_file_signals( + self, dataset_name: str, dataset_version: int, row: RowDict + ) -> Optional[dict]: + """ + Function that returns file signals from dataset row. + Note that signal names are without prefix, so if there was 'laion__file__source' + in original row, result will have just 'source' + Example output: + { + "source": "s3://ldb-public", + "parent": "animals/dogs", + "name": "dog.jpg", + ... + } + """ + from datachain.lib.signal_schema import DEFAULT_DELIMITER, SignalSchema + + version = self.get_dataset(dataset_name).get_version(dataset_version) + + file_signals_values = {} + + schema = SignalSchema.deserialize(version.feature_schema) + for file_signals in schema.get_file_signals(): + prefix = file_signals.replace(".", DEFAULT_DELIMITER) + DEFAULT_DELIMITER + file_signals_values[file_signals] = { + c_name.removeprefix(prefix): c_value + for c_name, c_value in row.items() + if c_name.startswith(prefix) + and DEFAULT_DELIMITER not in c_name.removeprefix(prefix) + } + + if not file_signals_values: + return None + + # there can be multiple file signals in a schema, but taking the first + # one for now. In future we might add ability to choose from which one + # to open object + return next(iter(file_signals_values.values())) + + def open_object( + self, + dataset_name: str, + dataset_version: int, + row: RowDict, + use_cache: bool = True, + **config: Any, + ): + file_signals = self.get_file_signals(dataset_name, dataset_version, row) + if not file_signals: + raise RuntimeError("Cannot open object without file signals") + + config = config or self.client_config + client = self.get_client(file_signals["source"], **config) + return client.open_object( + self._get_row_uid(file_signals), # type: ignore [arg-type] + use_cache=use_cache, + ) + + def _get_row_uid(self, row: RowDict) -> UniqueId: + return UniqueId( + row["source"], + row["parent"], + row["name"], + row["etag"], + row["size"], + row["vtype"], + row["location"], + ) + + def ls( + self, + sources: list[str], + fields: Iterable[str], + ttl=TTL_INT, + update=False, + skip_indexing=False, + *, + client_config=None, + ) -> Iterator[tuple[DataSource, Iterable[tuple]]]: + data_sources = self.enlist_sources( + sources, + ttl, + update, + skip_indexing=skip_indexing, + client_config=client_config or self.client_config, + ) + + for source in data_sources: # type: ignore [union-attr] + yield source, source.ls(fields) + + def ls_storage_uris(self) -> Iterator[str]: + yield from self.metastore.get_all_storage_uris() + + def get_storage(self, uri: StorageURI) -> Storage: + return self.metastore.get_storage(uri) + + def ls_storages(self) -> list[Storage]: + return self.metastore.list_storages() + + def pull_dataset( + self, + dataset_uri: str, + output: Optional[str] = None, + no_cp: bool = False, + force: bool = False, + edatachain: bool = False, + edatachain_file: Optional[str] = None, + *, + client_config=None, + remote_config=None, + ) -> None: + # TODO add progress bar https://github.com/iterative/dvcx/issues/750 + # TODO copy correct remote dates https://github.com/iterative/dvcx/issues/new + # TODO compare dataset stats on remote vs local pull to assert it's ok + def _instantiate_dataset(): + if no_cp: + return + self.cp( + [dataset_uri], + output, + force=force, + no_edatachain_file=not edatachain, + edatachain_file=edatachain_file, + client_config=client_config, + ) + print(f"Dataset {dataset_uri} instantiated locally to {output}") + + if not output and not no_cp: + raise ValueError("Please provide output directory for instantiation") + + client_config = client_config or self.client_config + remote_config = remote_config or get_remote_config( + read_config(DataChainDir.find().root), remote="" + ) + + studio_client = StudioClient( + remote_config["url"], remote_config["username"], remote_config["token"] + ) + + try: + remote_dataset_name, version = parse_dataset_uri(dataset_uri) + except Exception as e: + raise DataChainError("Error when parsing dataset uri") from e + + dataset = None + try: + dataset = self.get_dataset(remote_dataset_name) + except DatasetNotFoundError: + # we will create new one if it doesn't exist + pass + + remote_dataset = self.get_remote_dataset( + remote_dataset_name, remote_config=remote_config + ) + # if version is not specified in uri, take the latest one + if not version: + version = remote_dataset.latest_version + print(f"Version not specified, pulling the latest one (v{version})") + # updating dataset uri with latest version + dataset_uri = create_dataset_uri(remote_dataset_name, version) + + assert version + + if dataset and dataset.has_version(version): + print(f"Local copy of dataset {dataset_uri} already present") + _instantiate_dataset() + return + + try: + remote_dataset_version = remote_dataset.get_version(version) + except (ValueError, StopIteration) as exc: + raise DataChainError( + f"Dataset {remote_dataset_name} doesn't have version {version}" + " on server" + ) from exc + + stats_response = studio_client.dataset_stats(remote_dataset_name, version) + if not stats_response.ok: + _raise_remote_error(stats_response.message) + dataset_stats = stats_response.data + + dataset_save_progress_bar = tqdm( + desc=f"Saving dataset {dataset_uri} locally: ", + unit=" rows", + unit_scale=True, + unit_divisor=1000, + total=dataset_stats.num_objects, # type: ignore [union-attr] + ) + + schema = DatasetRecord.parse_schema(remote_dataset_version.schema) + + columns = tuple( + sa.Column(name, typ) for name, typ in schema.items() if name != "id" + ) + # creating new dataset (version) locally + dataset = self.create_dataset( + remote_dataset_name, + version, + query_script=remote_dataset_version.query_script, + create_rows=True, + columns=columns, + validate_version=False, + ) + + # asking remote to export dataset rows table to s3 and to return signed + # urls of exported parts, which are in parquet format + export_response = studio_client.export_dataset_table( + remote_dataset_name, version + ) + if not export_response.ok: + _raise_remote_error(export_response.message) + + signed_urls = export_response.data + + if signed_urls: + shuffle(signed_urls) + + rows_fetcher = DatasetRowsFetcher( + self.metastore.clone(), + self.warehouse.clone(), + remote_config, + dataset.name, + version, + schema, + ) + try: + rows_fetcher.run( + batched( + signed_urls, + math.ceil(len(signed_urls) / PULL_DATASET_MAX_THREADS), + ), + dataset_save_progress_bar, + ) + except: + self.remove_dataset(dataset.name, version) + raise + + dataset = self.metastore.update_dataset_status( + dataset, + DatasetStatus.COMPLETE, + version=version, + error_message=remote_dataset.error_message, + error_stack=remote_dataset.error_stack, + script_output=remote_dataset.error_stack, + ) + self.update_dataset_version_with_warehouse_info(dataset, version) + + dataset_save_progress_bar.close() + print(f"Dataset {dataset_uri} saved locally") + + _instantiate_dataset() + + def clone( + self, + sources: list[str], + output: str, + force: bool = False, + update: bool = False, + recursive: bool = False, + no_glob: bool = False, + no_cp: bool = False, + edatachain: bool = False, + edatachain_file: Optional[str] = None, + ttl: int = TTL_INT, + *, + client_config=None, + ) -> None: + """ + This command takes cloud path(s) and duplicates files and folders in + them into the dataset folder. + It also adds those files to a dataset in database, which is + created if doesn't exist yet + Optionally, it creates a .edatachain file + """ + if not no_cp or edatachain: + self.cp( + sources, + output, + force=force, + update=update, + recursive=recursive, + no_glob=no_glob, + edatachain_only=no_cp, + no_edatachain_file=not edatachain, + edatachain_file=edatachain_file, + ttl=ttl, + client_config=client_config, + ) + else: + # since we don't call cp command, which does listing implicitly, + # it needs to be done here + self.enlist_sources( + sources, + ttl, + update, + client_config=client_config or self.client_config, + ) + + self.create_dataset_from_sources( + output, sources, client_config=client_config, recursive=recursive + ) + + def apply_udf( + self, + udf_location: str, + source: str, + target_name: str, + parallel: Optional[int] = None, + params: Optional[str] = None, + ): + from datachain.query import DatasetQuery + + if source.startswith(DATASET_PREFIX): + ds = DatasetQuery(name=source[len(DATASET_PREFIX) :], catalog=self) + else: + ds = DatasetQuery(path=source, catalog=self) + udf = import_object(udf_location) + if params: + args, kwargs = parse_params_string(params) + udf = udf(*args, **kwargs) + ds.add_signals(udf, parallel=parallel).save(target_name) + + def query( + self, + query_script: str, + envs: Optional[Mapping[str, str]] = None, + python_executable: Optional[str] = None, + save: bool = False, + save_as: Optional[str] = None, + preview_limit: int = 10, + preview_offset: int = 0, + preview_columns: Optional[list[str]] = None, + capture_output: bool = True, + output_hook: Callable[[str], None] = noop, + params: Optional[dict[str, str]] = None, + job_id: Optional[str] = None, + ) -> QueryResult: + """ + Method to run custom user Python script to run a query and, as result, + creates new dataset from the results of a query. + Returns tuple of result dataset and script output. + + Constraints on query script: + 1. datachain.query.DatasetQuery should be used in order to create query + for a dataset + 2. There should not be any .save() call on DatasetQuery since the idea + is to create only one dataset as the outcome of the script + 3. Last statement must be an instance of DatasetQuery + + If save is set to True, we are creating new dataset with results + from dataset query. If it's set to False, we will just print results + without saving anything + + Example of query script: + from datachain.query import DatasetQuery, C + DatasetQuery('s3://ldb-public/remote/datasets/mnist-tiny/').filter( + C.size > 1000 + ) + """ + from datachain.query.dataset import ExecutionResult + + feature_file = tempfile.NamedTemporaryFile( + dir=os.getcwd(), suffix=".py", delete=False + ) + _, feature_module = os.path.split(feature_file.name) + + try: + lines, proc, response_text = self.run_query( + python_executable or sys.executable, + query_script, + envs, + feature_file, + capture_output, + feature_module, + output_hook, + params, + preview_columns, + preview_limit, + preview_offset, + save, + save_as, + job_id, + ) + finally: + feature_file.close() + os.unlink(feature_file.name) + + output = "".join(lines) + + if proc.returncode: + if proc.returncode == QUERY_SCRIPT_CANCELED_EXIT_CODE: + raise QueryScriptCancelError( + "Query script was canceled by user", + return_code=proc.returncode, + output=output, + ) + if proc.returncode == QUERY_SCRIPT_INVALID_LAST_STATEMENT_EXIT_CODE: + raise QueryScriptRunError( + "Last line in a script was not an instance of DatasetQuery", + return_code=proc.returncode, + output=output, + ) + raise QueryScriptRunError( + f"Query script exited with error code {proc.returncode}", + return_code=proc.returncode, + output=output, + ) + + try: + response = json.loads(response_text) + except ValueError: + response = {} + exec_result = ExecutionResult(**response) + + dataset: Optional[DatasetRecord] = None + version: Optional[int] = None + if save or save_as: + dataset, version = self.save_result( + query_script, exec_result, output, version, job_id + ) + + return QueryResult( + dataset=dataset, + version=version, + output=output, + preview=exec_result.preview, + metrics=exec_result.metrics, + ) + + def run_query( + self, + python_executable: str, + query_script: str, + envs: Optional[Mapping[str, str]], + feature_file: IO[bytes], + capture_output: bool, + feature_module: str, + output_hook: Callable[[str], None], + params: Optional[dict[str, str]], + preview_columns: Optional[list[str]], + preview_limit: int, + preview_offset: int, + save: bool, + save_as: Optional[str], + job_id: Optional[str], + ) -> tuple[list[str], subprocess.Popen, str]: + try: + feature_code, query_script_compiled = self.compile_query_script( + query_script, feature_module[:-3] + ) + if feature_code: + feature_file.write(feature_code.encode()) + feature_file.flush() + + except Exception as exc: + raise QueryScriptCompileError( + f"Query script failed to compile, reason: {exc}" + ) from exc + if save_as and save_as.startswith(QUERY_DATASET_PREFIX): + raise ValueError( + f"Cannot use {QUERY_DATASET_PREFIX} prefix for dataset name" + ) + r, w = os.pipe() + if os.name == "nt": + import msvcrt + + os.set_inheritable(w, True) + + startupinfo = subprocess.STARTUPINFO() # type: ignore[attr-defined] + handle = msvcrt.get_osfhandle(w) # type: ignore[attr-defined] + startupinfo.lpAttributeList["handle_list"].append(handle) + kwargs: dict[str, Any] = {"startupinfo": startupinfo} + else: + handle = w + kwargs = {"pass_fds": [w]} + envs = dict(envs or os.environ) + if feature_code: + envs["DATACHAIN_FEATURE_CLASS_SOURCE"] = json.dumps( + {feature_module: feature_code} + ) + envs.update( + { + "DATACHAIN_QUERY_PARAMS": json.dumps(params or {}), + "PYTHONPATH": os.getcwd(), # For local imports + "DATACHAIN_QUERY_PREVIEW_ARGS": json.dumps( + { + "limit": preview_limit, + "offset": preview_offset, + "columns": preview_columns, + } + ), + "DATACHAIN_QUERY_SAVE": "1" if save else "", + "DATACHAIN_QUERY_SAVE_AS": save_as or "", + "PYTHONUNBUFFERED": "1", + "DATACHAIN_OUTPUT_FD": str(handle), + "DATACHAIN_JOB_ID": job_id or "", + }, + ) + with subprocess.Popen( # noqa: S603 + [python_executable, "-c", query_script_compiled], + env=envs, + stdout=subprocess.PIPE if capture_output else None, + stderr=subprocess.STDOUT if capture_output else None, + bufsize=1, + text=True, + **kwargs, + ) as proc: + os.close(w) + + out = proc.stdout + _lines: list[str] = [] + ctx = print_and_capture(out, output_hook) if out else nullcontext(_lines) + + with ctx as lines, open(r) as f: + response_text = "" + while proc.poll() is None: + response_text += f.readline() + time.sleep(0.1) + response_text += f.readline() + return lines, proc, response_text + + def save_result(self, query_script, exec_result, output, version, job_id): + if not exec_result.dataset: + raise QueryScriptDatasetNotFound( + "No dataset found after running Query script", + output=output, + ) + name, version = exec_result.dataset + # finding returning dataset + try: + dataset = self.get_dataset(name) + dataset.get_version(version) + except (DatasetNotFoundError, ValueError) as e: + raise QueryScriptDatasetNotFound( + "No dataset found after running Query script", + output=output, + ) from e + dataset = self.update_dataset( + dataset, + script_output=output, + query_script=query_script, + ) + self.update_dataset_version_with_warehouse_info( + dataset, + version, + script_output=output, + query_script=query_script, + job_id=job_id, + is_job_result=True, + ) + return dataset, version + + def cp( + self, + sources: list[str], + output: str, + force: bool = False, + update: bool = False, + recursive: bool = False, + edatachain_file: Optional[str] = None, + edatachain_only: bool = False, + no_edatachain_file: bool = False, + no_glob: bool = False, + ttl: int = TTL_INT, + *, + client_config=None, + ) -> list[dict[str, Any]]: + """ + This function copies files from cloud sources to local destination directory + If cloud source is not indexed, or has expired index, it runs indexing + It also creates .edatachain file by default, if not specified differently + """ + client_config = client_config or self.client_config + node_groups = self.enlist_sources_grouped( + sources, + ttl, + update, + no_glob, + client_config=client_config, + ) + + always_copy_dir_contents, copy_to_filename = prepare_output_for_cp( + node_groups, output, force, edatachain_only, no_edatachain_file + ) + dataset_file = check_output_dataset_file( + output, force, edatachain_file, no_edatachain_file + ) + + total_size, total_files = collect_nodes_for_cp(node_groups, recursive) + + if total_files == 0: + # Nothing selected to cp + return [] + + desc_max_len = max(len(output) + 16, 19) + bar_format = ( + "{desc:<" + f"{desc_max_len}" + "}{percentage:3.0f}%|{bar}| {n_fmt:>5}/{total_fmt:<5} " + "[{elapsed}<{remaining}, {rate_fmt:>8}]" + ) + + if not edatachain_only: + with get_download_bar(bar_format, total_size) as pbar: + for node_group in node_groups: + node_group.download(recursive=recursive, pbar=pbar) + + instantiate_node_groups( + node_groups, + output, + bar_format, + total_files, + force, + recursive, + edatachain_only, + always_copy_dir_contents, + copy_to_filename, + ) + if no_edatachain_file: + return [] + + metafile_data = compute_metafile_data(node_groups) + if metafile_data: + # Don't write the metafile if nothing was copied + print(f"Creating '{dataset_file}'") + with open(dataset_file, "w", encoding="utf-8") as fd: + yaml.dump(metafile_data, fd, sort_keys=False) + + return metafile_data + + def du( + self, + sources, + depth=0, + ttl=TTL_INT, + update=False, + *, + client_config=None, + ) -> Iterable[tuple[str, float]]: + sources = self.enlist_sources( + sources, + ttl, + update, + client_config=client_config or self.client_config, + ) + + def du_dirs(src, node, subdepth): + if subdepth > 0: + subdirs = src.listing.get_dirs_by_parent_path(node.path) + for sd in subdirs: + yield from du_dirs(src, sd, subdepth - 1) + yield ( + src.get_node_full_path(node), + src.listing.du(node)[0], + ) + + for src in sources: + yield from du_dirs(src, src.node, depth) + + def find( + self, + sources, + ttl=TTL_INT, + update=False, + names=None, + inames=None, + paths=None, + ipaths=None, + size=None, + typ=None, + columns=None, + *, + client_config=None, + ) -> Iterator[str]: + sources = self.enlist_sources( + sources, + ttl, + update, + client_config=client_config or self.client_config, + ) + if not columns: + columns = ["path"] + field_set = set() + for column in columns: + if column == "du": + field_set.add("dir_type") + field_set.add("size") + field_set.add("parent") + field_set.add("name") + elif column == "name": + field_set.add("name") + elif column == "owner": + field_set.add("owner_name") + elif column == "path": + field_set.add("dir_type") + field_set.add("parent") + field_set.add("name") + elif column == "size": + field_set.add("size") + elif column == "type": + field_set.add("dir_type") + fields = list(field_set) + field_lookup = {f: i for i, f in enumerate(fields)} + for src in sources: + results = src.listing.find( + src.node, fields, names, inames, paths, ipaths, size, typ + ) + for row in results: + yield "\t".join( + find_column_to_str(row, field_lookup, src, column) + for column in columns + ) + + def index( + self, + sources, + ttl=TTL_INT, + update=False, + *, + client_config=None, + ) -> None: + root_sources = [ + src for src in sources if Client.get_implementation(src).is_root_url(src) + ] + non_root_sources = [ + src + for src in sources + if not Client.get_implementation(src).is_root_url(src) + ] + + client_config = client_config or self.client_config + + # for root sources (e.g s3://) we are just getting all buckets and + # saving them as storages, without further indexing in each bucket + for source in root_sources: + for bucket in Client.get_implementation(source).ls_buckets(**client_config): + client = self.get_client(bucket.uri, **client_config) + print(f"Registering storage {client.uri}") + self.metastore.create_storage_if_not_registered(client.uri) + + self.enlist_sources( + non_root_sources, + ttl, + update, + client_config=client_config, + only_index=True, + ) + + def find_stale_storages(self) -> None: + self.metastore.find_stale_storages() diff --git a/src/datachain/catalog/datasource.py b/src/datachain/catalog/datasource.py new file mode 100644 index 000000000..18945d4c9 --- /dev/null +++ b/src/datachain/catalog/datasource.py @@ -0,0 +1,45 @@ +from collections.abc import Iterable + +from datachain.node import DirType, NodeWithPath + + +class DataSource: + def __init__(self, listing, node, as_container=False): + self.listing = listing + self.node = node + self.as_container = ( + as_container # Indicates whether a .tar file is handled as a container + ) + + def get_full_path(self): + return self.get_node_full_path(self.node) + + def get_node_full_path(self, node): + return self.listing.client.get_full_path(node.full_path) + + def get_node_full_path_from_path(self, full_path): + return self.listing.client.get_full_path(full_path) + + def is_single_object(self): + return self.node.dir_type == DirType.FILE or ( + not self.as_container and self.node.dir_type == DirType.TAR_ARCHIVE + ) + + def is_container(self): + return not self.is_single_object() + + def ls(self, fields) -> Iterable[tuple]: + if self.is_single_object(): + return [tuple(getattr(self.node, f) for f in fields)] + return self.listing.ls_path(self.node, fields) + + def dirname(self): + if self.is_single_object(): + return self.node.parent + return self.node.path + + def find(self, *, sort=None): + if self.is_single_object(): + return [NodeWithPath(self.node, [])] + + return self.listing.subtree_files(self.node, sort=sort) diff --git a/src/datachain/catalog/loader.py b/src/datachain/catalog/loader.py new file mode 100644 index 000000000..beee5ac8a --- /dev/null +++ b/src/datachain/catalog/loader.py @@ -0,0 +1,173 @@ +import os +from importlib import import_module +from typing import Any, Optional + +from datachain.catalog import Catalog +from datachain.data_storage import ( + AbstractIDGenerator, + AbstractMetastore, + AbstractWarehouse, +) +from datachain.data_storage.serializer import deserialize +from datachain.data_storage.sqlite import ( + SQLiteIDGenerator, + SQLiteMetastore, + SQLiteWarehouse, +) +from datachain.utils import get_envs_by_prefix + +ID_GENERATOR_SERIALIZED = "DATACHAIN__ID_GENERATOR" +ID_GENERATOR_IMPORT_PATH = "DATACHAIN_ID_GENERATOR" +ID_GENERATOR_ARG_PREFIX = "DATACHAIN_ID_GENERATOR_ARG_" +METASTORE_SERIALIZED = "DATACHAIN__METASTORE" +METASTORE_IMPORT_PATH = "DATACHAIN_METASTORE" +METASTORE_ARG_PREFIX = "DATACHAIN_METASTORE_ARG_" +WAREHOUSE_SERIALIZED = "DATACHAIN__WAREHOUSE" +WAREHOUSE_IMPORT_PATH = "DATACHAIN_WAREHOUSE" +WAREHOUSE_ARG_PREFIX = "DATACHAIN_WAREHOUSE_ARG_" +DISTRIBUTED_IMPORT_PATH = "DATACHAIN_DISTRIBUTED" +DISTRIBUTED_ARG_PREFIX = "DATACHAIN_DISTRIBUTED_ARG_" + + +def get_id_generator() -> "AbstractIDGenerator": + id_generator_serialized = os.environ.get(ID_GENERATOR_SERIALIZED) + if id_generator_serialized: + id_generator_obj = deserialize(id_generator_serialized) + if not isinstance(id_generator_obj, AbstractIDGenerator): + raise RuntimeError( + "Deserialized ID generator is not an instance of AbstractIDGenerator: " + f"{id_generator_obj}" + ) + return id_generator_obj + + id_generator_import_path = os.environ.get(ID_GENERATOR_IMPORT_PATH) + id_generator_arg_envs = get_envs_by_prefix(ID_GENERATOR_ARG_PREFIX) + # Convert env variable names to keyword argument names by lowercasing them + id_generator_args = {k.lower(): v for k, v in id_generator_arg_envs.items()} + + if id_generator_import_path: + # ID generator paths are specified as (for example): + # datachain.data_storage.SQLiteIDGenerator + if "." not in id_generator_import_path: + raise RuntimeError( + f"Invalid {ID_GENERATOR_IMPORT_PATH} import path:" + f"{id_generator_import_path}" + ) + module_name, _, class_name = id_generator_import_path.rpartition(".") + id_generator = import_module(module_name) + id_generator_class = getattr(id_generator, class_name) + else: + id_generator_class = SQLiteIDGenerator + return id_generator_class(**id_generator_args) + + +def get_metastore(id_generator: Optional["AbstractIDGenerator"]) -> "AbstractMetastore": + metastore_serialized = os.environ.get(METASTORE_SERIALIZED) + if metastore_serialized: + metastore_obj = deserialize(metastore_serialized) + if not isinstance(metastore_obj, AbstractMetastore): + raise RuntimeError( + "Deserialized Metastore is not an instance of AbstractMetastore: " + f"{metastore_obj}" + ) + return metastore_obj + + if id_generator is None: + id_generator = get_id_generator() + + metastore_import_path = os.environ.get(METASTORE_IMPORT_PATH) + metastore_arg_envs = get_envs_by_prefix(METASTORE_ARG_PREFIX) + # Convert env variable names to keyword argument names by lowercasing them + metastore_args = {k.lower(): v for k, v in metastore_arg_envs.items()} + + if metastore_import_path: + # Metastore paths are specified as (for example): + # datachain.data_storage.SQLiteMetastore + if "." not in metastore_import_path: + raise RuntimeError( + f"Invalid {METASTORE_IMPORT_PATH} import path: {metastore_import_path}" + ) + module_name, _, class_name = metastore_import_path.rpartition(".") + metastore = import_module(module_name) + metastore_class = getattr(metastore, class_name) + else: + metastore_class = SQLiteMetastore + return metastore_class(id_generator, **metastore_args) + + +def get_warehouse(id_generator: Optional["AbstractIDGenerator"]) -> "AbstractWarehouse": + warehouse_serialized = os.environ.get(WAREHOUSE_SERIALIZED) + if warehouse_serialized: + warehouse_obj = deserialize(warehouse_serialized) + if not isinstance(warehouse_obj, AbstractWarehouse): + raise RuntimeError( + "Deserialized Warehouse is not an instance of AbstractWarehouse: " + f"{warehouse_obj}" + ) + return warehouse_obj + + if id_generator is None: + id_generator = get_id_generator() + + warehouse_import_path = os.environ.get(WAREHOUSE_IMPORT_PATH) + warehouse_arg_envs = get_envs_by_prefix(WAREHOUSE_ARG_PREFIX) + # Convert env variable names to keyword argument names by lowercasing them + warehouse_args = {k.lower(): v for k, v in warehouse_arg_envs.items()} + + if warehouse_import_path: + # Warehouse paths are specified as (for example): + # datachain.data_storage.SQLiteWarehouse + if "." not in warehouse_import_path: + raise RuntimeError( + f"Invalid {WAREHOUSE_IMPORT_PATH} import path: {warehouse_import_path}" + ) + module_name, _, class_name = warehouse_import_path.rpartition(".") + warehouse = import_module(module_name) + warehouse_class = getattr(warehouse, class_name) + else: + warehouse_class = SQLiteWarehouse + return warehouse_class(id_generator, **warehouse_args) + + +def get_distributed_class(**kwargs): + distributed_import_path = os.environ.get(DISTRIBUTED_IMPORT_PATH) + distributed_arg_envs = get_envs_by_prefix(DISTRIBUTED_ARG_PREFIX) + # Convert env variable names to keyword argument names by lowercasing them + distributed_args = {k.lower(): v for k, v in distributed_arg_envs.items()} + + if not distributed_import_path: + raise RuntimeError( + f"{DISTRIBUTED_IMPORT_PATH} import path is required " + "for distributed UDF processing." + ) + # Distributed class paths are specified as (for example): + # module.classname + if "." not in distributed_import_path: + raise RuntimeError( + f"Invalid {DISTRIBUTED_IMPORT_PATH} import path: {distributed_import_path}" + ) + module_name, _, class_name = distributed_import_path.rpartition(".") + distributed = import_module(module_name) + distributed_class = getattr(distributed, class_name) + return distributed_class(**distributed_args | kwargs) + + +def get_catalog(client_config: Optional[dict[str, Any]] = None) -> Catalog: + """ + Function that creates Catalog instance with appropriate metastore + and warehouse classes. Metastore class can be provided with env variable + DATACHAIN_METASTORE and if not provided, default one is used. Warehouse class + can be provided with env variable DATACHAIN_WAREHOUSE and if not provided, + + If classes expects some kwargs, they can be provided via env variables + by adding them with prefix (DATACHAIN_METASTORE_ARG_ and DATACHAIN_WAREHOUSE_ARG_) + and name of variable after, e.g. if it accepts team_id as kwargs + we can provide DATACHAIN_METASTORE_ARG_TEAM_ID=12345 env variable. + """ + id_generator = get_id_generator() + return Catalog( + id_generator=id_generator, + metastore=get_metastore(id_generator), + warehouse=get_warehouse(id_generator), + client_config=client_config, + ) diff --git a/src/datachain/catalog/subclass.py b/src/datachain/catalog/subclass.py new file mode 100644 index 000000000..338be525b --- /dev/null +++ b/src/datachain/catalog/subclass.py @@ -0,0 +1,60 @@ +import ast + + +class SubclassFinder(ast.NodeVisitor): + """Finds subclasses of a target class in an AST.""" + + def __init__(self, target_classes: list[str]): + self.imports: list[ast.AST] = [] + self.main_body: list[ast.AST] = [] + + self.target_classes: list[str] = target_classes + self.aliases: dict[str, str] = {} + self.feature_class: list[ast.AST] = [] + + def visit_ImportFrom(self, node): # noqa: N802 + module = node.module + for alias in node.names: + full_name = f"{module}.{alias.name}" + self.aliases[alias.asname or alias.name] = full_name + self.imports.append(node) + + def visit_Import(self, node): # noqa: N802 + for alias in node.names: + self.aliases[alias.asname or alias.name] = alias.name + self.imports.append(node) + + def visit_ClassDef(self, node): # noqa: N802 + base_names = [self.get_base_name(base) for base in node.bases] + if any(self.is_subclass(name) for name in base_names): + self.feature_class.append(node) + else: + self.main_body.append(node) + + def visit(self, node): + if isinstance( + node, + (ast.Import, ast.ImportFrom, ast.ClassDef, ast.Module), + ): + return super().visit(node) + self.main_body.append(node) + return node + + def get_base_name(self, node): + if isinstance(node, ast.Name): + return self.aliases.get(node.id, node.id) + if isinstance(node, ast.Attribute): + return self.get_full_attr_name(node) + if isinstance(node, ast.Subscript): + return self.get_base_name(node.value) + return None + + def get_full_attr_name(self, node): + if isinstance(node.value, ast.Name): + return f"{node.value.id}.{node.attr}" + if isinstance(node.value, ast.Attribute): + return f"{self.get_full_attr_name(node.value)}.{node.attr}" + return node.attr + + def is_subclass(self, base_name): + return base_name and base_name.split(".")[-1] in self.target_classes diff --git a/src/datachain/cli.py b/src/datachain/cli.py new file mode 100644 index 000000000..e783da6c2 --- /dev/null +++ b/src/datachain/cli.py @@ -0,0 +1,1112 @@ +import logging +import os +import shlex +import sys +import traceback +from argparse import SUPPRESS, Action, ArgumentParser, ArgumentTypeError, Namespace +from collections.abc import Iterable, Iterator, Mapping, Sequence +from importlib.metadata import PackageNotFoundError, version +from itertools import chain +from multiprocessing import freeze_support +from typing import TYPE_CHECKING, Optional, Union + +import shtab + +from datachain import utils +from datachain.cli_utils import BooleanOptionalAction, CommaSeparatedArgs, KeyValueArgs +from datachain.utils import DataChainDir + +if TYPE_CHECKING: + from datachain.catalog import Catalog + +logger = logging.getLogger("datachain") + +TTL_HUMAN = "4h" +TTL_INT = 4 * 60 * 60 +FIND_COLUMNS = ["du", "name", "owner", "path", "size", "type"] + + +def human_time_type(value_str: str, can_be_none: bool = False) -> Optional[int]: + value = utils.human_time_to_int(value_str) + + if value: + return value + if can_be_none: + return None + + raise ArgumentTypeError( + "This option supports only a human-readable time interval like 12h or 4w." + ) + + +def parse_find_column(column: str) -> str: + column_lower = column.strip().lower() + if column_lower in FIND_COLUMNS: + return column_lower + raise ArgumentTypeError( + f"Invalid column for find: '{column}' Options are: {','.join(FIND_COLUMNS)}" + ) + + +def find_columns_type( + columns_str: str, + default_colums_str: str = "path", +) -> list[str]: + if not columns_str: + columns_str = default_colums_str + + return [parse_find_column(c) for c in columns_str.split(",")] + + +def add_sources_arg(parser: ArgumentParser, nargs: Union[str, int] = "+") -> Action: + return parser.add_argument( + "sources", + type=str, + nargs=nargs, + help="Data sources - paths to cloud storage dirs", + ) + + +def add_show_args(parser: ArgumentParser) -> None: + parser.add_argument( + "--limit", + action="store", + default=10, + type=int, + help="Number of rows to show", + ) + parser.add_argument( + "--offset", + action="store", + default=0, + type=int, + help="Number of rows to offset", + ) + parser.add_argument( + "--columns", + default=[], + action=CommaSeparatedArgs, + help="Columns to show", + ) + parser.add_argument( + "--no-collapse", + action="store_true", + default=False, + help="Do not collapse the columns", + ) + + +def get_parser() -> ArgumentParser: # noqa: PLR0915 + try: + __version__ = version("datachain") + except PackageNotFoundError: + # package is not installed + __version__ = "unknown" + + parser = ArgumentParser( + description="DataChain: Wrangle unstructured AI data at scale", prog="datachain" + ) + + parser.add_argument("-V", "--version", action="version", version=__version__) + parser.add_argument("--internal-run-udf", action="store_true", help=SUPPRESS) + parser.add_argument("--internal-run-udf-worker", action="store_true", help=SUPPRESS) + + parent_parser = ArgumentParser(add_help=False) + parent_parser.add_argument( + "--aws-endpoint-url", + type=str, + help="AWS endpoint URL", + ) + parent_parser.add_argument( + "--anon", + action="store_true", + help="AWS anon (aka awscli's --no-sign-request)", + ) + parent_parser.add_argument( + "--ttl", + type=human_time_type, + default=TTL_HUMAN, + help="Time-to-live of data source cache. Negative equals forever.", + ) + parent_parser.add_argument( + "-u", "--update", action="count", default=0, help="Update cache" + ) + parent_parser.add_argument( + "-v", "--verbose", action="count", default=0, help="Verbose" + ) + parent_parser.add_argument( + "-q", "--quiet", action="count", default=0, help="Be quiet" + ) + parent_parser.add_argument( + "--debug-sql", + action="store_true", + default=False, + help="Show All SQL Queries (very verbose output, for debugging only)", + ) + parent_parser.add_argument( + "--pdb", + action="store_true", + default=False, + help="Drop into the pdb debugger on fatal exception", + ) + + subp = parser.add_subparsers(help="Sub-command help", dest="command") + parse_cp = subp.add_parser( + "cp", parents=[parent_parser], help="Copy data files from the cloud" + ) + add_sources_arg(parse_cp).complete = shtab.DIR # type: ignore[attr-defined] + parse_cp.add_argument("output", type=str, help="Output") + parse_cp.add_argument( + "-f", + "--force", + default=False, + action="store_true", + help="Force creating outputs", + ) + parse_cp.add_argument( + "-r", + "-R", + "--recursive", + default=False, + action="store_true", + help="Copy directories recursively", + ) + parse_cp.add_argument( + "--no-glob", + default=False, + action="store_true", + help="Do not expand globs (such as * or ?)", + ) + + parse_clone = subp.add_parser( + "clone", parents=[parent_parser], help="Copy data files from the cloud" + ) + add_sources_arg(parse_clone).complete = shtab.DIR # type: ignore[attr-defined] + parse_clone.add_argument("output", type=str, help="Output") + parse_clone.add_argument( + "-f", + "--force", + default=False, + action="store_true", + help="Force creating outputs", + ) + parse_clone.add_argument( + "-r", + "-R", + "--recursive", + default=False, + action="store_true", + help="Copy directories recursively", + ) + parse_clone.add_argument( + "--no-glob", + default=False, + action="store_true", + help="Do not expand globs (such as * or ?)", + ) + parse_clone.add_argument( + "--no-cp", + default=False, + action="store_true", + help="Do not copy files, just create a dataset", + ) + parse_clone.add_argument( + "--edatachain", + default=False, + action="store_true", + help="Create a .edatachain file", + ) + parse_clone.add_argument( + "--edatachain-file", + help="Use a different filename for the resulting .edatachain file", + ) + + parse_pull = subp.add_parser( + "pull", parents=[parent_parser], help="Pull specific dataset version from SaaS" + ) + parse_pull.add_argument( + "dataset", + type=str, + help="Name and version of remote dataset created in SaaS", + ) + parse_pull.add_argument("-o", "--output", type=str, help="Output") + parse_pull.add_argument( + "-f", + "--force", + default=False, + action="store_true", + help="Force creating outputs", + ) + parse_pull.add_argument( + "-r", + "-R", + "--recursive", + default=False, + action="store_true", + help="Copy directories recursively", + ) + parse_pull.add_argument( + "--no-cp", + default=False, + action="store_true", + help="Do not copy files, just pull a remote dataset into local DB", + ) + parse_pull.add_argument( + "--edatachain", + default=False, + action="store_true", + help="Create .edatachain file", + ) + parse_pull.add_argument( + "--edatachain-file", + help="Use a different filename for the resulting .edatachain file", + ) + + parse_edit_dataset = subp.add_parser( + "edit-dataset", parents=[parent_parser], help="Edit dataset metadata" + ) + parse_edit_dataset.add_argument("name", type=str, help="Dataset name") + parse_edit_dataset.add_argument( + "--new-name", + action="store", + default="", + help="Dataset new name", + ) + parse_edit_dataset.add_argument( + "--description", + action="store", + default="", + help="Dataset description", + ) + parse_edit_dataset.add_argument( + "--labels", + default=[], + nargs="+", + help="Dataset labels", + ) + + subp.add_parser("ls-datasets", parents=[parent_parser], help="List datasets") + rm_dataset_parser = subp.add_parser( + "rm-dataset", parents=[parent_parser], help="Removes dataset" + ) + rm_dataset_parser.add_argument("name", type=str, help="Dataset name") + rm_dataset_parser.add_argument( + "--version", + action="store", + default=None, + type=int, + help="Dataset version", + ) + rm_dataset_parser.add_argument( + "--force", + default=False, + action=BooleanOptionalAction, + help="Force delete registered dataset with all of it's versions", + ) + + dataset_stats_parser = subp.add_parser( + "dataset-stats", parents=[parent_parser], help="Shows basic dataset stats" + ) + dataset_stats_parser.add_argument("name", type=str, help="Dataset name") + dataset_stats_parser.add_argument( + "--version", + action="store", + default=None, + type=int, + help="Dataset version", + ) + dataset_stats_parser.add_argument( + "-b", + "--bytes", + default=False, + action="store_true", + help="Display size in bytes instead of human-readable size", + ) + dataset_stats_parser.add_argument( + "--si", + default=False, + action="store_true", + help="Display size using powers of 1000 not 1024", + ) + + parse_merge_datasets = subp.add_parser( + "merge-datasets", parents=[parent_parser], help="Merges datasets" + ) + parse_merge_datasets.add_argument( + "--src", + action="store", + default=None, + help="Source dataset name", + ) + parse_merge_datasets.add_argument( + "--dst", + action="store", + default=None, + help="Destination dataset name", + ) + parse_merge_datasets.add_argument( + "--src-version", + action="store", + default=None, + type=int, + help="Source dataset version", + ) + parse_merge_datasets.add_argument( + "--dst-version", + action="store", + default=None, + type=int, + help="Destination dataset version", + ) + + parse_ls = subp.add_parser( + "ls", parents=[parent_parser], help="List storage contents" + ) + add_sources_arg(parse_ls, nargs="*") + parse_ls.add_argument( + "-l", + "--long", + action="count", + default=0, + help="List files in the long format", + ) + parse_ls.add_argument( + "--remote", + action="store", + default="", + help="Name of remote to use", + ) + + parse_du = subp.add_parser( + "du", parents=[parent_parser], help="Display space usage" + ) + add_sources_arg(parse_du) + parse_du.add_argument( + "-b", + "--bytes", + default=False, + action="store_true", + help="Display sizes in bytes instead of human-readable sizes", + ) + parse_du.add_argument( + "-d", + "--depth", + "--max-depth", + default=0, + type=int, + metavar="N", + help=( + "Display sizes for N directory depths below the given directory, " + "the default is 0 (summarize provided directory only)." + ), + ) + parse_du.add_argument( + "--si", + default=False, + action="store_true", + help="Display sizes using powers of 1000 not 1024", + ) + + parse_find = subp.add_parser( + "find", parents=[parent_parser], help="Search in a directory hierarchy" + ) + add_sources_arg(parse_find) + parse_find.add_argument( + "--name", + type=str, + action="append", + help="Filename to match pattern.", + ) + parse_find.add_argument( + "--iname", + type=str, + action="append", + help="Like -name but case insensitive.", + ) + parse_find.add_argument( + "--path", + type=str, + action="append", + help="Path to match pattern.", + ) + parse_find.add_argument( + "--ipath", + type=str, + action="append", + help="Like -path but case insensitive.", + ) + parse_find.add_argument( + "--size", + type=str, + help=( + "Filter by size (+ is greater or equal, - is less or equal). " + "Specified size is in bytes, or use a suffix like K, M, G for " + "kilobytes, megabytes, gigabytes, etc." + ), + ) + parse_find.add_argument( + "--type", + type=str, + help='File type: "f" - regular, "d" - directory', + ) + parse_find.add_argument( + "-c", + "--columns", + type=find_columns_type, + default=None, + help=( + "A comma-separated list of columns to print for each result. " + f"Options are: {','.join(FIND_COLUMNS)} (Default: path)" + ), + ) + + parse_index = subp.add_parser( + "index", parents=[parent_parser], help="Index storage location" + ) + add_sources_arg(parse_index) + + subp.add_parser( + "find-stale-storages", + parents=[parent_parser], + help="Finds and marks stale storages", + ) + + show_parser = subp.add_parser( + "show", + parents=[parent_parser], + help="Create a new dataset with a query script", + ) + show_parser.add_argument("name", type=str, help="Dataset name") + show_parser.add_argument( + "--version", + action="store", + default=None, + type=int, + help="Dataset version", + ) + add_show_args(show_parser) + + query_parser = subp.add_parser( + "query", + parents=[parent_parser], + help="Create a new dataset with a query script", + ) + query_parser.add_argument( + "script", metavar="", type=str, help="Filepath for script" + ) + query_parser.add_argument( + "dataset_name", nargs="?", type=str, help="Save result dataset as" + ) + query_parser.add_argument( + "--parallel", + nargs="?", + type=int, + const=-1, + default=None, + metavar="N", + help=( + "Use multiprocessing to run any query script UDFs with N worker processes. " + "N defaults to the CPU count." + ), + ) + add_show_args(query_parser) + query_parser.add_argument( + "-p", + "--param", + metavar="param=value", + nargs=1, + action=KeyValueArgs, + help="Query parameters", + ) + + apply_udf_parser = subp.add_parser( + "apply-udf", parents=[parent_parser], help="Apply UDF" + ) + apply_udf_parser.add_argument("udf", type=str, help="UDF location") + apply_udf_parser.add_argument("source", type=str, help="Source storage or dataset") + apply_udf_parser.add_argument("target", type=str, help="Target dataset name") + apply_udf_parser.add_argument( + "--parallel", + nargs="?", + type=int, + const=-1, + default=None, + metavar="N", + help=( + "Use multiprocessing to run the UDF with N worker processes. " + "N defaults to the CPU count." + ), + ) + apply_udf_parser.add_argument( + "--udf-params", type=str, default=None, help="UDF class parameters" + ) + subp.add_parser( + "clear-cache", parents=[parent_parser], help="Clear the local file cache" + ) + subp.add_parser( + "gc", parents=[parent_parser], help="Garbage collect temporary tables" + ) + + add_completion_parser(subp, [parent_parser]) + return parser + + +def add_completion_parser(subparsers, parents): + parser = subparsers.add_parser( + "completion", + parents=parents, + help="Output shell completion script", + ) + parser.add_argument( + "-s", + "--shell", + help="Shell syntax for completions.", + default="bash", + choices=shtab.SUPPORTED_SHELLS, + ) + + +def get_logging_level(args: Namespace) -> int: + if args.quiet: + return logging.CRITICAL + if args.verbose: + return logging.DEBUG + return logging.INFO + + +def ls_urls( + sources, + catalog: "Catalog", + long: bool = False, + **kwargs, +) -> Iterator[tuple[str, Iterator[str]]]: + curr_dir = None + value_iterables = [] + for next_dir, values in _ls_urls_flat(sources, long, catalog, **kwargs): + if curr_dir is None or next_dir == curr_dir: # type: ignore[unreachable] + value_iterables.append(values) + else: + yield curr_dir, chain(*value_iterables) # type: ignore[unreachable] + value_iterables = [values] + curr_dir = next_dir + if curr_dir is not None: + yield curr_dir, chain(*value_iterables) + + +def _node_data_to_ls_values(row, long_format=False): + from datachain.node import DirType, long_line_str + + name = row[0] + is_dir = row[1] == DirType.DIR + ending = "/" if is_dir else "" + value = name + ending + if long_format: + last_modified = row[2] + owner_name = row[3] + timestamp = last_modified if not is_dir else None + return long_line_str(value, timestamp, owner_name) + return value + + +def _ls_urls_flat( + sources, + long: bool, + catalog: "Catalog", + **kwargs, +) -> Iterator[tuple[str, Iterator[str]]]: + from datachain.client import Client + from datachain.node import long_line_str + + for source in sources: + client_cls = Client.get_implementation(source) + if client_cls.is_root_url(source): + buckets = client_cls.ls_buckets(**catalog.client_config) + if long: + values = (long_line_str(b.name, b.created, "") for b in buckets) + else: + values = (b.name for b in buckets) + yield source, values + else: + found = False + fields = ["name", "dir_type"] + if long: + fields.extend(["last_modified", "owner_name"]) + for data_source, results in catalog.ls([source], fields=fields, **kwargs): + values = (_node_data_to_ls_values(r, long) for r in results) + found = True + yield data_source.dirname(), values + if not found: + raise FileNotFoundError(f"No such file or directory: {source}") + + +def ls_indexed_storages(catalog: "Catalog", long: bool = False) -> Iterator[str]: + from datachain.node import long_line_str + + storage_uris = catalog.ls_storage_uris() + if long: + for uri in storage_uris: + # TODO: add Storage.created so it can be used here + yield long_line_str(uri, None, "") + else: + yield from storage_uris + + +def ls_local( + sources, + long: bool = False, + catalog: Optional["Catalog"] = None, + client_config=None, + **kwargs, +): + if catalog is None: + from .catalog import get_catalog + + catalog = get_catalog(client_config=client_config) + if sources: + actual_sources = list(ls_urls(sources, catalog=catalog, long=long, **kwargs)) + if len(actual_sources) == 1: + for _, entries in actual_sources: + for entry in entries: + print(format_ls_entry(entry)) + else: + first = True + for source, entries in actual_sources: + # print a newline between directory listings + if first: + first = False + else: + print() + if source: + print(f"{source}:") + for entry in entries: + print(format_ls_entry(entry)) + else: + for entry in ls_indexed_storages(catalog, long=long): + print(format_ls_entry(entry)) + + +def format_ls_entry(entry: str) -> str: + if entry.endswith("/") or not entry: + entry = shlex.quote(entry[:-1]) + return f"{entry}/" + return shlex.quote(entry) + + +def ls_remote( + url: str, + username: str, + token: str, + paths: Iterable[str], + long: bool = False, +): + from datachain.node import long_line_str + from datachain.remote.studio import StudioClient + + client = StudioClient(url, username, token) + first = True + for path, response in client.ls(paths): + if not first: + print() + if not response.ok or response.data is None: + print(f"{path}:\n Error: {response.message}\n") + continue + + print(f"{path}:") + if long: + for row in response.data: + entry = long_line_str( + row["name"] + ("/" if row["dir_type"] else ""), + row["last_modified"], + row["owner_name"], + ) + print(format_ls_entry(entry)) + else: + for row in response.data: + entry = row["name"] + ("/" if row["dir_type"] else "") + print(format_ls_entry(entry)) + first = False + + +def ls( + sources, + long: bool = False, + remote: str = "", + config: Optional[Mapping[str, str]] = None, + **kwargs, +): + if config is None: + from .config import get_remote_config, read_config + + config = get_remote_config(read_config(DataChainDir.find().root), remote=remote) + remote_type = config["type"] + if remote_type == "local": + ls_local(sources, long=long, **kwargs) + else: + ls_remote( + config["url"], + config["username"], + config["token"], + sources, + long=long, + ) + + +def ls_datasets(catalog: "Catalog"): + for d in catalog.ls_datasets(): + for v in d.versions: + print(f"{d.name} (v{v.version})") + + +def rm_dataset( + catalog: "Catalog", + name: str, + version: Optional[int] = None, + force: Optional[bool] = False, +): + catalog.remove_dataset(name, version=version, force=force) + + +def dataset_stats( + catalog: "Catalog", + name: str, + version: int, + show_bytes=False, + si=False, +): + stats = catalog.dataset_stats(name, version) + + if stats: + print(f"Number of objects: {stats.num_objects}") + if show_bytes: + print(f"Total objects size: {stats.size}") + else: + print(f"Total objects size: {utils.sizeof_fmt(stats.size, si=si): >7}") + + +def du(catalog: "Catalog", sources, show_bytes=False, si=False, **kwargs): + for path, size in catalog.du(sources, **kwargs): + if show_bytes: + print(f"{size} {path}") + else: + print(f"{utils.sizeof_fmt(size, si=si): >7} {path}") + + +def index( + catalog: "Catalog", + sources, + **kwargs, +): + catalog.index(sources, **kwargs) + + +def show( + catalog: "Catalog", + name: str, + version: Optional[int] = None, + limit: int = 10, + offset: int = 0, + columns: Sequence[str] = (), + no_collapse: bool = False, +) -> None: + from datachain.query import DatasetQuery + from datachain.utils import show_records + + if columns: + columns = ("id", *columns) + query = ( + DatasetQuery(name=name, version=version, catalog=catalog) + .select(*columns) + .limit(limit) + .offset(offset) + ) + records = query.to_records() + show_records(records, collapse_columns=not no_collapse) + + +def query( + catalog: "Catalog", + script: str, + dataset_name: Optional[str] = None, + parallel: Optional[int] = None, + limit: int = 10, + offset: int = 0, + columns: Optional[list[str]] = None, + no_collapse: bool = False, + params: Optional[dict[str, str]] = None, +) -> None: + from datachain.data_storage import JobQueryType, JobStatus + from datachain.utils import show_records + + with open(script, encoding="utf-8") as f: + script_content = f.read() + + if parallel is not None: + # This also sets this environment variable for any subprocesses + os.environ["DATACHAIN_SETTINGS_PARALLEL"] = str(parallel) + + python_version = f"{sys.version_info.major}.{sys.version_info.minor}" + python_executable = sys.executable + + job_id = catalog.metastore.create_job( + name=os.path.basename(script), + query=script_content, + query_type=JobQueryType.PYTHON, + python_version=python_version, + params=params, + ) + + try: + result = catalog.query( + script_content, + python_executable=python_executable, + save_as=dataset_name, + preview_limit=limit, + preview_offset=offset, + preview_columns=columns, + capture_output=False, + params=params, + job_id=job_id, + ) + except Exception as e: + error_message = str(e) + error_stack = traceback.format_exc() + catalog.metastore.set_job_status( + job_id, + JobStatus.FAILED, + error_message=error_message, + error_stack=error_stack, + ) + raise + + catalog.metastore.set_job_status(job_id, JobStatus.COMPLETE, metrics=result.metrics) + + show_records(result.preview, collapse_columns=not no_collapse) + + +def clear_cache(catalog: "Catalog"): + catalog.cache.clear() + + +def garbage_collect(catalog: "Catalog"): + temp_tables = catalog.get_temp_table_names() + if not temp_tables: + print("Nothing to clean up.") + else: + print(f"Garbage collecting {len(temp_tables)} tables.") + catalog.cleanup_temp_tables(temp_tables) + + +def completion(shell: str) -> str: + return shtab.complete( + get_parser(), + shell=shell, + ) + + +def main(argv: Optional[list[str]] = None) -> int: # noqa: C901, PLR0911, PLR0912, PLR0915 + # Required for Windows multiprocessing support + freeze_support() + + parser = get_parser() + args = parser.parse_args(argv) + + if args.internal_run_udf: + from datachain.query.dispatch import udf_entrypoint + + return udf_entrypoint() + + if args.internal_run_udf_worker: + from datachain.query.dispatch import udf_worker_entrypoint + + return udf_worker_entrypoint() + + if args.command is None: + parser.print_help() + return 1 + + from .catalog import get_catalog + + logger.addHandler(logging.StreamHandler()) + logging_level = get_logging_level(args) + logger.setLevel(logging_level) + + client_config = { + "aws_endpoint_url": args.aws_endpoint_url, + "anon": args.anon, + } + + if args.debug_sql: + # This also sets this environment variable for any subprocesses + os.environ["DEBUG_SHOW_SQL_QUERIES"] = "True" + + try: + catalog = get_catalog(client_config=client_config) + if args.command == "cp": + catalog.cp( + args.sources, + args.output, + force=bool(args.force), + update=bool(args.update), + recursive=bool(args.recursive), + edatachain_file=None, + edatachain_only=False, + no_edatachain_file=True, + no_glob=args.no_glob, + ttl=args.ttl, + ) + elif args.command == "clone": + catalog.clone( + args.sources, + args.output, + force=bool(args.force), + update=bool(args.update), + recursive=bool(args.recursive), + no_glob=args.no_glob, + ttl=args.ttl, + no_cp=args.no_cp, + edatachain=args.edatachain, + edatachain_file=args.edatachain_file, + ) + elif args.command == "pull": + catalog.pull_dataset( + args.dataset, + args.output, + no_cp=args.no_cp, + force=bool(args.force), + edatachain=args.edatachain, + edatachain_file=args.edatachain_file, + ) + elif args.command == "edit-dataset": + catalog.edit_dataset( + args.name, + description=args.description, + new_name=args.new_name, + labels=args.labels, + ) + elif args.command == "merge-datasets": + catalog.merge_datasets( + catalog.get_dataset(args.src), + catalog.get_dataset(args.dst), + args.src_version, + dst_version=args.dst_version, + ) + elif args.command == "ls": + ls( + args.sources, + long=bool(args.long), + remote=args.remote, + ttl=args.ttl, + update=bool(args.update), + client_config=client_config, + ) + elif args.command == "ls-datasets": + ls_datasets(catalog) + elif args.command == "show": + show( + catalog, + args.name, + args.version, + limit=args.limit, + offset=args.offset, + columns=args.columns, + no_collapse=args.no_collapse, + ) + elif args.command == "rm-dataset": + rm_dataset(catalog, args.name, version=args.version, force=args.force) + elif args.command == "dataset-stats": + dataset_stats( + catalog, + args.name, + args.version, + show_bytes=args.bytes, + si=args.si, + ) + elif args.command == "du": + du( + catalog, + args.sources, + show_bytes=args.bytes, + depth=args.depth, + si=args.si, + ttl=args.ttl, + update=bool(args.update), + client_config=client_config, + ) + elif args.command == "find": + results_found = False + for result in catalog.find( + args.sources, + ttl=args.ttl, + update=bool(args.update), + names=args.name, + inames=args.iname, + paths=args.path, + ipaths=args.ipath, + size=args.size, + typ=args.type, + columns=args.columns, + ): + print(result) + results_found = True + if not results_found: + print("No results") + elif args.command == "index": + index( + catalog, + args.sources, + ttl=args.ttl, + update=bool(args.update), + ) + elif args.command == "completion": + print(completion(args.shell)) + elif args.command == "find-stale-storages": + catalog.find_stale_storages() + elif args.command == "query": + query( + catalog, + args.script, + dataset_name=args.dataset_name, + parallel=args.parallel, + limit=args.limit, + offset=args.offset, + columns=args.columns, + no_collapse=args.no_collapse, + params=args.param, + ) + elif args.command == "apply-udf": + catalog.apply_udf( + args.udf, args.source, args.target, args.parallel, args.udf_params + ) + elif args.command == "clear-cache": + clear_cache(catalog) + elif args.command == "gc": + garbage_collect(catalog) + else: + print(f"invalid command: {args.command}", file=sys.stderr) + return 1 + return 0 + except BrokenPipeError: + # Python flushes standard streams on exit; redirect remaining output + # to devnull to avoid another BrokenPipeError at shutdown + # See: https://docs.python.org/3/library/signal.html#note-on-sigpipe + devnull = os.open(os.devnull, os.O_WRONLY) + os.dup2(devnull, sys.stdout.fileno()) + return 141 # 128 + 13 (SIGPIPE) + except (KeyboardInterrupt, Exception) as exc: + if isinstance(exc, KeyboardInterrupt): + msg = "Operation cancelled by the user" + else: + msg = str(exc) + print("Error:", msg, file=sys.stderr) + if logging_level <= logging.DEBUG: + traceback.print_exception( + type(exc), + exc, + exc.__traceback__, + file=sys.stderr, + ) + if args.pdb: + import pdb # noqa: T100 + + pdb.post_mortem() + return 1 diff --git a/src/datachain/cli_utils.py b/src/datachain/cli_utils.py new file mode 100644 index 000000000..5fb36537b --- /dev/null +++ b/src/datachain/cli_utils.py @@ -0,0 +1,72 @@ +from argparse import SUPPRESS, Action, ArgumentError, _AppendAction + + +class BooleanOptionalAction(Action): + """ + Creates --[no-]option style bool options. + + Defined here since it doesn't exist in argparse in Python 3.8. + + Copied from: + https://github.com/python/cpython/blob/c33aaa9d559398bbf2b80e891bf3ae6a716e4b8c/Lib/argparse.py#L863-L901 + """ + + def __init__( + self, + option_strings, + dest, + default=None, + type=None, + choices=None, + required=False, + help=None, + metavar=None, + ): + _option_strings = [] + for option_string in option_strings: + _option_strings.append(option_string) + + if option_string.startswith("--"): + option_string = "--no-" + option_string[2:] + _option_strings.append(option_string) + + if help is not None and default is not None and default is not SUPPRESS: + help += " (default: %(default)s)" + + super().__init__( + option_strings=_option_strings, + dest=dest, + nargs=0, + default=default, + type=type, + choices=choices, + required=required, + help=help, + metavar=metavar, + ) + + def __call__(self, parser, namespace, values, option_string=None): + if option_string in self.option_strings: + setattr(namespace, self.dest, not option_string.startswith("--no-")) + + def format_usage(self): + return " | ".join(self.option_strings) + + +class CommaSeparatedArgs(_AppendAction): # pylint: disable=protected-access + def __call__(self, parser, namespace, values, option_string=None): + items = getattr(namespace, self.dest) or [] + items.extend(v for value in values.split(",") if (v := value.strip())) + setattr(namespace, self.dest, list(dict.fromkeys(items))) + + +class KeyValueArgs(_AppendAction): # pylint: disable=protected-access + def __call__(self, parser, namespace, values, option_string=None): + items = getattr(namespace, self.dest) or {} + for raw_value in filter(bool, values): + key, sep, value = raw_value.partition("=") + if not key or not sep or value == "": + raise ArgumentError(self, f"expected 'key=value', got {raw_value!r}") + items[key.strip()] = value + + setattr(namespace, self.dest, items) diff --git a/src/datachain/client/__init__.py b/src/datachain/client/__init__.py new file mode 100644 index 000000000..e922c949a --- /dev/null +++ b/src/datachain/client/__init__.py @@ -0,0 +1,4 @@ +from .fsspec import Client +from .s3 import ClientS3 + +__all__ = ["Client", "ClientS3"] diff --git a/src/datachain/client/azure.py b/src/datachain/client/azure.py new file mode 100644 index 000000000..0c21bbb99 --- /dev/null +++ b/src/datachain/client/azure.py @@ -0,0 +1,66 @@ +import posixpath +from typing import Any + +from adlfs import AzureBlobFileSystem +from tqdm import tqdm + +from datachain.node import Entry + +from .fsspec import DELIMITER, Client, ResultQueue + + +class AzureClient(Client): + FS_CLASS = AzureBlobFileSystem + PREFIX = "az://" + protocol = "az" + + def convert_info(self, v: dict[str, Any], parent: str) -> Entry: + version_id = v.get("version_id") + name = v.get("name", "").split(DELIMITER)[-1] + if version_id: + version_suffix = f"?versionid={version_id}" + if name.endswith(version_suffix): + name = name[: -len(version_suffix)] + return Entry.from_file( + parent=parent, + name=name, + etag=v.get("etag", "").strip('"'), + version=version_id or "", + is_latest=version_id is None or bool(v.get("is_current_version")), + last_modified=v["last_modified"], + size=v.get("size", ""), + ) + + async def _fetch_flat(self, start_prefix: str, result_queue: ResultQueue) -> None: + prefix = start_prefix + if prefix: + prefix = prefix.lstrip(DELIMITER) + DELIMITER + found = False + try: + with tqdm(desc=f"Listing {self.uri}", unit=" objects") as pbar: + async with self.fs.service_client.get_container_client( + container=self.name + ) as container_client: + async for page in container_client.list_blobs( + include=["metadata", "versions"], name_starts_with=prefix + ).by_page(): + entries = [] + async for b in page: + found = True + if not self._is_valid_key(b["name"]): + continue + info = (await self.fs._details([b]))[0] + full_path = info["name"] + parent = posixpath.dirname(self.rel_path(full_path)) + entries.append(self.convert_info(info, parent)) + if entries: + await result_queue.put(entries) + pbar.update(len(entries)) + if not found: + raise FileNotFoundError( + f"Unable to resolve remote path: {prefix}" + ) + finally: + result_queue.put_nowait(None) + + _fetch_default = _fetch_flat diff --git a/src/datachain/client/fileslice.py b/src/datachain/client/fileslice.py new file mode 100644 index 000000000..c162f402c --- /dev/null +++ b/src/datachain/client/fileslice.py @@ -0,0 +1,106 @@ +import io +from typing import IO + +from fsspec.callbacks import DEFAULT_CALLBACK, Callback + + +class FileWrapper(io.RawIOBase): + """Instrumented wrapper around an existing file object. + + It wraps the file's read() method to update the callback with the number of + bytes read. + + It assumes exclusive access to the underlying file object and closes it when it + gets closed itself. + """ + + def __init__(self, fileobj, callback: Callback = DEFAULT_CALLBACK): + self.fileobj = fileobj + self.callback = callback + + def readable(self) -> bool: + return True + + def writable(self) -> bool: + return False + + def seekable(self) -> bool: + return self.fileobj.seekable() + + def tell(self): + """Return the current file position.""" + return self.fileobj.tell() + + def seek(self, position, whence=io.SEEK_SET): + """Seek to a position in the file.""" + return self.fileobj.seek(position, whence) + + def readinto(self, b) -> int: + res = self.fileobj.readinto(b) + self.callback.relative_update(res) + return res + + def close(self): + self.fileobj.close() + super().close() + + +class FileSlice(io.RawIOBase): + """A thin wrapper around an existing file object that provides a part of its data + as an individual file object. + + It assumes exclusive access to the underlying file object and closes it when it + gets closed itself. + """ + + def __init__(self, fileobj: IO[bytes], offset: int, size: int, name: str): + self.fileobj = fileobj + self.offset = offset + self.size = size + self.position = 0 + self.name = name + + def readable(self): + return True + + def writable(self): + return False + + def seekable(self): + return self.fileobj.seekable() + + def tell(self): + """Return the current file position.""" + return self.position + + def seek(self, position, whence=io.SEEK_SET): + """Seek to a position in the file.""" + if whence == io.SEEK_SET: + self.position = min(max(position, 0), self.size) + elif whence == io.SEEK_CUR: + if position < 0: + self.position = max(self.position + position, 0) + else: + self.position = min(self.position + position, self.size) + elif whence == io.SEEK_END: + self.position = max(min(self.size + position, self.size), 0) + else: + raise ValueError("Invalid argument") + return self.position + + def readinto(self, b): + max_size = self.size - self.position + if max_size <= 0: + return 0 + self.fileobj.seek(self.offset + self.position) + if len(b) > max_size: + b = memoryview(b)[:max_size] + res = self.fileobj.readinto(b) + if res != len(b): + raise RuntimeError("unexpected end of data") + self.position += res + return res + + def close(self): + self.fileobj.close() + super().close() diff --git a/src/datachain/client/fsspec.py b/src/datachain/client/fsspec.py new file mode 100644 index 000000000..3efe23db4 --- /dev/null +++ b/src/datachain/client/fsspec.py @@ -0,0 +1,407 @@ +import asyncio +import functools +import logging +import multiprocessing +import os +import posixpath +import re +import sys +from abc import ABC, abstractmethod +from collections.abc import AsyncIterator, Iterator, Sequence +from datetime import datetime +from shutil import copy2 +from typing import ( + TYPE_CHECKING, + Any, + BinaryIO, + ClassVar, + NamedTuple, + Optional, +) +from urllib.parse import urlparse + +from botocore.exceptions import ClientError +from dvc_objects.fs.system import reflink +from fsspec.asyn import get_loop, sync +from fsspec.callbacks import DEFAULT_CALLBACK, Callback +from tqdm import tqdm + +from datachain.cache import DataChainCache, UniqueId +from datachain.client.fileslice import FileSlice, FileWrapper +from datachain.error import ClientError as DataChainClientError +from datachain.node import Entry +from datachain.nodes_fetcher import NodesFetcher +from datachain.nodes_thread_pool import NodeChunk +from datachain.storage import StorageURI + +if TYPE_CHECKING: + from fsspec.spec import AbstractFileSystem + + from datachain.data_storage import AbstractMetastore + +logger = logging.getLogger("datachain") + +FETCH_WORKERS = 100 +DELIMITER = "/" # Path delimiter. + +DATA_SOURCE_URI_PATTERN = re.compile(r"^[\w]+:\/\/.*$") + +ResultQueue = asyncio.Queue[Optional[Sequence[Entry]]] + + +def _is_win_local_path(uri: str) -> bool: + if sys.platform == "win32": + if len(uri) >= 1 and uri[0] == "\\": + return True + if ( + len(uri) >= 3 + and uri[1] == ":" + and (uri[2] == "/" or uri[2] == "\\") + and uri[0].isalpha() + ): + return True + return False + + +class Bucket(NamedTuple): + name: str + uri: StorageURI + created: Optional[datetime] + + +class Client(ABC): + MAX_THREADS = multiprocessing.cpu_count() + FS_CLASS: ClassVar[type["AbstractFileSystem"]] + PREFIX: ClassVar[str] + protocol: ClassVar[str] + + def __init__( + self, name: str, fs_kwargs: dict[str, Any], cache: DataChainCache + ) -> None: + self.name = name + self.fs_kwargs = fs_kwargs + self._fs: Optional[AbstractFileSystem] = None + self.cache = cache + self.uri = self.get_uri(self.name) + + @staticmethod + def get_implementation(url: str) -> type["Client"]: + from .azure import AzureClient + from .gcs import GCSClient + from .local import FileClient + from .s3 import ClientS3 + + protocol = urlparse(url).scheme + + if not protocol or _is_win_local_path(url): + return FileClient + + protocol = protocol.lower() + if protocol == ClientS3.protocol: + return ClientS3 + if protocol == GCSClient.protocol: + return GCSClient + if protocol == AzureClient.protocol: + return AzureClient + if protocol == FileClient.protocol: + return FileClient + + raise NotImplementedError(f"Unsupported protocol: {protocol}") + + @staticmethod + def is_data_source_uri(name: str) -> bool: + # Returns True if name is one of supported data sources URIs, e.g s3 bucket + return DATA_SOURCE_URI_PATTERN.match(name) is not None + + @staticmethod + def parse_url( + source: str, + metastore: "AbstractMetastore", + cache: DataChainCache, + **kwargs, + ) -> tuple["Client", str]: + cls = Client.get_implementation(source) + storage_url, rel_path = cls.split_url(source) + client = cls.from_name(storage_url, metastore, cache, kwargs) + return client, rel_path + + @classmethod + def create_fs(cls, **kwargs) -> "AbstractFileSystem": + kwargs.setdefault("version_aware", True) + fs = cls.FS_CLASS(**kwargs) + fs.invalidate_cache() + return fs + + @classmethod + def from_name( + cls, + name: str, + metastore: "AbstractMetastore", + cache: DataChainCache, + kwargs: dict[str, Any], + ) -> "Client": + return cls(name, kwargs, cache) + + @classmethod + def from_source( + cls, + uri: StorageURI, + cache: DataChainCache, + **kwargs, + ) -> "Client": + return cls(cls.FS_CLASS._strip_protocol(uri), kwargs, cache) + + @classmethod + def ls_buckets(cls, **kwargs) -> Iterator[Bucket]: + for entry in cls.create_fs(**kwargs).ls(cls.PREFIX, detail=True): + name = entry["name"].rstrip("/") + yield Bucket( + name=name, + uri=StorageURI(f"{cls.PREFIX}{name}"), + created=entry.get("CreationDate"), + ) + + @classmethod + def is_root_url(cls, url) -> bool: + return url == cls.PREFIX + + @classmethod + def get_uri(cls, name) -> StorageURI: + return StorageURI(f"{cls.PREFIX}{name}") + + @classmethod + def split_url(cls, url: str) -> tuple[str, str]: + fill_path = url[len(cls.PREFIX) :] + path_split = fill_path.split("/", 1) + bucket = path_split[0] + path = path_split[1] if len(path_split) > 1 else "" + return bucket, path + + @property + def fs(self) -> "AbstractFileSystem": + if not self._fs: + self._fs = self.create_fs(**self.fs_kwargs) + return self._fs + + def url(self, path: str, expires: int = 3600, **kwargs) -> str: + return self.fs.sign(self.get_full_path(path), expiration=expires, **kwargs) + + async def get_current_etag(self, uid: UniqueId) -> str: + info = await self.fs._info(self.get_full_path(uid.path)) + return self.convert_info(info, "").etag + + async def get_size(self, path: str) -> int: + return await self.fs._size(path) + + async def get_file(self, lpath, rpath, callback): + return await self.fs._get_file(lpath, rpath, callback=callback) + + async def scandir( + self, start_prefix: str, method: str = "default" + ) -> AsyncIterator[Sequence[Entry]]: + try: + impl = getattr(self, f"_fetch_{method}") + except AttributeError: + raise ValueError(f"Unknown indexing method '{method}'") from None + result_queue: ResultQueue = asyncio.Queue() + loop = get_loop() + main_task = loop.create_task(impl(start_prefix, result_queue)) + while (entry := await result_queue.get()) is not None: + yield entry + await main_task + + async def _fetch_nested(self, start_prefix: str, result_queue: ResultQueue) -> None: + progress_bar = tqdm(desc=f"Listing {self.uri}", unit=" objects") + loop = get_loop() + + queue: asyncio.Queue[str] = asyncio.Queue() + queue.put_nowait(start_prefix) + + async def process(queue) -> None: + while True: + prefix = await queue.get() + try: + subdirs = await self._fetch_dir(prefix, progress_bar, result_queue) + for subdir in subdirs: + queue.put_nowait(subdir) + except Exception: + while not queue.empty(): + queue.get_nowait() + queue.task_done() + raise + + finally: + queue.task_done() + + try: + workers: list[asyncio.Task] = [ + loop.create_task(process(queue)) for _ in range(FETCH_WORKERS) + ] + + # Wait for all fetch tasks to complete + await queue.join() + # Stop the workers + excs = [] + for worker in workers: + if worker.done() and (exc := worker.exception()): + excs.append(exc) + else: + worker.cancel() + if excs: + raise excs[0] + except ClientError as exc: + raise DataChainClientError( + exc.response.get("Error", {}).get("Message") or exc, + exc.response.get("Error", {}).get("Code"), + ) from exc + finally: + # This ensures the progress bar is closed before any exceptions are raised + progress_bar.close() + result_queue.put_nowait(None) + + async def _fetch_default( + self, start_prefix: str, result_queue: ResultQueue + ) -> None: + await self._fetch_nested(start_prefix, result_queue) + + async def _fetch_dir(self, prefix, pbar, result_queue) -> set[str]: + path = f"{self.name}/{prefix}" + infos = await self.ls_dir(path) + files = [] + subdirs = set() + for info in infos: + full_path = info["name"] + subprefix = self.rel_path(full_path) + if prefix.strip(DELIMITER) == subprefix.strip(DELIMITER): + continue + if info["type"] == "directory": + subdirs.add(subprefix) + else: + files.append(self.convert_info(info, prefix)) + if files: + await result_queue.put(files) + found_count = len(subdirs) + len(files) + pbar.update(found_count) + return subdirs + + @staticmethod + def _is_valid_key(key: str) -> bool: + """ + Check if the key looks like a valid path. + + Invalid keys are ignored when indexing. + """ + return not (key.startswith("/") or key.endswith("/") or "//" in key) + + async def ls_dir(self, path): + return await self.fs._ls(path, detail=True, versions=True) + + def rel_path(self, path: str) -> str: + return self.fs.split_path(path)[1] + + def get_full_path(self, rel_path: str) -> str: + return f"{self.PREFIX}{self.name}/{rel_path}" + + @abstractmethod + def convert_info(self, v: dict[str, Any], parent: str) -> Entry: ... + + def fetch_nodes( + self, + nodes, + shared_progress_bar=None, + ) -> None: + fetcher = NodesFetcher(self, self.MAX_THREADS, self.cache) + chunk_gen = NodeChunk(self.cache, self.uri, nodes) + fetcher.run(chunk_gen, shared_progress_bar) + + def instantiate_object( + self, + uid: UniqueId, + dst: str, + progress_bar: tqdm, + force: bool = False, + ) -> None: + if os.path.exists(dst): + if force: + os.remove(dst) + else: + progress_bar.close() + raise FileExistsError(f"Path {dst} already exists") + self.do_instantiate_object(uid, dst) + + def do_instantiate_object(self, uid: "UniqueId", dst: str) -> None: + src = self.cache.get_path(uid) + assert src is not None + + try: + reflink(src, dst) + except OSError: + # Default to copy if reflinks are not supported + copy2(src, dst) + + def open_object( + self, uid: UniqueId, use_cache: bool = True, cb: Callback = DEFAULT_CALLBACK + ) -> BinaryIO: + """Open a file, including files in tar archives.""" + location = uid.get_parsed_location() + if use_cache and (cache_path := self.cache.get_path(uid)): + return open(cache_path, mode="rb") # noqa: SIM115 + if location and location["vtype"] == "tar": + return self._open_tar(uid, use_cache=True) + return FileWrapper(self.fs.open(self.get_full_path(uid.path)), cb) # type: ignore[return-value] + + def _open_tar(self, uid: UniqueId, use_cache: bool = True): + location = uid.get_parsed_location() + assert location + + offset = location["offset"] + size = location["size"] + parent = location["parent"] + + parent_uid = UniqueId( + parent["source"], + parent["parent"], + parent["name"], + parent["etag"], + parent["size"], + parent["vtype"], + parent["location"], + ) + f = self.open_object(parent_uid, use_cache=use_cache) + return FileSlice(f, offset, size, posixpath.basename(uid.path)) + + def download(self, uid: UniqueId, *, callback: Callback = DEFAULT_CALLBACK) -> None: + sync(get_loop(), functools.partial(self._download, uid, callback=callback)) + + async def _download(self, uid: UniqueId, *, callback: "Callback" = None) -> None: + if self.cache.contains(uid): + # Already in cache, so there's nothing to do. + return + await self._put_in_cache(uid, callback=callback) + + def put_in_cache(self, uid: UniqueId, *, callback: "Callback" = None) -> None: + sync(get_loop(), functools.partial(self._put_in_cache, uid, callback=callback)) + + async def _put_in_cache( + self, uid: UniqueId, *, callback: "Callback" = None + ) -> None: + location = uid.get_parsed_location() + if location and location["vtype"] == "tar": + loop = asyncio.get_running_loop() + await loop.run_in_executor( + None, functools.partial(self._download_from_tar, uid, callback=callback) + ) + return + if uid.etag: + etag = await self.get_current_etag(uid) + if uid.etag != etag: + raise FileNotFoundError( + f"Invalid etag for {uid.storage}/{uid.path}: " + f"expected {uid.etag}, got {etag}" + ) + await self.cache.download(uid, self, callback=callback) + + def _download_from_tar(self, uid, *, callback: "Callback" = None): + with self._open_tar(uid, use_cache=False) as f: + contents = f.read() + self.cache.store_data(uid, contents) diff --git a/src/datachain/client/gcs.py b/src/datachain/client/gcs.py new file mode 100644 index 000000000..28989da2f --- /dev/null +++ b/src/datachain/client/gcs.py @@ -0,0 +1,132 @@ +import asyncio +import json +import os +import posixpath +from collections.abc import Iterable +from datetime import datetime +from typing import Any, Optional, cast + +from dateutil.parser import isoparse +from gcsfs import GCSFileSystem +from tqdm import tqdm + +from datachain.node import Entry + +from .fsspec import DELIMITER, Client, ResultQueue + +# Patch gcsfs for consistency with s3fs +GCSFileSystem.set_session = GCSFileSystem._set_session +PageQueue = asyncio.Queue[Optional[Iterable[dict[str, Any]]]] + + +class GCSClient(Client): + FS_CLASS = GCSFileSystem + PREFIX = "gs://" + protocol = "gs" + + @classmethod + def create_fs(cls, **kwargs) -> GCSFileSystem: + if os.environ.get("DATACHAIN_GCP_CREDENTIALS"): + kwargs["token"] = json.loads(os.environ["DATACHAIN_GCP_CREDENTIALS"]) + if kwargs.pop("anon", False): + kwargs["token"] = "anon" # noqa: S105 + + return cast(GCSFileSystem, super().create_fs(**kwargs)) + + @staticmethod + def parse_timestamp(timestamp: str) -> datetime: + """ + Parse timestamp string returned by GCSFileSystem. + + This ensures that the passed timestamp is timezone aware. + """ + dt = isoparse(timestamp) + assert dt.tzinfo is not None + return dt + + async def _fetch_flat(self, start_prefix: str, result_queue: ResultQueue) -> None: + prefix = start_prefix + if prefix: + prefix = prefix.lstrip(DELIMITER) + DELIMITER + found = False + try: + page_queue: PageQueue = asyncio.Queue(2) + consumer = asyncio.create_task( + self._process_pages(page_queue, result_queue) + ) + try: + await self._get_pages(prefix, page_queue) + found = await consumer + if not found: + raise FileNotFoundError(f"Unable to resolve remote path: {prefix}") + finally: + consumer.cancel() # In case _get_pages() raised + finally: + result_queue.put_nowait(None) + + _fetch_default = _fetch_flat + + async def _process_pages( + self, page_queue: PageQueue, result_queue: ResultQueue + ) -> bool: + found = False + with tqdm(desc=f"Listing {self.uri}", unit=" objects") as pbar: + while (page := await page_queue.get()) is not None: + if page: + found = True + entries = [ + self._entry_from_dict(d) + for d in page + if self._is_valid_key(d["name"]) + ] + if entries: + await result_queue.put(entries) + pbar.update(len(entries)) + return found + + async def _get_pages(self, path: str, page_queue: PageQueue) -> None: + page_size = 5000 + try: + next_page_token = None + while True: + page = await self.fs._call( + "GET", + "b/{}/o", + self.name, + delimiter="", + prefix=path, + maxResults=page_size, + pageToken=next_page_token, + json_out=True, + versions="true", + ) + assert page["kind"] == "storage#objects" + await page_queue.put(page.get("items", [])) + next_page_token = page.get("nextPageToken") + if next_page_token is None: + break + finally: + await page_queue.put(None) + + def _entry_from_dict(self, d: dict[str, Any]) -> Entry: + info = self.fs._process_object(self.name, d) + full_path = info["name"] + subprefix = self.rel_path(full_path) + parent = posixpath.dirname(subprefix) + return self.convert_info(info, parent) + + def convert_info(self, v: dict[str, Any], parent: str) -> Entry: + name = v.get("name", "").split(DELIMITER)[-1] + if "generation" in v: + gen = f"#{v['generation']}" + if name.endswith(gen): + name = name[: -len(gen)] + return Entry.from_file( + parent=parent, + name=name, + etag=v.get("etag", ""), + version=v.get("generation", ""), + is_latest=not v.get("timeDeleted"), + last_modified=self.parse_timestamp(v["updated"]), + size=v.get("size", ""), + ) diff --git a/src/datachain/client/local.py b/src/datachain/client/local.py new file mode 100644 index 000000000..268f4662c --- /dev/null +++ b/src/datachain/client/local.py @@ -0,0 +1,166 @@ +import os +import posixpath +from datetime import datetime, timezone +from pathlib import Path +from typing import TYPE_CHECKING, Any +from urllib.parse import urlparse + +from fsspec.implementations.local import LocalFileSystem + +from datachain.node import Entry +from datachain.storage import StorageURI + +from .fsspec import Client + +if TYPE_CHECKING: + from datachain.data_storage import AbstractMetastore + + +class FileClient(Client): + FS_CLASS = LocalFileSystem + PREFIX = "file://" + protocol = "file" + + def __init__( + self, name: str, fs_kwargs: dict[str, Any], cache, use_symlinks: bool = False + ) -> None: + super().__init__(name, fs_kwargs, cache) + self.use_symlinks = use_symlinks + + def url(self, path: str, expires: int = 3600, **kwargs) -> str: + raise TypeError("Signed urls are not implemented for local file system") + + @classmethod + def get_uri(cls, name) -> StorageURI: + """ + This returns root of FS as uri, e.g + Linux & Mac : file:/// + Windows: file:///C:/ + """ + return StorageURI(Path(name).as_uri()) + + @staticmethod + def root_dir() -> str: + """ + Returns file system root path. + Linux & MacOS: / + Windows: C:/ + """ + return Path.cwd().anchor.replace(os.sep, posixpath.sep) + + @staticmethod + def root_path() -> Path: + return Path(FileClient.root_dir()) + + @classmethod + def ls_buckets(cls, **kwargs): + return [] + + @classmethod + def path_to_uri(cls, path: str) -> str: + """ + Resolving path, that can be absolute or relative, to file URI which + starts with file:/// prefix + In unix like systems we support home shortcut as well. + Examples: + ./animals -> file:///home/user/working_dir/animals + ~/animals -> file:///home/user/animals + /home/user/animals -> file:///home/user/animals + /home/user/animals/ -> file:///home/user/animals/ + C:\\windows\animals -> file:///C:/windows/animals + """ + uri = Path(path).expanduser().absolute().resolve().as_uri() + if path[-1] == os.sep: + # we should keep os separator from the end of the path + uri += "/" # in uri (file:///...) all separators are / regardless of os + + return uri + + @classmethod + def split_url(cls, url: str) -> tuple[str, str]: + """ + Splits url into two components: + 1. root of the FS which will later on become the name of the storage + 2. path which will later on become partial path + Note that URL needs to be have file:/// protocol. + Examples: + file:///tmp/dir -> / + tmp/dir + file:///c:/windows/files -> c:/ + windows/files + """ + parsed = urlparse(url) + if parsed.scheme == "file": + scheme, rest = url.split(":", 1) + uri = f"{scheme.lower()}:{rest}" + else: + uri = cls.path_to_uri(url) + + return cls.root_dir(), uri.removeprefix(cls.root_path().as_uri()) + + @classmethod + def from_name( + cls, name: str, metastore: "AbstractMetastore", cache, kwargs + ) -> "FileClient": + use_symlinks = kwargs.pop("use_symlinks", False) + return cls(name, kwargs, cache, use_symlinks=use_symlinks) + + @classmethod + def from_source( + cls, + uri: str, + cache, + use_symlinks: bool = False, + **kwargs, + ) -> "FileClient": + return cls( + LocalFileSystem._strip_protocol(uri), + kwargs, + cache, + use_symlinks=use_symlinks, + ) + + async def get_current_etag(self, uid) -> str: + info = self.fs.info(self.get_full_path(uid.path)) + return self.convert_info(info, "").etag + + async def get_size(self, path: str) -> int: + return self.fs.size(path) + + async def get_file(self, lpath, rpath, callback): + return self.fs.get_file(lpath, rpath, callback=callback) + + async def ls_dir(self, path): + return self.fs.ls(path, detail=True) + + def rel_path(self, path): + return posixpath.relpath(path, self.name) + + def get_full_path(self, rel_path): + full_path = Path(self.name, rel_path).as_posix() + if rel_path.endswith("/") or not rel_path: + full_path += "/" + return full_path + + def convert_info(self, v: dict[str, Any], parent: str) -> Entry: + name = posixpath.basename(v["name"]) + return Entry.from_file( + parent=parent, + name=name, + etag=v["mtime"].hex(), + is_latest=True, + last_modified=datetime.fromtimestamp(v["mtime"], timezone.utc), + size=v.get("size", ""), + ) + + def fetch_nodes( + self, + nodes, + shared_progress_bar=None, + ) -> None: + if not self.use_symlinks: + super().fetch_nodes(nodes, shared_progress_bar) + + def do_instantiate_object(self, uid, dst): + if self.use_symlinks: + os.symlink(Path(self.name, uid.path), dst) + else: + super().do_instantiate_object(uid, dst) diff --git a/src/datachain/client/s3.py b/src/datachain/client/s3.py new file mode 100644 index 000000000..50bbc55e4 --- /dev/null +++ b/src/datachain/client/s3.py @@ -0,0 +1,173 @@ +import asyncio +import posixpath +from typing import Any, cast + +from botocore.exceptions import NoCredentialsError +from s3fs import S3FileSystem +from tqdm import tqdm + +from datachain.node import Entry + +from .fsspec import DELIMITER, Client, ResultQueue + +UPDATE_CHUNKSIZE = 1000 + + +class ClientS3(Client): + FS_CLASS = S3FileSystem + PREFIX = "s3://" + protocol = "s3" + + @classmethod + def create_fs(cls, **kwargs) -> S3FileSystem: + if "aws_endpoint_url" in kwargs: + kwargs.setdefault("client_kwargs", {}).setdefault( + "endpoint_url", kwargs.pop("aws_endpoint_url") + ) + if "aws_key" in kwargs: + kwargs.setdefault("key", kwargs.pop("aws_key")) + if "aws_secret" in kwargs: + kwargs.setdefault("secret", kwargs.pop("aws_secret")) + if "aws_token" in kwargs: + kwargs.setdefault("token", kwargs.pop("aws_token")) + + # caching bucket regions to use the right one in signed urls, otherwise + # it tries to randomly guess and creates wrong signature + kwargs.setdefault("cache_regions", True) + + # We want to use newer v4 signature version since regions added after + # 2014 are not going to support v2 which is the older one. + # All regions support v4. + kwargs.setdefault("config_kwargs", {}).setdefault("signature_version", "s3v4") + + if not kwargs.get("anon"): + try: + # Run an inexpensive check to see if credentials are available + super().create_fs(**kwargs).sign("s3://bucket/object") + except NoCredentialsError: + kwargs["anon"] = True + except NotImplementedError: + pass + + return cast(S3FileSystem, super().create_fs(**kwargs)) + + async def _fetch_flat(self, start_prefix: str, result_queue: ResultQueue) -> None: + async def get_pages(it, page_queue): + try: + async for page in it: + await page_queue.put(page.get(contents_key, [])) + finally: + await page_queue.put(None) + + async def process_pages(page_queue, result_queue): + found = False + with tqdm(desc=f"Listing {self.uri}", unit=" objects") as pbar: + while (res := await page_queue.get()) is not None: + if res: + found = True + entries = [ + self._entry_from_boto(d, self.name, versions) + for d in res + if self._is_valid_key(d["Key"]) + ] + if entries: + await result_queue.put(entries) + pbar.update(len(entries)) + if not found: + raise FileNotFoundError(f"Unable to resolve remote path: {prefix}") + + try: + prefix = start_prefix + if prefix: + prefix = prefix.lstrip(DELIMITER) + DELIMITER + versions = True + fs = self.fs + await fs.set_session() + s3 = await fs.get_s3(self.name) + if versions: + method = "list_object_versions" + contents_key = "Versions" + else: + method = "list_objects_v2" + contents_key = "Contents" + pag = s3.get_paginator(method) + it = pag.paginate( + Bucket=self.name, + Prefix=prefix, + Delimiter="", + ) + page_queue: asyncio.Queue[list] = asyncio.Queue(2) + consumer = asyncio.create_task(process_pages(page_queue, result_queue)) + try: + await get_pages(it, page_queue) + await consumer + finally: + consumer.cancel() # In case get_pages() raised + finally: + result_queue.put_nowait(None) + + async def _fetch_default( + self, start_prefix: str, result_queue: ResultQueue + ) -> None: + await self._fetch_flat(start_prefix, result_queue) + + def _entry_from_boto(self, v, bucket, versions=False): + parent, name = posixpath.split(v["Key"]) + return Entry.from_file( + parent=parent, + name=name, + etag=v.get("ETag", "").strip('"'), + version=ClientS3.clean_s3_version(v.get("VersionId", "")), + is_latest=v.get("IsLatest", True), + last_modified=v.get("LastModified", ""), + size=v["Size"], + owner_name=v.get("Owner", {}).get("DisplayName", ""), + owner_id=v.get("Owner", {}).get("ID", ""), + ) + + async def _fetch_dir( + self, + prefix, + pbar, + result_queue, + ): + if prefix: + prefix = prefix.lstrip(DELIMITER) + DELIMITER + files = [] + subdirs = set() + found = False + async for info in self.fs._iterdir(self.name, prefix=prefix, versions=True): + full_path = info["name"] + _, subprefix, _ = self.fs.split_path(full_path) + if prefix.strip(DELIMITER) == subprefix.strip(DELIMITER): + found = True + continue + if info["type"] == "directory": + subdirs.add(subprefix) + else: + files.append(self.convert_info(info, prefix.rstrip("/"))) + pbar.update() + found = True + if not found: + raise FileNotFoundError(f"Unable to resolve remote path: {prefix}") + if files: + await result_queue.put(files) + pbar.update(len(subdirs)) + return subdirs + + @staticmethod + def clean_s3_version(ver): + return ver if ver != "null" else "" + + def convert_info(self, v: dict[str, Any], parent: str) -> Entry: + return Entry.from_file( + parent=parent, + name=v.get("Key", "").split(DELIMITER)[-1], + etag=v.get("ETag", "").strip('"'), + version=ClientS3.clean_s3_version(v.get("VersionId", "")), + is_latest=v.get("IsLatest", True), + last_modified=v.get("LastModified", ""), + size=v["size"], + owner_name=v.get("Owner", {}).get("DisplayName", ""), + owner_id=v.get("Owner", {}).get("ID", ""), + ) diff --git a/src/datachain/config.py b/src/datachain/config.py new file mode 100644 index 000000000..a6164efaf --- /dev/null +++ b/src/datachain/config.py @@ -0,0 +1,62 @@ +import os +from collections.abc import Mapping +from typing import TYPE_CHECKING, Optional + +from tomlkit import load + +if TYPE_CHECKING: + from tomlkit import TOMLDocument + + +def read_config(datachain_root: str) -> Optional["TOMLDocument"]: + config_path = os.path.join(datachain_root, "config") + try: + with open(config_path, encoding="utf-8") as f: + return load(f) + except FileNotFoundError: + return None + + +def get_remote_config( + config: Optional["TOMLDocument"], remote: str = "" +) -> Mapping[str, str]: + if config is None: + return {"type": "local"} + if not remote: + try: + remote = config["core"]["default-remote"] # type: ignore[index,assignment] + except KeyError: + return {"type": "local"} + try: + remote_conf: Mapping[str, str] = config["remote"][remote] # type: ignore[assignment,index] + except KeyError: + raise Exception( + f"missing config section for default remote: remote.{remote}" + ) from None + except Exception as exc: + raise Exception("invalid config") from exc + + if not isinstance(remote_conf, Mapping): + raise TypeError(f"config section remote.{remote} must be a mapping") + + remote_type = remote_conf.get("type") + if remote_type not in ("local", "http"): + raise Exception( + f'config section remote.{remote} must have "type" with one of: ' + '"local", "http"' + ) + + if remote_type == "http": + for key in ["url", "username", "token"]: + try: + remote_conf[key] + except KeyError: + raise Exception( + f"config section remote.{remote} of type {remote_type} " + f"must contain key {key}" + ) from None + elif remote_type != "local": + raise Exception( + f"config section remote.{remote} has invalid remote type {remote_type}" + ) + return remote_conf diff --git a/src/datachain/data_storage/__init__.py b/src/datachain/data_storage/__init__.py new file mode 100644 index 000000000..658dfaeb3 --- /dev/null +++ b/src/datachain/data_storage/__init__.py @@ -0,0 +1,14 @@ +from .id_generator import AbstractDBIDGenerator, AbstractIDGenerator +from .job import JobQueryType, JobStatus +from .metastore import AbstractDBMetastore, AbstractMetastore +from .warehouse import AbstractWarehouse + +__all__ = [ + "AbstractDBIDGenerator", + "AbstractDBMetastore", + "AbstractIDGenerator", + "AbstractMetastore", + "AbstractWarehouse", + "JobQueryType", + "JobStatus", +] diff --git a/src/datachain/data_storage/db_engine.py b/src/datachain/data_storage/db_engine.py new file mode 100644 index 000000000..e21d0f0cd --- /dev/null +++ b/src/datachain/data_storage/db_engine.py @@ -0,0 +1,108 @@ +import logging +from abc import ABC, abstractmethod +from collections.abc import Iterator +from typing import TYPE_CHECKING, Any, ClassVar, Optional, Union + +import sqlalchemy as sa +from attrs import frozen +from sqlalchemy.sql import FROM_LINTING +from sqlalchemy.sql.roles import DDLRole + +from datachain.data_storage.serializer import Serializable + +if TYPE_CHECKING: + from sqlalchemy import MetaData, Table + from sqlalchemy.engine.base import Engine + from sqlalchemy.engine.interfaces import Dialect + from sqlalchemy.sql.compiler import Compiled + from sqlalchemy.sql.elements import ClauseElement + + +logger = logging.getLogger("datachain") + +SELECT_BATCH_SIZE = 100_000 # number of rows to fetch at a time + + +@frozen +class DatabaseEngine(ABC, Serializable): + dialect: ClassVar["Dialect"] + + engine: "Engine" + metadata: "MetaData" + + @abstractmethod + def clone(self) -> "DatabaseEngine": + """Clones DatabaseEngine implementation.""" + + @classmethod + def compile(cls, statement: "ClauseElement", **kwargs) -> "Compiled": + """ + Compile a sqlalchemy query or ddl object to a Compiled object. + + Use the `string` and `params` properties of this object to get + the resulting sql string and parameters. + """ + if not isinstance(statement, DDLRole): + # render_postcompile is needed for in_ queries to work + kwargs["compile_kwargs"] = { + **kwargs.pop("compile_kwargs", {}), + "render_postcompile": True, + } + kwargs = {"linting": FROM_LINTING} | kwargs + return statement.compile(dialect=cls.dialect, **kwargs) + + @classmethod + def compile_to_args( + cls, statement: "ClauseElement", **kwargs + ) -> Union[tuple[str], tuple[str, dict[str, Any]]]: + """ + Compile a sqlalchemy query or ddl object to an args tuple. + + This tuple is formatted specifically for calling + `cursor.execute(*args)` according to the python DB-API. + """ + result = cls.compile(statement, **kwargs) + params = result.params + if params is None: + return (result.string,) + return result.string, params + + @abstractmethod + def execute( + self, + query, + cursor: Optional[Any] = None, + conn: Optional[Any] = None, + ) -> Iterator[tuple[Any, ...]]: ... + + @abstractmethod + def executemany( + self, query, params, cursor: Optional[Any] = None + ) -> Iterator[tuple[Any, ...]]: ... + + @abstractmethod + def execute_str(self, sql: str, parameters=None) -> Iterator[tuple[Any, ...]]: ... + + @abstractmethod + def insert_dataframe(self, table_name: str, df) -> int: ... + + @abstractmethod + def close(self) -> None: ... + + @abstractmethod + def transaction(self): ... + + def has_table(self, name: str) -> bool: + """ + Return True if a table exists with the given name + """ + return sa.inspect(self.engine).has_table(name) + + @abstractmethod + def create_table(self, table: "Table", if_not_exists: bool = True) -> None: ... + + @abstractmethod + def drop_table(self, table: "Table", if_exists: bool = False) -> None: ... + + @abstractmethod + def rename_table(self, old_name: str, new_name: str): ... diff --git a/src/datachain/data_storage/id_generator.py b/src/datachain/data_storage/id_generator.py new file mode 100644 index 000000000..b311cb5d2 --- /dev/null +++ b/src/datachain/data_storage/id_generator.py @@ -0,0 +1,122 @@ +import logging +from abc import ABC, abstractmethod +from collections.abc import Iterable +from functools import cached_property +from typing import TYPE_CHECKING, Optional + +from sqlalchemy import Column, Integer, Table, Text + +from datachain.data_storage.serializer import Serializable + +if TYPE_CHECKING: + from sqlalchemy.schema import SchemaItem + + from datachain.data_storage.db_engine import DatabaseEngine + + +logger = logging.getLogger("datachain") + + +class AbstractIDGenerator(ABC, Serializable): + """ + Abstract ID Generator class. This class is responsible for generating + unique IDs for each prefix (e.g. S3 bucket or dataset). + """ + + @abstractmethod + def clone(self) -> "AbstractIDGenerator": + """Clones AbstractIDGenerator implementation.""" + + def init(self) -> None: + """Initialize ID generator.""" + + def cleanup_for_tests(self): + """Cleanup for tests.""" + + @abstractmethod + def init_id(self, uri: str) -> None: + """Initializes the ID generator for the given URI with zero last_id.""" + + @abstractmethod + def get_next_ids(self, uri: str, count: int) -> range: + """Returns a range of IDs for the given URI.""" + + def get_next_id(self, uri: str) -> int: + """Returns the next ID for the given URI.""" + return self.get_next_ids(uri, 1)[0] + + def delete_uri(self, uri: str): + """Deletes the given URI.""" + self.delete_uris([uri]) + + def delete_uris(self, uris: Iterable[str]): + """Deletes the given URIs.""" + + +class AbstractDBIDGenerator(AbstractIDGenerator): + """ + Abstract ID Generator class, to be implemented by any Database Adapters + for a specific database system. This class is responsible for generating + unique IDs for each prefix (e.g. S3 bucket or dataset) and storing them + in a database. It is also responsible for initializing the database + and creating the necessary tables. + """ + + _db: "DatabaseEngine" + _table_prefix: Optional[str] = None + _skip_db_init: bool = False + _base_table_name = "id_generator" + + def __init__( + self, + db: "DatabaseEngine", + table_prefix: Optional[str] = None, + skip_db_init: bool = False, + ): + self._db = db + self._table_prefix = table_prefix + self._skip_db_init = skip_db_init + if db and not skip_db_init: + self.init() + + @abstractmethod + def clone(self) -> "AbstractDBIDGenerator": + """Clones AbstractIDGenerator implementation.""" + + @property + def db(self) -> "DatabaseEngine": + return self._db + + @property + def _columns(self) -> list["SchemaItem"]: + return [ + Column("uri", Text, primary_key=True, nullable=False), + # This is the last id used (and starts at zero if no ids have been used) + Column("last_id", Integer, nullable=False), + ] + + @cached_property + def _table(self) -> Table: + table_name = self._base_table_name + if self._table_prefix: + table_name = f"{self._table_prefix}_{table_name}" + return Table(table_name, self.db.metadata, *self._columns, extend_existing=True) + + def init(self) -> None: + self.db.create_table(self._table, if_not_exists=True) + + def cleanup_for_tests(self): + """Cleanup for tests.""" + self.db.drop_table(self._table, if_exists=True) + + @abstractmethod + def init_id(self, uri: str) -> None: + """Initializes the ID generator for the given URI with zero last_id.""" + + @abstractmethod + def get_next_ids(self, uri: str, count: int) -> range: + """Returns a range of IDs for the given URI.""" + + def delete_uris(self, uris: Iterable[str]): + """Deletes the given URIs from the database.""" + self.db.execute(self._table.delete().where(self._table.c.uri.in_(uris))) diff --git a/src/datachain/data_storage/job.py b/src/datachain/data_storage/job.py new file mode 100644 index 000000000..744b3055a --- /dev/null +++ b/src/datachain/data_storage/job.py @@ -0,0 +1,22 @@ +from enum import Enum + + +class JobStatus(int, Enum): + CREATED = 1 + QUEUED = 2 + INIT = 3 + RUNNING = 4 + COMPLETE = 5 + FAILED = 6 + CANCELING = 7 + CANCELED = 8 + CANCELING_SCHEDULED = 9 + + @classmethod + def finished(cls) -> tuple[int, ...]: + return cls.COMPLETE, cls.FAILED, cls.CANCELED + + +class JobQueryType(int, Enum): + PYTHON = 1 + SHELL = 2 diff --git a/src/datachain/data_storage/metastore.py b/src/datachain/data_storage/metastore.py new file mode 100644 index 000000000..8f1056c9e --- /dev/null +++ b/src/datachain/data_storage/metastore.py @@ -0,0 +1,1578 @@ +import copy +import hashlib +import json +import logging +import os +import posixpath +from abc import ABC, abstractmethod +from collections.abc import Iterator +from datetime import datetime, timezone +from functools import cached_property, reduce +from itertools import groupby +from typing import TYPE_CHECKING, Any, Optional +from uuid import uuid4 + +from sqlalchemy import ( + JSON, + BigInteger, + Boolean, + Column, + DateTime, + ForeignKey, + Integer, + Table, + Text, + UniqueConstraint, + select, +) +from sqlalchemy.sql import func + +from datachain.data_storage import JobQueryType, JobStatus +from datachain.data_storage.serializer import Serializable +from datachain.dataset import ( + DatasetDependency, + DatasetRecord, + DatasetStatus, + DatasetVersion, +) +from datachain.error import ( + DatasetNotFoundError, + StorageNotFoundError, + TableMissingError, +) +from datachain.storage import Storage, StorageStatus, StorageURI +from datachain.utils import JSONSerialize, is_expired + +if TYPE_CHECKING: + from sqlalchemy import Delete, Insert, Select, Update + from sqlalchemy.schema import SchemaItem + + from datachain.data_storage import AbstractIDGenerator, schema + from datachain.data_storage.db_engine import DatabaseEngine + + +logger = logging.getLogger("datachain") + + +class AbstractMetastore(ABC, Serializable): + """ + Abstract Metastore class. + This manages the storing, searching, and retrieval of indexed metadata. + """ + + uri: StorageURI + partial_id: Optional[int] + + schema: "schema.Schema" + storage_class: type[Storage] = Storage + dataset_class: type[DatasetRecord] = DatasetRecord + dependency_class: type[DatasetDependency] = DatasetDependency + + def __init__( + self, + uri: StorageURI = StorageURI(""), + partial_id: Optional[int] = None, + ): + self.uri = uri + self.partial_id: Optional[int] = partial_id + + @abstractmethod + def clone( + self, + uri: StorageURI = StorageURI(""), + partial_id: Optional[int] = None, + use_new_connection: bool = False, + ) -> "AbstractMetastore": + """Clones AbstractMetastore implementation for some Storage input. + Setting use_new_connection will always use a new database connection. + New connections should only be used if needed due to errors with + closed connections.""" + + @abstractmethod + def init(self, uri: StorageURI) -> None: + """Initialize partials table for given storage uri.""" + + def close(self) -> None: + """Closes any active database or HTTP connections.""" + + def cleanup_temp_tables(self, temp_table_names: list[str]) -> None: + """Cleanup temp tables.""" + + def cleanup_for_tests(self) -> None: + """Cleanup for tests.""" + + # + # Storages + # + + @abstractmethod + def create_storage_if_not_registered(self, uri: StorageURI) -> None: + """Saves new storage if it doesn't exist in database.""" + + @abstractmethod + def register_storage_for_indexing( + self, + uri: StorageURI, + force_update: bool = True, + prefix: str = "", + ) -> tuple[Storage, bool, bool, Optional[int], Optional[str]]: + """ + Prepares storage for indexing operation. + This method should be called before index operation is started + It returns: + - storage, prepared for indexing + - boolean saying if indexing is needed + - boolean saying if indexing is currently pending (running) + - partial id + - partial path + """ + + @abstractmethod + def find_stale_storages(self) -> None: + """ + Finds all pending storages for which the last inserted node has happened + before STALE_MINUTES_LIMIT minutes, and marks it as STALE. + """ + + @abstractmethod + def mark_storage_indexed( + self, + uri: StorageURI, + status: int, + ttl: int, + end_time: Optional[datetime] = None, + prefix: str = "", + partial_id: int = 0, + error_message: str = "", + error_stack: str = "", + dataset: Optional[DatasetRecord] = None, + ) -> None: + """ + Marks storage as indexed. + This method should be called when index operation is finished. + """ + + @abstractmethod + def mark_storage_not_indexed(self, uri: StorageURI) -> None: + """ + Mark storage as not indexed. + This method should be called when storage index is deleted. + """ + + @abstractmethod + def update_last_inserted_at(self, uri: Optional[StorageURI] = None) -> None: + """Updates last inserted datetime in bucket with current time.""" + + @abstractmethod + def get_all_storage_uris(self) -> Iterator[StorageURI]: + """Returns all storage uris.""" + + @abstractmethod + def get_storage(self, uri: StorageURI) -> Storage: + """ + Gets storage representation from database. + E.g. if s3 is used as storage this would be s3 bucket data. + """ + + @abstractmethod + def list_storages(self) -> list[Storage]: + """Returns all storages.""" + + @abstractmethod + def mark_storage_pending(self, storage: Storage) -> Storage: + """Marks storage as pending.""" + + # + # Partial Indexes + # + + @abstractmethod + def init_partial_id(self, uri: StorageURI) -> None: + """Initializes partial id for given storage.""" + + @abstractmethod + def get_next_partial_id(self, uri: StorageURI) -> int: + """Returns next partial id for given storage.""" + + @abstractmethod + def get_valid_partial_id( + self, uri: StorageURI, prefix: str, raise_exc: bool = True + ) -> tuple[Optional[int], Optional[str]]: + """ + Returns valid partial id and it's path, if they exist, for a given storage. + """ + + @abstractmethod + def get_last_partial_path(self, uri: StorageURI) -> Optional[str]: + """Returns last partial path for given storage.""" + + # + # Datasets + # + + @abstractmethod + def create_dataset( + self, + name: str, + status: int = DatasetStatus.CREATED, + sources: Optional[list[str]] = None, + feature_schema: Optional[dict] = None, + query_script: str = "", + schema: Optional[dict[str, Any]] = None, + ignore_if_exists: bool = False, + ) -> DatasetRecord: + """Creates new dataset.""" + + @abstractmethod + def create_dataset_version( # noqa: PLR0913 + self, + dataset: DatasetRecord, + version: int, + status: int = DatasetStatus.CREATED, + sources: str = "", + feature_schema: Optional[dict] = None, + query_script: str = "", + error_message: str = "", + error_stack: str = "", + script_output: str = "", + created_at: Optional[datetime] = None, + finished_at: Optional[datetime] = None, + schema: Optional[dict[str, Any]] = None, + ignore_if_exists: bool = False, + num_objects: Optional[int] = None, + size: Optional[int] = None, + preview: Optional[list[dict]] = None, + job_id: Optional[str] = None, + is_job_result: bool = False, + ) -> DatasetRecord: + """Creates new dataset version.""" + + @abstractmethod + def remove_dataset(self, dataset: DatasetRecord) -> None: + """Removes dataset.""" + + @abstractmethod + def update_dataset(self, dataset: DatasetRecord, **kwargs) -> DatasetRecord: + """Updates dataset fields.""" + + @abstractmethod + def update_dataset_version( + self, dataset: DatasetRecord, version: int, **kwargs + ) -> DatasetVersion: + """Updates dataset version fields.""" + + @abstractmethod + def remove_dataset_version( + self, dataset: DatasetRecord, version: int + ) -> DatasetRecord: + """ + Deletes one single dataset version. + If it was last version, it removes dataset completely. + """ + + @abstractmethod + def list_datasets(self) -> Iterator[DatasetRecord]: + """Lists all datasets.""" + + @abstractmethod + def list_datasets_by_prefix(self, prefix: str) -> Iterator["DatasetRecord"]: + """Lists all datasets which names start with prefix.""" + + @abstractmethod + def get_dataset(self, name: str) -> DatasetRecord: + """Gets a single dataset by name.""" + + @abstractmethod + def update_dataset_status( + self, + dataset: DatasetRecord, + status: int, + version: Optional[int] = None, + error_message="", + error_stack="", + script_output="", + ) -> DatasetRecord: + """Updates dataset status and appropriate fields related to status.""" + + # + # Dataset dependencies + # + + def add_dependency( + self, + dependency: DatasetDependency, + source_dataset_name: str, + source_dataset_version: int, + ) -> None: + """Add dependency to dataset or storage.""" + if dependency.is_dataset: + self.add_dataset_dependency( + source_dataset_name, + source_dataset_version, + dependency.name, + int(dependency.version), + ) + else: + self.add_storage_dependency( + source_dataset_name, + source_dataset_version, + StorageURI(dependency.name), + dependency.version, + ) + + @abstractmethod + def add_storage_dependency( + self, + source_dataset_name: str, + source_dataset_version: int, + storage_uri: StorageURI, + storage_timestamp_str: Optional[str] = None, + ) -> None: + """Adds storage dependency to dataset.""" + + @abstractmethod + def add_dataset_dependency( + self, + source_dataset_name: str, + source_dataset_version: int, + dataset_name: str, + dataset_version: int, + ) -> None: + """Adds dataset dependency to dataset.""" + + @abstractmethod + def update_dataset_dependency_source( + self, + source_dataset: DatasetRecord, + source_dataset_version: int, + new_source_dataset: Optional[DatasetRecord] = None, + new_source_dataset_version: Optional[int] = None, + ) -> None: + """Updates dataset dependency source.""" + + @abstractmethod + def get_direct_dataset_dependencies( + self, dataset: DatasetRecord, version: int + ) -> list[Optional[DatasetDependency]]: + """Gets direct dataset dependencies.""" + + @abstractmethod + def remove_dataset_dependencies( + self, dataset: DatasetRecord, version: Optional[int] = None + ) -> None: + """ + When we remove dataset, we need to clean up it's dependencies as well. + """ + + @abstractmethod + def remove_dataset_dependants( + self, dataset: DatasetRecord, version: Optional[int] = None + ) -> None: + """ + When we remove dataset, we need to clear its references in other dataset + dependencies. + """ + + # + # Jobs + # + + @abstractmethod + def create_job( + self, + name: str, + query: str, + query_type: JobQueryType = JobQueryType.PYTHON, + workers: int = 1, + python_version: Optional[str] = None, + params: Optional[dict[str, str]] = None, + ) -> str: + """ + Creates a new job. + Returns the job id. + """ + + @abstractmethod + def set_job_status( + self, + job_id: str, + status: JobStatus, + error_message: Optional[str] = None, + error_stack: Optional[str] = None, + metrics: Optional[dict[str, Any]] = None, + ) -> None: + """Set the status of the given job.""" + + @abstractmethod + def get_job_status(self, job_id: str) -> Optional[JobStatus]: + """Returns the status of the given job.""" + + @abstractmethod + def set_job_and_dataset_status( + self, + job_id: str, + job_status: JobStatus, + dataset_status: DatasetStatus, + ) -> None: + """Set the status of the given job and dataset.""" + + @abstractmethod + def get_possibly_stale_jobs(self) -> list[tuple[str, str, int]]: + """Returns the possibly stale jobs.""" + + +class AbstractDBMetastore(AbstractMetastore): + """ + Abstract Database Metastore class, to be implemented + by any Database Adapters for a specific database system. + This manages the storing, searching, and retrieval of indexed metadata, + and has shared logic for all database systems currently in use. + """ + + PARTIALS_TABLE_NAME_PREFIX = "prt_" + STORAGE_TABLE = "buckets" + DATASET_TABLE = "datasets" + DATASET_VERSION_TABLE = "datasets_versions" + DATASET_DEPENDENCY_TABLE = "datasets_dependencies" + JOBS_TABLE = "jobs" + + id_generator: "AbstractIDGenerator" + db: "DatabaseEngine" + + def __init__( + self, + id_generator: "AbstractIDGenerator", + uri: StorageURI = StorageURI(""), + partial_id: Optional[int] = None, + ): + self.id_generator = id_generator + super().__init__(uri, partial_id) + + @abstractmethod + def init(self, uri: StorageURI) -> None: + """Initialize partials table for given storage uri.""" + + def close(self) -> None: + """Closes any active database connections.""" + self.db.close() + + def cleanup_temp_tables(self, temp_table_names: list[str]) -> None: + """Cleanup temp tables.""" + self.id_generator.delete_uris(temp_table_names) + + @classmethod + def _buckets_columns(cls) -> list["SchemaItem"]: + """Buckets (storages) table columns.""" + return [ + Column("id", Integer, primary_key=True, nullable=False), + Column("uri", Text, nullable=False), + Column("timestamp", DateTime(timezone=True)), + Column("expires", DateTime(timezone=True)), + Column("started_inserting_at", DateTime(timezone=True)), + Column("last_inserted_at", DateTime(timezone=True)), + Column("status", Integer, nullable=False), + Column("error_message", Text, nullable=False, default=""), + Column("error_stack", Text, nullable=False, default=""), + ] + + @classmethod + def _datasets_columns(cls) -> list["SchemaItem"]: + """Datasets table columns.""" + return [ + Column("id", Integer, primary_key=True), + Column("name", Text, nullable=False), + Column("description", Text), + Column("labels", JSON, nullable=True), + Column("shadow", Boolean, nullable=False), + Column("status", Integer, nullable=False), + Column("feature_schema", JSON, nullable=True), + Column("created_at", DateTime(timezone=True)), + Column("finished_at", DateTime(timezone=True)), + Column("error_message", Text, nullable=False, default=""), + Column("error_stack", Text, nullable=False, default=""), + Column("script_output", Text, nullable=False, default=""), + Column("sources", Text, nullable=False, default=""), + Column("query_script", Text, nullable=False, default=""), + Column("schema", JSON, nullable=True), + ] + + @cached_property + def _dataset_fields(self) -> list[str]: + return [ + c.name # type: ignore [attr-defined] + for c in self._datasets_columns() + if c.name # type: ignore [attr-defined] + ] + + @classmethod + def _datasets_versions_columns(cls) -> list["SchemaItem"]: + """Datasets versions table columns.""" + return [ + Column("id", Integer, primary_key=True), + Column( + "dataset_id", + Integer, + ForeignKey(f"{cls.DATASET_TABLE}.id", ondelete="CASCADE"), + nullable=False, + ), + Column("version", Integer, nullable=False), + # adding default for now until we fully remove shadow datasets + Column("status", Integer, nullable=False, default=DatasetStatus.COMPLETE), + Column("feature_schema", JSON, nullable=True), + Column("created_at", DateTime(timezone=True)), + Column("finished_at", DateTime(timezone=True)), + Column("error_message", Text, nullable=False, default=""), + Column("error_stack", Text, nullable=False, default=""), + Column("script_output", Text, nullable=False, default=""), + Column("num_objects", BigInteger, nullable=True), + Column("size", BigInteger, nullable=True), + Column("preview", JSON, nullable=True), + Column("sources", Text, nullable=False, default=""), + Column("query_script", Text, nullable=False, default=""), + Column("schema", JSON, nullable=True), + Column("job_id", Text, nullable=True), + Column("is_job_result", Boolean, nullable=False, default=False), + UniqueConstraint("dataset_id", "version"), + ] + + @cached_property + def _dataset_version_fields(self) -> list[str]: + return [ + c.name # type: ignore [attr-defined] + for c in self._datasets_versions_columns() + if c.name # type: ignore [attr-defined] + ] + + @classmethod + def _datasets_dependencies_columns(cls) -> list["SchemaItem"]: + """Datasets dependencies table columns.""" + return [ + Column("id", Integer, primary_key=True), + # TODO remove when https://github.com/iterative/dvcx/issues/959 is done + Column( + "source_dataset_id", + Integer, + ForeignKey(f"{cls.DATASET_TABLE}.id"), + nullable=False, + ), + Column( + "source_dataset_version_id", + Integer, + ForeignKey(f"{cls.DATASET_VERSION_TABLE}.id"), + nullable=True, + ), + # TODO remove when https://github.com/iterative/dvcx/issues/959 is done + Column( + "dataset_id", + Integer, + ForeignKey(f"{cls.DATASET_TABLE}.id"), + nullable=True, + ), + Column( + "dataset_version_id", + Integer, + ForeignKey(f"{cls.DATASET_VERSION_TABLE}.id"), + nullable=True, + ), + # TODO remove when https://github.com/iterative/dvcx/issues/1121 is done + # If we unify datasets and bucket listing then both bucket fields won't + # be needed + Column( + "bucket_id", + Integer, + ForeignKey(f"{cls.STORAGE_TABLE}.id"), + nullable=True, + ), + Column("bucket_version", Text, nullable=True), + ] + + @classmethod + def _storage_partial_columns(cls) -> list["SchemaItem"]: + """Storage partial table columns.""" + return [ + Column("path_str", Text, nullable=False), + # This is generated before insert and is not the SQLite rowid, + # so it is not the primary key. + Column("partial_id", Integer, nullable=False, index=True), + Column("timestamp", DateTime(timezone=True)), + Column("expires", DateTime(timezone=True)), + ] + + def _get_storage_partial_table(self, name: str) -> Table: + table = self.db.metadata.tables.get(name) + if table is None: + table = Table( + name, + self.db.metadata, + *self._storage_partial_columns(), + ) + return table + + # + # Query Tables + # + + def _partials_table(self, uri: StorageURI) -> Table: + return self._get_storage_partial_table(self._partials_table_name(uri)) + + @cached_property + def _storages(self) -> Table: + return Table(self.STORAGE_TABLE, self.db.metadata, *self._buckets_columns()) + + @cached_property + def _partials(self) -> Table: + assert ( + self._current_partials_table_name + ), "Partials can only be used if uri/current_partials_table_name is set" + return self._get_storage_partial_table(self._current_partials_table_name) + + @cached_property + def _datasets(self) -> Table: + return Table(self.DATASET_TABLE, self.db.metadata, *self._datasets_columns()) + + @cached_property + def _datasets_versions(self) -> Table: + return Table( + self.DATASET_VERSION_TABLE, + self.db.metadata, + *self._datasets_versions_columns(), + ) + + @cached_property + def _datasets_dependencies(self) -> Table: + return Table( + self.DATASET_DEPENDENCY_TABLE, + self.db.metadata, + *self._datasets_dependencies_columns(), + ) + + # + # Query Starters (These can be overridden by subclasses) + # + + @abstractmethod + def _storages_insert(self) -> "Insert": ... + + def _storages_select(self, *columns) -> "Select": + if not columns: + return self._storages.select() + return select(*columns) + + def _storages_update(self) -> "Update": + return self._storages.update() + + def _storages_delete(self) -> "Delete": + return self._storages.delete() + + @abstractmethod + def _partials_insert(self) -> "Insert": ... + + def _partials_select(self, *columns) -> "Select": + if not columns: + return self._partials.select() + return select(*columns) + + def _partials_update(self) -> "Update": + return self._partials.update() + + @abstractmethod + def _datasets_insert(self) -> "Insert": ... + + def _datasets_select(self, *columns) -> "Select": + if not columns: + return self._datasets.select() + return select(*columns) + + def _datasets_update(self) -> "Update": + return self._datasets.update() + + def _datasets_delete(self) -> "Delete": + return self._datasets.delete() + + @abstractmethod + def _datasets_versions_insert(self) -> "Insert": ... + + def _datasets_versions_select(self, *columns) -> "Select": + if not columns: + return self._datasets_versions.select() + return select(*columns) + + def _datasets_versions_update(self) -> "Update": + return self._datasets_versions.update() + + def _datasets_versions_delete(self) -> "Delete": + return self._datasets_versions.delete() + + @abstractmethod + def _datasets_dependencies_insert(self) -> "Insert": ... + + def _datasets_dependencies_select(self, *columns) -> "Select": + if not columns: + return self._datasets_dependencies.select() + return select(*columns) + + def _datasets_dependencies_update(self) -> "Update": + return self._datasets_dependencies.update() + + def _datasets_dependencies_delete(self) -> "Delete": + return self._datasets_dependencies.delete() + + # + # Table Name Internal Functions + # + + def _partials_table_name(self, uri: StorageURI) -> str: + sha = hashlib.sha256(uri.encode("utf-8")).hexdigest()[:12] + return f"{self.PARTIALS_TABLE_NAME_PREFIX}_{sha}" + + @property + def _current_partials_table_name(self) -> Optional[str]: + if not self.uri: + return None + return self._partials_table_name(self.uri) + + # + # Storages + # + + def create_storage_if_not_registered(self, uri: StorageURI, conn=None) -> None: + """Saves new storage if it doesn't exist in database.""" + query = self._storages_insert().values( + uri=uri, + status=StorageStatus.CREATED, + error_message="", + error_stack="", + ) + if hasattr(query, "on_conflict_do_nothing"): + # SQLite and PostgreSQL both support 'on_conflict_do_nothing', + # but generic SQL does not + query = query.on_conflict_do_nothing() + self.db.execute(query, conn=conn) + + def register_storage_for_indexing( + self, + uri: StorageURI, + force_update: bool = True, + prefix: str = "", + ) -> tuple[Storage, bool, bool, Optional[int], Optional[str]]: + """ + Prepares storage for indexing operation. + This method should be called before index operation is started + It returns: + - storage, prepared for indexing + - boolean saying if indexing is needed + - boolean saying if indexing is currently pending (running) + - partial id + - partial path + """ + # This ensures that all calls to the DB are in a single transaction + # and commit is automatically called once this function returns + with self.db.transaction() as conn: + # Create storage if it doesn't exist + self.create_storage_if_not_registered(uri, conn=conn) + storage = self.get_storage(uri, conn=conn) + + if storage.status == StorageStatus.PENDING: + return storage, False, True, None, None + + if storage.is_expired or storage.status == StorageStatus.STALE: + storage = self.mark_storage_pending(storage, conn=conn) + return storage, True, False, None, None + + if ( + storage.status in (StorageStatus.PARTIAL, StorageStatus.COMPLETE) + and not force_update + ): + partial_id, partial_path = self.get_valid_partial_id( + uri, prefix, raise_exc=False + ) + if partial_id is not None: + return storage, False, False, partial_id, partial_path + return storage, True, False, None, None + + storage = self.mark_storage_pending(storage, conn=conn) + return storage, True, False, None, None + + def find_stale_storages(self) -> None: + """ + Finds all pending storages for which the last inserted node has happened + before STALE_MINUTES_LIMIT minutes, and marks it as STALE. + """ + s = self._storages + with self.db.transaction() as conn: + pending_storages = map( + self.storage_class._make, + self.db.execute( + self._storages_select().where(s.c.status == StorageStatus.PENDING), + conn=conn, + ), + ) + for storage in pending_storages: + if storage.is_stale: + print(f"Marking storage {storage.uri} as stale") + self._mark_storage_stale(storage.id, conn=conn) + + def mark_storage_indexed( + self, + uri: StorageURI, + status: int, + ttl: int, + end_time: Optional[datetime] = None, + prefix: str = "", + partial_id: int = 0, + error_message: str = "", + error_stack: str = "", + dataset: Optional[DatasetRecord] = None, + ) -> None: + """ + Marks storage as indexed. + This method should be called when index operation is finished. + """ + if status == StorageStatus.PARTIAL and not prefix: + raise AssertionError("Partial indexing requires a prefix") + + if end_time is None: + end_time = datetime.now(timezone.utc) + expires = Storage.get_expiration_time(end_time, ttl) + + s = self._storages + with self.db.transaction() as conn: + self.db.execute( + self._storages_update() + .where(s.c.uri == uri) + .values( # type: ignore [attr-defined] + timestamp=end_time, + expires=expires, + status=status, + last_inserted_at=end_time, + error_message=error_message, + error_stack=error_stack, + ), + conn=conn, + ) + + if not self._current_partials_table_name: + # This only occurs in tests + return + + if status in (StorageStatus.PARTIAL, StorageStatus.COMPLETE): + dir_prefix = posixpath.join(prefix, "") + self.db.execute( + self._partials_insert().values( + path_str=dir_prefix, + timestamp=end_time, + expires=expires, + partial_id=partial_id, + ), + conn=conn, + ) + + # update underlying dataset status as well + if status == StorageStatus.FAILED and dataset: + self.update_dataset_status( + dataset, + DatasetStatus.FAILED, + dataset.latest_version, + error_message=error_message, + error_stack=error_stack, + conn=conn, + ) + + if status in (StorageStatus.PARTIAL, StorageStatus.COMPLETE) and dataset: + self.update_dataset_status( + dataset, DatasetStatus.COMPLETE, dataset.latest_version, conn=conn + ) + + def update_last_inserted_at(self, uri: Optional[StorageURI] = None) -> None: + """Updates last inserted datetime in bucket with current time""" + uri = uri or self.uri + updates = {"last_inserted_at": datetime.now(timezone.utc)} + s = self._storages + self.db.execute( + self._storages_update().where(s.c.uri == uri).values(**updates) # type: ignore [attr-defined] + ) + + def get_all_storage_uris(self) -> Iterator[StorageURI]: + """Returns all storage uris.""" + s = self._storages + yield from (r[0] for r in self.db.execute(self._storages_select(s.c.uri))) + + def get_storage(self, uri: StorageURI, conn=None) -> Storage: + """ + Gets storage representation from database. + E.g. if s3 is used as storage this would be s3 bucket data + """ + s = self._storages + result = next( + self.db.execute(self._storages_select().where(s.c.uri == uri), conn=conn), + None, + ) + if not result: + raise StorageNotFoundError(f"Storage {uri} not found.") + + return self.storage_class._make(result) + + def list_storages(self) -> list[Storage]: + result = self.db.execute(self._storages_select()) + if not result: + return [] + + return [self.storage_class._make(r) for r in result] + + def mark_storage_pending(self, storage: Storage, conn=None) -> Storage: + # Update status to pending and dates + updates = { + "status": StorageStatus.PENDING, + "timestamp": None, + "expires": None, + "last_inserted_at": None, + "started_inserting_at": datetime.now(timezone.utc), + } + storage = storage._replace(**updates) # type: ignore [arg-type] + s = self._storages + self.db.execute( + self._storages_update().where(s.c.uri == storage.uri).values(**updates), # type: ignore [attr-defined] + conn=conn, + ) + return storage + + def _mark_storage_stale(self, storage_id: int, conn=None) -> None: + # Update status to pending and dates + updates = {"status": StorageStatus.STALE, "timestamp": None, "expires": None} + s = self._storages + self.db.execute( + self._storages.update().where(s.c.id == storage_id).values(**updates), # type: ignore [attr-defined] + conn=conn, + ) + + # + # Partial Indexes + # + + def init_partial_id(self, uri: StorageURI) -> None: + """Initializes partial id for given storage.""" + if not uri: + raise ValueError("uri for get_next_partial_id() cannot be empty") + self.id_generator.init_id(f"partials:{uri}") + + def get_next_partial_id(self, uri: StorageURI) -> int: + """Returns next partial id for given storage.""" + if not uri: + raise ValueError("uri for get_next_partial_id() cannot be empty") + return self.id_generator.get_next_id(f"partials:{uri}") + + def get_valid_partial_id( + self, uri: StorageURI, prefix: str, raise_exc: bool = True + ) -> tuple[Optional[int], Optional[str]]: + """ + Returns valid partial id and it's path, if they exist, for a given storage. + """ + # This SQL statement finds all entries that are + # prefixes of the given prefix, matching this or parent directories + # that are indexed. + dir_prefix = posixpath.join(prefix, "") + p = self._partials_table(uri) + expire_values = self.db.execute( + select(p.c.expires, p.c.partial_id, p.c.path_str) + .where( + p.c.path_str == func.substr(dir_prefix, 1, func.length(p.c.path_str)) + ) + .order_by(p.c.expires.desc()) + ) + for expires, partial_id, path_str in expire_values: + if not is_expired(expires): + return partial_id, path_str + if raise_exc: + raise RuntimeError(f"Unable to get valid partial_id: {uri=}, {prefix=}") + return None, None + + def get_last_partial_path(self, uri: StorageURI) -> Optional[str]: + """Returns last partial path for given storage.""" + p = self._partials_table(uri) + if not self.db.has_table(p.name): + raise StorageNotFoundError(f"Storage {uri} partials are not found.") + last_partial = self.db.execute( + select(p.c.path_str).order_by(p.c.timestamp.desc()).limit(1) + ) + for (path_str,) in last_partial: + return path_str + return None + + # + # Datasets + # + + def create_dataset( + self, + name: str, + status: int = DatasetStatus.CREATED, + sources: Optional[list[str]] = None, + feature_schema: Optional[dict] = None, + query_script: str = "", + schema: Optional[dict[str, Any]] = None, + ignore_if_exists: bool = False, + **kwargs, # TODO registered = True / False + ) -> DatasetRecord: + """Creates new dataset.""" + # TODO abstract this method and add registered = True based on kwargs + query = self._datasets_insert().values( + name=name, + shadow=False, + status=status, + feature_schema=json.dumps(feature_schema or {}), + created_at=datetime.now(timezone.utc), + error_message="", + error_stack="", + script_output="", + sources="\n".join(sources) if sources else "", + query_script=query_script, + schema=json.dumps(schema or {}), + ) + if ignore_if_exists and hasattr(query, "on_conflict_do_nothing"): + # SQLite and PostgreSQL both support 'on_conflict_do_nothing', + # but generic SQL does not + query = query.on_conflict_do_nothing(index_elements=["name"]) + self.db.execute(query) + + return self.get_dataset(name) + + def create_dataset_version( # noqa: PLR0913 + self, + dataset: DatasetRecord, + version: int, + status: int = DatasetStatus.CREATED, + sources: str = "", + feature_schema: Optional[dict] = None, + query_script: str = "", + error_message: str = "", + error_stack: str = "", + script_output: str = "", + created_at: Optional[datetime] = None, + finished_at: Optional[datetime] = None, + schema: Optional[dict[str, Any]] = None, + ignore_if_exists: bool = False, + num_objects: Optional[int] = None, + size: Optional[int] = None, + preview: Optional[list[dict]] = None, + job_id: Optional[str] = None, + is_job_result: bool = False, + conn=None, + ) -> DatasetRecord: + """Creates new dataset version.""" + if status in [DatasetStatus.COMPLETE, DatasetStatus.FAILED]: + finished_at = finished_at or datetime.now(timezone.utc) + else: + finished_at = None + + query = self._datasets_versions_insert().values( + dataset_id=dataset.id, + version=version, + status=status, # for now until we remove shadow datasets + feature_schema=json.dumps(feature_schema or {}), + created_at=created_at or datetime.now(timezone.utc), + finished_at=finished_at, + error_message=error_message, + error_stack=error_stack, + script_output=script_output, + sources=sources, + query_script=query_script, + schema=json.dumps(schema or {}), + num_objects=num_objects, + size=size, + preview=json.dumps(preview or []), + job_id=job_id or os.getenv("DATACHAIN_JOB_ID"), + is_job_result=is_job_result, + ) + if ignore_if_exists and hasattr(query, "on_conflict_do_nothing"): + # SQLite and PostgreSQL both support 'on_conflict_do_nothing', + # but generic SQL does not + query = query.on_conflict_do_nothing( + index_elements=["dataset_id", "version"] + ) + self.db.execute(query, conn=conn) + + return self.get_dataset(dataset.name, conn=conn) + + def remove_dataset(self, dataset: DatasetRecord) -> None: + """Removes dataset.""" + d = self._datasets + with self.db.transaction(): + self.remove_dataset_dependencies(dataset) + self.remove_dataset_dependants(dataset) + self.db.execute(self._datasets_delete().where(d.c.id == dataset.id)) + + def update_dataset( + self, dataset: DatasetRecord, conn=None, **kwargs + ) -> DatasetRecord: + """Updates dataset fields.""" + values = {} + dataset_values = {} + for field, value in kwargs.items(): + if field in self._dataset_fields[1:]: + if field in ["labels", "schema"]: + values[field] = json.dumps(value) if value else None + else: + values[field] = value + if field == "schema": + dataset_values[field] = DatasetRecord.parse_schema(value) + else: + dataset_values[field] = value + + if not values: + # Nothing to update + return dataset + + d = self._datasets + self.db.execute( + self._datasets_update().where(d.c.name == dataset.name).values(values), + conn=conn, + ) # type: ignore [attr-defined] + + result_ds = copy.deepcopy(dataset) + result_ds.update(**dataset_values) + return result_ds + + def update_dataset_version( + self, dataset: DatasetRecord, version: int, conn=None, **kwargs + ) -> DatasetVersion: + """Updates dataset fields.""" + dataset_version = dataset.get_version(version) + + values = {} + for field, value in kwargs.items(): + if field in self._dataset_version_fields[1:]: + if field == "schema": + dataset_version.update(**{field: DatasetRecord.parse_schema(value)}) + values[field] = json.dumps(value) if value else None + elif field == "feature_schema": + values[field] = json.dumps(value) if value else None + elif field == "preview" and isinstance(value, list): + values[field] = json.dumps(value, cls=JSONSerialize) + else: + values[field] = value + dataset_version.update(**{field: value}) + + if not values: + # Nothing to update + return dataset_version + + dv = self._datasets_versions + self.db.execute( + self._datasets_versions_update() + .where(dv.c.id == dataset_version.id) + .values(values), + conn=conn, + ) # type: ignore [attr-defined] + + return dataset_version + + def _parse_dataset(self, rows) -> Optional[DatasetRecord]: + versions = [self.dataset_class.parse(*r) for r in rows] + if not versions: + return None + return reduce(lambda ds, version: ds.merge_versions(version), versions) + + def _parse_datasets(self, rows) -> Iterator["DatasetRecord"]: + # grouping rows by dataset id + for _, g in groupby(rows, lambda r: r[0]): + dataset = self._parse_dataset(list(g)) + if dataset: + yield dataset + + def _base_dataset_query(self): + if not ( + self.db.has_table(self._datasets.name) + and self.db.has_table(self._datasets_versions.name) + ): + raise TableMissingError + + d = self._datasets + dv = self._datasets_versions + query = self._datasets_select( + *(getattr(d.c, f) for f in self._dataset_fields), + *(getattr(dv.c, f) for f in self._dataset_version_fields), + ) + j = d.join(dv, d.c.id == dv.c.dataset_id, isouter=True) + return query.select_from(j) + + def list_datasets(self) -> Iterator["DatasetRecord"]: + """Lists all datasets.""" + yield from self._parse_datasets(self.db.execute(self._base_dataset_query())) + + def list_datasets_by_prefix( + self, prefix: str, conn=None + ) -> Iterator["DatasetRecord"]: + query = self._base_dataset_query() + query = query.where(self._datasets.c.name.startswith(prefix)) + yield from self._parse_datasets(self.db.execute(query)) + + def get_dataset(self, name: str, conn=None) -> DatasetRecord: + """Gets a single dataset by name""" + d = self._datasets + query = self._base_dataset_query() + query = query.where(d.c.name == name) # type: ignore [attr-defined] + ds = self._parse_dataset(self.db.execute(query, conn=conn)) + if not ds: + raise DatasetNotFoundError(f"Dataset {name} not found.") + return ds + + def remove_dataset_version( + self, dataset: DatasetRecord, version: int + ) -> DatasetRecord: + """ + Deletes one single dataset version. + If it was last version, it removes dataset completely + """ + if not dataset.has_version(version): + raise DatasetNotFoundError( + f"Dataset {dataset.name} version {version} not found." + ) + + self.remove_dataset_dependencies(dataset, version) + self.remove_dataset_dependants(dataset, version) + + d = self._datasets + dv = self._datasets_versions + self.db.execute( + self._datasets_versions_delete().where( + (dv.c.dataset_id == dataset.id) & (dv.c.version == version) + ) + ) + + if dataset.versions and len(dataset.versions) == 1: + # had only one version, fully deleting dataset + self.db.execute(self._datasets_delete().where(d.c.id == dataset.id)) + + dataset.remove_version(version) + return dataset + + def update_dataset_status( + self, + dataset: DatasetRecord, + status: int, + version: Optional[int] = None, + error_message="", + error_stack="", + script_output="", + conn=None, + ) -> DatasetRecord: + """ + Updates dataset status and appropriate fields related to status + It also updates version if specified. + """ + update_data: dict[str, Any] = {"status": status} + if status in [DatasetStatus.COMPLETE, DatasetStatus.FAILED]: + # if in final state, updating finished_at datetime + update_data["finished_at"] = datetime.now(timezone.utc) + if script_output: + update_data["script_output"] = script_output + + if status == DatasetStatus.FAILED: + update_data["error_message"] = error_message + update_data["error_stack"] = error_stack + + self.update_dataset(dataset, conn=conn, **update_data) + + if version: + self.update_dataset_version(dataset, version, conn=conn, **update_data) + + return dataset + + # + # Dataset dependencies + # + + def _insert_dataset_dependency(self, data: dict[str, Any]) -> None: + """Method for inserting dependencies.""" + self.db.execute(self._datasets_dependencies_insert().values(**data)) + + def add_storage_dependency( + self, + source_dataset_name: str, + source_dataset_version: int, + storage_uri: StorageURI, + storage_timestamp_str: Optional[str] = None, + ) -> None: + source_dataset = self.get_dataset(source_dataset_name) + storage = self.get_storage(storage_uri) + + self._insert_dataset_dependency( + { + "source_dataset_id": source_dataset.id, + "source_dataset_version_id": ( + source_dataset.get_version(source_dataset_version).id + ), + "bucket_id": storage.id, + "bucket_version": storage_timestamp_str, + } + ) + + def add_dataset_dependency( + self, + source_dataset_name: str, + source_dataset_version: int, + dataset_name: str, + dataset_version: int, + ) -> None: + """Adds dataset dependency to dataset.""" + source_dataset = self.get_dataset(source_dataset_name) + dataset = self.get_dataset(dataset_name) + + self._insert_dataset_dependency( + { + "source_dataset_id": source_dataset.id, + "source_dataset_version_id": ( + source_dataset.get_version(source_dataset_version).id + ), + "dataset_id": dataset.id, + "dataset_version_id": dataset.get_version(dataset_version).id, + } + ) + + def update_dataset_dependency_source( + self, + source_dataset: DatasetRecord, + source_dataset_version: int, + new_source_dataset: Optional[DatasetRecord] = None, + new_source_dataset_version: Optional[int] = None, + ) -> None: + dd = self._datasets_dependencies + + if not new_source_dataset: + new_source_dataset = source_dataset + + q = self._datasets_dependencies_update().where( + dd.c.source_dataset_id == source_dataset.id + ) + q = q.where( + dd.c.source_dataset_version_id + == source_dataset.get_version(source_dataset_version).id + ) + + data = {"source_dataset_id": new_source_dataset.id} + if new_source_dataset_version: + data["source_dataset_version_id"] = new_source_dataset.get_version( + new_source_dataset_version + ).id + + q = q.values(**data) + self.db.execute(q) + + @abstractmethod + def _dataset_dependencies_select_columns(self) -> list["SchemaItem"]: + """ + Returns a list of columns to select in a query for fetching dataset dependencies + """ + + def get_direct_dataset_dependencies( + self, dataset: DatasetRecord, version: int + ) -> list[Optional[DatasetDependency]]: + d = self._datasets + dd = self._datasets_dependencies + dv = self._datasets_versions + s = self._storages + + dataset_version = dataset.get_version(version) + + select_cols = self._dataset_dependencies_select_columns() + + query = ( + self._datasets_dependencies_select(*select_cols) + .select_from( + dd.join(d, dd.c.dataset_id == d.c.id, isouter=True) + .join(s, dd.c.bucket_id == s.c.id, isouter=True) + .join(dv, dd.c.dataset_version_id == dv.c.id, isouter=True) + ) + .where( + (dd.c.source_dataset_id == dataset.id) + & (dd.c.source_dataset_version_id == dataset_version.id) + ) + ) + if version: + dataset_version = dataset.get_version(version) + query = query.where(dd.c.source_dataset_version_id == dataset_version.id) + + return [self.dependency_class.parse(*r) for r in self.db.execute(query)] + + def remove_dataset_dependencies( + self, dataset: DatasetRecord, version: Optional[int] = None + ) -> None: + """ + When we remove dataset, we need to clean up it's dependencies as well + """ + dd = self._datasets_dependencies + + q = self._datasets_dependencies_delete().where( + dd.c.source_dataset_id == dataset.id + ) + + if version: + q = q.where( + dd.c.source_dataset_version_id == dataset.get_version(version).id + ) + + self.db.execute(q) + + def remove_dataset_dependants( + self, dataset: DatasetRecord, version: Optional[int] = None + ) -> None: + """ + When we remove dataset, we need to clear its references in other dataset + dependencies + """ + dd = self._datasets_dependencies + + q = self._datasets_dependencies_update().where(dd.c.dataset_id == dataset.id) + if version: + q = q.where(dd.c.dataset_version_id == dataset.get_version(version).id) + + q = q.values(dataset_id=None, dataset_version_id=None) + + self.db.execute(q) + + # + # Jobs + # + + @staticmethod + def _jobs_columns() -> "list[SchemaItem]": + return [ + Column( + "id", + Text, + default=uuid4, + primary_key=True, + nullable=False, + ), + Column("name", Text, nullable=False, default=""), + Column("status", Integer, nullable=False, default=JobStatus.CREATED), + # When this Job was created + Column("created_at", DateTime(timezone=True), nullable=False), + # When this Job finished (or failed) + Column("finished_at", DateTime(timezone=True), nullable=True), + # This is the workers value from query settings, and determines both + # the default and maximum number of workers for distributed UDFs. + Column("query", Text, nullable=False, default=""), + Column( + "query_type", + Integer, + nullable=False, + default=JobQueryType.PYTHON, + ), + Column("workers", Integer, nullable=False, default=1), + Column("python_version", Text, nullable=True), + Column("error_message", Text, nullable=False, default=""), + Column("error_stack", Text, nullable=False, default=""), + Column("params", JSON, nullable=False), + Column("metrics", JSON, nullable=False), + ] + + @cached_property + def _jobs(self) -> "Table": + return Table(self.JOBS_TABLE, self.db.metadata, *self._jobs_columns()) + + @abstractmethod + def _jobs_insert(self) -> "Insert": ... + + def _jobs_select(self, *columns) -> "Select": + if not columns: + return self._jobs.select() + return select(*columns) + + def _jobs_update(self, *where) -> "Update": + if not where: + return self._jobs.update() + return self._jobs.update().where(*where) + + def create_job( + self, + name: str, + query: str, + query_type: JobQueryType = JobQueryType.PYTHON, + workers: int = 1, + python_version: Optional[str] = None, + params: Optional[dict[str, str]] = None, + conn: Optional[Any] = None, + ) -> str: + """ + Creates a new job. + Returns the job id. + """ + job_id = str(uuid4()) + self.db.execute( + self._jobs_insert().values( + id=job_id, + name=name, + status=JobStatus.CREATED, + created_at=datetime.now(timezone.utc), + query=query, + query_type=query_type.value, + workers=workers, + python_version=python_version, + error_message="", + error_stack="", + params=json.dumps(params or {}), + metrics=json.dumps({}), + ), + conn=conn, + ) + return job_id + + def set_job_status( + self, + job_id: str, + status: JobStatus, + error_message: Optional[str] = None, + error_stack: Optional[str] = None, + metrics: Optional[dict[str, Any]] = None, + conn: Optional[Any] = None, + ) -> None: + """Set the status of the given job.""" + values: dict = {"status": status.value} + if status.value in JobStatus.finished(): + values["finished_at"] = datetime.now(timezone.utc) + if error_message: + values["error_message"] = error_message + if error_stack: + values["error_stack"] = error_stack + if metrics: + values["metrics"] = json.dumps(metrics) + self.db.execute( + self._jobs_update(self._jobs.c.id == job_id).values(**values), + conn=conn, + ) + + def get_job_status( + self, + job_id: str, + conn: Optional[Any] = None, + ) -> Optional[JobStatus]: + """Returns the status of the given job.""" + results = list( + self.db.execute( + self._jobs_select(self._jobs.c.status).where(self._jobs.c.id == job_id), + conn=conn, + ), + ) + if not results: + return None + return results[0][0] + + def set_job_and_dataset_status( + self, + job_id: str, + job_status: JobStatus, + dataset_status: DatasetStatus, + ) -> None: + """Set the status of the given job and dataset.""" + with self.db.transaction() as conn: + self.set_job_status(job_id, status=job_status, conn=conn) + dv = self._datasets_versions + query = ( + self._datasets_versions_update() + .where( + (dv.c.job_id == job_id) & (dv.c.status != DatasetStatus.COMPLETE) + ) + .values(status=dataset_status) + ) + self.db.execute(query, conn=conn) # type: ignore[attr-defined] diff --git a/src/datachain/data_storage/schema.py b/src/datachain/data_storage/schema.py new file mode 100644 index 000000000..d51c575e0 --- /dev/null +++ b/src/datachain/data_storage/schema.py @@ -0,0 +1,266 @@ +import inspect +from collections.abc import Iterable, Iterator, Sequence +from typing import ( + TYPE_CHECKING, + Any, + Generic, + Optional, + TypeVar, +) + +import sqlalchemy as sa +from sqlalchemy.sql import func as f +from sqlalchemy.sql.expression import null, true + +from datachain.node import DirType +from datachain.sql.functions import path +from datachain.sql.types import Int, SQLType, UInt64 + +if TYPE_CHECKING: + from sqlalchemy import Engine + from sqlalchemy.engine.interfaces import Dialect + from sqlalchemy.sql.base import Executable, ReadOnlyColumnCollection + from sqlalchemy.sql.elements import KeyedColumnElement + + +def dedup_columns(columns: Iterable[sa.Column]) -> list[sa.Column]: + """ + Removes duplicate columns from a list of columns. + If column with the same name and different type is found, exception is + raised + """ + c_set: dict[str, sa.Column] = {} + for c in columns: + if (ec := c_set.get(c.name, None)) is not None: + if str(ec.type) != str(c.type): + raise ValueError( + f"conflicting types for column {c.name}:{c.type!s} and {ec.type!s}" + ) + continue + c_set[c.name] = c + + return list(c_set.values()) + + +def convert_rows_custom_column_types( + columns: "ReadOnlyColumnCollection[str, KeyedColumnElement[Any]]", + rows: Iterator[tuple[Any, ...]], + dialect: "Dialect", +): + """ + This function converts values of rows columns based on their types which are + defined in columns. We are only converting column values for which types are + subclasses of our SQLType, as only for those we have converters registered. + """ + # indexes of SQLType column in a list of columns so that we can skip the rest + custom_columns_types: list[tuple[int, SQLType]] = [ + (idx, c.type) for idx, c in enumerate(columns) if isinstance(c.type, SQLType) + ] + + if not custom_columns_types: + yield from rows + + for row in rows: + row_list = list(row) + for idx, t in custom_columns_types: + row_list[idx] = t.on_read_convert(row_list[idx], dialect) + + yield tuple(row_list) + + +class DirExpansion: + @staticmethod + def base_select(q): + return sa.select( + q.c.id, + q.c.vtype, + (q.c.dir_type == DirType.DIR).label("is_dir"), + q.c.source, + q.c.parent, + q.c.name, + q.c.version, + q.c.location, + ) + + @staticmethod + def apply_group_by(q): + return ( + sa.select( + f.min(q.c.id).label("id"), + q.c.vtype, + q.c.is_dir, + q.c.source, + q.c.parent, + q.c.name, + q.c.version, + f.max(q.c.location).label("location"), + ) + .select_from(q) + .group_by( + q.c.source, q.c.parent, q.c.name, q.c.vtype, q.c.is_dir, q.c.version + ) + .order_by( + q.c.source, q.c.parent, q.c.name, q.c.vtype, q.c.is_dir, q.c.version + ) + ) + + @classmethod + def query(cls, q): + q = cls.base_select(q).cte(recursive=True) + parent_parent = path.parent(q.c.parent) + parent_name = path.name(q.c.parent) + q = q.union_all( + sa.select( + sa.literal(-1).label("id"), + sa.literal("").label("vtype"), + true().label("is_dir"), + q.c.source, + parent_parent.label("parent"), + parent_name.label("name"), + sa.literal("").label("version"), + null().label("location"), + ).where((parent_name != "") | (parent_parent != "")) + ) + return cls.apply_group_by(q) + + +class DataTable: + dataset_dir_expansion = staticmethod(DirExpansion.query) + + def __init__( + self, + name: str, + engine: "Engine", + metadata: Optional["sa.MetaData"] = None, + column_types: Optional[dict[str, SQLType]] = None, + ): + self.name: str = name + self.engine = engine + self.metadata: sa.MetaData = metadata if metadata is not None else sa.MetaData() + self.column_types: dict[str, SQLType] = column_types or {} + + @staticmethod + def copy_column(column: sa.Column): + """ + Copy a sqlalchemy Column object intended for use as a signal column. + + This does not copy all attributes as certain attributes such as + table are too context-dependent and the purpose of this function is + adding a signal column from one table to another table. + + We can't use Column.copy() as it only works in certain contexts. + See https://github.com/sqlalchemy/sqlalchemy/issues/5953 + """ + return sa.Column( + column.name, + column.type, + primary_key=column.primary_key, + index=column.index, + nullable=column.nullable, + default=column.default, + server_default=column.server_default, + unique=column.unique, + ) + + @classmethod + def new_table( + cls, + name: str, + columns: Sequence["sa.Column"] = (), + metadata: Optional["sa.MetaData"] = None, + ): + # copy columns, since re-using the same objects from another table + # may raise an error + columns = cls.sys_columns() + [cls.copy_column(c) for c in columns] + columns = dedup_columns(columns) + + if metadata is None: + metadata = sa.MetaData() + return sa.Table(name, metadata, *columns) + + def get_table(self) -> "sa.Table": + table = self.metadata.tables.get(self.name) + if table is None: + sa.Table(self.name, self.metadata, autoload_with=self.engine) + # ^^^ This table may not be correctly initialised on some dialects + # Grab it from metadata instead. + table = self.metadata.tables[self.name] + + column_types = self.column_types | {c.name: c.type for c in self.sys_columns()} + # adjusting types for custom columns to be instances of SQLType if possible + for c in table.columns: + if c.name in column_types: + t = column_types[c.name] + c.type = t() if inspect.isclass(t) else t + return table + + @property + def columns(self) -> "ReadOnlyColumnCollection[str, sa.Column[Any]]": + return self.table.columns + + @property + def c(self): + return self.columns + + @property + def table(self) -> "sa.Table": + return self.get_table() + + def apply_conditions(self, query: "Executable") -> "Executable": + """ + Apply any conditions that belong on all selecting queries. + + This could be used to filter tables that use access control. + """ + return query + + def select(self, *columns): + if not columns: + query = self.table.select() + else: + query = sa.select(*columns) + return self.apply_conditions(query) + + def insert(self): + return self.table.insert() + + def update(self): + return self.apply_conditions(self.table.update()) + + def delete(self): + return self.apply_conditions(self.table.delete()) + + @staticmethod + def sys_columns(): + return [ + sa.Column("id", Int, primary_key=True), + sa.Column( + "random", UInt64, nullable=False, server_default=f.abs(f.random()) + ), + ] + + def dir_expansion(self): + return self.dataset_dir_expansion(self) + + +PARTITION_COLUMN_ID = "partition_id" + +partition_col_names = [PARTITION_COLUMN_ID] + + +def partition_columns() -> Sequence["sa.Column"]: + return [ + sa.Column(PARTITION_COLUMN_ID, sa.Integer), + ] + + +DataTableT = TypeVar("DataTableT", bound=DataTable) + + +class Schema(Generic[DataTableT]): + dataset_row_cls: type[DataTableT] + + +class DefaultSchema(Schema[DataTable]): + def __init__(self): + self.dataset_row_cls = DataTable diff --git a/src/datachain/data_storage/serializer.py b/src/datachain/data_storage/serializer.py new file mode 100644 index 000000000..4be843760 --- /dev/null +++ b/src/datachain/data_storage/serializer.py @@ -0,0 +1,29 @@ +import base64 +import pickle +from abc import abstractmethod +from collections.abc import Callable +from typing import Any + + +class Serializable: + @abstractmethod + def clone_params(self) -> tuple[Callable[..., Any], list[Any], dict[str, Any]]: + """ + Returns the class, args, and kwargs needed to instantiate a cloned copy + of this instance for use in separate processes or machines. + """ + + def serialize(self) -> str: + """ + Returns a string representation of clone params. + This is useful for storing the state of an object in environment variable. + """ + return base64.b64encode(pickle.dumps(self.clone_params())).decode() + + +def deserialize(s: str) -> Serializable: + """ + Returns a new instance of the class represented by the string. + """ + (f, args, kwargs) = pickle.loads(base64.b64decode(s.encode())) # noqa: S301 + return f(*args, **kwargs) diff --git a/src/datachain/data_storage/sqlite.py b/src/datachain/data_storage/sqlite.py new file mode 100644 index 000000000..60f5c67a8 --- /dev/null +++ b/src/datachain/data_storage/sqlite.py @@ -0,0 +1,710 @@ +import logging +import os +import sqlite3 +from collections.abc import Iterable, Sequence +from contextlib import contextmanager +from functools import wraps +from time import sleep +from typing import ( + TYPE_CHECKING, + Any, + Callable, + ClassVar, + Optional, + Union, +) + +import sqlalchemy +from attrs import frozen +from sqlalchemy import MetaData, Table, UniqueConstraint, exists, select +from sqlalchemy.dialects import sqlite +from sqlalchemy.schema import CreateIndex, CreateTable, DropTable +from sqlalchemy.sql import func +from sqlalchemy.sql.expression import bindparam, cast + +import datachain.sql.sqlite +from datachain.data_storage import AbstractDBMetastore, AbstractWarehouse +from datachain.data_storage.db_engine import DatabaseEngine +from datachain.data_storage.id_generator import AbstractDBIDGenerator +from datachain.data_storage.schema import ( + DefaultSchema, + convert_rows_custom_column_types, +) +from datachain.dataset import DatasetRecord +from datachain.error import DataChainError +from datachain.sql.sqlite import create_user_defined_sql_functions, sqlite_dialect +from datachain.sql.sqlite.base import load_usearch_extension +from datachain.sql.types import SQLType +from datachain.storage import StorageURI +from datachain.utils import DataChainDir + +if TYPE_CHECKING: + from sqlalchemy.dialects.sqlite import Insert + from sqlalchemy.schema import SchemaItem + from sqlalchemy.sql.elements import ColumnClause, ColumnElement, TextClause + from sqlalchemy.types import TypeEngine + + +logger = logging.getLogger("datachain") + +RETRY_START_SEC = 0.01 +RETRY_MAX_TIMES = 10 +RETRY_FACTOR = 2 + +Column = Union[str, "ColumnClause[Any]", "TextClause"] + +datachain.sql.sqlite.setup() + +quote_schema = sqlite_dialect.identifier_preparer.quote_schema +quote = sqlite_dialect.identifier_preparer.quote + + +def get_retry_sleep_sec(retry_count: int) -> int: + return RETRY_START_SEC * (RETRY_FACTOR**retry_count) + + +def retry_sqlite_locks(func): + # This retries the database modification in case of concurrent access + @wraps(func) + def wrapper(*args, **kwargs): + exc = None + for retry_count in range(RETRY_MAX_TIMES): + try: + return func(*args, **kwargs) + except sqlite3.OperationalError as operror: + exc = operror + sleep(get_retry_sleep_sec(retry_count)) + raise exc + + return wrapper + + +@frozen +class SQLiteDatabaseEngine(DatabaseEngine): + dialect = sqlite_dialect + + db: sqlite3.Connection + db_file: Optional[str] + + @classmethod + def from_db_file(cls, db_file: Optional[str] = None) -> "SQLiteDatabaseEngine": + detect_types = sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES + + try: + if db_file == ":memory:": + # Enable multithreaded usage of the same in-memory db + db = sqlite3.connect( + "file::memory:?cache=shared", uri=True, detect_types=detect_types + ) + else: + db = sqlite3.connect( + db_file or DataChainDir.find().db, detect_types=detect_types + ) + create_user_defined_sql_functions(db) + engine = sqlalchemy.create_engine( + "sqlite+pysqlite:///", creator=lambda: db, future=True + ) + + db.isolation_level = None # Use autocommit mode + db.execute("PRAGMA foreign_keys = ON") + db.execute("PRAGMA cache_size = -102400") # 100 MiB + # Enable Write-Ahead Log Journaling + db.execute("PRAGMA journal_mode = WAL") + db.execute("PRAGMA synchronous = NORMAL") + db.execute("PRAGMA case_sensitive_like = ON") + if os.environ.get("DEBUG_SHOW_SQL_QUERIES"): + db.set_trace_callback(print) + + load_usearch_extension(db) + + return cls(engine, MetaData(), db, db_file) + except RuntimeError: + raise DataChainError("Can't connect to SQLite DB") from None + + def clone(self) -> "SQLiteDatabaseEngine": + """Clones DatabaseEngine implementation.""" + return SQLiteDatabaseEngine.from_db_file(self.db_file) + + def clone_params(self) -> tuple[Callable[..., Any], list[Any], dict[str, Any]]: + """ + Returns the function, args, and kwargs needed to instantiate a cloned copy + of this DatabaseEngine implementation, for use in separate processes + or machines. + """ + return ( + SQLiteDatabaseEngine.from_db_file, + [self.db_file], + {}, + ) + + @retry_sqlite_locks + def execute( + self, + query, + cursor: Optional[sqlite3.Cursor] = None, + conn=None, + ) -> sqlite3.Cursor: + if cursor is not None: + result = cursor.execute(*self.compile_to_args(query)) + elif conn is not None: + result = conn.execute(*self.compile_to_args(query)) + else: + result = self.db.execute(*self.compile_to_args(query)) + if isinstance(query, CreateTable) and query.element.indexes: + for index in query.element.indexes: + self.execute(CreateIndex(index, if_not_exists=True), cursor=cursor) + return result + + @retry_sqlite_locks + def executemany( + self, query, params, cursor: Optional[sqlite3.Cursor] = None + ) -> sqlite3.Cursor: + if cursor: + return cursor.executemany(self.compile(query).string, params) + return self.db.executemany(self.compile(query).string, params) + + def execute_str(self, sql: str, parameters=None) -> sqlite3.Cursor: + if parameters is None: + return self.db.execute(sql) + return self.db.execute(sql, parameters) + + def insert_dataframe(self, table_name: str, df) -> int: + return df.to_sql(table_name, self.db, if_exists="append", index=False) + + def cursor(self, factory=None): + if factory is None: + return self.db.cursor() + return self.db.cursor(factory) + + def close(self) -> None: + self.db.close() + + @contextmanager + def transaction(self): + db = self.db + with db: + db.execute("begin") + yield db + + def has_table(self, name: str) -> bool: + """ + Return True if a table exists with the given name + + We cannot simply use `inspect(engine).has_table(name)` like the + parent class does because that will return False for a table + created during a pending transaction. Instead, we check the + sqlite_master table. + """ + query = select( + exists( + select(1) + .select_from(sqlalchemy.table("sqlite_master")) + .where( + (sqlalchemy.column("type") == "table") + & (sqlalchemy.column("name") == name) + ) + ) + ) + return bool(next(self.execute(query))[0]) + + def create_table(self, table: "Table", if_not_exists: bool = True) -> None: + self.execute(CreateTable(table, if_not_exists=if_not_exists)) + + def drop_table(self, table: "Table", if_exists: bool = False) -> None: + self.execute(DropTable(table, if_exists=if_exists)) + + def rename_table(self, old_name: str, new_name: str): + comp_old_name = quote_schema(old_name) + comp_new_name = quote_schema(new_name) + self.execute_str(f"ALTER TABLE {comp_old_name} RENAME TO {comp_new_name}") + + +class SQLiteIDGenerator(AbstractDBIDGenerator): + _db: "SQLiteDatabaseEngine" + + def __init__( + self, + db: Optional["SQLiteDatabaseEngine"] = None, + table_prefix: Optional[str] = None, + skip_db_init: bool = False, + db_file: Optional[str] = None, + ): + db = db or SQLiteDatabaseEngine.from_db_file(db_file) + + super().__init__(db, table_prefix, skip_db_init) + + def clone(self) -> "SQLiteIDGenerator": + """Clones SQLiteIDGenerator implementation.""" + return SQLiteIDGenerator( + self._db.clone(), self._table_prefix, skip_db_init=True + ) + + def clone_params(self) -> tuple[Callable[..., Any], list[Any], dict[str, Any]]: + """ + Returns the function, args, and kwargs needed to instantiate a cloned copy + of this SQLiteIDGenerator implementation, for use in separate processes + or machines. + """ + return ( + SQLiteIDGenerator.init_after_clone, + [], + { + "db_clone_params": self._db.clone_params(), + "table_prefix": self._table_prefix, + }, + ) + + @classmethod + def init_after_clone( + cls, + *, + db_clone_params: tuple[Callable, list, dict[str, Any]], + table_prefix: Optional[str] = None, + ) -> "SQLiteIDGenerator": + """ + Initializes a new instance of this SQLiteIDGenerator implementation + using the given parameters, which were obtained from a call to clone_params. + """ + (db_class, db_args, db_kwargs) = db_clone_params + return cls( + db=db_class(*db_args, **db_kwargs), + table_prefix=table_prefix, + skip_db_init=True, + ) + + @property + def db(self) -> "SQLiteDatabaseEngine": + return self._db + + def init_id(self, uri: str) -> None: + """Initializes the ID generator for the given URI with zero last_id.""" + self._db.execute( + sqlite.insert(self._table) + .values(uri=uri, last_id=0) + .on_conflict_do_nothing() + ) + + def get_next_ids(self, uri: str, count: int) -> range: + """Returns a range of IDs for the given URI.""" + + # NOTE: we can't use RETURNING clause here because it is only available + # in sqlalchemy v2, see + # https://github.com/sqlalchemy/sqlalchemy/issues/6195#issuecomment-1248700677 + # After we upgrade to sqlalchemy v2, we can use the following code, + # leaving fallback to the current implementation for older versions of SQLite, + # which is still supported, for example, in Ubuntu 20.04 LTS (Focal Fossa), + # where SQLite version 3.31.1 is used. + + # sqlite_version = version.parse(sqlite3.sqlite_version) + # if sqlite_version >= version.parse("3.35.0"): + # # RETURNING is supported on SQLite 3.35.0 (2021-03-12) or newer + # stmt = ( + # sqlite.insert(self._table) + # .values(uri=uri, last_id=count) + # .on_conflict_do_update( + # index_elements=["uri"], + # set_={"last_id": self._table.c.last_id + count}, + # ) + # .returning(self._table.c.last_id) + # ) + # last_id = self._db.execute(stmt).fetchone()[0] + # else: + # (fallback to the current implementation with a transaction) + + # Transactions ensure no concurrency conflicts + with self._db.transaction() as conn: + # UPSERT syntax was added to SQLite with version 3.24.0 (2018-06-04). + stmt_ins = ( + sqlite.insert(self._table) + .values(uri=uri, last_id=count) + .on_conflict_do_update( + index_elements=["uri"], + set_={"last_id": self._table.c.last_id + count}, + ) + ) + self._db.execute(stmt_ins, conn=conn) + + stmt_sel = select(self._table.c.last_id).where(self._table.c.uri == uri) + last_id = self._db.execute(stmt_sel, conn=conn).fetchone()[0] + + return range(last_id - count + 1, last_id + 1) + + +class SQLiteMetastore(AbstractDBMetastore): + """ + SQLite Metastore uses SQLite3 for storing indexed data locally. + This is currently used for the local cli. + """ + + id_generator: "SQLiteIDGenerator" + db: "SQLiteDatabaseEngine" + + def __init__( + self, + id_generator: "SQLiteIDGenerator", + uri: StorageURI = StorageURI(""), + partial_id: Optional[int] = None, + db: Optional["SQLiteDatabaseEngine"] = None, + db_file: Optional[str] = None, + ): + self.schema: DefaultSchema = DefaultSchema() + super().__init__(id_generator, uri, partial_id) + + # needed for dropping tables in correct order for tests because of + # foreign keys + self.default_table_names: list[str] = [] + + self.db = db or SQLiteDatabaseEngine.from_db_file(db_file) + + self._init_tables() + + def clone( + self, + uri: StorageURI = StorageURI(""), + partial_id: Optional[int] = None, + use_new_connection: bool = False, + ) -> "SQLiteMetastore": + if not uri: + if partial_id is not None: + raise ValueError("if partial_id is used, uri cannot be empty") + if self.uri: + uri = self.uri + if self.partial_id: + partial_id = self.partial_id + return SQLiteMetastore( + self.id_generator.clone(), + uri=uri, + partial_id=partial_id, + db=self.db.clone(), + ) + + def clone_params(self) -> tuple[Callable[..., Any], list[Any], dict[str, Any]]: + """ + Returns the class, args, and kwargs needed to instantiate a cloned copy of this + SQLiteDataStorage implementation, for use in separate processes or machines. + """ + return ( + SQLiteMetastore.init_after_clone, + [], + { + "id_generator_clone_params": self.id_generator.clone_params(), + "uri": self.uri, + "partial_id": self.partial_id, + "db_clone_params": self.db.clone_params(), + }, + ) + + @classmethod + def init_after_clone( + cls, + *, + id_generator_clone_params: tuple[Callable, list, dict[str, Any]], + uri: StorageURI, + partial_id: Optional[int], + db_clone_params: tuple[Callable, list, dict[str, Any]], + ) -> "SQLiteMetastore": + ( + id_generator_class, + id_generator_args, + id_generator_kwargs, + ) = id_generator_clone_params + (db_class, db_args, db_kwargs) = db_clone_params + return cls( + id_generator=id_generator_class(*id_generator_args, **id_generator_kwargs), + uri=uri, + partial_id=partial_id, + db=db_class(*db_args, **db_kwargs), + ) + + def _init_tables(self) -> None: + """Initialize tables.""" + self.db.create_table(self._storages, if_not_exists=True) + self.default_table_names.append(self._storages.name) + self.db.create_table(self._datasets, if_not_exists=True) + self.default_table_names.append(self._datasets.name) + self.db.create_table(self._datasets_versions, if_not_exists=True) + self.default_table_names.append(self._datasets_versions.name) + self.db.create_table(self._datasets_dependencies, if_not_exists=True) + self.default_table_names.append(self._datasets_dependencies.name) + self.db.create_table(self._jobs, if_not_exists=True) + self.default_table_names.append(self._jobs.name) + + def init(self, uri: StorageURI) -> None: + if not uri: + raise ValueError("uri for init() cannot be empty") + partials_table = self._partials_table(uri) + self.db.create_table(partials_table, if_not_exists=True) + + @classmethod + def _buckets_columns(cls) -> list["SchemaItem"]: + """Buckets (storages) table columns.""" + return [*super()._buckets_columns(), UniqueConstraint("uri")] + + @classmethod + def _datasets_columns(cls) -> list["SchemaItem"]: + """Datasets table columns.""" + return [*super()._datasets_columns(), UniqueConstraint("name")] + + def _storages_insert(self) -> "Insert": + return sqlite.insert(self._storages) + + def _partials_insert(self) -> "Insert": + return sqlite.insert(self._partials) + + def _datasets_insert(self) -> "Insert": + return sqlite.insert(self._datasets) + + def _datasets_versions_insert(self) -> "Insert": + return sqlite.insert(self._datasets_versions) + + def _datasets_dependencies_insert(self) -> "Insert": + return sqlite.insert(self._datasets_dependencies) + + # + # Storages + # + + def mark_storage_not_indexed(self, uri: StorageURI) -> None: + """ + Mark storage as not indexed. + This method should be called when storage index is deleted. + """ + self.db.execute(self._storages_delete().where(self._storages.c.uri == uri)) + + # + # Dataset dependencies + # + + def _dataset_dependencies_select_columns(self) -> list["SchemaItem"]: + return [ + self._datasets_dependencies.c.id, + self._datasets_dependencies.c.dataset_id, + self._datasets_dependencies.c.dataset_version_id, + self._datasets_dependencies.c.bucket_id, + self._datasets_dependencies.c.bucket_version, + self._datasets.c.name, + self._datasets.c.created_at, + self._datasets_versions.c.version, + self._datasets_versions.c.created_at, + self._storages.c.uri, + ] + + # + # Jobs + # + + def _jobs_insert(self) -> "Insert": + return sqlite.insert(self._jobs) + + def get_possibly_stale_jobs(self) -> list[tuple[str, str, int]]: + raise NotImplementedError("get_possibly_stale_jobs not implemented for SQLite") + + +class SQLiteWarehouse(AbstractWarehouse): + """ + SQLite Warehouse uses SQLite3 for storing indexed data locally. + This is currently used for the local cli. + """ + + id_generator: "SQLiteIDGenerator" + db: "SQLiteDatabaseEngine" + + # Cache for our defined column types to dialect specific TypeEngine relations + _col_python_type: ClassVar[dict[type, "TypeEngine"]] = {} + + def __init__( + self, + id_generator: "SQLiteIDGenerator", + db: Optional["SQLiteDatabaseEngine"] = None, + db_file: Optional[str] = None, + ): + self.schema: DefaultSchema = DefaultSchema() + super().__init__(id_generator) + + self.db = db or SQLiteDatabaseEngine.from_db_file(db_file) + + def clone(self, use_new_connection: bool = False) -> "SQLiteWarehouse": + return SQLiteWarehouse(self.id_generator.clone(), db=self.db.clone()) + + def clone_params(self) -> tuple[Callable[..., Any], list[Any], dict[str, Any]]: + """ + Returns the class, args, and kwargs needed to instantiate a cloned copy of this + SQLiteDataStorage implementation, for use in separate processes or machines. + """ + return ( + SQLiteWarehouse.init_after_clone, + [], + { + "id_generator_clone_params": self.id_generator.clone_params(), + "db_clone_params": self.db.clone_params(), + }, + ) + + @classmethod + def init_after_clone( + cls, + *, + id_generator_clone_params: tuple[Callable, list, dict[str, Any]], + db_clone_params: tuple[Callable, list, dict[str, Any]], + ) -> "SQLiteWarehouse": + ( + id_generator_class, + id_generator_args, + id_generator_kwargs, + ) = id_generator_clone_params + (db_class, db_args, db_kwargs) = db_clone_params + return cls( + id_generator=id_generator_class(*id_generator_args, **id_generator_kwargs), + db=db_class(*db_args, **db_kwargs), + ) + + def _reflect_tables(self, filter_tables=None): + """ + Since some tables are prone to schema extension, meaning we can add + additional columns to it, we should reflect changes in metadata + to have the latest columns when dealing with those tables. + If filter function is defined, it's used to filter out tables to reflect, + otherwise all tables are reflected + """ + self.db.metadata.reflect( + bind=self.db.engine, + extend_existing=True, + only=filter_tables, + ) + + def is_ready(self, timeout: Optional[int] = None) -> bool: + return True + + def create_dataset_rows_table( + self, + name: str, + columns: Sequence["sqlalchemy.Column"] = (), + if_not_exists: bool = True, + ) -> Table: + table = self.schema.dataset_row_cls.new_table( + name, + columns=columns, + metadata=self.db.metadata, + ) + self.db.create_table(table, if_not_exists=if_not_exists) + return table + + def dataset_rows_select( + self, select_query: sqlalchemy.sql.selectable.Select, **kwargs + ): + rows = self.db.execute(select_query, **kwargs) + yield from convert_rows_custom_column_types( + select_query.columns, rows, sqlite_dialect + ) + + def get_dataset_sources( + self, dataset: DatasetRecord, version: int + ) -> list[StorageURI]: + dr = self.dataset_rows(dataset, version) + query = dr.select(dr.c.source).distinct() + cur = self.db.cursor() + cur.row_factory = sqlite3.Row # type: ignore[assignment] + + return [StorageURI(row["source"]) for row in self.db.execute(query, cursor=cur)] + + def merge_dataset_rows( + self, + src: DatasetRecord, + dst: DatasetRecord, + src_version: int, + dst_version: int, + ) -> None: + dst_empty = False + + if not self.db.has_table(self.dataset_table_name(src.name, src_version)): + # source table doesn't exist, nothing to do + return + + src_dr = self.dataset_rows(src, src_version).table + + if not self.db.has_table(self.dataset_table_name(dst.name, dst_version)): + # destination table doesn't exist, create it + self.create_dataset_rows_table( + self.dataset_table_name(dst.name, dst_version), + columns=src_dr.c, + ) + dst_empty = True + + dst_dr = self.dataset_rows(dst, dst_version).table + merge_fields = [c.name for c in src_dr.c if c.name != "id"] + select_src = select(*(getattr(src_dr.c, f) for f in merge_fields)) + + if dst_empty: + # we don't need union, but just select from source to destination + insert_query = sqlite.insert(dst_dr).from_select(merge_fields, select_src) + else: + dst_version_latest = None + # find the previous version of the destination dataset + dst_previous_versions = [ + v.version + for v in dst.versions # type: ignore [union-attr] + if v.version < dst_version + ] + if dst_previous_versions: + dst_version_latest = max(dst_previous_versions) + + dst_dr_latest = self.dataset_rows(dst, dst_version_latest).table + + select_dst_latest = select( + *(getattr(dst_dr_latest.c, f) for f in merge_fields) + ) + union_query = sqlalchemy.union(select_src, select_dst_latest) + insert_query = ( + sqlite.insert(dst_dr) + .from_select(merge_fields, union_query) + .prefix_with("OR IGNORE") + ) + + self.db.execute(insert_query) + + def insert_rows(self, table: Table, rows: Iterable[dict[str, Any]]) -> None: + rows = list(rows) + if not rows: + return + self.db.executemany( + table.insert().values({f: bindparam(f) for f in rows[0]}), + rows, + ) + + def insert_dataset_rows(self, df, dataset: DatasetRecord, version: int) -> int: + dr = self.dataset_rows(dataset, version) + return self.db.insert_dataframe(dr.table.name, df) + + def instr(self, source, target) -> "ColumnElement": + return cast(func.instr(source, target), sqlalchemy.Boolean) + + def get_table(self, name: str) -> sqlalchemy.Table: + # load table with latest schema to metadata + self._reflect_tables(filter_tables=lambda t, _: t == name) + return self.db.metadata.tables[name] + + def python_type(self, col_type: Union["TypeEngine", "SQLType"]) -> Any: + if isinstance(col_type, SQLType): + # converting our defined column types to dialect specific TypeEngine + col_type_cls = type(col_type) + if col_type_cls not in self._col_python_type: + self._col_python_type[col_type_cls] = col_type.type_engine( + sqlite_dialect + ) + col_type = self._col_python_type[col_type_cls] + + return col_type.python_type + + def dataset_table_export_file_names( + self, dataset: DatasetRecord, version: int + ) -> list[str]: + raise NotImplementedError("Exporting dataset table not implemented for SQLite") + + def export_dataset_table( + self, + bucket_uri: str, + dataset: DatasetRecord, + version: int, + client_config=None, + ) -> list[str]: + raise NotImplementedError("Exporting dataset table not implemented for SQLite") diff --git a/src/datachain/data_storage/warehouse.py b/src/datachain/data_storage/warehouse.py new file mode 100644 index 000000000..27588e67b --- /dev/null +++ b/src/datachain/data_storage/warehouse.py @@ -0,0 +1,960 @@ +import glob +import json +import logging +import posixpath +from abc import ABC, abstractmethod +from collections.abc import Generator, Iterable, Iterator, Sequence +from typing import TYPE_CHECKING, Any, Optional, Union +from urllib.parse import urlparse + +import attrs +import sqlalchemy as sa +from sqlalchemy import Table, case, select +from sqlalchemy.sql import func +from sqlalchemy.sql.expression import true + +from datachain.client import Client +from datachain.data_storage.serializer import Serializable +from datachain.dataset import DatasetRecord, RowDict +from datachain.node import DirType, DirTypeGroup, Entry, Node, NodeWithPath, get_path +from datachain.sql.types import Int, SQLType +from datachain.storage import StorageURI +from datachain.utils import sql_escape_like + +if TYPE_CHECKING: + from sqlalchemy.sql._typing import _ColumnsClauseArgument + from sqlalchemy.sql.elements import ColumnElement + from sqlalchemy.types import TypeEngine + + from datachain.data_storage import AbstractIDGenerator, schema + from datachain.data_storage.db_engine import DatabaseEngine + from datachain.data_storage.schema import DataTable + +try: + import numpy as np + + numpy_imported = True +except ImportError: + numpy_imported = False + + +logger = logging.getLogger("datachain") + +SELECT_BATCH_SIZE = 100_000 # number of rows to fetch at a time + + +class AbstractWarehouse(ABC, Serializable): + """ + Abstract Warehouse class, to be implemented by any Database Adapters + for a specific database system. This manages the storing, searching, and + retrieval of datasets data, and has shared logic for all database + systems currently in use. + """ + + # + # Constants, Initialization, and Tables + # + + DATASET_TABLE_PREFIX = "ds_" + DATASET_SOURCE_TABLE_PREFIX = "src_" + UDF_TABLE_NAME_PREFIX = "udf_" + TMP_TABLE_NAME_PREFIX = "tmp_" + + id_generator: "AbstractIDGenerator" + schema: "schema.Schema" + db: "DatabaseEngine" + + def __init__(self, id_generator: "AbstractIDGenerator"): + self.id_generator = id_generator + + def cleanup_for_tests(self): + """Cleanup for tests.""" + + def convert_type( # noqa: PLR0911 + self, + val: Any, + col_type: SQLType, + col_python_type: Any, + col_type_name: str, + col_name: str, + ) -> Any: + """ + Tries to convert value to specific type if needed and if compatible, + otherwise throws an ValueError. + If value is a list or some other iterable, it tries to convert sub elements + as well + """ + if numpy_imported and isinstance(val, (np.ndarray, np.generic)): + val = val.tolist() + + # Optimization: Precompute all the column type variables. + value_type = type(val) + + exc = None + try: + if col_python_type is list and value_type in (list, tuple, set): + if len(val) == 0: + return [] + item_python_type = self.python_type(col_type.item_type) + if item_python_type is not list: + if isinstance(val[0], item_python_type): + return val + if item_python_type is float and isinstance(val[0], int): + return [float(i) for i in val] + # Optimization: Reuse these values for each function call within the + # list comprehension. + item_type_info = ( + col_type.item_type, + item_python_type, + type(col_type.item_type).__name__, + col_name, + ) + return [self.convert_type(i, *item_type_info) for i in val] + # Special use case with JSON type as we save it as string + if col_python_type is dict or col_type_name == "JSON": + if value_type is str: + return val + if value_type in (dict, list): + return json.dumps(val) + raise ValueError( + f"Cannot convert value {val!r} with type {value_type} to JSON" + ) + + if isinstance(val, col_python_type): + return val + if col_python_type is float and isinstance(val, int): + return float(val) + except Exception as e: # noqa: BLE001 + exc = e + ve = ValueError( + f"Value {val!r} with type {value_type} incompatible for " + f"column type {col_type_name}" + ) + # This is the same as "from exc" when not raising the exception. + if exc: + ve.__cause__ = exc + # Optimization: Log here, so only one try/except is needed, since the ValueError + # above is raised after logging. + logger.exception( + "Error while validating/converting type for column " + "%s with value %s, original error %s", + col_name, + val, + ve, + ) + raise ve + + @abstractmethod + def clone(self, use_new_connection: bool = False) -> "AbstractWarehouse": + """Clones Warehouse implementation for some Storage input. + Setting use_new_connection will always use a new database connection. + New connections should only be used if needed due to errors with + closed connections.""" + + def close(self) -> None: + """Closes any active database connections.""" + self.db.close() + + # + # Query Tables + # + + @abstractmethod + def is_ready(self, timeout: Optional[int] = None) -> bool: ... + + def dataset_rows(self, dataset: DatasetRecord, version: Optional[int] = None): + version = version or dataset.latest_version + + table_name = self.dataset_table_name(dataset.name, version) + return self.schema.dataset_row_cls( + table_name, + self.db.engine, + self.db.metadata, + dataset.get_schema(version), + ) + + @property + def dataset_row_cls(self) -> type["DataTable"]: + return self.schema.dataset_row_cls + + # + # Query Execution + # + + def dataset_select_paginated( + self, + query, + limit: Optional[int] = None, + order_by: tuple["ColumnElement[Any]", ...] = (), + page_size: int = SELECT_BATCH_SIZE, + ) -> Generator[RowDict, None, None]: + """ + This is equivalent to `db.execute`, but for selecting rows in batches + """ + cols = query.selected_columns + cols_names = [c.name for c in cols] + + if not order_by: + ordering = [cols.id] + else: + ordering = order_by # type: ignore[assignment] + + # reset query order by and apply new order by id + paginated_query = query.order_by(None).order_by(*ordering).limit(page_size) + + results = None + offset = 0 + num_yielded = 0 + try: + while True: + if limit is not None: + limit -= num_yielded + if limit == 0: + break + if limit < page_size: + paginated_query = paginated_query.limit(None).limit(limit) + + results = self.db.execute(paginated_query.offset(offset)) + + processed = False + for row in results: + processed = True + yield RowDict(zip(cols_names, row)) + num_yielded += 1 + + if not processed: + break # no more results + offset += page_size + finally: + # https://www2.sqlite.org/cvstrac/wiki?p=DatabaseIsLocked (SELECT not + # finalized or reset) to prevent database table is locked error when an + # exception is raised in the middle of processing the results (e.g. + # https://github.com/iterative/dvcx/issues/924). Connections close + # apparently is not enough in some cases, at least on sqlite + # https://www.sqlite.org/c3ref/close.html + if results and hasattr(results, "close"): + results.close() + + # + # Table Name Internal Functions + # + + @staticmethod + def uri_to_storage_info(uri: str) -> tuple[str, str]: + parsed = urlparse(uri) + name = parsed.path if parsed.scheme == "file" else parsed.netloc + return parsed.scheme, name + + def dataset_table_name(self, dataset_name: str, version: int) -> str: + prefix = self.DATASET_TABLE_PREFIX + if Client.is_data_source_uri(dataset_name): + # for datasets that are created for bucket listing we use different prefix + prefix = self.DATASET_SOURCE_TABLE_PREFIX + return f"{prefix}{dataset_name}_{version}" + + # + # Datasets + # + + @abstractmethod + def create_dataset_rows_table( + self, + name: str, + columns: Sequence["sa.Column"] = (), + if_not_exists: bool = True, + ) -> Table: + """Creates a dataset rows table for the given dataset name and columns""" + + def drop_dataset_rows_table( + self, + dataset: DatasetRecord, + version: int, + if_exists: bool = True, + ) -> None: + """Drops a dataset rows table for the given dataset name.""" + table_name = self.dataset_table_name(dataset.name, version) + table = Table(table_name, self.db.metadata) + self.db.drop_table(table, if_exists=if_exists) + + @abstractmethod + def merge_dataset_rows( + self, + src: "DatasetRecord", + dst: "DatasetRecord", + src_version: int, + dst_version: int, + ) -> None: + """ + Merges source dataset rows and current latest destination dataset rows + into a new rows table created for new destination dataset version. + Note that table for new destination version must be created upfront. + Merge results should not contain duplicates. + """ + + @abstractmethod + def dataset_rows_select(self, select_query: sa.sql.selectable.Select, **kwargs): + """ + Method for fetching dataset rows from database. This is abstract since + in some DBs we need to use special settings + """ + + @abstractmethod + def get_dataset_sources( + self, dataset: DatasetRecord, version: int + ) -> list[StorageURI]: ... + + def nodes_dataset_query( + self, + dataset_rows: "DataTable", + *, + column_names: Iterable[str], + path: Optional[str] = None, + recursive: Optional[bool] = False, + ) -> "sa.Select": + """ + Creates query pointing to certain bucket listing represented by dataset_rows + The given `column_names` + will be selected in the order they're given. `path` is a glob which + will select files in matching directories, or if `recursive=True` is + set then the entire tree under matching directories will be selected. + """ + dr = dataset_rows + + def _is_glob(path: str) -> bool: + return any(c in path for c in ["*", "?", "[", "]"]) + + column_objects = [dr.c[c] for c in column_names] + # include all object types - file, tar archive, tar file (subobject) + select_query = dr.select(*column_objects).where( + dr.c.dir_type.in_(DirTypeGroup.FILE) & (dr.c.is_latest == true()) + ) + if path is None: + return select_query + if recursive: + root = False + where = self.path_expr(dr).op("GLOB")(path) + if not path or path == "/": + # root of the bucket, e.g s3://bucket/ -> getting all the nodes + # in the bucket + root = True + + if not root and not _is_glob(path): + # not a root and not a explicit glob, so it's pointing to some directory + # and we are adding a proper glob syntax for it + # e.g s3://bucket/dir1 -> s3://bucket/dir1/* + dir_path = path.rstrip("/") + "/*" + where = where | self.path_expr(dr).op("GLOB")(dir_path) + + if not root: + # not a root, so running glob query + select_query = select_query.where(where) + + else: + parent = self.get_node_by_path(dr, path.lstrip("/").rstrip("/*")) + select_query = select_query.where( + (dr.c.parent == parent.path) | (self.path_expr(dr) == path) + ) + return select_query + + def rename_dataset_table( + self, + old_name: str, + new_name: str, + old_version: int, + new_version: int, + ) -> None: + old_ds_table_name = self.dataset_table_name(old_name, old_version) + new_ds_table_name = self.dataset_table_name(new_name, new_version) + + self.db.rename_table(old_ds_table_name, new_ds_table_name) + + def dataset_rows_count(self, dataset: DatasetRecord, version=None) -> int: + """Returns total number of rows in a dataset""" + dr = self.dataset_rows(dataset, version) + table = dr.get_table() + query = select(sa.func.count(table.c.id)) + (res,) = self.db.execute(query) + return res[0] + + def dataset_stats( + self, dataset: DatasetRecord, version: int + ) -> tuple[Optional[int], Optional[int]]: + """ + Returns tuple with dataset stats: total number of rows and total dataset size. + """ + if not (self.db.has_table(self.dataset_table_name(dataset.name, version))): + return None, None + + dr = self.dataset_rows(dataset, version) + table = dr.get_table() + expressions: tuple[_ColumnsClauseArgument[Any], ...] = ( + sa.func.count(table.c.id), + ) + if "size" in table.columns: + expressions = (*expressions, sa.func.sum(table.c.size)) + query = select(*expressions) + ((nrows, *rest),) = self.db.execute(query) + return nrows, rest[0] if rest else None + + def prepare_entries( + self, uri: str, entries: Iterable[Entry] + ) -> list[dict[str, Any]]: + """ + Prepares bucket listing entry (row) for inserting into database + """ + + def _prepare_entry(entry: Entry): + assert entry.dir_type is not None + return attrs.asdict(entry) | {"source": uri} + + return [_prepare_entry(e) for e in entries] + + @abstractmethod + def insert_rows(self, table: Table, rows: Iterable[dict[str, Any]]) -> None: + """Does batch inserts of any kind of rows into table""" + + def insert_rows_done(self, table: Table) -> None: + """ + Only needed for certain implementations + to signal when rows inserts are complete. + """ + + @abstractmethod + def insert_dataset_rows(self, df, dataset: DatasetRecord, version: int) -> int: + """Inserts dataset rows directly into dataset table""" + + @abstractmethod + def instr(self, source, target) -> "ColumnElement": + """ + Return SQLAlchemy Boolean determining if a target substring is present in + source string column + """ + + @abstractmethod + def get_table(self, name: str) -> sa.Table: + """ + Returns a SQLAlchemy Table object by name. If table doesn't exist, it should + create it + """ + + @abstractmethod + def dataset_table_export_file_names( + self, dataset: DatasetRecord, version: int + ) -> list[str]: + """ + Returns list of file names that will be created when user runs dataset export + """ + + @abstractmethod + def export_dataset_table( + self, + bucket_uri: str, + dataset: DatasetRecord, + version: int, + client_config=None, + ) -> list[str]: + """ + Exports dataset table to the cloud, e.g to some s3 bucket + """ + + def python_type(self, col_type: Union["TypeEngine", "SQLType"]) -> Any: + """Returns python type representation of some Sqlalchemy column type""" + return col_type.python_type + + def add_node_type_where( + self, + query: sa.Select, + type: str, + include_subobjects: bool = True, + ) -> sa.Select: + file_group: Sequence[int] + if type in {"f", "file", "files"}: + if include_subobjects: + file_group = DirTypeGroup.SUBOBJ_FILE + else: + file_group = DirTypeGroup.FILE + elif type in {"d", "dir", "directory", "directories"}: + if include_subobjects: + file_group = DirTypeGroup.SUBOBJ_DIR + else: + file_group = DirTypeGroup.DIR + else: + raise ValueError(f"invalid file type: {type!r}") + + c = query.selected_columns + q = query.where(c.dir_type.in_(file_group)) + if not include_subobjects: + q = q.where(c.vtype == "") + return q + + def get_nodes(self, query) -> Iterator[Node]: + """ + This gets nodes based on the provided query, and should be used sparingly, + as it will be slow on any OLAP database systems. + """ + columns = [c.name for c in query.columns] + for row in self.db.execute(query): + d = dict(zip(columns, row)) + yield Node(**d) + + def get_dirs_by_parent_path( + self, + dataset_rows: "DataTable", + parent_path: str, + ) -> Iterator[Node]: + """Gets nodes from database by parent path, with optional filtering""" + dr = dataset_rows + query = self._find_query( + dr, + parent_path, + type="dir", + conds=[sa.Column("parent") == parent_path], + order_by=["source", "parent", "name"], + ) + return self.get_nodes(query) + + def _get_nodes_by_glob_path_pattern( + self, dataset_rows: "DataTable", path_list: list[str], glob_name: str + ) -> Iterator[Node]: + """Finds all Nodes that correspond to GLOB like path pattern.""" + dr = dataset_rows + de = dr.dataset_dir_expansion( + dr.select().where(dr.c.is_latest == true()).subquery() + ).subquery() + path_glob = "/".join([*path_list, glob_name]) + dirpath = path_glob[: -len(glob_name)] + relpath = func.substr(self.path_expr(de), len(dirpath) + 1) + + return self.get_nodes( + self.expand_query(de, dr) + .where( + (self.path_expr(de).op("GLOB")(path_glob)) + & ~self.instr(relpath, "/") + & (self.path_expr(de) != dirpath) + ) + .order_by(de.c.source, de.c.parent, de.c.name, de.c.version) + ) + + def _get_node_by_path_list( + self, dataset_rows: "DataTable", path_list: list[str], name: str + ) -> Node: + """ + Gets node that correspond some path list, e.g ["data-lakes", "dogs-and-cats"] + """ + parent = "/".join(path_list) + dr = dataset_rows + de = dr.dataset_dir_expansion( + dr.select().where(dr.c.is_latest == true()).subquery() + ).subquery() + query = self.expand_query(de, dr) + + q = query.where((de.c.parent == parent) & (de.c.name == name)).order_by( + de.c.source, de.c.parent, de.c.name, de.c.version + ) + row = next(self.dataset_rows_select(q), None) + if not row: + path = f"{parent}/{name}" + raise FileNotFoundError(f"Unable to resolve path {path!r}") + return Node(*row) + + def _populate_nodes_by_path( + self, dataset_rows: "DataTable", path_list: list[str] + ) -> list[Node]: + """ + Puts all nodes found by path_list into the res input variable. + Note that path can have GLOB like pattern matching which means that + res can have multiple nodes as result. + If there is no GLOB pattern, res should have one node as result that + match exact path by path_list + """ + if not path_list: + return [self._get_node_by_path_list(dataset_rows, [], "")] + matched_paths: list[list[str]] = [[]] + for curr_name in path_list[:-1]: + if glob.has_magic(curr_name): + new_paths: list[list[str]] = [] + for path in matched_paths: + nodes = self._get_nodes_by_glob_path_pattern( + dataset_rows, path, curr_name + ) + new_paths.extend([*path, n.name] for n in nodes if n.is_container) + matched_paths = new_paths + else: + for path in matched_paths: + path.append(curr_name) + curr_name = path_list[-1] + if glob.has_magic(curr_name): + result: list[Node] = [] + for path in matched_paths: + nodes = self._get_nodes_by_glob_path_pattern( + dataset_rows, path, curr_name + ) + result.extend(nodes) + else: + result = [ + self._get_node_by_path_list(dataset_rows, path, curr_name) + for path in matched_paths + ] + return result + + @staticmethod + def expand_query(dir_expanded_query, dataset_rows: "DataTable"): + dr = dataset_rows + de = dir_expanded_query + + def with_default(column): + default = getattr(attrs.fields(Node), column.name).default + return func.coalesce(column, default).label(column.name) + + return sa.select( + de.c.id, + with_default(dr.c.vtype), + case((de.c.is_dir == true(), DirType.DIR), else_=dr.c.dir_type).label( + "dir_type" + ), + de.c.parent, + de.c.name, + with_default(dr.c.etag), + de.c.version, + with_default(dr.c.is_latest), + dr.c.last_modified, + with_default(dr.c.size), + with_default(dr.c.owner_name), + with_default(dr.c.owner_id), + with_default(dr.c.random), + dr.c.location, + de.c.source, + ).select_from(de.outerjoin(dr.table, de.c.id == dr.c.id)) + + def get_node_by_path(self, dataset_rows: "DataTable", path: str) -> Node: + """Gets node that corresponds to some path""" + if path == "": + return Node.root() + dr = dataset_rows + if not path.endswith("/"): + query = dr.select().where( + self.path_expr(dr) == path, + dr.c.is_latest == true(), + dr.c.dir_type != DirType.DIR, + ) + row = next(self.db.execute(query), None) + if row is not None: + return Node(*row) + path += "/" + query = sa.select(1).where( + dr.select() + .where( + dr.c.is_latest == true(), + dr.c.dir_type != DirType.DIR, + (dr.c.parent + "/").startswith(path), + ) + .exists() + ) + row = next(self.db.execute(query), None) + if not row: + raise FileNotFoundError(f"Unable to resolve path {path}") + path = path.removesuffix("/") + parent, name = path.rsplit("/", 1) if "/" in path else ("", path) + return Node.from_dir(parent, name) + + def expand_path(self, dataset_rows: "DataTable", path: str) -> list[Node]: + """Simulates Unix-like shell expansion""" + clean_path = path.strip("/") + path_list = clean_path.split("/") if clean_path != "" else [] + res = self._populate_nodes_by_path(dataset_rows, path_list) + if path.endswith("/"): + res = [node for node in res if node.dir_type in DirTypeGroup.SUBOBJ_DIR] + return res + + def select_node_fields_by_parent_path( + self, + dataset_rows: "DataTable", + parent_path: str, + fields: Iterable[str], + ) -> Iterator[tuple[Any, ...]]: + """ + Gets latest-version file nodes from the provided parent path + """ + dr = dataset_rows + de = dr.dataset_dir_expansion( + dr.select().where(dr.c.is_latest == true()).subquery() + ).subquery() + where_cond = de.c.parent == parent_path + if parent_path == "": + # Exclude the root dir + where_cond = where_cond & (de.c.name != "") + inner_query = self.expand_query(de, dr).where(where_cond).subquery() + return self.db.execute( + sa.select(*(getattr(inner_query.c, f) for f in fields)) + .select_from(inner_query) + .order_by( + inner_query.c.source, + inner_query.c.parent, + inner_query.c.name, + inner_query.c.version, + ) + ) + + def select_node_fields_by_parent_path_tar( + self, dataset_rows: "DataTable", parent_path: str, fields: Iterable[str] + ) -> Iterator[tuple[Any, ...]]: + """ + Gets latest-version file nodes from the provided parent path + """ + dr = dataset_rows + dirpath = f"{parent_path}/" + relpath = func.substr(self.path_expr(dr), len(dirpath) + 1) + + def field_to_expr(f): + if f == "name": + return relpath + return getattr(dr.c, f) + + q = ( + select(*(field_to_expr(f) for f in fields)) + .where( + self.path_expr(dr).like(f"{sql_escape_like(dirpath)}%"), + ~self.instr(relpath, "/"), + dr.c.is_latest == true(), + ) + .order_by(dr.c.source, dr.c.parent, dr.c.name, dr.c.version, dr.c.etag) + ) + return self.db.execute(q) + + def size( + self, + dataset_rows: "DataTable", + node: Union[Node, dict[str, Any]], + count_files: bool = False, + ) -> tuple[int, int]: + """ + Calculates size of some node (and subtree below node). + Returns size in bytes as int and total files as int + """ + if isinstance(node, dict): + is_dir = node.get("is_dir", node["dir_type"] in DirTypeGroup.SUBOBJ_DIR) + node_size = node["size"] + path = get_path(node["parent"], node["name"]) + else: + is_dir = node.is_container + node_size = node.size + path = node.path + if not is_dir: + # Return node size if this is not a directory + return node_size, 1 + + sub_glob = posixpath.join(path, "*") + dr = dataset_rows + selections = [ + func.sum(dr.c.size), + ] + if count_files: + selections.append( + func.sum(dr.c.dir_type.in_(DirTypeGroup.FILE)), + ) + results = next( + self.db.execute( + dr.select(*selections).where( + (self.path_expr(dr).op("GLOB")(sub_glob)) + & (dr.c.is_latest == true()) + ) + ), + (0, 0), + ) + if count_files: + return results[0] or 0, results[1] or 0 + return results[0] or 0, 0 + + def path_expr(self, t): + return case((t.c.parent == "", t.c.name), else_=t.c.parent + "/" + t.c.name) + + def _find_query( + self, + dataset_rows: "DataTable", + parent_path: str, + fields: Optional[Sequence[str]] = None, + type: Optional[str] = None, + conds=None, + order_by: Optional[Union[str, list[str]]] = None, + include_subobjects: bool = True, + ) -> sa.Select: + if not conds: + conds = [] + + dr = dataset_rows + de = dr.dataset_dir_expansion( + dr.select().where(dr.c.is_latest == true()).subquery() + ).subquery() + q = self.expand_query(de, dr).subquery() + path = self.path_expr(q) + + if parent_path: + sub_glob = posixpath.join(parent_path, "*") + conds.append(path.op("GLOB")(sub_glob)) + else: + conds.append(path != "") + + columns = q.c + if fields: + columns = [getattr(columns, f) for f in fields] + + query = sa.select(*columns) + query = query.where(*conds) + if type is not None: + query = self.add_node_type_where(query, type, include_subobjects) + if order_by is not None: + if isinstance(order_by, str): + order_by = [order_by] + query = query.order_by(*order_by) + return query + + def get_subtree_files( + self, + dataset_rows: "DataTable", + node: Node, + sort: Union[list[str], str, None] = None, + include_subobjects: bool = True, + ) -> Iterator[NodeWithPath]: + """ + Returns all file nodes that are "below" some node. + Nodes can be sorted as well. + """ + dr = dataset_rows + query = self._find_query( + dr, + node.path, + type="f", + include_subobjects=include_subobjects, + ) + if sort is not None: + if not isinstance(sort, list): + sort = [sort] + query = query.order_by(*(sa.text(s) for s in sort)) # type: ignore [attr-defined] + + prefix_len = len(node.path) + + def make_node_with_path(node: Node) -> NodeWithPath: + return NodeWithPath(node, node.path[prefix_len:].lstrip("/").split("/")) + + return map(make_node_with_path, self.get_nodes(query)) + + def find( + self, + dataset_rows: "DataTable", + node: Node, + fields: Sequence[str], + type=None, + conds=None, + order_by=None, + ) -> Iterator[tuple[Any, ...]]: + """ + Finds nodes that match certain criteria and only looks for latest nodes + under the passed node. + """ + query = self._find_query( + dataset_rows, + node.path, + fields=fields, + type=type, + conds=conds, + order_by=order_by, + ) + return self.db.execute(query) + + def update_node(self, node_id: int, values: dict[str, Any]) -> None: + # TODO used only in formats which will be deleted + """Update entry of a specific node in the database.""" + + def create_udf_table( + self, + name: str, + columns: Sequence["sa.Column"] = (), + ) -> "sa.Table": + """ + Create a temporary table for storing custom signals generated by a UDF. + SQLite TEMPORARY tables cannot be directly used as they are process-specific, + and UDFs are run in other processes when run in parallel. + """ + tbl = sa.Table( + name, + sa.MetaData(), + sa.Column("id", Int, primary_key=True), + *columns, + ) + self.db.create_table(tbl, if_not_exists=True) + return tbl + + def is_temp_table_name(self, name: str) -> bool: + """Returns if the given table name refers to a temporary + or no longer needed table.""" + return name.startswith( + (self.TMP_TABLE_NAME_PREFIX, self.UDF_TABLE_NAME_PREFIX, "ds_shadow_") + ) or name.endswith("_shadow") + + def get_temp_table_names(self) -> list[str]: + return [ + t + for t in sa.inspect(self.db.engine).get_table_names() + if self.is_temp_table_name(t) + ] + + def cleanup_temp_tables(self, names: Iterable[str]) -> None: + """ + Drop tables created temporarily when processing datasets. + + This should be implemented even if temporary tables are used to + ensure that they are cleaned up as soon as they are no longer + needed. When running the same `DatasetQuery` multiple times we + may use the same temporary table names. + """ + for name in names: + self.db.drop_table(Table(name, self.db.metadata), if_exists=True) + + def subtract_query( + self, + source_query: sa.sql.selectable.Select, + target_query: sa.sql.selectable.Select, + ) -> sa.sql.selectable.Select: + sq = source_query.alias("source_query") + tq = target_query.alias("target_query") + + source_target_join = sa.join( + sq, + tq, + (sq.c.source == tq.c.source) + & (sq.c.parent == tq.c.parent) + & (sq.c.name == tq.c.name), + isouter=True, + ) + + return ( + select(*sq.c) + .select_from(source_target_join) + .where((tq.c.name == None) | (tq.c.name == "")) # noqa: E711 + ) + + def changed_query( + self, + source_query: sa.sql.selectable.Select, + target_query: sa.sql.selectable.Select, + ) -> sa.sql.selectable.Select: + sq = source_query.alias("source_query") + tq = target_query.alias("target_query") + + source_target_join = sa.join( + sq, + tq, + (sq.c.source == tq.c.source) + & (sq.c.parent == tq.c.parent) + & (sq.c.name == tq.c.name), + ) + + return ( + select(*sq.c) + .select_from(source_target_join) + .where( + (sq.c.last_modified > tq.c.last_modified) + & (sq.c.is_latest == true()) + & (tq.c.is_latest == true()) + ) + ) diff --git a/src/datachain/dataset.py b/src/datachain/dataset.py new file mode 100644 index 000000000..7ac9b8b1a --- /dev/null +++ b/src/datachain/dataset.py @@ -0,0 +1,487 @@ +import builtins +import json +from dataclasses import dataclass, fields +from datetime import datetime +from typing import ( + TYPE_CHECKING, + Any, + Optional, + TypeVar, + Union, +) +from urllib.parse import urlparse + +from dateutil.parser import isoparse + +from datachain.client import Client +from datachain.sql.types import NAME_TYPES_MAPPING, SQLType + +if TYPE_CHECKING: + from datachain.storage import StorageURI + +T = TypeVar("T", bound="DatasetRecord") +V = TypeVar("V", bound="DatasetVersion") +DD = TypeVar("DD", bound="DatasetDependency") + +DATASET_PREFIX = "ds://" +QUERY_DATASET_PREFIX = "ds_query_" + + +def parse_dataset_uri(uri: str) -> tuple[str, Optional[int]]: + """ + Parse dataser uri to extract name and version out of it (if version is defined) + Example: + Input: ds://zalando@v3 + Output: (zalando, 3) + """ + p = urlparse(uri) + if p.scheme != "ds": + raise Exception("Dataset uri should start with ds://") + s = p.netloc.split("@v") + name = s[0] + if len(s) == 1: + return name, None + if len(s) != 2: + raise Exception( + "Wrong dataset uri format, it should be: ds://@v" + ) + version = int(s[1]) + return name, version + + +def create_dataset_uri(name: str, version: Optional[int] = None) -> str: + """ + Creates a dataset uri based on dataset name and optionally version + Example: + Input: zalando, 3 + Output: ds//zalando@v3 + """ + uri = f"{DATASET_PREFIX}{name}" + if version: + uri += f"@v{version}" + + return uri + + +class DatasetDependencyType: + DATASET = "dataset" + STORAGE = "storage" + + +@dataclass +class DatasetDependency: + id: int + type: str + name: str # when the type is STORAGE, this is actually StorageURI + version: str # string until we'll have proper bucket listing versions + created_at: datetime + dependencies: list[Optional["DatasetDependency"]] + + @classmethod + def parse( + cls: builtins.type[DD], + id: int, + dataset_id: Optional[int], + dataset_version_id: Optional[int], + bucket_id: Optional[int], + bucket_version: Optional[str], + dataset_name: Optional[str], + dataset_created_at: Optional[datetime], + dataset_version: Optional[int], + dataset_version_created_at: Optional[datetime], + bucket_uri: Optional["StorageURI"], + ) -> Optional["DatasetDependency"]: + if dataset_id: + assert dataset_name is not None + return cls( + id, + DatasetDependencyType.DATASET, + dataset_name, + ( + str(dataset_version) # type: ignore[arg-type] + if dataset_version + else None + ), + dataset_version_created_at or dataset_created_at, # type: ignore[arg-type] + [], + ) + if bucket_uri: + return cls( + id, + DatasetDependencyType.STORAGE, + bucket_uri, + bucket_version, # type: ignore[arg-type] + isoparse(bucket_version), # type: ignore[arg-type] + [], + ) + # dependency has been removed + # TODO we should introduce flags for removed datasets, instead of + # removing them from tables so that we can still have references + return None + + @property + def is_dataset(self) -> bool: + return self.type == DatasetDependencyType.DATASET + + def __eq__(self, other): + if not isinstance(other, DatasetDependency): + return False + + return ( + self.type == other.type + and self.name == other.name + and self.version == other.version + ) + + def __hash__(self): + return hash(f"{self.type}_{self.name}_{self.version}") + + +@dataclass +class DatasetStats: + num_objects: Optional[int] # None if table is missing + size: Optional[int] # in bytes None if table is missing or empty + + +class DatasetStatus: + CREATED = 1 + PENDING = 2 + FAILED = 3 + COMPLETE = 4 + STALE = 6 + + +@dataclass +class DatasetVersion: + id: int + dataset_id: int + version: int + status: int + feature_schema: dict + created_at: datetime + finished_at: Optional[datetime] + error_message: str + error_stack: str + script_output: str + schema: dict[str, Union[SQLType, type[SQLType]]] + num_objects: Optional[int] + size: Optional[int] + preview: Optional[list[dict]] + sources: str = "" + query_script: str = "" + job_id: Optional[str] = None + is_job_result: bool = False + + @classmethod + def parse( # noqa: PLR0913 + cls: type[V], + id: int, + dataset_id: int, + version: int, + status: int, + feature_schema: Optional[str], + created_at: datetime, + finished_at: Optional[datetime], + error_message: str, + error_stack: str, + script_output: str, + num_objects: Optional[int], + size: Optional[int], + preview: Optional[str], + schema: dict[str, Union[SQLType, type[SQLType]]], + sources: str = "", + query_script: str = "", + job_id: Optional[str] = None, + is_job_result: bool = False, + ): + return cls( + id, + dataset_id, + version, + status, + json.loads(feature_schema) if feature_schema else {}, + created_at, + finished_at, + error_message, + error_stack, + script_output, + schema, + num_objects, + size, + json.loads(preview) if preview else None, + sources, + query_script, + job_id, + is_job_result, + ) + + def __eq__(self, other): + if not isinstance(other, DatasetVersion): + return False + return self.version == other.version and self.dataset_id == other.dataset_id + + def __lt__(self, other): + if not isinstance(other, DatasetVersion): + return False + return self.version < other.version + + def __hash__(self): + return hash(f"{self.dataset_id}_{self.version}") + + def is_final_status(self) -> bool: + return self.status in [ + DatasetStatus.FAILED, + DatasetStatus.COMPLETE, + DatasetStatus.STALE, + ] + + def update(self, **kwargs): + for key, value in kwargs.items(): + if hasattr(self, key): + setattr(self, key, value) + + @property + def serialized_schema(self) -> dict[str, Any]: + return { + c_name: c_type.to_dict() + if isinstance(c_type, SQLType) + else c_type().to_dict() + for c_name, c_type in self.schema.items() + } + + @classmethod + def from_dict(cls, d: dict[str, Any]) -> "DatasetVersion": + kwargs = {f.name: d[f.name] for f in fields(cls) if f.name in d} + return cls(**kwargs) + + +@dataclass +class DatasetRecord: + id: int + name: str + description: Optional[str] + labels: list[str] + shadow: bool + schema: dict[str, Union[SQLType, type[SQLType]]] + feature_schema: dict + versions: list[DatasetVersion] + status: int = DatasetStatus.CREATED + created_at: Optional[datetime] = None + finished_at: Optional[datetime] = None + error_message: str = "" + error_stack: str = "" + script_output: str = "" + sources: str = "" + query_script: str = "" + + @staticmethod + def parse_schema( + ct: dict[str, Any], + ) -> dict[str, Union[SQLType, type[SQLType]]]: + return { + c_name: NAME_TYPES_MAPPING[c_type["type"]].from_dict(c_type) # type: ignore [attr-defined] + for c_name, c_type in ct.items() + } + + @classmethod + def parse( # noqa: PLR0913 + cls: type[T], + id: int, + name: str, + description: Optional[str], + labels: str, + shadow: int, + status: int, + feature_schema: Optional[str], + created_at: datetime, + finished_at: Optional[datetime], + error_message: str, + error_stack: str, + script_output: str, + sources: str, + query_script: str, + schema: str, + version_id: int, + version_dataset_id: int, + version: int, + version_status: int, + version_feature_schema: Optional[str], + version_created_at: datetime, + version_finished_at: Optional[datetime], + version_error_message: str, + version_error_stack: str, + version_script_output: str, + version_num_objects: Optional[int], + version_size: Optional[int], + version_preview: Optional[str], + version_sources: Optional[str], + version_query_script: Optional[str], + version_schema: str, + version_job_id: Optional[str] = None, + version_is_job_result: bool = False, + ) -> "DatasetRecord": + labels_lst: list[str] = json.loads(labels) if labels else [] + schema_dct: dict[str, Any] = json.loads(schema) if schema else {} + version_schema_dct: dict[str, str] = ( + json.loads(version_schema) if version_schema else {} + ) + + dataset_version = DatasetVersion.parse( + version_id, + version_dataset_id, + version, + version_status, + version_feature_schema, + version_created_at, + version_finished_at, + version_error_message, + version_error_stack, + version_script_output, + version_num_objects, + version_size, + version_preview, + cls.parse_schema(version_schema_dct), # type: ignore[arg-type] + version_sources, # type: ignore[arg-type] + version_query_script, # type: ignore[arg-type] + version_job_id, + version_is_job_result, + ) + + return cls( + id, + name, + description, + labels_lst, + bool(shadow), + cls.parse_schema(schema_dct), # type: ignore[arg-type] + json.loads(feature_schema) if feature_schema else {}, + [dataset_version], + status, + created_at, + finished_at, + error_message, + error_stack, + script_output, + sources, + query_script, + ) + + @property + def serialized_schema(self) -> dict[str, Any]: + return { + c_name: c_type.to_dict() + if isinstance(c_type, SQLType) + else c_type().to_dict() + for c_name, c_type in self.schema.items() + } + + def get_schema(self, version: int) -> dict[str, Union[SQLType, type[SQLType]]]: + return self.get_version(version).schema if version else self.schema + + def update(self, **kwargs): + for key, value in kwargs.items(): + if hasattr(self, key): + setattr(self, key, value) + + def merge_versions(self, other: "DatasetRecord") -> "DatasetRecord": + """Merge versions from another dataset""" + if other.id != self.id: + raise RuntimeError("Cannot merge versions of datasets with different ids") + if not other.versions: + # nothing to merge + return self + if not self.versions: + self.versions = [] + + self.versions = list(set(self.versions + other.versions)) + self.versions.sort(key=lambda v: v.version) + return self + + def has_version(self, version: int) -> bool: + return version in self.versions_values + + def is_valid_next_version(self, version: int) -> bool: + """ + Checks if a number can be a valid next latest version for dataset. + The only rule is that it cannot be lower than current latest version + """ + return not (self.latest_version and self.latest_version >= version) + + def get_version(self, version: int) -> DatasetVersion: + if not self.has_version(version): + raise ValueError(f"Dataset {self.name} does not have version {version}") + return next( + v + for v in self.versions # type: ignore [union-attr] + if v.version == version + ) + + def remove_version(self, version: int) -> None: + if not self.versions or not self.has_version(version): + return + + self.versions = [v for v in self.versions if v.version != version] + + def identifier(self, version: int) -> str: + """ + Get identifier in the form my-dataset@v3 + """ + if not self.has_version(version): + raise ValueError(f"Dataset {self.name} doesn't have a version {version}") + return f"{self.name}@v{version}" + + def uri(self, version: int) -> str: + """ + Dataset uri example: ds://dogs@v3 + """ + identifier = self.identifier(version) + return f"{DATASET_PREFIX}{identifier}" + + @property + def is_bucket_listing(self) -> bool: + """ + For bucket listing we implicitly create underlying dataset to hold data. This + method is checking if this is one of those datasets. + """ + return Client.is_data_source_uri(self.name) + + @property + def versions_values(self) -> list[int]: + """ + Extracts actual versions from list of DatasetVersion objects + in self.versions attribute + """ + if not self.versions: + return [] + + return sorted(v.version for v in self.versions) + + @property + def next_version(self) -> int: + """Returns what should be next autoincrement version of dataset""" + if not self.versions: + return 1 + return max(self.versions_values) + 1 + + @property + def latest_version(self) -> int: + """Returns latest version of a dataset""" + return max(self.versions_values) + + @property + def prev_version(self) -> Optional[int]: + """Returns previous version of a dataset""" + if len(self.versions) == 1: + return None + + return sorted(self.versions_values)[-2] + + @classmethod + def from_dict(cls, d: dict[str, Any]) -> "DatasetRecord": + versions = [DatasetVersion.from_dict(v) for v in d.pop("versions", [])] + kwargs = {f.name: d[f.name] for f in fields(cls) if f.name in d} + return cls(**kwargs, versions=versions) + + +class RowDict(dict): + pass diff --git a/src/datachain/error.py b/src/datachain/error.py new file mode 100644 index 000000000..ffc5d653d --- /dev/null +++ b/src/datachain/error.py @@ -0,0 +1,61 @@ +class DataChainError(RuntimeError): + pass + + +class NotFoundError(Exception): + pass + + +class DatasetNotFoundError(NotFoundError): + pass + + +class DatasetInvalidVersionError(Exception): + pass + + +class StorageNotFoundError(NotFoundError): + pass + + +class PendingIndexingError(Exception): + """An indexing operation is already in progress.""" + + +class QueryScriptCompileError(Exception): + pass + + +class QueryScriptRunError(Exception): + """Error raised by `subprocess.run`. + + Attributes: + message Explanation of the error + return_code Code returned by the subprocess + output STDOUT + STDERR output of the subprocess + """ + + def __init__(self, message: str, return_code: int = 0, output: str = ""): + self.message = message + self.return_code = return_code + self.output = output + super().__init__(self.message) + + +class QueryScriptDatasetNotFound(QueryScriptRunError): # noqa: N818 + pass + + +class QueryScriptCancelError(QueryScriptRunError): + pass + + +class ClientError(RuntimeError): + def __init__(self, message, error_code=None): + super().__init__(message) + # error code from the cloud itself + self.error_code = error_code + + +class TableMissingError(DataChainError): + pass diff --git a/src/datachain/lib/__init__.py b/src/datachain/lib/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/datachain/lib/arrow.py b/src/datachain/lib/arrow.py new file mode 100644 index 000000000..16f2e56c5 --- /dev/null +++ b/src/datachain/lib/arrow.py @@ -0,0 +1,79 @@ +import re +from typing import TYPE_CHECKING, Optional + +from pyarrow.dataset import dataset + +from datachain.lib.file import File, IndexedFile +from datachain.lib.udf import Generator + +if TYPE_CHECKING: + import pyarrow as pa + + +class ArrowGenerator(Generator): + def __init__(self, schema: Optional["pa.Schema"] = None, **kwargs): + """ + Generator for getting rows from tabular files. + + Parameters: + + schema : Optional pyarrow schema for validation. + kwargs: Parameters to pass to pyarrow.dataset.dataset. + """ + super().__init__() + self.schema = schema + self.kwargs = kwargs + + def process(self, file: File): + path = file.get_path() + ds = dataset(path, filesystem=file.get_fs(), schema=self.schema, **self.kwargs) + index = 0 + for record_batch in ds.to_batches(): + for record in record_batch.to_pylist(): + source = IndexedFile(file=file, index=index) + yield [source, *record.values()] + index += 1 + + +def schema_to_output(schema: "pa.Schema"): + """Generate UDF output schema from pyarrow schema.""" + default_column = 0 + output = {"source": IndexedFile} + for field in schema: + column = field.name.lower() + column = re.sub("[^0-9a-z_]+", "", column) + if not column: + column = f"c{default_column}" + default_column += 1 + output[column] = _arrow_type_mapper(field.type) # type: ignore[assignment] + + return output + + +def _arrow_type_mapper(col_type: "pa.DataType") -> type: # noqa: PLR0911 + """Convert pyarrow types to basic types.""" + from datetime import datetime + + import pyarrow as pa + + if pa.types.is_timestamp(col_type): + return datetime + if pa.types.is_binary(col_type): + return bytes + if pa.types.is_floating(col_type): + return float + if pa.types.is_integer(col_type): + return int + if pa.types.is_boolean(col_type): + return bool + if pa.types.is_date(col_type): + return datetime + if pa.types.is_string(col_type) or pa.types.is_large_string(col_type): + return str + if pa.types.is_list(col_type): + return list[_arrow_type_mapper(col_type.value_type)] # type: ignore[misc] + if pa.types.is_struct(col_type) or pa.types.is_map(col_type): + return dict + if isinstance(col_type, pa.lib.DictionaryType): + return _arrow_type_mapper(col_type.value_type) # type: ignore[return-value] + raise TypeError(f"{col_type!r} datatypes not supported") diff --git a/src/datachain/lib/cached_stream.py b/src/datachain/lib/cached_stream.py new file mode 100644 index 000000000..c10519ecb --- /dev/null +++ b/src/datachain/lib/cached_stream.py @@ -0,0 +1,38 @@ +from abc import ABC +from contextlib import AbstractContextManager + +from datachain.cache import UniqueId + + +class AbstractCachedStream(AbstractContextManager, ABC): + def __init__(self, catalog, uid: UniqueId): + self.catalog = catalog + self.uid = uid + self.mode = "rb" + + def set_mode(self, mode): + self.mode = mode + + +class PreCachedStream(AbstractCachedStream): + def __init__(self, catalog, uid: UniqueId): + super().__init__(catalog, uid) + self.client = self.catalog.get_client(self.uid.storage) + self.cached_file = None + + def get_path_in_cache(self): + return self.catalog.cache.path_from_checksum(self.uid.get_hash()) + + def __enter__(self): + self.client.download(self.uid) + self.cached_file = open(self.get_path_in_cache(), self.mode) + return self.cached_file + + def __exit__(self, *args): + self.cached_file.close() + + +class PreDownloadStream(PreCachedStream): + def __exit__(self, *args): + super().__exit__(*args) + self.catalog.cache.remove(self.uid) diff --git a/src/datachain/lib/claude.py b/src/datachain/lib/claude.py new file mode 100644 index 000000000..b5759a675 --- /dev/null +++ b/src/datachain/lib/claude.py @@ -0,0 +1,69 @@ +import os +from typing import Callable, Literal, Optional + +import anthropic + +from datachain.lib.feature import Feature +from datachain.lib.file import File + +default_model_name = "claude-3-haiku-20240307" +DEFAULT_OUTPUT_TOKENS = 1024 + +# This classes can be auto-generated: +# >> from anthropic.types.message import Message +# >> ClaudeMessage = pydantic_to_feature(Message) +# However, auto-generated pydentic classes do not work in multithreading mode. + + +class UsageFr(Feature): + input_tokens: int = 0 + output_tokens: int = 0 + + +class TextBlockFr(Feature): + text: str = "" + type: str = "text" + + +class ClaudeMessage(Feature): + id: str = "" + content: list[TextBlockFr] + model: str = "" + role: str = "" + stop_reason: Optional[Literal["end_turn", "max_tokens", "stop_sequence"]] = None + stop_sequence: Optional[str] = None + type: Literal["message"] = "message" + usage: UsageFr = UsageFr() + + +def claude_processor( + prompt: str, + messages: Optional[list] = None, + model: str = "claude-3-haiku-20240307", + api_key: Optional[str] = "", + max_retries: int = 5, + temperature: float = 0.9, + max_tokens: int = 1024, + **kwargs, +) -> Callable: + if not messages: + messages = [] + api_key = api_key or os.environ.get("ANTHROPIC_API_KEY") + + def claude_func(file) -> ClaudeMessage: + try: + data = file.get_value() if isinstance(file, File) else file + client = anthropic.Anthropic(api_key=api_key, max_retries=max_retries) + response = client.messages.create( + model=model, + system=prompt, + messages=[{"role": "user", "content": data}, *messages], + temperature=temperature, + max_tokens=max_tokens, + **kwargs, + ) + return ClaudeMessage(**response.model_dump()) + except Exception: # noqa: BLE001 + return ClaudeMessage(content=[]) + + return claude_func diff --git a/src/datachain/lib/clip.py b/src/datachain/lib/clip.py new file mode 100644 index 000000000..600f86092 --- /dev/null +++ b/src/datachain/lib/clip.py @@ -0,0 +1,151 @@ +import inspect +from typing import Any, Callable, Literal, Union + +from datachain.lib.image import convert_images +from datachain.lib.text import convert_text + +try: + import torch + from PIL import Image + from transformers.modeling_utils import PreTrainedModel +except ImportError as exc: + raise ImportError( + "Missing dependencies for computer vision:\n" + "To install run:\n\n" + " pip install 'datachain[cv]'\n" + ) from exc + + +def _get_encoder(model: Any, type: Literal["image", "text"]) -> Callable: + # Check for transformers CLIPModel + method_name = f"get_{type}_features" + if isinstance(model, PreTrainedModel) and ( + hasattr(model, method_name) and inspect.ismethod(getattr(model, method_name)) + ): + method = getattr(model, method_name) + return lambda x: method(torch.tensor(x)) + + # Check for model from clip or open_clip library + method_name = f"encode_{type}" + if hasattr(model, method_name) and inspect.ismethod(getattr(model, method_name)): + return getattr(model, method_name) + + raise ValueError( + f"Error encoding {type}: " + "'model' must be a CLIP model from clip, open_clip, or transformers library." + ) + + +def similarity_scores( + images: Union[None, Image.Image, list[Image.Image]], + text: Union[None, str, list[str]], + model: Any, + preprocess: Callable, + tokenizer: Callable, + prob: bool = False, + image_to_text: bool = True, +) -> list[list[float]]: + """ + Calculate CLIP similarity scores between one or more images and/or text. + + Args: + images: Images to use as inputs. + text: Text to use as inputs. + model: Model from clip or open_clip packages. + preprocess: Image preprocessor to apply. + tokenizer: Text tokenizer. + prob: Compute softmax probabilities. + image_to_text: Whether to compute for image-to-text or text-to-image. Ignored if + only one of images or text provided. + + + Examples + -------- + + using https://github.com/openai/CLIP + >>> import clip + >>> model, preprocess = clip.load("ViT-B/32") + >>> similarity_scores(img, "cat", model, preprocess, clip.tokenize) + [[21.813]] + + using https://github.com/mlfoundations/open_clip + >>> import open_clip + >>> model, _, preprocess = open_clip.create_model_and_transforms( + ... "ViT-B-32", pretrained="laion2b_s34b_b79k" + ... ) + >>> tokenizer = open_clip.get_tokenizer("ViT-B-32") + >>> similarity_scores(img, "cat", model, preprocess, tokenizer) + [[21.813]] + + using https://huggingface.co/docs/transformers/en/model_doc/clip + >>> from transformers import CLIPProcessor, CLIPModel + >>> model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32") + >>> processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32") + >>> scores = similarity_scores( + ... img, "cat", model, processor.image_processor, processor.tokenizer + ... ) + [[21.813]] + + image -> list of text + >>> similarity_scores(img, ["cat", "dog"], model, preprocess, tokenizer) + [[21.813, 35.313]] + + list of images -> text + >>> similarity_scores([img1, img2], "cat", model, preprocess, tokenizer) + [[21.813], [83.123]] + + list of images -> list of text + >>> similarity_scores([img1, img2], ["cat", "dog"], model, preprocess, tokenizer) + [[21.813, 35.313], [83.123, 34.843]] + + list of images -> list of images + >>> similarity_scores([img1, img2], None, model, preprocess, tokenizer) + [[94.189, 37.092]] + + list of text -> list of text + >>> similarity_scores(None, ["cat", "dog"], model, preprocess, tokenizer) + [[67.334, 23.588]] + + text -> list of images + >>> similarity_scores([img1, img2], "cat", ..., image_to_text=False) + [[19.708, 19.842]] + + show scores as softmax probabilities + >>> similarity_scores(img, ["cat", "dog"], ..., prob=True) + [[0.423, 0.577]] + """ + + with torch.no_grad(): + if images is not None: + encoder = _get_encoder(model, "image") + image_features = convert_images( + images, transform=preprocess, encoder=encoder + ) + image_features /= image_features.norm(dim=-1, keepdim=True) # type: ignore[union-attr] + + if text is not None: + encoder = _get_encoder(model, "text") + text_features = convert_text(text, tokenizer, encoder=encoder) + text_features /= text_features.norm(dim=-1, keepdim=True) # type: ignore[union-attr] + + if images is not None and text is not None: + if image_to_text: + logits = 100.0 * image_features @ text_features.T # type: ignore[operator,union-attr] + else: + logits = 100.0 * text_features @ image_features.T # type: ignore[operator,union-attr] + elif images is not None: + logits = 100.0 * image_features @ image_features.T # type: ignore[operator,union-attr] + elif text is not None: + logits = 100.0 * text_features @ text_features.T # type: ignore[operator,union-attr] + else: + raise ValueError( + "Error calculating CLIP similarity - " + "provide at least one of images or text" + ) + + if prob: + scores = logits.softmax(dim=1) + else: + scores = logits + + return scores.tolist() diff --git a/src/datachain/lib/dc.py b/src/datachain/lib/dc.py new file mode 100644 index 000000000..5316419c2 --- /dev/null +++ b/src/datachain/lib/dc.py @@ -0,0 +1,925 @@ +import re +from collections.abc import Iterator, Sequence +from typing import ( + TYPE_CHECKING, + Any, + Callable, + ClassVar, + Literal, + Optional, + Union, +) + +import sqlalchemy + +from datachain.lib.feature import Feature, FeatureType +from datachain.lib.feature_utils import features_to_tuples +from datachain.lib.file import File, IndexedFile, get_file +from datachain.lib.meta_formats import read_meta, read_schema +from datachain.lib.settings import Settings +from datachain.lib.signal_schema import SignalSchema +from datachain.lib.udf import ( + Aggregator, + BatchMapper, + Generator, + Mapper, + UDFBase, +) +from datachain.lib.udf_signature import UdfSignature +from datachain.lib.utils import DataChainParamsError +from datachain.query import Session +from datachain.query.dataset import ( + DatasetQuery, + PartitionByType, + detach, +) +from datachain.query.schema import Column, DatasetRow + +if TYPE_CHECKING: + import pandas as pd + from typing_extensions import Self + + from datachain.catalog import Catalog + +C = Column + + +class DatasetPrepareError(DataChainParamsError): + def __init__(self, name, msg, output=None): + name = f" '{name}'" if name else "" + output = f" output '{output}'" if output else "" + super().__init__(f"Dataset{name}{output} processing prepare error: {msg}") + + +class DatasetFromFeatureError(DataChainParamsError): + def __init__(self, name, msg): + name = f" '{name}'" if name else "" + super().__init__(f"Dataset {name} from feature error: {msg}") + + +class DatasetMergeError(DataChainParamsError): + def __init__(self, on: Sequence[str], right_on: Optional[Sequence[str]], msg: str): + on_str = ", ".join(on) if isinstance(on, Sequence) else "" + right_on_str = ( + ", right_on='" + ", ".join(right_on) + "'" + if right_on and isinstance(right_on, Sequence) + else "" + ) + super().__init__(f"Merge error on='{on_str}'{right_on_str}: {msg}") + + +class DataChain(DatasetQuery): + """AI 🔗 DataChain - a data structure for batch data processing and evaluation. + + It represents a sequence of data manipulation steps such as reading data from + storages, running AI or LLM models or calling external services API to validate or + enrich data. + + Data in DataChain is presented as Python classes with arbitrary set of fields, + including nested classes. The data classes have to inherit from `Feature` class. + The supported set of field types include: majority of the type supported by the + underlyind library `Pydantic`. + + See Also: + `DataChain.from_storage("s3://my-bucket/my-dir/")` - reading unstructured + data files from storages such as S3, gs or Azure ADLS. + + `DataChain.save("name")` - saving to a dataset. + + `DataChain.from_dataset("name")` - reading from a dataset. + + `DataChain.from_features(fib=[1, 2, 3, 5, 8])` - generating from a values. + + + Example: + ```py + from datachain import DataChain, Feature + from datachain.lib.claude import claude_processor + + class Rating(Feature): + status: str = "" + explanation: str = "" + + PROMPT = "A 'user' is a human trying to find the best mobile plan.... " + MODEL = "claude-3-opus-20240229" + + chain = ( + DataChain.from_storage("s3://my-bucket/my") + .filter(C.name.glob("*.txt")) + .limit(5) + .map(claude=claude_processor(prompt=PROMPT, model=MODEL)) + .map( + rating=lambda claude: Rating( + **(json.loads(claude.content[0].text) if claude.content else {}) + ), + output=Rating, + ) + chain.save("ratings") + print(chain) + ``` + """ + + DEFAULT_FILE_RECORD: ClassVar[dict] = { + "id": 0, + "source": "", + "name": "", + "vtype": "", + "size": 0, + "random": 0, + } + + def __init__(self, *args, **kwargs): + """This method needs to be redefined as a part of Dataset and DacaChin + decoupling.""" + super().__init__( + *args, + **kwargs, + indexing_column_types=File._datachain_column_types, + ) + self._settings = Settings() + self._setup = {} + + if self.feature_schema: + self.signals_schema = SignalSchema.deserialize(self.feature_schema) + else: + self.signals_schema = SignalSchema.from_column_types(self.column_types) + + @property + def schema(self): + return self.signals_schema.values if self.signals_schema else None + + def print_schema(self): + self.signals_schema.print_tree() + + def create_model(self, name: str) -> type[Feature]: + return self.signals_schema.create_model(name) + + def settings( + self, cache=None, batch=None, parallel=None, workers=None, min_task_size=None + ) -> "Self": + """Change settings for chain. + + This function changes specified settings without changing not specified ones. + It returns chain, so, it can be chained later with next operation. + + Parameters: + cache : data caching (default=False) + batch : size of the batch (default=1000) + parallel : number of thread for processors. True is a special value to + enable all available CPUs (default=1) + workers : number of distributed workers. Only for Studio mode. (default=1) + min_task_size : minimum number of tasks (default=1) + + Example: + ```py + chain = ( + chain + .settings(cache=True, parallel=8) + .map(laion=process_webdataset(spec=WDSLaion), params="file") + ) + ``` + """ + self._settings.add(Settings(cache, batch, parallel, workers, min_task_size)) + return self + + def reset_settings(self, settings: Optional[Settings] = None) -> "Self": + """Reset all settings to default values.""" + self._settings = settings if settings else Settings() + return self + + def reset_schema(self, signals_schema: SignalSchema) -> "Self": + self.signals_schema = signals_schema + return self + + def add_schema(self, signals_schema: SignalSchema) -> "Self": + union = self.signals_schema.values | signals_schema.values + self.signals_schema = SignalSchema(union) + return self + + def get_file_signals(self) -> list[str]: + return self.signals_schema.get_file_signals() + + @classmethod + def from_storage( + cls, + path, + *, + type: Literal["binary", "text", "image"] = "binary", + catalog: Optional["Catalog"] = None, + recursive: Optional[bool] = True, + anon: bool = False, + ) -> "Self": + """Get data from a storage as a list of file with all file attributes. It + returns the chain itself as usual. + + Parameters: + path : storage URI with directory. URI must start with storage prefix such + as `s3://`, `gs://`, `az://` or "file:///" + type : read file as "binary", "text", or "image" data. Default is "binary". + recursive : search recursively for the given path. + anon : use anonymous mode to access the storage. + + Example: + ```py + chain = DataChain.from_storage("s3://my-bucket/my-dir") + ``` + """ + func = get_file(type) + return cls(path, catalog=catalog, recursive=recursive, anon=anon).map(file=func) + + @classmethod + def from_dataset(cls, name: str, version: Optional[int] = None) -> "DataChain": + """Get data from dataset. It returns the chain itself. + + Parameters: + name : dataset name + version : dataset version + + Examples: + >>> chain = DataChain.from_dataset("my_cats") + """ + return DataChain(name=name, version=version) + + @classmethod + def from_csv( + cls, + path, + type: Literal["binary", "text", "image"] = "text", + anon: bool = False, + spec: Optional[FeatureType] = None, + schema_from: Optional[str] = "auto", + object_name: Optional[str] = "csv", + model_name: Optional[str] = None, + show_schema: Optional[bool] = False, + ) -> "DataChain": + """Get data from CSV. It returns the chain itself. + + Parameters: + path : storage URI with directory. URI must start with storage prefix such + as `s3://`, `gs://`, `az://` or "file:///" + type : read file as "binary", "text", or "image" data. Default is "text". + anon : use anonymous mode to access the storage. + spec : Data Model for CSV file + object_name : generated object column name + model_name : generated model name + schema_from : path to sample to infer spec from + show_schema : print auto-generated schema + + Examples: + infer model from the first two lines (header + data) + >>> chain = DataChain.from_csv("gs://csv") + + use a particular data model + >>> chain = DataChain.from_csv("gs://csv"i, spec=MyModel) + """ + if schema_from == "auto": + schema_from = path + + chain = DataChain.from_storage(path=path, type=type, anon=anon) + signal_dict = { + object_name: read_meta( + schema_from=schema_from, + meta_type="csv", + spec=spec, + model_name=model_name, + show_schema=show_schema, + ) + } + return chain.gen(**signal_dict) # type: ignore[misc, arg-type] + + @classmethod + def from_json( + cls, + path, + type: Literal["binary", "text", "image"] = "text", + anon: bool = False, + spec: Optional[FeatureType] = None, + schema_from: Optional[str] = "auto", + jmespath: Optional[str] = None, + object_name: Optional[str] = None, + model_name: Optional[str] = None, + show_schema: Optional[bool] = False, + meta_type: Optional[str] = "json", + ) -> "DataChain": + """Get data from JSON. It returns the chain itself. + + Parameters: + path : storage URI with directory. URI must start with storage prefix such + as `s3://`, `gs://`, `az://` or "file:///" + type : read file as "binary", "text", or "image" data. Default is "binary". + anon : use anonymous mode to access the storage. + spec : optional Data Model + schema_from : path to sample to infer spec from + object_name : generated object column name + model_name : generated model name + show_schema : print auto-generated schema + jmespath : JMESPATH expression to reduce JSON + + Examples: + infer JSON schema from data, reduce using JMESPATH, print schema + >>> chain = DataChain.from_json("gs://json", jmespath="key1.key2") + + infer JSON schema from a particular path, print data model + >>> chain = DataChain.from_json("gs://json_ds", schema_from="gs://json/my.json") + """ + if schema_from == "auto": + schema_from = path + + def jmespath_to_name(s: str): + name_end = re.search(r"\W", s).start() if re.search(r"\W", s) else len(s) # type: ignore[union-attr] + return s[:name_end] + + if (not object_name) and jmespath: + object_name = jmespath_to_name(jmespath) + if not object_name: + object_name = "json" + chain = DataChain.from_storage(path=path, type=type, anon=anon) + signal_dict = { + object_name: read_meta( + schema_from=schema_from, + meta_type=meta_type, + spec=spec, + model_name=model_name, + show_schema=show_schema, + jmespath=jmespath, + ) + } + return chain.gen(**signal_dict) # type: ignore[arg-type] + + def show_json_schema( # type: ignore[override] + self, jmespath: Optional[str] = None, model_name: Optional[str] = None + ) -> "DataChain": + """Print JSON data model and save it. It returns the chain itself. + + Parameters: + jmespath : JMESPATH expression to reduce JSON + model_name : generated model name + + Examples: + print JSON schema and save to column "meta_from": + >>> uri = "gs://datachain-demo/coco2017/annotations_captions/" + >>> chain = DataChain.from_storage(uri) + >>> chain = chain.show_json_schema() + >>> chain.save() + """ + return self.map( + meta_schema=lambda file: read_schema( + file, data_type="json", expr=jmespath, model_name=model_name + ), + output=str, + ) + + def show_jsonl_schema( # type: ignore[override] + self, jmespath: Optional[str] = None, model_name: Optional[str] = None + ) -> "DataChain": + """Print JSON data model and save it. It returns the chain itself. + + Parameters: + jmespath : JMESPATH expression to reduce JSON + model_name : generated model name + """ + return self.map( + meta_schema=lambda file: read_schema( + file, data_type="jsonl", expr=jmespath, model_name=model_name + ), + output=str, + ) + + def save( # type: ignore[override] + self, name: Optional[str] = None, version: Optional[int] = None + ) -> "DataChain": + """Save to a Dataset. It returns the chain itself. + + Parameters: + name : dataset name. Empty name saves to a temporary dataset that will be + removed after process ends. Temp dataset are useful for optimization. + version : version of a dataset. Default - the last version that exist. + """ + schema = self.signals_schema.serialize() + return super().save(name=name, version=version, feature_schema=schema) + + def apply(self, func, *args, **kwargs): + return func(self, *args, **kwargs) + + def map( + self, + func: Optional[Callable] = None, + params: Union[None, str, Sequence[str]] = None, + output: Union[None, FeatureType, Sequence[str], dict[str, FeatureType]] = None, + **signal_map, + ) -> "Self": + """Apply a function to each row to create new signals. The function should + return a new object for each row. It returns a chain itself with new signals. + + Input-output relationship: 1:1 + + Parameters: + func : Function applied to each row. + params : List of column names used as input for the function. Default + is taken from function signature. + output : Dictionary defining new signals and their corresponding types. + Default type is taken from function signature. Default can be also + taken from kwargs - **signal_map (see below). + If signal name is defined using signal_map (see below) only a single + type value can be used. + **signal_map : kwargs can be used to define `func` together with it's return + signal name in format of `map(my_sign=my_func)`. This helps define + signal names and function in a nicer way. + + Examples: + Using signal_map and single type in output: + >>> chain = chain.map(value=lambda name: name[:-4] + ".json", output=str) + >>> chain.save("new_dataset") + + Using func and output as a map: + >>> chain = chain.map(lambda name: name[:-4] + ".json", output={"res": str}) + >>> chain.save("new_dataset") + """ + + udf_obj = self._udf_to_obj(Mapper, func, params, output, signal_map) + + chain = self.add_signals( + udf_obj.to_udf_wrapper(self._settings.batch), + **self._settings.to_dict(), + ) + + return chain.add_schema(udf_obj.output).reset_settings(self._settings) + + def gen( + self, + func: Optional[Callable] = None, + params: Union[None, str, Sequence[str]] = None, + output: Union[None, FeatureType, Sequence[str], dict[str, FeatureType]] = None, + **signal_map, + ) -> "Self": + """Apply a function to each row to create new rows (with potentially new + signals). The function needs to return a new objects for each of the new rows. + It returns a chain itself with new signals. + + Input-output relationship: 1:N + + This method is similar to `map()`, uses the same list of parameters, but with + one key differences: It produces a sequence of rows for each input row (like + extracting multiple file records from a single tar file or bounding boxes from a + single image file). + """ + + udf_obj = self._udf_to_obj(Generator, func, params, output, signal_map) + chain = DatasetQuery.generate( + self, + udf_obj.to_udf_wrapper(self._settings.batch), + **self._settings.to_dict(), + ) + + return chain.reset_schema(udf_obj.output).reset_settings(self._settings) + + def agg( + self, + func: Optional[Callable] = None, + partition_by: Optional[PartitionByType] = None, + params: Union[None, str, Sequence[str]] = None, + output: Union[None, FeatureType, Sequence[str], dict[str, FeatureType]] = None, + **signal_map, + ) -> "Self": + """Aggregate rows using `partition_by` statement and apply a function to the + groups of aggregated rows. The function needs to return new objects for each + group of the new rows. It returns a chain itself with new signals. + + Input-output relationship: N:M + + This method bears similarity to `gen()` and map(), employing a comparable set of + parameters, yet differs in two crucial aspects: + 1. The `partition_by` parameter: This specifies the column name or a list of + column names that determine the grouping criteria for aggregation. + 2. Group-based UDF function input: Instead of individual rows, the function + receives a list all rows within each group defined by `partition_by`. + """ + udf_obj = self._udf_to_obj(Aggregator, func, params, output, signal_map) + chain = DatasetQuery.generate( + self, + udf_obj.to_udf_wrapper(self._settings.batch), + partition_by=partition_by, + **self._settings.to_dict(), + ) + + return chain.reset_schema(udf_obj.output).reset_settings(self._settings) + + def batch_map( + self, + func: Optional[Callable] = None, + params: Union[None, str, Sequence[str]] = None, + output: Union[None, FeatureType, Sequence[str], dict[str, FeatureType]] = None, + **signal_map, + ) -> "Self": + """This is a batch version of map(). + + It accepts the same parameters plus an + additional parameter: + """ + udf_obj = self._udf_to_obj(BatchMapper, func, params, output, signal_map) + chain = DatasetQuery.generate( + self, + udf_obj.to_udf_wrapper(self._settings.batch), + **self._settings.to_dict(), + ) + + return chain.add_schema(udf_obj.output).reset_settings(self._settings) + + def _udf_to_obj( + self, + target_class: type[UDFBase], + func: Optional[Callable], + params: Union[None, str, Sequence[str]], + output: Union[None, FeatureType, Sequence[str], dict[str, FeatureType]], + signal_map, + ) -> UDFBase: + is_generator = target_class.is_output_batched + name = self.name or "" + + sign = UdfSignature.parse(name, signal_map, func, params, output, is_generator) + params_schema = self.signals_schema.slice(sign.params, self._setup) + + return UDFBase._create(target_class, sign, params_schema) + + def _extend_features(self, method_name, *args, **kwargs): + super_func = getattr(super(), method_name) + + new_schema = self.signals_schema.resolve(*args) + columns = [C(col) for col in new_schema.db_signals()] + res = super_func(*columns, **kwargs) + if isinstance(res, DataChain): + res.signals_schema = new_schema + + return res + + @detach + def select(self, *args: str) -> "Self": + """Select only a specified set of signals.""" + new_schema = self.signals_schema.resolve(*args) + columns = new_schema.db_signals() + chain = super().select(*columns) + chain.signals_schema = new_schema + return chain + + @detach + def select_except(self, *args: str) -> "Self": + """Select all the signals expect the specified signals.""" + new_schema = self.signals_schema.select_except_signals(*args) + columns = new_schema.db_signals() + chain = super().select(*columns) + chain.signals_schema = new_schema + return chain + + def iterate(self, *cols: str) -> Iterator[list[FeatureType]]: + """Iterate over rows. + + If columns are specified - limit them to specified + columns. + """ + chain = self.select(*cols) if cols else self + + db_signals = chain.signals_schema.db_signals() + with super().select(*db_signals).as_iterable() as rows_iter: + for row in rows_iter: + yield chain.signals_schema.row_to_features(row, chain.session.catalog) + + def iterate_one(self, col: str) -> Iterator[FeatureType]: + for item in self.iterate(col): + yield item[0] + + def collect(self, *cols: str) -> list[list[FeatureType]]: + return list(self.iterate(*cols)) + + def collect_one(self, col: str) -> list[FeatureType]: + return list(self.iterate_one(col)) + + def to_pytorch(self, **kwargs): + """Convert to pytorch dataset format.""" + + try: + import torch # noqa: F401 + except ImportError as exc: + raise ImportError( + "Missing required dependency 'torch' for Dataset.to_pytorch()" + ) from exc + from datachain.lib.pytorch import PytorchDataset + + if self.attached: + chain = self + else: + chain = self.save() + assert chain.name is not None # for mypy + return PytorchDataset(chain.name, chain.version, catalog=self.catalog, **kwargs) + + def remove_file_signals(self) -> "Self": + schema = self.signals_schema.clone_without_file_signals() + return self.select(*schema.values.keys()) + + @detach + def merge( + self, + right_ds: "DataChain", + on: Union[str, Sequence[str]], + right_on: Union[str, Sequence[str], None] = None, + inner=False, + rname="right_", + ) -> "Self": + """Merge two chains based on the specified criteria. + + Parameters: + right_ds : Chain to join with. + on : Predicate or list of Predicates to join on. If both chains have the + same predicates then this predicate is enough for the join. Otherwise, + `right_on` parameter has to specify the predicates for the other chain. + right_on: Optional predicate or list of Predicates + for the `right_ds` to join. + inner (bool): Whether to run inner join or outer join. + rname (str): name prefix for conflicting signal names. + + Examples: + >>> meta = meta_emd.merge(meta_pq, on=(C.name, C.emd__index), + right_on=(C.name, C.pq__index)) + """ + if on is None: + raise DatasetMergeError(["None"], None, "'on' must be specified") + + if isinstance(on, str): + on = [on] + elif not isinstance(on, Sequence): + raise DatasetMergeError( + on, + right_on, + f"'on' must be 'str' or 'Sequence' object but got type '{type(on)}'", + ) + + on_columns = self.signals_schema.resolve(*on).db_signals() + + if right_on is not None: + if isinstance(right_on, str): + right_on = [right_on] + elif not isinstance(right_on, Sequence): + raise DatasetMergeError( + on, + right_on, + "'right_on' must be 'str' or 'Sequence' object" + f" but got type '{right_on}'", + ) + + if len(right_on) != len(on): + raise DatasetMergeError( + on, right_on, "'on' and 'right_on' must have the same length'" + ) + + right_on_columns = right_ds.signals_schema.resolve(*right_on).db_signals() + + if len(right_on_columns) != len(on_columns): + on_str = ", ".join(right_on_columns) + right_on_str = ", ".join(right_on_columns) + raise DatasetMergeError( + on, + right_on, + "'on' and 'right_on' must have the same number of columns in db'." + f" on -> {on_str}, right_on -> {right_on_str}", + ) + else: + right_on = on + right_on_columns = on_columns + + if self == right_ds: + right_ds = right_ds.clone(new_table=True) + + ops = [ + self.c(left) == right_ds.c(right) + for left, right in zip(on_columns, right_on_columns) + ] + + ds = self.join(right_ds, sqlalchemy.and_(*ops), inner, rname + "{name}") + + ds.feature_schema = None + ds.signals_schema = self.signals_schema.merge(right_ds.signals_schema, rname) + + return ds + + @classmethod + def from_features( + cls, + ds_name: str = "", + session: Optional[Session] = None, + output: Union[None, FeatureType, Sequence[str], dict[str, FeatureType]] = None, + **fr_map, + ) -> "DataChain": + """Generate chain from list of features.""" + tuple_type, output, tuples = features_to_tuples(ds_name, output, **fr_map) + + def _func_fr() -> Iterator[tuple_type]: # type: ignore[valid-type] + yield from tuples + + chain = DataChain.create_empty(DataChain.DEFAULT_FILE_RECORD, session=session) + return chain.gen(_func_fr, output=output) + + @classmethod + def from_pandas( # type: ignore[override] + cls, df: "pd.DataFrame", name: str = "", session: Optional[Session] = None + ) -> "DataChain": + """Generate chain from pandas data-frame.""" + fr_map = {col.lower(): df[col].tolist() for col in df.columns} + + for column in fr_map: + if column in DatasetRow.schema: + raise DatasetPrepareError( + name, + f"import from pandas error - column '{column}' conflicts with" + " default schema", + ) + if not column.isidentifier(): + raise DatasetPrepareError( + name, + f"import from pandas error - '{column}' cannot be a column name", + ) + + return cls.from_features(name, session, **fr_map) + + def parse_tabular( + self, + output: Optional[dict[str, FeatureType]] = None, + **kwargs, + ) -> "DataChain": + """Generate chain from list of tabular files. + + Parameters: + output : Dictionary defining column names and their corresponding types. + kwargs : Parameters to pass to pyarrow.dataset.dataset. + + Examples: + Reading a json lines file: + >>> dc = DataChain.from_storage("s3://mybucket/file.jsonl") + >>> dc = dc.parse_tabular(format="json") + + Reading a filtered list of files as a dataset: + >>> dc = DataChain.from_storage("s3://mybucket") + >>> dc = dc.filter(C("file.name").glob("*.jsonl")) + >>> dc = dc.parse_tabular(format="json") + """ + from pyarrow import unify_schemas + from pyarrow.dataset import dataset + + from datachain.lib.arrow import ArrowGenerator, schema_to_output + + schema = None + if output: + output = {"source": IndexedFile} | output + else: + schemas = [] + for row in self.select("file").iterate(): + file = row[0] + ds = dataset(file.get_path(), filesystem=file.get_fs(), **kwargs) # type: ignore[union-attr] + schemas.append(ds.schema) + if not schemas: + msg = "error parsing tabular data schema - found no files to parse" + raise DatasetPrepareError(self.name, msg) + schema = unify_schemas(schemas) + try: + output = schema_to_output(schema) + except ValueError as e: + raise DatasetPrepareError(self.name, e) from e + + return self.gen(ArrowGenerator(schema, **kwargs), output=output) + + def parse_csv( + self, + delimiter: str = ",", + header: bool = True, + column_names: Optional[list[str]] = None, + output: Optional[dict[str, FeatureType]] = None, + ) -> "DataChain": + """Generate chain from list of csv files. + + Parameters: + delimiter : Character for delimiting columns. + header : Whether the files include a header row. + column_names : Column names if no header. Implies `header = False`. + output : Dictionary defining column names and their corresponding types. + + Examples: + Reading a csv file: + >>> dc = DataChain.from_storage("s3://mybucket/file.csv") + >>> dc = dc.parse_tabular(format="csv") + + Reading a filtered list of csv files as a dataset: + >>> dc = DataChain.from_storage("s3://mybucket") + >>> dc = dc.filter(C("file.name").glob("*.csv")) + >>> dc = dc.parse_tabular() + """ + from pyarrow.csv import ParseOptions, ReadOptions + from pyarrow.dataset import CsvFileFormat + + if column_names and output: + msg = "error parsing csv - only one of column_names or output is allowed" + raise DatasetPrepareError(self.name, msg) + + if not header and not column_names: + if output: + column_names = list(output.keys()) + else: + msg = "error parsing csv - provide column_names or output if no header" + raise DatasetPrepareError(self.name, msg) + + parse_options = ParseOptions(delimiter=delimiter) + read_options = ReadOptions(column_names=column_names) + format = CsvFileFormat(parse_options=parse_options, read_options=read_options) + return self.parse_tabular(output=output, format=format) + + def parse_parquet( + self, + partitioning: Any = "hive", + output: Optional[dict[str, FeatureType]] = None, + ) -> "DataChain": + """Generate chain from list of parquet files. + + Parameters: + partitioning : Any pyarrow partitioning schema. + output : Dictionary defining column names and their corresponding types. + + Examples: + Reading a single file: + >>> dc = DataChain.from_storage("s3://mybucket/file.parquet") + >>> dc = dc.parse_tabular() + + Reading a partitioned dataset from a directory: + >>> dc = DataChain.from_storage("path/to/dir") + >>> dc = dc.parse_tabular() + + Reading a filtered list of files as a dataset: + >>> dc = DataChain.from_storage("s3://mybucket") + >>> dc = dc.filter(C("file.name").glob("*.parquet")) + >>> dc = dc.parse_tabular() + + Reading a filtered list of partitions as a dataset: + >>> dc = DataChain.from_storage("s3://mybucket") + >>> dc = dc.filter(C("file.parent").glob("*month=1*")) + >>> dc = dc.parse_tabular() + """ + return self.parse_tabular( + output=output, format="parquet", partitioning=partitioning + ) + + @classmethod + def create_empty( + cls, + to_insert: Optional[Union[dict, list[dict]]], + session: Optional[Session] = None, + ) -> "DataChain": + """Create empty chain. Returns a chain. This method is used for programmatically + generating a chains in contrast of reading data from storages or other sources. + + Parameters: + to_insert : records (or a single record) to insert. Each record is + a dictionary of signals and theirs values. + + Examples: + >>> empty = DataChain.create_empty() + >>> single_record = DataChain.create_empty(DataChain.DEFAULT_FILE_RECORD) + """ + session = Session.get(session) + catalog = session.catalog + + name = session.generate_temp_dataset_name() + columns: tuple[sqlalchemy.Column[Any], ...] = tuple( + sqlalchemy.Column(name, typ) + for name, typ in File._datachain_column_types.items() + ) + dsr = catalog.create_dataset(name, columns=columns) + + if isinstance(to_insert, dict): + to_insert = [to_insert] + elif not to_insert: + to_insert = [] + + warehouse = catalog.warehouse + dr = warehouse.dataset_rows(dsr) + db = warehouse.db + insert_q = dr.get_table().insert() + for record in to_insert: + db.execute(insert_q.values(**record)) + return DataChain(name=dsr.name) + + def sum(self, fr: FeatureType): # type: ignore[override] + return self._extend_features("sum", fr) + + def avg(self, fr: FeatureType): # type: ignore[override] + return self._extend_features("avg", fr) + + def min(self, fr: FeatureType): # type: ignore[override] + return self._extend_features("min", fr) + + def max(self, fr: FeatureType): # type: ignore[override] + return self._extend_features("max", fr) + + def setup(self, **kwargs) -> "Self": + intersection = set(self._setup.keys()) & set(kwargs.keys()) + if intersection: + keys = ", ".join(intersection) + raise DatasetPrepareError(self.name, f"this value(s) already setup: {keys}") + + self._setup = self._setup | kwargs + return self diff --git a/src/datachain/lib/feature.py b/src/datachain/lib/feature.py new file mode 100644 index 000000000..aec5a5778 --- /dev/null +++ b/src/datachain/lib/feature.py @@ -0,0 +1,407 @@ +import copy +import inspect +import re +import warnings +from collections.abc import Iterable, Sequence +from datetime import datetime +from functools import lru_cache +from types import GenericAlias +from typing import ( + TYPE_CHECKING, + Any, + ClassVar, + Literal, + Union, + get_args, + get_origin, +) + +import attrs +import numpy as np +import pandas as pd +from pydantic import BaseModel +from typing_extensions import Literal as LiteralEx + +from datachain.lib.feature_registry import Registry +from datachain.query import C +from datachain.query.schema import DEFAULT_DELIMITER +from datachain.sql.types import ( + JSON, + Array, + Binary, + Boolean, + DateTime, + Float, + Int, + Int32, + Int64, + NullType, + SQLType, + String, +) + +if TYPE_CHECKING: + from datachain.catalog import Catalog + +FeatureStandardType = Union[ + type[int], + type[str], + type[float], + type[bool], + type[list], + type[dict], + type[bytes], + type[datetime], +] + +FeatureType = Union[type["Feature"], FeatureStandardType] +FeatureTypeNames = "Feature, int, str, float, bool, list, dict, bytes, datetime" + + +TYPE_TO_DATACHAIN = { + int: Int64, + str: String, + Literal: String, + LiteralEx: String, + float: Float, + bool: Boolean, + datetime: DateTime, # Note, list of datetime is not supported yet + bytes: Binary, # Note, list of bytes is not supported yet + list: Array, + dict: JSON, +} + +DATACHAIN_TO_TYPE = { + Int: int, + Int32: int, + Int64: int, + String: str, + Float: float, + Boolean: bool, + DateTime: datetime, + Binary: bytes, + Array(NullType): list, + JSON: dict, +} + + +NUMPY_TO_DATACHAIN = { + np.dtype("int8"): Int, + np.dtype("int16"): Int, + np.dtype("int32"): Int, + np.dtype("int64"): Int, + np.dtype("uint8"): Int, + np.dtype("uint16"): Int, + np.dtype("uint32"): Int, + np.dtype("uint64"): Int, + np.dtype("float16"): Float, + np.dtype("float32"): Float, + np.dtype("float64"): Float, + np.dtype("object"): String, + pd.CategoricalDtype(): String, +} + + +# Disable Pydantic warning, see https://github.com/iterative/dvcx/issues/1285 +warnings.filterwarnings( + "ignore", + message="Field name .* shadows an attribute in parent", + category=UserWarning, +) + + +# Optimization: Store feature classes in this lookup variable so extra checks can be +# skipped within loops. +feature_classes_lookup: dict[type, bool] = {} + + +class Feature(BaseModel): + """A base class for defining data classes that serve as inputs and outputs for + DataChain processing functions like `map()`, `gen()`, etc. Inherits from + `pydantic`'s BaseModel. + """ + + _is_file: ClassVar[bool] = False + _version: ClassVar[int] = 1 + + @classmethod + def _is_hidden(cls): + return cls.__name__.startswith("_") + + def get_value(self, *args: Any, **kwargs: Any) -> Any: + name = self.__class__.__name__ + raise NotImplementedError(f"get_value() is not defined for feature '{name}'") + + @classmethod + def _name(cls) -> str: + return f"{cls.__name__}@{cls._version}" + + @classmethod + def __pydantic_init_subclass__(cls): + Registry.add(cls) + for name, field_info in cls.model_fields.items(): + attr_value = _resolve(cls, name, field_info, prefix=[]) + setattr(cls, name, RestrictedAttribute(attr_value, cls, name)) + + @classmethod + def _prefix(cls) -> str: + return cls._normalize(cls.__name__) + + @classmethod + def _normalize(cls, name: str) -> str: + if DEFAULT_DELIMITER in name: + raise RuntimeError( + f"variable '{name}' cannot be used " + f"because it contains {DEFAULT_DELIMITER}" + ) + return Feature._to_snake_case(name) + + @staticmethod + def _to_snake_case(name: str) -> str: + """Convert a CamelCase name to snake_case.""" + s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name) + return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower() + + def _set_stream(self, catalog: "Catalog", caching_enabled: bool = False) -> None: + pass + + @classmethod + def get_file_signals(cls, path: list[str]) -> Iterable[list[str]]: + if cls._is_file: + yield path + + for name, f_info in cls.model_fields.items(): + anno = f_info.annotation + if Feature.is_feature(anno): + yield from anno.get_file_signals([*path, name]) # type: ignore[union-attr] + + @classmethod + def is_feature(cls, anno) -> bool: + if anno in feature_classes_lookup: + # Optimization: Skip expensive subclass checks if already checked. + return feature_classes_lookup[anno] + is_class = inspect.isclass(anno) + result = ( + is_class + and not isinstance(anno, GenericAlias) + and issubclass(anno, Feature) + ) + if is_class: + # Only cache types in the feature classes lookup dict (not instances). + feature_classes_lookup[anno] = result + return result + + @classmethod + def is_standard_type(cls, t: type) -> bool: + return any( + t is ft or t is get_args(ft)[0] for ft in get_args(FeatureStandardType) + ) + + @classmethod + def is_feature_type(cls, t: type) -> bool: + if cls.is_standard_type(t): + return True + if get_origin(t) is list and len(get_args(t)) == 1: + return cls.is_feature_type(get_args(t)[0]) + return cls.is_feature(t) + + def _flatten_fields_values(self, fields, model): + for name, f_info in fields.items(): + anno = f_info.annotation + # Optimization: Access attributes directly to skip the model_dump() call. + value = getattr(model, name) + + if isinstance(value, list): + yield [ + val.model_dump() if Feature.is_feature(type(val)) else val + for val in value + ] + elif isinstance(value, dict): + yield { + key: val.model_dump() if Feature.is_feature(type(val)) else val + for key, val in value.items() + } + elif Feature.is_feature(anno): + yield from self._flatten_fields_values(anno.model_fields, value) + else: + yield value + + def _flatten(self): + return tuple(self._flatten_fields_values(self.model_fields, self)) + + @staticmethod + def _flatten_list(objs): + return tuple( + val + for obj in objs + for val in obj._flatten_fields_values(obj.model_fields, obj) + ) + + @classmethod + def _unflatten_with_path(cls, dump, name_path: list[str]): + res = {} + for name, f_info in cls.model_fields.items(): + anno = f_info.annotation + name_norm = cls._normalize(name) + lst = copy.copy(name_path) + + if inspect.isclass(anno) and issubclass(anno, Feature): + lst.append(name_norm) + val = anno._unflatten_with_path(dump, lst) + res[name] = val + else: + lst.append(name_norm) + curr_path = DEFAULT_DELIMITER.join(lst) + res[name] = dump[curr_path] + return cls(**res) + + @classmethod + def _unflatten(cls, dump): + return cls._unflatten_with_path(dump, []) + + @classmethod + def _unflatten_to_json(cls, row: Sequence[Any], pos=0) -> dict: + return cls._unflatten_to_json_pos(row, pos)[0] + + @classmethod + def _unflatten_to_json_pos(cls, row: Sequence[Any], pos=0) -> tuple[dict, int]: + res = {} + for name, f_info in cls.model_fields.items(): + anno = f_info.annotation + origin = get_origin(anno) + if ( + origin not in (list, dict) + and inspect.isclass(anno) + and issubclass(anno, Feature) + ): + res[name], pos = anno._unflatten_to_json_pos(row, pos) + else: + res[name] = row[pos] + pos += 1 + return res, pos + + @classmethod + @lru_cache(maxsize=1000) + def build_tree(cls): + res = {} + + for name, f_info in cls.model_fields.items(): + anno = f_info.annotation + subtree = anno.build_tree() if Feature.is_feature(anno) else None + res[name] = (anno, subtree) + + return res + + +class RestrictedAttribute: + """Descriptor implementing an attribute that can only be accessed through + the defining class and not from subclasses or instances. + + Since it is a non-data descriptor, instance dicts have precedence over it. + Cannot be used with slotted classes. + """ + + def __init__(self, value, cls=None, name=None): + self.cls = cls + self.value = value + self.name = name + + def __get__(self, instance, owner): + if owner is not self.cls: + raise AttributeError( + f"'{type(owner).__name__}' object has no attribute '{self.name}'" + ) + if instance is not None: + raise RuntimeError( + f"Invalid attempt to access class attribute '{self.name}' through " + f"'{type(owner).__name__}' instance" + ) + return self.value + + def __set_name__(self, cls, name): + self.cls = cls + self.name = name + + +@attrs.define +class FeatureAttributeWrapper: + cls: type[Feature] + prefix: list[str] + + @property + def name(self) -> str: + return DEFAULT_DELIMITER.join(self.prefix) + + def __getattr__(self, name): + field_info = self.cls.model_fields.get(name) + if field_info: + return _resolve(self.cls, name, field_info, prefix=self.prefix) + raise AttributeError( + f"'{type(self).__name__}' object has no attribute '{name}'" + ) + + +def _resolve(cls, name, field_info, prefix: list[str]): + """Resolve feature attributes so they can be used in select(), join() + and similar functions. + + Users just use `MyClass.sub_attr1.sub_attr2.field` and it will return a DB column + with a proper name (with default naming - `my_class__sub_attr1__sub_attr2__field`). + """ + anno = field_info.annotation + norm_name = cls._normalize(name) + + if not cls.is_feature(anno): + try: + anno_sql_class = convert_type_to_datachain(anno) + except TypeError: + anno_sql_class = NullType + new_prefix = copy.copy(prefix) + new_prefix.append(norm_name) + return C(DEFAULT_DELIMITER.join(new_prefix), anno_sql_class) + + return FeatureAttributeWrapper(anno, [*prefix, norm_name]) + + +def convert_type_to_datachain(typ): # noqa: PLR0911 + if inspect.isclass(typ) and issubclass(typ, SQLType): + return typ + + res = TYPE_TO_DATACHAIN.get(typ) + if res: + return res + + orig = get_origin(typ) + + if orig in (Literal, LiteralEx): + return String + + args = get_args(typ) + if inspect.isclass(orig) and (issubclass(list, orig) or issubclass(tuple, orig)): + if args is None or len(args) != 1: + raise TypeError(f"Cannot resolve type '{typ}' for flattening features") + + args0 = args[0] + if Feature.is_feature(args0): + return Array(JSON()) + + next_type = convert_type_to_datachain(args0) + return Array(next_type) + + if inspect.isclass(orig) and issubclass(dict, orig): + return JSON + + if orig == Union and len(args) == 2 and (type(None) in args): + return convert_type_to_datachain(args[0]) + + # Special case for list in JSON: Union[dict, list[dict]] + if orig == Union and len(args) >= 2: + args_no_nones = [arg for arg in args if arg != type(None)] + if len(args_no_nones) == 2: + args_no_dicts = [arg for arg in args_no_nones if arg is not dict] + if len(args_no_dicts) == 1 and get_origin(args_no_dicts[0]) is list: + arg = get_args(args_no_dicts[0]) + if len(arg) == 1 and arg[0] is dict: + return JSON + + raise TypeError(f"Cannot recognize type {typ}") diff --git a/src/datachain/lib/feature_registry.py b/src/datachain/lib/feature_registry.py new file mode 100644 index 000000000..e70ca4e04 --- /dev/null +++ b/src/datachain/lib/feature_registry.py @@ -0,0 +1,51 @@ +import logging +from typing import Any, ClassVar, Optional + +logger = logging.getLogger(__name__) + + +class Registry: + reg: ClassVar[dict[str, dict[int, Any]]] = {} + + @classmethod + def add(cls, fr: type) -> None: + if fr._is_hidden(): # type: ignore[attr-defined] + return + name = fr.__name__ + if name not in cls.reg: + cls.reg[name] = {} + version = fr._version # type: ignore[attr-defined] + if version in cls.reg[name]: + full_name = f"{name}@{version}" + logger.warning("Feature %s is already registered", full_name) + cls.reg[name][version] = fr + + @classmethod + def get(cls, name: str, version: Optional[int] = None) -> Optional[type]: + class_dict = cls.reg.get(name, None) + if class_dict is None: + return None + if version is None: + max_ver = max(class_dict.keys(), default=None) + if max_ver is None: + return None + return class_dict[max_ver] + return class_dict.get(version, None) + + @classmethod + def parse_name_version(cls, fullname: str) -> tuple[str, int]: + name = fullname + version = 1 + + if "@" in fullname: + name, version_str = fullname.split("@") + if version_str.strip() != "": + version = int(version_str) + + return name, version + + @classmethod + def remove(cls, fr: type) -> None: + version = fr._version # type: ignore[attr-defined] + if fr.__name__ in cls.reg and version in cls.reg[fr.__name__]: + del cls.reg[fr.__name__][version] diff --git a/src/datachain/lib/feature_utils.py b/src/datachain/lib/feature_utils.py new file mode 100644 index 000000000..69cfaa15a --- /dev/null +++ b/src/datachain/lib/feature_utils.py @@ -0,0 +1,136 @@ +import string +from collections.abc import Sequence +from typing import Any, Union, get_args, get_origin + +from pydantic import BaseModel, create_model + +from datachain.lib.feature import ( + TYPE_TO_DATACHAIN, + Feature, + FeatureType, + FeatureTypeNames, + convert_type_to_datachain, +) +from datachain.lib.utils import DataChainParamsError + +AUTO_FEATURE_PREFIX = "_auto_fr" +SUFFIX_SYMBOLS = string.digits + string.ascii_lowercase + + +class FeatureToTupleError(DataChainParamsError): + def __init__(self, ds_name, msg): + if ds_name: + ds_name = f"' {ds_name}'" + super().__init__(f"Cannot convert features for dataset{ds_name}: {msg}") + + +feature_cache: dict[type[BaseModel], type[Feature]] = {} + + +def pydantic_to_feature(data_cls: type[BaseModel]) -> type[Feature]: + if data_cls in feature_cache: + return feature_cache[data_cls] + + fields = {} + for name, field_info in data_cls.model_fields.items(): + anno = field_info.annotation + if anno not in TYPE_TO_DATACHAIN: + orig = get_origin(anno) + if orig is list: + anno = get_args(anno) # type: ignore[assignment] + if isinstance(anno, Sequence): + anno = anno[0] # type: ignore[unreachable] + is_list = True + else: + is_list = False + + try: + convert_type_to_datachain(anno) + except TypeError: + if not Feature.is_feature(anno): # type: ignore[arg-type] + anno = pydantic_to_feature(anno) # type: ignore[arg-type] + + if is_list: + anno = list[anno] # type: ignore[valid-type] + fields[name] = (anno, field_info.default) + + cls = create_model( + data_cls.__name__, + __base__=(data_cls, Feature), # type: ignore[call-overload] + **fields, + ) + feature_cache[data_cls] = cls + return cls + + +def features_to_tuples( + ds_name: str = "", + output: Union[None, FeatureType, Sequence[str], dict[str, FeatureType]] = None, + **fr_map, +) -> tuple[Any, Any, Any]: + types_map = {} + length = -1 + for k, v in fr_map.items(): + if not isinstance(v, Sequence) or isinstance(v, str): + raise FeatureToTupleError(ds_name, f"features '{k}' is not a sequence") + len_ = len(v) + + if len_ == 0: + raise FeatureToTupleError(ds_name, f"feature '{k}' is empty list") + + if length < 0: + length = len_ + elif length != len_: + raise FeatureToTupleError( + ds_name, + f"feature '{k}' should have length {length} while {len_} is given", + ) + typ = type(v[0]) + if not Feature.is_feature_type(typ): + raise FeatureToTupleError( + ds_name, + f"feature '{k}' has unsupported type '{typ.__name__}'." + f" Please use Feature types: {FeatureTypeNames}", + ) + types_map[k] = typ + if output: + if not isinstance(output, Sequence) and not isinstance(output, str): + if len(fr_map) != 1: + raise FeatureToTupleError( + ds_name, + f"only one output type was specified, {len(fr_map)} expected", + ) + if not isinstance(output, type): + raise FeatureToTupleError( + ds_name, + f"output must specify a type while '{output}' was given", + ) + + key: str = next(iter(fr_map.keys())) + output = {key: output} # type: ignore[dict-item] + + if len(output) != len(fr_map): + raise FeatureToTupleError( + ds_name, + f"number of outputs '{len(output)}' should match" + f" number of features '{len(fr_map)}'", + ) + if isinstance(output, dict): + raise FeatureToTupleError( + ds_name, + "output type must be dict[str, FeatureType] while " + f"'{type(output).__name__}' is given", + ) + else: + output = types_map + + output_types: list[type] = list(output.values()) # type: ignore[union-attr,arg-type] + if len(output) > 1: + tuple_type = tuple(output_types) + res_type = tuple[tuple_type] # type: ignore[valid-type] + res_values = list(zip(*fr_map.values())) + else: + res_type = output_types[0] # type: ignore[misc] + res_values = next(iter(fr_map.values())) + + return res_type, output, res_values diff --git a/src/datachain/lib/file.py b/src/datachain/lib/file.py new file mode 100644 index 000000000..6fc5bb6c1 --- /dev/null +++ b/src/datachain/lib/file.py @@ -0,0 +1,288 @@ +import json +from abc import ABC, abstractmethod +from datetime import datetime +from pathlib import Path +from typing import TYPE_CHECKING, Any, ClassVar, Literal, Optional, Union +from urllib.parse import unquote, urlparse +from urllib.request import url2pathname + +from fsspec.implementations.local import LocalFileSystem +from pydantic import Field, field_validator + +from datachain.cache import UniqueId +from datachain.client.fileslice import FileSlice +from datachain.lib.cached_stream import PreCachedStream, PreDownloadStream +from datachain.lib.feature import Feature +from datachain.lib.utils import DataChainError +from datachain.sql.types import JSON, Int, String +from datachain.utils import TIME_ZERO + +if TYPE_CHECKING: + from datachain.catalog import Catalog + + +class FileFeature(Feature): + _is_file = True + + def open(self): + raise NotImplementedError + + def read(self): + with self.open() as stream: + return stream.read() + + def get_value(self): + return self.read() + + +class VFileError(DataChainError): + def __init__(self, file: "File", message: str, vtype: str = ""): + type_ = f" of vtype '{vtype}'" if vtype else "" + super().__init__(f"Error in v-file '{file.get_uid().path}'{type_}: {message}") + + +class FileError(DataChainError): + def __init__(self, file: "File", message: str): + super().__init__(f"Error in file {file.get_uri()}: {message}") + + +class VFile(ABC): + @classmethod + @abstractmethod + def get_vtype(cls) -> str: + pass + + @classmethod + @abstractmethod + def open(cls, file: "File", location: list[dict]): + pass + + +class TarVFile(VFile): + @classmethod + def get_vtype(cls) -> str: + return "tar" + + @classmethod + def open(cls, file: "File", location: list[dict]): + if len(location) > 1: + VFileError(file, "multiple 'location's are not supported yet") + + loc = location[0] + + if (offset := loc.get("offset", None)) is None: + VFileError(file, "'offset' is not specified") + + if (size := loc.get("size", None)) is None: + VFileError(file, "'size' is not specified") + + if (parent := loc.get("parent", None)) is None: + VFileError(file, "'parent' is not specified") + + tar_file = File(**parent) + tar_file._set_stream(file._catalog) + + tar_file_uid = tar_file.get_uid() + client = file._catalog.get_client(tar_file_uid.storage) + fd = client.open_object(tar_file_uid, use_cache=file._caching_enabled) + return FileSlice(fd, offset, size, file.name) + + +class VFileRegistry: + _vtype_readers: ClassVar[dict[str, type["VFile"]]] = {"tar": TarVFile} + + @classmethod + def register(cls, reader: type["VFile"]): + cls._vtype_readers[reader.get_vtype()] = reader + + @classmethod + def resolve(cls, file: "File", location: list[dict]): + if len(location) == 0: + raise VFileError(file, "'location' must not be list of JSONs") + + if not (vtype := location[0].get("vtype", "")): + raise VFileError(file, "vtype is not specified") + + reader = cls._vtype_readers.get(vtype, None) + if not reader: + raise VFileError(file, "reader not registered", vtype) + + return reader.open(file, location) + + +class File(FileFeature): + source: str = Field(default="") + parent: str = Field(default="") + name: str + size: int = Field(default=0) + version: str = Field(default="") + etag: str = Field(default="") + is_latest: bool = Field(default=True) + last_modified: datetime = Field(default=TIME_ZERO) + location: Optional[Union[dict, list[dict]]] = Field(default=None) + vtype: str = Field(default="") + + _datachain_column_types: ClassVar[dict[str, Any]] = { + "source": String, + "parent": String, + "name": String, + "version": String, + "etag": String, + "size": Int, + "vtype": String, + "location": JSON, + } + + _unique_id_keys: ClassVar[list[str]] = [ + "source", + "parent", + "name", + "etag", + "size", + "vtype", + "location", + ] + + @staticmethod + def to_dict( + v: Optional[Union[str, dict, list[dict]]], + ) -> Optional[Union[str, dict, list[dict]]]: + if v is None or v == "": + return None + if isinstance(v, str): + try: + return json.loads(v) + except Exception as e: # noqa: BLE001 + raise ValueError( + f"Unable to convert string '{v}' to dict for File feature: {e}" + ) from None + return v + + # Workaround for empty JSONs converted to empty strings in some DBs. + @field_validator("location", mode="before") + @classmethod + def validate_location(cls, v): + return File.to_dict(v) + + @field_validator("parent", mode="before") + @classmethod + def validate_path(cls, path): + if path == "": + return "" + return Path(path).as_posix() + + def model_dump_custom(self): + res = self.model_dump() + res["last_modified"] = str(res["last_modified"]) + return res + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._stream = None + self._catalog = None + self._caching_enabled = False + + def open(self): + if self._stream is None: + raise FileError(self, "stream is not set") + + if self.location: + return VFileRegistry.resolve(self, self.location) + + return self._stream + + def _set_stream(self, catalog: "Catalog", caching_enabled: bool = False) -> None: + self._catalog = catalog + stream_class = PreCachedStream if caching_enabled else PreDownloadStream + self._stream = stream_class(self._catalog, self.get_uid()) + self._caching_enabled = caching_enabled + + def get_uid(self) -> UniqueId: + dump = self.model_dump() + return UniqueId(*(dump[k] for k in self._unique_id_keys)) + + def get_local_path(self) -> Optional[str]: + """Get path to a file in a local cache. + Return None if file is not cached. Throws an exception if cache is not setup.""" + if self._catalog is None: + raise RuntimeError( + "cannot resolve local file path because catalog is not setup" + ) + return self._catalog.cache.get_path(self.get_uid()) + + def get_file_suffix(self): + return Path(self.name).suffix + + def get_file_ext(self): + return Path(self.name).suffix.strip(".") + + def get_file_stem(self): + return Path(self.name).stem + + def get_full_name(self): + return (Path(self.parent) / self.name).as_posix() + + def get_uri(self): + return f"{self.source}/{self.get_full_name()}" + + def get_path(self) -> str: + path = unquote(self.get_uri()) + fs = self.get_fs() + if isinstance(fs, LocalFileSystem): + # Drop file:// protocol + path = urlparse(path).path + path = url2pathname(path) + return path + + def get_fs(self): + return self._catalog.get_client(self.source).fs + + +class TextFile(File): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._stream = None + + def _set_stream(self, catalog: "Catalog", caching_enabled: bool = False) -> None: + super()._set_stream(catalog, caching_enabled) + self._stream.set_mode("r") + + +def get_file(type: Literal["binary", "text", "image"] = "binary"): + file = File + if type == "text": + file = TextFile + elif type == "image": + from datachain.lib.image import ImageFile + + file = ImageFile # type: ignore[assignment] + + def get_file_type( + source: str, + parent: str, + name: str, + version: str, + etag: str, + size: int, + vtype: str, + location: Optional[Union[dict, list[dict]]], + ) -> file: # type: ignore[valid-type] + return file( + source=source, + parent=parent, + name=name, + version=version, + etag=etag, + size=size, + vtype=vtype, + location=location, + ) + + return get_file_type + + +class IndexedFile(Feature): + """File source info for tables.""" + + file: File + index: int diff --git a/src/datachain/lib/gpt4_vision.py b/src/datachain/lib/gpt4_vision.py new file mode 100644 index 000000000..c898d907d --- /dev/null +++ b/src/datachain/lib/gpt4_vision.py @@ -0,0 +1,105 @@ +import base64 +import io +import os + +import requests + +try: + from PIL import Image, ImageOps, UnidentifiedImageError +except ImportError as exc: + raise ImportError( + "Missing dependency Pillow for computer vision:\n" + "To install run:\n\n" + " pip install 'datachain[cv]'\n" + ) from exc + +from datachain.query import Object, udf +from datachain.sql.types import String + +DEFAULT_FIT_BOX = (500, 500) +DEFAULT_TOKENS = 300 + + +def encode_image(raw): + try: + img = Image.open(raw) + except UnidentifiedImageError: + return None + img.load() + img = ImageOps.fit(img, DEFAULT_FIT_BOX) + output = io.BytesIO() + img.save(output, format="JPEG") + hex_data = output.getvalue() + return base64.b64encode(hex_data).decode("utf-8") + + +@udf( + params=(Object(encode_image),), # Columns consumed by the UDF. + output={ + "description": String, + "error": String, + }, # Signals being returned by the UDF. + method="image_description", +) +class DescribeImage: + def __init__( + self, + prompt="What is in this image?", + max_tokens=DEFAULT_TOKENS, + key="", + timeout=30, + ): + if not key: + key = os.getenv("OPENAI_API_KEY", "") + if not key: + raise ValueError( + "No key found. Please pass key or set the OPENAI_API_KEY " + "environment variable." + ) + self.prompt = prompt + self.max_tokens = max_tokens + self.headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {key}", + } + self.timeout = timeout + + def image_description(self, base64_image): + if base64_image is None: + return ("", "Unknown image format") + + payload = { + "model": "gpt-4-vision-preview", + "messages": [ + { + "role": "user", + "content": [ + {"type": "text", "text": self.prompt}, + { + "type": "image_url", + "image_url": { + "url": f"data:image/jpeg;base64,{base64_image}" + }, + }, + ], + } + ], + "max_tokens": self.max_tokens, + } + + response = requests.post( + "https://api.openai.com/v1/chat/completions", + headers=self.headers, + json=payload, + timeout=self.timeout, + ) + json_response = response.json() + + if "error" in json_response: + error = str(json_response["error"]) + openai_description = "" + else: + error = "" + openai_description = json_response["choices"][0]["message"]["content"] + + return (openai_description, error) diff --git a/src/datachain/lib/hf_image_to_text.py b/src/datachain/lib/hf_image_to_text.py new file mode 100644 index 000000000..01cff6f6c --- /dev/null +++ b/src/datachain/lib/hf_image_to_text.py @@ -0,0 +1,105 @@ +try: + import numpy as np + import torch + from PIL import Image, ImageOps, UnidentifiedImageError + from transformers import ( + AutoProcessor, + Blip2ForConditionalGeneration, + Blip2Processor, + LlavaForConditionalGeneration, + ) +except ImportError as exc: + raise ImportError( + "Missing dependencies for computer vision:\n" + "To install run:\n\n" + " pip install 'datachain[cv]'\n" + ) from exc + + +from datachain.query import Object, udf +from datachain.sql.types import String + +DEFAULT_FIT_BOX = (500, 500) + + +def encode_image(raw): + try: + img = Image.open(raw) + except UnidentifiedImageError: + return None + img.load() + img = img.convert("RGB") + return ImageOps.fit(img, DEFAULT_FIT_BOX) + + +def infer_dtype(device): + if device == "cpu": + return torch.float32 + return torch.float16 + + +@udf( + params=(Object(encode_image),), # Columns consumed by the UDF. + output={ + "description": String, + "error": String, + }, # Signals being returned by the UDF. + batch=64, + method="describe", +) +class BLIP2describe: + def __init__(self, device="cpu", model="Salesforce/blip2-opt-2.7b", max_tokens=300): + self.torch_dtype = infer_dtype(device) + self.processor = Blip2Processor.from_pretrained(model) + self.model = Blip2ForConditionalGeneration.from_pretrained( + model, torch_dtype=self.torch_dtype + ) + self.device = device + self.model.to(device) + self.max_tokens = max_tokens + + def describe(self, imgs): + images = np.squeeze(np.asarray(imgs)) + inputs = self.processor(images=images, return_tensors="pt").to( + self.device, self.torch_dtype + ) + + generated_ids = self.model.generate(**inputs, max_new_tokens=self.max_tokens) + generated_text = self.processor.batch_decode( + generated_ids, skip_special_tokens=True + ) + return [(desc.strip(), "") for desc in generated_text] + + +@udf( + params=(Object(encode_image),), # Columns consumed by the UDF. + output={ + "description": String, + "error": String, + }, # Signals being returned by the UDF. + batch=16, + method="describe", +) +class LLaVAdescribe: + def __init__(self, device="cpu", model="llava-hf/llava-1.5-7b-hf", max_tokens=300): + self.device = device + self.torch_dtype = infer_dtype(device) + self.processor = AutoProcessor.from_pretrained(model) + self.model = LlavaForConditionalGeneration.from_pretrained( + model, torch_dtype=self.torch_dtype, low_cpu_mem_usage=True + ) + self.model.to(device) + self.max_tokens = max_tokens + self.prompt = "USER: \nDescribe this picture\nASSISTANT:" + + def describe(self, imgs): + images = np.squeeze(np.asarray(imgs)) + inputs = self.processor( + text=[self.prompt] * len(imgs), images=images, return_tensors="pt" + ).to(self.device, self.torch_dtype) + + generated_ids = self.model.generate(**inputs, max_new_tokens=self.max_tokens) + generated_text = self.processor.batch_decode( + generated_ids, skip_special_tokens=True + ) + return [(desc.split("ASSISTANT:")[-1].strip(), "") for desc in generated_text] diff --git a/src/datachain/lib/hf_pipeline.py b/src/datachain/lib/hf_pipeline.py new file mode 100644 index 000000000..2cc9b19d4 --- /dev/null +++ b/src/datachain/lib/hf_pipeline.py @@ -0,0 +1,98 @@ +import json + +from transformers import pipeline + +from datachain.query import Object, udf +from datachain.sql.types import JSON, String + +try: + from PIL import ( + Image, + UnidentifiedImageError, + ) +except ImportError as exc: + raise ImportError( + "Missing dependency Pillow for computer vision:\n" + "To install run:\n\n" + " pip install 'datachain[cv]'\n" + ) from exc + + +def read_image(raw): + try: + img = Image.open(raw) + except UnidentifiedImageError: + return None + img.load() + return img.convert("RGB") + + +def read_object(raw): + return raw.read() + + +def read_text(raw): + return read_object(raw).decode("utf-8") + + +@udf( + params=(Object(read_image),), # Columns consumed by the UDF. + output={ + "model_output": JSON, + "error": String, + }, # Signals being returned by the UDF. + method="image_processor", +) +class ImageHelper: + def __init__(self, model, device, **kwargs): + self.helper = pipeline(model=model, device=device) + self.kwargs = kwargs + + def image_processor(self, imgs): + result = self.helper( + imgs, + **self.kwargs, + ) + return (json.dumps(result), "") + + +@udf( + params=(Object(read_text),), # Columns consumed by the UDF. + output={ + "model_output": JSON, + "error": String, + }, # Signals being returned by the UDF. + method="text_processor", +) +class TextHelper: + def __init__(self, model, device, **kwargs): + self.helper = pipeline(model=model, device=device) + self.kwargs = kwargs + + def text_processor(self, text): + result = self.helper( + text, + **self.kwargs, + ) + return (json.dumps(result), "") + + +@udf( + params=(Object(read_object),), # Columns consumed by the UDF. + output={ + "model_output": JSON, + "error": String, + }, # Signals being returned by the UDF. + method="raw_processor", +) +class RawHelper: + def __init__(self, model, device, **kwargs): + self.helper = pipeline(model=model, device=device) + self.kwargs = kwargs + + def raw_processor(self, obj): + result = self.helper( + obj, + **self.kwargs, + ) + return (json.dumps(result), "") diff --git a/src/datachain/lib/image.py b/src/datachain/lib/image.py new file mode 100644 index 000000000..5759d5f64 --- /dev/null +++ b/src/datachain/lib/image.py @@ -0,0 +1,89 @@ +from io import BytesIO +from typing import Callable, Optional, Union + +from datachain.lib.file import File + +try: + import torch + from PIL import Image +except ImportError as exc: + raise ImportError( + "Missing dependencies for computer vision:\n" + "To install run:\n\n" + " pip install 'datachain[cv]'\n" + ) from exc + + +class ImageFile(File): + def get_value(self): + value = super().get_value() + return Image.open(BytesIO(value)) + + +def convert_image( + img: Image.Image, + mode: str = "RGB", + size: Optional[tuple[int, int]] = None, + transform: Optional[Callable] = None, + encoder: Optional[Callable] = None, +) -> Union[Image.Image, torch.Tensor]: + """ + Resize, transform, and otherwise convert an image. + + Args: + img (Image): PIL.Image object. + mode (str): PIL.Image mode. + size (tuple[int, int]): Size in (width, height) pixels for resizing. + transform (Callable): Torchvision transform or huggingface processor to apply. + encoder (Callable): Encode image using model. + """ + if mode: + img = img.convert(mode) + if size: + img = img.resize(size) + if transform: + img = transform(img) + + try: + from transformers.image_processing_utils import BaseImageProcessor + + if isinstance(transform, BaseImageProcessor): + img = torch.tensor(img.pixel_values[0]) # type: ignore[assignment,attr-defined] + except ImportError: + pass + if encoder: + img = img.unsqueeze(0) # type: ignore[attr-defined] + if encoder: + img = encoder(img) + return img + + +def convert_images( + images: Union[Image.Image, list[Image.Image]], + mode: str = "RGB", + size: Optional[tuple[int, int]] = None, + transform: Optional[Callable] = None, + encoder: Optional[Callable] = None, +) -> Union[list[Image.Image], torch.Tensor]: + """ + Resize, transform, and otherwise convert one or more images. + + Args: + img (Image, list[Image]): PIL.Image object or list of objects. + mode (str): PIL.Image mode. + size (tuple[int, int]): Size in (width, height) pixels for resizing. + transform (Callable): Torchvision transform or huggingface processor to apply. + encoder (Callable): Encode image using model. + """ + if isinstance(images, Image.Image): + images = [images] + + converted = [convert_image(img, mode, size, transform) for img in images] + + if isinstance(converted[0], torch.Tensor): + converted = torch.stack(converted) # type: ignore[assignment,arg-type] + + if encoder: + converted = encoder(converted) + + return converted # type: ignore[return-value] diff --git a/src/datachain/lib/image_transform.py b/src/datachain/lib/image_transform.py new file mode 100644 index 000000000..958311ed0 --- /dev/null +++ b/src/datachain/lib/image_transform.py @@ -0,0 +1,104 @@ +import os + +import fsspec +from PIL import Image + +from datachain.catalog import get_catalog +from datachain.query import DatasetRow, Object, udf + + +def load_image(raw): + img = Image.open(raw) + img.load() + return img + + +@udf( + output=DatasetRow.schema, + params=(Object(load_image), *tuple(DatasetRow.schema.keys())), +) +class ImageTransform: + def __init__( + self, + *, + image_filter, + bucket_name, + prefix, + output_folder, + file_prefix="", + vtype="", + ): + # Once we fix the UDF decorator situation, it would make more sense to put this + # into a child class and make apply_filter an abstractmethod. + self.image_filter = image_filter + self.folder_name = output_folder + self.file_prefix = file_prefix + self.prefix = prefix + self.vtype = vtype + + catalog = get_catalog() + self.client, _ = catalog.parse_url(os.path.join(self.prefix, bucket_name)) + + def apply_filter(self, image): + return image.filter(self.image_filter) + + def save(self, image, source, name, format): + # Make name for new image + new_name = f"{self.file_prefix}{name}" + + # Do writeback + blob_name = os.path.join(self.folder_name, new_name) + urlpath = os.path.join(source, blob_name) + cloud_file = fsspec.open(urlpath=urlpath, mode="wb") + with cloud_file as fp: + image.save(fp, format=format) + + # Get the blob info + info_ = self.client.fs.info(urlpath) + info = self.client.convert_info(info_, self.folder_name) + info.name = new_name + return info + + def __call__( + self, + image, + *args, + ): + # Build a dict from row contents + record = dict(zip(DatasetRow.schema.keys(), args)) + del record["random"] # random will be populated automatically + record["is_latest"] = record["is_latest"] > 0 # needs to be a bool + + # yield same row back + yield DatasetRow.create(**record) + + # Don't apply the filter twice + if record["parent"] == self.folder_name: + return + + # Apply the filter + image_b = self.apply_filter(image) + + # Save the image and get the cloud object info + entry = self.save( + image_b, record["source"], name=record["name"], format=image.format + ) + + # Build the new row + yield DatasetRow.create( + name=entry.name, + source=record["source"], + parent=self.folder_name, + size=entry.size, + location=record["name"] + if not record["parent"] + else f"{record['parent']}/{record['name']}", + vtype=self.vtype, + dir_type=record["dir_type"], + owner_name=entry.owner_name, + owner_id=entry.owner_id, + is_latest=record["is_latest"], + last_modified=entry.last_modified, + version=entry.version, + etag=entry.etag, + ) diff --git a/src/datachain/lib/iptc_exif_xmp.py b/src/datachain/lib/iptc_exif_xmp.py new file mode 100644 index 000000000..7a59091ec --- /dev/null +++ b/src/datachain/lib/iptc_exif_xmp.py @@ -0,0 +1,83 @@ +import json + +from datachain.query import Object, udf +from datachain.sql.types import JSON, String + +try: + from PIL import ( + ExifTags, + Image, + IptcImagePlugin, + TiffImagePlugin, + UnidentifiedImageError, + ) +except ImportError as exc: + raise ImportError( + "Missing dependency Pillow for computer vision:\n" + "To install run:\n\n" + " pip install 'datachain[cv]'\n" + ) from exc + + +def encode_image(raw): + try: + img = Image.open(raw) + except UnidentifiedImageError: + return None + return img + + +@udf( + params=(Object(encode_image),), # Columns consumed by the UDF. + output={ + "xmp": JSON, + "exif": JSON, + "iptc": JSON, + "error": String, + }, # Signals being returned by the UDF. + method="image_description", +) +class GetMetadata: + def cast(self, v): # to JSON serializable types + if isinstance(v, TiffImagePlugin.IFDRational): + return float(v) + if isinstance(v, tuple): + return tuple(self.cast(t) for t in v) + if isinstance(v, bytes): + return v.decode(encoding="utf-8", errors="ignore") + if isinstance(v, dict): + for kk, vv in v.items(): + v[kk] = self.cast(vv) + return v + if isinstance(v, list): + return [self.cast(kk) for kk in v] + return v + + def image_description(self, img): + (xmp, exif, iptc) = ({}, {}, {}) + if img is None: + error = "Image format not understood" + return ({}, {}, {}, error) + error = "" + xmp = img.getxmp() + img_exif = img.getexif() + img_iptc = IptcImagePlugin.getiptcinfo(img) + + if img_iptc: + for k, v in img_iptc.items(): + iptc[str(k)] = self.cast(v) + + if img_exif: + for k, v in img_exif.items(): + v = self.cast(v) + if k in ExifTags.TAGS: + exif[ExifTags.TAGS[k]] = v + if k in ExifTags.GPSTAGS: + exif[ExifTags.GPSTAGS[k]] = v + + return ( + json.dumps(xmp), + json.dumps(exif), + json.dumps(iptc), + error, + ) diff --git a/src/datachain/lib/meta_formats.py b/src/datachain/lib/meta_formats.py new file mode 100644 index 000000000..2362542c9 --- /dev/null +++ b/src/datachain/lib/meta_formats.py @@ -0,0 +1,196 @@ +# pip install datamodel-code-generator +# pip install jmespath +# +import csv +import io +import json +import subprocess +import sys +import uuid +from collections.abc import Iterator +from typing import Any, Callable + +import jmespath as jsp +from pydantic import ValidationError + +from datachain.lib.feature_utils import pydantic_to_feature # noqa: F401 +from datachain.lib.file import File + +# from datachain.lib.dc import C, DataChain + + +def generate_uuid(): + return uuid.uuid4() # Generates a random UUID. + + +# JSON decoder +def load_json_from_string(json_string): + try: + return json.loads(json_string) + except json.JSONDecodeError: + print(f"Failed to decode JSON: {json_string} is not formatted correctly.") + return None + + +# Validate and reduce JSON +def process_json(data_string, jmespath): + json_dict = load_json_from_string(data_string) + if jmespath: + json_dict = jsp.search(jmespath, json_dict) + return json_dict + + +# Print a dynamic datamodel-codegen output from JSON or CSV on stdout +def read_schema(source_file, data_type="csv", expr=None, model_name=None): + data_string = "" + # using uiid to get around issue #1617 + if not model_name: + uid_str = str(generate_uuid()).replace( + "-", "" + ) # comply with Python class names + model_name = f"Model{data_type}{uid_str}" + try: + with source_file.open() as fd: # CSV can be larger than memory + if data_type == "csv": + data_string += fd.readline().decode("utf-8", "ignore").replace("\r", "") + data_string += fd.readline().decode("utf-8", "ignore").replace("\r", "") + elif data_type == "jsonl": + data_string = fd.readline().decode("utf-8", "ignore").replace("\r", "") + else: + data_string = fd.read() # other meta must fit into RAM + except OSError as e: + print(f"An unexpected file error occurred: {e}") + return + if data_type in ("json", "jsonl"): + json_object = process_json(data_string, expr) + if data_type == "json" and isinstance(json_object, list): + json_object = json_object[0] # sample the 1st object from JSON array + if data_type == "jsonl": + data_type = "json" # treat json line as plain JSON in auto-schema + data_string = json.dumps(json_object) + command = [ + "datamodel-codegen", + "--input-file-type", + data_type, + "--class-name", + model_name, + ] + try: + result = subprocess.run( # noqa: S603 + command, + input=data_string, + text=True, + capture_output=True, + check=True, + ) + model_output = ( + result.stdout + ) # This will contain the output from datamodel-codegen + except subprocess.CalledProcessError as e: + model_output = f"An error occurred in datamodel-codegen: {e.stderr}" + print(f"{model_output}") + print("\n" + f"spec=pydantic_to_feature({model_name})" + "\n") + return model_output + + +# +# UDF mapper which calls chain in the setup to infer the dynamic schema +# +def read_meta( # noqa: C901 + spec=None, + schema_from=None, + meta_type="json", + jmespath=None, + show_schema=False, + model_name=None, +) -> Callable: + from datachain.lib.dc import DataChain + + # ugly hack: datachain is run redirecting printed outputs to a variable + if schema_from: + captured_output = io.StringIO() + current_stdout = sys.stdout + sys.stdout = captured_output + try: + chain = ( + DataChain.from_storage(schema_from) + .limit(1) + .map( # dummy column created (#1615) + meta_schema=lambda file: read_schema( + file, data_type=meta_type, expr=jmespath, model_name=model_name + ), + output=str, + ) + ) + # dummy executor (#1616) + chain.save() + finally: + sys.stdout = current_stdout + model_output = captured_output.getvalue() + captured_output.close() + + if show_schema: + print(f"{model_output}") + # Below 'spec' should be a dynamically converted Feature from Pydantic datamodel + if not spec: + local_vars: dict[str, Any] = {} + exec(model_output, globals(), local_vars) # noqa: S102 + spec = local_vars["spec"] + + if not (spec) and not (schema_from): + raise ValueError( + "Must provide a static schema in spec: or metadata sample in schema_from:" + ) + + # + # UDF mapper parsing a JSON or CSV file using schema spec + # + + def parse_data( + file: File, + DataModel=spec, # noqa: N803 + meta_type=meta_type, + jmespath=jmespath, + ) -> Iterator[spec]: + def validator(json_object: dict) -> spec: + json_string = json.dumps(json_object) + try: + data_instance = DataModel.model_validate_json(json_string) + yield data_instance + except ValidationError as e: + print(f"Validation error occurred in file {file.name}:", e) + + if meta_type == "csv": + with ( + file.open() as fd + ): # TODO: if schema is statically given, should allow CSV without headers + reader = csv.DictReader(fd) + for row in reader: # CSV can be larger than memory + yield from validator(row) + + if meta_type == "json": + try: + with file.open() as fd: # JSON must fit into RAM + data_string = fd.read() + except OSError as e: + print(f"An unexpected file error occurred in file {file.name}: {e}") + json_object = process_json(data_string, jmespath) + if not isinstance(json_object, list): + yield from validator(json_object) + + else: + for json_dict in json_object: + yield from validator(json_dict) + + if meta_type == "jsonl": + try: + with file.open() as fd: + data_string = fd.readline().replace("\r", "") + while data_string: + json_object = process_json(data_string, jmespath) + data_string = fd.readline() + yield from validator(json_object) + except OSError as e: + print(f"An unexpected file error occurred in file {file.name}: {e}") + + return parse_data diff --git a/src/datachain/lib/pytorch.py b/src/datachain/lib/pytorch.py new file mode 100644 index 000000000..116106d4a --- /dev/null +++ b/src/datachain/lib/pytorch.py @@ -0,0 +1,152 @@ +import logging +from collections.abc import Iterator +from typing import TYPE_CHECKING, Any, Callable, Optional + +from torch import float32 +from torch.distributed import get_rank, get_world_size +from torch.utils.data import IterableDataset, get_worker_info + +from datachain.catalog import Catalog, get_catalog +from datachain.lib.dc import DataChain +from datachain.lib.feature import Feature +from datachain.lib.text import convert_text + +if TYPE_CHECKING: + from torchvision.transforms.v2 import Transform + + +logger = logging.getLogger("datachain") + + +try: + from PIL import Image + from torchvision.transforms import v2 + + DEFAULT_TRANSFORM = v2.Compose([v2.ToImage(), v2.ToDtype(float32, scale=True)]) +except ImportError: + logger.warning( + "Missing dependencies for computer vision:\n" + "To install run:\n\n" + " pip install 'datachain[cv]'\n" + ) + Image = None # type: ignore[assignment] + v2 = None + DEFAULT_TRANSFORM = None + + +def label_to_int(value: str, classes: list) -> int: + return classes.index(value) + + +class PytorchDataset(IterableDataset): + def __init__( + self, + name: str, + version: Optional[int] = None, + catalog: Optional["Catalog"] = None, + transform: Optional["Transform"] = DEFAULT_TRANSFORM, + tokenizer: Optional[Callable] = None, + tokenizer_kwargs: Optional[dict[str, Any]] = None, + num_samples: int = 0, + ): + """ + Pytorch IterableDataset that streams DataChain datasets. + + Args: + name (str): Name of DataChain dataset to stream. + version (int): Version of DataChain dataset to stream. + catalog (Catalog): DataChain catalog to which dataset belongs. + transform (Transform): Torchvision transforms to apply to the dataset. + tokenizer (Callable): Tokenizer to use to tokenize text values. + tokenizer_kwargs (dict): Additional kwargs to pass when calling tokenizer. + num_samples (int): Number of random samples to draw for each epoch. + This argument is ignored if `num_samples=0` (the default). + """ + self.name = name + self.version = version + self.transform = transform + self.tokenizer = tokenizer + self.tokenizer_kwargs = tokenizer_kwargs or {} + self.num_samples = num_samples + if catalog is None: + catalog = get_catalog() + self._init_catalog(catalog) + + def _init_catalog(self, catalog: "Catalog"): + # For compatibility with multiprocessing, + # we can only store params in __init__(), as Catalog isn't picklable + # see https://github.com/iterative/dvcx/issues/954 + self._idgen_params = catalog.id_generator.clone_params() + self._ms_params = catalog.metastore.clone_params() + self._wh_params = catalog.warehouse.clone_params() + self._catalog_params = catalog.get_init_params() + self.catalog: Optional[Catalog] = None + + def _get_catalog(self) -> "Catalog": + idgen_cls, idgen_args, idgen_kwargs = self._idgen_params + idgen = idgen_cls(*idgen_args, **idgen_kwargs) + ms_cls, ms_args, ms_kwargs = self._ms_params + ms = ms_cls(*ms_args, **ms_kwargs) + wh_cls, wh_args, wh_kwargs = self._wh_params + wh = wh_cls(*wh_args, **wh_kwargs) + return Catalog(idgen, ms, wh, **self._catalog_params) + + def __iter__(self) -> Iterator[Any]: + if self.catalog is None: + self.catalog = self._get_catalog() + total_rank, total_workers = self.get_rank_and_workers() + ds = DataChain(name=self.name, version=self.version, catalog=self.catalog) + ds = ds.remove_file_signals() + + if self.num_samples > 0: + ds = ds.sample(self.num_samples) + ds = ds.chunk(total_rank, total_workers) + stream = ds.iterate() + for row_features in stream: + row = [] + for fr in row_features: + if isinstance(fr, Feature): + row.append(fr.get_value()) # type: ignore[unreachable] + else: + row.append(fr) + # Apply transforms + if self.transform: + try: + if v2 and isinstance(self.transform, v2.Transform): + row = self.transform(row) + elif Image: + for i, val in enumerate(row): + if isinstance(val, Image.Image): + row[i] = self.transform(val) + except ValueError: + logger.warning("Skipping transform due to unsupported data types.") + self.transform = None + if self.tokenizer: + for i, val in enumerate(row): + if isinstance(val, str) or ( + isinstance(val, list) and isinstance(val[0], str) + ): + row[i] = convert_text( + val, self.tokenizer, self.tokenizer_kwargs + ).squeeze(0) # type: ignore[union-attr] + yield row + + @staticmethod + def get_rank_and_workers() -> tuple[int, int]: + """Get combined rank and number of workers across all nodes.""" + try: + world_rank = get_rank() + world_size = get_world_size() + except (RuntimeError, ValueError): + world_rank = 0 + world_size = 1 + worker_info = get_worker_info() + if worker_info: + worker_rank = worker_info.id + num_workers = worker_info.num_workers + else: + worker_rank = 0 + num_workers = 1 + total_workers = world_size * num_workers + total_rank = world_rank * num_workers + worker_rank + return total_rank, total_workers diff --git a/src/datachain/lib/settings.py b/src/datachain/lib/settings.py new file mode 100644 index 000000000..b76b69bed --- /dev/null +++ b/src/datachain/lib/settings.py @@ -0,0 +1,84 @@ +from datachain.lib.utils import DataChainParamsError + + +class SettingsError(DataChainParamsError): + def __init__(self, msg): + super().__init__(f"Dataset settings error: {msg}") + + +class Settings: + def __init__( + self, cache=None, batch=None, parallel=None, workers=None, min_task_size=None + ): + self._cache = cache + self._batch = batch + self.parallel = parallel + self._workers = workers + self.min_task_size = min_task_size + + if not isinstance(cache, bool) and cache is not None: + raise SettingsError( + "'cache' argument must be bool" + f" while {cache.__class__.__name__} was given" + ) + + if not isinstance(batch, int) and batch is not None: + raise SettingsError( + "'batch' argument must be int or None" + f" while {batch.__class__.__name__} was given" + ) + + if not isinstance(parallel, int) and parallel is not None: + raise SettingsError( + "'parallel' argument must be int or None" + f" while {parallel.__class__.__name__} was given" + ) + + if ( + not isinstance(workers, bool) + and not isinstance(workers, int) + and workers is not None + ): + raise SettingsError( + "'workers' argument must be int or bool" + f" while {workers.__class__.__name__} was given" + ) + + if min_task_size is not None and not isinstance(min_task_size, int): + raise SettingsError( + "'min_task_size' argument must be int or None" + f", {min_task_size.__class__.__name__} was given" + ) + + @property + def cache(self): + return self._cache if self._cache is not None else False + + @property + def batch(self): + return self._batch if self._batch is not None else 1 + + @property + def workers(self): + return self._workers if self._workers is not None else False + + def to_dict(self): + res = {} + if self._cache is not None: + res["cache"] = self.cache + if self._batch is not None: + res["batch"] = self.batch + if self.parallel is not None: + res["parallel"] = self.parallel + if self._workers is not None: + res["workers"] = self.workers + if self.min_task_size is not None: + res["min_task_size"] = self.min_task_size + return res + + def add(self, settings: "Settings"): + self._cache = settings._cache or self._cache + self._batch = settings._batch or self._batch + self.parallel = settings.parallel or self.parallel + self._workers = settings._workers or self._workers + self.min_task_size = settings.min_task_size or self.min_task_size diff --git a/src/datachain/lib/signal_schema.py b/src/datachain/lib/signal_schema.py new file mode 100644 index 000000000..1d3670576 --- /dev/null +++ b/src/datachain/lib/signal_schema.py @@ -0,0 +1,331 @@ +import copy +from collections.abc import Iterator, Sequence +from datetime import datetime +from typing import TYPE_CHECKING, Any, Callable, Optional, Union, get_args, get_origin + +from pydantic import create_model + +from datachain.lib.feature import ( + DATACHAIN_TO_TYPE, + DEFAULT_DELIMITER, + Feature, + FeatureType, + convert_type_to_datachain, +) +from datachain.lib.feature_registry import Registry +from datachain.lib.file import File +from datachain.lib.utils import DataChainParamsError + +if TYPE_CHECKING: + from datachain.catalog import Catalog + + +NAMES_TO_TYPES = { + "int": int, + "str": str, + "float": float, + "bool": bool, + "list": list, + "dict": dict, + "bytes": bytes, + "datetime": datetime, +} + + +class SignalSchemaError(DataChainParamsError): + pass + + +class SignalResolvingError(SignalSchemaError): + def __init__(self, path: Optional[list[str]], msg: str): + name = " '" + ".".join(path) + "'" if path else "" + super().__init__(f"cannot resolve signal name{name}: {msg}") + + +class SetupError(SignalSchemaError): + def __init__(self, name: str, msg: str): + super().__init__(f"cannot setup value '{name}': {msg}") + + +class SignalResolvingTypeError(SignalResolvingError): + def __init__(self, method: str, field): + super().__init__( + None, + f"{method} supports only `str` type" + f" while '{field}' has type '{type(field)}'", + ) + + +class SignalSchema: + def __init__( + self, + values: dict[str, FeatureType], + setup: Optional[dict[str, Callable]] = None, + ): + self.values = values + self.tree = self._build_tree(values) + + self.setup_func = setup or {} + self.setup_values = None + for key, func in self.setup_func.items(): + if not callable(func): + raise SetupError(key, "value must be function or callable class") + + def _init_setup_values(self): + if self.setup_values is not None: + return self.setup_values + + res = {} + for key, func in self.setup_func.items(): + try: + res[key] = func() + except Exception as ex: + raise SetupError(key, f"error when call function: '{ex}'") from ex + self.setup_values = res + + @staticmethod + def from_column_types(col_types: dict[str, Any]) -> "SignalSchema": + signals: dict[str, FeatureType] = {} + for field, type_ in col_types.items(): + type_ = DATACHAIN_TO_TYPE.get(type_, None) + if type_ is None: + raise SignalSchemaError( + f"signal schema cannot be obtained for column '{field}':" + f" unsupported type '{type_}'" + ) + signals[field] = type_ + return SignalSchema(signals) + + def serialize(self) -> dict[str, str]: + signals = {} + for name, fr_type in self.values.items(): + if Feature.is_feature(fr_type): + signals[name] = fr_type._name() # type: ignore[union-attr] + else: + orig = get_origin(fr_type) + args = get_args(fr_type) + # Check if fr_type is Optional + if orig == Union and len(args) == 2 and (type(None) in args): + fr_type = args[0] + signals[name] = fr_type.__name__ + return signals + + @staticmethod + def deserialize(schema: dict[str, str]) -> "SignalSchema": + if not isinstance(schema, dict): + raise SignalSchemaError(f"cannot deserialize signal schema: {schema}") + + signals: dict[str, FeatureType] = {} + for signal, type_name in schema.items(): + try: + fr = NAMES_TO_TYPES.get(type_name) + if not fr: + type_name, version = Registry.parse_name_version(type_name) + fr = Registry.get(type_name, version) + except TypeError as err: + raise SignalSchemaError( + f"cannot deserialize '{signal}': {err}" + ) from err + + if not fr: + raise SignalSchemaError( + f"cannot deserialize '{signal}': unsupported type '{type_name}'" + ) + signals[signal] = fr + + return SignalSchema(signals) + + def to_udf_spec(self) -> dict[str, Any]: + res = {} + for path, type_, has_subtree, _ in self.get_flat_tree(): + if path[0] in self.setup_func: + continue + if not has_subtree: + db_name = DEFAULT_DELIMITER.join(path) + res[db_name] = convert_type_to_datachain(type_) + return res + + def row_to_objs(self, row: Sequence[Any]) -> list[FeatureType]: + self._init_setup_values() + + objs = [] + pos = 0 + for name, fr_type in self.values.items(): + if val := self.setup_values.get(name, None): # type: ignore[attr-defined] + objs.append(val) + elif Feature.is_feature(fr_type): + j, pos = fr_type._unflatten_to_json_pos(row, pos) # type: ignore[union-attr] + objs.append(fr_type(**j)) + else: + objs.append(row[pos]) + pos += 1 + return objs # type: ignore[return-value] + + def contains_file(self) -> bool: + return any( + fr._is_file # type: ignore[union-attr] + for fr in self.values.values() + if Feature.is_feature(fr) + ) + + def slice( + self, keys: Sequence[str], setup: Optional[dict[str, Callable]] = None + ) -> "SignalSchema": + setup = setup or {} + setup_no_types = dict.fromkeys(setup.keys(), str) + union = self.values | setup_no_types + schema = {k: union[k] for k in keys if k in union} + return SignalSchema(schema, setup) + + def row_to_features(self, row: Sequence, catalog: "Catalog") -> list[FeatureType]: + res = [] + pos = 0 + for fr_cls in self.values.values(): + if not Feature.is_feature(fr_cls): + res.append(row[pos]) + pos += 1 + else: + json, pos = fr_cls._unflatten_to_json_pos(row, pos) # type: ignore[union-attr] + obj = fr_cls(**json) + if isinstance(obj, File): + obj._set_stream(catalog) + res.append(obj) + return res + + def db_signals(self) -> list[str]: + return [ + DEFAULT_DELIMITER.join(path) + for path, _, has_subtree, _ in self.get_flat_tree() + if not has_subtree + ] + + def resolve(self, *names: str) -> "SignalSchema": + schema = {} + for field in names: + if not isinstance(field, str): + raise SignalResolvingTypeError("select()", field) + schema[field] = self._find_in_tree(field.split(".")) + + return SignalSchema(schema) + + def _find_in_tree(self, path: list[str]) -> FeatureType: + curr_tree = self.tree + curr_type = None + i = 0 + while curr_tree is not None and i < len(path): + if val := curr_tree.get(path[i], None): + curr_type, curr_tree = val + else: + curr_type = None + i += 1 + + if curr_type is None: + raise SignalResolvingError(path, "is not found") + + return curr_type + + def select_except_signals(self, *args: str) -> "SignalSchema": + schema = copy.deepcopy(self.values) + for field in args: + if not isinstance(field, str): + raise SignalResolvingTypeError("select_except()", field) + + if field not in self.values: + raise SignalResolvingError( + field.split("."), + "select_except() error - the feature name does not exist or " + "inside of feature (not supported)", + ) + del schema[field] + + return SignalSchema(schema) + + def clone_without_file_signals(self) -> "SignalSchema": + schema = copy.deepcopy(self.values) + + for signal in File._datachain_column_types: + if signal in schema: + del schema[signal] + return SignalSchema(schema) + + def merge( + self, + right_schema: "SignalSchema", + rname: str, + ) -> "SignalSchema": + schema_right = { + rname + key if key in self.values else key: type_ + for key, type_ in right_schema.values.items() + } + + return SignalSchema(self.values | schema_right) + + def get_file_signals(self) -> Iterator[str]: + for path, type_, has_subtree, _ in self.get_flat_tree(): + if has_subtree and issubclass(type_, File): + yield ".".join(path) + + def create_model(self, name: str) -> type[Feature]: + fields = {key: (value, None) for key, value in self.values.items()} + + return create_model( + name, + __base__=(Feature,), # type: ignore[call-overload] + **fields, + ) + + @staticmethod + def _build_tree(values: dict[str, FeatureType]) -> dict[str, Any]: + res = {} + + for name, val in values.items(): + subtree = val.build_tree() if Feature.is_feature(val) else None # type: ignore[union-attr] + res[name] = (val, subtree) + + return res + + def get_flat_tree(self) -> Iterator[tuple[list[str], type, bool, int]]: + yield from self._get_flat_tree(self.tree, [], 0) + + def _get_flat_tree( + self, tree: dict, prefix: list[str], depth: int + ) -> Iterator[tuple[list[str], type, bool, int]]: + for name, (type_, substree) in tree.items(): + suffix = name.split(".") + new_prefix = prefix + suffix + has_subtree = substree is not None + yield new_prefix, type_, has_subtree, depth + if substree is not None: + yield from self._get_flat_tree(substree, new_prefix, depth + 1) + + def print_tree(self, indent: int = 4, start_at: int = 0): + for path, type_, _, depth in self.get_flat_tree(): + total_indent = start_at + depth * indent + print(" " * total_indent, f"{path[-1]}:", SignalSchema._type_to_str(type_)) + + if get_origin(type_) is list: + args = get_args(type_) + if len(args) > 0 and Feature.is_feature(args[0]): + sub_schema = SignalSchema({"* list of": args[0]}) + sub_schema.print_tree(indent=indent, start_at=total_indent + indent) + + @staticmethod + def _type_to_str(type_): + if get_origin(type_) == Union: + args = get_args(type_) + formatted_types = ", ".join(SignalSchema._type_to_str(arg) for arg in args) + return f"Union[{formatted_types}]" + if get_origin(type_) == Optional: + args = get_args(type_) + type_str = SignalSchema._type_to_str(args[0]) + return f"Optional[{type_str}]" + if get_origin(type_) is list: + args = get_args(type_) + type_str = SignalSchema._type_to_str(args[0]) + return f"list[{type_str}]" + if get_origin(type_) is dict: + args = get_args(type_) + type_str = SignalSchema._type_to_str(args[0]) + vals = f", {SignalSchema._type_to_str(args[1])}" if len(args) > 1 else "" + return f"dict[{type_str}{vals}]" + return type_.__name__ diff --git a/src/datachain/lib/text.py b/src/datachain/lib/text.py new file mode 100644 index 000000000..4e4f124f4 --- /dev/null +++ b/src/datachain/lib/text.py @@ -0,0 +1,49 @@ +from typing import TYPE_CHECKING, Any, Callable, Optional, Union + +if TYPE_CHECKING: + import torch + + +def convert_text( + text: Union[str, list[str]], + tokenizer: Optional[Callable] = None, + tokenizer_kwargs: Optional[dict[str, Any]] = None, + encoder: Optional[Callable] = None, +) -> Union[str, list[str], "torch.Tensor"]: + """ + Tokenize and otherwise transform text. + + Args: + text (str): Text to convert. + tokenizer (Callable): Tokenizer to use to tokenize objects. + tokenizer_kwargs (dict): Additional kwargs to pass when calling tokenizer. + encoder (Callable): Encode text using model. + """ + if not tokenizer: + return text + + if isinstance(text, str): + text = [text] + + if tokenizer_kwargs: + res = tokenizer(text, **tokenizer_kwargs) + else: + res = tokenizer(text) + try: + from transformers.tokenization_utils_base import PreTrainedTokenizerBase + + tokens = ( + res.input_ids if isinstance(tokenizer, PreTrainedTokenizerBase) else res + ) + except ImportError: + tokens = res + + if not encoder: + return tokens + + try: + import torch + except ImportError: + "Missing dependency 'torch' needed to encode text." + + return encoder(torch.tensor(tokens)) diff --git a/src/datachain/lib/udf.py b/src/datachain/lib/udf.py new file mode 100644 index 000000000..38c4d6023 --- /dev/null +++ b/src/datachain/lib/udf.py @@ -0,0 +1,214 @@ +import inspect +import sys +import traceback +from typing import TYPE_CHECKING, Callable + +from datachain.lib.feature import Feature +from datachain.lib.signal_schema import SignalSchema +from datachain.lib.udf_signature import UdfSignature +from datachain.lib.utils import AbstractUDF, DataChainError, DataChainParamsError +from datachain.query import udf + +if TYPE_CHECKING: + from datachain.query.udf import UDFWrapper + + +class UdfError(DataChainParamsError): + def __init__(self, msg): + super().__init__(f"UDF error: {msg}") + + +class UDFBase(AbstractUDF): + is_input_batched = False + is_output_batched = False + is_input_grouped = False + + def __init__(self): + self.params = None + self.output = None + self.params_spec = None + self.output_spec = None + self._contains_stream = None + self._catalog = None + self._func = None + + def process(self, *args, **kwargs): + """Processing function that needs to be defined by user""" + if not self._func: + raise NotImplementedError("UDF processing is not implemented") + return self._func(*args, **kwargs) + + def setup(self): + """Initialization process executed on each worker before processing begins. + This is needed for tasks like pre-loading ML models prior to scoring. + """ + + def teardown(self): + """Teardown process executed on each process/worker after processing ends. + This is needed for tasks like closing connections to end-points. + """ + + def _init(self, sign: UdfSignature, params: SignalSchema, func: Callable): + self.params = params + self.output = sign.output_schema + + params_spec = self.params.to_udf_spec() + self.params_spec = list(params_spec.keys()) + self.output_spec = self.output.to_udf_spec() + + self._func = func + + @classmethod + def _create( + cls, + target_class: type["UDFBase"], + sign: UdfSignature, + params: SignalSchema, + ) -> "UDFBase": + if isinstance(sign.func, AbstractUDF): + if not isinstance(sign.func, target_class): # type: ignore[unreachable] + raise UdfError( + f"cannot create UDF: provided UDF '{sign.func.__name__}'" + f" must be a child of target class '{target_class.__name__}'", + ) + result = sign.func + func = None + else: + result = target_class() + func = sign.func + + result._init(sign, params, func) + return result + + @property + def name(self): + return self.__class__.__name__ + + def set_catalog(self, catalog): + self._catalog = catalog.copy(db=False) + + @property + def catalog(self): + return self._catalog + + def to_udf_wrapper(self, batch=1) -> "UDFWrapper": + udf_wrapper = udf(self.params_spec, self.output_spec, batch=batch) + return udf_wrapper(self) + + def validate_results(self, results, *args, **kwargs): + return results + + def __call__(self, *rows): + if self.is_input_grouped: + objs = self._parse_grouped_rows(rows) + else: + objs = self._parse_rows(rows) + + if not self.is_input_batched: + objs = objs[0] + + result_objs = self.process_safe(objs) + + if not self.is_output_batched: + result_objs = [result_objs] + + if len(self.output.values) > 1: + res = [] + for tuple_ in result_objs: + flat = [] + for obj in tuple_: + if isinstance(obj, Feature): + flat.extend(Feature._flatten(obj)) + else: + flat.append(obj) + res.append(flat) + else: + # Generator expression is required, otherwise the value will be materialized + res = ( + obj._flatten() if isinstance(obj, Feature) else (obj,) + for obj in result_objs + ) + + if not self.is_output_batched: + res = list(res) + assert len(res) == 1, ( + f"{self.name} returns {len(res)} " f"rows while it's not batched" + ) + if isinstance(res[0], tuple): + res = res[0] + + return res + + def _parse_rows(self, rows): + if not self.is_input_batched: + rows = [rows] + objs = [] + for row in rows: + obj_row = self.params.row_to_objs(row) + for obj in obj_row: + if isinstance(obj, Feature): + obj._set_stream(self._catalog, caching_enabled=True) + objs.append(obj_row) + return objs + + def _parse_grouped_rows(self, rows): + group = rows[0] + spec_map = {} + output_map = {} + for name, (anno, subtree) in self.params.tree.items(): + if inspect.isclass(anno) and issubclass(anno, Feature): + length = sum(1 for _ in self.params._get_flat_tree(subtree, [], 0)) + else: + length = 1 + spec_map[name] = anno, length + output_map[name] = [] + + for flat_obj in group: + position = 0 + for signal, (cls, length) in spec_map.items(): + slice = flat_obj[position : position + length] + position += length + + if Feature.is_feature(cls): + obj = cls(**cls._unflatten_to_json(slice)) + else: + obj = slice[0] + + if isinstance(obj, Feature): + obj._set_stream(self._catalog) + output_map[signal].append(obj) + + return list(output_map.values()) + + def process_safe(self, obj_rows): + try: + result_objs = self.process(*obj_rows) + except Exception as e: # noqa: BLE001 + msg = f"============== Error in user code: '{self.name}' ==============" + print(msg) + exc_type, exc_value, exc_traceback = sys.exc_info() + traceback.print_exception(exc_type, exc_value, exc_traceback.tb_next) + print("=" * len(msg)) + raise DataChainError( + f"Error in user code in class '{self.name}': {e!s}" + ) from None + return result_objs + + +class Mapper(UDFBase): + pass + + +class BatchMapper(Mapper): + is_input_batched = True + is_output_batched = True + + +class Generator(UDFBase): + is_output_batched = True + + +class Aggregator(UDFBase): + is_input_batched = True + is_output_batched = True + is_input_grouped = True diff --git a/src/datachain/lib/udf_signature.py b/src/datachain/lib/udf_signature.py new file mode 100644 index 000000000..389e970a7 --- /dev/null +++ b/src/datachain/lib/udf_signature.py @@ -0,0 +1,196 @@ +import inspect +from collections.abc import Generator, Iterator, Sequence +from dataclasses import dataclass +from typing import Callable, Optional, Union, get_args, get_origin + +from datachain.lib.feature import Feature, FeatureType, FeatureTypeNames +from datachain.lib.signal_schema import SignalSchema +from datachain.lib.utils import AbstractUDF, DataChainParamsError + + +class UdfSignatureError(DataChainParamsError): + def __init__(self, chain: str, msg): + suffix = f"(dataset '{chain}')" if chain else "" + super().__init__(f"processor signature error{suffix}: {msg}") + + +@dataclass +class UdfSignature: + func: Callable + params: Sequence[str] + output_schema: SignalSchema + + DEFAULT_RETURN_TYPE = str + + @classmethod + def parse( + cls, + chain: str, + signal_map: dict[str, Callable], + func: Optional[Callable] = None, + params: Union[None, str, Sequence[str]] = None, + output: Union[None, FeatureType, Sequence[str], dict[str, FeatureType]] = None, + is_generator: bool = True, + ) -> "UdfSignature": + keys = ", ".join(signal_map.keys()) + if len(signal_map) > 1: + raise UdfSignatureError( + chain, + f"multiple signals '{keys}' are not supported in processors." + " Chain multiple processors instead.", + ) + if len(signal_map) == 1: + if func is not None: + raise UdfSignatureError( + chain, + f"processor can't have signal '{keys}' with function '{func}'", + ) + signal_name, udf_func = next(iter(signal_map.items())) + else: + if func is None: + raise UdfSignatureError(chain, "user function is not defined") + + udf_func = func + signal_name = None + + if not callable(udf_func): + raise UdfSignatureError(chain, f"UDF '{udf_func}' is not callable") + + func_params_map_sign, func_outs_sign, is_iterator = ( + UdfSignature._func_signature(chain, udf_func) + ) + if params: + udf_params = [params] if isinstance(params, str) else params + elif not func_params_map_sign: + udf_params = [] + else: + udf_params = list(func_params_map_sign.keys()) + if output: + udf_output_map = UdfSignature._validate_output( + chain, signal_name, func, func_outs_sign, output + ) + else: + if not func_outs_sign: + raise UdfSignatureError( + chain, + f"outputs are not defined in function '{udf_func.__name__}'" + " hints or 'output'", + ) + + if not signal_name: + raise UdfSignatureError( + chain, + "signal name is not specified." + " Define it as signal name 's1=func() or in 'output'", + ) + + if is_generator and not is_iterator: + raise UdfSignatureError( + chain, + f"function '{func}' cannot be used in generator/aggregator" + " because it returns a type that is not Iterator/Generator." + f" Instead, it returns '{func_outs_sign}'", + ) + + if isinstance(func_outs_sign, tuple): + udf_output_map = { + signal_name + f"_{num}": typ + for num, typ in enumerate(func_outs_sign) + } + else: + udf_output_map = {signal_name: func_outs_sign[0]} + + return cls( + func=udf_func, + params=udf_params, + output_schema=SignalSchema(udf_output_map), + ) + + @staticmethod + def _validate_output(chain, signal_name, func, func_outs_sign, output): + if isinstance(output, str): + output = [output] + if isinstance(output, Sequence): + if len(func_outs_sign) != len(output): + raise UdfSignatureError( + chain, + f"length of outputs names ({len(output)}) and function '{func}'" + f" return type length ({len(func_outs_sign)}) does not match", + ) + + udf_output_map = dict(zip(output, func_outs_sign)) + elif isinstance(output, dict): + for key, value in output.items(): + if not isinstance(key, str): + raise UdfSignatureError( + chain, + f"output signal '{key}' has type '{type(key)}'" + " while 'str' is expected", + ) + if not Feature.is_feature_type(value): + raise UdfSignatureError( + chain, + f"output type '{value.__name__}' of signal '{key}' is not" + f" supported. Please use Feature types: {FeatureTypeNames}", + ) + + udf_output_map = output + elif Feature.is_feature_type(output): + udf_output_map = {signal_name: output} + else: + raise UdfSignatureError( + chain, + f"unknown output type: {output}. List of signals or dict of signals" + " to function are expected.", + ) + return udf_output_map + + def __eq__(self, other) -> bool: + return ( + self.func == other.func + and self.params == other.params + and self.output_schema.values == other.output_schema.values + ) + + @staticmethod + def _func_signature( + chain: str, udf_func: Callable + ) -> tuple[dict[str, type], Sequence[type], bool]: + if isinstance(udf_func, AbstractUDF): + func = udf_func.process # type: ignore[unreachable] + else: + func = udf_func + + sign = inspect.signature(func) + + input_map = {prm.name: prm.annotation for prm in sign.parameters.values()} + is_iterator = False + + anno = sign.return_annotation + if anno == inspect.Signature.empty: + output_types: list[type] = [] + else: + orig = get_origin(anno) + if inspect.isclass(orig) and issubclass(orig, Iterator): + args = get_args(anno) + if len(args) > 1 and not ( + issubclass(orig, Generator) and len(args) == 3 + ): + raise UdfSignatureError( + chain, + f"function '{func}' should return iterator with a single" + f" value while '{args}' are specified", + ) + is_iterator = True + anno = args[0] + orig = get_origin(anno) + + if orig and orig is tuple: + output_types = tuple(get_args(anno)) # type: ignore[assignment] + else: + output_types = [anno] + + if not output_types: + output_types = [UdfSignature.DEFAULT_RETURN_TYPE] + + return input_map, output_types, is_iterator diff --git a/src/datachain/lib/unstructured.py b/src/datachain/lib/unstructured.py new file mode 100644 index 000000000..33ddb0358 --- /dev/null +++ b/src/datachain/lib/unstructured.py @@ -0,0 +1,41 @@ +import shutil +import tempfile + +from unstructured.partition.auto import partition +from unstructured.staging.base import convert_to_dataframe + +from datachain.lib.udf import Mapper +from datachain.query import Stream +from datachain.sql.types import JSON, String + + +class PartitionObject(Mapper): + def __init__(self): + super().__init__( + [ + Stream(), + ], + { + "elements": JSON, + "title": String, + "text": String, + "error": String, + }, + ) + + def encode_object(self, raw): + fname = str(raw).replace(">", "").replace("<", "") + output = tempfile.TemporaryFile() + shutil.copyfileobj(raw, output) + elements = partition(file=output, metadata_filename=fname) + output.close() + return elements + + def __call__(self, stream): + with stream: + elements = self.encode_object(stream) + + title = str(elements[0]) + text = "\n\n".join([str(el) for el in elements]) + df = convert_to_dataframe(elements) + return (df.to_json(), title, text, "") diff --git a/src/datachain/lib/utils.py b/src/datachain/lib/utils.py new file mode 100644 index 000000000..5b653265d --- /dev/null +++ b/src/datachain/lib/utils.py @@ -0,0 +1,25 @@ +from abc import ABC, abstractmethod + + +class AbstractUDF(ABC): + @abstractmethod + def process(self, *args, **kwargs): + pass + + @abstractmethod + def setup(self): + pass + + @abstractmethod + def teardown(self): + pass + + +class DataChainError(Exception): + def __init__(self, message): + super().__init__(message) + + +class DataChainParamsError(DataChainError): + def __init__(self, message): + super().__init__(message) diff --git a/src/datachain/lib/vfile.py b/src/datachain/lib/vfile.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/datachain/lib/webdataset.py b/src/datachain/lib/webdataset.py new file mode 100644 index 000000000..f1c9b7fbc --- /dev/null +++ b/src/datachain/lib/webdataset.py @@ -0,0 +1,264 @@ +import hashlib +import json +import tarfile +from collections.abc import Iterator, Sequence +from pathlib import Path +from typing import ( + Any, + Callable, + ClassVar, + Optional, + Union, + get_args, + get_origin, +) + +from pydantic import Field + +from datachain.lib.feature import Feature +from datachain.lib.file import File, TarVFile +from datachain.lib.utils import DataChainError + + +class WDSError(DataChainError): + def __init__(self, tar_stream, message: str): + super().__init__(f"WebDataset error '{tar_stream.get_full_name()}': {message}") + + +class CoreFileDuplicationError(WDSError): + def __init__(self, tar_stream, file1: str, file2: str): + super().__init__( + tar_stream, f"duplication of files with core extensions: {file1}, {file2}" + ) + + +class CoreFileNotFoundError(WDSError): + def __init__(self, tar_stream, extensions, stem): + super().__init__( + tar_stream, + f"no files with the extensions '{','.join(extensions)}'" + f" were found for file stem {stem}", + ) + + +class UnknownFileExtensionError(WDSError): + def __init__(self, tar_stream, name, ext): + super().__init__(tar_stream, f"unknown extension '{ext}' for file '{name}'") + + +class WDSBasic(Feature): + file: File + + +class WDSAllFile(WDSBasic): + txt: Optional[str] = Field(default=None) + text: Optional[str] = Field(default=None) + cap: Optional[str] = Field(default=None) + transcript: Optional[str] = Field(default=None) + cls: Optional[int] = Field(default=None) + cls2: Optional[int] = Field(default=None) + index: Optional[int] = Field(default=None) + inx: Optional[int] = Field(default=None) + id: Optional[int] = Field(default=None) + json: Optional[dict] = Field(default=None) # type: ignore[assignment] + jsn: Optional[dict] = Field(default=None) + + pyd: Optional[bytes] = Field(default=None) + pickle: Optional[bytes] = Field(default=None) + pth: Optional[bytes] = Field(default=None) + ten: Optional[bytes] = Field(default=None) + tb: Optional[bytes] = Field(default=None) + mp: Optional[bytes] = Field(default=None) + msg: Optional[bytes] = Field(default=None) + npy: Optional[bytes] = Field(default=None) + npz: Optional[bytes] = Field(default=None) + cbor: Optional[bytes] = Field(default=None) + + +class WDSReadableSubclass(Feature): + @staticmethod + def _reader(builder, item: tarfile.TarInfo) -> "WDSReadableSubclass": + raise NotImplementedError + + +class BuilderState: + def __init__(self): + self.stem = None + self.core_file = None + self.data = {} + + +class Builder: + DEFAULT_TYPES_READERS: ClassVar[dict[type, Any]] = { + str: lambda bld, item: bld.read_text(item), + int: lambda bld, item: int(bld.read_text(item)), + float: lambda bld, item: float(bld.read_text(item)), + bytes: lambda bld, item: bld.read(item), + dict: lambda bld, item: json.loads(bld.read_text(item)), + } + + def __init__( + self, + tar_stream: File, + core_extensions: list[str], + wds_class: type[WDSBasic], + tar, + encoding="utf-8", + ): + self._core_extensions = core_extensions + self._tar_stream = tar_stream + self._wds_class = wds_class + self._tar = tar + self._encoding = encoding + self.state = BuilderState() + + def read(self, item): + return self._tar.extractfile(item).read() + + def read_text(self, item): + return self._tar.extractfile(item).read().decode(self._encoding) + + def add(self, file: tarfile.TarInfo): + fstream = File(name=file.name) + ext = fstream.get_file_ext() + stem = fstream.get_file_stem() + + if self.state.stem is not None and self.state.stem != stem: + raise StopIteration + + if self.state.stem is None: + self.state.stem = stem + + if ext in self._core_extensions: + if self.state.core_file is not None: + raise CoreFileDuplicationError( + self._tar_stream, file.name, self.state.core_file.name + ) + self.state.core_file = file + elif ext in self.state.data: + raise WDSError( + self._tar_stream, + f"file with extension '.{ext}' already exists in the archive", + ) + else: + type_ = self._get_type(ext) + if type_ is None: + raise UnknownFileExtensionError(self._tar_stream, fstream.name, ext) + + if issubclass(type_, WDSReadableSubclass): + reader = type_._reader + else: + reader = self.DEFAULT_TYPES_READERS.get(type_, None) + + if reader is None: + raise WDSError( + self._tar_stream, + f"unable to find a reader for type {type_}, extension .{ext}", + ) + self.state.data[ext] = reader(self, file) + + def produce(self): + if self.state.core_file is None: + raise CoreFileNotFoundError( + self._tar_stream, self._core_extensions, self.state.stem + ) + + file = self.build_file_record() + wds = self._wds_class(**self.state.data | {"file": file}) + self.state = BuilderState() + return wds + + def build_file_record(self): + new_parent = self._tar_stream.get_full_name() + core_file = self.state.core_file + etag_string = "-".join( + [self._tar_stream.etag, core_file.name, str(core_file.mtime)] + ) + etag = hashlib.md5(etag_string.encode(), usedforsecurity=False).hexdigest() + return File( + name=core_file.name, + source=self._tar_stream.source, + parent=new_parent, + version=self._tar_stream.version, + size=core_file.size, + etag=etag, + location=[ + { + "vtype": TarVFile.get_vtype(), + "parent": self._tar_stream.model_dump_custom(), + "size": core_file.size, + "offset": core_file.offset_data, + } + ], + ) + + def _get_type(self, ext): + field = self._wds_class.model_fields.get(ext, None) + if field is None: + return + + anno = field.annotation + if get_origin(anno) == Union: + args = get_args(anno) + anno = args[0] + + return anno + + +class TarStream(File): + @staticmethod + def to_text(data): + return data.decode("utf-8") + + _DATA_CONVERTERS: ClassVar[dict[type, Any]] = { + str: lambda data: TarStream.to_text(data), + int: lambda data: int(TarStream.to_text(data)), + float: lambda data: float(TarStream.to_text(data)), + bytes: lambda data: data, + dict: lambda data: json.loads(TarStream.to_text(data)), + } + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._tar = None + + def open(self): + self._tar = tarfile.open(fileobj=super().open()) + return self + + def getmembers(self) -> list[tarfile.TarInfo]: + return self._tar.getmembers() + + def read_member(self, member: tarfile.TarInfo, type): + fd = self._tar.extractfile(member) + data = fd.read() + converter = self._DATA_CONVERTERS.get(type, None) + if not converter: + raise ValueError("") + return converter(data) + + +def get_tar_groups(stream, tar, core_extensions, spec, encoding="utf-8"): + builder = Builder(stream, core_extensions, spec, tar, encoding) + + for item in sorted(tar.getmembers(), key=lambda m: Path(m.name).stem): + if not item.isfile(): + continue + try: + builder.add(item) + except StopIteration: + yield builder.produce() + builder.add(item) + if builder.state.stem is not None: + yield builder.produce() + + +def process_webdataset( + core_extensions: Sequence[str] = ("jpg", "png"), spec=WDSAllFile, encoding="utf-8" +) -> Callable: + def wds_func(file: File) -> Iterator[spec]: + with file.open() as fd: + with tarfile.open(fileobj=fd) as tar: + yield from get_tar_groups(file, tar, core_extensions, spec, encoding) + + return wds_func diff --git a/src/datachain/lib/webdataset_laion.py b/src/datachain/lib/webdataset_laion.py new file mode 100644 index 000000000..6106670a8 --- /dev/null +++ b/src/datachain/lib/webdataset_laion.py @@ -0,0 +1,65 @@ +from collections.abc import Iterator +from typing import Optional + +import numpy as np +from pydantic import Field + +from datachain.lib.feature import Feature +from datachain.lib.file import File +from datachain.lib.webdataset import WDSBasic, WDSReadableSubclass + + +class Laion(WDSReadableSubclass): + uid: str = Field(default="") + face_bboxes: Optional[list[list[float]]] = Field(default=None) + caption: Optional[str] = Field(default=None) + url: Optional[str] = Field(default=None) + key: Optional[str] = Field(default=None) + status: Optional[str] = Field(default=None) + error_message: Optional[str] = Field(default=None) + width: Optional[int] = Field(default=None) + height: Optional[int] = Field(default=None) + original_width: Optional[int] = Field(default=None) + original_height: Optional[int] = Field(default=None) + exif: Optional[str] = Field(default=None) + sha256: Optional[str] = Field(default=None) + + @staticmethod + def _reader(builder, item): + return Laion.model_validate_json(builder.read_text(item)) + + +class WDSLaion(WDSBasic): + txt: Optional[str] = Field(default=None) + json: Laion # type: ignore[assignment] + + +class LaionMeta(Feature): + file: File + index: Optional[int] = Field(default=None) + b32_img: list[float] = Field(default=None) + b32_txt: list[float] = Field(default=None) + l14_img: list[float] = Field(default=None) + l14_txt: list[float] = Field(default=None) + dedup: list[float] = Field(default=None) + + +def process_laion_meta(file: File) -> Iterator[LaionMeta]: + with file.open() as fd_npz: + npz_file = np.load(fd_npz) + b32_img = npz_file["b32_img"] + b32_txt = npz_file["b32_txt"] + l14_img = npz_file["l14_img"] + l14_txt = npz_file["l14_txt"] + dedup = npz_file["dedup"] + + for index in range(len(b32_img)): + yield LaionMeta( + file=file, + index=index, + b32_img=b32_img[index], + b32_txt=b32_txt[index], + l14_img=l14_img[index], + l14_txt=l14_txt[index], + dedup=dedup[index], + ) diff --git a/src/datachain/listing.py b/src/datachain/listing.py new file mode 100644 index 000000000..1fe62bbb2 --- /dev/null +++ b/src/datachain/listing.py @@ -0,0 +1,248 @@ +import glob +import os +from collections.abc import Iterable, Iterator +from itertools import zip_longest +from typing import TYPE_CHECKING, Optional + +from fsspec.asyn import get_loop, sync +from sqlalchemy import Column, case +from sqlalchemy.sql import func +from tqdm import tqdm + +from datachain.node import DirType, Entry, Node, NodeWithPath +from datachain.utils import suffix_to_number + +if TYPE_CHECKING: + from datachain.catalog.datasource import DataSource + from datachain.client import Client + from datachain.data_storage import AbstractMetastore, AbstractWarehouse + from datachain.dataset import DatasetRecord + from datachain.storage import Storage + + +class Listing: + def __init__( + self, + storage: Optional["Storage"], + metastore: "AbstractMetastore", + warehouse: "AbstractWarehouse", + client: "Client", + dataset: Optional["DatasetRecord"], + ): + self.storage = storage + self.metastore = metastore + self.warehouse = warehouse + self.client = client + self.dataset = dataset # dataset representing bucket listing + + def clone(self) -> "Listing": + return self.__class__( + self.storage, + self.metastore.clone(), + self.warehouse.clone(), + self.client, + self.dataset, + ) + + @property + def id(self): + return self.storage.id + + @property + def dataset_rows(self): + return self.warehouse.dataset_rows(self.dataset, self.dataset.latest_version) + + def fetch(self, start_prefix="", method: str = "default") -> None: + sync(get_loop(), self._fetch, start_prefix, method) + + async def _fetch(self, start_prefix: str, method: str) -> None: + self = self.clone() + if start_prefix: + start_prefix = start_prefix.rstrip("/") + try: + async for entries in self.client.scandir(start_prefix, method=method): + self.insert_entries(entries) + if len(entries) > 1: + self.metastore.update_last_inserted_at() + finally: + self.insert_entries_done() + + def insert_entry(self, entry: Entry) -> None: + self.warehouse.insert_rows( + self.dataset_rows.get_table(), + self.warehouse.prepare_entries(self.client.uri, [entry]), + ) + + def insert_entries(self, entries: Iterable[Entry]) -> None: + self.warehouse.insert_rows( + self.dataset_rows.get_table(), + self.warehouse.prepare_entries(self.client.uri, entries), + ) + + def insert_entries_done(self) -> None: + self.warehouse.insert_rows_done(self.dataset_rows.get_table()) + + def expand_path(self, path, use_glob=True) -> list[Node]: + if use_glob and glob.has_magic(path): + return self.warehouse.expand_path(self.dataset_rows, path) + return [self.resolve_path(path)] + + def resolve_path(self, path) -> Node: + return self.warehouse.get_node_by_path(self.dataset_rows, path) + + def ls_path(self, node, fields): + if node.vtype == "tar" or node.dir_type == DirType.TAR_ARCHIVE: + return self.warehouse.select_node_fields_by_parent_path_tar( + self.dataset_rows, node.path, fields + ) + return self.warehouse.select_node_fields_by_parent_path( + self.dataset_rows, node.path, fields + ) + + def collect_nodes_to_instantiate( + self, + sources: Iterable["DataSource"], + copy_to_filename: Optional[str], + recursive=False, + copy_dir_contents=False, + relative_path=None, + from_edatachain=False, + from_dataset=False, + ) -> list[NodeWithPath]: + rel_path_elements = relative_path.split("/") if relative_path else [] + all_nodes: list[NodeWithPath] = [] + for src in sources: + node = src.node + if recursive and src.is_container(): + dir_path = [] + if not copy_dir_contents: + dir_path.append(node.name) + subtree_nodes = src.find(sort=["parent", "name"]) + all_nodes.extend( + NodeWithPath(n.n, path=dir_path + n.path) for n in subtree_nodes + ) + else: + node_path = [] + if from_edatachain: + for rpe, npe in zip_longest( + rel_path_elements, node.path.split("/") + ): + if rpe == npe: + continue + if npe: + node_path.append(npe) + elif copy_to_filename: + node_path = [os.path.basename(copy_to_filename)] + elif from_dataset: + node_path = [ + src.listing.client.name, + node.parent, + node.name, + ] + else: + node_path = [node.name] + all_nodes.append(NodeWithPath(node, path=node_path)) + return all_nodes + + def instantiate_nodes( + self, + all_nodes, + output, + total_files=None, + force=False, + shared_progress_bar=None, + ): + progress_bar = shared_progress_bar or tqdm( + desc=f"Instantiating '{output}'", + unit=" files", + unit_scale=True, + unit_divisor=1000, + total=total_files, + ) + + counter = 0 + for node in all_nodes: + dst = os.path.join(output, *node.path) + dst_dir = os.path.dirname(dst) + os.makedirs(dst_dir, exist_ok=True) + uid = node.n.as_uid(self.client.uri) + self.client.instantiate_object(uid, dst, progress_bar, force) + counter += 1 + if counter > 1000: + progress_bar.update(counter) + counter = 0 + + progress_bar.update(counter) + + def find( + self, + node, + fields, + names=None, + inames=None, + paths=None, + ipaths=None, + size=None, + type=None, + order_by=None, + ): + dr = self.dataset_rows + conds = [] + if names: + f = Column("name").op("GLOB") + conds.extend(f(name) for name in names) + if inames: + f = func.lower(Column("name")).op("GLOB") + conds.extend(f(iname.lower()) for iname in inames) + if paths: + node_path = case( + (Column("parent") == "", Column("name")), + else_=Column("parent") + "/" + Column("name"), + ) + f = node_path.op("GLOB") + conds.extend(f(path) for path in paths) + if ipaths: + node_path = case( + (Column("parent") == "", Column("name")), + else_=Column("parent") + "/" + Column("name"), + ) + f = func.lower(node_path).op("GLOB") + conds.extend(f(ipath.lower()) for ipath in ipaths) + + if size is not None: + size_limit = suffix_to_number(size) + if size_limit >= 0: + conds.append(Column("size") >= size_limit) + else: + conds.append(Column("size") <= -size_limit) + + return self.warehouse.find( + dr, + node, + fields, + type=type, + conds=conds, + order_by=order_by, + ) + + def du(self, node: Node, count_files: bool = False): + return self.warehouse.size(self.dataset_rows, node, count_files) + + def subtree_files(self, node: Node, sort=None): + if node.dir_type == DirType.TAR_ARCHIVE or node.vtype != "": + include_subobjects = True + else: + include_subobjects = False + + return self.warehouse.get_subtree_files( + self.dataset_rows, + node, + sort=sort, + include_subobjects=include_subobjects, + ) + + def get_dirs_by_parent_path( + self, + parent_path: str, + ) -> Iterator[Node]: + return self.warehouse.get_dirs_by_parent_path(self.dataset_rows, parent_path) diff --git a/src/datachain/node.py b/src/datachain/node.py new file mode 100644 index 000000000..7d5b7ad06 --- /dev/null +++ b/src/datachain/node.py @@ -0,0 +1,210 @@ +from datetime import datetime +from typing import TYPE_CHECKING, Any, Optional + +import attrs + +from datachain.cache import UniqueId +from datachain.storage import StorageURI +from datachain.utils import time_to_str + +if TYPE_CHECKING: + from typing_extensions import Self + + +class DirType: + FILE = 0 + DIR = 1 + TAR_ARCHIVE = 5 + + +class DirTypeGroup: + """ + Groups of DirTypes for selecting storage nodes or dataset entries. + + When filtering with FILE and DIR together or alternatively when + using SUBOBJ_FILE and SUBOBJ_DIR together, we achieve a + filesystem-compatible view of a storage location. Such a view + avoids path conflicts and could be downloaded as a directory tree. + + FILE, DIR + The respective types which appear on the indexed filesystem or + object store as a file or directory. + + SUBOBJ_FILE, SUBOBJ_DIR + The respective types that we want to consider to be a file or + directory when including subobjects which are generated from other + files. In this case, we treat tar archives as directories so tar + subobjects (TAR_FILE) can be viewed under the directory tree of + the parent tar archive. + """ + + FILE = (DirType.FILE, DirType.TAR_ARCHIVE) + DIR = (DirType.DIR,) + SUBOBJ_FILE = (DirType.FILE,) + SUBOBJ_DIR = (DirType.DIR, DirType.TAR_ARCHIVE) + + +@attrs.define +class Node: + id: int = 0 + random: int = -1 + vtype: str = "" + dir_type: Optional[int] = None + parent: str = "" + name: str = "" + etag: str = "" + version: Optional[str] = None + is_latest: bool = True + last_modified: Optional[datetime] = None + size: int = 0 + owner_name: str = "" + owner_id: str = "" + location: Optional[str] = None + source: StorageURI = StorageURI("") + + @property + def path(self) -> str: + return f"{self.parent}/{self.name}" if self.parent else self.name + + @property + def is_dir(self) -> bool: + return self.dir_type == DirType.DIR + + @property + def is_container(self) -> bool: + return self.dir_type in DirTypeGroup.SUBOBJ_DIR + + @property + def is_downloadable(self) -> bool: + return bool(not self.is_dir and self.name) + + def append_to_file(self, fd, path: str): + fd.write(f"- name: {path}\n") + fd.write(f" etag: {self.etag}\n") + version = self.version + if version: + fd.write(f" version: {self.version}\n") + fd.write(f" last_modified: '{time_to_str(self.last_modified)}'\n") + size = self.size + fd.write(f" size: {self.size}\n") + return size + + def get_metafile_data(self, path: str): + data: dict[str, Any] = { + "name": path, + "etag": self.etag, + } + version = self.version + if version: + data["version"] = version + data["last_modified"] = time_to_str(self.last_modified) + data["size"] = self.size + return data + + @property + def full_path(self) -> str: + if self.is_dir and self.path: + return self.path + "/" + return self.path + + def as_uid(self, storage: Optional[StorageURI] = None): + if storage is None: + storage = self.source + return UniqueId( + storage, + self.parent, + self.name, + self.etag, + self.size, + self.vtype, + self.location, + ) + + @classmethod + def from_dict(cls, d: dict[str, Any]) -> "Self": + kw = {f.name: d[f.name] for f in attrs.fields(cls) if f.name in d} + return cls(**kw) + + @classmethod + def from_dir(cls, parent, name, **kwargs) -> "Node": + return cls(id=-1, dir_type=DirType.DIR, parent=parent, name=name, **kwargs) + + @classmethod + def root(cls) -> "Node": + return cls(-1, dir_type=DirType.DIR) + + +@attrs.define +class Entry: + vtype: str = "" + dir_type: Optional[int] = None + parent: str = "" + name: str = "" + etag: str = "" + version: str = "" + is_latest: bool = True + last_modified: Optional[datetime] = None + size: int = 0 + owner_name: str = "" + owner_id: str = "" + location: Optional[str] = None + + @property + def is_dir(self) -> bool: + return self.dir_type == DirType.DIR + + @classmethod + def from_dir(cls, parent: str, name: str, **kwargs) -> "Entry": + return cls(dir_type=DirType.DIR, parent=parent, name=name, **kwargs) + + @classmethod + def from_file(cls, parent: str, name: str, **kwargs) -> "Entry": + return cls(dir_type=DirType.FILE, parent=parent, name=name, **kwargs) + + @classmethod + def root(cls): + return cls(dir_type=DirType.DIR) + + @property + def path(self) -> str: + return f"{self.parent}/{self.name}" if self.parent else self.name + + @property + def full_path(self) -> str: + if self.is_dir and self.path: + return self.path + "/" + return self.path + + +def get_path(parent: str, name: str): + return f"{parent}/{name}" if parent else name + + +@attrs.define +class NodeWithPath: + n: Node + path: list[str] = attrs.field(factory=list) + + def append_to_file(self, fd): + return self.n.append_to_file(fd, "/".join(self.path)) + + def get_metafile_data(self): + return self.n.get_metafile_data("/".join(self.path)) + + @property + def full_path(self) -> str: + path = "/".join(self.path) + if self.n.is_dir and path: + path += "/" + return path + + +TIME_FMT = "%Y-%m-%d %H:%M" + + +def long_line_str(name: str, timestamp: Optional[datetime], owner: str) -> str: + if timestamp is None: + time = "-" + else: + time = timestamp.strftime(TIME_FMT) + return f"{owner: <19} {time: <19} {name}" diff --git a/src/datachain/nodes_fetcher.py b/src/datachain/nodes_fetcher.py new file mode 100644 index 000000000..2326b239e --- /dev/null +++ b/src/datachain/nodes_fetcher.py @@ -0,0 +1,30 @@ +import logging + +from datachain.nodes_thread_pool import NodesThreadPool + +logger = logging.getLogger("datachain") + + +class NodesFetcher(NodesThreadPool): + def __init__(self, client, max_threads, cache): + super().__init__(max_threads) + self.client = client + self.cache = cache + + def done_task(self, done): + for task in done: + task.result() + + def do_task(self, chunk): + from fsspec import Callback + + class _CB(Callback): + def relative_update(_, inc: int = 1): # noqa: N805 + self.increase_counter(inc) + + for node in chunk: + uid = node.as_uid(self.client.uri) + if self.cache.contains(uid): + self.increase_counter(node.size) + else: + self.client.put_in_cache(uid, callback=_CB()) diff --git a/src/datachain/nodes_thread_pool.py b/src/datachain/nodes_thread_pool.py new file mode 100644 index 000000000..3c038eb72 --- /dev/null +++ b/src/datachain/nodes_thread_pool.py @@ -0,0 +1,115 @@ +import concurrent +import concurrent.futures +import threading +from abc import ABC, abstractmethod + + +class NodeChunk: + def __init__( + self, cache, storage, nodes, size_limit=10 * 1024 * 1024, file_limit=100 + ): + self.cache = cache + self.storage = storage + self.nodes = nodes + self.size_limit = size_limit + self.file_limit = file_limit + + def __iter__(self): + return self + + def next_downloadable(self): + node = next(self.nodes, None) + while node and ( + not node.is_downloadable or self.cache.contains(node.as_uid(self.storage)) + ): + node = next(self.nodes, None) + return node + + def __next__(self): + node = self.next_downloadable() + + total_size = 0 + total_files = 0 + bucket = [] + + while ( + node + and total_size + node.size < self.size_limit + and total_files + 1 < self.file_limit + ): + bucket.append(node) + total_size += node.size + total_files += 1 + node = self.next_downloadable() + + if node: + bucket.append(node) + total_size += node.size + total_files += 1 + + if bucket: + return bucket + raise StopIteration + + +class NodesThreadPool(ABC): + def __init__(self, max_threads): + self._max_threads = max_threads + self._thread_counter = 0 + self._thread_lock = threading.Lock() + + def run( + self, + chunk_gen, + progress_bar=None, + ): + results = [] + with concurrent.futures.ThreadPoolExecutor(self._max_threads) as th_pool: + tasks = set() + self._thread_counter = 0 + for chunk in chunk_gen: + while len(tasks) >= self._max_threads: + done, _ = concurrent.futures.wait( + tasks, timeout=1, return_when="FIRST_COMPLETED" + ) + self.done_task(done) + + tasks = tasks - done + self.update_progress_bar(progress_bar) + + tasks.add(th_pool.submit(self.do_task, chunk)) + self.update_progress_bar(progress_bar) + + while tasks: + done, _ = concurrent.futures.wait( + tasks, timeout=1, return_when="FIRST_COMPLETED" + ) + task_results = self.done_task(done) + if task_results: + results.extend(task_results) + + tasks = tasks - done + self.update_progress_bar(progress_bar) + + th_pool.shutdown() + + return results + + def update_progress_bar(self, progress_bar): + if progress_bar is not None: + with self._thread_lock: + if self._thread_counter: + progress_bar.update(self._thread_counter) + self._thread_counter = 0 + + def increase_counter(self, value): + with self._thread_lock: + self._thread_counter += value + + @abstractmethod + def do_task(self, chunk): + pass + + @abstractmethod + def done_task(self, done): + pass diff --git a/src/datachain/progress.py b/src/datachain/progress.py new file mode 100644 index 000000000..99ed0218c --- /dev/null +++ b/src/datachain/progress.py @@ -0,0 +1,149 @@ +"""Manages progress bars.""" + +import logging +import os +import re +import sys +from threading import RLock +from typing import Any, ClassVar + +from fsspec.callbacks import TqdmCallback +from tqdm import tqdm + +logger = logging.getLogger(__name__) +tqdm.set_lock(RLock()) + + +def env2bool(var, undefined=False): + """ + undefined: return value if env var is unset + """ + var = os.getenv(var, None) + if var is None: + return undefined + return bool(re.search("1|y|yes|true", var, flags=re.IGNORECASE)) + + +class Tqdm(tqdm): + """ + maximum-compatibility tqdm-based progressbars + """ + + BAR_FMT_DEFAULT = ( + "{percentage:3.0f}% {desc}|{bar}|" + "{postfix[info]}{n_fmt}/{total_fmt}" + " [{elapsed}<{remaining}, {rate_fmt:>11}]" + ) + # nested bars should have fixed bar widths to align nicely + BAR_FMT_DEFAULT_NESTED = ( + "{percentage:3.0f}%|{bar:10}|{desc:{ncols_desc}.{ncols_desc}}" + "{postfix[info]}{n_fmt}/{total_fmt}" + " [{elapsed}<{remaining}, {rate_fmt:>11}]" + ) + BAR_FMT_NOTOTAL = "{desc}{bar:b}|{postfix[info]}{n_fmt} [{elapsed}, {rate_fmt:>11}]" + BYTES_DEFAULTS: ClassVar[dict[str, Any]] = { + "unit": "B", + "unit_scale": True, + "unit_divisor": 1024, + "miniters": 1, + } + + def __init__( + self, + iterable=None, + disable=None, + level=logging.ERROR, + desc=None, + leave=False, + bar_format=None, + bytes=False, + file=None, + total=None, + postfix=None, + **kwargs, + ): + """ + bytes : shortcut for + `unit='B', unit_scale=True, unit_divisor=1024, miniters=1` + desc : persists after `close()` + level : effective logging level for determining `disable`; + used only if `disable` is unspecified + disable : If (default: None) or False, + will be determined by logging level. + May be overridden to `True` due to non-TTY status. + Skip override by specifying env var `DVC_IGNORE_ISATTY`. + kwargs : anything accepted by `tqdm.tqdm()` + """ + kwargs = kwargs.copy() + if bytes: + kwargs = self.BYTES_DEFAULTS | kwargs + else: + kwargs.setdefault("unit_scale", total > 999 if total else True) + if file is None: + file = sys.stderr + # auto-disable based on `logger.level` + if not disable: + disable = logger.getEffectiveLevel() > level + # auto-disable based on TTY + if ( + not disable + and not env2bool("DVC_IGNORE_ISATTY") + and hasattr(file, "isatty") + ): + disable = not file.isatty() + super().__init__( + iterable=iterable, + disable=disable, + leave=leave, + desc=desc, + bar_format="!", + lock_args=(False,), + total=total, + **kwargs, + ) + self.postfix = postfix or {"info": ""} + if bar_format is None: + if self.__len__(): + self.bar_format = ( + self.BAR_FMT_DEFAULT_NESTED if self.pos else self.BAR_FMT_DEFAULT + ) + else: + self.bar_format = self.BAR_FMT_NOTOTAL + else: + self.bar_format = bar_format + self.refresh() + + def close(self): + self.postfix["info"] = "" + # remove ETA (either unknown or zero); remove completed bar + self.bar_format = self.bar_format.replace("<{remaining}", "").replace( + "|{bar:10}|", " " + ) + super().close() + + @property + def format_dict(self): + """inject `ncols_desc` to fill the display width (`ncols`)""" + d = super().format_dict + ncols = d["ncols"] or 80 + # assumes `bar_format` has max one of ("ncols_desc" & "ncols_info") + + meter = self.format_meter( # type: ignore[call-arg] + ncols_desc=1, ncols_info=1, **d + ) + ncols_left = ncols - len(meter) + 1 + ncols_left = max(ncols_left, 0) + if ncols_left: + d["ncols_desc"] = d["ncols_info"] = ncols_left + else: + # work-around for zero-width description + d["ncols_desc"] = d["ncols_info"] = 1 + d["prefix"] = "" + return d + + +class CombinedDownloadCallback(TqdmCallback): + def set_size(self, size): + # This is a no-op to prevent fsspec's .get_file() from setting the combined + # download size to the size of the current file. + pass diff --git a/src/datachain/py.typed b/src/datachain/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/src/datachain/query/__init__.py b/src/datachain/query/__init__.py new file mode 100644 index 000000000..0b2d6172e --- /dev/null +++ b/src/datachain/query/__init__.py @@ -0,0 +1,17 @@ +from .dataset import DatasetQuery +from .params import param +from .schema import C, DatasetRow, LocalFilename, Object, Stream +from .session import Session +from .udf import udf + +__all__ = [ + "C", + "DatasetQuery", + "DatasetRow", + "LocalFilename", + "Object", + "Session", + "Stream", + "param", + "udf", +] diff --git a/src/datachain/query/batch.py b/src/datachain/query/batch.py new file mode 100644 index 000000000..a11d07151 --- /dev/null +++ b/src/datachain/query/batch.py @@ -0,0 +1,121 @@ +import contextlib +import math +from abc import ABC, abstractmethod +from collections.abc import Generator, Sequence +from dataclasses import dataclass +from typing import TYPE_CHECKING, Callable, Optional, Union + +import sqlalchemy as sa + +from datachain.data_storage.schema import PARTITION_COLUMN_ID +from datachain.data_storage.warehouse import SELECT_BATCH_SIZE + +if TYPE_CHECKING: + from datachain.dataset import RowDict + + +@dataclass +class RowBatch: + rows: Sequence["RowDict"] + + +BatchingResult = Union["RowDict", RowBatch] + + +class BatchingStrategy(ABC): + """BatchingStrategy provides means of batching UDF executions.""" + + @abstractmethod + def __call__( + self, + execute: Callable, + query: sa.sql.selectable.Select, + ) -> Generator[BatchingResult, None, None]: + """Apply the provided parameters to the UDF.""" + + +class NoBatching(BatchingStrategy): + """ + NoBatching implements the default batching strategy, which is not to + batch UDF calls. + """ + + def __call__( + self, + execute: Callable, + query: sa.sql.selectable.Select, + ) -> Generator["RowDict", None, None]: + return execute(query, limit=query._limit, order_by=query._order_by_clauses) + + +class Batch(BatchingStrategy): + """ + Batch implements UDF call batching, where each execution of a UDF + is passed a sequence of multiple parameter sets. + """ + + def __init__(self, count: int): + self.count = count + + def __call__( + self, + execute: Callable, + query: sa.sql.selectable.Select, + ) -> Generator[RowBatch, None, None]: + # choose page size that is a multiple of the batch size + page_size = math.ceil(SELECT_BATCH_SIZE / self.count) * self.count + + # select rows in batches + results: list[RowDict] = [] + + with contextlib.closing( + execute( + query, + page_size=page_size, + limit=query._limit, + order_by=query._order_by_clauses, + ) + ) as rows: + for row in rows: + results.append(row) + if len(results) >= self.count: + batch, results = results[: self.count], results[self.count :] + yield RowBatch(batch) + + if len(results) > 0: + yield RowBatch(results) + + +class Partition(BatchingStrategy): + """ + Partition implements UDF call batching, where each execution of a UDF + is run on a list of dataset rows grouped by the specified column. + Dataset rows need to be sorted by the grouping column. + """ + + def __call__( + self, + execute: Callable, + query: sa.sql.selectable.Select, + ) -> Generator[RowBatch, None, None]: + current_partition: Optional[int] = None + batch: list[RowDict] = [] + + with contextlib.closing( + execute( + query, + order_by=(PARTITION_COLUMN_ID, "id", *query._order_by_clauses), + limit=query._limit, + ) + ) as rows: + for row in rows: + partition = row[PARTITION_COLUMN_ID] + if current_partition != partition: + current_partition = partition + if len(batch) > 0: + yield RowBatch(batch) + batch = [] + batch.append(row) + + if len(batch) > 0: + yield RowBatch(batch) diff --git a/src/datachain/query/builtins.py b/src/datachain/query/builtins.py new file mode 100644 index 000000000..7f667d891 --- /dev/null +++ b/src/datachain/query/builtins.py @@ -0,0 +1,117 @@ +import hashlib +import tarfile +from functools import partial + +from datachain.sql.types import String + +from .schema import C, DatasetRow, Object +from .udf import udf + +md5 = partial(hashlib.md5, usedforsecurity=False) + +__all__ = ["checksum", "index_tar"] + + +def load_tar(raw): + with tarfile.open(fileobj=raw, mode="r:") as tar: + return tar.getmembers() + + +@udf( + ( + C.source, + C.name, + C.parent, + C.size, + C.vtype, + C.dir_type, + C.owner_name, + C.owner_id, + C.is_latest, + C.last_modified, + C.version, + C.etag, + Object(load_tar), + ), + DatasetRow.schema, +) +def index_tar( + source, + name, + parent, + size, + vtype, + dir_type, + owner_name, + owner_id, + is_latest, + last_modified, + version, + etag, + tar_entries, +): + # generate original tar files as well, along with subobjects + yield DatasetRow.create( + name, + source=source, + parent=parent, + size=size, + vtype=vtype, + dir_type=dir_type, + owner_name=owner_name, + owner_id=owner_id, + is_latest=bool(is_latest), + last_modified=last_modified, + version=version, + etag=etag, + ) + + parent_path = name if not parent else f"{parent}/{name}" + for info in tar_entries: + if info.isfile(): + full_path = f"{parent_path}/{info.name}" + parent_dir, subobject_name = full_path.rsplit("/", 1) + yield DatasetRow.create( + subobject_name, + source=source, + parent=parent_dir, + size=info.size, + vtype="tar", + location={ + "vtype": "tar", + "offset": info.offset_data, + "size": info.size, + "parent": { + "source": source, + "parent": parent, + "name": name, + "version": version, + "size": size, + "etag": etag, + "vtype": "", + "location": None, + }, + }, + ) + + +BUFSIZE = 2**18 + + +def file_digest(fileobj): + """Calculate the digest of a file-like object.""" + buf = bytearray(BUFSIZE) # Reusable buffer to reduce allocations. + view = memoryview(buf) + digestobj = md5() + # From 3.11's hashlib.filedigest() + while True: + size = fileobj.readinto(buf) + if size == 0: + break # EOF + digestobj.update(view[:size]) + return digestobj.hexdigest() + + +@udf(params=[Object(file_digest)], output={"checksum": String}) +def checksum(digest): + return (digest,) diff --git a/src/datachain/query/dataset.py b/src/datachain/query/dataset.py new file mode 100644 index 000000000..7314a84f8 --- /dev/null +++ b/src/datachain/query/dataset.py @@ -0,0 +1,1906 @@ +import ast +import contextlib +import datetime +import inspect +import json +import logging +import os +import random +import re +import string +import subprocess +import sys +import types +from abc import ABC, abstractmethod +from collections.abc import Generator, Iterable, Iterator, Sequence +from copy import copy +from functools import wraps +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Optional, + Protocol, + TypeVar, + Union, +) + +import attrs +import pandas as pd +import sqlalchemy +from attrs import frozen +from dill import dumps, source +from fsspec.callbacks import DEFAULT_CALLBACK, Callback, TqdmCallback +from sqlalchemy import Column +from sqlalchemy.sql import func as f +from sqlalchemy.sql.elements import ColumnClause, ColumnElement +from sqlalchemy.sql.expression import label +from sqlalchemy.sql.schema import TableClause +from sqlalchemy.sql.selectable import Select + +from datachain.asyn import ASYNC_WORKERS, AsyncMapper, OrderedMapper +from datachain.catalog import ( + QUERY_SCRIPT_CANCELED_EXIT_CODE, + QUERY_SCRIPT_INVALID_LAST_STATEMENT_EXIT_CODE, + get_catalog, +) +from datachain.data_storage.schema import ( + PARTITION_COLUMN_ID, + partition_col_names, + partition_columns, +) +from datachain.dataset import DatasetStatus, RowDict +from datachain.error import DatasetNotFoundError, QueryScriptCancelError +from datachain.progress import CombinedDownloadCallback +from datachain.query.schema import DEFAULT_DELIMITER +from datachain.sql.functions import rand +from datachain.storage import Storage, StorageURI +from datachain.utils import batched, determine_processes, inside_notebook + +from .batch import RowBatch +from .metrics import metrics +from .schema import C, UDFParamSpec, normalize_param +from .session import Session +from .udf import UDFBase, UDFClassWrapper, UDFFactory, UDFType + +if TYPE_CHECKING: + from sqlalchemy.sql.elements import ClauseElement + from sqlalchemy.sql.schema import Table + from sqlalchemy.sql.selectable import GenerativeSelect + from typing_extensions import Concatenate, ParamSpec, Self + + from datachain.catalog import Catalog + from datachain.data_storage import AbstractWarehouse + from datachain.dataset import DatasetRecord + + from .udf import UDFResult + + P = ParamSpec("P") + + +INSERT_BATCH_SIZE = 10000 + +PartitionByType = Union[ColumnElement, Sequence[ColumnElement]] +JoinPredicateType = Union[str, ColumnClause, ColumnElement] +# dependency can be either dataset_name + dataset_version tuple or just storage uri +# depending what type of dependency we are adding +DatasetDependencyType = Union[tuple[str, int], StorageURI] + +logger = logging.getLogger("datachain") + + +T = TypeVar("T", bound="DatasetQuery") + + +def detach( + method: "Callable[Concatenate[T, P], T]", +) -> "Callable[Concatenate[T, P], T]": + """ + Decorator that needs to be put on a method that modifies existing DatasetQuery + which was 100% representing one particular dataset and had name and version of + that dataset set, and which returns new instance of it. + This kind of DatasetQuery, which represent one whole dataset, we return from + .save() method. + Example of modifying method is .filter() as that one filters out part + of a dataset which means DatasetQuery no longer 100% represents it (in this case + it can represents only a part of it) + """ + + @wraps(method) + def _inner(self: T, *args: "P.args", **kwargs: "P.kwargs") -> T: + cloned = method(self, *args, **kwargs) + cloned.name = None + cloned.version = None + return cloned + + return _inner + + +class QueryGeneratorFunc(Protocol): + def __call__(self, *columns: ColumnElement) -> Select: ... + + +@frozen +class QueryGenerator: + func: QueryGeneratorFunc + columns: tuple[ColumnElement, ...] + + def exclude(self, column_names) -> Select: + return self.func(*(c for c in self.columns if c.name not in column_names)) + + def select(self, column_names=None) -> Select: + if column_names is None: + return self.func(*self.columns) + return self.func(*(c for c in self.columns if c.name in column_names)) + + +@frozen +class StepResult: + query_generator: QueryGenerator + dependencies: tuple[DatasetDependencyType, ...] + + +def step_result( + func: QueryGeneratorFunc, + columns: Iterable[ColumnElement], + dependencies: Iterable[DatasetDependencyType] = (), +) -> "StepResult": + return StepResult( + query_generator=QueryGenerator(func=func, columns=tuple(columns)), + dependencies=tuple(dependencies), + ) + + +class StartingStep(ABC): + """An initial query processing step, referencing a data source.""" + + @abstractmethod + def apply(self) -> "StepResult": ... + + +@frozen +class Step(ABC): + """A query processing step (filtering, mutation, etc.)""" + + @abstractmethod + def apply( + self, query_generator: "QueryGenerator", temp_tables: list[str] + ) -> "StepResult": + """Apply the processing step.""" + + +@frozen +class QueryStep(StartingStep): + catalog: "Catalog" + dataset_name: str + dataset_version: int + + def apply(self): + def q(*columns): + return sqlalchemy.select(*columns) + + dataset = self.catalog.get_dataset(self.dataset_name) + table = self.catalog.warehouse.dataset_rows(dataset, self.dataset_version) + + return step_result( + q, table.c, dependencies=[(self.dataset_name, self.dataset_version)] + ) + + +@frozen +class IndexingStep(StartingStep): + path: str + catalog: "Catalog" + kwargs: dict[str, Any] + recursive: Optional[bool] = True + + def apply(self): + self.catalog.index([self.path], **self.kwargs) + uri, path = self.parse_path() + _partial_id, partial_path = self.catalog.metastore.get_valid_partial_id( + uri, path + ) + dataset = self.catalog.get_dataset(Storage.dataset_name(uri, partial_path)) + dataset_rows = self.catalog.warehouse.dataset_rows( + dataset, dataset.latest_version + ) + + def q(*columns): + col_names = [c.name for c in columns] + return self.catalog.warehouse.nodes_dataset_query( + dataset_rows, + column_names=col_names, + path=path, + recursive=self.recursive, + ) + + storage = self.catalog.get_storage(uri) + + return step_result(q, dataset_rows.c, dependencies=[storage.uri]) + + def parse_path(self): + client_config = self.kwargs.get("client_config") or {} + client, path = self.catalog.parse_url(self.path, **client_config) + return client.uri, path + + +def generator_then_call(generator, func: Callable): + """ + Yield items from generator then execute a function and yield + its result. + """ + yield from generator + yield func() or [] + + +@frozen +class DatasetDiffOperation(Step): + """ + Abstract class for operations that are calculation some kind of diff between + datasets queries like subtract, changed etc. + """ + + dq: "DatasetQuery" + catalog: "Catalog" + + def clone(self) -> "Self": + return self.__class__(self.dq, self.catalog) + + @abstractmethod + def query( + self, + source_query: Select, + target_query: Select, + ) -> Select: + """ + Should return select query that calculates desired diff between dataset queries + """ + + def apply(self, query_generator, temp_tables: list[str]): + source_query = query_generator.exclude(("id",)) + target_query = self.dq.apply_steps().select() + temp_tables.extend(self.dq.temp_table_names) + + # creating temp table that will hold subtract results + temp_table_name = self.catalog.warehouse.TMP_TABLE_NAME_PREFIX + _random_string( + 6 + ) + temp_tables.append(temp_table_name) + + columns = [ + c if isinstance(c, Column) else Column(c.name, c.type) + for c in source_query.columns + ] + temp_table = self.catalog.warehouse.create_dataset_rows_table( + temp_table_name, + columns=columns, + if_not_exists=False, + ) + + diff_q = self.query(source_query, target_query) + + insert_q = temp_table.insert().from_select( + source_query.selected_columns, diff_q + ) + + self.catalog.warehouse.db.execute(insert_q) + + def q(*columns): + return sqlalchemy.select(*columns) + + return step_result(q, temp_table.c) + + +@frozen +class Subtract(DatasetDiffOperation): + """ + Calculates rows that are in a source query but are not in target query (diff) + This can be used to do delta updates (calculate UDF only on newly added rows) + Example: + >>> ds = DatasetQuery(name="dogs_cats") # some older dataset with embeddings + >>> ds_updated = ( + DatasetQuery("gs://dvcx-datalakes/dogs-and-cats") + .filter(C.size > 1000) # we can also filter out source query + .subtract(ds) + .add_signals(calc_embeddings) # calculae embeddings only on new rows + .union(ds) # union with old dataset that's missing new rows + .save("dogs_cats_updated") + ) + """ + + def query(self, source_query: Select, target_query: Select) -> Select: + return self.catalog.warehouse.subtract_query(source_query, target_query) + + +@frozen +class Changed(DatasetDiffOperation): + """ + Calculates rows that are changed in a source query compared to target query + Changed means it has same source + parent + name but different last_modified + Example: + >>> ds = DatasetQuery(name="dogs_cats") # some older dataset with embeddings + >>> ds_updated = ( + DatasetQuery("gs://dvcx-datalakes/dogs-and-cats") + .filter(C.size > 1000) # we can also filter out source query + .changed(ds) + .add_signals(calc_embeddings) # calculae embeddings only on changed rows + .union(ds) # union with old dataset that's missing updated rows + .save("dogs_cats_updated") + ) + + """ + + def query(self, source_query: Select, target_query: Select) -> Select: + return self.catalog.warehouse.changed_query(source_query, target_query) + + +def adjust_outputs( + warehouse: "AbstractWarehouse", row: dict[str, Any], udf_col_types: list[tuple] +) -> dict[str, Any]: + """ + This function does a couple of things to prepare a row for inserting into the db: + 1. Fill default values for columns that have None and add missing columns + 2. Validate values with its corresponding DB column types and convert types + if needed and possible + """ + # Optimization: Use precomputed column type values as these do not change for each + # row in the same UDF. + for ( + col_name, + col_type, + col_python_type, + col_type_name, + default_value, + ) in udf_col_types: + row_val = row.get(col_name) + + # Fill None or missing values with defaults (get returns None if not in the row) + if row_val is None: + row[col_name] = default_value + continue + + # Validate and convert type if needed and possible + row[col_name] = warehouse.convert_type( + row_val, col_type, col_python_type, col_type_name, col_name + ) + return row + + +def get_udf_col_types(warehouse: "AbstractWarehouse", udf: UDFBase) -> list[tuple]: + """Optimization: Precompute UDF column types so these don't have to be computed + in the convert_type function for each row in a loop.""" + dialect = warehouse.db.dialect + return [ + ( + col_name, + # Check if type is already instantiated or not + col_type_inst := col_type() if inspect.isclass(col_type) else col_type, + warehouse.python_type(col_type_inst), + type(col_type_inst).__name__, + col_type.default_value(dialect), + ) + for col_name, col_type in udf.output.items() + ] + + +def process_udf_outputs( + warehouse: "AbstractWarehouse", + udf_table: "Table", + udf_results: Iterator[Iterable["UDFResult"]], + udf: UDFBase, + batch_size=INSERT_BATCH_SIZE, + cb: Callback = DEFAULT_CALLBACK, +) -> None: + rows: list[UDFResult] = [] + # Optimization: Compute row types once, rather than for every row. + udf_col_types = get_udf_col_types(warehouse, udf) + + for udf_output in udf_results: + if not udf_output: + continue + for row in udf_output: + cb.relative_update() + rows.append(adjust_outputs(warehouse, row, udf_col_types)) + if len(rows) >= batch_size: + for row_chunk in batched(rows, batch_size): + warehouse.insert_rows(udf_table, row_chunk) + rows.clear() + + if rows: + for row_chunk in batched(rows, batch_size): + warehouse.insert_rows(udf_table, row_chunk) + + +def get_download_callback() -> Callback: + return CombinedDownloadCallback( + {"desc": "Download", "unit": "B", "unit_scale": True, "unit_divisor": 1024} + ) + + +def get_processed_callback() -> Callback: + return TqdmCallback({"desc": "Processed", "unit": " rows"}) + + +def get_generated_callback(is_generator: bool = False) -> Callback: + if is_generator: + return TqdmCallback({"desc": "Generated", "unit": " rows"}) + return DEFAULT_CALLBACK + + +def run_udf( + udf, + udf_inputs, + catalog, + is_generator, + cache, + download_cb: Callback = DEFAULT_CALLBACK, + processed_cb: Callback = DEFAULT_CALLBACK, +) -> Iterator[Iterable["UDFResult"]]: + for batch in udf_inputs: + n_rows = len(batch.rows) if isinstance(batch, RowBatch) else 1 + output = udf(catalog, batch, is_generator, cache, cb=download_cb) + processed_cb.relative_update(n_rows) + yield output + + +@frozen +class UDF(Step, ABC): + udf: UDFType + catalog: "Catalog" + partition_by: Optional[PartitionByType] = None + parallel: Optional[int] = None + workers: Union[bool, int] = False + min_task_size: Optional[int] = None + is_generator = False + cache: bool = False + + @abstractmethod + def create_udf_table(self, query: Select) -> "Table": + """Method that creates a table where temp udf results will be saved""" + + def process_input_query(self, query: Select) -> tuple[Select, list["Table"]]: + """Apply any necessary processing to the input query""" + return query, [] + + @abstractmethod + def create_result_query( + self, udf_table: "Table", query: Select + ) -> tuple[QueryGeneratorFunc, list["sqlalchemy.Column"]]: + """ + Method that should return query to fetch results from udf and columns + to select + """ + + def udf_table_name(self) -> str: + return self.catalog.warehouse.UDF_TABLE_NAME_PREFIX + _random_string(6) + + def populate_udf_table(self, udf_table: "Table", query: Select) -> None: + use_partitioning = self.partition_by is not None + batching = self.udf.properties.get_batching(use_partitioning) + workers = self.workers + if ( + not workers + and os.environ.get("DATACHAIN_DISTRIBUTED") + and os.environ.get("DATACHAIN_SETTINGS_WORKERS") + ): + # Enable distributed processing by default if the module is available, + # and a default number of workers is provided. + workers = True + + processes = determine_processes(self.parallel) + + try: + if workers: + from datachain.catalog.loader import get_distributed_class + + distributor = get_distributed_class(min_task_size=self.min_task_size) + distributor( + self.udf, + self.catalog, + udf_table, + query, + workers, + processes, + is_generator=self.is_generator, + use_partitioning=use_partitioning, + cache=self.cache, + ) + elif processes: + # Parallel processing (faster for more CPU-heavy UDFs) + udf_info = { + "udf": self.udf, + "catalog_init": self.catalog.get_init_params(), + "id_generator_clone_params": ( + self.catalog.id_generator.clone_params() + ), + "metastore_clone_params": self.catalog.metastore.clone_params(), + "warehouse_clone_params": self.catalog.warehouse.clone_params(), + "table": udf_table, + "query": query, + "batching": batching, + "processes": processes, + "is_generator": self.is_generator, + "cache": self.cache, + } + + # Run the UDFDispatcher in another process to avoid needing + # if __name__ == '__main__': in user scripts + datachain_exec_path = os.environ.get("DATACHAIN_EXEC_PATH", "datachain") + + envs = dict(os.environ) + envs.update({"PYTHONPATH": os.getcwd()}) + with self.process_feature_module(): + process_data = dumps(udf_info, recurse=True) + result = subprocess.run( # noqa: S603 + [datachain_exec_path, "--internal-run-udf"], + input=process_data, + check=False, + env=envs, + ) + if result.returncode != 0: + raise RuntimeError("UDF Execution Failed!") + + else: + # Otherwise process single-threaded (faster for smaller UDFs) + # Optionally instantiate the UDF instance if a class is provided. + if isinstance(self.udf, UDFFactory): + udf: UDFBase = self.udf() + else: + udf = self.udf + + if hasattr(udf.func, "setup") and callable(udf.func.setup): + udf.func.setup() + + warehouse = self.catalog.warehouse + + with contextlib.closing( + batching(warehouse.dataset_select_paginated, query) + ) as udf_inputs: + download_cb = get_download_callback() + processed_cb = get_processed_callback() + generated_cb = get_generated_callback(self.is_generator) + try: + udf_results = run_udf( + udf, + udf_inputs, + self.catalog, + self.is_generator, + self.cache, + download_cb, + processed_cb, + ) + process_udf_outputs( + warehouse, + udf_table, + udf_results, + udf, + cb=generated_cb, + ) + finally: + download_cb.close() + processed_cb.close() + generated_cb.close() + + warehouse.insert_rows_done(udf_table) + + if hasattr(udf.func, "teardown") and callable(udf.func.teardown): + udf.func.teardown() + + except QueryScriptCancelError: + self.catalog.warehouse.close() + sys.exit(QUERY_SCRIPT_CANCELED_EXIT_CODE) + except (Exception, KeyboardInterrupt): + # Close any open database connections if an error is encountered + self.catalog.warehouse.close() + raise + + @contextlib.contextmanager + def process_feature_module(self): + # Generate a random name for the feature module + feature_module_name = "tmp" + _random_string(10) + # Create a dynamic module with the generated name + dynamic_module = types.ModuleType(feature_module_name) + # Get the import lines for the necessary objects from the main module + main_module = sys.modules["__main__"] + if getattr(main_module, "__file__", None): + import_lines = list(get_imports(main_module)) + else: + import_lines = [ + source.getimport(obj, alias=name) + for name, obj in main_module.__dict__.items() + if _imports(obj) and not (name.startswith("__") and name.endswith("__")) + ] + + # Get the feature classes from the main module + feature_classes = { + name: obj + for name, obj in main_module.__dict__.items() + if _feature_predicate(obj) + } + if not feature_classes: + yield None + return + + # Get the source code of the feature classes + feature_sources = [source.getsource(cls) for _, cls in feature_classes.items()] + # Set the module name for the feature classes to the generated name + for name, cls in feature_classes.items(): + cls.__module__ = feature_module_name + setattr(dynamic_module, name, cls) + # Add the dynamic module to the sys.modules dictionary + sys.modules[feature_module_name] = dynamic_module + # Combine the import lines and feature sources + feature_file = "\n".join(import_lines) + "\n" + "\n".join(feature_sources) + + # Write the module content to a .py file + with open(f"{feature_module_name}.py", "w") as module_file: + module_file.write(feature_file) + + try: + yield feature_module_name + finally: + for cls in feature_classes.values(): + cls.__module__ = main_module.__name__ + os.unlink(f"{feature_module_name}.py") + # Remove the dynamic module from sys.modules + del sys.modules[feature_module_name] + + def create_partitions_table(self, query: Select) -> "Table": + """ + Create temporary table with group by partitions. + """ + assert self.partition_by is not None + + if isinstance(self.partition_by, Sequence): + list_partition_by = self.partition_by + else: + list_partition_by = [self.partition_by] + + # create table with partitions + tbl = self.catalog.warehouse.create_udf_table( + self.udf_table_name(), partition_columns() + ) + + # fill table with partitions + cols = [ + query.selected_columns.id, + f.dense_rank().over(order_by=list_partition_by).label(PARTITION_COLUMN_ID), + ] + self.catalog.warehouse.db.execute( + tbl.insert().from_select(cols, query.with_only_columns(*cols)) + ) + + return tbl + + def clone(self, partition_by: Optional[PartitionByType] = None) -> "Self": + if partition_by is not None: + return self.__class__( + self.udf, + self.catalog, + partition_by=partition_by, + parallel=self.parallel, + workers=self.workers, + min_task_size=self.min_task_size, + ) + return self.__class__(self.udf, self.catalog) + + def apply( + self, query_generator: QueryGenerator, temp_tables: list[str] + ) -> "StepResult": + _query = query = query_generator.select() + + # Apply partitioning if needed. + if self.partition_by is not None: + partition_tbl = self.create_partitions_table(query) + temp_tables.append(partition_tbl.name) + + subq = query.subquery() + query = ( + sqlalchemy.select(*subq.c) + .outerjoin(partition_tbl, partition_tbl.c.id == subq.c.id) + .add_columns(*partition_columns()) + ) + + query, tables = self.process_input_query(query) + temp_tables.extend(t.name for t in tables) + udf_table = self.create_udf_table(_query) + temp_tables.append(udf_table.name) + self.populate_udf_table(udf_table, query) + q, cols = self.create_result_query(udf_table, query) + + return step_result(q, cols) + + +@frozen +class UDFSignal(UDF): + is_generator = False + + def create_udf_table(self, query: Select) -> "Table": + udf_output_columns: list[sqlalchemy.Column[Any]] = [ + sqlalchemy.Column(col_name, col_type) + for (col_name, col_type) in self.udf.output.items() + ] + + return self.catalog.warehouse.create_udf_table( + self.udf_table_name(), udf_output_columns + ) + + def create_pre_udf_table(self, query: Select) -> "Table": + columns = [ + sqlalchemy.Column(c.name, c.type) + for c in query.selected_columns + if c.name != "id" + ] + table = self.catalog.warehouse.create_udf_table(self.udf_table_name(), columns) + select_q = query.with_only_columns( + *[c for c in query.selected_columns if c.name != "id"] + ) + + # if there is order by clause we need row_number to preserve order + # if there is no order by clause we still need row_number to generate + # unique ids as uniqueness is important for this table + select_q = select_q.add_columns( + f.row_number().over(order_by=select_q._order_by_clauses).label("id") + ) + + self.catalog.warehouse.db.execute( + table.insert().from_select(list(select_q.selected_columns), select_q) + ) + return table + + def process_input_query(self, query: Select) -> tuple[Select, list["Table"]]: + if os.getenv("DATACHAIN_DISABLE_QUERY_CACHE", "") not in ("", "0"): + return query, [] + table = self.create_pre_udf_table(query) + q: Select = sqlalchemy.select(*table.c) + if query._order_by_clauses: + # we are adding ordering only if it's explicitly added by user in + # query part before adding signals + q = q.order_by(table.c.id) + return q, [table] + + def create_result_query( + self, udf_table, query + ) -> tuple[QueryGeneratorFunc, list["sqlalchemy.Column"]]: + subq = query.subquery() + original_cols = [c for c in subq.c if c.name not in partition_col_names] + + # new signal columns that are added to udf_table + signal_cols = [c for c in udf_table.c if c.name != "id"] + signal_name_cols = {c.name: c for c in signal_cols} + cols = signal_cols + + def q(*columns): + cols1 = [] + cols2 = [] + for c in columns: + if c.name in partition_col_names: + continue + cols.append(signal_name_cols.get(c.name, c)) + if c.name in signal_name_cols: + cols2.append(c) + else: + cols1.append(c) + + if cols2: + res = ( + sqlalchemy.select(*cols1) + .select_from(subq) + .outerjoin(udf_table, udf_table.c.id == subq.c.id) + .add_columns(*cols2) + ) + else: + res = sqlalchemy.select(*cols1).select_from(subq) + + if query._order_by_clauses: + # if ordering is used in query part before adding signals, we + # will have it as order by id from select from pre-created udf table + res = res.order_by(subq.c.id) + + if self.partition_by is not None: + subquery = res.subquery() + res = sqlalchemy.select(*subquery.c).select_from(subquery) + + return res + + return q, [*original_cols, *cols] + + +@frozen +class RowGenerator(UDF): + """Extend dataset with new rows.""" + + is_generator = True + + def create_udf_table(self, query: Select) -> "Table": + warehouse = self.catalog.warehouse + + table_name = self.udf_table_name() + columns: tuple[Column, ...] = tuple( + Column(name, typ) for name, typ in self.udf.output.items() + ) + return warehouse.create_dataset_rows_table( + table_name, + columns=columns, + if_not_exists=False, + ) + + def create_result_query( + self, udf_table, query: Select + ) -> tuple[QueryGeneratorFunc, list["sqlalchemy.Column"]]: + if not query._order_by_clauses: + # if we are not selecting all rows in UDF, we need to ensure that + # we get the same rows as we got as inputs of UDF since selecting + # without ordering can be non deterministic in some databases + c = query.selected_columns + query = query.order_by(c.id) + + udf_table_query = udf_table.select().subquery() + udf_table_cols: list[sqlalchemy.Label[Any]] = [ + label(c.name, c) for c in udf_table_query.columns + ] + + def q(*columns): + names = {c.name for c in columns} + # Columns for the generated table. + cols = [c for c in udf_table_cols if c.name in names] + return sqlalchemy.select(*cols).select_from(udf_table_query) + + return q, udf_table_query.columns + + +@frozen +class SQLClause(Step, ABC): + def apply( + self, query_generator: QueryGenerator, temp_tables: list[str] + ) -> StepResult: + query = query_generator.select() + new_query = self.apply_sql_clause(query) + + def q(*columns): + return new_query.with_only_columns(*columns) + + return step_result(q, new_query.selected_columns) + + @abstractmethod + def apply_sql_clause(self, query): + pass + + +@frozen +class SQLSelect(SQLClause): + args: tuple[Union[str, ColumnElement], ...] + + def apply_sql_clause(self, query) -> Select: + subquery = query.subquery() + + args = [subquery.c[str(c)] if isinstance(c, (str, C)) else c for c in self.args] + if not args: + args = subquery.c + + return sqlalchemy.select(*args).select_from(subquery) + + +@frozen +class SQLSelectExcept(SQLClause): + args: tuple[str, ...] + + def apply_sql_clause(self, query: Select) -> Select: + subquery = query.subquery() + names = set(self.args) + args = [c for c in subquery.c if c.name not in names] + return sqlalchemy.select(*args).select_from(subquery) + + +@frozen +class SQLMutate(SQLClause): + args: tuple[ColumnElement, ...] + + def apply_sql_clause(self, query: Select) -> Select: + subquery = query.subquery() + return sqlalchemy.select(*subquery.c, *self.args).select_from(subquery) + + +@frozen +class SQLFilter(SQLClause): + expressions: tuple[ColumnElement, ...] + + def __and__(self, other): + return self.__class__(self.expressions + other) + + def apply_sql_clause(self, query: Select) -> Select: + return query.filter(*self.expressions) + + +@frozen +class SQLOrderBy(SQLClause): + args: tuple[ColumnElement, ...] + + def apply_sql_clause(self, query: Select) -> Select: + return query.order_by(*self.args) + + +@frozen +class SQLLimit(SQLClause): + n: int + + def apply_sql_clause(self, query: Select) -> Select: + return query.limit(self.n) + + +@frozen +class SQLOffset(SQLClause): + offset: int + + def apply_sql_clause(self, query: "GenerativeSelect"): + return query.offset(self.offset) + + +@frozen +class SQLCount(SQLClause): + def apply_sql_clause(self, query): + return sqlalchemy.select(f.count(1)).select_from(query.subquery()) + + +@frozen +class SQLUnion(Step): + query1: "DatasetQuery" + query2: "DatasetQuery" + + def apply( + self, query_generator: QueryGenerator, temp_tables: list[str] + ) -> StepResult: + q1 = self.query1.apply_steps().select().subquery() + temp_tables.extend(self.query1.temp_table_names) + q2 = self.query2.apply_steps().select().subquery() + temp_tables.extend(self.query2.temp_table_names) + columns1, columns2 = fill_columns(q1.columns, q2.columns) + + def q(*columns): + names = {c.name for c in columns} + col1 = [c for c in columns1 if c.name in names] + col2 = [c for c in columns2 if c.name in names] + res = ( + sqlalchemy.select(*col1) + .select_from(q1) + .union_all(sqlalchemy.select(*col2).select_from(q2)) + ) + + subquery = res.subquery() + return sqlalchemy.select(*subquery.c).select_from(subquery) + + return step_result( + q, + columns1, + dependencies=self.query1.dependencies | self.query2.dependencies, + ) + + +@frozen +class SQLJoin(Step): + query1: "DatasetQuery" + query2: "DatasetQuery" + predicates: Union[JoinPredicateType, tuple[JoinPredicateType, ...]] + inner: bool + rname: str + + def validate_expression(self, exp: "ClauseElement", q1, q2): + """ + Checking if columns used in expression actually exist in left / right + part of the join. + """ + for c in exp.get_children(): + if isinstance(c, ColumnClause): + assert isinstance(c.table, TableClause) + + q1_c = q1.c.get(c.name) + q2_c = q2.c.get(c.name) + + if c.table.name == q1.name and q1_c is None: + raise ValueError( + f"Column {c.name} was not found in left part of the join" + ) + + if c.table.name == q2.name and q2_c is None: + raise ValueError( + f"Column {c.name} was not found in right part of the join" + ) + if c.table.name not in [q1.name, q2.name]: + raise ValueError( + f"Column {c.name} was not found in left or right" + " part of the join" + ) + continue + self.validate_expression(c, q1, q2) + + def apply( + self, query_generator: QueryGenerator, temp_tables: list[str] + ) -> StepResult: + q1 = self.query1.apply_steps().select().subquery(self.query1.table.name) + temp_tables.extend(self.query1.temp_table_names) + q2 = self.query2.apply_steps().select().subquery(self.query2.table.name) + temp_tables.extend(self.query2.temp_table_names) + + q1_columns = list(q1.c) + q1_column_names = {c.name for c in q1_columns} + q2_columns = [ + c + if c.name not in q1_column_names and c.name != "id" + else c.label(self.rname.format(name=c.name)) + for c in q2.c + ] + + res_columns = q1_columns + q2_columns + predicates = ( + (self.predicates,) + if not isinstance(self.predicates, tuple) + else self.predicates + ) + + expressions = [] + for p in predicates: + if isinstance(p, ColumnClause): + expressions.append(self.query1.c(p.name) == self.query2.c(p.name)) + elif isinstance(p, str): + expressions.append(self.query1.c(p) == self.query2.c(p)) + elif isinstance(p, ColumnElement): + expressions.append(p) + else: + raise TypeError(f"Unsupported predicate {p} for join expression") + + if not expressions: + raise ValueError("Missing predicates") + + join_expression = sqlalchemy.and_(*expressions) + self.validate_expression(join_expression, q1, q2) + + def q(*columns): + join_query = sqlalchemy.join( + q1, + q2, + join_expression, + isouter=not self.inner, + ) + + res = sqlalchemy.select(*columns).select_from(join_query) + subquery = res.subquery() + return sqlalchemy.select(*subquery.c).select_from(subquery) + + return step_result( + q, + res_columns, + dependencies=self.query1.dependencies | self.query2.dependencies, + ) + + +@frozen +class GroupBy(Step): + """Group rows by a specific column.""" + + cols: PartitionByType + + def clone(self) -> "Self": + return self.__class__(self.cols) + + def apply( + self, query_generator: QueryGenerator, temp_tables: list[str] + ) -> StepResult: + query = query_generator.select() + grouped_query = query.group_by(*self.cols) + + def q(*columns): + return grouped_query.with_only_columns(*columns) + + return step_result(q, grouped_query.selected_columns) + + +def fill_columns( + *column_iterables: Iterable[ColumnElement], +) -> list[list[ColumnElement]]: + column_dicts = [{c.name: c for c in columns} for columns in column_iterables] + combined_columns = {n: c for col_dict in column_dicts for n, c in col_dict.items()} + + result: list[list[ColumnElement]] = [[] for _ in column_dicts] + for n in combined_columns: + col = next(col_dict[n] for col_dict in column_dicts if n in col_dict) + for col_dict, out in zip(column_dicts, result): + if n in col_dict: + out.append(col_dict[n]) + else: + # Cast the NULL to ensure all columns are aware of their type + # Label it to ensure it's aware of its name + out.append(sqlalchemy.cast(sqlalchemy.null(), col.type).label(n)) + return result + + +@attrs.define +class ResultIter: + _row_iter: Iterable[Any] + columns: list[str] + + def __iter__(self): + yield from self._row_iter + + +class DatasetQuery: + def __init__( + self, + path: str = "", + name: str = "", + version: Optional[int] = None, + catalog: Optional["Catalog"] = None, + client_config=None, + recursive: Optional[bool] = True, + session: Optional[Session] = None, + anon: bool = False, + indexing_feature_schema: Optional[dict] = None, + indexing_column_types: Optional[dict[str, Any]] = None, + ): + if client_config is None: + client_config = {} + + if anon: + client_config["anon"] = True + + self.steps: list[Step] = [] + self.catalog = catalog or get_catalog(client_config=client_config) + self._chunk_index: Optional[int] = None + self._chunk_total: Optional[int] = None + self.temp_table_names: list[str] = [] + self.dependencies: set[DatasetDependencyType] = set() + self.table = self.get_table() + self.starting_step: StartingStep + self.name: Optional[str] = None + self.version: Optional[int] = None + self.feature_schema: Optional[dict] = None + self.column_types: Optional[dict[str, Any]] = None + self.session = Session.get(session, catalog=catalog) + + if path: + self.starting_step = IndexingStep(path, self.catalog, {}, recursive) + self.feature_schema = indexing_feature_schema + self.column_types = indexing_column_types + elif name: + ds = self.catalog.get_dataset(name) + self.version = version or ds.latest_version + self.feature_schema = ds.get_version(self.version).feature_schema + self.column_types = copy(ds.schema) + if "id" in self.column_types: + self.column_types.pop("id") + self.starting_step = QueryStep(self.catalog, name, self.version) + # attaching to specific dataset + self.name = name + self.version = version + else: + raise ValueError("must provide path or name") + + @staticmethod + def is_storage_path(path): + return bool(re.compile(r"^[a-zA-Z0-9]+://").match(path)) + + def __iter__(self): + return iter(self.results()) + + def __or__(self, other): + return self.union(other) + + @staticmethod + def get_table() -> "TableClause": + table_name = "".join( + random.choice(string.ascii_letters) # noqa: S311 + for _ in range(16) + ) + return sqlalchemy.table(table_name) + + @staticmethod + def delete( + name: str, version: Optional[int] = None, catalog: Optional["Catalog"] = None + ) -> None: + catalog = catalog or get_catalog() + version = version or catalog.get_dataset(name).latest_version + catalog.remove_dataset(name, version) + + @property + def attached(self) -> bool: + """ + DatasetQuery is considered "attached" to underlying dataset if it represents + it completely. If this is the case, name and version of underlying dataset + will be defined. + DatasetQuery instance can become attached in two scenarios: + 1. ds = DatasetQuery(name="dogs", version=1) -> ds is attached to dogs + 2. ds = ds.save("dogs", version=1) -> ds is attached to dogs dataset + It can move to detached state if filter or similar methods are called on it, + as then it no longer 100% represents underlying datasets. + """ + return self.name is not None and self.version is not None + + def c(self, name: Union[C, str]) -> "ColumnClause[Any]": + col = sqlalchemy.column(name) if isinstance(name, str) else name + col.table = self.table + return col + + def apply_steps(self) -> QueryGenerator: + """ + Apply the steps in the query and return the resulting + sqlalchemy.SelectBase. + """ + query = self.clone() + + index = os.getenv("DATACHAIN_QUERY_CHUNK_INDEX", self._chunk_index) + total = os.getenv("DATACHAIN_QUERY_CHUNK_TOTAL", self._chunk_total) + + if index is not None and total is not None: + index, total = int(index), int(total) # os.getenv returns str + + if not (0 <= index < total): + raise ValueError("chunk index must be between 0 and total") + + # Respect limit in chunks + query.steps = self._chunk_limit(query.steps, index, total) + + # Prepend the chunk filter to the step chain. + query = query.filter(C.random % total == index) + query.steps = query.steps[-1:] + query.steps[:-1] + + result = query.starting_step.apply() + group_by = None + self.dependencies.update(result.dependencies) + + for step in query.steps: + if isinstance(step, GroupBy): + if group_by is not None: + raise TypeError("only one group_by allowed") + group_by = step + continue + + result = step.apply( + result.query_generator, self.temp_table_names + ) # a chain of steps linked by results + self.dependencies.update(result.dependencies) + + if group_by: + result = group_by.apply(result.query_generator, self.temp_table_names) + self.dependencies.update(result.dependencies) + + return result.query_generator + + @staticmethod + def _chunk_limit(steps: list["Step"], index: int, total: int) -> list["Step"]: + no_limit_steps = [] + limit = None + for step in steps: + # Remember last limit + if isinstance(step, SQLLimit): + limit = step.n + # Only keep non-limit steps + else: + no_limit_steps.append(step) + # Chunk the limit + if limit: + limit_modulo = limit % total + limit = limit // total + if index < limit_modulo: + limit += 1 + return [*no_limit_steps, SQLLimit(limit)] + return steps + + def cleanup(self) -> None: + """Cleanup any temporary tables.""" + if not self.temp_table_names: + # Nothing to clean up. + return + # This is needed to always use a new connection with all metastore and warehouse + # implementations, as errors may close or render unusable the existing + # connections. + metastore = self.catalog.metastore.clone(use_new_connection=True) + metastore.cleanup_temp_tables(self.temp_table_names) + metastore.close() + warehouse = self.catalog.warehouse.clone(use_new_connection=True) + warehouse.cleanup_temp_tables(self.temp_table_names) + warehouse.close() + self.temp_table_names = [] + + def results(self, row_factory=None, **kwargs): + with self.as_iterable(**kwargs) as result: + if row_factory: + cols = result.columns + return [row_factory(cols, r) for r in result] + return list(result) + + @contextlib.contextmanager + def as_iterable(self, **kwargs) -> Iterator[ResultIter]: + try: + query = self.apply_steps().select() + selected_columns = [c.name for c in query.columns] + yield ResultIter( + self.catalog.warehouse.dataset_rows_select(query, **kwargs), + selected_columns, + ) + finally: + self.cleanup() + + def extract( + self, *params: UDFParamSpec, workers=ASYNC_WORKERS, **kwargs + ) -> Iterable[tuple]: + """ + Extract columns from each row in the query. + + Returns an iterable of tuples matching the given params. + + To ensure prompt resource cleanup, it is recommended to wrap this + with contextlib.closing(). + """ + actual_params = [normalize_param(p) for p in params] + try: + query = self.apply_steps().select() + + def row_iter() -> Generator[RowDict, None, None]: + # warehouse isn't threadsafe, we need to clone() it + # in the thread that uses the results + warehouse = None + try: + warehouse = self.catalog.warehouse.clone() + gen = warehouse.dataset_select_paginated( + query, limit=query._limit, order_by=query._order_by_clauses + ) + with contextlib.closing(gen) as rows: + yield from rows + finally: + # clone doesn't necessarily create a new connection + # we can't do `warehouse.close()` for now. It is a bad design + # in clone / close interface that needs to be fixed. + pass + + async def get_params(row: RowDict) -> tuple: + return tuple( + [ + await p.get_value_async(self.catalog, row, mapper, **kwargs) + for p in actual_params + ] + ) + + MapperCls = OrderedMapper if query._order_by_clauses else AsyncMapper # noqa: N806 + with contextlib.closing(row_iter()) as rows: + mapper = MapperCls(get_params, rows, workers=workers) + yield from mapper.iterate() + finally: + self.cleanup() + + def to_records(self) -> list[dict]: + with self.as_iterable() as result: + cols = result.columns + return [dict(zip(cols, row)) for row in result] + + def to_pandas(self) -> "pd.DataFrame": + records = self.to_records() + df = pd.DataFrame.from_records(records) + df.columns = [c.replace(DEFAULT_DELIMITER, ".") for c in df.columns] + return df + + def shuffle(self) -> "Self": + # ToDo: implement shaffle based on seed and/or generating random column + return self.order_by(C.random) + + def sample(self, n) -> "Self": + """ + Return a random sample from the dataset. + + Args: + n (int): Number of samples to draw. + + NOTE: Sampled are not deterministic, and streamed/paginated queries or + multiple workers will draw samples with replacement. + """ + sampled = self.order_by(rand()) + + return sampled.limit(n) + + def show(self, limit=20) -> None: + df = self.limit(limit).to_pandas() + + options = ["display.max_colwidth", 50, "display.show_dimensions", False] + with pd.option_context(*options): + if inside_notebook(): + from IPython.display import display + + display(df) + + else: + print(df.to_string()) + + if len(df) == limit: + print(f"[limited by {limit} objects]") + + def clone(self, new_table=True) -> "Self": + obj = copy(self) + obj.steps = obj.steps.copy() + if new_table: + obj.table = self.get_table() + return obj + + @detach + def select(self, *args, **kwargs) -> "Self": + """ + Select the given columns or expressions using a subquery. + + If used with no arguments, this simply creates a subquery and + select all columns from it. + + Note that the `save` function expects default dataset columns to + be present. This function is meant to be followed by a call to + `results` if used to exclude any default columns. + + Example: + >>> ds.select(C.name, C.size * 10).results() + >>> ds.select(C.name, size10x=C.size * 10).order_by(C.size10x).results() + """ + named_args = [v.label(k) for k, v in kwargs.items()] + query = self.clone() + query.steps.append(SQLSelect((*args, *named_args))) + return query + + @detach + def select_except(self, *args) -> "Self": + """ + Exclude certain columns from this query using a subquery. + + Note that the `save` function expects default dataset columns to + be present. This function is meant to be followed by a call to + `results` if used to exclude any default columns. + + Example: + >>> ( + ... ds.mutate(size10x=C.size * 10) + ... .order_by(C.size10x) + ... .select_except(C.size10x) + ... .results() + ... ) + """ + + if not args: + raise TypeError("select_except expected at least 1 argument, got 0") + query_args = [c if isinstance(c, str) else c.name for c in args] + query = self.clone() + query.steps.append(SQLSelectExcept(query_args)) # type: ignore [arg-type] + return query + + @detach + def mutate(self, *args, **kwargs) -> "Self": + """ + Add new columns to this query. + + This function selects all existing columns from this query and + adds in the new columns specified. + + Example: + >>> ds.mutate(size10x=C.size * 10).order_by(C.size10x).results() + """ + query_args = [v.label(k) for k, v in dict(args, **kwargs).items()] + query = self.clone() + query.steps.append(SQLMutate((*query_args,))) + return query + + @detach + def filter(self, *args) -> "Self": + query = self.clone(new_table=False) + steps = query.steps + if steps and isinstance(steps[-1], SQLFilter): + steps[-1] = steps[-1] & args + else: + steps.append(SQLFilter(args)) + return query + + @detach + def order_by(self, *args) -> "Self": + query = self.clone(new_table=False) + query.steps.append(SQLOrderBy(args)) + return query + + @detach + def limit(self, n: int) -> "Self": + query = self.clone(new_table=False) + query.steps.append(SQLLimit(n)) + return query + + @detach + def offset(self, offset: int) -> "Self": + query = self.clone(new_table=False) + query.steps.append(SQLOffset(offset)) + return query + + def count(self) -> int: + query = self.clone() + query.steps.append(SQLCount()) + return query.results()[0][0] + + def sum(self, col: ColumnElement): + query = self.clone() + query.steps.append(SQLSelect((f.sum(col),))) + return query.results()[0][0] + + def avg(self, col: ColumnElement): + query = self.clone() + query.steps.append(SQLSelect((f.avg(col),))) + return query.results()[0][0] + + def min(self, col: ColumnElement): + query = self.clone() + query.steps.append(SQLSelect((f.min(col),))) + return query.results()[0][0] + + def max(self, col: ColumnElement): + query = self.clone() + query.steps.append(SQLSelect((f.max(col),))) + return query.results()[0][0] + + @detach + def group_by(self, *cols: ColumnElement) -> "Self": + query = self.clone() + query.steps.append(GroupBy(cols)) + return query + + @detach + def union(self, dataset_query: "DatasetQuery") -> "Self": + left = self.clone() + right = dataset_query.clone() + new_query = self.clone() + new_query.steps = [SQLUnion(left, right)] + return new_query + + @detach + def join( + self, + dataset_query: "DatasetQuery", + predicates: Union[JoinPredicateType, Sequence[JoinPredicateType]], + inner=False, + rname="{name}_right", + ) -> "Self": + left = self.clone(new_table=False) + if self.table.name == dataset_query.table.name: + # for use case where we join with itself, e.g dogs.join(dogs, "name") + right = dataset_query.clone(new_table=True) + else: + right = dataset_query.clone(new_table=False) + + new_query = self.clone() + predicates = ( + predicates + if isinstance(predicates, (str, ColumnClause, ColumnElement)) + else tuple(predicates) + ) + new_query.steps = [SQLJoin(left, right, predicates, inner, rname)] + return new_query + + @detach + def chunk(self, index: int, total: int) -> "Self": + """Split a query into smaller chunks for e.g. parallelization. + Example: + >>> query = DatasetQuery(...) + >>> chunk_1 = query._chunk(0, 2) + >>> chunk_2 = query._chunk(1, 2) + Note: + Bear in mind that `index` is 0-indexed but `total` isn't. + Use 0/3, 1/3 and 2/3, not 1/3, 2/3 and 3/3. + """ + query = self.clone() + query._chunk_index, query._chunk_total = index, total + return query + + @detach + def add_signals( + self, + udf: UDFType, + parallel: Optional[int] = None, + workers: Union[bool, int] = False, + min_task_size: Optional[int] = None, + partition_by: Optional[PartitionByType] = None, + cache: bool = False, + ) -> "Self": + """ + Adds one or more signals based on the results from the provided UDF. + + Parallel can optionally be specified as >= 1 for parallel processing with a + specific number of processes, or set to -1 for the default of + the number of CPUs (cores) on the current machine. + + For distributed processing with the appropriate distributed module installed, + workers can optionally be specified as >= 1 for a specific number of workers, + or set to True for the default of all nodes in the cluster. + As well, a custom minimum task size (min_task_size) can be provided to send + at least that minimum number of rows to each distributed worker, mostly useful + if there are a very large number of small tasks to process. + """ + if isinstance(udf, UDFClassWrapper): # type: ignore[unreachable] + # This is a bare decorated class, "instantiate" it now. + udf = udf() # type: ignore[unreachable] + query = self.clone() + query.steps.append( + UDFSignal( + udf, + self.catalog, + partition_by=partition_by, + parallel=parallel, + workers=workers, + min_task_size=min_task_size, + cache=cache, + ) + ) + return query + + @detach + def subtract(self, dq: "DatasetQuery") -> "Self": + query = self.clone() + query.steps.append(Subtract(dq, self.catalog)) + return query + + @detach + def changed(self, dq: "DatasetQuery") -> "Self": + query = self.clone() + query.steps.append(Changed(dq, self.catalog)) + return query + + @detach + def generate( + self, + udf: UDFType, + parallel: Optional[int] = None, + workers: Union[bool, int] = False, + min_task_size: Optional[int] = None, + partition_by: Optional[PartitionByType] = None, + cache: bool = False, + ) -> "Self": + if isinstance(udf, UDFClassWrapper): # type: ignore[unreachable] + # This is a bare decorated class, "instantiate" it now. + udf = udf() # type: ignore[unreachable] + query = self.clone() + steps = query.steps + steps.append( + RowGenerator( + udf, + self.catalog, + partition_by=partition_by, + parallel=parallel, + workers=workers, + min_task_size=min_task_size, + cache=cache, + ) + ) + return query + + def _add_dependencies(self, dataset: "DatasetRecord", version: int): + for dependency in self.dependencies: + if isinstance(dependency, tuple): + # dataset dependency + ds_dependency_name, ds_dependency_version = dependency + self.catalog.metastore.add_dataset_dependency( + dataset.name, + version, + ds_dependency_name, + ds_dependency_version, + ) + else: + # storage dependency - its name is a valid StorageURI + storage = self.catalog.get_storage(dependency) + self.catalog.metastore.add_storage_dependency( + StorageURI(dataset.name), + version, + storage.uri, + storage.timestamp_str, + ) + + def exec(self) -> "Self": + """Execute the query.""" + try: + query = self.clone() + query.apply_steps() + finally: + self.cleanup() + return query + + def save( + self, + name: Optional[str] = None, + version: Optional[int] = None, + feature_schema: Optional[dict] = None, + **kwargs, + ) -> "Self": + """Save the query as a dataset.""" + try: + if name and version and self.catalog.get_dataset(name).has_version(version): + raise RuntimeError(f"Dataset {name} already has version {version}") + except DatasetNotFoundError: + pass + if not name and version: + raise RuntimeError("Cannot set version for temporary datasets") + + if not name: + name = self.session.generate_temp_dataset_name() + + try: + query = self.apply_steps() + + columns = [ + c if isinstance(c, Column) else Column(c.name, c.type) + for c in query.columns + ] + if not [c for c in columns if c.name != "id"]: + raise RuntimeError( + "No columns to save in the query. " + "Ensure at least one column (other than 'id') is selected." + ) + + dataset = self.catalog.create_dataset( + name, + version=version, + feature_schema=feature_schema, + columns=columns, + **kwargs, + ) + version = version or dataset.latest_version + + dr = self.catalog.warehouse.dataset_rows(dataset) + + # Exclude the id column and let the db create it to avoid unique + # constraint violations. + q = query.exclude(("id",)) + if q._order_by_clauses: + # ensuring we have id sorted by order by clause if it exists in a query + q = q.add_columns( + f.row_number().over(order_by=q._order_by_clauses).label("id") + ) + + cols = tuple(c.name for c in q.columns) + insert_q = sqlalchemy.insert(dr.get_table()).from_select(cols, q) + self.catalog.warehouse.db.execute(insert_q, **kwargs) + self.catalog.metastore.update_dataset_status( + dataset, DatasetStatus.COMPLETE, version=version + ) + self.catalog.update_dataset_version_with_warehouse_info(dataset, version) + + self._add_dependencies(dataset, version) # type: ignore [arg-type] + finally: + self.cleanup() + return self.__class__(name=name, version=version, catalog=self.catalog) + + +def _get_output_fd_for_write() -> Union[str, int]: + handle = os.getenv("DATACHAIN_OUTPUT_FD") + if not handle: + return os.devnull + + if os.name != "nt": + return int(handle) + + import msvcrt + + return msvcrt.open_osfhandle(int(handle), os.O_WRONLY) # type: ignore[attr-defined] + + +@attrs.define +class ExecutionResult: + preview: list[dict] = attrs.field(factory=list) + dataset: Optional[tuple[str, int]] = None + metrics: dict[str, Any] = attrs.field(factory=dict) + + +def _send_result(dataset_query: DatasetQuery) -> None: + class JSONSerialize(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, (datetime.datetime, datetime.date)): + return obj.isoformat() + if isinstance(obj, bytes): + return list(obj[:1024]) + return super().default(obj) + + try: + preview_args: dict[str, Any] = json.loads( + os.getenv("DATACHAIN_QUERY_PREVIEW_ARGS", "") + ) + except ValueError: + preview_args = {} + + columns = preview_args.get("columns") or [] + + preview_query = ( + dataset_query.select(*columns) + .limit(preview_args.get("limit", 10)) + .offset(preview_args.get("offset", 0)) + ) + + dataset: Optional[tuple[str, int]] = None + if dataset_query.attached: + assert dataset_query.name, "Dataset name should be provided" + assert dataset_query.version, "Dataset version should be provided" + dataset = dataset_query.name, dataset_query.version + + preview = preview_query.to_records() + result = ExecutionResult(preview, dataset, metrics) + data = attrs.asdict(result) + + with open(_get_output_fd_for_write(), mode="w") as f: + json.dump(data, f, cls=JSONSerialize) + + +def query_wrapper(dataset_query: DatasetQuery) -> DatasetQuery: + """ + Wrapper function that wraps the last statement of user query script. + Last statement MUST be instance of DatasetQuery, otherwise script exits with + error code 10 + """ + if not isinstance(dataset_query, DatasetQuery): + sys.exit(QUERY_SCRIPT_INVALID_LAST_STATEMENT_EXIT_CODE) + + catalog = dataset_query.catalog + save = bool(os.getenv("DATACHAIN_QUERY_SAVE")) + save_as = os.getenv("DATACHAIN_QUERY_SAVE_AS") + + if save_as: + if dataset_query.attached: + dataset_name = dataset_query.name + version = dataset_query.version + assert dataset_name, "Dataset name should be provided in attached mode" + assert version, "Dataset version should be provided in attached mode" + + dataset = catalog.get_dataset(dataset_name) + + try: + target_dataset = catalog.get_dataset(save_as) + except DatasetNotFoundError: + target_dataset = None + + if target_dataset: + dataset = catalog.register_dataset(dataset, version, target_dataset) + else: + dataset = catalog.register_new_dataset(dataset, version, save_as) + + dataset_query = DatasetQuery( + name=dataset.name, + version=dataset.latest_version, + catalog=catalog, + ) + else: + dataset_query = dataset_query.save(save_as) + elif save and not dataset_query.attached: + name = catalog.generate_query_dataset_name() + dataset_query = dataset_query.save(name) + + _send_result(dataset_query) + return dataset_query + + +def _random_string(length: int) -> str: + return "".join( + random.choice(string.ascii_letters + string.digits) # noqa: S311 + for i in range(length) + ) + + +def _feature_predicate(obj): + from datachain.lib.feature import Feature + + return inspect.isclass(obj) and source.isfrommain(obj) and issubclass(obj, Feature) + + +def _imports(obj): + return not source.isfrommain(obj) + + +def get_imports(m): + root = ast.parse(inspect.getsource(m)) + + for node in ast.iter_child_nodes(root): + if isinstance(node, ast.Import): + module = None + elif isinstance(node, ast.ImportFrom): + module = node.module + else: + continue + + for n in node.names: + import_script = "" + if module: + import_script += f"from {module} " + import_script += f"import {n.name}" + if n.asname: + import_script += f" as {n.asname}" + yield import_script diff --git a/src/datachain/query/dispatch.py b/src/datachain/query/dispatch.py new file mode 100644 index 000000000..17cfc8754 --- /dev/null +++ b/src/datachain/query/dispatch.py @@ -0,0 +1,394 @@ +import contextlib +from collections.abc import Iterator, Sequence +from itertools import chain +from multiprocessing import cpu_count +from queue import Empty, Full, Queue +from sys import stdin +from time import sleep +from types import GeneratorType +from typing import Any, Optional + +import attrs +import multiprocess +from dill import load +from fsspec.callbacks import DEFAULT_CALLBACK, Callback +from multiprocess import get_context + +from datachain.catalog import Catalog +from datachain.catalog.loader import get_distributed_class +from datachain.query.batch import RowBatch +from datachain.query.dataset import ( + get_download_callback, + get_generated_callback, + get_processed_callback, + process_udf_outputs, +) +from datachain.query.udf import UDFBase, UDFFactory, UDFResult + +DEFAULT_BATCH_SIZE = 10000 +STOP_SIGNAL = "STOP" +OK_STATUS = "OK" +FINISHED_STATUS = "FINISHED" +FAILED_STATUS = "FAILED" +NOTIFY_STATUS = "NOTIFY" + + +def full_module_type_path(typ: type) -> str: + return f"{typ.__module__}.{typ.__qualname__}" + + +def get_n_workers_from_arg(n_workers: Optional[int] = None) -> int: + if not n_workers: + return cpu_count() + if n_workers < 1: + raise RuntimeError("Must use at least one worker for parallel UDF execution!") + return n_workers + + +# For more context on the get_from_queue and put_into_queue functions, see the +# discussion here: +# https://github.com/iterative/dvcx/pull/1297#issuecomment-2026308773 +# This problem is not exactly described by, but is also related to these Python issues: +# https://github.com/python/cpython/issues/66587 +# https://github.com/python/cpython/issues/88628 +# https://github.com/python/cpython/issues/108645 + + +def get_from_queue(queue: Queue) -> Any: + """ + Gets an item from a queue. + This is required to handle signals, such as KeyboardInterrupt exceptions + while waiting for items to be available, although only on certain installations. + (See the above comment for more context.) + """ + while True: + try: + return queue.get_nowait() + except Empty: + sleep(0.01) + + +def put_into_queue(queue: Queue, item: Any) -> None: + """ + Puts an item into a queue. + This is required to handle signals, such as KeyboardInterrupt exceptions + while waiting for items to be queued, although only on certain installations. + (See the above comment for more context.) + """ + while True: + try: + queue.put_nowait(item) + return + except Full: + sleep(0.01) + + +def udf_entrypoint() -> int: + # Load UDF info from stdin + udf_info = load(stdin.buffer) # noqa: S301 + + ( + warehouse_class, + warehouse_args, + warehouse_kwargs, + ) = udf_info["warehouse_clone_params"] + warehouse = warehouse_class(*warehouse_args, **warehouse_kwargs) + + # Parallel processing (faster for more CPU-heavy UDFs) + dispatch = UDFDispatcher( + udf_info["udf"], + udf_info["catalog_init"], + udf_info["id_generator_clone_params"], + udf_info["metastore_clone_params"], + udf_info["warehouse_clone_params"], + is_generator=udf_info.get("is_generator", False), + cache=udf_info["cache"], + ) + + query = udf_info["query"] + batching = udf_info["batching"] + table = udf_info["table"] + n_workers = udf_info["processes"] + udf = udf_info["udf"] + if n_workers is True: + # Use default number of CPUs (cores) + n_workers = None + + with contextlib.closing( + batching(warehouse.dataset_select_paginated, query) + ) as udf_inputs: + download_cb = get_download_callback() + processed_cb = get_processed_callback() + generated_cb = get_generated_callback(dispatch.is_generator) + try: + udf_results = dispatch.run_udf_parallel( + udf_inputs, + n_workers=n_workers, + processed_cb=processed_cb, + download_cb=download_cb, + ) + process_udf_outputs(warehouse, table, udf_results, udf, cb=generated_cb) + finally: + download_cb.close() + processed_cb.close() + generated_cb.close() + + warehouse.insert_rows_done(table) + + return 0 + + +def udf_worker_entrypoint() -> int: + return get_distributed_class().run_worker() + + +class UDFDispatcher: + _batch_size: Optional[int] = None + + def __init__( + self, + udf, + catalog_init_params, + id_generator_clone_params, + metastore_clone_params, + warehouse_clone_params, + cache, + is_generator=False, + buffer_size=DEFAULT_BATCH_SIZE, + ): + # isinstance cannot be used here, as dill packages the entire class definition, + # and so these two types are not considered exactly equal, + # even if they have the same import path. + if full_module_type_path(type(udf)) != full_module_type_path(UDFFactory): + self.udf = udf + else: + self.udf = None + self.udf_factory = udf + self.catalog_init_params = catalog_init_params + ( + self.id_generator_class, + self.id_generator_args, + self.id_generator_kwargs, + ) = id_generator_clone_params + ( + self.metastore_class, + self.metastore_args, + self.metastore_kwargs, + ) = metastore_clone_params + ( + self.warehouse_class, + self.warehouse_args, + self.warehouse_kwargs, + ) = warehouse_clone_params + self.is_generator = is_generator + self.cache = cache + self.catalog = None + self.task_queue = None + self.done_queue = None + self.buffer_size = buffer_size + self.ctx = get_context("spawn") + + @property + def batch_size(self): + if not self.udf: + self.udf = self.udf_factory() + if self._batch_size is None: + if hasattr(self.udf, "properties") and hasattr( + self.udf.properties, "batch" + ): + self._batch_size = self.udf.properties.batch + else: + self._batch_size = 1 + return self._batch_size + + def _create_worker(self) -> "UDFWorker": + if not self.catalog: + id_generator = self.id_generator_class( + *self.id_generator_args, **self.id_generator_kwargs + ) + metastore = self.metastore_class( + *self.metastore_args, **self.metastore_kwargs + ) + warehouse = self.warehouse_class( + *self.warehouse_args, **self.warehouse_kwargs + ) + self.catalog = Catalog( + id_generator, metastore, warehouse, **self.catalog_init_params + ) + if not self.udf: + self.udf = self.udf_factory() + + return UDFWorker( + self.catalog, + self.udf, + self.task_queue, + self.done_queue, + self.is_generator, + self.cache, + ) + + def _run_worker(self) -> None: + try: + worker = self._create_worker() + worker.run() + except (Exception, KeyboardInterrupt) as e: + put_into_queue(self.done_queue, {"status": FAILED_STATUS, "exception": e}) + raise + + @staticmethod + def send_stop_signal_to_workers(task_queue, n_workers: Optional[int] = None): + n_workers = get_n_workers_from_arg(n_workers) + for _ in range(n_workers): + put_into_queue(task_queue, STOP_SIGNAL) + + def create_input_queue(self): + return self.ctx.Queue() + + def run_udf_parallel( # noqa: C901, PLR0912 + self, + input_rows, + n_workers: Optional[int] = None, + cache: bool = False, + input_queue=None, + processed_cb: Callback = DEFAULT_CALLBACK, + download_cb: Callback = DEFAULT_CALLBACK, + ) -> Iterator[Sequence[UDFResult]]: + n_workers = get_n_workers_from_arg(n_workers) + + if self.buffer_size < n_workers: + raise RuntimeError( + "Parallel run error: buffer size is smaller than " + f"number of workers: {self.buffer_size} < {n_workers}" + ) + + if input_queue: + streaming_mode = True + self.task_queue = input_queue + else: + streaming_mode = False + self.task_queue = self.ctx.Queue() + self.done_queue = self.ctx.Queue() + pool = [ + self.ctx.Process(name=f"Worker-UDF-{i}", target=self._run_worker) + for i in range(n_workers) + ] + for p in pool: + p.start() + + # Will be set to True if all tasks complete normally + normal_completion = False + try: + # Will be set to True when the input is exhausted + input_finished = False + + if not streaming_mode: + # Stop all workers after the input rows have finished processing + input_data = chain(input_rows, [STOP_SIGNAL] * n_workers) + + # Add initial buffer of tasks + for _ in range(self.buffer_size): + try: + put_into_queue(self.task_queue, next(input_data)) + except StopIteration: + input_finished = True + break + + # Process all tasks + while n_workers > 0: + result = get_from_queue(self.done_queue) + status = result["status"] + if status == NOTIFY_STATUS: + download_cb.relative_update(result["downloaded"]) + elif status == FINISHED_STATUS: + # Worker finished + n_workers -= 1 + elif status == OK_STATUS: + processed_cb.relative_update(result["processed"]) + yield result["result"] + else: # Failed / error + n_workers -= 1 + exc = result.get("exception") + if exc: + raise exc + raise RuntimeError("Internal error: Parallel UDF execution failed") + + if not streaming_mode and not input_finished: + try: + put_into_queue(self.task_queue, next(input_data)) + except StopIteration: + input_finished = True + + # Finished with all tasks normally + normal_completion = True + finally: + if not normal_completion: + # Stop all workers if there is an unexpected exception + for _ in pool: + put_into_queue(self.task_queue, STOP_SIGNAL) + self.task_queue.close() + + # This allows workers (and this process) to exit without + # consuming any remaining data in the queues. + # (If they exit due to an exception.) + self.task_queue.cancel_join_thread() + self.done_queue.cancel_join_thread() + + # Flush all items from the done queue. + # This is needed if any workers are still running. + while n_workers > 0: + result = get_from_queue(self.done_queue) + status = result["status"] + if status != OK_STATUS: + n_workers -= 1 + + # Wait for workers to stop + for p in pool: + p.join() + + +class WorkerCallback(Callback): + def __init__(self, queue: multiprocess.Queue): + self.queue = queue + super().__init__() + + def relative_update(self, inc: int = 1) -> None: + put_into_queue(self.queue, {"status": NOTIFY_STATUS, "downloaded": inc}) + + +@attrs.define +class UDFWorker: + catalog: Catalog + udf: UDFBase + task_queue: multiprocess.Queue + done_queue: multiprocess.Queue + is_generator: bool + cache: bool + cb: Callback = attrs.field() + + @cb.default + def _default_callback(self) -> WorkerCallback: + return WorkerCallback(self.done_queue) + + def run(self) -> None: + if hasattr(self.udf.func, "setup") and callable(self.udf.func.setup): + self.udf.func.setup() + while (batch := get_from_queue(self.task_queue)) != STOP_SIGNAL: + n_rows = len(batch.rows) if isinstance(batch, RowBatch) else 1 + udf_output = self.udf( + self.catalog, + batch, + is_generator=self.is_generator, + cache=self.cache, + cb=self.cb, + ) + if isinstance(udf_output, GeneratorType): + udf_output = list(udf_output) # can not pickle generator + put_into_queue( + self.done_queue, + {"status": OK_STATUS, "result": udf_output, "processed": n_rows}, + ) + + if hasattr(self.udf.func, "teardown") and callable(self.udf.func.teardown): + self.udf.func.teardown() + + put_into_queue(self.done_queue, {"status": FINISHED_STATUS}) diff --git a/src/datachain/query/metrics.py b/src/datachain/query/metrics.py new file mode 100644 index 000000000..065ad83ef --- /dev/null +++ b/src/datachain/query/metrics.py @@ -0,0 +1,19 @@ +from typing import Optional, Union + +metrics: dict[str, Union[str, int, float, bool, None]] = {} + + +def set(key: str, value: Union[str, int, float, bool, None]) -> None: # noqa: PYI041 + """Set a metric value.""" + if not isinstance(key, str): + raise TypeError("Key must be a string") + if not key: + raise ValueError("Key must not be empty") + if not isinstance(value, (str, int, float, bool, type(None))): + raise TypeError("Value must be a string, int, float or bool") + metrics[key] = value + + +def get(key: str) -> Optional[Union[str, int, float, bool]]: + """Get a metric value.""" + return metrics[key] diff --git a/src/datachain/query/params.py b/src/datachain/query/params.py new file mode 100644 index 000000000..20bbc678f --- /dev/null +++ b/src/datachain/query/params.py @@ -0,0 +1,27 @@ +import json +import os +from typing import Optional + +params_cache: Optional[dict[str, str]] = None + + +def param(key: str, default: Optional[str] = None) -> Optional[str]: + """Get query parameter.""" + if not isinstance(key, str): + raise TypeError("Param key must be a string") + + global params_cache # noqa: PLW0603 + if params_cache is None: + if env_params := os.getenv("DATACHAIN_QUERY_PARAMS"): + try: + params_parsed = json.loads(env_params) + except (TypeError, ValueError): + raise ValueError("Invalid params provided") from None + if isinstance(params_parsed, dict): + params_cache = params_parsed + else: + raise ValueError("Invalid params provided") + else: + params_cache = {} + + return params_cache.get(key, default) diff --git a/src/datachain/query/schema.py b/src/datachain/query/schema.py new file mode 100644 index 000000000..743ffb6b3 --- /dev/null +++ b/src/datachain/query/schema.py @@ -0,0 +1,289 @@ +import functools +import json +from abc import ABC, abstractmethod +from datetime import datetime, timezone +from fnmatch import fnmatch +from typing import TYPE_CHECKING, Any, Callable, ClassVar, Optional, Union + +import attrs +import sqlalchemy as sa +from fsspec.callbacks import DEFAULT_CALLBACK, Callback + +from datachain.sql.types import JSON, Boolean, DateTime, Int, Int64, SQLType, String + +if TYPE_CHECKING: + from datachain.catalog import Catalog + from datachain.dataset import RowDict + + +DEFAULT_DELIMITER = "__" + + +class ColumnMeta(type): + @staticmethod + def to_db_name(name: str) -> str: + return name.replace(".", DEFAULT_DELIMITER) + + def __getattr__(cls, name: str): + return cls(ColumnMeta.to_db_name(name)) + + +class Column(sa.ColumnClause, metaclass=ColumnMeta): + inherit_cache: Optional[bool] = True + + def __init__(self, text, type_=None, is_literal=False, _selectable=None): + self.name = ColumnMeta.to_db_name(text) + super().__init__( + self.name, type_=type_, is_literal=is_literal, _selectable=_selectable + ) + + def __getattr__(self, name: str): + return Column(self.name + DEFAULT_DELIMITER + name) + + def glob(self, glob_str): + return self.op("GLOB")(glob_str) + + +class UDFParameter(ABC): + @abstractmethod + def get_value(self, catalog: "Catalog", row: "RowDict", **kwargs) -> Any: ... + + async def get_value_async( + self, catalog: "Catalog", row: "RowDict", mapper, **kwargs + ) -> Any: + return self.get_value(catalog, row, **kwargs) + + +@attrs.define(slots=False) +class ColumnParameter(UDFParameter): + name: str + + def get_value(self, catalog, row, **kwargs): + return row[self.name] + + +@attrs.define(slots=False) +class Object(UDFParameter): + """ + Object is used as a placeholder parameter to indicate the actual stored object + being passed as a parameter to the UDF. + """ + + reader: Callable + + def get_value( + self, + catalog: "Catalog", + row: "RowDict", + *, + cache: bool = False, + cb: Callback = DEFAULT_CALLBACK, + **kwargs, + ) -> Any: + client = catalog.get_client(row["source"]) + uid = catalog._get_row_uid(row) + if cache: + client.download(uid, callback=cb) + with client.open_object(uid, use_cache=cache, cb=cb) as f: + return self.reader(f) + + async def get_value_async( + self, + catalog: "Catalog", + row: "RowDict", + mapper, + *, + cache: bool = False, + cb: Callback = DEFAULT_CALLBACK, + **kwargs, + ) -> Any: + client = catalog.get_client(row["source"]) + uid = catalog._get_row_uid(row) + if cache: + await client._download(uid, callback=cb) + obj = await mapper.to_thread( + functools.partial(client.open_object, uid, use_cache=cache, cb=cb) + ) + with obj: + return await mapper.to_thread(self.reader, obj) + + +@attrs.define(slots=False) +class Stream(UDFParameter): + """ + A Stream() parameter receives a binary stream over the object contents. + """ + + def get_value( + self, + catalog: "Catalog", + row: "RowDict", + *, + cache: bool = False, + cb: Callback = DEFAULT_CALLBACK, + **kwargs, + ) -> Any: + client = catalog.get_client(row["source"]) + uid = catalog._get_row_uid(row) + if cache: + client.download(uid, callback=cb) + return client.open_object(uid, use_cache=cache, cb=cb) + + async def get_value_async( + self, + catalog: "Catalog", + row: "RowDict", + mapper, + *, + cache: bool = False, + cb: Callback = DEFAULT_CALLBACK, + **kwargs, + ) -> Any: + client = catalog.get_client(row["source"]) + uid = catalog._get_row_uid(row) + if cache: + await client._download(uid, callback=cb) + return await mapper.to_thread( + functools.partial(client.open_object, uid, use_cache=cache, cb=cb) + ) + + +@attrs.define(slots=False) +class LocalFilename(UDFParameter): + """ + Placeholder parameter representing the local path to a cached copy of the object. + + If glob is None, then all files will be returned. If glob is specified, + then only files matching the glob will be returned, + otherwise None will be returned. + """ + + glob: Optional[str] = None + + def get_value( + self, + catalog: "Catalog", + row: "RowDict", + *, + cb: Callback = DEFAULT_CALLBACK, + **kwargs, + ) -> Optional[str]: + if self.glob and not fnmatch(row["name"], self.glob): # type: ignore[type-var] + # If the glob pattern is specified and the row filename + # does not match it, then return None + return None + client = catalog.get_client(row["source"]) + uid = catalog._get_row_uid(row) + client.download(uid, callback=cb) + return client.cache.get_path(uid) + + async def get_value_async( + self, + catalog: "Catalog", + row: "RowDict", + mapper, + *, + cache: bool = False, + cb: Callback = DEFAULT_CALLBACK, + **kwargs, + ) -> Optional[str]: + if self.glob and not fnmatch(row["name"], self.glob): # type: ignore[type-var] + # If the glob pattern is specified and the row filename + # does not match it, then return None + return None + client = catalog.get_client(row["source"]) + uid = catalog._get_row_uid(row) + await client._download(uid, callback=cb) + return client.cache.get_path(uid) + + +UDFParamSpec = Union[str, Column, UDFParameter] + + +def normalize_param(param: UDFParamSpec) -> UDFParameter: + if isinstance(param, str): + return ColumnParameter(param) + if isinstance(param, Column): + return ColumnParameter(param.name) + if isinstance(param, UDFParameter): + return param + raise TypeError(f"Invalid UDF parameter: {param}") + + +class DatasetRow: + schema: ClassVar[dict[str, type[SQLType]]] = { + "source": String, + "parent": String, + "name": String, + "size": Int64, + "location": JSON, + "vtype": String, + "dir_type": Int, + "owner_name": String, + "owner_id": String, + "is_latest": Boolean, + "last_modified": DateTime, + "version": String, + "etag": String, + } + + @staticmethod + def create( + name: str, + source: str = "", + parent: str = "", + size: int = 0, + location: Optional[dict[str, Any]] = None, + vtype: str = "", + dir_type: int = 0, + owner_name: str = "", + owner_id: str = "", + is_latest: bool = True, + last_modified: Optional[datetime] = None, + version: str = "", + etag: str = "", + ) -> tuple[ + str, + str, + str, + int, + Optional[str], + str, + int, + str, + str, + bool, + datetime, + str, + str, + int, + ]: + if location: + location = json.dumps([location]) # type: ignore [assignment] + + last_modified = last_modified or datetime.now(timezone.utc) + + return ( # type: ignore [return-value] + source, + parent, + name, + size, + location, + vtype, + dir_type, + owner_name, + owner_id, + is_latest, + last_modified, + version, + etag, + ) + + @staticmethod + def extend(**columns): + cols = {**DatasetRow.schema} + cols.update(columns) + return cols + + +C = Column diff --git a/src/datachain/query/session.py b/src/datachain/query/session.py new file mode 100644 index 000000000..db9f6c6ae --- /dev/null +++ b/src/datachain/query/session.py @@ -0,0 +1,107 @@ +import atexit +import re +from typing import TYPE_CHECKING, Optional +from uuid import uuid4 + +from datachain.catalog import get_catalog +from datachain.error import TableMissingError + +if TYPE_CHECKING: + from datachain.catalog import Catalog + + +class Session: + """ + Session is a context that keeps track of temporary DataChain datasets for a proper + cleanup. By default, a global session is created. + + Temporary or ephemeral datasets are the ones created without specified name. + They are useful for optimization purposes and should be automatically removed. + + Temp dataset has specific name format: + "session___" + The suffix is optional. Both s are auto-generated. + + Temp dataset examples: + session_myname_624b41_48e8b4 + session_4b962d_2a5dff + + Parameters: + + `name` (str): The name of the session. Only latters and numbers are supported. + It can be empty. + `catalog` (Catalog): Catalog object. + """ + + GLOBAL_SESSION_CTX: Optional["Session"] = None + GLOBAL_SESSION: Optional["Session"] = None + + DATASET_PREFIX = "session_" + GLOBAL_SESSION_NAME = "global" + SESSION_UUID_LEN = 6 + TEMP_TABLE_UUID_LEN = 6 + + def __init__(self, name="", catalog: Optional["Catalog"] = None): + if re.match(r"^[0-9a-zA-Z]+$", name) is None: + raise ValueError( + f"Session name can contain only letters or numbers - '{name}' given." + ) + + if not name: + name = self.GLOBAL_SESSION_NAME + + session_uuid = uuid4().hex[: self.SESSION_UUID_LEN] + self.name = f"{name}_{session_uuid}" + self.catalog = catalog or get_catalog() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self._cleanup_temp_datasets() + + def generate_temp_dataset_name(self) -> str: + tmp_table_uid = uuid4().hex[: self.TEMP_TABLE_UUID_LEN] + return f"{self.DATASET_PREFIX}{self.name}_{tmp_table_uid}" + + def _cleanup_temp_datasets(self) -> None: + prefix = f"{self.DATASET_PREFIX}{self.name}" + try: + for dataset in list(self.catalog.metastore.list_datasets_by_prefix(prefix)): + self.catalog.remove_dataset(dataset.name, force=True) + # suppress error when metastore has been reset during testing + except TableMissingError: + pass + + @classmethod + def get( + cls, session: Optional["Session"] = None, catalog: Optional["Catalog"] = None + ) -> "Session": + """Creates a Session() object from a catalog. + + Parameters: + `session` (Session): Optional Session(). If not provided a new session will + be created. It's needed mostly for simplie API purposes. + `catalog` (Catalog): Optional catalog. By default a new catalog is created. + """ + if session: + return session + + if cls.GLOBAL_SESSION is None: + cls.GLOBAL_SESSION_CTX = Session(cls.GLOBAL_SESSION_NAME, catalog) + cls.GLOBAL_SESSION = cls.GLOBAL_SESSION_CTX.__enter__() + atexit.register(cls._global_cleanup) + return cls.GLOBAL_SESSION + + @classmethod + def cleanup_for_tests(cls): + if cls.GLOBAL_SESSION_CTX is not None: + cls.GLOBAL_SESSION_CTX.__exit__(None, None, None) + cls.GLOBAL_SESSION = None + cls.GLOBAL_SESSION_CTX = None + atexit.unregister(cls._global_cleanup) + + @staticmethod + def _global_cleanup(): + if Session.GLOBAL_SESSION_CTX is not None: + Session.GLOBAL_SESSION_CTX.__exit__(None, None, None) diff --git a/src/datachain/query/udf.py b/src/datachain/query/udf.py new file mode 100644 index 000000000..ddba7c5a7 --- /dev/null +++ b/src/datachain/query/udf.py @@ -0,0 +1,237 @@ +import typing +from collections.abc import Iterable, Mapping, Sequence +from dataclasses import dataclass +from functools import WRAPPER_ASSIGNMENTS +from inspect import isclass +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Optional, + Union, +) + +from fsspec.callbacks import DEFAULT_CALLBACK, Callback + +from datachain.dataset import RowDict +from datachain.lib.utils import AbstractUDF + +from .batch import Batch, BatchingStrategy, NoBatching, Partition, RowBatch +from .schema import ( + UDFParameter, + UDFParamSpec, + normalize_param, +) + +if TYPE_CHECKING: + from datachain.catalog import Catalog + + from .batch import BatchingResult + +ColumnType = Any + + +# Specification for the output of a UDF +UDFOutputSpec = typing.Mapping[str, ColumnType] + +# Result type when calling the UDF wrapper around the actual +# Python function / class implementing it. +UDFResult = dict[str, Any] + + +@dataclass +class UDFProperties: + """Container for basic UDF properties.""" + + params: list[UDFParameter] + output: UDFOutputSpec + batch: int = 1 + + def get_batching(self, use_partitioning: bool = False) -> BatchingStrategy: + if use_partitioning: + return Partition() + if self.batch == 1: + return NoBatching() + if self.batch > 1: + return Batch(self.batch) + raise ValueError(f"invalid batch size {self.batch}") + + def signal_names(self) -> Iterable[str]: + return self.output.keys() + + +def udf( + params: Sequence[UDFParamSpec], + output: UDFOutputSpec, + *, + method: Optional[str] = None, # only used for class-based UDFs + batch: int = 1, +): + """ + Decorate a function or a class to be used as a UDF. + + The decorator expects both the outputs and inputs of the UDF to be specified. + The outputs are defined as a collection of tuples containing the signal name + and type. + Parameters are defined as a list of column objects (e.g. C.name). + Optionally, UDFs can be run on batches of rows to improve performance, this + is determined by the 'batch' parameter. When operating on batches of inputs, + the UDF function will be called with a single argument - a list + of tuples containing inputs (e.g. ((input1_a, input1_b), (input2_a, input2b))). + """ + if isinstance(params, str): + params = (params,) + if not isinstance(output, Mapping): + raise TypeError(f"'output' must be a mapping, got {type(output).__name__}") + + properties = UDFProperties([normalize_param(p) for p in params], output, batch) + + def decorator(udf_base: Union[Callable, type]): + if isclass(udf_base): + return UDFClassWrapper(udf_base, properties, method=method) + if callable(udf_base): + return UDFWrapper(udf_base, properties) + + return decorator + + +class UDFBase: + """A base class for implementing stateful UDFs.""" + + def __init__( + self, + func: Callable, + properties: UDFProperties, + ): + self.func = func + self.properties = properties + self.signal_names = properties.signal_names() + self.output = properties.output + + def __call__( + self, + catalog: "Catalog", + arg: "BatchingResult", + is_generator: bool = False, + cache: bool = False, + cb: Callback = DEFAULT_CALLBACK, + ) -> Iterable[UDFResult]: + if isinstance(self.func, AbstractUDF): + self.func._catalog = catalog # type: ignore[unreachable] + + if isinstance(arg, RowBatch): + udf_inputs = [ + self.bind_parameters(catalog, row, cache=cache, cb=cb) + for row in arg.rows + ] + udf_outputs = self.func(udf_inputs) + return self._process_results(arg.rows, udf_outputs, is_generator) + if isinstance(arg, RowDict): + udf_inputs = self.bind_parameters(catalog, arg, cache=cache, cb=cb) + udf_outputs = self.func(*udf_inputs) + if not is_generator: + # udf_outputs is generator already if is_generator=True + udf_outputs = [udf_outputs] + return self._process_results([arg], udf_outputs, is_generator) + raise ValueError(f"Unexpected UDF argument: {arg}") + + def bind_parameters(self, catalog: "Catalog", row: "RowDict", **kwargs) -> list: + return [p.get_value(catalog, row, **kwargs) for p in self.properties.params] + + def _process_results( + self, + rows: Sequence["RowDict"], + results: Sequence[Sequence[Any]], + is_generator=False, + ) -> Iterable[UDFResult]: + """Create a list of dictionaries representing UDF results.""" + + # outputting rows + if is_generator: + # each row in results is a tuple of column values + return (dict(zip(self.signal_names, row)) for row in results) + + # outputting signals + row_ids = [row["id"] for row in rows] + return [ + dict(id=row_id, **dict(zip(self.signal_names, signals))) + for row_id, signals in zip(row_ids, results) + if signals is not None # skip rows with no output + ] + + +class UDFClassWrapper: + """ + A wrapper for class-based (stateful) UDFs. + """ + + def __init__( + self, + udf_class: type, + properties: UDFProperties, + method: Optional[str] = None, + ): + self.udf_class = udf_class + self.udf_method = method + self.properties = properties + self.output = properties.output + + def __call__(self, *args, **kwargs) -> "UDFFactory": + return UDFFactory( + self.udf_class, + args, + kwargs, + self.properties, + self.udf_method, + ) + + +class UDFWrapper(UDFBase): + """A wrapper class for function UDFs to be used in custom signal generation.""" + + def __init__( + self, + func: Callable, + properties: UDFProperties, + ): + super().__init__(func, properties) + # This emulates the behavior of functools.wraps for a class decorator + for attr in WRAPPER_ASSIGNMENTS: + if hasattr(func, attr): + setattr(self, attr, getattr(func, attr)) + + # This emulates the behavior of functools.wraps for a class decorator + def __repr__(self): + return repr(self.func) + + +class UDFFactory: + """ + A wrapper for late instantiation of UDF classes, primarily for use in parallelized + execution. + """ + + def __init__( + self, + udf_class: type, + args, + kwargs, + properties: UDFProperties, + method: Optional[str] = None, + ): + self.udf_class = udf_class + self.udf_method = method + self.args = args + self.kwargs = kwargs + self.properties = properties + self.output = properties.output + + def __call__(self) -> UDFWrapper: + udf_func = self.udf_class(*self.args, **self.kwargs) + if self.udf_method: + udf_func = getattr(udf_func, self.udf_method) + + return UDFWrapper(udf_func, self.properties) + + +UDFType = Union[UDFBase, UDFFactory] diff --git a/src/datachain/remote/__init__.py b/src/datachain/remote/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/datachain/remote/studio.py b/src/datachain/remote/studio.py new file mode 100644 index 000000000..f2b85c55e --- /dev/null +++ b/src/datachain/remote/studio.py @@ -0,0 +1,227 @@ +import json +import logging +from collections.abc import Iterable, Iterator +from datetime import datetime, timedelta, timezone +from struct import unpack +from typing import ( + Any, + Generic, + Optional, + TypeVar, +) + +from datachain.dataset import DatasetStats +from datachain.utils import retry_with_backoff + +T = TypeVar("T") +LsData = Optional[list[dict[str, Any]]] +DatasetInfoData = Optional[dict[str, Any]] +DatasetStatsData = Optional[DatasetStats] +DatasetRowsData = Optional[Iterable[dict[str, Any]]] +DatasetExportStatus = Optional[dict[str, Any]] +DatasetExportSignedUrls = Optional[list[str]] + + +logger = logging.getLogger("datachain") + +DATASET_ROWS_CHUNK_SIZE = 8192 + + +def _is_server_error(status_code: int) -> bool: + return str(status_code).startswith("5") + + +def _parse_dates(obj: dict, date_fields: list[str]): + """ + Function that converts string ISO dates to datetime.datetime instances in object + """ + for date_field in date_fields: + if obj.get(date_field): + obj[date_field] = datetime.fromisoformat(obj[date_field]) + + +class Response(Generic[T]): + def __init__(self, data: T, ok: bool, message: str) -> None: + self.data = data + self.ok = ok + self.message = message + + def __repr__(self): + return ( + f"{self.__class__.__name__}(ok={self.ok}, data={self.data}" + f", message={self.message})" + ) + + +class StudioClient: + def __init__( + self, url: str, username: str, token: str, timeout: float = 3600.0 + ) -> None: + self._check_dependencies() + self.url = url.rstrip("/") + self.username = username + self.token = token + self.timeout = timeout + + def _check_dependencies(self) -> None: + try: + import msgpack # noqa: F401 + import requests # noqa: F401 + except ImportError as exc: + raise Exception( + f"Missing dependency: {exc.name}\n" + "To install run:\n" + "\tpip install 'datachain[remote]'" + ) from None + + def _send_request_msgpack(self, route: str, data: dict[str, Any]) -> Response[Any]: + import msgpack + import requests + + response = requests.post( + f"{self.url}/{route}", + json={**data, "team_name": self.username}, + headers={ + "Content-Type": "application/json", + "Authorization": f"token {self.token}", + }, + timeout=self.timeout, + ) + ok = response.ok + content = msgpack.unpackb(response.content, ext_hook=self._unpacker_hook) + response_data = content.get("data") + if ok and response_data is None: + message = "Indexing in progress" + else: + message = content.get("message", "") + return Response(response_data, ok, message) + + @retry_with_backoff(retries=5) + def _send_request(self, route: str, data: dict[str, Any]) -> Response[Any]: + """ + Function that communicate Studio API. + It will raise an exception, and try to retry, if 5xx status code is + returned, or if ConnectionError or Timeout exceptions are thrown from + requests lib + """ + import requests + + response = requests.post( + f"{self.url}/{route}", + json={**data, "team_name": self.username}, + headers={ + "Content-Type": "application/json", + "Authorization": f"token {self.token}", + }, + timeout=self.timeout, + ) + try: + response.raise_for_status() + except requests.exceptions.HTTPError: + if _is_server_error(response.status_code): + # going to retry + raise + + ok = response.ok + try: + data = json.loads(response.content.decode("utf-8")) + except json.decoder.JSONDecodeError: + data = {} + + if not ok: + logger.error( + "Got bad response from Studio, content is %s", + response.content.decode("utf-8"), + ) + if response.status_code == 403: + message = "Not authorized" + else: + message = data.get("message", "") + else: + message = "" + + return Response(data, ok, message) + + @staticmethod + def _unpacker_hook(code, data): + import msgpack + + if code == 42: # for parsing datetime objects + has_timezone = False + timezone_offset = None + if len(data) == 8: + # we send only timestamp without timezone if data is 8 bytes + values = unpack("!d", data) + else: + has_timezone = True + values = unpack("!dl", data) + + timestamp = values[0] + if has_timezone: + timezone_offset = values[1] + return datetime.fromtimestamp( + timestamp, timezone(timedelta(seconds=timezone_offset)) + ) + return datetime.fromtimestamp(timestamp) # noqa: DTZ006 + + return msgpack.ExtType(code, data) + + def ls(self, paths: Iterable[str]) -> Iterator[tuple[str, Response[LsData]]]: + # TODO: change LsData (response.data value) to be list of lists + # to handle cases where a path will be expanded (i.e. globs) + response: Response[LsData] + for path in paths: + response = self._send_request_msgpack("ls", {"source": path}) + yield path, response + + def dataset_info(self, name: str) -> Response[DatasetInfoData]: + def _parse_dataset_info(dataset_info): + _parse_dates(dataset_info, ["created_at", "finished_at"]) + for version in dataset_info.get("versions"): + _parse_dates(version, ["created_at"]) + + return dataset_info + + response = self._send_request("dataset-info", {"dataset_name": name}) + if response.ok: + response.data = _parse_dataset_info(response.data) + return response + + def dataset_rows_chunk( + self, name: str, version: int, offset: int + ) -> Response[DatasetRowsData]: + def _parse_row(row): + row["id"] = int(row["id"]) + return row + + req_data = {"dataset_name": name, "dataset_version": version} + response = self._send_request_msgpack( + "dataset-rows", + {**req_data, "offset": offset, "limit": DATASET_ROWS_CHUNK_SIZE}, + ) + if response.ok: + response.data = [_parse_row(r) for r in response.data] + + return response + + def dataset_stats(self, name: str, version: int) -> Response[DatasetStatsData]: + response = self._send_request( + "dataset-stats", {"dataset_name": name, "dataset_version": version} + ) + if response.ok: + response.data = DatasetStats(**response.data) + return response + + def export_dataset_table( + self, name: str, version: int + ) -> Response[DatasetExportSignedUrls]: + return self._send_request( + "dataset-export", {"dataset_name": name, "dataset_version": version} + ) + + def dataset_export_status( + self, name: str, version: int + ) -> Response[DatasetExportStatus]: + return self._send_request( + "dataset-export-status", {"dataset_name": name, "dataset_version": version} + ) diff --git a/src/datachain/sql/__init__.py b/src/datachain/sql/__init__.py new file mode 100644 index 000000000..4fc757e4c --- /dev/null +++ b/src/datachain/sql/__init__.py @@ -0,0 +1,16 @@ +from sqlalchemy.sql.elements import literal +from sqlalchemy.sql.expression import column + +from . import functions +from .default import setup as default_setup +from .selectable import select, values + +__all__ = [ + "column", + "functions", + "literal", + "select", + "values", +] + +default_setup() diff --git a/src/datachain/sql/default/__init__.py b/src/datachain/sql/default/__init__.py new file mode 100644 index 000000000..9d60c188c --- /dev/null +++ b/src/datachain/sql/default/__init__.py @@ -0,0 +1,3 @@ +from .base import setup + +__all__ = ["setup"] diff --git a/src/datachain/sql/default/base.py b/src/datachain/sql/default/base.py new file mode 100644 index 000000000..54fea4e01 --- /dev/null +++ b/src/datachain/sql/default/base.py @@ -0,0 +1,22 @@ +from datachain.sql.types import ( + TypeConverter, + TypeDefaults, + TypeReadConverter, + register_backend_types, + register_type_defaults, + register_type_read_converters, +) + +setup_is_complete: bool = False + + +def setup() -> None: + global setup_is_complete # noqa: PLW0603 + if setup_is_complete: + return + + register_backend_types("default", TypeConverter()) + register_type_read_converters("default", TypeReadConverter()) + register_type_defaults("default", TypeDefaults()) + + setup_is_complete = True diff --git a/src/datachain/sql/functions/__init__.py b/src/datachain/sql/functions/__init__.py new file mode 100644 index 000000000..657e72908 --- /dev/null +++ b/src/datachain/sql/functions/__init__.py @@ -0,0 +1,25 @@ +from sqlalchemy.sql.expression import func + +from . import path, string +from .conditional import greatest, least +from .random import rand + +count = func.count +sum = func.sum +avg = func.avg +min = func.min +max = func.max + +__all__ = [ + "avg", + "count", + "func", + "greatest", + "least", + "max", + "min", + "path", + "rand", + "string", + "sum", +] diff --git a/src/datachain/sql/functions/array.py b/src/datachain/sql/functions/array.py new file mode 100644 index 000000000..b08d10c95 --- /dev/null +++ b/src/datachain/sql/functions/array.py @@ -0,0 +1,38 @@ +from sqlalchemy.sql.functions import GenericFunction + +from datachain.sql.types import Float, Int64 +from datachain.sql.utils import compiler_not_implemented + + +class cosine_distance(GenericFunction): # noqa: N801 + type = Float() + package = "array" + name = "cosine_distance" + inherit_cache = True + + +class euclidean_distance(GenericFunction): # noqa: N801 + type = Float() + package = "array" + name = "euclidean_distance" + inherit_cache = True + + +class length(GenericFunction): # noqa: N801 + type = Int64() + package = "array" + name = "length" + inherit_cache = True + + +class sip_hash_64(GenericFunction): # noqa: N801 + type = Int64() + package = "hash" + name = "sip_hash_64" + inherit_cache = True + + +compiler_not_implemented(cosine_distance) +compiler_not_implemented(euclidean_distance) +compiler_not_implemented(length) +compiler_not_implemented(sip_hash_64) diff --git a/src/datachain/sql/functions/conditional.py b/src/datachain/sql/functions/conditional.py new file mode 100644 index 000000000..42b4fea3b --- /dev/null +++ b/src/datachain/sql/functions/conditional.py @@ -0,0 +1,9 @@ +from sqlalchemy.sql.functions import ReturnTypeFromArgs + + +class greatest(ReturnTypeFromArgs): # noqa: N801 + inherit_cache = True + + +class least(ReturnTypeFromArgs): # noqa: N801 + inherit_cache = True diff --git a/src/datachain/sql/functions/path.py b/src/datachain/sql/functions/path.py new file mode 100644 index 000000000..687a3318b --- /dev/null +++ b/src/datachain/sql/functions/path.py @@ -0,0 +1,61 @@ +""" +This module provides generic SQL functions for path logic. + +These need to be implemented using dialect-specific compilation rules. +See https://docs.sqlalchemy.org/en/14/core/compiler.html +""" + +from sqlalchemy.sql.functions import GenericFunction + +from datachain.sql.types import String +from datachain.sql.utils import compiler_not_implemented + + +class parent(GenericFunction): # noqa: N801 + """ + Returns the directory component of a posix-style path. + """ + + type = String() + package = "path" + name = "parent" + inherit_cache = True + + +class name(GenericFunction): # noqa: N801 + """ + Returns the final component of a posix-style path. + """ + + type = String() + package = "path" + name = "name" + inherit_cache = True + + +class file_stem(GenericFunction): # noqa: N801 + """ + Strips an extension from the given path. + """ + + type = String() + package = "path" + name = "file_stem" + inherit_cache = True + + +class file_ext(GenericFunction): # noqa: N801 + """ + Returns the extension of the given path. + """ + + type = String() + package = "path" + name = "file_ext" + inherit_cache = True + + +compiler_not_implemented(parent) +compiler_not_implemented(name) +compiler_not_implemented(file_stem) +compiler_not_implemented(file_ext) diff --git a/src/datachain/sql/functions/random.py b/src/datachain/sql/functions/random.py new file mode 100644 index 000000000..29dc7e5f5 --- /dev/null +++ b/src/datachain/sql/functions/random.py @@ -0,0 +1,12 @@ +from sqlalchemy.sql.functions import GenericFunction + +from datachain.sql.types import Int64 +from datachain.sql.utils import compiler_not_implemented + + +class rand(GenericFunction): # noqa: N801 + type = Int64() + inherit_cache = True + + +compiler_not_implemented(rand) diff --git a/src/datachain/sql/functions/string.py b/src/datachain/sql/functions/string.py new file mode 100644 index 000000000..9b7d796ce --- /dev/null +++ b/src/datachain/sql/functions/string.py @@ -0,0 +1,22 @@ +from sqlalchemy.sql.functions import GenericFunction + +from datachain.sql.types import Array, Int64, String +from datachain.sql.utils import compiler_not_implemented + + +class length(GenericFunction): # noqa: N801 + type = Int64() + package = "string" + name = "length" + inherit_cache = True + + +class split(GenericFunction): # noqa: N801 + type = Array(String()) + package = "string" + name = "split" + inherit_cache = True + + +compiler_not_implemented(length) +compiler_not_implemented(split) diff --git a/src/datachain/sql/selectable.py b/src/datachain/sql/selectable.py new file mode 100644 index 000000000..20c0b5f0c --- /dev/null +++ b/src/datachain/sql/selectable.py @@ -0,0 +1,50 @@ +from sqlalchemy.ext.compiler import compiles +from sqlalchemy.sql import expression, selectable + + +class Values(selectable.Values): + def __init__(self, data, columns=None, **kwargs): + if columns is None: + num_columns = len(data[0]) + columns = [expression.column(f"c{i}") for i in range(1, num_columns + 1)] + else: + columns = [ + expression.column(c) if isinstance(c, str) else c for c in columns + ] + super().__init__(*columns, **kwargs) + self._data += tuple(data) + + +def values(data, columns=None, **kwargs) -> Values: + return Values(data, columns=columns, **kwargs) + + +def process_column_expressions(columns): + return [expression.column(c) if isinstance(c, str) else c for c in columns] + + +def select(*columns, **kwargs) -> "expression.Select": + columns = process_column_expressions(columns) + return expression.select(*columns, **kwargs) + + +def base_values_compiler(column_name_func, element, compiler, **kwargs): + columns = element.columns + base_values = expression.values(*columns).data(element._data) + col_expressions = [ + expression.column(column_name_func(i)).label(c.name) + for i, c in enumerate(columns, 1) + ] + expr = ( + expression.select(*col_expressions) + .select_from(base_values) + .subquery(element.name) + ) + return compiler.process(expr, **kwargs) + + +def compile_values(element, compiler, **kwargs): + return base_values_compiler(lambda i: f"c{i}", element, compiler, **kwargs) + + +compiles(Values, "default")(compile_values) diff --git a/src/datachain/sql/sqlite/__init__.py b/src/datachain/sql/sqlite/__init__.py new file mode 100644 index 000000000..ed3016bbf --- /dev/null +++ b/src/datachain/sql/sqlite/__init__.py @@ -0,0 +1,7 @@ +from .base import create_user_defined_sql_functions, setup, sqlite_dialect + +__all__ = [ + "create_user_defined_sql_functions", + "setup", + "sqlite_dialect", +] diff --git a/src/datachain/sql/sqlite/base.py b/src/datachain/sql/sqlite/base.py new file mode 100644 index 000000000..64b70af6e --- /dev/null +++ b/src/datachain/sql/sqlite/base.py @@ -0,0 +1,364 @@ +import logging +import sqlite3 +from collections.abc import Iterable +from datetime import MAXYEAR, MINYEAR, datetime, timezone +from types import MappingProxyType +from typing import Callable, Optional + +import sqlalchemy as sa +import ujson +from sqlalchemy.dialects import sqlite +from sqlalchemy.ext.compiler import compiles +from sqlalchemy.sql.elements import literal +from sqlalchemy.sql.expression import case +from sqlalchemy.sql.functions import func + +from datachain.sql.functions import array, conditional, random, string +from datachain.sql.functions import path as sql_path +from datachain.sql.selectable import Values, base_values_compiler +from datachain.sql.sqlite.types import ( + SQLiteTypeConverter, + SQLiteTypeReadConverter, + register_type_converters, +) +from datachain.sql.types import ( + TypeDefaults, + register_backend_types, + register_type_defaults, + register_type_read_converters, +) + +logger = logging.getLogger("datachain") + +_registered_function_creators: dict[str, Callable[[sqlite3.Connection], None]] = {} +registered_function_creators = MappingProxyType(_registered_function_creators) + +_compiler_hooks: dict[str, Callable] = {} + +sqlite_dialect = sqlite.dialect(paramstyle="named") + +setup_is_complete: bool = False + +slash = literal("/") +empty_str = literal("") +dot = literal(".") + + +def setup(): + global setup_is_complete # noqa: PLW0603 + if setup_is_complete: + return + + # sqlite 3.31.1 is the earliest version tested in CI + if sqlite3.sqlite_version_info < (3, 31, 1): + logger.warning( + "Possible sqlite incompatibility. The earliest tested version of " + "sqlite is 3.31.1 but you have %s", + sqlite3.sqlite_version, + ) + + # We want to show tracebacks for user-defined functions + sqlite3.enable_callback_tracebacks(True) + sqlite3.register_adapter(datetime, adapt_datetime) + sqlite3.register_converter("datetime", convert_datetime) + + register_type_converters() + register_backend_types("sqlite", SQLiteTypeConverter()) + register_type_read_converters("sqlite", SQLiteTypeReadConverter()) + register_type_defaults("sqlite", TypeDefaults()) + + compiles(sql_path.parent, "sqlite")(compile_path_parent) + compiles(sql_path.name, "sqlite")(compile_path_name) + compiles(sql_path.file_stem, "sqlite")(compile_path_file_stem) + compiles(sql_path.file_ext, "sqlite")(compile_path_file_ext) + compiles(array.length, "sqlite")(compile_array_length) + compiles(string.length, "sqlite")(compile_string_length) + compiles(string.split, "sqlite")(compile_string_split) + compiles(conditional.greatest, "sqlite")(compile_greatest) + compiles(conditional.least, "sqlite")(compile_least) + compiles(Values, "sqlite")(compile_values) + compiles(random.rand, "sqlite")(compile_rand) + + if load_usearch_extension(sqlite3.connect(":memory:")): + compiles(array.cosine_distance, "sqlite")(compile_cosine_distance_ext) + compiles(array.euclidean_distance, "sqlite")(compile_euclidean_distance_ext) + else: + compiles(array.cosine_distance, "sqlite")(compile_cosine_distance) + compiles(array.euclidean_distance, "sqlite")(compile_euclidean_distance) + + register_user_defined_sql_functions() + setup_is_complete = True + + +def run_compiler_hook(name): + try: + hook = _compiler_hooks[name] + except KeyError: + return + hook() + + +def functions_exist( + names: Iterable[str], connection: Optional[sqlite3.Connection] = None +) -> bool: + """ + Returns True if all function names are defined for the given connection. + """ + + names = list(names) + for n in names: + if not isinstance(n, str): + raise TypeError( + "functions_exist(): names argument must contain str values. " + f"Found value of type {type(n).__name__}: {n!r}" + ) + + if connection is None: + connection = sqlite3.connect(":memory:") + + if not names: + return True + column1 = sa.column("column1", sa.String) + func_name_query = column1.not_in( + sa.select(sa.column("name", sa.String)).select_from(func.pragma_function_list()) + ) + query = ( + sa.select(func.count() == 0) + .select_from(sa.values(column1).data([(n,) for n in names])) + .where(func_name_query) + ) + comp = query.compile(dialect=sqlite_dialect) + args = (comp.string, comp.params) if comp.params else (comp.string,) + return bool(connection.execute(*args).fetchone()[0]) + + +def create_user_defined_sql_functions(connection): + for function_creator in registered_function_creators.values(): + function_creator(connection) + + +def missing_vector_function(name, exc): + def unavailable_func(*args): + raise ImportError( + f"Missing dependencies for SQL vector function, {name}\n" + "To install run:\n\n" + " pip install 'datachain[vector]'\n" + ) from exc + + return unavailable_func + + +def sqlite_string_split(string: str, sep: str, maxsplit: int = -1) -> str: + return ujson.dumps(string.split(sep, maxsplit)) + + +def register_user_defined_sql_functions() -> None: + # Register optional functions if we have the necessary dependencies + # and otherwise register functions that will raise an exception with + # installation instructions + try: + from .vector import cosine_distance, euclidean_distance + except ImportError as exc: + # We want to throw an exception when trying to compile these + # functions and also if the functions are called using raw SQL. + cosine_distance = missing_vector_function("cosine_distance", exc) + euclidean_distance = missing_vector_function("euclidean_distance", exc) + _compiler_hooks["cosine_distance"] = cosine_distance + _compiler_hooks["euclidean_distance"] = euclidean_distance + + def create_vector_functions(conn): + conn.create_function("cosine_distance", 2, cosine_distance, deterministic=True) + conn.create_function( + "euclidean_distance", 2, euclidean_distance, deterministic=True + ) + + _registered_function_creators["vector_functions"] = create_vector_functions + + def create_string_functions(conn): + conn.create_function("split", 2, sqlite_string_split, deterministic=True) + conn.create_function("split", 3, sqlite_string_split, deterministic=True) + + _registered_function_creators["string_functions"] = create_string_functions + + has_json_extension = functions_exist(["json_array_length"]) + if not has_json_extension: + + def create_json_functions(conn): + conn.create_function( + "json_array_length", 1, py_json_array_length, deterministic=True + ) + + _registered_function_creators["json_functions"] = create_json_functions + + +def adapt_datetime(val: datetime) -> str: + if not (val.tzinfo is timezone.utc or val.tzname() == "UTC"): + try: + val = val.astimezone(timezone.utc) + except (OverflowError, ValueError, OSError): + if val.year == MAXYEAR: + val = datetime.max + elif val.year == MINYEAR: + val = datetime.min + else: + raise + return val.replace(tzinfo=None).isoformat(" ") + + +def convert_datetime(val: bytes) -> datetime: + return datetime.fromisoformat(val.decode()).replace(tzinfo=timezone.utc) + + +def path_parent(path): + return func.rtrim(func.rtrim(path, func.replace(path, slash, empty_str)), slash) + + +def path_name(path): + return func.ltrim(func.substr(path, func.length(path_parent(path)) + 1), slash) + + +def path_file_ext_length(path): + name = path_name(path) + expr = func.length(name) - func.length( + func.rtrim(name, func.replace(name, dot, empty_str)) + ) + return case((func.instr(name, dot) == 0, 0), else_=expr) + + +def path_file_stem(path): + return func.rtrim( + func.substr(path, 1, func.length(path) - path_file_ext_length(path)), dot + ) + + +def path_file_ext(path): + return func.substr(path, func.length(path) - path_file_ext_length(path) + 1) + + +def compile_path_parent(element, compiler, **kwargs): + return compiler.process(path_parent(*element.clauses.clauses), **kwargs) + + +def compile_path_name(element, compiler, **kwargs): + return compiler.process(path_name(*element.clauses.clauses), **kwargs) + + +def compile_path_file_stem(element, compiler, **kwargs): + return compiler.process(path_file_stem(*element.clauses.clauses), **kwargs) + + +def compile_path_file_ext(element, compiler, **kwargs): + return compiler.process(path_file_ext(*element.clauses.clauses), **kwargs) + + +def compile_cosine_distance_ext(element, compiler, **kwargs): + run_compiler_hook("cosine_distance") + return f"distance_cosine_f32({compiler.process(element.clauses, **kwargs)})" + + +def compile_cosine_distance(element, compiler, **kwargs): + run_compiler_hook("cosine_distance") + return f"cosine_distance({compiler.process(element.clauses, **kwargs)})" + + +def compile_euclidean_distance_ext(element, compiler, **kwargs): + run_compiler_hook("euclidean_distance") + return ( + f"sqrt(distance_sqeuclidean_f32({compiler.process(element.clauses, **kwargs)}))" + ) + + +def compile_euclidean_distance(element, compiler, **kwargs): + run_compiler_hook("euclidean_distance") + return f"euclidean_distance({compiler.process(element.clauses, **kwargs)})" + + +def py_json_array_length(arr): + return len(ujson.loads(arr)) + + +def compile_array_length(element, compiler, **kwargs): + return compiler.process(func.json_array_length(*element.clauses.clauses), **kwargs) + + +def compile_string_length(element, compiler, **kwargs): + return compiler.process(func.length(*element.clauses.clauses), **kwargs) + + +def compile_string_split(element, compiler, **kwargs): + return compiler.process(func.split(*element.clauses.clauses), **kwargs) + + +def compile_greatest(element, compiler, **kwargs): + """ + Compiles a sql function for `greatest(*args)` taking 1 or more args + + Compiles to: + - `max(arg1, arg2...)` for 2 or more args + - `arg1` for 1 arg + + sqlite's max() is a simple function when it has 2 or more + arguments but operates as an aggregate function if given only a + single argument + See https://www.sqlite.org/lang_corefunc.html#max_scalar + """ + args = element.clauses.clauses + nargs = len(args) + if nargs < 1: + raise TypeError( + f"conditional.greatest requires at least 1 argument ({nargs} found)" + ) + if nargs == 1: + expr = args[0] + else: + expr = func.max(*args) + return compiler.process(expr, **kwargs) + + +def compile_least(element, compiler, **kwargs): + """ + Compiles a sql function for `least(*args)` taking 1 or more args + + Compiles to: + - `min(arg1, arg2...)` for 2 or more args + - `arg1` for 1 arg + + sqlite's min() is a simple function when it has 2 or more + arguments but operates as an aggregate function if given only a + single argument + See https://www.sqlite.org/lang_corefunc.html#min_scalar + """ + args = element.clauses.clauses + nargs = len(args) + if nargs < 1: + raise TypeError( + f"conditional.least requires at least 1 argument ({nargs} found)" + ) + if nargs == 1: + expr = args[0] + else: + expr = func.min(*args) + return compiler.process(expr, **kwargs) + + +def compile_values(element, compiler, **kwargs): + return base_values_compiler(lambda i: f"column{i}", element, compiler, **kwargs) + + +def compile_rand(element, compiler, **kwargs): + return compiler.process(func.random(), **kwargs) + + +def load_usearch_extension(conn) -> bool: + try: + # usearch is part of the vector optional dependencies + # we use the extension's cosine and euclidean distance functions + from usearch import sqlite_path + + conn.enable_load_extension(True) + conn.load_extension(sqlite_path()) + conn.enable_load_extension(False) + return True + + except Exception: # noqa: BLE001 + return False diff --git a/src/datachain/sql/sqlite/types.py b/src/datachain/sql/sqlite/types.py new file mode 100644 index 000000000..87aa05d11 --- /dev/null +++ b/src/datachain/sql/sqlite/types.py @@ -0,0 +1,74 @@ +import json +import sqlite3 + +import ujson +from sqlalchemy import types + +from datachain.sql.types import TypeConverter, TypeReadConverter + +try: + import numpy as np + + numpy_imported = True +except ImportError: + numpy_imported = False + + +class Array(types.UserDefinedType): + cache_ok = True + + def __init__(self, item_type): + self.item_type = item_type + + @property + def python_type(self): + return list + + def get_col_spec(self, **kwargs): + return "ARRAY" + + +def adapt_array(arr): + return ujson.dumps(arr) + + +def convert_array(arr): + return ujson.loads(arr) + + +def adapt_np_array(arr): + def _json_serialize(obj): + if isinstance(obj, np.ndarray): + return obj.tolist() + return obj + + if np.issubdtype(arr.dtype, np.object_): + return json.dumps(arr.tolist(), default=_json_serialize) + return ujson.dumps(arr.tolist()) + + +def adapt_np_generic(val): + return val.tolist() + + +def register_type_converters(): + sqlite3.register_adapter(list, adapt_array) + sqlite3.register_converter("ARRAY", convert_array) + if numpy_imported: + sqlite3.register_adapter(np.ndarray, adapt_np_array) + sqlite3.register_adapter(np.int32, adapt_np_generic) + sqlite3.register_adapter(np.int64, adapt_np_generic) + sqlite3.register_adapter(np.float32, adapt_np_generic) + sqlite3.register_adapter(np.float64, adapt_np_generic) + + +class SQLiteTypeConverter(TypeConverter): + def array(self, item_type): + return Array(item_type) + + +class SQLiteTypeReadConverter(TypeReadConverter): + def array(self, value, item_type, dialect): + if isinstance(value, str): + value = ujson.loads(value) + return super().array(value, item_type, dialect) diff --git a/src/datachain/sql/sqlite/vector.py b/src/datachain/sql/sqlite/vector.py new file mode 100644 index 000000000..ab7e36e68 --- /dev/null +++ b/src/datachain/sql/sqlite/vector.py @@ -0,0 +1,23 @@ +import math + +import numpy as np + + +def euclidean_distance(a: str, b: str): + a_np = np.fromstring(a[1:-1], sep=",") + b_np = np.fromstring(b[1:-1], sep=",") + + return np.linalg.norm(b_np - a_np) + + +def cosine_distance(a: str, b: str): + u = np.fromstring(a[1:-1], sep=",") + v = np.fromstring(b[1:-1], sep=",") + + uv = np.inner(u, v) + uu = np.inner(u, u) + vv = np.inner(v, v) + + dist = 1.0 - uv / math.sqrt(uu * vv) + + return max(0, min(dist, 2.0)) diff --git a/src/datachain/sql/types.py b/src/datachain/sql/types.py new file mode 100644 index 000000000..15ae0b91a --- /dev/null +++ b/src/datachain/sql/types.py @@ -0,0 +1,454 @@ +""" +SQL types. + +This module provides SQL types to provide common features and interoperability +between different database backends which often have different typing systems. + +See https://docs.sqlalchemy.org/en/20/core/custom_types.html#sqlalchemy.types.TypeDecorator.load_dialect_impl + +For the corresponding python to db type conversion, it's often simpler and +more direct to use methods at the DBAPI rather than sqlalchemy. For example +for sqlite we can use `sqlite.register_converter` +( https://docs.python.org/3/library/sqlite3.html#sqlite3.register_converter ) +""" + +from datetime import datetime +from types import MappingProxyType +from typing import Any, Union + +from sqlalchemy import TypeDecorator, types + +_registry: dict[str, "TypeConverter"] = {} +registry = MappingProxyType(_registry) + +_read_converter_registry: dict[str, "TypeReadConverter"] = {} +read_converter_registry = MappingProxyType(_read_converter_registry) + +_type_defaults_registry: dict[str, "TypeDefaults"] = {} +type_defaults_registry = MappingProxyType(_type_defaults_registry) + +NullType = types.NullType + + +def register_backend_types(dialect_name: str, type_cls): + _registry[dialect_name] = type_cls + + +def register_type_read_converters(dialect_name: str, trc: "TypeReadConverter"): + _read_converter_registry[dialect_name] = trc + + +def register_type_defaults(dialect_name: str, td: "TypeDefaults"): + _type_defaults_registry[dialect_name] = td + + +def converter(dialect) -> "TypeConverter": + name = dialect.name + try: + return registry[name] + except KeyError: + raise ValueError( + f"No type converter registered for dialect: {dialect.name!r}" + ) from None + + +def read_converter(dialect) -> "TypeReadConverter": + name = dialect.name + try: + return read_converter_registry[name] + except KeyError: + raise ValueError( + f"No read type converter registered for dialect: {dialect.name!r}" + ) from None + + +def type_defaults(dialect) -> "TypeDefaults": + name = dialect.name + try: + return type_defaults_registry[name] + except KeyError: + raise ValueError(f"No type defaults registered for dialect: {name!r}") from None + + +class SQLType(TypeDecorator): + impl: type[types.TypeEngine[Any]] = types.TypeEngine + cache_ok = True + + def to_dict(self) -> dict[str, Any]: + return {"type": self.__class__.__name__} + + @classmethod + def from_dict(cls, _: dict[str, Any]) -> Union[type["SQLType"], "SQLType"]: + return cls + + +class String(SQLType): + impl = types.String + + @property + def python_type(self): + return str + + def load_dialect_impl(self, dialect): + return converter(dialect).string() + + @staticmethod + def default_value(dialect): + return type_defaults(dialect).string() + + def on_read_convert(self, value, dialect): + return read_converter(dialect).string(value) + + +class Boolean(SQLType): + impl = types.Boolean + + @property + def python_type(self): + return bool + + def load_dialect_impl(self, dialect): + return converter(dialect).boolean() + + @staticmethod + def default_value(dialect): + return type_defaults(dialect).boolean() + + def on_read_convert(self, value, dialect): + return read_converter(dialect).boolean(value) + + +class Int(SQLType): + impl = types.INTEGER + + @property + def python_type(self): + return int + + def load_dialect_impl(self, dialect): + return converter(dialect).int() + + @staticmethod + def default_value(dialect): + return type_defaults(dialect).int() + + def on_read_convert(self, value, dialect): + return read_converter(dialect).int(value) + + +class Int32(Int): + def load_dialect_impl(self, dialect): + return converter(dialect).int32() + + @staticmethod + def default_value(dialect): + return type_defaults(dialect).int32() + + def on_read_convert(self, value, dialect): + return read_converter(dialect).int32(value) + + +class Int64(Int): + def load_dialect_impl(self, dialect): + return converter(dialect).int64() + + @staticmethod + def default_value(dialect): + return type_defaults(dialect).int64() + + def on_read_convert(self, value, dialect): + return read_converter(dialect).int64(value) + + +class UInt64(Int): + def load_dialect_impl(self, dialect): + return converter(dialect).uint64() + + @staticmethod + def default_value(dialect): + return type_defaults(dialect).uint64() + + def on_read_convert(self, value, dialect): + return read_converter(dialect).uint64(value) + + +class Float(SQLType): + impl = types.INTEGER + + @property + def python_type(self): + return float + + def load_dialect_impl(self, dialect): + return converter(dialect).float() + + @staticmethod + def default_value(dialect): + return type_defaults(dialect).float() + + def on_read_convert(self, value, dialect): + return read_converter(dialect).float(value) + + +class Float32(Float): + def load_dialect_impl(self, dialect): + return converter(dialect).float32() + + @staticmethod + def default_value(dialect): + return type_defaults(dialect).float32() + + def on_read_convert(self, value, dialect): + return read_converter(dialect).float32(value) + + +class Float64(Float): + def load_dialect_impl(self, dialect): + return converter(dialect).float64() + + @staticmethod + def default_value(dialect): + return type_defaults(dialect).float64() + + def on_read_convert(self, value, dialect): + return read_converter(dialect).float64(value) + + +class Array(SQLType): + impl = types.ARRAY + + @property + def python_type(self): + return list + + def load_dialect_impl(self, dialect): + return converter(dialect).array(self.item_type) + + def to_dict(self) -> dict[str, Any]: + item_type_dict = ( + self.item_type.to_dict() + if isinstance(self.item_type, SQLType) + else self.item_type().to_dict() + ) + return { + "type": self.__class__.__name__, + "item_type": item_type_dict, + } + + @classmethod + def from_dict(cls, d: dict[str, Any]) -> Union[type["SQLType"], "SQLType"]: + sub_t = NAME_TYPES_MAPPING[d["item_type"]["type"]].from_dict( # type: ignore [attr-defined] + d["item_type"] + ) + return cls(sub_t) + + @staticmethod + def default_value(dialect): + return type_defaults(dialect).array() + + def on_read_convert(self, value, dialect): + return read_converter(dialect).array(value, self.item_type, dialect) + + +class JSON(SQLType): + impl = types.JSON + + @property + def python_type(self): + return dict + + def load_dialect_impl(self, dialect): + return converter(dialect).json() + + @staticmethod + def default_value(dialect): + return type_defaults(dialect).json() + + def on_read_convert(self, value, dialect): + return read_converter(dialect).json(value) + + +class DateTime(SQLType): + impl = types.DATETIME + + @property + def python_type(self): + return datetime + + def load_dialect_impl(self, dialect): + return converter(dialect).datetime() + + @staticmethod + def default_value(dialect): + return type_defaults(dialect).datetime() + + def on_read_convert(self, value, dialect): + return read_converter(dialect).datetime(value) + + +class Binary(SQLType): + impl = types.BINARY + + @property + def python_type(self): + return bytes + + def load_dialect_impl(self, dialect): + return converter(dialect).binary() + + @staticmethod + def default_value(dialect): + return type_defaults(dialect).binary() + + def on_read_convert(self, value, dialect): + return read_converter(dialect).binary(value) + + +class TypeReadConverter: + def string(self, value): + return value + + def boolean(self, value): + return value + + def int(self, value): + return value + + def int32(self, value): + return value + + def int64(self, value): + return value + + def uint64(self, value): + return value + + def float(self, value): + return value + + def float32(self, value): + return value + + def float64(self, value): + return value + + def array(self, value, item_type, dialect): + if value is None or item_type is None: + return value + return [item_type.on_read_convert(x, dialect) for x in value] + + def json(self, value): + return value + + def datetime(self, value): + return value + + def uuid(self, value): + return value + + def binary(self, value): + return value + + +class TypeConverter: + def string(self): + return types.String() + + def boolean(self): + return types.Boolean() + + def int(self): + return types.Integer() + + def int32(self): + return self.int() + + def int64(self): + return self.int() + + def uint64(self): + return self.int() + + def float(self): + return types.Float() + + def float32(self): + return self.float() + + def float64(self): + return self.float() + + def array(self, item_type): + return types.ARRAY(item_type) + + def json(self): + return types.JSON() + + def datetime(self): + return types.DATETIME() + + def binary(self): + return types.BINARY() + + +class TypeDefaults: + def string(self): + return None + + def boolean(self): + return None + + def int(self): + return None + + def int32(self): + return None + + def int64(self): + return None + + def uint64(self): + return None + + def float(self): + return None + + def float32(self): + return None + + def float64(self): + return None + + def array(self): + return None + + def json(self): + return None + + def datetime(self): + return None + + def uuid(self): + return None + + def binary(self): + return None + + +TYPES = [ + String, + Boolean, + Int, + Int32, + Int64, + UInt64, + Float, + Float32, + Float64, + Array, + JSON, + DateTime, + Binary, +] + +NAME_TYPES_MAPPING = {t.__name__: t for t in TYPES} diff --git a/src/datachain/sql/utils.py b/src/datachain/sql/utils.py new file mode 100644 index 000000000..f28c26daa --- /dev/null +++ b/src/datachain/sql/utils.py @@ -0,0 +1,23 @@ +from sqlalchemy.ext.compiler import compiles + + +def compiler_not_implemented(func, *spec): + package = getattr(func, "package", None) + if package is None: + func_identifier = func.name + else: + func_identifier = f"{func.package}.{func.name}" + + @compiles(func, *spec) + def raise_not_implemented(element, compiler, **kwargs): + try: + dialect_name = compiler.dialect.name + except AttributeError: + dialect_name = "unknown" + raise NotImplementedError( + f"Compiler not implemented for the SQLAlchemy function, {func_identifier}," + f" with dialect, {dialect_name}. For information on adding dialect-specific" + " compilers, see https://docs.sqlalchemy.org/en/14/core/compiler.html" + ) + + return raise_not_implemented diff --git a/src/datachain/storage.py b/src/datachain/storage.py new file mode 100644 index 000000000..21b1e8c41 --- /dev/null +++ b/src/datachain/storage.py @@ -0,0 +1,136 @@ +import posixpath +from abc import ABC, abstractmethod +from datetime import datetime, timedelta, timezone +from functools import cached_property +from typing import NamedTuple, NewType, Optional, Union +from urllib.parse import urlparse + +from datachain.utils import is_expired, time_to_local_str, time_to_str + +STALE_MINUTES_LIMIT = 15 + +# StorageURI represents a normalised URI to a valid storage location (full bucket or +# absolute local path). +# Valid examples: s3://foo, file:///var/data +# Invalid examples: s3://foo/, s3://foo/bar, file://~ +StorageURI = NewType("StorageURI", str) + + +class StorageStatus: + CREATED = 1 + PENDING = 2 + FAILED = 3 + COMPLETE = 4 + PARTIAL = 5 + STALE = 6 + INDEXING_SCHEDULED = 7 + DELETE_SCHEDULED = 8 + + +class AbstractStorage(ABC): + @property + @abstractmethod + def uri(self) -> StorageURI: ... + + @property + @abstractmethod + def timestamp(self) -> Optional[Union[datetime, str]]: ... + + @property + @abstractmethod + def expires(self) -> Optional[Union[datetime, str]]: ... + + @property + @abstractmethod + def status(self) -> int: ... + + @property + def type(self): + return self._parsed_uri.scheme + + @property + def name(self): + return self._parsed_uri.netloc + + @cached_property + def _parsed_uri(self): + return urlparse(self.uri) + + +class StorageRecord(NamedTuple): + id: int + uri: StorageURI + timestamp: Optional[Union[datetime, str]] = None + expires: Optional[Union[datetime, str]] = None + started_inserting_at: Optional[Union[datetime, str]] = None + last_inserted_at: Optional[Union[datetime, str]] = None + status: int = StorageStatus.CREATED + error_message: str = "" + error_stack: str = "" + + +class Storage(StorageRecord, AbstractStorage): + @property + def is_indexed(self) -> bool: + return self.status == StorageStatus.COMPLETE + + @property + def is_expired(self) -> bool: + return is_expired(self.expires) + + @property + def is_pending(self) -> bool: + return self.status == StorageStatus.PENDING + + @property + def is_stale(self) -> bool: + limit = datetime.now(timezone.utc) - timedelta(minutes=STALE_MINUTES_LIMIT) + date_to_check = self.last_inserted_at or self.started_inserting_at + + return self.is_pending and date_to_check < limit # type: ignore [operator] + + @property + def need_indexing(self) -> bool: + return self.is_expired or not self.is_indexed + + @property + def timestamp_str(self) -> Optional[str]: + if not self.timestamp: + return None + return time_to_str(self.timestamp) + + @property + def timestamp_to_local(self) -> Optional[str]: + if not self.timestamp: + return None + return time_to_local_str(self.timestamp) + + @property + def expires_to_local(self) -> Optional[str]: + if not self.expires: + return None + return time_to_local_str(self.expires) + + @staticmethod + def get_expiration_time(timestamp: datetime, ttl: int): + if ttl >= 0: + try: + return timestamp + timedelta(seconds=ttl) + except OverflowError: + return datetime.max + else: + return datetime.max + + @staticmethod + def dataset_name(uri: str, partial_path: str) -> str: + return f"{uri}/{partial_path}" + + def to_dict(self, file_path=""): + uri = self.uri + if file_path: + uri = posixpath.join(uri, *file_path.rstrip("/").split("/")) + return { + "uri": uri, + "timestamp": time_to_str(self.timestamp) if self.timestamp else None, + "expires": time_to_str(self.expires) if self.expires else None, + } diff --git a/src/datachain/utils.py b/src/datachain/utils.py new file mode 100644 index 000000000..d2dc39de7 --- /dev/null +++ b/src/datachain/utils.py @@ -0,0 +1,390 @@ +import glob +import importlib.util +import json +import os +import os.path as osp +import random +import stat +import sys +import time +from collections.abc import Iterable, Iterator, Sequence +from datetime import date, datetime, timezone +from itertools import islice +from typing import TYPE_CHECKING, Any, Optional, TypeVar, Union +from uuid import UUID + +from dateutil import tz +from dateutil.parser import isoparse + +if TYPE_CHECKING: + import pandas as pd + +NUL = b"\0" +TIME_ZERO = datetime.fromtimestamp(0, tz=timezone.utc) + +T = TypeVar("T", bound="DataChainDir") + + +class DataChainDir: + DEFAULT = ".datachain" + CACHE = "cache" + TMP = "tmp" + DB = "db" + ENV_VAR = "DATACHAIN_DIR" + ENV_VAR_DATACHAIN_ROOT = "DATACHAIN_ROOT_DIR" + + def __init__( + self, + root: Optional[str] = None, + cache: Optional[str] = None, + tmp: Optional[str] = None, + db: Optional[str] = None, + ) -> None: + self.root = osp.abspath(root) if root is not None else self.default_root() + self.cache = ( + osp.abspath(cache) if cache is not None else osp.join(self.root, self.CACHE) + ) + self.tmp = ( + osp.abspath(tmp) if tmp is not None else osp.join(self.root, self.TMP) + ) + self.db = osp.abspath(db) if db is not None else osp.join(self.root, self.DB) + + def init(self): + os.makedirs(self.root, exist_ok=True) + os.makedirs(self.cache, exist_ok=True) + os.makedirs(self.tmp, exist_ok=True) + os.makedirs(osp.split(self.db)[0], exist_ok=True) + + @classmethod + def default_root(cls) -> str: + try: + root_dir = os.environ[cls.ENV_VAR_DATACHAIN_ROOT] + except KeyError: + root_dir = os.getcwd() + + return osp.join(root_dir, cls.DEFAULT) + + @classmethod + def find(cls: type[T], create: bool = True) -> T: + try: + root = os.environ[cls.ENV_VAR] + except KeyError: + root = cls.default_root() + instance = cls(root) + if not osp.isdir(root): + if create: + instance.init() + else: + raise NotADirectoryError(root) + return instance + + +def human_time_to_int(time: str) -> Optional[int]: + if not time: + return None + + suffix = time[-1] + try: + num = int(time if suffix.isdigit() else time[:-1]) + except ValueError: + return None + return num * { + "h": 60 * 60, + "d": 60 * 60 * 24, + "w": 60 * 60 * 24 * 7, + "m": 31 * 24 * 60 * 60, + "y": 60 * 60 * 24 * 365, + }.get(suffix.lower(), 1) + + +def time_to_str(dt): + if isinstance(dt, str): + dt = isoparse(dt) + return dt.strftime("%Y-%m-%d %H:%M:%S") + + +def time_to_local(dt: Union[datetime, str]) -> datetime: + # TODO check usage + if isinstance(dt, str): + dt = isoparse(dt) + try: + return dt.astimezone(tz.tzlocal()) + except (OverflowError, OSError, ValueError): + return dt + + +def time_to_local_str(dt: Union[datetime, str]) -> str: + return time_to_str(time_to_local(dt)) + + +def is_expired(expires: Optional[Union[datetime, str]]): + if expires: + return time_to_local(expires) < time_to_local(datetime.now()) # noqa: DTZ005 + + return False + + +SIZE_SUFFIXES = ["", "K", "M", "G", "T", "P", "E", "Z", "Y", "R", "Q"] + + +def sizeof_fmt(num, suffix="", si=False): + power = 1000.0 if si else 1024.0 + for unit in SIZE_SUFFIXES[:-1]: + if abs(num) < power: + if not unit: + return f"{num:4.0f}{suffix}" + return f"{num:3.1f}{unit}{suffix}" + num /= power + return f"{num:.1f}Q{suffix}" + + +def suffix_to_number(num_str: str) -> int: + try: + if len(num_str) > 1: + suffix = num_str[-1].upper() + if suffix in SIZE_SUFFIXES: + suffix_idx = SIZE_SUFFIXES.index(suffix) + return int(num_str[:-1]) * (1024**suffix_idx) + return int(num_str) + except (TypeError, ValueError): + raise ValueError(f"Invalid number/suffix for: {num_str}") from None + + +def force_create_dir(name): + if not os.path.exists(name): + os.mkdir(name) + elif not os.path.isdir(name): + os.remove(name) + os.mkdir(name) + + +def datachain_paths_join(source_path: str, file_paths: Iterable[str]) -> Iterable[str]: + source_parts = source_path.rstrip("/").split("/") + if glob.has_magic(source_parts[-1]): + # Remove last element if it is a glob match (such as *) + source_parts.pop() + source_stripped = "/".join(source_parts) + return (f"{source_stripped}/{path.lstrip('/')}" for path in file_paths) + + +# From: https://docs.python.org/3/library/shutil.html#rmtree-example +def remove_readonly(func, path, _): + "Clear the readonly bit and reattempt the removal" + os.chmod(path, stat.S_IWRITE) + func(path) + + +def sql_escape_like(search: str, escape: str = "\\") -> str: + return ( + search.replace(escape, escape * 2) + .replace("%", f"{escape}%") + .replace("_", f"{escape}_") + ) + + +def get_envs_by_prefix(prefix: str) -> dict[str, str]: + """ + Function that searches env variables by some name prefix and returns + the ones found, but with prefix being excluded from it's names + """ + variables: dict[str, str] = {} + for env_name, env_value in os.environ.items(): + if env_name.startswith(prefix): + variables[env_name[len(prefix) :]] = env_value + + return variables + + +def import_object(object_spec): + filename, identifier = object_spec.rsplit(":", 1) + filename = filename.strip() + identifier = identifier.strip() + + if not identifier.isidentifier() or not filename.endswith(".py"): + raise ValueError(f"Invalid object spec: {object_spec}") + + modname = os.path.abspath(filename) + if modname in sys.modules: + module = sys.modules[modname] + else: + # Use importlib to find and load the module from the given filename + spec = importlib.util.spec_from_file_location(modname, filename) + module = importlib.util.module_from_spec(spec) + sys.modules[modname] = module + spec.loader.exec_module(module) + + return getattr(module, identifier) + + +def parse_params_string(params: str): + """ + Parse a string containing UDF class constructor parameters in the form + `a, b, key=val` into *args and **kwargs. + """ + args = [] + kwargs = {} + for part in params.split(): + if "=" in part: + key, val = part.split("=") + kwargs[key] = val + else: + args.append(part) + if any((args, kwargs)): + return args, kwargs + return None, None + + +_T_co = TypeVar("_T_co", covariant=True) + + +def batched(iterable: Iterable[_T_co], n: int) -> Iterator[tuple[_T_co, ...]]: + "Batch data into tuples of length n. The last batch may be shorter." + # Based on: https://docs.python.org/3/library/itertools.html#itertools-recipes + # batched('ABCDEFG', 3) --> ABC DEF G + if n < 1: + raise ValueError("Batch size must be at least one") + it = iter(iterable) + while batch := tuple(islice(it, n)): + yield batch + + +def flatten(items): + for item in items: + if isinstance(item, list): + yield from item + else: + yield item + + +def retry_with_backoff(retries=5, backoff_sec=1): + def retry(f): + def wrapper(*args, **kwargs): + num_tried = 0 + while True: + try: + return f(*args, **kwargs) + except Exception: + if num_tried == retries: + raise + sleep = ( + backoff_sec * 2** num_tried + random.uniform(0, 1) # noqa: S311 + ) + time.sleep(sleep) + num_tried += 1 + + return wrapper + + return retry + + +def determine_processes(parallel: Optional[Union[bool, int]]) -> Union[bool, int]: + if parallel is None and os.environ.get("DATACHAIN_SETTINGS_PARALLEL") is not None: + parallel = int(os.environ["DATACHAIN_SETTINGS_PARALLEL"]) + if parallel is None or parallel is False: + return False + if parallel is True: + return True + if parallel == 0: + return False + if parallel < 0: + return True + return parallel + + +def get_env_list( + key: str, default: Optional[Sequence] = None, sep: str = "," +) -> Optional[Sequence[str]]: + try: + str_val = os.environ[key] + except KeyError: + return default + return str_val.split(sep=sep) + + +def show_df( + df: "pd.DataFrame", collapse_columns: bool = True, system_columns: bool = False +) -> None: + import pandas as pd + + if df.empty: + return + + options: list[Any] = ["display.show_dimensions", False, "display.min_rows", 0] + if not collapse_columns: + options.extend(("display.max_columns", None)) # show all columns + options.extend(("display.max_colwidth", None)) # do not truncate cells + options.extend(("display.width", None)) # do not truncate table + + if not system_columns: + df.drop( + columns=[ + "dir_type", + "etag", + "is_latest", + "last_modified", + "owner_id", + "owner_name", + "size", + "version", + "vtype", + ], + inplace=True, + errors="ignore", + ) + + with pd.option_context("display.max_rows", None, *options): # show all rows + print(df) + + +def show_records( + records: Optional[list[dict]], + collapse_columns: bool = False, + system_columns: bool = False, +) -> None: + import pandas as pd + + if not records: + return + + df = pd.DataFrame.from_records(records) + return show_df(df, collapse_columns=collapse_columns, system_columns=system_columns) + + +class JSONSerialize(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, bytes): + return list(obj[:1024]) + if isinstance(obj, (datetime, date)): + return obj.isoformat() + if isinstance(obj, UUID): + return str(obj) + + return super().default(obj) + + +def inside_colab() -> bool: + try: + from google import colab # noqa: F401 + except ImportError: + return False + return True + + +def inside_notebook() -> bool: + if inside_colab(): + return True + + try: + shell = get_ipython().__class__.__name__ # type: ignore[name-defined] + except NameError: + return False + + if shell == "ZMQInteractiveShell": + try: + import IPython + + return IPython.__version__ >= "6.0.0" + except ImportError: + return False + + return False diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..6f4accc1c --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Test suite for the datachain package.""" diff --git a/tests/benchmarks/__init__.py b/tests/benchmarks/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/benchmarks/conftest.py b/tests/benchmarks/conftest.py new file mode 100644 index 000000000..ae3ef9ed1 --- /dev/null +++ b/tests/benchmarks/conftest.py @@ -0,0 +1,131 @@ +import os +import shutil +from subprocess import check_output + +import pytest +import virtualenv +from dulwich.porcelain import clone +from packaging import version + + +@pytest.fixture +def bucket(): + return "s3://noaa-bathymetry-pds/" + + +def pytest_generate_tests(metafunc): + str_revs = metafunc.config.getoption("--datachain-revs") + revs = str_revs.split(",") if str_revs else [None] + if "datachain_rev" in metafunc.fixturenames: + metafunc.parametrize("datachain_rev", revs, scope="session") + + +class VirtualEnv: + def __init__(self, path) -> None: + self.path = path + self.bin = self.path / ("Scripts" if os.name == "nt" else "bin") + + def create(self) -> None: + virtualenv.cli_run([os.fspath(self.path)]) + + def run(self, cmd: str, *args: str, env=None) -> None: + exe = self.which(cmd) + check_output([exe, *args], env=env) # noqa: S603 + + def which(self, cmd: str) -> str: + assert self.bin.exists() + return shutil.which(cmd, path=self.bin) or cmd + + +@pytest.fixture(scope="session", name="make_datachain_venv") +def fixture_make_datachain_venv(tmp_path_factory): + def _make_datachain_venv(name): + venv_dir = tmp_path_factory.mktemp(f"datachain-venv-{name}") + venv = VirtualEnv(venv_dir) + venv.create() + return venv + + return _make_datachain_venv + + +@pytest.fixture(scope="session", name="datachain_venvs") +def fixture_datachain_venvs(): + return {} + + +@pytest.fixture(scope="session", name="datachain_git_repo") +def fixture_datachain_git_repo(tmp_path_factory, test_config): + url = test_config.datachain_git_repo + + if os.path.isdir(url): + return url + + tmp_path = os.fspath(tmp_path_factory.mktemp("datachain-git-repo")) + clone(url, tmp_path) + + return tmp_path + + +@pytest.fixture(scope="session", name="datachain_bin") +def fixture_datachain_bin( + datachain_rev, + datachain_venvs, + make_datachain_venv, + datachain_git_repo, + test_config, +): + if datachain_rev: + venv = datachain_venvs.get(datachain_rev) + if not venv: + venv = make_datachain_venv(datachain_rev) + venv.run("pip", "install", "-U", "pip") + venv.run( + "pip", "install", f"git+file://{datachain_git_repo}@{datachain_rev}" + ) + datachain_venvs[datachain_rev] = venv + datachain_bin = venv.which("datachain") + else: + datachain_bin = test_config.datachain_bin + + def _datachain_bin(*args): + return check_output([datachain_bin, *args], text=True) # noqa: S603 + + actual = version.parse(_datachain_bin("--version")) + _datachain_bin.version = (actual.major, actual.minor, actual.micro) + + return _datachain_bin + + +@pytest.fixture(scope="function", name="make_bench") +def fixture_make_bench(request): + def _make_bench(name): + import pytest_benchmark.plugin + + # hack from https://github.com/ionelmc/pytest-benchmark/issues/166 + bench = pytest_benchmark.plugin.benchmark.__pytest_wrapped__.obj(request) + + suffix = f"-{name}" + + def add_suffix(_name): + start, sep, end = _name.partition("[") + return start + suffix + sep + end + + bench.name = add_suffix(bench.name) + bench.fullname = add_suffix(bench.fullname) + + return bench + + return _make_bench + + +@pytest.fixture( + scope="function", params=[pytest.param(None, marks=pytest.mark.benchmark)] +) +def bench_datachain(datachain_bin, make_bench): + def _bench_datachain(*args, **kwargs): + name = kwargs.pop("name", None) + name = f"-{name}" if name else "" + bench = make_bench(args[0] + name) + return bench.pedantic(datachain_bin, args=args, **kwargs) + + return _bench_datachain diff --git a/tests/benchmarks/test_ls.py b/tests/benchmarks/test_ls.py new file mode 100644 index 000000000..ed9a10494 --- /dev/null +++ b/tests/benchmarks/test_ls.py @@ -0,0 +1,2 @@ +def test_ls(bench_datachain, tmp_dir, bucket): + bench_datachain("ls", bucket, "--anon") diff --git a/tests/benchmarks/test_version.py b/tests/benchmarks/test_version.py new file mode 100644 index 000000000..fae3d6395 --- /dev/null +++ b/tests/benchmarks/test_version.py @@ -0,0 +1,2 @@ +def test_version(bench_datachain): + bench_datachain("--help", rounds=100) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..87ccfde12 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,551 @@ +import os +import os.path +import uuid +from collections.abc import Generator +from pathlib import PosixPath + +import attrs +import pytest +import pytest_servers.exceptions +import sqlalchemy +from pytest import MonkeyPatch, TempPathFactory +from upath.implementations.cloud import CloudPath + +from datachain.catalog import Catalog +from datachain.catalog.loader import get_id_generator, get_metastore, get_warehouse +from datachain.client.local import FileClient +from datachain.data_storage.sqlite import ( + SQLiteDatabaseEngine, + SQLiteIDGenerator, + SQLiteMetastore, + SQLiteWarehouse, +) +from datachain.dataset import DatasetRecord +from datachain.query.session import Session +from datachain.utils import DataChainDir, get_env_list + +from .utils import DEFAULT_TREE, get_simple_ds_query, instantiate_tree + +DEFAULT_DATACHAIN_BIN = "datachain" +DEFAULT_DATACHAIN_GIT_REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +collect_ignore = ["setup.py"] + + +@pytest.fixture(scope="session") +def monkeypatch_session() -> Generator[MonkeyPatch, None, None]: + """ + Like monkeypatch, but for session scope. + """ + mpatch = pytest.MonkeyPatch() + yield mpatch + mpatch.undo() + + +@pytest.fixture(scope="session", autouse=True) +def clean_environment( + monkeypatch_session: MonkeyPatch, + tmp_path_factory: TempPathFactory, +) -> None: + """ + Make sure we have a clean environment and won't write to userspace. + """ + working_dir = str(tmp_path_factory.mktemp("default_working_dir")) + monkeypatch_session.chdir(working_dir) + monkeypatch_session.delenv(DataChainDir.ENV_VAR, raising=False) + + +@pytest.fixture +def sqlite_db(): + return SQLiteDatabaseEngine.from_db_file(":memory:") + + +def cleanup_sqlite_db( + db: SQLiteDatabaseEngine, + cleanup_tables: list[str], +): + # Wipe the DB after the test + # Using new connection to check that the DB isn't locked + tables = db.execute_str( + "SELECT name FROM sqlite_master WHERE type='table'" + ).fetchall() + + # removing in reversed order because of foreign keys + for table in reversed(cleanup_tables): + db.execute_str(f"DROP TABLE IF EXISTS '{table}'") + + for (table,) in tables: + name = table.replace("'", "''") + db.execute_str(f"DROP TABLE IF EXISTS '{name}'") + + # Close the connection so that the SQLite file is no longer open, to avoid + # pytest throwing: OSError: [Errno 24] Too many open files + db.close() + + +@pytest.fixture +def id_generator(): + if os.environ.get("DATACHAIN_ID_GENERATOR"): + _id_generator = get_id_generator() + yield _id_generator + + _id_generator.cleanup_for_tests() + else: + db = SQLiteDatabaseEngine.from_db_file(":memory:") + _id_generator = SQLiteIDGenerator(db) + yield _id_generator + + _id_generator.cleanup_for_tests() + + # Close the connection so that the SQLite file is no longer open, to avoid + # pytest throwing: OSError: [Errno 24] Too many open files + _id_generator.db.close() + + +@pytest.fixture +def metastore(id_generator): + if os.environ.get("DATACHAIN_METASTORE"): + _metastore = get_metastore(id_generator) + yield _metastore + + _metastore.cleanup_for_tests() + else: + _metastore = SQLiteMetastore(id_generator, db_file=":memory:") + yield _metastore + + cleanup_sqlite_db(_metastore.db.clone(), _metastore.default_table_names) + Session.cleanup_for_tests() + + # Close the connection so that the SQLite file is no longer open, to avoid + # pytest throwing: OSError: [Errno 24] Too many open files + _metastore.db.close() + + +def check_temp_tables_cleaned_up(original_warehouse): + """Ensure that temporary tables are cleaned up.""" + warehouse = original_warehouse.clone() + assert [ + t + for t in sqlalchemy.inspect(warehouse.db.engine).get_table_names() + if t.startswith( + (warehouse.UDF_TABLE_NAME_PREFIX, warehouse.TMP_TABLE_NAME_PREFIX) + ) + ] == [] + + +@pytest.fixture +def warehouse(id_generator, metastore): + if os.environ.get("DATACHAIN_WAREHOUSE"): + _warehouse = get_warehouse(id_generator) + yield _warehouse + try: + check_temp_tables_cleaned_up(_warehouse) + finally: + _warehouse.cleanup_for_tests() + else: + _warehouse = SQLiteWarehouse(id_generator, db_file=":memory:") + yield _warehouse + try: + check_temp_tables_cleaned_up(_warehouse) + finally: + cleanup_sqlite_db(_warehouse.db.clone(), metastore.default_table_names) + + # Close the connection so that the SQLite file is no longer open, to avoid + # pytest throwing: OSError: [Errno 24] Too many open files + _warehouse.db.close() + + +@pytest.fixture +def catalog(id_generator, metastore, warehouse): + return Catalog(id_generator=id_generator, metastore=metastore, warehouse=warehouse) + + +@pytest.fixture +def id_generator_tmpfile(tmp_path): + if os.environ.get("DATACHAIN_ID_GENERATOR"): + _id_generator = get_id_generator() + yield _id_generator + + _id_generator.cleanup_for_tests() + else: + db = SQLiteDatabaseEngine.from_db_file(tmp_path / "test.db") + _id_generator = SQLiteIDGenerator(db) + yield _id_generator + + _id_generator.cleanup_for_tests() + + # Close the connection so that the SQLite file is no longer open, to avoid + # pytest throwing: OSError: [Errno 24] Too many open files + _id_generator.db.close() + + +@pytest.fixture +def metastore_tmpfile(tmp_path, id_generator_tmpfile): + if os.environ.get("DATACHAIN_METASTORE"): + _metastore = get_metastore(id_generator_tmpfile) + yield _metastore + + _metastore.cleanup_for_tests() + else: + _metastore = SQLiteMetastore(id_generator_tmpfile, db_file=tmp_path / "test.db") + yield _metastore + + cleanup_sqlite_db(_metastore.db.clone(), _metastore.default_table_names) + Session.cleanup_for_tests() + + # Close the connection so that the SQLite file is no longer open, to avoid + # pytest throwing: OSError: [Errno 24] Too many open files + _metastore.db.close() + + +@pytest.fixture +def warehouse_tmpfile(tmp_path, id_generator_tmpfile, metastore_tmpfile): + if os.environ.get("DATACHAIN_WAREHOUSE"): + _warehouse = get_warehouse(id_generator_tmpfile) + yield _warehouse + try: + check_temp_tables_cleaned_up(_warehouse) + finally: + _warehouse.cleanup_for_tests() + else: + _warehouse = SQLiteWarehouse(id_generator_tmpfile, db_file=tmp_path / "test.db") + yield _warehouse + try: + check_temp_tables_cleaned_up(_warehouse) + finally: + cleanup_sqlite_db( + _warehouse.db.clone(), metastore_tmpfile.default_table_names + ) + + # Close the connection so that the SQLite file is no longer open, to avoid + # pytest throwing: OSError: [Errno 24] Too many open files + _warehouse.db.close() + + +@pytest.fixture +def tmp_dir(tmp_path_factory, monkeypatch): + dpath = tmp_path_factory.mktemp("datachain-test") + monkeypatch.chdir(dpath) + return dpath + + +def pytest_addoption(parser): + parser.addoption( + "--datachain-bin", + type=str, + default=DEFAULT_DATACHAIN_BIN, + help="Path to datachain binary", + ) + + parser.addoption( + "--datachain-revs", + type=str, + help="Comma-separated list of DataChain revisions to test " + "(overrides `--datachain-bin`)", + ) + + parser.addoption( + "--datachain-git-repo", + type=str, + default=DEFAULT_DATACHAIN_GIT_REPO, + help="Path or url to datachain git repo", + ) + parser.addoption( + "--azure-connection-string", + type=str, + help=( + "Connection string to run tests against a real, versioned " + "Azure storage account" + ), + ) + + +class DataChainTestConfig: + def __init__(self): + self.datachain_bin = DEFAULT_DATACHAIN_BIN + self.datachain_revs = None + self.datachain_git_repo = DEFAULT_DATACHAIN_GIT_REPO + + +@pytest.fixture(scope="session") +def test_config(request): + return request.config.datachain_config + + +def pytest_configure(config): + config.datachain_config = DataChainTestConfig() + + config.datachain_config.datachain_bin = config.getoption("--datachain-bin") + config.datachain_config.datachain_revs = config.getoption("--datachain-revs") + config.datachain_config.datachain_git_repo = config.getoption( + "--datachain-git-repo" + ) + + +@pytest.fixture(scope="session", params=[DEFAULT_TREE]) +def tree(request): + return request.param + + +@attrs.define +class CloudServer: + kind: str + src: CloudPath + client_config: dict[str, str] + + @property + def src_uri(self): + if self.kind == "file": + return self.src.as_uri() + return str(self.src).rstrip("/") + + +def make_cloud_server(src_path, cloud_type, tree): + fs = src_path.fs + if cloud_type == "s3": + endpoint_url = fs.client_kwargs["endpoint_url"] + client_config = {"aws_endpoint_url": endpoint_url} + elif cloud_type in ("gs", "gcs"): + endpoint_url = fs._endpoint + client_config = {"endpoint_url": endpoint_url} + elif cloud_type == "azure": + client_config = fs.storage_options.copy() + elif cloud_type == "file": + client_config = {} + else: + raise ValueError(f"invalid cloud_type: {cloud_type}") + + instantiate_tree(src_path, tree) + return CloudServer(kind=cloud_type, src=src_path, client_config=client_config) + + +@attrs.define +class CloudTestCatalog: + server: CloudServer + working_dir: PosixPath + catalog: Catalog + + @property + def src(self): + return self.server.src + + @property + def src_uri(self): + return self.server.src_uri + + @property + def storage_uri(self): + if self.server.kind == "file": + return FileClient.root_path().as_uri() + return self.server.src_uri + + @property + def partial_path(self): + if self.server.kind == "file": + _, rel_path = FileClient.split_url(self.src_uri) + return rel_path + return "" + + @property + def client_config(self): + return self.server.client_config + + +@pytest.fixture(scope="session", params=["file", "s3", "gs", "azure"]) +def cloud_type(request): + return request.param + + +@pytest.fixture(scope="session", params=[False, True]) +def version_aware(request): + return request.param + + +@pytest.fixture(scope="session") +def cloud_server(request, tmp_upath_factory, cloud_type, version_aware, tree): + # DATACHAIN_TEST_SKIP_MISSING_REMOTES can be set to a comma-separated list + # of remotes to skip tests for if unavailable or "all" to skip all + # unavailable remotes: + # DATACHAIN_TEST_SKIP_MISSING_REMOTES=azure,gs + # DATACHAIN_TEST_SKIP_MISSING_REMOTES=all + skip_missing_remotes = set(get_env_list("DATACHAIN_TEST_SKIP_MISSING_REMOTES", [])) + try: + if cloud_type == "azure" and version_aware: + if conn_str := request.config.getoption("--azure-connection-string"): + src_path = tmp_upath_factory.azure(conn_str) + else: + pytest.skip("Can't test versioning with Azure") + elif cloud_type == "file": + if version_aware: + pytest.skip("Local storage can't be versioned") + else: + src_path = tmp_upath_factory.mktemp("local") + else: + src_path = tmp_upath_factory.mktemp(cloud_type, version_aware=version_aware) + except pytest_servers.exceptions.RemoteUnavailable as exc: + if "all" in skip_missing_remotes or cloud_type in skip_missing_remotes: + pytest.skip(str(exc)) + raise + + return make_cloud_server(src_path, cloud_type, tree) + + +@pytest.fixture() +def datachain_job_id(monkeypatch): + job_id = uuid.uuid4().hex + monkeypatch.setenv("DATACHAIN_JOB_ID", job_id) + + +@pytest.fixture +def cloud_server_credentials(cloud_server, monkeypatch): + if cloud_server.kind == "s3": + cfg = cloud_server.src.fs.client_kwargs + try: + monkeypatch.delenv("AWS_PROFILE") + except KeyError: + pass + monkeypatch.setenv("AWS_ACCESS_KEY_ID", cfg.get("aws_access_key_id")) + monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", cfg.get("aws_secret_access_key")) + monkeypatch.setenv("AWS_SESSION_TOKEN", cfg.get("aws_session_token")) + monkeypatch.setenv("AWS_DEFAULT_REGION", cfg.get("region_name")) + + +def get_cloud_test_catalog(cloud_server, tmp_path, id_generator, metastore, warehouse): + cache_dir = tmp_path / ".datachain" / "cache" + cache_dir.mkdir(parents=True) + tmpfile_dir = tmp_path / ".datachain" / "tmp" + tmpfile_dir.mkdir() + + catalog = Catalog( + id_generator=id_generator, + metastore=metastore, + warehouse=warehouse, + cache_dir=str(cache_dir), + tmp_dir=str(tmpfile_dir), + client_config=cloud_server.client_config, + ) + return CloudTestCatalog(server=cloud_server, working_dir=tmp_path, catalog=catalog) + + +@pytest.fixture +def cloud_test_catalog( + cloud_server, + cloud_server_credentials, + tmp_path, + id_generator, + metastore, + warehouse, +): + return get_cloud_test_catalog( + cloud_server, tmp_path, id_generator, metastore, warehouse + ) + + +@pytest.fixture +def cloud_test_catalog_tmpfile( + cloud_server, + cloud_server_credentials, + tmp_path, + id_generator_tmpfile, + metastore_tmpfile, + warehouse_tmpfile, +): + return get_cloud_test_catalog( + cloud_server, + tmp_path, + id_generator_tmpfile, + metastore_tmpfile, + warehouse_tmpfile, + ) + + +@pytest.fixture +def listed_bucket(cloud_test_catalog): + list(cloud_test_catalog.catalog.ls([cloud_test_catalog.src_uri], fields=["name"])) + + +@pytest.fixture +def dogs_dataset(listed_bucket, cloud_test_catalog): + name = uuid.uuid4().hex + catalog = cloud_test_catalog.catalog + src_uri = cloud_test_catalog.src_uri + dataset = catalog.create_dataset_from_sources( + name, [f"{src_uri}/dogs/*"], recursive=True + ) + return catalog.update_dataset( + dataset, {"description": "dogs dataset", "labels": ["dogs", "dataset"]} + ) + + +@pytest.fixture +def cats_dataset(listed_bucket, cloud_test_catalog): + name = uuid.uuid4().hex + catalog = cloud_test_catalog.catalog + src_uri = cloud_test_catalog.src_uri + dataset = catalog.create_dataset_from_sources( + name, [f"{src_uri}/cats/*"], recursive=True + ) + return catalog.update_dataset( + dataset, {"description": "cats dataset", "labels": ["cats", "dataset"]} + ) + + +@pytest.fixture +def simple_ds_query(cloud_test_catalog): + return get_simple_ds_query( + path=cloud_test_catalog.src_uri, catalog=cloud_test_catalog.catalog + ) + + +@pytest.fixture +def dataset_record(): + return DatasetRecord( + id=1, + name=f"ds_{uuid.uuid4().hex}", + description="", + labels=[], + versions=[], + shadow=False, + status=1, + schema={}, + feature_schema={}, + ) + + +@pytest.fixture +def dataset_rows(): + int_example = 25 + return [ + { + "id": i, + "location": "", + "source": "s3://my-bucket", + "dir_type": 0, + "vtype": "", + "parent": "input/text_emd_1m", + "version": "7e589b7d-382c-49a5-931f-2b999c930c5e", + "is_latest": True, + "name": f"dql_1m_meta_text_emd.parquet_3_{i}_0.snappy.parquet", + "etag": f"72b35c8e9b8eed1636c91eb94241c2f8-{i}", + "owner_id": "owner", + "owner_name": "aws-iterative-sandbox", + "last_modified": "2024-02-23T10:42:31.842944+00:00", + "size": 49807360, + "random": 12123123123, + "int_col": 5, + "int_col_32": 5, + "int_col_64": 5, + "float_col": 0.5, + "float_col_32": 0.5, + "float_col_64": 0.5, + "array_col": [0.5], + "array_col_nested": [[0.5]], + "array_col_32": [0.5], + "array_col_64": [0.5], + "string_col": "a string", + "bool_col": True, + "json_col": '{"a": 1}', + "binary_col": int_example.to_bytes(2, "big"), + } + for i in range(19) + ] diff --git a/tests/data.py b/tests/data.py new file mode 100644 index 000000000..090fa9cd7 --- /dev/null +++ b/tests/data.py @@ -0,0 +1,126 @@ +from datetime import datetime, timezone + +from datachain.node import Entry + +utc = timezone.utc +TIME_ZERO = datetime.fromtimestamp(0, tz=utc) + +ENTRIES = [ + Entry.from_file( + parent="", + name="description", + etag="60a7605e934638ab9113e0f9cf852239", + version="7e589b7d-382c-49a5-931f-2b999c930c5e", + is_latest=True, + last_modified=datetime(2023, 2, 27, 18, 28, 54, tzinfo=utc), + size=13, + owner_name="webfile", + owner_id="75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a", + ), + Entry.from_file( + parent="cats", + name="cat1", + etag="4a4be40c96ac6314e91d93f38043a634", + version="309eb4a4-bba9-47c1-afcd-d7c51110af6f", + is_latest=True, + last_modified=datetime(2023, 2, 27, 18, 28, 54, tzinfo=utc), + size=4, + owner_name="webfile", + owner_id="75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a", + ), + Entry.from_file( + parent="cats", + name="cat2", + etag="0268c692ff940a830e1e7296aa48c176", + version="f9d168d3-6d1b-47ef-8f6a-81fce48de141", + is_latest=True, + last_modified=datetime(2023, 2, 27, 18, 28, 54, tzinfo=utc), + size=4, + owner_name="webfile", + owner_id="75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a", + ), + Entry.from_file( + parent="dogs", + name="dog1", + etag="8fdb60801e9d39a5286aa01dd1f4f4f3", + version="b9c31cf7-d011-466a-bf16-cf9da0cb422a", + is_latest=True, + last_modified=datetime(2023, 2, 27, 18, 28, 54, tzinfo=utc), + size=4, + owner_name="webfile", + owner_id="75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a", + ), + Entry.from_file( + parent="dogs", + name="dog2", + etag="2d50c921b22aa164a56c68d71eeb4100", + version="3a8bb6d9-38db-47a8-8bcb-8972ea95aa20", + is_latest=True, + last_modified=datetime(2023, 2, 27, 18, 28, 54, tzinfo=utc), + size=3, + owner_name="webfile", + owner_id="75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a", + ), + Entry.from_file( + parent="dogs", + name="dog3", + etag="33c6c2397a1b079e903c474df792d0e2", + version="ee49e963-36a8-492a-b03a-e801b93afb40", + is_latest=True, + last_modified=datetime(2023, 2, 27, 18, 28, 54, tzinfo=utc), + size=4, + owner_name="webfile", + owner_id="75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a", + ), + Entry.from_file( + parent="dogs/others", + name="dog4", + etag="a5e1a5d93ff242b745f5cf87aeb726d5", + version="c5969421-6900-4060-bc39-d54f4a49b9fc", + is_latest=True, + last_modified=datetime(2023, 2, 27, 18, 28, 54, tzinfo=utc), + size=4, + owner_name="webfile", + owner_id="75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a", + ), +] + +# files with directory name collisions: +# dogs/others/ +# dogs/others +# dogs/ +INVALID_ENTRIES = [ + Entry.from_file( + parent="dogs/others", + name="", + etag="68b329da9893e34099c7d8ad5cb9c940", + version="85969421-6900-4060-bc39-d54f4a49b9ab", + is_latest=True, + last_modified=datetime(2023, 2, 27, 18, 28, 54, tzinfo=utc), + size=4, + owner_name="webfile", + owner_id="75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a", + ), + Entry.from_file( + parent="dogs", + name="others", + etag="68b329da9893e34099c7d8ad5cb9c940", + version="85969421-6900-4060-bc39-d54f4a49b9ab", + is_latest=True, + last_modified=datetime(2023, 2, 27, 18, 28, 54, tzinfo=utc), + size=4, + owner_name="webfile", + owner_id="75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a", + ), + Entry.from_file( + parent="dogs", + name="", + etag="68b329da9893e34099c7d8ad5cb9c940", + version="85969421-6900-4060-bc39-d54f4a49b9ab", + is_latest=True, + last_modified=datetime(2023, 2, 27, 18, 28, 54, tzinfo=utc), + size=4, + owner_name="webfile", + owner_id="75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a", + ), +] diff --git a/tests/examples/__init__.py b/tests/examples/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/examples/test_wds_e2e.py b/tests/examples/test_wds_e2e.py new file mode 100644 index 000000000..63e203f37 --- /dev/null +++ b/tests/examples/test_wds_e2e.py @@ -0,0 +1,130 @@ +import io +import json +import tarfile +from pathlib import Path + +import pandas as pd +import pytest + +from datachain.client.local import FileClient +from datachain.lib.dc import DataChain +from datachain.lib.file import File +from datachain.lib.webdataset import process_webdataset +from datachain.lib.webdataset_laion import Laion, WDSLaion +from tests.examples.wds_data import WDS_META, WDS_TAR_SHARDS + + +@pytest.fixture +def webdataset_tars(tmp_path): + """ + Creates tar file with webdataset data (.json, .txt and .jpg files in it) + Returns path to a directory of tar file + """ + data_path = tmp_path / "datacomp-sample" + shards_path = data_path / "shards" + + fh = io.BytesIO() + with tarfile.open(fileobj=fh, mode="w:gz") as tar: + for idx, rec in enumerate(WDS_TAR_SHARDS): + # json file + data = json.dumps(rec).encode() + info = tarfile.TarInfo(f"{idx}.json") + info.size = len(data) + tar.addfile(info, io.BytesIO(data)) + + # image file + data = b"123" # some dummy data + info = tarfile.TarInfo(f"{idx}.jpg") + info.size = len(data) + tar.addfile(info, io.BytesIO(data)) + + # txt file + data = rec["caption"].encode() + info = tarfile.TarInfo(f"{idx}.txt") + info.size = len(data) + tar.addfile(info, io.BytesIO(data)) + + shards_path.mkdir(parents=True, exist_ok=True) + with open(shards_path / "00000000.tar", "wb") as f: + f.write(fh.getvalue()) + + return shards_path + + +@pytest.fixture +def webdataset_metadata(tmp_path): + """ + Creates webdataset metadata parquet file which goes with webdataset_tars + fixture + Returns path to a directory of parquet file + """ + data_path = tmp_path / "datacomp-sample" + metadata_path = data_path / "metadata" + + metadata_path.mkdir(parents=True, exist_ok=True) + + df = pd.DataFrame.from_dict(WDS_META) + df.to_parquet(metadata_path / "00000000.parquet") + + return metadata_path + + +def test_wds(catalog, webdataset_tars): + res = DataChain.from_storage(Path(webdataset_tars).as_uri()).gen( + laion=process_webdataset(spec=WDSLaion), params="file" + ) + + num_rows = 0 + for laion_wds in res.iterate_one("laion"): + num_rows += 1 + assert isinstance(laion_wds, WDSLaion) + idx, data = next( + (i, d) + for i, d in enumerate(WDS_TAR_SHARDS) + if d["uid"] == laion_wds.json.uid + ) + + assert laion_wds.txt == data["caption"] + assert laion_wds.file.location + assert laion_wds.file.source == FileClient.root_path().as_uri() + assert laion_wds.file.parent + assert laion_wds.file.name == f"{idx}.jpg" + assert laion_wds.file.location + assert laion_wds.json.dict() == Laion(**data).dict() + + assert num_rows == len(WDS_TAR_SHARDS) + + +def test_wds_merge_with_parquet_meta(catalog, webdataset_tars, webdataset_metadata): + wds = DataChain.from_storage(Path(webdataset_tars).as_uri()).gen( + laion=process_webdataset(spec=WDSLaion), params="file" + ) + + meta = DataChain.from_storage(Path(webdataset_metadata).as_uri()).parse_parquet() + + res = wds.merge(meta, on="laion.json.uid", right_on="uid") + + num_rows = 0 + for r in res.collect_one("laion"): + num_rows += 1 + assert isinstance(r, WDSLaion) + assert isinstance(r.file, File) + assert isinstance(r.json, Laion) + data = next(d for d in WDS_TAR_SHARDS if d["uid"] == r.json.uid) + assert r.txt == data["caption"] + assert r.json.uid == data["uid"] + + assert num_rows == len(WDS_TAR_SHARDS) + + meta_res = res.collect(*WDS_META.keys()) + + for field_name_idx, rows_values in enumerate(WDS_META.values()): + assert sorted(rows_values.values()) == sorted( + [r[field_name_idx] for r in meta_res] + ) + + # validate correct merge + for laion_uid, uid in res.iterate("laion.json.uid", "uid"): + assert laion_uid == uid + for caption, text in res.iterate("laion.json.caption", "text"): + assert caption == text diff --git a/tests/examples/wds_data.py b/tests/examples/wds_data.py new file mode 100644 index 000000000..90a7ceca1 --- /dev/null +++ b/tests/examples/wds_data.py @@ -0,0 +1,153 @@ +# data that should go to `.json` files of webdataset +WDS_TAR_SHARDS = [ + { + "uid": "d142ae70686e14ccc379c01a571501b5", + "face_bboxes": [], + "caption": "Customizing Windows 7 Setup Please Help Solved", + "url": "https://i.imgur.com/mXQrfNs.png", + "key": "000000000000", + "status": "success", + "error_message": None, + "width": 512, + "height": 270, + "original_width": 1704, + "original_height": 899, + "exif": "{}", + "sha256": "4052f3d8b1f5acee73e234ac8e7c614c5a6e10312d21a65d77b3ffad4edd6b31", + }, + { + "uid": "99e71895357f06965a6c5b00d506e5aa", + "face_bboxes": [ + [ + 0.5005972981452942, + 0.13604149222373962, + 0.8109994530677795, + 0.7247588038444519, + ] + ], + "caption": "Jack Strong Official Trailer 1 (2015) Patrick Wilson Drama Thriller HD", + "url": "http://i.ytimg.com/vi/if2V1iszwuA/default.jpg", + "key": "000000000001", + "status": "success", + "error_message": None, + "width": 120, + "height": 90, + "original_width": 120, + "original_height": 90, + "exif": "{}", + "sha256": "9619f8da8b04628994741ed7626d88ace59962824aff43407fee95da488d0ca7", + }, + { + "uid": "abff9068a3bf22d8325b61a9b1404009", + "face_bboxes": [], + "caption": "mercedes jeep 2015 mercedes jeep 2015 mercedes g63 amg best images collections", + "url": "http://t0.gstatic.com/images?q=tbn:ANd9GcScIHR33LnMpupkxbZRqnj1YMvOXsc9uUTj8Wa2v8bhSjWTxTRo1w", + "key": "000000000002", + "status": "success", + "error_message": None, + "width": 275, + "height": 183, + "original_width": 275, + "original_height": 183, + "exif": "{}", + "sha256": "042442926251b1d05345715ea91fb3b2f77b37b2c43af37cbf32cab54a0e6b1a", + }, + { + "uid": "7fa8b7f6ececc9dc80434f3cb6897c27", + "face_bboxes": [], + "caption": "WEN Fall Ginger Pumpkin Cleansing Conditioner ~ 16 oz ~ sealed plus 6 oz Mist", + "url": "http://thumbs.ebaystatic.com/images/g/5kAAAOSwc1FXcDFI/s-l225.jpg", + "key": "000000000003", + "status": "success", + "error_message": None, + "width": 80, + "height": 80, + "original_width": 80, + "original_height": 80, + "exif": "{}", + "sha256": "a567462f4edd496bdf5cd00da5bbde64131c283e3cf396bfd58c0fac26b13d9a", + }, + { + "uid": "1e9a6ad5ad6b0a3eef0582b2271c1e8f", + "face_bboxes": [], + "caption": "Couleur", + "url": "https://www.dhresource.com/600x600/f2/albu/g8/M00/78/73/rBVaV150TlWAcLR4AAHizzfChbU318.jpg", + "key": "000000000004", + "status": "success", + "error_message": None, + "width": 512, + "height": 384, + "original_width": 600, + "original_height": 450, + "exif": '{"Image Software": "www.meitu.com", "Image ExifOffset": "52", "EXIF ColorSpace": "sRGB", "EXIF ExifImageWidth": "1080", "EXIF ExifImageLength": "1440"}', + "sha256": "8f1095d595820272bbb79796c67d0cb86e2f8cafa18fd17579d89e3681fa3086", + }, +] + + +# data that represents metadata and goes to webdataset parquet file of webdataset +WDS_META = { + "uid": { + "0": "d142ae70686e14ccc379c01a571501b5", + "1": "99e71895357f06965a6c5b00d506e5aa", + "2": "abff9068a3bf22d8325b61a9b1404009", + "3": "7fa8b7f6ececc9dc80434f3cb6897c27", + "4": "1e9a6ad5ad6b0a3eef0582b2271c1e8f", + }, + "url": { + "0": "https://i.imgur.com/mXQrfNs.png", + "1": "http://i.ytimg.com/vi/if2V1iszwuA/default.jpg", + "2": "http://t0.gstatic.com/images?q=tbn:ANd9GcScIHR33LnMpupkxbZRqnj1YMvOXsc9uUTj8Wa2v8bhSjWTxTRo1w", + "3": "http://thumbs.ebaystatic.com/images/g/5kAAAOSwc1FXcDFI/s-l225.jpg", + "4": "https://www.dhresource.com/600x600/f2/albu/g8/M00/78/73/rBVaV150TlWAcLR4AAHizzfChbU318.jpg", + }, + "text": { + "0": "Customizing Windows 7 Setup Please Help Solved", + "1": "Jack Strong Official Trailer 1 (2015) Patrick Wilson Drama Thriller HD", + "2": "mercedes jeep 2015 mercedes jeep 2015 mercedes g63 amg best images collections", + "3": "WEN Fall Ginger Pumpkin Cleansing Conditioner ~ 16 oz ~ sealed plus 6 oz Mist", + "4": "Couleur", + }, + "original_width": { + "0": 1704, + "1": 120, + "2": 275, + "3": 80, + "4": 600, + }, + "original_height": { + "0": 899, + "1": 90, + "2": 183, + "3": 80, + "4": 450, + }, + "clip_b32_similarity_score": { + "0": 0.2734375, + "1": 0.3813476562, + "2": 0.3312988281, + "3": 0.2091064453, + "4": 0.2038574219, + }, + "clip_l14_similarity_score": { + "0": 0.2553710938, + "1": 0.3391113281, + "2": 0.2318115234, + "3": 0.1966552734, + "4": 0.1300048828, + }, + "face_bboxes": { + "0": [], + "1": [[0.5005972981, 0.1360414922, 0.8109994531, 0.7247588038]], + "2": [], + "3": [], + "4": [], + }, + "sha256": { + "0": "4052f3d8b1f5acee73e234ac8e7c614c5a6e10312d21a65d77b3ffad4edd6b31", + "1": "9619f8da8b04628994741ed7626d88ace59962824aff43407fee95da488d0ca7", + "2": "042442926251b1d05345715ea91fb3b2f77b37b2c43af37cbf32cab54a0e6b1a", + "3": "a567462f4edd496bdf5cd00da5bbde64131c283e3cf396bfd58c0fac26b13d9a", + "4": "8f1095d595820272bbb79796c67d0cb86e2f8cafa18fd17579d89e3681fa3086", + }, +} diff --git a/tests/func/__init__.py b/tests/func/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/func/test_catalog.py b/tests/func/test_catalog.py new file mode 100644 index 000000000..a6e790adf --- /dev/null +++ b/tests/func/test_catalog.py @@ -0,0 +1,1149 @@ +import io +import json +import os +from contextlib import suppress +from pathlib import Path +from textwrap import dedent +from urllib.parse import urlparse + +import pytest +import yaml +from fsspec.implementations.local import LocalFileSystem + +from datachain.catalog import parse_edatachain_file +from datachain.cli import garbage_collect +from datachain.error import ( + QueryScriptCompileError, + QueryScriptDatasetNotFound, + QueryScriptRunError, + StorageNotFoundError, +) +from tests.data import ENTRIES +from tests.utils import ( + DEFAULT_TREE, + TARRED_TREE, + assert_row_names, + create_tar_dataset, + make_index, + skip_if_not_sqlite, + tree_from_path, +) + + +@pytest.fixture +def pre_created_ds_name(): + return "pre_created_dataset" + + +@pytest.fixture +def mock_os_pipe(mocker): + r, w = os.pipe() + mocker.patch("os.pipe", return_value=(r, w)) + + try: + yield (r, w) + finally: + with suppress(OSError): + os.close(r) + with suppress(OSError): + os.close(w) + + +@pytest.fixture +def mock_popen(mocker): + m = mocker.patch( + "subprocess.Popen", returncode=0, stdout=io.StringIO(), stderr=io.StringIO() + ) + m.return_value.__enter__.return_value = m + # keep in sync with the returncode + m.poll.side_effect = lambda: m.returncode if m.poll.call_count > 1 else None + return m + + +@pytest.fixture +def mock_popen_dataset_created( + mock_popen, cloud_test_catalog, mock_os_pipe, listed_bucket +): + # create dataset which would be created in subprocess + ds_name = cloud_test_catalog.catalog.generate_query_dataset_name() + ds_version = 1 + cloud_test_catalog.catalog.create_dataset_from_sources( + ds_name, + [f"{cloud_test_catalog.src_uri}/dogs/*"], + recursive=True, + ) + + _, w = mock_os_pipe + with open(w, mode="w", closefd=False) as f: + f.write(json.dumps({"dataset": (ds_name, ds_version)})) + + mock_popen.configure_mock(stdout=io.StringIO("user log 1\nuser log 2")) + yield mock_popen + + +@pytest.fixture +def fake_index(catalog): + src = "s3://whatever" + make_index(catalog, src, ENTRIES) + return src + + +def test_find(catalog, fake_index): + src_uri = fake_index + dirs = ["cats/", "dogs/", "dogs/others/"] + expected_paths = dirs + [entry.full_path for entry in ENTRIES] + assert set(catalog.find([src_uri])) == { + f"{src_uri}/{path}" for path in expected_paths + } + + with pytest.raises(FileNotFoundError): + set(catalog.find([f"{src_uri}/does_not_exist"])) + + +def test_find_names_paths_size_type(catalog, fake_index): + src_uri = fake_index + + assert set(catalog.find([src_uri], names=["*cat*"])) == { + f"{src_uri}/cats/", + f"{src_uri}/cats/cat1", + f"{src_uri}/cats/cat2", + } + + assert set(catalog.find([src_uri], names=["*cat*"], typ="dir")) == { + f"{src_uri}/cats/", + } + + assert len(list(catalog.find([src_uri], names=["*CAT*"]))) == 0 + + assert set(catalog.find([src_uri], inames=["*CAT*"])) == { + f"{src_uri}/cats/", + f"{src_uri}/cats/cat1", + f"{src_uri}/cats/cat2", + } + + assert set(catalog.find([src_uri], paths=["*cats/cat*"])) == { + f"{src_uri}/cats/cat1", + f"{src_uri}/cats/cat2", + } + + assert len(list(catalog.find([src_uri], paths=["*caTS/CaT**"]))) == 0 + + assert set(catalog.find([src_uri], ipaths=["*caTS/CaT*"])) == { + f"{src_uri}/cats/cat1", + f"{src_uri}/cats/cat2", + } + + assert set(catalog.find([src_uri], size="5", typ="f")) == { + f"{src_uri}/description", + } + + assert set(catalog.find([src_uri], size="-3", typ="f")) == { + f"{src_uri}/dogs/dog2", + } + + +def test_find_names_columns(cloud_test_catalog, cloud_type): + src_uri = cloud_test_catalog.src_uri + catalog = cloud_test_catalog.catalog + + owner = "webfile" if cloud_type == "s3" else "" + + src_uri_path = src_uri + if cloud_type == "file": + src_uri_path = LocalFileSystem._strip_protocol(src_uri) + + assert set( + catalog.find( + [src_uri], + names=["*cat*"], + columns=["du", "name", "owner", "path", "size", "type"], + ) + ) == { + "\t".join(columns) + for columns in [ + ["8", "cats", "", f"{src_uri_path}/cats/", "0", "d"], + ["4", "cat1", owner, f"{src_uri_path}/cats/cat1", "4", "f"], + ["4", "cat2", owner, f"{src_uri_path}/cats/cat2", "4", "f"], + ] + } + + +@pytest.mark.parametrize( + "recursive,star,dir_exists", + ( + (True, True, False), + (True, False, False), + (True, False, True), + (False, True, False), + (False, False, False), + ), +) +def test_cp_root(cloud_test_catalog, recursive, star, dir_exists, cloud_type): + src_uri = cloud_test_catalog.src_uri + working_dir = cloud_test_catalog.working_dir + catalog = cloud_test_catalog.catalog + + dest = working_dir / "data" + + if star: + src_path = f"{src_uri}/*" + else: + src_path = src_uri + if cloud_type == "file": + src_path += "/" + + if star: + with pytest.raises(FileNotFoundError): + catalog.cp([src_path], str(dest), recursive=recursive) + + if dir_exists or star: + dest.mkdir() + + catalog.cp([src_path], str(dest), recursive=recursive) + + if not star and not recursive: + # The root directory is skipped, so nothing is copied + assert tree_from_path(dest) == {} + return + + assert (dest / "description").read_text() == "Cats and Dogs" + + # Testing DataChain File Contents + assert dest.with_suffix(".edatachain").is_file() + edatachain_contents = yaml.safe_load(dest.with_suffix(".edatachain").read_text()) + assert len(edatachain_contents) == 1 + data = edatachain_contents[0] + assert data["data-source"]["uri"] == src_path.rstrip("/") + expected_file_count = 7 if recursive else 1 + assert len(data["files"]) == expected_file_count + files_by_name = {f["name"]: f for f in data["files"]} + + # Directories should never be saved + assert "cats" not in files_by_name + assert "dogs" not in files_by_name + assert "others" not in files_by_name + assert "dogs/others" not in files_by_name + + # Description is always copied (if anything is copied) + prefix = ( + "" if star or (recursive and not dir_exists) or cloud_type == "file" else "/" + ) + assert files_by_name[f"{prefix}description"]["size"] == 13 + + if recursive: + assert tree_from_path(dest) == DEFAULT_TREE + assert files_by_name[f"{prefix}cats/cat1"]["size"] == 4 + assert files_by_name[f"{prefix}cats/cat2"]["size"] == 4 + assert files_by_name[f"{prefix}dogs/dog1"]["size"] == 4 + assert files_by_name[f"{prefix}dogs/dog2"]["size"] == 3 + assert files_by_name[f"{prefix}dogs/dog3"]["size"] == 4 + assert files_by_name[f"{prefix}dogs/others/dog4"]["size"] == 4 + return + + assert (dest / "cats").exists() is False + assert (dest / "dogs").exists() is False + for prefix in ["/", ""]: + assert f"{prefix}cats/cat1" not in files_by_name + assert f"{prefix}cats/cat2" not in files_by_name + assert f"{prefix}dogs/dog1" not in files_by_name + assert f"{prefix}dogs/dog2" not in files_by_name + assert f"{prefix}dogs/dog3" not in files_by_name + assert f"{prefix}dogs/others/dog4" not in files_by_name + + +@pytest.mark.parametrize( + "cloud_type", + ["s3", "gs", "azure"], + indirect=True, +) +def test_cp_local_dataset(cloud_test_catalog, dogs_dataset): + skip_if_not_sqlite() + working_dir = cloud_test_catalog.working_dir + catalog = cloud_test_catalog.catalog + + dest = working_dir / "data" + dest.mkdir() + + dataset_uri = dogs_dataset.uri(version=1) + + catalog.cp([dataset_uri], str(dest)) + + parsed = urlparse(str(cloud_test_catalog.src)) + netloc = Path(parsed.netloc.strip("/")) + path = Path(parsed.path.strip("/")) + + assert tree_from_path(dest / netloc / path) == { + "dogs": { + "dog1": "woof", + "dog2": "arf", + "dog3": "bark", + "others": {"dog4": "ruff"}, + } + } + + +@pytest.mark.parametrize("tree", [TARRED_TREE], indirect=True) +@pytest.mark.parametrize("suffix", ["/", "/*"]) +@pytest.mark.parametrize("recursive", [False, True]) +@pytest.mark.parametrize("dir_exists", [False, True]) +@pytest.mark.xfail(reason="Missing support for v-objects in cp") +def test_cp_tar_root(cloud_test_catalog, suffix, recursive, dir_exists): + ctc = cloud_test_catalog + catalog = ctc.catalog + create_tar_dataset(catalog, ctc.src_uri, "tarred") + dest = ctc.working_dir / "data" + if dir_exists: + dest.mkdir() + src = f"ds://tarred/animals.tar{suffix}" + dest_path = str(dest) + "/" + + if not dir_exists and suffix == "/*": + with pytest.raises(FileNotFoundError): + catalog.cp([src], dest_path, recursive=recursive, no_edatachain_file=True) + return + + catalog.cp([src], dest_path, recursive=recursive, no_edatachain_file=True) + + expected = DEFAULT_TREE.copy() + if not recursive: + # Directories are not copied + if suffix == "/": + expected = {} + else: + for key in list(expected): + if isinstance(expected[key], dict): + del expected[key] + + assert tree_from_path(dest) == expected + + +@pytest.mark.parametrize("tree", [TARRED_TREE], indirect=True) +@pytest.mark.xfail(reason="Missing support for v-objects in cp") +def test_cp_full_tar(cloud_test_catalog): + ctc = cloud_test_catalog + catalog = ctc.catalog + create_tar_dataset(catalog, ctc.src_uri, "tarred") + dest = ctc.working_dir / "data" + dest.mkdir() + src = "ds://tarred/" + catalog.cp([src], str(dest), recursive=True, no_edatachain_file=True) + + assert tree_from_path(dest, binary=True) == TARRED_TREE + + +@pytest.mark.parametrize( + "recursive,star,slash,dir_exists", + ( + (True, True, False, False), + (True, False, False, False), + (True, False, False, True), + (True, False, True, False), + (False, True, False, False), + (False, False, False, False), + (False, False, True, False), + ), +) +def test_cp_subdir(cloud_test_catalog, recursive, star, slash, dir_exists): + src_uri = f"{cloud_test_catalog.src_uri}/dogs" + working_dir = cloud_test_catalog.working_dir + catalog = cloud_test_catalog.catalog + + dest = working_dir / "data" + + if star: + src_path = f"{src_uri}/*" + elif slash: + src_path = f"{src_uri}/" + else: + src_path = src_uri + + if star: + with pytest.raises(FileNotFoundError): + catalog.cp([src_path], str(dest), recursive=recursive) + + if dir_exists or star: + dest.mkdir() + + catalog.cp([src_path], str(dest), recursive=recursive) + + if not star and not recursive: + # Directories are skipped, so nothing is copied + assert tree_from_path(dest) == {} + return + + # Testing DataChain File Contents + assert dest.with_suffix(".edatachain").is_file() + edatachain_contents = yaml.safe_load(dest.with_suffix(".edatachain").read_text()) + assert len(edatachain_contents) == 1 + data = edatachain_contents[0] + assert data["data-source"]["uri"] == src_path.rstrip("/") + expected_file_count = 4 if recursive else 3 + assert len(data["files"]) == expected_file_count + files_by_name = {f["name"]: f for f in data["files"]} + + # Directories should never be saved + assert "others" not in files_by_name + assert "dogs/others" not in files_by_name + + if not dir_exists: + assert (dest / "dog1").read_text() == "woof" + assert (dest / "dog2").read_text() == "arf" + assert (dest / "dog3").read_text() == "bark" + assert (dest / "dogs").exists() is False + assert files_by_name["dog1"]["size"] == 4 + assert files_by_name["dog2"]["size"] == 3 + assert files_by_name["dog3"]["size"] == 4 + if recursive: + assert (dest / "others" / "dog4").read_text() == "ruff" + assert files_by_name["others/dog4"]["size"] == 4 + else: + assert (dest / "others").exists() is False + assert "others/dog4" not in files_by_name + return + + assert tree_from_path(dest / "dogs") == DEFAULT_TREE["dogs"] + assert (dest / "dog1").exists() is False + assert (dest / "dog2").exists() is False + assert (dest / "dog3").exists() is False + assert (dest / "others").exists() is False + assert files_by_name["dogs/dog1"]["size"] == 4 + assert files_by_name["dogs/dog2"]["size"] == 3 + assert files_by_name["dogs/dog3"]["size"] == 4 + assert files_by_name["dogs/others/dog4"]["size"] == 4 + + +@pytest.mark.parametrize("tree", [TARRED_TREE], indirect=True) +@pytest.mark.parametrize("path", ["*/dogs", "animals.tar/dogs"]) +@pytest.mark.parametrize("suffix", ["", "/", "/*"]) +@pytest.mark.parametrize("recursive", [False, True]) +@pytest.mark.parametrize("dir_exists", [False, True]) +@pytest.mark.xfail(reason="Missing support for v-objects in cp") +def test_cp_tar_subdir(cloud_test_catalog, path, suffix, recursive, dir_exists): + ctc = cloud_test_catalog + catalog = ctc.catalog + create_tar_dataset(catalog, ctc.src_uri, "tarred") + dest = ctc.working_dir / "data" + if dir_exists: + dest.mkdir() + src = f"ds://tarred/{path}{suffix}" + + if not dir_exists and suffix == "/*": + with pytest.raises(FileNotFoundError): + catalog.cp([src], str(dest), recursive=recursive) + return + + catalog.cp([src], str(dest), recursive=recursive) + + expected = DEFAULT_TREE["dogs"].copy() + if suffix in ("",) and dir_exists: + expected = {"dogs": expected} + if not recursive: + # Directories are not copied + if not dir_exists or suffix == "/": + expected = {} + else: + for key in list(expected): + if isinstance(expected[key], dict): + del expected[key] + + assert tree_from_path(dest) == expected + + +@pytest.mark.parametrize( + "recursive,star,slash", + ( + (True, True, False), + (True, False, False), + (True, False, True), + (False, True, False), + (False, False, False), + (False, False, True), + ), +) +def test_cp_multi_subdir(cloud_test_catalog, recursive, star, slash): # noqa: PLR0915 + sources = [ + f"{cloud_test_catalog.src_uri}/cats", + f"{cloud_test_catalog.src_uri}/dogs", + ] + working_dir = cloud_test_catalog.working_dir + catalog = cloud_test_catalog.catalog + + dest = working_dir / "data" + + if star: + src_paths = [f"{src}/*" for src in sources] + elif slash: + src_paths = [f"{src}/" for src in sources] + else: + src_paths = sources + + with pytest.raises(FileNotFoundError): + catalog.cp(src_paths, str(dest), recursive=recursive) + + dest.mkdir() + + catalog.cp(src_paths, str(dest), recursive=recursive) + + if not star and not recursive: + # Directories are skipped, so nothing is copied + assert tree_from_path(dest) == {} + return + + # Testing DataChain File Contents + assert dest.with_suffix(".edatachain").is_file() + edatachain_contents = yaml.safe_load(dest.with_suffix(".edatachain").read_text()) + assert len(edatachain_contents) == 2 + data_cats = edatachain_contents[0] + data_dogs = edatachain_contents[1] + assert data_cats["data-source"]["uri"] == src_paths[0].rstrip("/") + assert data_dogs["data-source"]["uri"] == src_paths[1].rstrip("/") + assert len(data_cats["files"]) == 2 + assert len(data_dogs["files"]) == 4 if recursive else 3 + cat_files_by_name = {f["name"]: f for f in data_cats["files"]} + dog_files_by_name = {f["name"]: f for f in data_dogs["files"]} + + # Directories should never be saved + assert "others" not in dog_files_by_name + assert "dogs/others" not in dog_files_by_name + + if star or slash: + assert (dest / "cat1").read_text() == "meow" + assert (dest / "cat2").read_text() == "mrow" + assert (dest / "dog1").read_text() == "woof" + assert (dest / "dog2").read_text() == "arf" + assert (dest / "dog3").read_text() == "bark" + assert (dest / "cats").exists() is False + assert (dest / "dogs").exists() is False + assert cat_files_by_name["cat1"]["size"] == 4 + assert cat_files_by_name["cat2"]["size"] == 4 + assert dog_files_by_name["dog1"]["size"] == 4 + assert dog_files_by_name["dog2"]["size"] == 3 + assert dog_files_by_name["dog3"]["size"] == 4 + if recursive: + assert (dest / "others" / "dog4").read_text() == "ruff" + assert dog_files_by_name["others/dog4"]["size"] == 4 + else: + assert (dest / "others").exists() is False + assert "others/dog4" not in dog_files_by_name + return + + assert (dest / "cats" / "cat1").read_text() == "meow" + assert (dest / "cats" / "cat2").read_text() == "mrow" + assert (dest / "dogs" / "dog1").read_text() == "woof" + assert (dest / "dogs" / "dog2").read_text() == "arf" + assert (dest / "dogs" / "dog3").read_text() == "bark" + assert (dest / "dogs" / "others" / "dog4").read_text() == "ruff" + assert (dest / "cat1").exists() is False + assert (dest / "cat2").exists() is False + assert (dest / "dog1").exists() is False + assert (dest / "dog2").exists() is False + assert (dest / "dog3").exists() is False + assert (dest / "others").exists() is False + assert cat_files_by_name["cats/cat1"]["size"] == 4 + assert cat_files_by_name["cats/cat2"]["size"] == 4 + assert dog_files_by_name["dogs/dog1"]["size"] == 4 + assert dog_files_by_name["dogs/dog2"]["size"] == 3 + assert dog_files_by_name["dogs/dog3"]["size"] == 4 + assert dog_files_by_name["dogs/others/dog4"]["size"] == 4 + + +def test_cp_double_subdir(cloud_test_catalog): + src_path = f"{cloud_test_catalog.src_uri}/dogs/others" + working_dir = cloud_test_catalog.working_dir + catalog = cloud_test_catalog.catalog + dest = working_dir / "data" + + catalog.cp([src_path], str(dest), recursive=True) + + # Testing DataChain File Contents + assert dest.with_suffix(".edatachain").is_file() + edatachain_contents = yaml.safe_load(dest.with_suffix(".edatachain").read_text()) + assert len(edatachain_contents) == 1 + data = edatachain_contents[0] + assert data["data-source"]["uri"] == src_path.rstrip("/") + assert len(data["files"]) == 1 + files_by_name = {f["name"]: f for f in data["files"]} + + # Directories should never be saved + assert "others" not in files_by_name + assert "dogs/others" not in files_by_name + + assert (dest / "dogs").exists() is False + assert (dest / "others").exists() is False + assert (dest / "dog4").read_text() == "ruff" + assert files_by_name["dog4"]["size"] == 4 + + +@pytest.mark.parametrize("no_glob", (True, False)) +def test_cp_single_file(cloud_test_catalog, no_glob): + working_dir = cloud_test_catalog.working_dir + catalog = cloud_test_catalog.catalog + dest = working_dir / "data" + src_path = f"{cloud_test_catalog.src_uri}/dogs/dog1" + dest.mkdir() + + catalog.cp( + [src_path], str(dest / "local_dog"), no_edatachain_file=True, no_glob=no_glob + ) + + assert tree_from_path(dest) == {"local_dog": "woof"} + + +@pytest.mark.parametrize("tree", [{"foo": "original"}], indirect=True) +def test_storage_mutation(cloud_test_catalog): + working_dir = cloud_test_catalog.working_dir + catalog = cloud_test_catalog.catalog + src_path = f"{cloud_test_catalog.src_uri}/foo" + + dest = working_dir / "data1" + dest.mkdir() + catalog.cp([src_path], str(dest / "local"), no_edatachain_file=True) + assert tree_from_path(dest) == {"local": "original"} + + # Storage modified without reindexing, we get the old version from cache. + (cloud_test_catalog.src / "foo").write_text("modified") + dest = working_dir / "data2" + dest.mkdir() + catalog.cp([src_path], str(dest / "local"), no_edatachain_file=True) + assert tree_from_path(dest) == {"local": "original"} + + # Storage modified without reindexing. + # Since the old version cannot be found in storage or cache, it's an error. + catalog.cache.clear() + dest = working_dir / "data3" + dest.mkdir() + with pytest.raises(FileNotFoundError): + catalog.cp([src_path], str(dest / "local"), no_edatachain_file=True) + assert tree_from_path(dest) == {} + + # Storage modified with reindexing, we get the new version. + catalog.index([cloud_test_catalog.src_uri], update=True) + dest = working_dir / "data4" + dest.mkdir() + catalog.cp([src_path], str(dest / "local"), no_edatachain_file=True) + assert tree_from_path(dest) == {"local": "modified"} + + +def test_cp_edatachain_file_options(cloud_test_catalog): + working_dir = cloud_test_catalog.working_dir + catalog = cloud_test_catalog.catalog + dest = working_dir / "data" + src_path = f"{cloud_test_catalog.src_uri}/dogs/*" + edatachain_file = working_dir / "custom_name.edatachain" + + catalog.cp( + [src_path], + str(dest), + recursive=False, + edatachain_only=True, + edatachain_file=str(edatachain_file), + ) + + assert (dest / "dog1").exists() is False + assert (dest / "dog2").exists() is False + assert (dest / "dog3").exists() is False + assert (dest / "dogs").exists() is False + assert (dest / "others").exists() is False + assert dest.with_suffix(".edatachain").exists() is False + + # Testing DataChain File Contents + assert edatachain_file.is_file() + edatachain_contents = yaml.safe_load(edatachain_file.read_text()) + assert len(edatachain_contents) == 1 + data = edatachain_contents[0] + assert data["data-source"]["uri"] == src_path + expected_file_count = 3 + assert len(data["files"]) == expected_file_count + files_by_name = {f["name"]: f for f in data["files"]} + + assert parse_edatachain_file(str(edatachain_file)) == edatachain_contents + + # Directories should never be saved + assert "others" not in files_by_name + assert "dogs/others" not in files_by_name + + assert files_by_name["dog1"]["size"] == 4 + assert files_by_name["dog2"]["size"] == 3 + assert files_by_name["dog3"]["size"] == 4 + assert "others/dog4" not in files_by_name + + with pytest.raises(FileNotFoundError): + # Should fail, as * will not be expanded + catalog.cp( + [src_path], + str(dest), + recursive=False, + edatachain_only=True, + edatachain_file=str(edatachain_file), + no_glob=True, + ) + + # Should succeed, as the DataChain file exists check will be skipped + edatachain_only_data = catalog.cp( + [src_path], + str(dest), + recursive=False, + edatachain_only=True, + edatachain_file=str(edatachain_file), + force=True, + ) + + # Check the returned DataChain data contents + assert len(edatachain_only_data) == len(edatachain_contents) + edatachain_only_source = edatachain_only_data[0] + assert edatachain_only_source["data-source"]["uri"] == src_path.rstrip("/") + assert edatachain_only_source["files"] == data["files"] + + +def test_cp_edatachain_file_sources(cloud_test_catalog): # noqa: PLR0915 + sources = [ + f"{cloud_test_catalog.src_uri}/cats/", + f"{cloud_test_catalog.src_uri}/dogs/*", + ] + working_dir = cloud_test_catalog.working_dir + catalog = cloud_test_catalog.catalog + + dest = working_dir / "data" + + edatachain_files = [ + working_dir / "custom_cats.edatachain", + working_dir / "custom_dogs.edatachain", + ] + + catalog.cp( + sources[:1], + str(dest), + recursive=True, + edatachain_only=True, + edatachain_file=str(edatachain_files[0]), + ) + + catalog.cp( + sources[1:], + str(dest), + recursive=True, + edatachain_only=True, + edatachain_file=str(edatachain_files[1]), + ) + + # Files should not be copied yet + assert (dest / "cat1").exists() is False + assert (dest / "cat2").exists() is False + assert (dest / "cats").exists() is False + assert (dest / "dog1").exists() is False + assert (dest / "dog2").exists() is False + assert (dest / "dog3").exists() is False + assert (dest / "dogs").exists() is False + assert (dest / "others").exists() is False + + # Testing DataChain File Contents + edatachain_data = [] + for dqf in edatachain_files: + assert dqf.is_file() + edatachain_contents = yaml.safe_load(dqf.read_text()) + assert len(edatachain_contents) == 1 + edatachain_data.extend(edatachain_contents) + + assert len(edatachain_data) == 2 + data_cats1 = edatachain_data[0] + data_dogs1 = edatachain_data[1] + assert data_cats1["data-source"]["uri"] == sources[0].rstrip("/") + assert data_dogs1["data-source"]["uri"] == sources[1].rstrip("/") + assert len(data_cats1["files"]) == 2 + assert len(data_dogs1["files"]) == 4 + cat_files_by_name1 = {f["name"]: f for f in data_cats1["files"]} + dog_files_by_name1 = {f["name"]: f for f in data_dogs1["files"]} + + # Directories should never be saved + assert "others" not in dog_files_by_name1 + assert "dogs/others" not in dog_files_by_name1 + + assert cat_files_by_name1["cat1"]["size"] == 4 + assert cat_files_by_name1["cat2"]["size"] == 4 + assert dog_files_by_name1["dog1"]["size"] == 4 + assert dog_files_by_name1["dog2"]["size"] == 3 + assert dog_files_by_name1["dog3"]["size"] == 4 + assert dog_files_by_name1["others/dog4"]["size"] == 4 + + assert not dest.exists() + + with pytest.raises(FileNotFoundError): + catalog.cp([str(dqf) for dqf in edatachain_files], str(dest), recursive=True) + + dest.mkdir() + + # Copy using these DataChain files as sources + catalog.cp([str(dqf) for dqf in edatachain_files], str(dest), recursive=True) + + # Files should now be copied + assert (dest / "cat1").read_text() == "meow" + assert (dest / "cat2").read_text() == "mrow" + assert (dest / "dog1").read_text() == "woof" + assert (dest / "dog2").read_text() == "arf" + assert (dest / "dog3").read_text() == "bark" + assert (dest / "others" / "dog4").read_text() == "ruff" + + # Testing DataChain File Contents + assert dest.with_suffix(".edatachain").is_file() + edatachain_contents = yaml.safe_load(dest.with_suffix(".edatachain").read_text()) + assert len(edatachain_contents) == 2 + data_cats2 = edatachain_contents[0] + data_dogs2 = edatachain_contents[1] + assert data_cats2["data-source"]["uri"] == sources[0].rstrip("/") + assert data_dogs2["data-source"]["uri"] == sources[1].rstrip("/") + assert len(data_cats2["files"]) == 2 + assert len(data_dogs2["files"]) == 4 + cat_files_by_name2 = {f["name"]: f for f in data_cats2["files"]} + dog_files_by_name2 = {f["name"]: f for f in data_dogs2["files"]} + + # Directories should never be saved + assert "others" not in dog_files_by_name2 + assert "dogs/others" not in dog_files_by_name2 + + assert cat_files_by_name2["cat1"]["size"] == 4 + assert cat_files_by_name2["cat2"]["size"] == 4 + assert dog_files_by_name2["dog1"]["size"] == 4 + assert dog_files_by_name2["dog2"]["size"] == 3 + assert dog_files_by_name2["dog3"]["size"] == 4 + assert dog_files_by_name2["others/dog4"]["size"] == 4 + + +@pytest.mark.parametrize("cloud_type, version_aware", [("file", False)], indirect=True) +def test_cp_symlinks(cloud_test_catalog): + catalog = cloud_test_catalog.catalog + catalog.client_config["use_symlinks"] = True + src_uri = cloud_test_catalog.src_uri + work_dir = cloud_test_catalog.working_dir + dest = work_dir / "data" + dest.mkdir() + catalog.cp([f"{src_uri}/dogs/"], str(dest), recursive=True) + + assert (dest / "dog1").is_symlink() + assert os.path.realpath(dest / "dog1") == str( + cloud_test_catalog.src / "dogs" / "dog1" + ) + assert (dest / "dog1").read_text() == "woof" + assert (dest / "others" / "dog4").is_symlink() + assert os.path.realpath(dest / "others" / "dog4") == str( + cloud_test_catalog.src / "dogs" / "others" / "dog4" + ) + assert (dest / "others" / "dog4").read_text() == "ruff" + + +def test_du(cloud_test_catalog, cloud_type): + src_uri = cloud_test_catalog.src_uri + catalog = cloud_test_catalog.catalog + + src_uri_path = src_uri + if cloud_type == "file": + src_uri_path = LocalFileSystem._strip_protocol(src_uri) + expected_results = [ + (f"{src_uri_path}/cats/", 8), + (f"{src_uri_path}/dogs/others/", 4), + (f"{src_uri_path}/dogs/", 15), + (f"{src_uri_path}/", 36), + ] + + results = catalog.du([src_uri]) + assert set(results) == set(expected_results[3:]) + + results = catalog.du([src_uri], depth=1) + assert set(results) == set(expected_results[:1] + expected_results[2:]) + + results = catalog.du([src_uri], depth=5) + assert set(results) == set(expected_results) + + +def test_ls_glob(cloud_test_catalog): + src_uri = cloud_test_catalog.src_uri + catalog = cloud_test_catalog.catalog + + assert sorted( + (source.node.name, [r[0] for r in results]) + for source, results in catalog.ls([f"{src_uri}/dogs/dog*"], fields=["name"]) + ) == [("dog1", ["dog1"]), ("dog2", ["dog2"]), ("dog3", ["dog3"])] + + +def test_ls_prefix_not_found(cloud_test_catalog): + src_uri = cloud_test_catalog.src_uri + catalog = cloud_test_catalog.catalog + with pytest.raises(FileNotFoundError): + list(catalog.ls([f"{src_uri}/bogus/"], fields=["name"])) + + +def clear_storages(catalog): + ds = catalog.metastore + ds.db.execute(ds._storages.delete()) + + +@pytest.mark.parametrize("tree", [TARRED_TREE], indirect=True) +@pytest.mark.xfail(reason="Missing support for datasets in ls") +def test_ls_subobjects(cloud_test_catalog): + ctc = cloud_test_catalog + catalog = ctc.catalog + create_tar_dataset(catalog, ctc.src_uri, "tarred") + + def do_ls(target): + ((_, results),) = list(catalog.ls([target], fields=["name"])) + results = list(results) + result_set = {x[0] for x in results} + assert len(result_set) == len(results) + return result_set + + ds = "ds://tarred" + assert do_ls(ds) == {"animals.tar"} + assert do_ls(f"{ds}/animals.tar") == {"animals.tar"} + assert do_ls(f"{ds}/animals.tar/dogs") == { + "dog1", + "dog2", + "dog3", + "others", + } + assert do_ls(f"{ds}/animals.tar/") == {"description", "cats", "dogs"} + assert do_ls(f"{ds}/*.tar/") == {"description", "cats", "dogs"} + assert do_ls(f"{ds}/*.tar/desc*") == {"description"} + + +def test_index_error(cloud_test_catalog): + protocol = cloud_test_catalog.src_uri.split("://", 1)[0] + # XXX: different clients raise inconsistent exceptions + with pytest.raises(Exception): # noqa: B017 + cloud_test_catalog.catalog.index([f"{protocol}://does_not_exist"]) + + +def test_query(cloud_test_catalog, mock_popen_dataset_created): + catalog = cloud_test_catalog.catalog + src_uri = cloud_test_catalog.src_uri + + query_script = f"""\ + from datachain.query import C, DatasetQuery + DatasetQuery({src_uri!r}) + """ + query_script = dedent(query_script) + + result = catalog.query(query_script, save=True) + assert result.dataset + assert_row_names( + catalog, + result.dataset, + result.version, + { + "dog1", + "dog2", + "dog3", + "dog4", + }, + ) + assert result.dataset.query_script == query_script + assert result.dataset.sources == "" + + +def test_query_save_size(cloud_test_catalog, mock_popen_dataset_created): + catalog = cloud_test_catalog.catalog + src_uri = cloud_test_catalog.src_uri + + query_script = f"""\ + from datachain.query import C, DatasetQuery + DatasetQuery({src_uri!r}) + """ + query_script = dedent(query_script) + + result = catalog.query(query_script, save=True) + dataset_version = result.dataset.get_version(result.version) + assert dataset_version.num_objects == 4 + assert dataset_version.size == 15 + + +def test_query_fail_to_compile(cloud_test_catalog): + catalog = cloud_test_catalog.catalog + + query_script = "syntax error" + + with pytest.raises(QueryScriptCompileError): + catalog.query(query_script) + + +def test_query_fail_wrong_dataset_name(cloud_test_catalog): + catalog = cloud_test_catalog.catalog + + query_script = """\ + from datachain.query import DatasetQuery + DatasetQuery("s3://bucket-name") + """ + query_script = dedent(query_script) + + with pytest.raises( + ValueError, match="Cannot use ds_query_ prefix for dataset name" + ): + catalog.query(query_script, save_as="ds_query_dataset") + + +def test_query_subprocess_wrong_return_code(mock_popen, cloud_test_catalog): + mock_popen.configure_mock(returncode=1) + catalog = cloud_test_catalog.catalog + src_uri = cloud_test_catalog.src_uri + + query_script = f""" +from datachain.query import DatasetQuery, C +DatasetQuery('{src_uri}') + """ + + with pytest.raises(QueryScriptRunError) as exc_info: + catalog.query(query_script) + assert str(exc_info.value).startswith("Query script exited with error code 1") + + +def test_query_last_statement_not_expression(mock_popen, cloud_test_catalog): + mock_popen.configure_mock(returncode=10) + catalog = cloud_test_catalog.catalog + src_uri = cloud_test_catalog.src_uri + + query_script = f""" +from datachain.query import DatasetQuery, C +ds = DatasetQuery('{src_uri}') + """ + + with pytest.raises(QueryScriptCompileError) as exc_info: + catalog.query(query_script) + assert str(exc_info.value).startswith( + "Query script failed to compile, " + "reason: Last line in a script was not an expression" + ) + + +def test_query_last_statement_not_ds_query_instance(mock_popen, cloud_test_catalog): + mock_popen.configure_mock(returncode=10) + catalog = cloud_test_catalog.catalog + src_uri = cloud_test_catalog.src_uri + + query_script = f""" +from datachain.query import DatasetQuery, C +ds = DatasetQuery('{src_uri}') +5 + """ + + with pytest.raises(QueryScriptRunError) as exc_info: + catalog.query(query_script) + assert str(exc_info.value).startswith( + "Last line in a script was not an instance of DatasetQuery" + ) + + +def test_query_dataset_not_returned(mock_popen, cloud_test_catalog): + mock_popen.configure_mock(stdout=io.StringIO("random str")) + catalog = cloud_test_catalog.catalog + src_uri = cloud_test_catalog.src_uri + + query_script = f""" +from datachain.query import DatasetQuery, C +DatasetQuery('{src_uri}') + """ + + with pytest.raises(QueryScriptDatasetNotFound) as e: + catalog.query(query_script, save=True) + assert e.value.output == "random str" + + +@pytest.mark.parametrize("cloud_type", ["s3", "azure", "gs"], indirect=True) +def test_storage_stats(cloud_test_catalog): + catalog = cloud_test_catalog.catalog + src_uri = cloud_test_catalog.src_uri + + with pytest.raises(StorageNotFoundError): + catalog.storage_stats(src_uri) + + catalog.enlist_source(src_uri, ttl=1234) + stats = catalog.storage_stats(src_uri) + assert stats.num_objects == 7 + assert stats.size == 36 + + catalog.enlist_source(f"{src_uri}/dogs/", ttl=1234, force_update=True) + stats = catalog.storage_stats(src_uri) + assert stats.num_objects == 4 + assert stats.size == 15 + + catalog.enlist_source(f"{src_uri}/dogs/", ttl=1234) + stats = catalog.storage_stats(src_uri) + assert stats.num_objects == 4 + assert stats.size == 15 + + +@pytest.mark.parametrize("from_cli", [False, True]) +def test_garbage_collect(cloud_test_catalog, from_cli, capsys): + catalog = cloud_test_catalog.catalog + assert catalog.get_temp_table_names() == [] + temp_tables = ["tmp_vc12F", "udf_jh653", "ds_shadow_12345", "old_ds_shadow"] + for t in temp_tables: + catalog.warehouse.create_udf_table(t) + assert set(catalog.get_temp_table_names()) == set(temp_tables) + if from_cli: + garbage_collect(catalog) + captured = capsys.readouterr() + assert captured.out == "Garbage collecting 4 tables.\n" + else: + catalog.cleanup_temp_tables(temp_tables) + assert catalog.get_temp_table_names() == [] + + +def test_get_file_signals(cloud_test_catalog, dogs_dataset): + catalog = cloud_test_catalog.catalog + catalog.metastore.update_dataset_version( + dogs_dataset, + 1, + feature_schema={ + "name": "str", + "age": "str", + "f1": "File@1", + "f2": "File@1", + }, + ) + row = { + "name": "Jon", + "age": 25, + "f1__source": "s3://first_bucket", + "f1__name": "image1.jpg", + "f2__source": "s3://second_bucket", + "f2__name": "image2.jpg", + } + + assert catalog.get_file_signals(dogs_dataset.name, 1, row) == { + "source": "s3://first_bucket", + "name": "image1.jpg", + } + + +def test_get_file_signals_no_signals(cloud_test_catalog, dogs_dataset): + catalog = cloud_test_catalog.catalog + catalog.metastore.update_dataset_version( + dogs_dataset, + 1, + feature_schema={ + "name": "str", + "age": "str", + }, + ) + row = { + "name": "Jon", + "age": 25, + } + + assert catalog.get_file_signals(dogs_dataset.name, 1, row) is None + + +def test_open_object_no_file_signals(cloud_test_catalog, dogs_dataset): + catalog = cloud_test_catalog.catalog + catalog.metastore.update_dataset_version( + dogs_dataset, + 1, + feature_schema={ + "name": "str", + "age": "str", + }, + ) + row = { + "name": "Jon", + "age": 25, + } + + with pytest.raises(RuntimeError): + assert catalog.open_object(dogs_dataset.name, 1, row) diff --git a/tests/func/test_client.py b/tests/func/test_client.py new file mode 100644 index 000000000..41e100cc3 --- /dev/null +++ b/tests/func/test_client.py @@ -0,0 +1,93 @@ +import asyncio + +import pytest +from fsspec.asyn import sync +from tqdm import tqdm + +from datachain.asyn import get_loop +from datachain.client import Client +from tests.data import ENTRIES + + +@pytest.fixture +def client(cloud_server, cloud_server_credentials): + uri = cloud_server.src_uri + return Client.get_implementation(uri).from_source( + uri, cache=None, **cloud_server.client_config + ) + + +def normalize_entries(entries): + return {(e.parent, e.name) for e in entries} + + +def match_entries(result, expected): + assert len(result) == len(expected) + assert normalize_entries(result) == normalize_entries(expected) + + +async def find(client, prefix, method="default"): + results = [] + async for entries in client.scandir(prefix, method=method): + results.extend(entries) + return results + + +def scandir(client, prefix, method="default"): + return sync(get_loop(), find, client, prefix, method) + + +def test_scandir_error(client): + with pytest.raises(FileNotFoundError): + scandir(client, "bogus") + + +@pytest.mark.xfail +def test_scandir_not_dir(client): + with pytest.raises(FileNotFoundError): + scandir(client, "description") + + +def test_scandir_success(client): + results = scandir(client, "") + match_entries(results, ENTRIES) + + +@pytest.mark.parametrize("cloud_type", ["s3", "gs", "azure"], indirect=True) +def test_scandir_alternate(client): + results = scandir(client, "", method="nested") + match_entries(results, ENTRIES) + + +def test_gcs_client_gets_credentials_from_env(monkeypatch, mocker): + from datachain.client.gcs import GCSClient + + monkeypatch.setenv( + "DATACHAIN_GCP_CREDENTIALS", '{"token": "test-credentials-token"}' + ) + init = mocker.patch( + "datachain.client.gcs.GCSFileSystem.__init__", return_value=None + ) + mocker.patch( + "datachain.client.gcs.GCSFileSystem.invalidate_cache", return_value=None + ) + + GCSClient.create_fs() + + init.assert_called_once_with( + token={"token": "test-credentials-token"}, version_aware=True + ) + + +@pytest.mark.parametrize("tree", [{}], indirect=True) +def test_fetch_dir_does_not_return_self(client, cloud_type): + if cloud_type == "file": + pytest.skip() + + client.fs.touch(f"{client.uri}/directory//file") + + subdirs = sync( + get_loop(), client._fetch_dir, "directory/", tqdm(disable=True), asyncio.Queue() + ) + + assert "directory" not in subdirs diff --git a/tests/func/test_datachain.py b/tests/func/test_datachain.py new file mode 100644 index 000000000..4f7c98bb2 --- /dev/null +++ b/tests/func/test_datachain.py @@ -0,0 +1,40 @@ +import pytest + +from datachain.lib.dc import DataChain +from datachain.lib.file import File + + +@pytest.mark.parametrize("anon", [True, False]) +def test_catalog_anon(catalog, anon): + chain = ( + DataChain.from_storage("gs://dvcx-datalakes/dogs-and-cats/", anon=anon) + .limit(5) + .save("test_catalog_anon") + ) + assert chain.catalog.client_config.get("anon", False) is anon + + +def test_from_storage(cloud_test_catalog): + ctc = cloud_test_catalog + dc = DataChain.from_storage(ctc.src_uri, catalog=ctc.catalog) + assert dc.count() == 7 + + +def test_map_file(cloud_test_catalog): + ctc = cloud_test_catalog + + def new_signal(file: File) -> str: + with file.open() as f: + return file.name + " -> " + f.read().decode("utf-8") + + dc = DataChain.from_storage(ctc.src_uri, catalog=ctc.catalog).map(signal=new_signal) + expected = { + "description -> Cats and Dogs", + "cat1 -> meow", + "cat2 -> mrow", + "dog1 -> woof", + "dog2 -> arf", + "dog3 -> bark", + "dog4 -> ruff", + } + assert set(dc.collect_one("signal")) == expected diff --git a/tests/func/test_dataset_query.py b/tests/func/test_dataset_query.py new file mode 100644 index 000000000..8111e1737 --- /dev/null +++ b/tests/func/test_dataset_query.py @@ -0,0 +1,3646 @@ +import io +import json +import math +import os +import pickle +import random +import uuid +from datetime import datetime, timedelta, timezone +from json import dumps +from textwrap import dedent +from unittest.mock import ANY, patch + +import numpy as np +import pytest +import sqlalchemy +from dateutil.parser import isoparse +from sqlalchemy import tuple_ + +from datachain.catalog import QUERY_SCRIPT_CANCELED_EXIT_CODE +from datachain.dataset import DatasetDependencyType, DatasetStatus +from datachain.error import DatasetInvalidVersionError, DatasetNotFoundError +from datachain.node import Node +from datachain.query import ( + C, + DatasetQuery, + DatasetRow, + LocalFilename, + Object, + Stream, + udf, +) +from datachain.query.builtins import checksum, index_tar +from datachain.query.dataset import QueryStep +from datachain.sql import functions +from datachain.sql.functions.array import cosine_distance, euclidean_distance +from datachain.sql.types import ( + JSON, + Array, + Binary, + Boolean, + DateTime, + Float, + Float32, + Float64, + Int, + Int32, + Int64, + SQLType, + String, +) +from tests.data import ENTRIES +from tests.utils import ( + DEFAULT_TREE, + NUM_TREE, + SIMPLE_DS_QUERY_RECORDS, + TARRED_TREE, + WEBFORMAT_TREE, + assert_row_names, + create_tar_dataset, + dataset_dependency_asdict, + make_index, + text_embedding, +) + + +def from_result_row(col_names, row): + return dict(zip(col_names, row)) + + +@pytest.fixture +def dogs_cats_dataset(listed_bucket, cloud_test_catalog, dogs_dataset, cats_dataset): + dataset_name = uuid.uuid4().hex + catalog = cloud_test_catalog.catalog + ( + DatasetQuery(name=dogs_dataset.name, version=1, catalog=catalog) + .union(DatasetQuery(name=cats_dataset.name, version=1, catalog=catalog)) + .save(dataset_name) + ) + return catalog.get_dataset(dataset_name) + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_delete_dataset(cloud_test_catalog): + catalog = cloud_test_catalog.catalog + path = f"{cloud_test_catalog.src_uri}/cats" + DatasetQuery(path=path, catalog=catalog).save("cats", version=1) + DatasetQuery(path=path, catalog=catalog).save("cats", version=2) + + DatasetQuery.delete("cats", version=1, catalog=catalog) + dataset = catalog.get_dataset("cats") + assert dataset.versions_values == [2] + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_delete_dataset_latest_version(cloud_test_catalog): + catalog = cloud_test_catalog.catalog + path = f"{cloud_test_catalog.src_uri}/cats" + DatasetQuery(path=path, catalog=catalog).save("cats", version=1) + DatasetQuery(path=path, catalog=catalog).save("cats", version=2) + + DatasetQuery.delete("cats", catalog=catalog) + dataset = catalog.get_dataset("cats") + assert dataset.versions_values == [1] + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_delete_dataset_only_version(cloud_test_catalog): + catalog = cloud_test_catalog.catalog + path = f"{cloud_test_catalog.src_uri}/cats" + DatasetQuery(path=path, catalog=catalog).save("cats", version=1) + + DatasetQuery.delete("cats", catalog=catalog) + with pytest.raises(DatasetNotFoundError): + catalog.get_dataset("cats") + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_delete_dataset_missing_version(cloud_test_catalog): + catalog = cloud_test_catalog.catalog + path = f"{cloud_test_catalog.src_uri}/cats" + DatasetQuery(path=path, catalog=catalog).save("cats", version=1) + DatasetQuery(path=path, catalog=catalog).save("cats", version=2) + + with pytest.raises(DatasetInvalidVersionError): + DatasetQuery.delete("cats", version=5, catalog=catalog) + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_save_dataset_version_already_exists(cloud_test_catalog): + catalog = cloud_test_catalog.catalog + path = f"{cloud_test_catalog.src_uri}/cats" + DatasetQuery(path=path, catalog=catalog).save("cats", version=1) + with pytest.raises(RuntimeError) as exc_info: + DatasetQuery(path=path, catalog=catalog).save("cats", version=1) + + assert str(exc_info.value) == "Dataset cats already has version 1" + + +@pytest.mark.parametrize("from_path", [True]) +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_save_multiple_versions(cloud_test_catalog, from_path): + catalog = cloud_test_catalog.catalog + # ensure we can select a subset of a bucket properly + path = cloud_test_catalog.src_uri + if from_path: + ds = DatasetQuery(path=path, catalog=catalog) + else: + sources = [path] + globs = [s.rstrip("/") + "/*" for s in sources] + catalog.index(sources) + catalog.create_dataset_from_sources("animals", globs, recursive=True) + ds = DatasetQuery(name="animals", version=1, catalog=catalog) + + ds_name = "animals_cats" + q = ds + q.save(ds_name) + + q = q.filter(C.parent.glob("cats*") | (C.size < 4)) + q.save(ds_name) + q.save(ds_name) + + dataset_record = catalog.get_dataset(ds_name) + assert dataset_record.status == DatasetStatus.COMPLETE + assert DatasetQuery(name=ds_name, version=1, catalog=catalog).count() == 7 + assert DatasetQuery(name=ds_name, version=2, catalog=catalog).count() == 3 + assert DatasetQuery(name=ds_name, version=3, catalog=catalog).count() == 3 + + with pytest.raises(ValueError): + DatasetQuery(name=ds_name, version=4, catalog=catalog).count() + + +@pytest.mark.parametrize("from_path", [True, False]) +@pytest.mark.parametrize("save", [True, False]) +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_filter(cloud_test_catalog, save, from_path): + catalog = cloud_test_catalog.catalog + # ensure we can select a subset of a bucket properly + path = f"{cloud_test_catalog.src_uri}/cats" + if from_path: + ds = DatasetQuery(path=path, catalog=catalog) + else: + sources = [path] + globs = [s.rstrip("/") + "/*" for s in sources] + catalog.index(sources) + catalog.create_dataset_from_sources("animals", globs, recursive=True) + ds = DatasetQuery(name="animals", version=1, catalog=catalog) + q = ds.filter(C.size < 13).filter(C.parent.glob("cats*") | (C.size < 4)) + if save: + ds_name = "animals_cats" + q.save(ds_name) + q = DatasetQuery(name=ds_name, catalog=catalog) + dataset_record = catalog.get_dataset(ds_name) + assert dataset_record.status == DatasetStatus.COMPLETE + result = q.results() + count = q.count() + assert len(result) == 2 + assert count == 2 + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_instance_returned_after_save(cloud_test_catalog, dogs_cats_dataset): + catalog = cloud_test_catalog.catalog + ds = DatasetQuery(name=dogs_cats_dataset.name, version=1, catalog=catalog) + + ds2 = ds.save("dogs_cats_2") + assert isinstance(ds2, DatasetQuery) + expected_names = {"cat1", "cat2", "dog1", "dog2", "dog3", "dog4"} + assert_row_names(catalog, dogs_cats_dataset, 1, expected_names) + assert_row_names(catalog, catalog.get_dataset("dogs_cats_2"), 1, expected_names) + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_query_specific_dataset_set_proper_dataset_name_version( + cloud_test_catalog, dogs_cats_dataset +): + catalog = cloud_test_catalog.catalog + ds = DatasetQuery(name=dogs_cats_dataset.name, version=1, catalog=catalog) + assert ds.name == dogs_cats_dataset.name + assert ds.version == 1 + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_save_set_proper_dataset_name_version(cloud_test_catalog, dogs_cats_dataset): + catalog = cloud_test_catalog.catalog + ds = DatasetQuery(name=dogs_cats_dataset.name, version=1, catalog=catalog) + ds = ds.filter(C.name.glob("dog*")) + ds2 = ds.save("dogs_small") + + assert ds2.name == "dogs_small" + assert ds2.version == 1 + assert len(ds2.steps) == 0 + + # old dataset query remains detached + assert ds.name is None + assert ds.version is None + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("file", False)], + indirect=True, +) +def test_exec(cloud_test_catalog, dogs_cats_dataset): + catalog = cloud_test_catalog.catalog + all_names = set() + + @udf(params=("name",), output={}) + def name_len(name): + all_names.add(name) + + existing_datasets = list(catalog.ls_datasets()) + dq = ( + DatasetQuery(name=dogs_cats_dataset.name, version=1, catalog=catalog) + .add_signals(name_len) + .exec() + ) + assert isinstance(dq, DatasetQuery) + assert all_names == {"dog1", "dog2", "dog3", "dog4", "cat1", "cat2"} + # exec should not leave any datasets behind + assert list(catalog.ls_datasets()) == existing_datasets + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_reset_dataset_name_version_after_filter(cloud_test_catalog, dogs_cats_dataset): + catalog = cloud_test_catalog.catalog + ds = DatasetQuery(name=dogs_cats_dataset.name, version=1, catalog=catalog) + ds2 = ds.save("dogs_small") + assert ds2.name == "dogs_small" + assert ds2.version == 1 + + ds3 = ds2.filter(C.name.glob("dog1")) + assert ds3.name is None + assert ds3.version is None + + # old ds2 remains attached + assert ds2.name == "dogs_small" + assert ds2.version == 1 + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +@patch("random.randint") +def test_avoid_recalculation_after_save(randint_mock, cloud_test_catalog): + @udf(("name",), {"name_len": Int}) + def name_len(name): + random.randint(1, 5) # noqa: S311 using to check how many times we called UDF + return (len(name),) + + path = cloud_test_catalog.src_uri + catalog = cloud_test_catalog.catalog + ds = ( + DatasetQuery(path=path, catalog=catalog) + .filter(C.name == "dog1") + .add_signals(name_len) + ) + ds2 = ds.save("ds1") + + assert ds2.steps == [] + assert ds2.dependencies == set() + assert isinstance(ds2.starting_step, QueryStep) + ds2.save("ds2") + assert randint_mock.call_count == 1 # UDF should be called only once + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_chain_after_save(cloud_test_catalog, dogs_cats_dataset): + catalog = cloud_test_catalog.catalog + ds = DatasetQuery(name=dogs_cats_dataset.name, version=1, catalog=catalog) + ds.filter(C.name.glob("dog*")).save("ds1").filter(C.size < 4).save("ds2") + + assert_row_names( + catalog, catalog.get_dataset("ds1"), 1, {"dog1", "dog2", "dog3", "dog4"} + ) + assert_row_names(catalog, catalog.get_dataset("ds2"), 1, {"dog2"}) + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_select(cloud_test_catalog): + catalog = cloud_test_catalog.catalog + path = cloud_test_catalog.src_uri + ds = DatasetQuery(path=path, catalog=catalog) + q = ( + ds.order_by(C.size.desc(), C.name) + .limit(6) + .select(C.name, size10x=C.size * 10, size100x=C.size * 100) + ) + result = q.results() + assert result == [ + ("description", 130, 1300), + ("cat1", 40, 400), + ("cat2", 40, 400), + ("dog1", 40, 400), + ("dog3", 40, 400), + ("dog4", 40, 400), + ] + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_select_missing_column(cloud_test_catalog): + catalog = cloud_test_catalog.catalog + path = cloud_test_catalog.src_uri + ds = DatasetQuery(path=path, catalog=catalog) + ds1 = ds.select(C.missing_column_name) + ds2 = ds.select("missing_column_name") + # The exception type varies by database backend + exc1 = pytest.raises(Exception, ds1.results) + assert "missing_column_name" in str(exc1.value) + exc2 = pytest.raises(KeyError, ds2.results) + assert "missing_column_name" in str(exc2.value) + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_select_except(cloud_test_catalog): + catalog = cloud_test_catalog.catalog + path = cloud_test_catalog.src_uri + ds = DatasetQuery(path=path, catalog=catalog) + q = ( + ds.order_by(C.size.desc(), C.name) + .limit(6) + .select(C.parent, "name", C.size, size10x=C.size * 10, size100x=C.size * 100) + .select_except(C.parent, C.size10x) + ) + result = q.results() + assert result == [ + ("description", 13, 1300), + ("cat1", 4, 400), + ("cat2", 4, 400), + ("dog1", 4, 400), + ("dog3", 4, 400), + ("dog4", 4, 400), + ] + + +@pytest.mark.parametrize("save", [True, False]) +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_mutate(cloud_test_catalog, save): + catalog = cloud_test_catalog.catalog + path = cloud_test_catalog.src_uri + ds = DatasetQuery(path=path, catalog=catalog) + q = ( + ds.mutate(size10x=C.size * 10) + .mutate(size1000x=C.size10x * 100) + .mutate( + ("s2", C.size * 2), + ("s3", C.size * 3), + s4=C.size * 4, + ) + .filter((C.size10x < 40) | (C.size10x > 100) | C.name.glob("cat*")) + .order_by(C.size10x.desc(), C.name) + ) + if save: + ds_name = "animals_cats" + q.save(ds_name) + new_query = DatasetQuery(name=ds_name, catalog=catalog).order_by( + C.size10x.desc(), C.name + ) + result = new_query.results(row_factory=lambda c, v: dict(zip(c, v))) + dataset_record = catalog.get_dataset(ds_name) + assert dataset_record.status == DatasetStatus.COMPLETE + else: + result = q.results(row_factory=lambda c, v: dict(zip(c, v))) + assert len(result) == 4 + assert len(result[0]) == 20 + cols = {"size10x", "size1000x", "s2", "s3", "s4"} + new_data = [[v for k, v in r.items() if k in cols] for r in result] + assert new_data == [ + [130, 13000, 26, 39, 52], + [40, 4000, 8, 12, 16], + [40, 4000, 8, 12, 16], + [30, 3000, 6, 9, 12], + ] + + +@pytest.mark.parametrize("save", [True, False]) +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_order_by_limit(cloud_test_catalog, save): + catalog = cloud_test_catalog.catalog + path = cloud_test_catalog.src_uri + ds = DatasetQuery(path=path, catalog=catalog) + q = ds.order_by(C.name.desc()).limit(5) + if save: + ds_name = "animals_cats" + q.save(ds_name) + new_query = DatasetQuery(name=ds_name, catalog=catalog).order_by(C.name.desc()) + result = new_query.results() + dataset_record = catalog.get_dataset(ds_name) + assert dataset_record.status == DatasetStatus.COMPLETE + else: + result = q.results() + assert [r[5] for r in result] == ["dog4", "dog3", "dog2", "dog1", "description"] + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_row_number_without_explicit_order_by(cloud_test_catalog): + catalog = cloud_test_catalog.catalog + conf = cloud_test_catalog.client_config + path = cloud_test_catalog.src_uri + ds_name = uuid.uuid4().hex + + DatasetQuery(path=path, catalog=catalog, client_config=conf).filter( + C.size > 0 + ).save(ds_name) + + results = DatasetQuery(name=ds_name, catalog=catalog).to_records() + assert len(results) == 7 # unordered, just checking num of results + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_row_number_with_order_by_name_descending(cloud_test_catalog): + catalog = cloud_test_catalog.catalog + conf = cloud_test_catalog.client_config + path = cloud_test_catalog.src_uri + ds_name = uuid.uuid4().hex + + DatasetQuery(path=path, catalog=catalog, client_config=conf).order_by( + C.name.desc() + ).save(ds_name) + + results = DatasetQuery(name=ds_name, catalog=catalog).to_records() + results_name_id = [ + {k: v for k, v in r.items() if k in ["id", "name"]} for r in results + ] + assert sorted(results_name_id, key=lambda k: k["id"]) == [ + {"id": 1, "name": "dog4"}, + {"id": 2, "name": "dog3"}, + {"id": 3, "name": "dog2"}, + {"id": 4, "name": "dog1"}, + {"id": 5, "name": "description"}, + {"id": 6, "name": "cat2"}, + {"id": 7, "name": "cat1"}, + ] + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_row_number_with_order_by_name_ascending(cloud_test_catalog): + catalog = cloud_test_catalog.catalog + conf = cloud_test_catalog.client_config + path = cloud_test_catalog.src_uri + ds_name = uuid.uuid4().hex + + DatasetQuery(path=path, catalog=catalog, client_config=conf).order_by( + C.name.asc() + ).save(ds_name) + + results = DatasetQuery(name=ds_name, catalog=catalog).to_records() + results_name_id = [ + {k: v for k, v in r.items() if k in ["id", "name"]} for r in results + ] + assert sorted(results_name_id, key=lambda k: k["id"]) == [ + {"id": 1, "name": "cat1"}, + {"id": 2, "name": "cat2"}, + {"id": 3, "name": "description"}, + {"id": 4, "name": "dog1"}, + {"id": 5, "name": "dog2"}, + {"id": 6, "name": "dog3"}, + {"id": 7, "name": "dog4"}, + ] + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_row_number_with_order_by_name_len_desc_and_name_asc(cloud_test_catalog): + catalog = cloud_test_catalog.catalog + conf = cloud_test_catalog.client_config + path = cloud_test_catalog.src_uri + ds_name = uuid.uuid4().hex + + @udf(("name",), {"name_len": Int}) + def name_len(name): + return (len(name),) + + DatasetQuery(path=path, catalog=catalog, client_config=conf).add_signals( + name_len + ).order_by(C.name_len.desc(), C.name.asc()).save(ds_name) + + results = DatasetQuery(name=ds_name, catalog=catalog).to_records() + results_name_id = [ + {k: v for k, v in r.items() if k in ["id", "name"]} for r in results + ] + assert sorted(results_name_id, key=lambda k: k["id"]) == [ + {"id": 1, "name": "description"}, + {"id": 2, "name": "cat1"}, + {"id": 3, "name": "cat2"}, + {"id": 4, "name": "dog1"}, + {"id": 5, "name": "dog2"}, + {"id": 6, "name": "dog3"}, + {"id": 7, "name": "dog4"}, + ] + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_row_number_with_order_by_before_add_signals(cloud_test_catalog): + catalog = cloud_test_catalog.catalog + conf = cloud_test_catalog.client_config + path = cloud_test_catalog.src_uri + ds_name = uuid.uuid4().hex + + @udf(("name",), {"name_len": Int}) + def name_len(name): + return (len(name),) + + DatasetQuery(path=path, catalog=catalog, client_config=conf).order_by( + C.name.asc() + ).add_signals(name_len).save(ds_name) + + results = DatasetQuery(name=ds_name, catalog=catalog).to_records() + results_name_id = [ + {k: v for k, v in r.items() if k in ["id", "name"]} for r in results + ] + # we should preserve order in final result based on order by which was added + # before add_signals + assert sorted(results_name_id, key=lambda k: k["id"]) == [ + {"id": 1, "name": "cat1"}, + {"id": 2, "name": "cat2"}, + {"id": 3, "name": "description"}, + {"id": 4, "name": "dog1"}, + {"id": 5, "name": "dog2"}, + {"id": 6, "name": "dog3"}, + {"id": 7, "name": "dog4"}, + ] + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_udf(cloud_test_catalog): + catalog = cloud_test_catalog.catalog + sources = [cloud_test_catalog.src_uri] + globs = [s.rstrip("/") + "/*" for s in sources] + catalog.index(sources) + catalog.create_dataset_from_sources("animals", globs, recursive=True) + + @udf(("name",), {"name_len": Int}) + def name_len(name): + # A very simple udf. + return (len(name),) + + q = ( + DatasetQuery(name="animals", version=1, catalog=catalog) + .filter(C.size < 13) + .filter(C.parent.glob("cats*") | (C.size < 4)) + .add_signals(name_len) + ) + result1 = q.select(C.name, C.name_len).results() + # ensure that we're able to run with same query multiple times + result2 = q.select(C.name, C.name_len).results() + count = q.count() + assert len(result1) == 3 + assert len(result2) == 3 + assert count == 3 + + for r1, r2 in zip(result1, result2): + # Check that the UDF ran successfully + assert len(r1[0]) == r1[1] + assert len(r2[0]) == r2[1] + + q.save("test_udf") + dataset = catalog.get_dataset("test_udf") + dr = catalog.warehouse.schema.dataset_row_cls + sys_schema = {c.name: type(c.type) for c in dr.sys_columns()} + expected_schema = DatasetRow.schema | sys_schema | {"name_len": Int} + assert dataset.schema == expected_schema + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_udf_different_types(cloud_test_catalog): + catalog = cloud_test_catalog.catalog + sources = [cloud_test_catalog.src_uri] + globs = [s.rstrip("/") + "/*" for s in sources] + catalog.index(sources) + catalog.create_dataset_from_sources("animals", globs, recursive=True) + + obj = {"name": "John", "age": 30} + + @udf( + (), + { + "int_col": Int, + "int_col_32": Int32, + "int_col_64": Int64, + "float_col": Float, + "float_col_32": Float32, + "float_col_64": Float64, + "array_col": Array(Float), + "array_col_nested": Array(Array(Float)), + "array_col_32": Array(Float32), + "array_col_64": Array(Float64), + "string_col": String, + "bool_col": Boolean, + "json_col": JSON, + "binary_col": Binary, + }, + ) + def test_types(): + return ( + 5, + 5, + 5, + 0.5, + 0.5, + 0.5, + [0.5], + [[0.5], [0.5]], + [0.5], + [0.5], + "s", + True, + dumps({"a": 1}), + pickle.dumps(obj), + ) + + q = ( + DatasetQuery(name="animals", version=1, catalog=catalog) + .filter(C.name == "cat1") + .add_signals(test_types) + ) + + results = q.select().to_records() + col_values = [ + ( + r["int_col"], + r["int_col_32"], + r["int_col_64"], + r["float_col"], + r["float_col_32"], + r["float_col_64"], + r["array_col"], + r["array_col_nested"], + r["array_col_32"], + r["array_col_64"], + r["string_col"], + r["bool_col"], + r["json_col"], + pickle.loads(r["binary_col"]), # noqa: S301 + ) + for r in results + ] + + assert col_values == [ + ( + 5, + 5, + 5, + 0.5, + 0.5, + 0.5, + [0.5], + [[0.5], [0.5]], + [0.5], + [0.5], + "s", + True, + dumps({"a": 1}), + obj, + ) + ] + + q.save("test_udf") + dataset = catalog.get_dataset("test_udf") + + dr = catalog.warehouse.schema.dataset_row_cls + sys_schema = {c.name: type(c.type) for c in dr.sys_columns()} + expected_schema = ( + DatasetRow.schema + | sys_schema + | { + "int_col": Int, + "int_col_32": Int32, + "int_col_64": Int64, + "float_col": Float, + "float_col_32": Float32, + "float_col_64": Float64, + "array_col": Array(Float()), + "array_col_nested": Array(Array(Float())), + "array_col_32": Array(Float32()), + "array_col_64": Array(Float64()), + "string_col": String, + "bool_col": Boolean, + "json_col": JSON, + "binary_col": Binary, + } + ) + + for c_name, c_type in dataset.schema.items(): + assert c_name in expected_schema + c_type_expected = expected_schema[c_name] + if not isinstance(c_type, SQLType): + c_type = c_type() + c_type_expected = c_type_expected() + + assert c_type.to_dict() == c_type_expected.to_dict() + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +@pytest.mark.parametrize("batch", [1, 4]) +def test_class_udf(cloud_test_catalog, batch): + catalog = cloud_test_catalog.catalog + sources = [cloud_test_catalog.src_uri] + globs = [s.rstrip("/") + "/*" for s in sources] + catalog.index(sources) + catalog.create_dataset_from_sources("animals", globs, recursive=True) + + @udf(("size",), {"total": Int}, method="sum", batch=batch) + class MyUDF: + def __init__(self, constant, multiplier=1): + self.constant = constant + self.multiplier = multiplier + self.batch = batch + + def sum(self, size): + if self.batch > 1: + return [(self.constant + size_ * self.multiplier,) for (size_,) in size] + return (self.constant + size * self.multiplier,) + + q = ( + DatasetQuery(name="animals", version=1, catalog=catalog) + .filter(C.size < 13) + .add_signals(MyUDF(5, multiplier=2)) + ) + results = q.select(C.size, C.total).order_by(C.size).results() + assert results == [ + (3, 11), + (4, 13), + (4, 13), + (4, 13), + (4, 13), + (4, 13), + ] + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +@pytest.mark.parametrize("batch", [False, True]) +def test_udf_parallel(cloud_test_catalog_tmpfile, batch): + catalog = cloud_test_catalog_tmpfile.catalog + sources = [cloud_test_catalog_tmpfile.src_uri] + globs = [s.rstrip("/") + "/*" for s in sources] + catalog.index(sources) + catalog.create_dataset_from_sources("animals", globs, recursive=True) + + @udf(("name",), {"name_len": Int}) + def name_len_local(name): + # A very simple udf. + return (len(name),) + + @udf(("name",), {"name_len": Int}, batch=2) + def name_len_batch(names): + # A very simple udf. + return [(len(name),) for (name,) in names] + + if batch: + # Batching is enabled, we need a udf that acts on + # lists of inputs. + udf_func = name_len_batch + else: + udf_func = name_len_local + + q = ( + DatasetQuery(name="animals", version=1, catalog=catalog) + .filter(C.size < 13) + .filter(C.parent.glob("cats*") | (C.size < 4)) + .add_signals(udf_func, parallel=-1) + .select(C.name, C.name_len) + ) + result = q.results() + + assert len(result) == 3 + for r in result: + # Check that the UDF ran successfully + assert len(r[0]) == r[1] + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +@pytest.mark.parametrize("batch", [1, 4]) +def test_class_udf_parallel(cloud_test_catalog_tmpfile, batch): + catalog = cloud_test_catalog_tmpfile.catalog + sources = [cloud_test_catalog_tmpfile.src_uri] + globs = [s.rstrip("/") + "/*" for s in sources] + catalog.index(sources) + catalog.create_dataset_from_sources("animals", globs, recursive=True) + + @udf(("size",), {"total": Int}, method="sum", batch=batch) + class MyUDF: + def __init__(self, constant, multiplier=1): + self.constant = constant + self.multiplier = multiplier + self.batch = batch + + def sum(self, size): + if self.batch > 1: + return [(self.constant + size_ * self.multiplier,) for (size_,) in size] + return (self.constant + size * self.multiplier,) + + q = ( + DatasetQuery(name="animals", version=1, catalog=catalog) + .filter(C.size < 13) + .add_signals(MyUDF(5, multiplier=2), parallel=2) + ) + results = q.select(C.size, C.total).order_by(C.size).results() + assert results == [ + (3, 11), + (4, 13), + (4, 13), + (4, 13), + (4, 13), + (4, 13), + ] + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_udf_parallel_exec_error(cloud_test_catalog_tmpfile): + catalog = cloud_test_catalog_tmpfile.catalog + sources = [cloud_test_catalog_tmpfile.src_uri] + globs = [s.rstrip("/") + "/*" for s in sources] + catalog.index(sources) + catalog.create_dataset_from_sources("animals", globs, recursive=True) + + @udf((C.name,), {"name_len": Int}) + def name_len_error(_name): + # A udf that raises an exception + raise RuntimeError("Test Error!") + + q = ( + DatasetQuery(name="animals", version=1, catalog=catalog) + .filter(C.size < 13) + .filter(C.parent.glob("cats*") | (C.size < 4)) + .add_signals(name_len_error, parallel=-1) + ) + with pytest.raises(RuntimeError, match="UDF Execution Failed!"): + q.results() + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_udf_parallel_interrupt(cloud_test_catalog_tmpfile, capfd): + catalog = cloud_test_catalog_tmpfile.catalog + sources = [cloud_test_catalog_tmpfile.src_uri] + globs = [s.rstrip("/") + "/*" for s in sources] + catalog.index(sources) + catalog.create_dataset_from_sources("animals", globs, recursive=True) + + @udf(("name",), {"name_len": Int}) + def name_len_interrupt(_name): + # A UDF that emulates cancellation due to a KeyboardInterrupt. + raise KeyboardInterrupt + + q = ( + DatasetQuery(name="animals", version=1, catalog=catalog) + .filter(C.size < 13) + .filter(C.parent.glob("cats*") | (C.size < 4)) + .add_signals(name_len_interrupt, parallel=-1) + ) + with pytest.raises(RuntimeError, match="UDF Execution Failed!"): + q.results() + captured = capfd.readouterr() + assert "KeyboardInterrupt" in captured.err + assert "semaphore" not in captured.err + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +@pytest.mark.parametrize("batch", [False, True]) +@pytest.mark.parametrize("workers", (1, 2)) +@pytest.mark.skipif( + "not os.environ.get('DATACHAIN_DISTRIBUTED')", + reason="Set the DATACHAIN_DISTRIBUTED environment variable " + "to test distributed UDFs", +) +def test_udf_distributed(cloud_test_catalog_tmpfile, batch, workers, datachain_job_id): + catalog = cloud_test_catalog_tmpfile.catalog + sources = [cloud_test_catalog_tmpfile.src_uri] + globs = [s.rstrip("/") + "/*" for s in sources] + catalog.index(sources) + catalog.create_dataset_from_sources("animals", globs, recursive=True) + + @udf(("name",), {"name_len": Int, "blank": String}) + def name_len_local(name): + # A very simple udf. + return len(name), None + + @udf(("name",), {"name_len": Int, "blank": String}, batch=2) + def name_len_batch(names): + # A very simple udf. + return [(len(name), None) for (name,) in names] + + if batch: + # Batching is enabled, we need a udf that acts on lists of inputs. + udf_func = name_len_batch + else: + udf_func = name_len_local + + q = ( + DatasetQuery(name="animals", version=1, catalog=catalog) + .filter(C.size < 13) + .filter(C.parent.glob("cats*") | (C.size < 4)) + .add_signals(udf_func, parallel=2, workers=workers) + .select(C.name, C.name_len, C.blank) + ) + result = q.results() + + assert len(result) == 3 + string_default = String.default_value(catalog.warehouse.db.dialect) + for r in result: + # Check that the UDF ran successfully + assert len(r[0]) == r[1] + assert r[2] == string_default + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +@pytest.mark.parametrize("workers", (1, 2)) +@pytest.mark.skipif( + "not os.environ.get('DATACHAIN_DISTRIBUTED')", + reason="Set the DATACHAIN_DISTRIBUTED environment variable " + "to test distributed UDFs", +) +def test_udf_distributed_exec_error( + cloud_test_catalog_tmpfile, workers, datachain_job_id +): + catalog = cloud_test_catalog_tmpfile.catalog + sources = [cloud_test_catalog_tmpfile.src_uri] + globs = [s.rstrip("/") + "/*" for s in sources] + catalog.index(sources) + catalog.create_dataset_from_sources("animals", globs, recursive=True) + + @udf((C.name,), {"name_len": Int}) + def name_len_error(_name): + # A udf that raises an exception + raise RuntimeError("Test Error!") + + q = ( + DatasetQuery(name="animals", version=1, catalog=catalog) + .filter(C.size < 13) + .filter(C.parent.glob("cats*") | (C.size < 4)) + .add_signals(name_len_error, parallel=2, workers=workers) + ) + with pytest.raises(RuntimeError, match="Test Error!"): + q.results() + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +@pytest.mark.skipif( + "not os.environ.get('DATACHAIN_DISTRIBUTED')", + reason="Set the DATACHAIN_DISTRIBUTED environment variable " + "to test distributed UDFs", +) +def test_udf_distributed_interrupt(cloud_test_catalog_tmpfile, capfd, datachain_job_id): + catalog = cloud_test_catalog_tmpfile.catalog + sources = [cloud_test_catalog_tmpfile.src_uri] + globs = [s.rstrip("/") + "/*" for s in sources] + catalog.index(sources) + catalog.create_dataset_from_sources("animals", globs, recursive=True) + + @udf(("name",), {"name_len": Int}) + def name_len_interrupt(_name): + # A UDF that emulates cancellation due to a KeyboardInterrupt. + raise KeyboardInterrupt + + q = ( + DatasetQuery(name="animals", version=1, catalog=catalog) + .filter(C.size < 13) + .filter(C.parent.glob("cats*") | (C.size < 4)) + .add_signals(name_len_interrupt, parallel=2, workers=2) + ) + with pytest.raises(RuntimeError, match=r"Worker Killed \(KeyboardInterrupt\)"): + q.results() + captured = capfd.readouterr() + assert "KeyboardInterrupt" in captured.err + assert "semaphore" not in captured.err + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +@pytest.mark.skipif( + "not os.environ.get('DATACHAIN_DISTRIBUTED')", + reason="Set the DATACHAIN_DISTRIBUTED environment variable " + "to test distributed UDFs", +) +def test_udf_distributed_cancel(cloud_test_catalog_tmpfile, capfd, datachain_job_id): + catalog = cloud_test_catalog_tmpfile.catalog + metastore = catalog.metastore + sources = [cloud_test_catalog_tmpfile.src_uri] + globs = [s.rstrip("/") + "/*" for s in sources] + catalog.index(sources) + catalog.create_dataset_from_sources("animals", globs, recursive=True) + + job_id = os.environ.get("DATACHAIN_JOB_ID") + + # A job is required for query script cancellation (not using a KeyboardInterrupt) + metastore.db.execute( + metastore._jobs_insert().values( + id=job_id, + status=7, # CANCELING + celery_task_id="", + name="Test Cancel Job", + workers=2, + team_id=metastore.team_id, + created_at=datetime.now(timezone.utc), + params="{}", + metrics="{}", + ), + ) + + @udf(("name",), {"name_len": Int}) + def name_len_slow(name): + # A very simple udf, that processes slowly to emulate being stuck. + from time import sleep + + sleep(10) + return len(name), None + + q = ( + DatasetQuery(name="animals", version=1, catalog=catalog) + .filter(C.size < 13) + .filter(C.parent.glob("cats*") | (C.size < 4)) + .add_signals(name_len_slow, parallel=2, workers=2) + ) + + with pytest.raises(SystemExit) as excinfo: + q.results() + + assert excinfo.value.code == QUERY_SCRIPT_CANCELED_EXIT_CODE + captured = capfd.readouterr() + assert "canceled" in captured.out + assert "semaphore" not in captured.err + + +def test_apply_udf(cloud_test_catalog, tmp_path): + catalog = cloud_test_catalog.catalog + sources = [cloud_test_catalog.src_uri] + globs = [s.rstrip("/") + "/*" for s in sources] + catalog.index(sources) + catalog.create_dataset_from_sources("animals", globs, recursive=True) + + code = """\ + from datachain.query import C, udf + from datachain.sql.types import Int + + @udf((C.name,), {"name_len": Int}) + def name_len(name): + # A very simple udf. + return (len(name),) + """ + script = tmp_path / "foo.py" + script.write_text(dedent(code)) + + catalog.apply_udf(f"{script}:name_len", cloud_test_catalog.src_uri, "from-storage") + q = DatasetQuery(name="from-storage", version=1, catalog=catalog).filter( + C.name_len == 4 + ) + assert len(q.results()) == 6 + + catalog.apply_udf(f"{script}:name_len", "ds://animals", "from-dataset") + q = DatasetQuery(name="from-dataset", version=1, catalog=catalog).filter( + C.name_len == 4 + ) + assert len(q.results()) == 6 + + +def to_str(buf) -> str: + return io.TextIOWrapper(buf, encoding="utf8").read() + + +@pytest.mark.parametrize("param", [LocalFilename(), Object(to_str)]) +@pytest.mark.parametrize("use_cache", [False, True]) +def test_udf_object_param(cloud_test_catalog, dogs_dataset, param, use_cache): + catalog = cloud_test_catalog.catalog + if isinstance(param, Object): + + @udf((C.name, param), {"signal": String}) + def signal(name, obj): + # A very simple udf. + return (name + " -> " + obj,) + + else: + + @udf(("name", param), {"signal": String}) + def signal(name, local_filename): + with open(local_filename, encoding="utf8") as f: + obj = f.read() + return (name + " -> " + obj,) + + q = DatasetQuery(name=dogs_dataset.name, version=1, catalog=catalog).add_signals( + signal, cache=use_cache + ) + result = q.results() + + assert len(result) == 4 + signals = {r[-1] for r in result} + assert signals == {"dog1 -> woof", "dog2 -> arf", "dog3 -> bark", "dog4 -> ruff"} + + uid = Node(*result[0][:-1]).as_uid() + assert catalog.cache.contains(uid) is ( + use_cache or isinstance(param, LocalFilename) + ) + + +@pytest.mark.parametrize("use_cache", [False, True]) +def test_udf_stream_param(cloud_test_catalog, dogs_dataset, use_cache): + catalog = cloud_test_catalog.catalog + + @udf((C.name, Stream()), {"signal": String}) + def signal(name, stream): + with stream as buf: + return (name + " -> " + buf.read().decode("utf-8"),) + + q = DatasetQuery(name=dogs_dataset.name, version=1, catalog=catalog).add_signals( + signal, cache=use_cache + ) + result = q.results() + + assert len(result) == 4 + signals = {r[-1] for r in result} + assert signals == {"dog1 -> woof", "dog2 -> arf", "dog3 -> bark", "dog4 -> ruff"} + + uid = Node(*result[0][:-1]).as_uid() + assert catalog.cache.contains(uid) is use_cache + + +@pytest.mark.parametrize("use_cache", [False, True]) +def test_extract(cloud_test_catalog, dogs_dataset, use_cache): + catalog = cloud_test_catalog.catalog + q = DatasetQuery(name=dogs_dataset.name, version=1, catalog=catalog) + results = set() + for name, stream in q.extract("name", Stream(), cache=use_cache): + with stream: + value = stream.read().decode("utf-8") + results.add((name, value)) + assert results == { + ("dog1", "woof"), + ("dog2", "arf"), + ("dog3", "bark"), + ("dog4", "ruff"), + } + + +def test_extract_object(cloud_test_catalog, dogs_dataset): + ctc = cloud_test_catalog + ds = DatasetQuery(name=dogs_dataset.name, version=1, catalog=ctc.catalog) + data = ds.extract(Object(to_str), "name") + assert set(data) == { + ("woof", "dog1"), + ("arf", "dog2"), + ("bark", "dog3"), + ("ruff", "dog4"), + } + + +def test_extract_chunked(cloud_test_catalog, dogs_dataset): + ctc = cloud_test_catalog + n = 5 + all_data = [] + ds = DatasetQuery(name=dogs_dataset.name, version=1, catalog=ctc.catalog) + for i in range(n): + data = ds.chunk(i, n).extract(Object(to_str), "name") + all_data.extend(data) + + assert set(all_data) == { + ("woof", "dog1"), + ("arf", "dog2"), + ("bark", "dog3"), + ("ruff", "dog4"), + } + + +def test_extract_chunked_limit(cloud_test_catalog, dogs_dataset): + ctc = cloud_test_catalog + chunks = 5 + limit = 1 + all_data = [] + q = DatasetQuery(name=dogs_dataset.name, version=1, catalog=ctc.catalog) + # Add sufficient rows to ensure each chunk has rows + for _ in range(5): + q = q.union(q) + for i in range(chunks): + data = q.limit(limit).chunk(i, chunks).extract(Object(to_str), "name") + all_data.extend(data) + + assert len(all_data) == limit + + +@pytest.mark.parametrize( + "cloud_type, version_aware", + [("file", False)], + indirect=True, +) +def test_extract_limit(cloud_test_catalog, dogs_dataset): + catalog = cloud_test_catalog.catalog + q = DatasetQuery(name=dogs_dataset.name, version=1, catalog=catalog) + results = list(q.limit(2).extract("name")) + assert len(results) == 2 + + +@pytest.mark.parametrize( + "cloud_type, version_aware", + [("file", False)], + indirect=True, +) +def test_extract_order_by(cloud_test_catalog, dogs_dataset): + catalog = cloud_test_catalog.catalog + q = DatasetQuery(name=dogs_dataset.name, version=1, catalog=catalog) + results = list(q.order_by("random").extract("name")) + pairs = list(q.extract("random", "name")) + assert results == [(p[1],) for p in sorted(pairs)] + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_union(cloud_test_catalog): + catalog = cloud_test_catalog.catalog + sources = [str(cloud_test_catalog.src_uri)] + catalog.index(sources) + + src = cloud_test_catalog.src_uri + catalog.create_dataset_from_sources("dogs", [f"{src}/dogs/*"], recursive=True) + catalog.create_dataset_from_sources("cats", [f"{src}/cats/*"], recursive=True) + + dogs = DatasetQuery(name="dogs", version=1, catalog=catalog) + cats = DatasetQuery(name="cats", version=1, catalog=catalog) + + (dogs | cats).save("dogs_cats") + + q = DatasetQuery(name="dogs_cats", version=1, catalog=catalog) + result = q.results() + count = q.count() + assert len(result) == 6 + assert count == 6 + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +@pytest.mark.parametrize("predicates", ["name", C.name]) +def test_join_left_one_column_predicate( + cloud_test_catalog, + dogs_dataset, + dogs_cats_dataset, + predicates, +): + catalog = cloud_test_catalog.catalog + + @udf((), {"sig1": Int}) + def signals1(): + return (1,) + + @udf((), {"sig2": Int}) + def signals2(): + return (2,) + + dogs_cats = DatasetQuery( + name=dogs_cats_dataset.name, version=1, catalog=catalog + ).add_signals(signals1) + dogs = DatasetQuery(name=dogs_dataset.name, version=1, catalog=catalog).add_signals( + signals2 + ) + + joined_records = dogs_cats.join(dogs, predicates).to_records() + assert len(joined_records) == 6 + + cat_records_names = ["cat1", "cat2"] + + dogs_cats_records = DatasetQuery( + name=dogs_cats_dataset.name, version=1, catalog=catalog + ).to_records() + + # rows that found match have both signals + assert all( + r["sig1"] == 1 and r["sig2"] == 2 + for r in joined_records + if r["name"] not in cat_records_names + ) + + int_default = Int.default_value(catalog.warehouse.db.dialect) + # rows from the left that didn't find match (cats) don't have sig2 + assert all( + r["sig1"] == 1 and r["sig2"] == int_default + for r in joined_records + if r["name"] in cat_records_names + ) + # check core duplicated columns + for r in joined_records: + dog_r = next(dr for dr in dogs_cats_records if dr["name"] == r["name"]) + assert all([r[f"{k}_right"] == dog_r[k]] for k in dog_r) + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +@pytest.mark.parametrize( + "predicates", [["name", "parent"], [C.name, C.parent], ["name", C.parent]] +) +def test_join_left_multiple_column_pedicates( + cloud_test_catalog, + dogs_dataset, + dogs_cats_dataset, + predicates, +): + catalog = cloud_test_catalog.catalog + + @udf((), {"sig1": Int}) + def signals1(): + return (1,) + + @udf((), {"sig2": Int}) + def signals2(): + return (2,) + + dogs_cats = DatasetQuery( + name=dogs_cats_dataset.name, version=1, catalog=catalog + ).add_signals(signals1) + dogs = DatasetQuery(name=dogs_dataset.name, version=1, catalog=catalog).add_signals( + signals2 + ) + + cat_records_names = ["cat1", "cat2"] + + dogs_cats_records = DatasetQuery( + name=dogs_cats_dataset.name, version=1, catalog=catalog + ).to_records() + + joined_records = dogs_cats.join(dogs, predicates).to_records() + assert len(joined_records) == 6 + + # rows that found match have both signals + assert all( + r["sig1"] == 1 and r["sig2"] == 2 + for r in joined_records + if r["name"] not in cat_records_names + ) + int_default = Int.default_value(catalog.warehouse.db.dialect) + # rows from the left that didn't find match (cats) don't have sig2 + assert all( + r["sig1"] == 1 and r["sig2"] == int_default + for r in joined_records + if r["name"] in cat_records_names + ) + # check core duplicated columns + for r in joined_records: + dog_r = next(dr for dr in dogs_cats_records if dr["name"] == r["name"]) + assert all([r[f"{k}_right"] == dog_r[k]] for k in dog_r) + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +@pytest.mark.parametrize("inner", [True, False]) +def test_join_with_binary_expression_on_one_column( + cloud_test_catalog, + dogs_dataset, + cats_dataset, + inner, +): + catalog = cloud_test_catalog.catalog + dogs = DatasetQuery(name=dogs_dataset.name, version=1, catalog=catalog) + cats = DatasetQuery(name=cats_dataset.name, version=1, catalog=catalog) + dogs_cats = dogs.union(cats) + + res = dogs_cats.join( + dogs, dogs_cats.c("name") == dogs.c("name"), inner=inner + ).to_records() + + if inner: + expected = [ + ("dog1", "dog1"), + ("dog2", "dog2"), + ("dog3", "dog3"), + ("dog4", "dog4"), + ] + else: + string_default = String.default_value(catalog.warehouse.db.dialect) + expected = [ + ("cat1", string_default), + ("cat2", string_default), + ("dog1", "dog1"), + ("dog2", "dog2"), + ("dog3", "dog3"), + ("dog4", "dog4"), + ] + + assert ( + sorted(((r["name"], r["name_right"]) for r in res), key=lambda x: x[0]) + == expected + ) + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +@pytest.mark.parametrize("inner", [True, False]) +def test_join_with_binary_expression_on_multiple_columns( + cloud_test_catalog, + dogs_dataset, + dogs_cats_dataset, + inner, +): + catalog = cloud_test_catalog.catalog + dogs = DatasetQuery(name=dogs_dataset.name, version=1, catalog=catalog) + dogs_cats = DatasetQuery(name=dogs_cats_dataset.name, version=1, catalog=catalog) + + res = dogs_cats.join( + dogs, + ( + (dogs_cats.c("name") == dogs.c("name")) + & (dogs_cats.c("parent") == dogs.c("parent")) + ), + inner=inner, + ).to_records() + + if inner: + expected = [ + ("dog1", "dog1"), + ("dog2", "dog2"), + ("dog3", "dog3"), + ("dog4", "dog4"), + ] + else: + string_default = String.default_value(catalog.warehouse.db.dialect) + expected = [ + ("cat1", string_default), + ("cat2", string_default), + ("dog1", "dog1"), + ("dog2", "dog2"), + ("dog3", "dog3"), + ("dog4", "dog4"), + ] + + assert ( + sorted(((r["name"], r["name_right"]) for r in res), key=lambda x: x[0]) + == expected + ) + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +@pytest.mark.parametrize("inner", [True, False]) +@pytest.mark.parametrize("column_predicate", ["name", C.name]) +def test_join_with_combination_binary_expression_and_column_predicates( + cloud_test_catalog, + dogs_dataset, + dogs_cats_dataset, + inner, + column_predicate, +): + catalog = cloud_test_catalog.catalog + dogs = DatasetQuery(name=dogs_dataset.name, version=1, catalog=catalog) + dogs_cats = DatasetQuery(name=dogs_cats_dataset.name, version=1, catalog=catalog) + + res = dogs_cats.join( + dogs, + [column_predicate, dogs_cats.c("parent") == dogs.c("parent")], + inner=inner, + ).to_records() + + if inner: + expected = [ + ("dog1", "dog1"), + ("dog2", "dog2"), + ("dog3", "dog3"), + ("dog4", "dog4"), + ] + else: + string_default = String.default_value(catalog.warehouse.db.dialect) + expected = [ + ("cat1", string_default), + ("cat2", string_default), + ("dog1", "dog1"), + ("dog2", "dog2"), + ("dog3", "dog3"), + ("dog4", "dog4"), + ] + + assert ( + sorted(((r["name"], r["name_right"]) for r in res), key=lambda x: x[0]) + == expected + ) + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +@pytest.mark.parametrize("inner", [True, False]) +def test_join_with_binary_expression_with_arithmetics( + cloud_test_catalog, + dogs_dataset, + cats_dataset, + inner, +): + catalog = cloud_test_catalog.catalog + dogs = DatasetQuery(name=dogs_dataset.name, version=1, catalog=catalog) + cats = DatasetQuery(name=cats_dataset.name, version=1, catalog=catalog) + + res = cats.join( + dogs, cats.c("size") == dogs.c("size") + 1, inner=inner + ).to_records() + + assert sorted(((r["name"], r["name_right"]) for r in res), key=lambda x: x[0]) == [ + ("cat1", "dog2"), + ("cat2", "dog2"), + ] + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_join_conflicting_custom_columns(cloud_test_catalog, dogs_dataset): + catalog = cloud_test_catalog.catalog + + @udf((), {"sig1": Int}) + def signals1(): + return (1,) + + @udf((), {"sig1": Int}) + def signals2(): + return (2,) + + ds1 = DatasetQuery(name=dogs_dataset.name, version=1, catalog=catalog).add_signals( + signals1 + ) + ds2 = DatasetQuery(name=dogs_dataset.name, version=1, catalog=catalog).add_signals( + signals2 + ) + + joined_records = ds1.join(ds2, "name").to_records() + assert len(joined_records) == 4 + + # check custom columns + assert all(r["sig1"] == 1 and r["sig1_right"] == 2 for r in joined_records) + + joined_records = ds1.join(ds2, "name", rname="{name}_dupl").to_records() + assert len(joined_records) == 4 + + # check custom columns + assert all(r["sig1"] == 1 and r["sig1_dupl"] == 2 for r in joined_records) + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_join_inner( + cloud_test_catalog, + dogs_dataset, + dogs_cats_dataset, +): + catalog = cloud_test_catalog.catalog + + @udf((), {"sig1": Int}) + def signals1(): + return (1,) + + @udf((), {"sig2": Int}) + def signals2(): + return (2,) + + dogs_cats = DatasetQuery( + name=dogs_cats_dataset.name, version=1, catalog=catalog + ).add_signals(signals1) + dogs = DatasetQuery(name=dogs_dataset.name, version=1, catalog=catalog).add_signals( + signals2 + ) + + joined_records = dogs_cats.join(dogs, "name", inner=True).to_records() + assert len(joined_records) == 4 + + dogs_records = DatasetQuery( + name=dogs_dataset.name, version=1, catalog=catalog + ).to_records() + + # check custom columns + assert all(r["sig1"] == 1 and r["sig2"] == 2 for r in joined_records) + for r in joined_records: + dog_r = next(dr for dr in dogs_records if dr["name"] == r["name"]) + assert all([r[f"{k}_right"] == dog_r[k]] for k in dog_r) + + # joining on multiple fields + joined_records = dogs_cats.join(dogs, ["parent", "name"], inner=True).to_records() + assert len(joined_records) == 4 + + # check custom columns + assert all(r["sig1"] == 1 and r["sig2"] == 2 for r in joined_records) + # check core duplicated columns + for r in joined_records: + dog_r = next(dr for dr in dogs_records if dr["name"] == r["name"]) + assert all([r[f"{k}_right"] == dog_r[k]] for k in dog_r) + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_join_with_self(cloud_test_catalog, dogs_dataset): + catalog = cloud_test_catalog.catalog + + @udf((), {"sig1": Int}) + def signals1(): + return (1,) + + dogs_records = DatasetQuery( + name=dogs_dataset.name, version=1, catalog=catalog + ).to_records() + + dogs = DatasetQuery(name=dogs_dataset.name, version=1, catalog=catalog).add_signals( + signals1 + ) + + joined_records = dogs.join(dogs, "name").to_records() + assert len(joined_records) == 4 + + # check custom columns + assert all(r["sig1"] == 1 and r["sig1_right"] == 1 for r in joined_records) + # check core duplicated columns + for r in joined_records: + dog_r = next(dr for dr in dogs_records if dr["name"] == r["name"]) + assert all([r[f"{k}_right"] == dog_r[k]] for k in dog_r) + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_join_with_missing_predicates(cloud_test_catalog, dogs_dataset): + catalog = cloud_test_catalog.catalog + + @udf((), {"sig1": Int}) + def signals1(): + return (1,) + + @udf((), {"sig2": Int}) + def signals2(): + return (1,) + + dogs1 = DatasetQuery( + name=dogs_dataset.name, version=1, catalog=catalog + ).add_signals(signals1) + dogs2 = DatasetQuery( + name=dogs_dataset.name, version=1, catalog=catalog + ).add_signals(signals2) + + with pytest.raises(ValueError) as excinfo: + dogs1.join(dogs2, "sig1").to_records() + assert str(excinfo.value) == "Column sig1 was not found in right part of the join" + + with pytest.raises(ValueError) as excinfo: + dogs1.join(dogs2, "sig2").to_records() + assert str(excinfo.value) == "Column sig2 was not found in left part of the join" + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_join_with_wrong_predicates(cloud_test_catalog, dogs_dataset): + catalog = cloud_test_catalog.catalog + + dogs1 = DatasetQuery(name=dogs_dataset.name, version=1, catalog=catalog) + dogs2 = DatasetQuery(name=dogs_dataset.name, version=1, catalog=catalog) + + with pytest.raises(ValueError) as excinfo: + dogs1.join(dogs2, []).to_records() + assert str(excinfo.value) == "Missing predicates" + + with pytest.raises(TypeError) as excinfo: + dogs1.join(dogs2, [[]]).to_records() + assert str(excinfo.value) == "Unsupported predicate [] for join expression" + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_join_with_missing_columns_in_expression( + cloud_test_catalog, dogs_dataset, cats_dataset +): + catalog = cloud_test_catalog.catalog + + dogs1 = DatasetQuery(name=dogs_dataset.name, version=1, catalog=catalog) + dogs2 = DatasetQuery(name=dogs_dataset.name, version=1, catalog=catalog) + cats = DatasetQuery(name=cats_dataset.name, version=1, catalog=catalog) + + with pytest.raises(ValueError) as excinfo: + dogs1.join(dogs2, dogs1.c("wrong") == dogs2.c("name")).to_records() + assert str(excinfo.value) == "Column wrong was not found in left part of the join" + + with pytest.raises(ValueError) as excinfo: + dogs1.join(dogs2, dogs1.c("name") == dogs2.c("wrong")).to_records() + assert str(excinfo.value) == "Column wrong was not found in right part of the join" + + with pytest.raises(ValueError) as excinfo: + dogs1.join(dogs2, dogs1.c("name") == cats.c("name")).to_records() + assert str(excinfo.value) == ( + "Column name was not found in left or right part of the join" + ) + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +@pytest.mark.parametrize("inner", [True, False]) +def test_join_with_using_functions_in_expression( + cloud_test_catalog, dogs_dataset, dogs_cats_dataset, inner +): + catalog = cloud_test_catalog.catalog + dogs = DatasetQuery(name=dogs_dataset.name, version=1, catalog=catalog) + dogs_cats = DatasetQuery(name=dogs_cats_dataset.name, version=1, catalog=catalog) + + res = dogs_cats.join( + dogs, + ( + sqlalchemy.func.upper(dogs_cats.c("name")) + == sqlalchemy.func.upper(dogs.c("name")) + ), + inner=inner, + ).to_records() + + if inner: + expected = [ + ("dog1", "dog1"), + ("dog2", "dog2"), + ("dog3", "dog3"), + ("dog4", "dog4"), + ] + else: + string_default = String.default_value(catalog.warehouse.db.dialect) + expected = [ + ("cat1", string_default), + ("cat2", string_default), + ("dog1", "dog1"), + ("dog2", "dog2"), + ("dog3", "dog3"), + ("dog4", "dog4"), + ] + + assert ( + sorted(((r["name"], r["name_right"]) for r in res), key=lambda x: x[0]) + == expected + ) + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_row_generator(cloud_test_catalog, dogs_dataset): + catalog = cloud_test_catalog.catalog + + @udf(("name", C.parent), DatasetRow.schema) + def gen(name, parent): + # A very simple file row generator. + parent_path = name if not parent else f"{parent}/{name}" + yield DatasetRow.create("subobject", size=50, parent=parent_path) + yield DatasetRow.create("subobject2", size=70, parent=parent_path) + + q = DatasetQuery(name=dogs_dataset.name, version=1, catalog=catalog).generate(gen) + result = q.to_records() + + parents_names = [(r["parent"], r["name"]) for r in result] + parents_names.sort(key=lambda x: (x[1], x[0])) + + assert parents_names == [ + ("dogs/dog1", "subobject"), + ("dogs/dog2", "subobject"), + ("dogs/dog3", "subobject"), + ("dogs/others/dog4", "subobject"), + ("dogs/dog1", "subobject2"), + ("dogs/dog2", "subobject2"), + ("dogs/dog3", "subobject2"), + ("dogs/others/dog4", "subobject2"), + ] + + q.save("test_generator") + dataset = catalog.get_dataset("test_generator") + schema = dataset.schema + dr = catalog.warehouse.schema.dataset_row_cls + sys_schema = {c.name: type(c.type) for c in dr.sys_columns()} + assert schema == DatasetRow.schema | sys_schema + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_row_generator_with_filter(cloud_test_catalog, dogs_dataset): + catalog = cloud_test_catalog.catalog + + @udf(("name", C.parent), DatasetRow.schema) + def gen(name, parent): + # A very simple file row generator. + parent_path = name if not parent else f"{parent}/{name}" + yield DatasetRow.create("subobject", size=50, parent=parent_path) + yield DatasetRow.create("subobject2", size=70, parent=parent_path) + + q = ( + DatasetQuery(name=dogs_dataset.name, version=1, catalog=catalog) + .generate(gen) + .filter(C.name == "subobject") + ) + result = q.to_records() + + parents_names = [(r["parent"], r["name"]) for r in result] + parents_names.sort(key=lambda x: (x[1], x[0])) + + assert len(parents_names) == 4 + + assert parents_names == [ + ("dogs/dog1", "subobject"), + ("dogs/dog2", "subobject"), + ("dogs/dog3", "subobject"), + ("dogs/others/dog4", "subobject"), + ] + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_row_generator_with_limit(cloud_test_catalog, dogs_dataset): + catalog = cloud_test_catalog.catalog + + @udf((C.name, C.parent), DatasetRow.schema) + def gen(name, parent): + # A very simple file row generator. + parent_path = name if not parent else f"{parent}/{name}" + yield DatasetRow.create("subobject", size=50, parent=parent_path) + yield DatasetRow.create("subobject2", size=70, parent=parent_path) + + q = ( + DatasetQuery(name=dogs_dataset.name, version=1, catalog=catalog) + .order_by(C.parent, C.name) + .limit(1) + .generate(gen) + ) + result = q.to_records() + + parents_names = [(r["parent"], r["name"]) for r in result] + parents_names.sort(key=lambda x: (x[0], x[1])) + + assert parents_names == [ + ("dogs/dog1", "subobject"), + ("dogs/dog1", "subobject2"), + ] + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_row_generator_parallel(cloud_test_catalog_tmpfile): + # Setup catalog. + dogs_dataset_name = uuid.uuid4().hex + catalog = cloud_test_catalog_tmpfile.catalog + catalog.index([cloud_test_catalog_tmpfile.src_uri]) + src_uri = cloud_test_catalog_tmpfile.src_uri + + dogs_dataset = catalog.create_dataset_from_sources( + dogs_dataset_name, [f"{src_uri}/dogs/*"], recursive=True + ) + + @udf(("name", "parent"), DatasetRow.schema) + def gen(name, parent): + # A very simple file row generator. + parent_path = name if not parent else f"{parent}/{name}" + yield DatasetRow.create("subobject", size=50, parent=parent_path) + yield DatasetRow.create("subobject2", size=70, parent=parent_path) + + q = DatasetQuery(name=dogs_dataset.name, version=1, catalog=catalog).generate( + gen, parallel=-1 + ) + result = q.to_records() + + parents_names = [(r["parent"], r["name"]) for r in result] + parents_names.sort(key=lambda x: (x[1], x[0])) + + assert parents_names == [ + ("dogs/dog1", "subobject"), + ("dogs/dog2", "subobject"), + ("dogs/dog3", "subobject"), + ("dogs/others/dog4", "subobject"), + ("dogs/dog1", "subobject2"), + ("dogs/dog2", "subobject2"), + ("dogs/dog3", "subobject2"), + ("dogs/others/dog4", "subobject2"), + ] + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_row_generator_batch(cloud_test_catalog, dogs_dataset): + catalog = cloud_test_catalog.catalog + + @udf(("name", "parent"), DatasetRow.schema, batch=4) + def gen(inputs): + for name, parent in inputs: + parent_path = name if not parent else f"{parent}/{name}" + yield DatasetRow.create("subobject", size=50, parent=parent_path) + yield DatasetRow.create("subobject2", size=70, parent=parent_path) + + q = DatasetQuery(name=dogs_dataset.name, version=1, catalog=catalog).generate(gen) + result = q.to_records() + parents_names = [(r["parent"], r["name"]) for r in result] + parents_names.sort(key=lambda x: (x[1], x[0])) + + assert parents_names == [ + ("dogs/dog1", "subobject"), + ("dogs/dog2", "subobject"), + ("dogs/dog3", "subobject"), + ("dogs/others/dog4", "subobject"), + ("dogs/dog1", "subobject2"), + ("dogs/dog2", "subobject2"), + ("dogs/dog3", "subobject2"), + ("dogs/others/dog4", "subobject2"), + ] + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_row_generator_class(cloud_test_catalog, dogs_dataset): + catalog = cloud_test_catalog.catalog + + @udf( + params=(C.name, C.parent), + output=DatasetRow.schema, + method="generate_subobjects", + ) + class Subobjects: + def __init__(self): + pass + + def generate_subobjects(self, name, parent): + parent_path = name if not parent else f"{parent}/{name}" + yield DatasetRow.create("subobject", size=50, parent=parent_path) + yield DatasetRow.create("subobject2", size=70, parent=parent_path) + + q = DatasetQuery(name=dogs_dataset.name, version=1, catalog=catalog).generate( + Subobjects + ) + result = q.to_records() + parents_names = [(r["parent"], r["name"]) for r in result] + parents_names.sort(key=lambda x: (x[1], x[0])) + + assert parents_names == [ + ("dogs/dog1", "subobject"), + ("dogs/dog2", "subobject"), + ("dogs/dog3", "subobject"), + ("dogs/others/dog4", "subobject"), + ("dogs/dog1", "subobject2"), + ("dogs/dog2", "subobject2"), + ("dogs/dog3", "subobject2"), + ("dogs/others/dog4", "subobject2"), + ] + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_row_generator_class_batch(cloud_test_catalog, dogs_dataset): + catalog = cloud_test_catalog.catalog + + @udf( + params=(C.name, C.parent), + output=DatasetRow.schema, + method="generate_subobjects", + batch=4, + ) + class Subobjects: + def __init__(self): + pass + + def generate_subobjects(self, inputs): + for name, parent in inputs: + parent_path = name if not parent else f"{parent}/{name}" + yield DatasetRow.create("subobject", size=50, parent=parent_path) + yield DatasetRow.create("subobject2", size=70, parent=parent_path) + + q = DatasetQuery(name=dogs_dataset.name, version=1, catalog=catalog).generate( + Subobjects + ) + result = q.to_records() + parents_names = [(r["parent"], r["name"]) for r in result] + parents_names.sort(key=lambda x: (x[1], x[0])) + + assert parents_names == [ + ("dogs/dog1", "subobject"), + ("dogs/dog2", "subobject"), + ("dogs/dog3", "subobject"), + ("dogs/others/dog4", "subobject"), + ("dogs/dog1", "subobject2"), + ("dogs/dog2", "subobject2"), + ("dogs/dog3", "subobject2"), + ("dogs/others/dog4", "subobject2"), + ] + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_row_generator_partition_by(cloud_test_catalog, dogs_dataset): + catalog = cloud_test_catalog.catalog + + @udf(("name", "parent"), DatasetRow.extend(cnt=Int)) + def gen(inputs): + cnt = len(inputs) + for name, parent in inputs: + parent_path = name if not parent else f"{parent}/{name}" + yield (*DatasetRow.create("subobject", size=50, parent=parent_path), cnt) + yield (*DatasetRow.create("subobject2", size=70, parent=parent_path), cnt) + + result = ( + DatasetQuery(name=dogs_dataset.name, version=1, catalog=catalog) + .generate(gen, partition_by="parent") + .to_records() + ) + parents_names = [(r["parent"], r["name"], r["cnt"]) for r in result] + parents_names.sort(key=lambda x: (x[1], x[0])) + + assert parents_names == [ + ("dogs/dog1", "subobject", 3), + ("dogs/dog2", "subobject", 3), + ("dogs/dog3", "subobject", 3), + ("dogs/others/dog4", "subobject", 1), + ("dogs/dog1", "subobject2", 3), + ("dogs/dog2", "subobject2", 3), + ("dogs/dog3", "subobject2", 3), + ("dogs/others/dog4", "subobject2", 1), + ] + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_row_generator_partition_by_parallel(cloud_test_catalog_tmpfile): + # Setup catalog. + dogs_dataset_name = uuid.uuid4().hex + catalog = cloud_test_catalog_tmpfile.catalog + catalog.index([cloud_test_catalog_tmpfile.src_uri]) + src_uri = cloud_test_catalog_tmpfile.src_uri + + dogs_dataset = catalog.create_dataset_from_sources( + dogs_dataset_name, [f"{src_uri}/dogs/*"], recursive=True + ) + + @udf(("name", "parent"), DatasetRow.extend(cnt=Int)) + def gen(inputs): + cnt = len(inputs) + for name, parent in inputs: + parent_path = name if not parent else f"{parent}/{name}" + yield (*DatasetRow.create("subobject", size=50, parent=parent_path), cnt) + yield (*DatasetRow.create("subobject2", size=70, parent=parent_path), cnt) + + q = DatasetQuery(name=dogs_dataset.name, version=1, catalog=catalog).generate( + gen, partition_by="parent", parallel=-1 + ) + result = q.to_records() + + parents_names = [(r["parent"], r["name"], r["cnt"]) for r in result] + parents_names.sort(key=lambda x: (x[1], x[0])) + + assert parents_names == [ + ("dogs/dog1", "subobject", 3), + ("dogs/dog2", "subobject", 3), + ("dogs/dog3", "subobject", 3), + ("dogs/others/dog4", "subobject", 1), + ("dogs/dog1", "subobject2", 3), + ("dogs/dog2", "subobject2", 3), + ("dogs/dog3", "subobject2", 3), + ("dogs/others/dog4", "subobject2", 1), + ] + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_row_generator_partition_by_batch(cloud_test_catalog, dogs_dataset): + catalog = cloud_test_catalog.catalog + + @udf(("name", "parent"), DatasetRow.extend(cnt=Int), batch=2) + def gen(inputs): + cnt = len(inputs) + for name, parent in inputs: + parent_path = name if not parent else f"{parent}/{name}" + yield (*DatasetRow.create("subobject", size=50, parent=parent_path), cnt) + yield (*DatasetRow.create("subobject2", size=70, parent=parent_path), cnt) + + result = ( + DatasetQuery(name=dogs_dataset.name, version=1, catalog=catalog) + .generate(gen, partition_by="parent") + .to_records() + ) + parents_names = [(r["parent"], r["name"], r["cnt"]) for r in result] + parents_names.sort(key=lambda x: (x[1], x[0])) + + assert parents_names == [ + ("dogs/dog1", "subobject", 3), + ("dogs/dog2", "subobject", 3), + ("dogs/dog3", "subobject", 3), + ("dogs/others/dog4", "subobject", 1), + ("dogs/dog1", "subobject2", 3), + ("dogs/dog2", "subobject2", 3), + ("dogs/dog3", "subobject2", 3), + ("dogs/others/dog4", "subobject2", 1), + ] + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_row_generator_with_new_columns(cloud_test_catalog, dogs_dataset): + catalog = cloud_test_catalog.catalog + now = datetime.now(timezone.utc).replace(microsecond=0) + + int_example = 25 + + new_columns = { + "string_col": String, + "int_col": Int, + "int_col_32": Int32, + "int_col_64": Int64, + "bool_col": Boolean, + "float_col": Float, + "float_col_32": Float32, + "float_col_64": Float64, + "json_col": JSON, + "datetime_col": DateTime, + "binary_col": Binary, + "array_col": Array(Float), + "array_col_nested": Array(Array(Float)), + "array_col_32": Array(Float32), + "array_col_64": Array(Float64), + } + + @udf( + params=(C.name, C.parent), + output=DatasetRow.schema | new_columns, + method="generate_subobjects", + ) + class Subobjects: + def __init__(self): + pass + + def generate_subobjects(self, name, parent): + parent_path = name if not parent else f"{parent}/{name}" + yield ( + *DatasetRow.create("subobject", size=50, parent=parent_path), + "some_string", + 10, + 11, + 12, + True, + 0.5, + 0.5, + 0.5, + dumps({"a": 1}), + now, + int_example.to_bytes(2, "big"), + [0.5, 0.5], + [[0.5], [0.5]], + [0.5, 0.5], + [0.5, 0.5], + ) + + DatasetQuery(name=dogs_dataset.name, version=1, catalog=catalog).generate( + Subobjects + ).save("dogs_with_rows_and_signals") + + q = DatasetQuery(name="dogs_with_rows_and_signals", catalog=catalog) + result = q.select().to_records() + + col_values = [ + ( + r["parent"], + r["name"], + r["string_col"], + r["int_col"], + r["int_col_32"], + r["int_col_64"], + r["bool_col"], + r["float_col"], + r["float_col_32"], + r["float_col_64"], + r["json_col"], + r["datetime_col"].astimezone(timezone.utc) if r["datetime_col"] else None, + int.from_bytes(r["binary_col"], "big"), # converting from binary to int + r["array_col"], + r["array_col_nested"], + r["array_col_32"], + r["array_col_64"], + ) + for r in result + ] + + col_values.sort(key=lambda x: (x[1], x[0])) + + new_col_values = ( + "some_string", + 10, + 11, + 12, + True, + 0.5, + 0.5, + 0.5, + dumps({"a": 1}), + now, + int_example, + [0.5, 0.5], + [[0.5], [0.5]], + [0.5, 0.5], + [0.5, 0.5], + ) + + assert col_values == [ + ("dogs/dog1", "subobject", *new_col_values), + ("dogs/dog2", "subobject", *new_col_values), + ("dogs/dog3", "subobject", *new_col_values), + ("dogs/others/dog4", "subobject", *new_col_values), + ] + + dataset = catalog.get_dataset("dogs_with_rows_and_signals") + expected_schema = DatasetRow.schema | new_columns + + dr = catalog.warehouse.schema.dataset_row_cls + schema = dataset.schema + assert all(isinstance(c.type, schema.pop(c.name)) for c in dr.sys_columns()) + + for c_name, c_type in schema.items(): + assert c_name in expected_schema + c_type_expected = expected_schema[c_name] + if not isinstance(c_type, SQLType): + c_type = c_type() + c_type_expected = c_type_expected() + + assert c_type.to_dict() == c_type_expected.to_dict() + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_row_generators_sequence_with_new_columns(cloud_test_catalog, dogs_dataset): + catalog = cloud_test_catalog.catalog + + @udf(params="name", output=DatasetRow.schema | {"name_upper": String, "p1": Int}) + def upper(name): + yield *DatasetRow.create(name), name.upper(), 1 + + @udf(params="name", output=DatasetRow.schema | {"name_lower": String, "p2": Int}) + def lower(name): + yield *DatasetRow.create(name), name.lower(), 2 + + DatasetQuery(name=dogs_dataset.name, catalog=catalog).generate(upper).save("upper") + for res in DatasetQuery(name="upper", catalog=catalog).to_records(): + assert "name_upper" in res + assert res["name_upper"] == res["name"].upper() + assert "p1" in res + assert res["p1"] == 1 + + DatasetQuery(name="upper", catalog=catalog).generate(lower).save("lower") + for res in DatasetQuery(name="lower", catalog=catalog).to_records(): + assert "name_upper" not in res + assert "name_lower" in res + assert res["name_lower"] == res["name"].lower() + assert "p1" not in res + assert "p2" in res + assert res["p2"] == 2 + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_row_generator_with_new_columns_empty_values(cloud_test_catalog, dogs_dataset): + catalog = cloud_test_catalog.catalog + dialect = catalog.warehouse.db.dialect + + new_columns = { + "int_col": Int, + "int_col_32": Int32, + "int_col_64": Int64, + "bool_col": Boolean, + "float_col": Float, + "float_col_32": Float32, + "float_col_64": Float64, + "json_col": JSON, + "datetime_col": DateTime, + "binary_col": Binary, + "array_col": Array(Float), + "array_col_nested": Array(Array(Float)), + "array_col_32": Array(Float32), + "array_col_64": Array(Float64), + } + new_col_values_empty = tuple(t.default_value(dialect) for t in new_columns.values()) + + @udf( + params=(C.name, C.parent), + output=DatasetRow.schema | new_columns, + method="generate_subobjects", + ) + class Subobjects: + def __init__(self): + pass + + def generate_subobjects(self, name, parent): + parent_path = name if not parent else f"{parent}/{name}" + yield ( + DatasetRow.create("subobject", size=50, parent=parent_path) + + new_col_values_empty + ) + + DatasetQuery(name=dogs_dataset.name, version=1, catalog=catalog).generate( + Subobjects + ).save("dogs_with_rows_and_signals") + + q = DatasetQuery(name="dogs_with_rows_and_signals", catalog=catalog) + result = q.to_records() + + col_values = [ + ( + r["parent"], + r["name"], + r["int_col"], + r["int_col_32"], + r["int_col_64"], + r["bool_col"], + r["float_col"], + r["float_col_32"], + r["float_col_64"], + r["json_col"], + r["datetime_col"], + r["binary_col"], + r["array_col"], + r["array_col_nested"], + r["array_col_32"], + r["array_col_64"], + ) + for r in result + ] + + col_values.sort(key=lambda x: (x[1], x[0])) + + assert col_values == [ + ("dogs/dog1", "subobject", *new_col_values_empty), + ("dogs/dog2", "subobject", *new_col_values_empty), + ("dogs/dog3", "subobject", *new_col_values_empty), + ("dogs/others/dog4", "subobject", *new_col_values_empty), + ] + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_row_generator_with_new_columns_numpy(cloud_test_catalog, dogs_dataset): + catalog = cloud_test_catalog.catalog + + new_columns = { + "int_col_32": Int32, + "int_col_64": Int64, + "float_col_32": Float32, + "float_col_64": Float64, + "int_float_col_32": Float32, + "array_col_nested": Array(Array(Float32)), + "array_col_32": Array(Float32), + "array_col_64": Array(Float64), + "array_int_float_col_32": Array(Float32), + "array_empty_col_32": Array(Float32), + } + + @udf( + params=(C.name, C.parent), + output=DatasetRow.schema | new_columns, + method="generate_subobjects", + ) + class Subobjects: + def __init__(self): + pass + + def generate_subobjects(self, name, parent): + parent_path = name if not parent else f"{parent}/{name}" + yield ( + *DatasetRow.create("subobject", size=50, parent=parent_path), + np.int32(11), + np.int64(12), + np.float32(0.5), + np.float64(0.5), + np.int32(13), + np.array([[0.5], [0.5]], dtype=np.float32), + np.array([0.5, 0.5], dtype=np.float32), + np.array([0.5, 0.5], dtype=np.float64), + np.array([14, 15], dtype=np.int32), + np.array([], dtype=np.float32), + ) + + DatasetQuery(name=dogs_dataset.name, version=1, catalog=catalog).generate( + Subobjects + ).save("dogs_with_rows_and_signals") + + q = DatasetQuery(name="dogs_with_rows_and_signals", catalog=catalog) + result = q.to_records() + + col_values = [ + ( + r["parent"], + r["name"], + r["int_col_32"], + r["int_col_64"], + r["float_col_32"], + r["float_col_64"], + r["int_float_col_32"], + r["array_col_nested"], + r["array_col_32"], + r["array_col_64"], + r["array_int_float_col_32"], + r["array_empty_col_32"], + ) + for r in result + ] + + col_values.sort(key=lambda x: (x[1], x[0])) + + new_col_values = ( + 11, + 12, + 0.5, + 0.5, + 13.0, + [[0.5], [0.5]], + [0.5, 0.5], + [0.5, 0.5], + [14.0, 15.0], + [], + ) + + assert col_values == [ + ("dogs/dog1", "subobject", *new_col_values), + ("dogs/dog2", "subobject", *new_col_values), + ("dogs/dog3", "subobject", *new_col_values), + ("dogs/others/dog4", "subobject", *new_col_values), + ] + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_row_generator_with_new_columns_wrong_type(cloud_test_catalog, dogs_dataset): + catalog = cloud_test_catalog.catalog + + @udf( + params=(C.name, C.parent), + output={**DatasetRow.schema, "int_col": Int}, + method="generate_subobjects", + ) + class Subobjects: + def __init__(self): + pass + + def generate_subobjects(self, name, parent): + parent_path = name if not parent else f"{parent}/{name}" + yield (*DatasetRow.create("subobject", size=50, parent=parent_path), 0.5) + + with pytest.raises(ValueError): + DatasetQuery(name=dogs_dataset.name, version=1, catalog=catalog).generate( + Subobjects + ).to_records() + + +@pytest.mark.parametrize("tree", [TARRED_TREE], indirect=True) +def test_index_tar(cloud_test_catalog): + ctc = cloud_test_catalog + catalog = ctc.catalog + catalog.index([ctc.src_uri]) + catalog.create_dataset_from_sources("animals", [ctc.src_uri]) + + q = DatasetQuery(name="animals", version=1, catalog=catalog).generate(index_tar) + q.save("extracted") + + assert_row_names( + catalog, + catalog.get_dataset("extracted"), + 1, + { + "animals.tar", + "cat1", + "cat2", + "description", + "dog1", + "dog2", + "dog3", + "dog4", + }, + ) + + rows = catalog.ls_dataset_rows("extracted", 1) + + offsets = [ + json.loads(row["location"])[0]["offset"] + for row in rows + if row["name"] != "animals.tar" + ] + # Check that offsets are unique integers + assert all(isinstance(offset, int) for offset in offsets) + assert len(set(offsets)) == len(offsets) + + assert all(row["vtype"] == "tar" for row in rows if row["name"] != "animals.tar") + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_checksum_udf(cloud_test_catalog, dogs_dataset): + catalog = cloud_test_catalog.catalog + + q = DatasetQuery(name=dogs_dataset.name, version=1, catalog=catalog).add_signals( + checksum + ) + result = q.results() + + assert len(result) == 4 + + +@pytest.mark.parametrize("tree", [TARRED_TREE], indirect=True) +def test_tar_loader(cloud_test_catalog): + ctc = cloud_test_catalog + catalog = ctc.catalog + catalog.index([ctc.src_uri]) + catalog.create_dataset_from_sources("animals", [ctc.src_uri]) + q = DatasetQuery(name="animals", version=1, catalog=catalog).generate(index_tar) + q.save("extracted") + + q = DatasetQuery(name="extracted", catalog=catalog).filter(C.parent.glob("*/cats*")) + assert len(q.results()) == 2 + + ds = q.extract(Object(to_str), "name") + assert set(ds) == {("meow", "cat1"), ("mrow", "cat2")} + + +@pytest.mark.parametrize("cloud_type", ["s3", "azure", "gs"], indirect=True) +@pytest.mark.parametrize("tree", [DEFAULT_TREE | TARRED_TREE], indirect=True) +def test_simple_dataset_query(cloud_test_catalog): + ctc = cloud_test_catalog + catalog = ctc.catalog + metastore = catalog.metastore + warehouse = catalog.warehouse + create_tar_dataset(catalog, ctc.src_uri, "ds1") + DatasetQuery(name="ds1", version=1, catalog=catalog).save("ds2") + + ds_queries = [] + for ds_name in ("ds1", "ds2"): + ds = metastore.get_dataset(ds_name) + dr = warehouse.dataset_rows(ds) + dq = dr.select().order_by(dr.c.parent, dr.c.name) + ds_queries.append(dq) + + ds1, ds2 = ( + [ + {k.name: v for k, v in zip(q.selected_columns, r) if k.name != "id"} + for r in warehouse.db.execute(q) + ] + for q in ds_queries + ) + + # everything except the id field should match + assert ds1 == ds2 + assert [(r["parent"], r["name"]) for r in ds1] == [ + ("", "animals.tar"), + ("", "description"), + ("animals.tar", "description"), + ("animals.tar/cats", "cat1"), + ("animals.tar/cats", "cat2"), + ("animals.tar/dogs", "dog1"), + ("animals.tar/dogs", "dog2"), + ("animals.tar/dogs", "dog3"), + ("animals.tar/dogs/others", "dog4"), + ("cats", "cat1"), + ("cats", "cat2"), + ("dogs", "dog1"), + ("dogs", "dog2"), + ("dogs", "dog3"), + ("dogs/others", "dog4"), + ] + + +@pytest.mark.parametrize("tree", [DEFAULT_TREE | TARRED_TREE], indirect=True) +def test_similarity_search(cloud_test_catalog): + ctc = cloud_test_catalog + catalog = ctc.catalog + create_tar_dataset(catalog, ctc.src_uri, "ds1") + + @udf( + params=(Object(to_str),), + output={"embedding": Array(Float32)}, + method="embedding", + ) + class TextEmbeddingGenerator: + def embedding(self, text): + return (text_embedding(text),) + + target_embedding, source, parent, name = ( + DatasetQuery(name="ds1", catalog=catalog) + .filter(~C.name.glob("*.tar")) + .order_by(C.source, C.parent, C.name) + .limit(1) + .add_signals(TextEmbeddingGenerator()) + .select(C.embedding, C.source, C.parent, C.name) + .results()[0] + ) + q = ( + DatasetQuery(name="ds1", catalog=catalog) + .filter( + ~C.name.glob("*.tar"), + tuple_(C.source, C.parent, C.name) != (source, parent, name), + ) + .add_signals(TextEmbeddingGenerator()) + .mutate( + cos_dist=cosine_distance(C.embedding, target_embedding), + eucl_dist=euclidean_distance(C.embedding, target_embedding), + ) + .select(C.parent, C.name, C.cos_dist, C.eucl_dist) + .order_by(C.source, C.parent, C.name) + ) + count = q.count() + assert count == 13 + + result = q.results() + expected = [ + ("animals.tar", "description", 0.0, 0.0), + ("animals.tar/cats", "cat1", 0.8508677010357059, 1.9078358385397216), + ("animals.tar/cats", "cat2", 0.8508677010357059, 1.9078358385397216), + ("animals.tar/dogs", "dog1", 0.7875133863812602, 1.8750659656122843), + ("animals.tar/dogs", "dog2", 0.7356502722055684, 1.775619888314893), + ("animals.tar/dogs", "dog3", 0.7695916496857775, 1.8344983482620636), + ("animals.tar/dogs/others", "dog4", 0.9789704524691446, 2.0531542018152322), + ("cats", "cat1", 0.8508677010357059, 1.9078358385397216), + ("cats", "cat2", 0.8508677010357059, 1.9078358385397216), + ("dogs", "dog1", 0.7875133863812602, 1.8750659656122843), + ("dogs", "dog2", 0.7356502722055684, 1.775619888314893), + ("dogs", "dog3", 0.7695916496857775, 1.8344983482620636), + ("dogs/others", "dog4", 0.9789704524691446, 2.0531542018152322), + ] + + for (p1, n1, c1, e1), (p2, n2, c2, e2) in zip(result, expected): + assert p1.endswith(p2) + assert n1 == n2 + assert math.isclose(c1, c2, abs_tol=1e-5) + assert math.isclose(e1, e2, abs_tol=1e-5) + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True), ("file", False)], + indirect=True, +) +def test_subtract(cloud_test_catalog): + @udf(("name",), {"name_len": Int}) + def name_len(name): + # A very simple udf. + return (len(name),) + + catalog = cloud_test_catalog.catalog + sources = [str(cloud_test_catalog.src_uri)] + catalog.index(sources) + + src = cloud_test_catalog.src_uri + catalog.create_dataset_from_sources("dogs", [f"{src}/dogs/*"], recursive=True) + catalog.create_dataset_from_sources("cats", [f"{src}/cats/*"], recursive=True) + + dogs = DatasetQuery(name="dogs", version=1, catalog=catalog) + cats = DatasetQuery(name="cats", version=1, catalog=catalog) + + (dogs | cats).save("dogs_cats") + + dogs_cats = DatasetQuery(name="dogs_cats", catalog=catalog) + + # subtracting dataset from dataset + q = dogs_cats.subtract(dogs) + result = q.results(row_factory=from_result_row) + assert sorted(r["name"] for r in result) == ["cat1", "cat2"] + + # subtracting dataset out of index + q = DatasetQuery(f"{src}", catalog=catalog).subtract(cats) + result = q.results(row_factory=from_result_row) + assert sorted(r["name"] for r in result) == [ + "description", + "dog1", + "dog2", + "dog3", + "dog4", + ] + + # subtracting index out of index + q = DatasetQuery(f"{src}", catalog=catalog).subtract( + DatasetQuery(f"{src}/dogs/*", catalog=catalog) + ) + result = q.results(row_factory=from_result_row) + assert sorted(r["name"] for r in result) == ["cat1", "cat2", "description"] + + # subtracting with filter + q = ( + DatasetQuery(f"{src}", catalog=catalog) + .filter(C.name.glob("dog*")) + .subtract(cats) + ) + result = q.results(row_factory=from_result_row) + assert sorted(r["name"] for r in result) == ["dog1", "dog2", "dog3", "dog4"] + + # chain subtracting + q = dogs_cats.subtract(dogs).subtract(cats) + result = q.results() + count = q.count() + assert len(result) == 0 + assert count == 0 + + # filtering after subtract + q = ( + DatasetQuery(f"{src}", catalog=catalog) + .subtract(cats) + .filter(C.name.glob("dog*")) + ) + result = q.results(row_factory=from_result_row) + assert sorted(r["name"] for r in result) == ["dog1", "dog2", "dog3", "dog4"] + + # subtract with usage of udfs and union + # simulates updating dataset with new changes in index and not re-calculating + # all udfs, but only for those that are new + cats.add_signals(name_len).save("cats_with_signals") + cats_with_signals = DatasetQuery(name="cats_with_signals", catalog=catalog) + q = ( + DatasetQuery(f"{src}", catalog=catalog) + .subtract(cats_with_signals) + .add_signals(name_len) + .union(cats_with_signals) + ) + result = q.results(row_factory=from_result_row) + assert sorted(r["name"] for r in result) == sorted( + ["description", "dog1", "dog2", "dog3", "dog4", "cat1", "cat2"] + ) + assert all(r["name_len"] > 0 for r in result) + + # subtracting with source and target filter + # only dog2 file has size less then 4 + all_except_dog2 = DatasetQuery(f"{src}", catalog=catalog).filter(C.size > 3) + only_cats = dogs_cats.filter(C.name.glob("cat*")) + q = all_except_dog2.subtract(only_cats) + result = q.results(row_factory=from_result_row) + assert sorted(r["name"] for r in result) == ["description", "dog1", "dog3", "dog4"] + + # subtracting after union + q = dogs.union(cats).subtract(dogs) + result = q.results(row_factory=from_result_row) + assert sorted(r["name"] for r in result) == ["cat1", "cat2"] + + # subtract with itself + q = dogs.subtract(dogs) + result = q.results() + count = q.count() + assert len(result) == 0 + assert count == 0 + + +def test_aggregate(cloud_test_catalog, dogs_dataset): + catalog = cloud_test_catalog.catalog + + q = DatasetQuery(name=dogs_dataset.name, version=1, catalog=catalog) + assert q.count() == 4 + assert q.sum(C.size) == 15 + assert q.avg(C.size) == 15 / 4 + assert q.min(C.size) == 3 + assert q.max(C.size) == 4 + + +def test_group_by(cloud_test_catalog, cloud_type, dogs_dataset): + catalog = cloud_test_catalog.catalog + + q = ( + DatasetQuery(name=dogs_dataset.name, version=1, catalog=catalog) + .group_by(C.parent) + .select( + C.parent, + functions.count(), + functions.sum(C.size), + functions.avg(C.size), + functions.min(C.size), + functions.max(C.size), + ) + ) + result = q.results() + assert len(result) == 2 + + result_dict = {r[0]: r[1:] for r in result} + if cloud_type == "file": + assert result_dict == { + f"{cloud_test_catalog.partial_path}/dogs": (3, 11, 11 / 3, 3, 4), + f"{cloud_test_catalog.partial_path}/dogs/others": (1, 4, 4, 4, 4), + } + + else: + assert result_dict == { + "dogs": (3, 11, 11 / 3, 3, 4), + "dogs/others": (1, 4, 4, 4, 4), + } + + +@pytest.mark.parametrize("tree", [WEBFORMAT_TREE], indirect=True) +def test_json_loader(cloud_test_catalog): + catalog = cloud_test_catalog.catalog + dialect = catalog.warehouse.db.dialect + + @udf( + params=(C.name,), + output={"basename": String, "ext": String}, + ) + def split_name(name): + return os.path.splitext(name) + + json_output = {"similarity": Float, "md5": String} + + @udf( + params=("ext", LocalFilename("*.json")), + output=json_output, + ) + def attach_json(rows): + # Locate json row and load its data + json_data = None + for ext, file_path in rows: + if ext == ".json" and file_path: + with open(file_path, encoding="utf8") as f: + json_data = json.load(f) + + # Attach json-loaded signals to all other rows in the group + signals = [] + for ext, _ in rows: + if json_data and ext != ".json": + signals.append([json_data.get(k) for k in json_output]) + else: + signals.append(None) + + return signals + + col_default_values = tuple(t.default_value(dialect) for t in json_output.values()) + + expected = [ + ("f1.json", col_default_values[0], col_default_values[1]), + ("f1.raw", 0.001, "deadbeef"), + ("f2.json", col_default_values[0], col_default_values[1]), + ("f2.raw", 0.005, "foobar"), + ] + + q = ( + DatasetQuery(cloud_test_catalog.src_uri, catalog=catalog) + .add_signals(split_name) + .add_signals(attach_json, partition_by=C.basename) + .select(C.name, C.similarity, C.md5) + .order_by(C.name) + ) + assert q.count() == 4 + res = q.results() + assert len(res) == 4 + assert [r[0] for r in res] == [r[0] for r in expected] + assert [r[1] for r in res] == pytest.approx([r[1] for r in expected]) + assert [r[2] for r in res] == [r[2] for r in expected] + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_changed(cloud_test_catalog): + now = datetime.now(timezone.utc) + + @udf(("name",), {"name_len": Int}) + def name_len(name): + # A very simple udf. + return (len(name),) + + def _index(catalog, uri, entries_updated_last_mod): + """ + Custom indexing with setting some of the files to future last modified + to simulate scenario where they are updated in cloud + """ + + catalog.metastore.create_storage_if_not_registered(uri) + entries = [] + + for entry in ENTRIES: + if entry.name in entries_updated_last_mod: + entry.last_modified = now + timedelta(days=2) + else: + entry.last_modified = now + entries.append(entry) + + make_index(catalog, uri, entries) + + catalog = cloud_test_catalog.catalog + src = cloud_test_catalog.src_uri + + # first index + _index(catalog, src, []) + + catalog.create_dataset_from_sources("dogs", [f"{src}/dogs/*"], recursive=True) + catalog.create_dataset_from_sources("cats", [f"{src}/cats/*"], recursive=True) + + dogs = DatasetQuery(name="dogs", version=1, catalog=catalog) + cats = DatasetQuery(name="cats", version=1, catalog=catalog) + + # re-index with simulating dog2 to be updated + _index(catalog, src, ["dog2"]) + + catalog.create_dataset_from_sources( + "dogs_updated_1", [f"{src}/dogs/*"], recursive=True + ) + + # re-index with simulating dog1 and dog2 to be updated + _index(catalog, src, ["dog1", "dog2"]) + + catalog.create_dataset_from_sources( + "dogs_updated_2", [f"{src}/dogs/*"], recursive=True + ) + + dogs_updated_1 = DatasetQuery(name="dogs_updated_1", version=1, catalog=catalog) + dogs_updated_2 = DatasetQuery(name="dogs_updated_2", version=1, catalog=catalog) + + # changed between dataset and dataset + q = dogs_updated_1.changed(dogs) + result = q.results(row_factory=from_result_row) + assert sorted(r["name"] for r in result) == ["dog2"] + + q = dogs_updated_2.changed(dogs) + result = q.results(row_factory=from_result_row) + assert sorted(r["name"] for r in result) == ["dog1", "dog2"] + + # changed between dataset and dataset, no change + q = dogs.changed(dogs) + result = q.results() + count = q.count() + assert len(result) == 0 + assert count == 0 + + # changed between index and dataset + q = DatasetQuery(f"{src}/dogs/*", catalog=catalog).changed(dogs) + result = q.results(row_factory=from_result_row) + assert sorted(r["name"] for r in result) == ["dog1", "dog2"] + + # changed with filters + q = ( + DatasetQuery(f"{src}", catalog=catalog) + .filter(C.name.glob("dog*")) + .changed(dogs) + ) + result = q.results(row_factory=from_result_row) + assert sorted(r["name"] for r in result) == ["dog1", "dog2"] + + # chain changed + q = dogs_updated_2.changed(dogs).changed(dogs_updated_1) + result = q.results(row_factory=from_result_row) + assert sorted(r["name"] for r in result) == ["dog1"] + + # filtering after changed + q = ( + DatasetQuery(f"{src}/dogs/*", catalog=catalog) + .changed(dogs) + .filter(C.name.glob("dog1*")) + ) + result = q.results(row_factory=from_result_row) + assert sorted(r["name"] for r in result) == ["dog1"] + + # changed with usage of udfs + q = ( + DatasetQuery(f"{src}/dogs/*", catalog=catalog) + .changed(dogs) + .add_signals(name_len) + ) + result = q.results(row_factory=from_result_row) + assert sorted(r["name"] for r in result) == sorted(["dog1", "dog2"]) + assert all(r["name_len"] > 0 for r in result) + + # changed after union + q = dogs_updated_2.union(cats).changed(dogs) + result = q.results(row_factory=from_result_row) + assert sorted(r["name"] for r in result) == ["dog1", "dog2"] + + # changed with itself + q = dogs.changed(dogs) + result = q.results() + count = q.count() + assert len(result) == 0 + assert count == 0 + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_to_records(simple_ds_query): + assert simple_ds_query.to_records() == SIMPLE_DS_QUERY_RECORDS + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_to_pandas(simple_ds_query): + import pandas as pd + + df = simple_ds_query.to_pandas() + expected = pd.DataFrame.from_records(SIMPLE_DS_QUERY_RECORDS) + assert (df == expected).all(axis=None) + + +@pytest.mark.parametrize("method", ["to_records", "extract"]) +@pytest.mark.parametrize("save", [True, False]) +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True), ("file", False)], + indirect=True, +) +def test_udf_after_union(cloud_test_catalog, save, method): + catalog = cloud_test_catalog.catalog + sources = [cloud_test_catalog.src_uri] + globs = [s.rstrip("/") + "/*" for s in sources] + catalog.index(sources) + catalog.create_dataset_from_sources("animals", globs, recursive=True) + + @udf(("name",), {"name_len": Int}) + def name_len(name): + # A very simple udf. + return (len(name),) + + ds_cats = DatasetQuery(name="animals", version=1, catalog=catalog).filter( + C.parent.glob("*cats*") + ) + if save: + ds_cats.save("cats") + ds_cats = DatasetQuery(name="cats", version=1, catalog=catalog) + ds_dogs = DatasetQuery(name="animals", version=1, catalog=catalog).filter( + C.parent.glob("*dogs*") + ) + if save: + ds_dogs.save("dogs") + ds_dogs = DatasetQuery(name="dogs", version=1, catalog=catalog) + + if method == "to_records": + + def get_result(query): + result = [(r["name"], r["name_len"]) for r in query.to_records()] + result.sort() + return result + + elif method == "extract": + + def get_result(query): + result = list(query.extract("name", "name_len")) + result.sort() + return result + + q = ds_cats.union(ds_dogs).add_signals(name_len) + result1 = get_result(q) + assert result1 == [ + ("cat1", 4), + ("cat2", 4), + ("dog1", 4), + ("dog2", 4), + ("dog3", 4), + ("dog4", 4), + ] + + result2 = get_result(q.union(q)) + assert result2 == [ + ("cat1", 4), + ("cat1", 4), + ("cat2", 4), + ("cat2", 4), + ("dog1", 4), + ("dog1", 4), + ("dog2", 4), + ("dog2", 4), + ("dog3", 4), + ("dog3", 4), + ("dog4", 4), + ("dog4", 4), + ] + + +@pytest.mark.parametrize("method", ["to_records", "extract"]) +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True), ("file", False)], + indirect=True, +) +def test_udf_after_union_same_rows_with_mutate(cloud_test_catalog, method): + catalog = cloud_test_catalog.catalog + sources = [cloud_test_catalog.src_uri] + globs = [s.rstrip("/") + "/*" for s in sources] + catalog.index(sources) + catalog.create_dataset_from_sources("animals", globs, recursive=True) + + @udf(("name",), {"name_len": Int}) + def name_len(name): + # A very simple udf. + return (len(name),) + + q_base = DatasetQuery(name="animals", version=1, catalog=catalog).filter( + C.parent.glob("*dogs*") + ) + q1 = q_base.mutate(x=sqlalchemy.cast(C.name + "_1", String())) + q2 = q_base.mutate(x=sqlalchemy.cast(C.name + "_2", String())) + + if method == "to_records": + + def get_result(query): + result = [(r["name"], r["x"], r["name_len"]) for r in query.to_records()] + result.sort() + return result + + elif method == "extract": + + def get_result(query): + return sorted(query.extract("name", "x", "name_len")) + + q = q1.union(q2).add_signals(name_len) + result1 = get_result(q) + assert result1 == [ + ("dog1", "dog1_1", 4), + ("dog1", "dog1_2", 4), + ("dog2", "dog2_1", 4), + ("dog2", "dog2_2", 4), + ("dog3", "dog3_1", 4), + ("dog3", "dog3_2", 4), + ("dog4", "dog4_1", 4), + ("dog4", "dog4_2", 4), + ] + + result2 = get_result(q.union(q)) + assert result2 == [ + ("dog1", "dog1_1", 4), + ("dog1", "dog1_1", 4), + ("dog1", "dog1_2", 4), + ("dog1", "dog1_2", 4), + ("dog2", "dog2_1", 4), + ("dog2", "dog2_1", 4), + ("dog2", "dog2_2", 4), + ("dog2", "dog2_2", 4), + ("dog3", "dog3_1", 4), + ("dog3", "dog3_1", 4), + ("dog3", "dog3_2", 4), + ("dog3", "dog3_2", 4), + ("dog4", "dog4_1", 4), + ("dog4", "dog4_1", 4), + ("dog4", "dog4_2", 4), + ("dog4", "dog4_2", 4), + ] + + +@pytest.mark.parametrize("method", ["select", "extract"]) +@pytest.mark.parametrize( + "cloud_type,version_aware,tree", + [("s3", True, NUM_TREE), ("file", False, NUM_TREE)], + indirect=True, +) +def test_udf_after_limit(cloud_test_catalog, method): + catalog = cloud_test_catalog.catalog + sources = [cloud_test_catalog.src_uri] + globs = [s.rstrip("/") + "/*" for s in sources] + catalog.index(sources) + catalog.create_dataset_from_sources("animals", globs, recursive=True) + + @udf(("name",), {"name_int": Int}) + def name_int(name): + try: + return (int(name),) + except ValueError: + return 0 + + if method == "select": + + def get_result(query): + return ( + query.limit(100) + .add_signals(name_int) + .select("name", "name_int") + .to_records() + ) + + elif method == "extract": + + def get_result(query): + data = query.limit(100).add_signals(name_int).extract("name", "name_int") + return [{"name": name, "name_int": name_int} for name, name_int in data] + + expected = [{"name": f"{i:06d}", "name_int": i} for i in range(100)] + ds = DatasetQuery(name="animals", version=1, catalog=catalog) + # We test a few different orderings here, because we've had strange + # bugs in the past where calling add_signals() after limit() gave us + # incorrect results on clickhouse cloud. + # See https://github.com/iterative/dvcx/issues/940 + assert get_result(ds.order_by("name")) == expected + assert len(get_result(ds.order_by("random"))) == 100 + assert len(get_result(ds)) == 100 + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True), ("file", False)], + indirect=True, +) +@pytest.mark.parametrize("indirect", [True, False]) +def test_dataset_dependencies_one_storage_as_dependency( + cloud_test_catalog, listed_bucket, indirect +): + ds_name = uuid.uuid4().hex + catalog = cloud_test_catalog.catalog + storage = catalog.get_storage(cloud_test_catalog.storage_uri) + + path = f"{cloud_test_catalog.src_uri}/cats" + + DatasetQuery(path=path, catalog=catalog).save(ds_name) + + assert [ + dataset_dependency_asdict(d) + for d in catalog.get_dataset_dependencies(ds_name, 1, indirect=indirect) + ] == [ + { + "id": ANY, + "type": DatasetDependencyType.STORAGE, + "name": storage.uri, + "version": storage.timestamp_str, + "created_at": isoparse(storage.timestamp_str), + "dependencies": [], + } + ] + + +@pytest.mark.parametrize("indirect", [True, False]) +def test_dataset_dependencies_one_registered_dataset_as_dependency( + cloud_test_catalog, dogs_dataset, indirect +): + ds_name = uuid.uuid4().hex + catalog = cloud_test_catalog.catalog + storage = catalog.get_storage(cloud_test_catalog.storage_uri) + + DatasetQuery(name=dogs_dataset.name, catalog=catalog).save(ds_name) + + expected = [ + { + "id": ANY, + "type": DatasetDependencyType.DATASET, + "name": dogs_dataset.name, + "version": str(1), + "created_at": dogs_dataset.get_version(1).created_at, + "dependencies": [], + } + ] + + if indirect: + expected[0]["dependencies"] = [ + { + "id": ANY, + "type": DatasetDependencyType.STORAGE, + "name": storage.uri, + "version": storage.timestamp_str, + "created_at": isoparse(storage.timestamp_str), + "dependencies": [], + } + ] + + assert [ + dataset_dependency_asdict(d) + for d in catalog.get_dataset_dependencies(ds_name, 1, indirect=indirect) + ] == expected + + catalog.remove_dataset(dogs_dataset.name, force=True) + # None means dependency was there but was removed in the meantime + assert catalog.get_dataset_dependencies(ds_name, 1) == [None] + + +@pytest.mark.parametrize("method", ["union", "join"]) +def test_dataset_dependencies_multiple_direct_dataset_dependencies( + cloud_test_catalog, dogs_dataset, cats_dataset, method +): + # multiple direct dataset dependencies can be achieved with methods that are + # combining multiple DatasetQuery instances into new one like union or join + ds_name = uuid.uuid4().hex + catalog = cloud_test_catalog.catalog + storage = catalog.get_storage(cloud_test_catalog.storage_uri) + + dogs = DatasetQuery(name=dogs_dataset.name, version=1, catalog=catalog) + cats = DatasetQuery(name=cats_dataset.name, version=1, catalog=catalog) + + if method == "union": + dogs.union(cats).save(ds_name) + else: + dogs.join(cats, "name").save(ds_name) + + storage_depenedncy = { + "id": ANY, + "type": DatasetDependencyType.STORAGE, + "name": storage.uri, + "version": storage.timestamp_str, + "created_at": isoparse(storage.timestamp_str), + "dependencies": [], + } + + expected = [ + { + "id": ANY, + "type": DatasetDependencyType.DATASET, + "name": dogs_dataset.name, + "version": str(1), + "created_at": dogs_dataset.get_version(1).created_at, + "dependencies": [storage_depenedncy], + }, + { + "id": ANY, + "type": DatasetDependencyType.DATASET, + "name": cats_dataset.name, + "version": str(1), + "created_at": cats_dataset.get_version(1).created_at, + "dependencies": [storage_depenedncy], + }, + ] + + assert sorted( + ( + dataset_dependency_asdict(d) + for d in catalog.get_dataset_dependencies(ds_name, 1, indirect=True) + ), + key=lambda d: d["name"], + ) == sorted(expected, key=lambda d: d["name"]) + + # check when removing one dependency + catalog.remove_dataset(dogs_dataset.name, force=True) + expected[0] = None + expected[1]["dependencies"] = [] + + assert sorted( + ( + dataset_dependency_asdict(d) + for d in catalog.get_dataset_dependencies(ds_name, 1) + ), + key=lambda d: d["name"] if d else "", + ) == sorted(expected, key=lambda d: d["name"] if d else "") + + # check when removing the other dependency + catalog.remove_dataset(cats_dataset.name, force=True) + assert catalog.get_dataset_dependencies(ds_name, 1) == [None, None] + + +def test_dataset_dependencies_multiple_union( + cloud_test_catalog, dogs_dataset, cats_dataset +): + ds_name = uuid.uuid4().hex + catalog = cloud_test_catalog.catalog + storage = catalog.get_storage(cloud_test_catalog.storage_uri) + + dogs = DatasetQuery(name=dogs_dataset.name, version=1, catalog=catalog) + cats = DatasetQuery(name=cats_dataset.name, version=1, catalog=catalog) + dogs_other = DatasetQuery(name=dogs_dataset.name, version=1, catalog=catalog) + + dogs.union(cats).union(dogs_other).save(ds_name) + + storage_depenedncy = { + "id": ANY, + "type": DatasetDependencyType.STORAGE, + "name": storage.uri, + "version": storage.timestamp_str, + "created_at": isoparse(storage.timestamp_str), + "dependencies": [], + } + + expected = [ + { + "id": ANY, + "type": DatasetDependencyType.DATASET, + "name": dogs_dataset.name, + "version": str(1), + "created_at": dogs_dataset.get_version(1).created_at, + "dependencies": [storage_depenedncy], + }, + { + "id": ANY, + "type": DatasetDependencyType.DATASET, + "name": cats_dataset.name, + "version": str(1), + "created_at": cats_dataset.get_version(1).created_at, + "dependencies": [storage_depenedncy], + }, + ] + + assert sorted( + ( + dataset_dependency_asdict(d) + for d in catalog.get_dataset_dependencies(ds_name, 1, indirect=True) + ), + key=lambda d: d["name"], + ) == sorted(expected, key=lambda d: d["name"]) + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_save_subset_of_columns(cloud_test_catalog): + catalog = cloud_test_catalog.catalog + path = f"{cloud_test_catalog.src_uri}/cats" + DatasetQuery(path=path, catalog=catalog).select(C.name).save("cats", version=1) + + dataset = catalog.get_dataset("cats") + assert dataset.schema == {"name": String} + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_single_file(cloud_test_catalog): + catalog = cloud_test_catalog.catalog + path = f"{cloud_test_catalog.src_uri}/cats/cat1" + ds = DatasetQuery(path=path, catalog=catalog) + assert ds.count() == 1 + + +def test_recursive(cloud_test_catalog): + catalog = cloud_test_catalog.catalog + ds = DatasetQuery(path=cloud_test_catalog.src_uri, catalog=catalog, recursive=False) + assert ds.count() == 1 diff --git a/tests/func/test_datasets.py b/tests/func/test_datasets.py new file mode 100644 index 000000000..11e78af80 --- /dev/null +++ b/tests/func/test_datasets.py @@ -0,0 +1,945 @@ +import uuid +from json import dumps +from unittest.mock import ANY + +import pytest +import sqlalchemy as sa +from dateutil.parser import isoparse + +from datachain.catalog.catalog import DATASET_INTERNAL_ERROR_MESSAGE +from datachain.data_storage.sqlite import SQLiteWarehouse +from datachain.dataset import DatasetDependencyType, DatasetStatus +from datachain.error import DatasetInvalidVersionError, DatasetNotFoundError +from datachain.query import DatasetQuery, udf +from datachain.query.schema import DatasetRow +from datachain.sql.types import ( + JSON, + Array, + Binary, + Boolean, + Float, + Float32, + Float64, + Int, + Int32, + Int64, + String, +) +from tests.utils import assert_row_names, dataset_dependency_asdict + + +def add_column(engine, table_name, column, catalog): + # Simple method that adds new column to a table, with default value if specified + column_name = column.compile(dialect=engine.dialect) + column_type = column.type.compile(engine.dialect) + query_str = f"ALTER TABLE {table_name} ADD COLUMN {column_name} {column_type}" + if column.default: + query_str += f" DEFAULT {column.default.arg}" + catalog.warehouse.db.execute_str(query_str) + + +@pytest.mark.parametrize("create_rows", [True, False]) +def test_create_dataset_no_version_specified(cloud_test_catalog, create_rows): + catalog = cloud_test_catalog.catalog + + name = uuid.uuid4().hex + dataset = catalog.create_dataset( + name, + query_script="script", + columns=[sa.Column("similarity", Float32)], + create_rows=create_rows, + ) + + assert dataset.versions_values == [1] + + dataset_version = dataset.get_version(1) + + assert dataset.name == name + assert dataset.query_script == "script" + assert dataset_version.query_script == "script" + assert dataset.schema["similarity"] == Float32 + assert dataset_version.schema["similarity"] == Float32 + assert dataset_version.status == DatasetStatus.PENDING + assert dataset.status == DatasetStatus.CREATED # dataset status is deprecated + if create_rows: + assert dataset_version.num_objects == 0 + else: + assert dataset_version.num_objects is None + + +@pytest.mark.parametrize("create_rows", [True, False]) +def test_create_dataset_with_explicit_version(cloud_test_catalog, create_rows): + catalog = cloud_test_catalog.catalog + + name = uuid.uuid4().hex + dataset = catalog.create_dataset( + name, + version=1, + query_script="script", + columns=[sa.Column("similarity", Float32)], + create_rows=create_rows, + ) + + assert dataset.versions_values == [1] + + dataset_version = dataset.get_version(1) + + assert dataset.name == name + assert dataset.query_script == "script" + assert dataset_version.query_script == "script" + assert dataset.schema["similarity"] == Float32 + assert dataset_version.schema["similarity"] == Float32 + assert dataset_version.status == DatasetStatus.PENDING + assert dataset.status == DatasetStatus.CREATED + if create_rows: + assert dataset_version.num_objects == 0 + else: + assert dataset_version.num_objects is None + + +@pytest.mark.parametrize("create_rows", [True, False]) +def test_create_dataset_already_exist(cloud_test_catalog, dogs_dataset, create_rows): + catalog = cloud_test_catalog.catalog + + dataset = catalog.create_dataset( + dogs_dataset.name, + query_script="script", + columns=[sa.Column("similarity", Float32)], + create_rows=create_rows, + ) + + assert dataset.versions_values == [1, 2] + + dataset_version = dataset.get_version(2) + + assert dataset.name == dogs_dataset.name + assert dataset_version.query_script == "script" + assert dataset_version.schema["similarity"] == Float32 + assert dataset_version.status == DatasetStatus.PENDING + assert dataset.status == DatasetStatus.COMPLETE + if create_rows: + assert dataset_version.num_objects == 0 + else: + assert dataset_version.num_objects is None + + +@pytest.mark.parametrize("create_rows", [True, False]) +def test_create_dataset_already_exist_wrong_version( + cloud_test_catalog, dogs_dataset, create_rows +): + catalog = cloud_test_catalog.catalog + + with pytest.raises(DatasetInvalidVersionError) as exc_info: + catalog.create_dataset( + dogs_dataset.name, + version=1, + columns=[sa.Column(name, typ) for name, typ in dogs_dataset.schema.items()], + create_rows=create_rows, + ) + assert str(exc_info.value) == ( + f"Version 1 already exists in dataset {dogs_dataset.name}" + ) + + +def test_get_dataset(cloud_test_catalog, dogs_dataset): + catalog = cloud_test_catalog.catalog + + dataset = catalog.get_dataset(dogs_dataset.name) + assert dataset.name == dogs_dataset.name + + with pytest.raises(DatasetNotFoundError): + catalog.get_dataset("wrong name") + + +# Returns None if the table does not exist +def get_table_row_count(db, table_name): + if not db.has_table(table_name): + return None + query = sa.select(sa.func.count()).select_from(sa.table(table_name)) + return next(db.execute(query), (None,))[0] + + +def test_create_dataset_from_sources(listed_bucket, cloud_test_catalog): + dataset_name = uuid.uuid4().hex + src_uri = cloud_test_catalog.src_uri + catalog = cloud_test_catalog.catalog + + dataset = catalog.create_dataset_from_sources( + dataset_name, [f"{src_uri}/dogs/*"], recursive=True + ) + + dataset_version = dataset.get_version(dataset.latest_version) + + assert dataset.name == dataset_name + assert dataset.description is None + assert dataset.versions_values == [1] + assert dataset.labels == [] + assert dataset.status == DatasetStatus.COMPLETE + + assert dataset_version.status == DatasetStatus.COMPLETE + assert dataset_version.created_at + assert dataset_version.finished_at + assert dataset_version.error_message == "" + assert dataset_version.error_stack == "" + assert dataset_version.script_output == "" + assert dataset_version.sources == f"{src_uri}/dogs/*" + + dr = catalog.warehouse.schema.dataset_row_cls + sys_schema = {c.name: type(c.type) for c in dr.sys_columns()} + default_dataset_schema = DatasetRow.schema | sys_schema + assert dataset.schema == default_dataset_schema + assert dataset.query_script == "" + + assert dataset_version.schema == default_dataset_schema + assert dataset_version.query_script == "" + assert dataset_version.num_objects + assert dataset_version.preview + + +def test_create_dataset_from_sources_empty_sources(cloud_test_catalog): + dataset_name = uuid.uuid4().hex + catalog = cloud_test_catalog.catalog + + with pytest.raises(ValueError) as exc_info: + catalog.create_dataset_from_sources(dataset_name, [], recursive=True) + + assert str(exc_info.value) == "Sources needs to be non empty list" + + +def test_create_dataset_from_sources_failed(listed_bucket, cloud_test_catalog, mocker): + dataset_name = uuid.uuid4().hex + src_uri = cloud_test_catalog.src_uri + catalog = cloud_test_catalog.catalog + with mocker.patch.object( + catalog.warehouse.__class__, + "create_dataset_rows_table", + side_effect=RuntimeError("Error"), + ) as _: + with pytest.raises(RuntimeError): + catalog.create_dataset_from_sources( + dataset_name, [f"{src_uri}/dogs/*"], recursive=True + ) + + dataset = catalog.get_dataset(dataset_name) + dataset_version = dataset.get_version(dataset.latest_version) + + assert dataset.name == dataset_name + assert dataset.status == DatasetStatus.FAILED + assert dataset.versions_values == [1] + assert dataset.created_at + assert dataset.finished_at + assert dataset.error_message == DATASET_INTERNAL_ERROR_MESSAGE + assert dataset.error_stack + assert dataset.query_script == "" + + assert dataset_version.status == DatasetStatus.FAILED + assert dataset_version.created_at + assert dataset_version.finished_at + assert dataset_version.error_message == DATASET_INTERNAL_ERROR_MESSAGE + assert dataset_version.error_stack + assert dataset_version.sources == f"{src_uri}/dogs/*" + assert dataset_version.num_objects is None + assert dataset_version.size is None + assert dataset_version.preview is None + + +def test_create_dataset_whole_bucket(listed_bucket, cloud_test_catalog): + dataset_name_1 = uuid.uuid4().hex + dataset_name_2 = uuid.uuid4().hex + src_uri = cloud_test_catalog.src_uri + catalog = cloud_test_catalog.catalog + + ds1 = catalog.create_dataset_from_sources( + dataset_name_1, [f"{src_uri}"], recursive=True + ) + ds2 = catalog.create_dataset_from_sources( + dataset_name_2, [f"{src_uri}/"], recursive=True + ) + + expected_rows = { + "description", + "cat1", + "cat2", + "dog1", + "dog2", + "dog3", + "dog4", + } + + assert_row_names(catalog, ds1, ds1.latest_version, expected_rows) + + assert_row_names(catalog, ds2, ds2.latest_version, expected_rows) + + +@pytest.mark.parametrize("target_version", [None, 3]) +def test_registering_dataset( + cloud_test_catalog, dogs_dataset, cats_dataset, target_version +): + catalog = cloud_test_catalog.catalog + + # make sure there is a custom columns inside, other than default ones + catalog.metastore.update_dataset_version( + dogs_dataset, + 1, + sources="s3://ldb-public", + query_script="DatasetQuery()", + error_message="no error", + error_stack="no error stack", + script_output="log", + ) + + dogs_version = dogs_dataset.get_version(1) + + dataset = catalog.register_dataset( + dogs_dataset, + 1, + cats_dataset, + target_version=target_version, + ) + + # if not provided, it will end up being next dataset version + target_version = target_version or cats_dataset.next_version + + assert dataset.name == cats_dataset.name + assert dataset.status == DatasetStatus.COMPLETE + assert dataset.versions_values == [1, target_version] + + version1 = dataset.get_version(1) + assert version1.status == DatasetStatus.COMPLETE + + version2 = dataset.get_version(target_version) + assert version2.status == DatasetStatus.COMPLETE + assert version2.sources == "s3://ldb-public" + assert version2.query_script == "DatasetQuery()" + assert version2.error_message == "no error" + assert version2.error_stack == "no error stack" + assert version2.script_output == "log" + assert version2.schema == dogs_version.schema + assert version2.created_at == dogs_version.created_at + assert version2.finished_at == dogs_version.finished_at + assert dogs_version.num_objects + assert version2.num_objects == dogs_version.num_objects + assert dogs_version.size + assert version2.size == dogs_version.size + + assert_row_names( + catalog, + dataset, + 1, + { + "cat1", + "cat2", + }, + ) + + assert_row_names( + catalog, + dataset, + target_version, + { + "dog1", + "dog2", + "dog3", + "dog4", + }, + ) + + with pytest.raises(DatasetNotFoundError): + # since it had only one version, it should be completely removed + catalog.get_dataset(dogs_dataset.name) + + +@pytest.mark.parametrize("target_version", [None, 3]) +def test_registering_dataset_source_dataset_with_multiple_versions( + cloud_test_catalog, dogs_dataset, cats_dataset, target_version +): + catalog = cloud_test_catalog.catalog + + # creating one more version for dogs, not to end up completely removed + columns = tuple(sa.Column(name, typ) for name, typ in dogs_dataset.schema.items()) + dogs_dataset = catalog.create_new_dataset_version(dogs_dataset, 2, columns=columns) + dataset = catalog.register_dataset( + dogs_dataset, + 1, + cats_dataset, + target_version=target_version, + ) + + # if not provided, it will end up being next dataset version + target_version = target_version or cats_dataset.next_version + + assert dataset.name == cats_dataset.name + assert dataset.versions_values == [1, target_version] + + assert_row_names( + catalog, + dataset, + 1, + { + "cat1", + "cat2", + }, + ) + + assert_row_names( + catalog, + dataset, + target_version, + { + "dog1", + "dog2", + "dog3", + "dog4", + }, + ) + + # check if dogs dataset is still present as it has one more version + dogs_dataset = catalog.get_dataset(dogs_dataset.name) + assert dogs_dataset.versions_values == [2] + dataset_version = dogs_dataset.get_version(2) + assert dataset_version.num_objects == 0 + + +@pytest.mark.parametrize("target_version", [None, 3]) +def test_registering_dataset_with_new_version_of_itself( + cloud_test_catalog, cats_dataset, target_version +): + catalog = cloud_test_catalog.catalog + + dataset = catalog.register_dataset( + cats_dataset, + 1, + cats_dataset, + target_version=target_version, + ) + + # if not provided, it will end up being next dataset version + target_version = target_version or cats_dataset.next_version + + assert dataset.name == cats_dataset.name + assert dataset.versions_values == [target_version] + + assert_row_names(catalog, dataset, target_version, {"cat1", "cat2"}) + + +def test_registering_dataset_invalid_target_version( + cloud_test_catalog, cats_dataset, dogs_dataset +): + catalog = cloud_test_catalog.catalog + + with pytest.raises(DatasetInvalidVersionError) as exc_info: + catalog.register_dataset( + dogs_dataset, + 1, + cats_dataset, + target_version=1, + ) + assert str(exc_info.value) == "Version 1 must be higher than the current latest one" + + +def test_registering_dataset_invalid_source_version( + cloud_test_catalog, cats_dataset, dogs_dataset +): + catalog = cloud_test_catalog.catalog + + with pytest.raises(ValueError) as exc_info: + catalog.register_dataset( + dogs_dataset, + 5, + cats_dataset, + target_version=2, + ) + assert str(exc_info.value) == f"Dataset {dogs_dataset.name} does not have version 5" + + +def test_registering_dataset_source_version_in_non_final_status( + cloud_test_catalog, cats_dataset, dogs_dataset +): + catalog = cloud_test_catalog.catalog + catalog.metastore.update_dataset_version( + dogs_dataset, + 1, + status=DatasetStatus.PENDING, + ) + + with pytest.raises(ValueError) as exc_info: + catalog.register_dataset( + dogs_dataset, + 1, + cats_dataset, + target_version=2, + ) + assert str(exc_info.value) == "Cannot register dataset version in non final status" + + +def test_remove_dataset(cloud_test_catalog, dogs_dataset): + catalog = cloud_test_catalog.catalog + + dataset_version = dogs_dataset.get_version(1) + assert dataset_version.num_objects + + catalog.remove_dataset(dogs_dataset.name, force=True) + with pytest.raises(DatasetNotFoundError): + catalog.get_dataset(dogs_dataset.name) + + dataset_table_name = catalog.warehouse.dataset_table_name(dogs_dataset.name, 1) + assert get_table_row_count(catalog.warehouse.db, dataset_table_name) is None + + assert catalog.metastore.get_direct_dataset_dependencies(dogs_dataset, 1) == [] + + +def test_remove_dataset_with_multiple_versions(cloud_test_catalog, dogs_dataset): + catalog = cloud_test_catalog.catalog + + columns = tuple(sa.Column(name, typ) for name, typ in dogs_dataset.schema.items()) + updated_dogs_dataset = catalog.create_new_dataset_version( + dogs_dataset, 2, columns=columns + ) + assert updated_dogs_dataset.has_version(2) + assert updated_dogs_dataset.has_version(1) + + catalog.remove_dataset(updated_dogs_dataset.name, force=True) + with pytest.raises(DatasetNotFoundError): + catalog.get_dataset(updated_dogs_dataset.name) + + assert ( + catalog.metastore.get_direct_dataset_dependencies(updated_dogs_dataset, 1) == [] + ) + + +def test_remove_dataset_dataset_not_found(cloud_test_catalog): + catalog = cloud_test_catalog.catalog + + with pytest.raises(DatasetNotFoundError): + catalog.remove_dataset("wrong_name", force=True) + + +def test_remove_dataset_wrong_version(cloud_test_catalog, dogs_dataset): + catalog = cloud_test_catalog.catalog + + with pytest.raises(DatasetInvalidVersionError): + catalog.remove_dataset(dogs_dataset.name, version=100) + + +def test_edit_dataset(cloud_test_catalog, dogs_dataset): + dataset_old_name = dogs_dataset.name + dataset_new_name = uuid.uuid4().hex + catalog = cloud_test_catalog.catalog + + catalog.edit_dataset( + dogs_dataset.name, + new_name=dataset_new_name, + description="new description", + labels=["cats", "birds"], + ) + + dataset = catalog.get_dataset(dataset_new_name) + assert dataset.versions_values == [1] + assert dataset.name == dataset_new_name + assert dataset.description == "new description" + assert dataset.labels == ["cats", "birds"] + + # check if dataset tables are renamed correctly + old_dataset_table_name = catalog.warehouse.dataset_table_name(dataset_old_name, 1) + new_dataset_table_name = catalog.warehouse.dataset_table_name(dataset_new_name, 1) + assert get_table_row_count(catalog.warehouse.db, old_dataset_table_name) is None + expected_table_row_count = get_table_row_count( + catalog.warehouse.db, new_dataset_table_name + ) + assert expected_table_row_count + assert dataset.get_version(1).num_objects == expected_table_row_count + + +def test_edit_dataset_same_name(cloud_test_catalog, dogs_dataset): + dataset_old_name = dogs_dataset.name + dataset_new_name = dogs_dataset.name + catalog = cloud_test_catalog.catalog + + catalog.edit_dataset(dogs_dataset.name, new_name=dataset_new_name) + + dataset = catalog.get_dataset(dataset_new_name) + assert dataset.name == dataset_new_name + + # check if dataset tables are renamed correctly + old_dataset_table_name = catalog.warehouse.dataset_table_name(dataset_old_name, 1) + new_dataset_table_name = catalog.warehouse.dataset_table_name(dataset_new_name, 1) + expected_table_row_count = get_table_row_count( + catalog.warehouse.db, old_dataset_table_name + ) + assert expected_table_row_count + assert dataset.get_version(1).num_objects == expected_table_row_count + assert expected_table_row_count == get_table_row_count( + catalog.warehouse.db, new_dataset_table_name + ) + + +def test_edit_dataset_remove_labels_and_description(cloud_test_catalog, dogs_dataset): + dataset_new_name = uuid.uuid4().hex + catalog = cloud_test_catalog.catalog + + catalog.edit_dataset( + dogs_dataset.name, + new_name=dataset_new_name, + description="", + labels=[], + ) + + dataset = catalog.get_dataset(dataset_new_name) + assert dataset.versions_values == [1] + assert dataset.name == dataset_new_name + assert dataset.description == "" + assert dataset.labels == [] + + +def test_ls_dataset_rows(cloud_test_catalog, dogs_dataset): + catalog = cloud_test_catalog.catalog + + assert {r["name"] for r in catalog.ls_dataset_rows(dogs_dataset.name, 1)} == { + "dog1", + "dog2", + "dog3", + "dog4", + } + + +def test_ls_dataset_rows_with_limit_offset(cloud_test_catalog, dogs_dataset): + catalog = cloud_test_catalog.catalog + + # these should be sorted by id already + all_rows = list( + catalog.ls_dataset_rows( + dogs_dataset.name, + 1, + ) + ) + + assert { + r["name"] + for r in catalog.ls_dataset_rows( + dogs_dataset.name, + 1, + offset=2, + limit=1, + ) + } == { + all_rows[2]["name"], + } + + +def test_ls_dataset_rows_with_custom_columns(cloud_test_catalog, dogs_dataset): + catalog = cloud_test_catalog.catalog + int_example = 25 + + @udf( + (), + { + "int_col": Int, + "int_col_32": Int32, + "int_col_64": Int64, + "float_col": Float, + "float_col_32": Float32, + "float_col_64": Float64, + "array_col": Array(Float), + "array_col_nested": Array(Array(Float)), + "array_col_32": Array(Float32), + "array_col_64": Array(Float64), + "string_col": String, + "bool_col": Boolean, + "json_col": JSON, + "binary_col": Binary, + }, + ) + def test_types(): + return ( + 5, + 5, + 5, + 0.5, + 0.5, + 0.5, + [0.5], + [[0.5], [0.5]], + [0.5], + [0.5], + "s", + True, + dumps({"a": 1}), + int_example.to_bytes(2, "big"), + ) + + ( + DatasetQuery(name=dogs_dataset.name, catalog=catalog) + .add_signals(test_types) + .save("dogs_custom_columns") + ) + + for r in catalog.ls_dataset_rows("dogs_custom_columns", 1): + assert r["int_col"] == 5 + assert r["int_col_32"] == 5 + assert r["int_col_64"] == 5 + assert r["float_col"] == 0.5 + assert r["float_col_32"] == 0.5 + assert r["float_col_64"] == 0.5 + assert r["array_col"] == [0.5] + assert r["array_col_nested"] == [[0.5], [0.5]] + assert r["array_col_32"] == [0.5] + assert r["array_col_64"] == [0.5] + assert r["string_col"] == "s" + assert r["bool_col"] + assert r["json_col"] == dumps({"a": 1}) + assert r["binary_col"] == int_example.to_bytes(2, "big") + + +def test_dataset_preview_custom_columns(cloud_test_catalog, dogs_dataset): + catalog = cloud_test_catalog.catalog + int_example = 25 + + @udf( + (), + { + "int_col": Int, + "int_col_32": Int32, + "int_col_64": Int64, + "float_col": Float, + "float_col_32": Float32, + "float_col_64": Float64, + "array_col": Array(Float), + "array_col_nested": Array(Array(Float)), + "array_col_32": Array(Float32), + "array_col_64": Array(Float64), + "string_col": String, + "bool_col": Boolean, + "json_col": JSON, + "binary_col": Binary, + }, + ) + def test_types(): + return ( + 5, + 5, + 5, + 0.5, + 0.5, + 0.5, + [0.5], + [[0.5], [0.5]], + [0.5], + [0.5], + "s", + True, + dumps({"a": 1}), + int_example.to_bytes(2, "big"), + ) + + ( + DatasetQuery(name=dogs_dataset.name, catalog=catalog) + .add_signals(test_types) + .save("dogs_custom_columns") + ) + + for r in catalog.get_dataset("dogs_custom_columns").get_version(1).preview: + assert r["int_col"] == 5 + assert r["int_col_32"] == 5 + assert r["int_col_64"] == 5 + assert r["float_col"] == 0.5 + assert r["float_col_32"] == 0.5 + assert r["float_col_64"] == 0.5 + assert r["array_col"] == [0.5] + assert r["array_col_nested"] == [[0.5], [0.5]] + assert r["array_col_32"] == [0.5] + assert r["array_col_64"] == [0.5] + assert r["string_col"] == "s" + assert r["bool_col"] + assert r["json_col"] == '{"a": 1}' + assert r["binary_col"] == [0, 25] + + +def test_dataset_preview_last_modified(cloud_test_catalog, dogs_dataset): + catalog = cloud_test_catalog.catalog + + DatasetQuery(name=dogs_dataset.name, catalog=catalog).save("dogs_custom_columns") + + for r in catalog.get_dataset("dogs_custom_columns").get_version(1).preview: + assert isinstance(r.get("last_modified"), str) + + +def test_merge_datasets(cloud_test_catalog, dogs_dataset, cats_dataset): + catalog = cloud_test_catalog.catalog + catalog.merge_datasets( + cats_dataset, + dogs_dataset, + 1, + dst_version=2, + ) + + dogs_dataset = catalog.get_dataset(dogs_dataset.name) + assert dogs_dataset.versions_values == [1, 2] + + assert {r["name"] for r in catalog.ls_dataset_rows(dogs_dataset.name, 2)} == { + "dog1", + "dog2", + "dog3", + "dog4", + "cat1", + "cat2", + } + + cats_dep = catalog.get_dataset_dependencies(cats_dataset.name, 1) + dogs_old_dep = catalog.get_dataset_dependencies(dogs_dataset.name, 1) + dogs_new_dep = catalog.get_dataset_dependencies(dogs_dataset.name, 2) + + assert set(dogs_new_dep) == set(cats_dep + dogs_old_dep) + + +def test_merge_datasets_invalid_version(cloud_test_catalog, dogs_dataset, cats_dataset): + catalog = cloud_test_catalog.catalog + with pytest.raises(DatasetInvalidVersionError) as exc_info: + catalog.merge_datasets( + cats_dataset, + dogs_dataset, + 1, + dst_version=1, + ) + assert str(exc_info.value) == "Version 1 must be higher than the current latest one" + + +def test_merge_datasets_existing_pending_version( + cloud_test_catalog, dogs_dataset, cats_dataset +): + catalog = cloud_test_catalog.catalog + + # adding custom column + @udf( + (), + {"similarity": Float32}, + ) + def test_types(): + return (0.5,) + + ( + DatasetQuery(name=cats_dataset.name, catalog=catalog) + .add_signals(test_types) + .save("cats_custom_column") + ) + + cats_custom_column = catalog.get_dataset("cats_custom_column") + columns = tuple(sa.Column(name, typ) for name, typ in dogs_dataset.schema.items()) + dogs_dataset = catalog.create_new_dataset_version( + dogs_dataset, + 2, + columns=columns, + create_rows_table=False, + ) + + catalog.merge_datasets( + cats_custom_column, + dogs_dataset, + 1, + dst_version=2, + ) + + dogs_dataset = catalog.get_dataset(dogs_dataset.name) + assert dogs_dataset.versions_values == [1, 2] + + assert ( + dogs_dataset.get_version(2).schema == cats_custom_column.get_version(1).schema + ) + + assert "similarity" in dogs_dataset.get_version(2).schema + + assert {r["name"] for r in catalog.ls_dataset_rows(dogs_dataset.name, 2)} == { + "cat1", + "cat2", + } + + cats_dep = catalog.get_dataset_dependencies(cats_custom_column.name, 1) + dogs_dep = catalog.get_dataset_dependencies(dogs_dataset.name, 2) + + assert set(dogs_dep) == set(cats_dep) + + +def test_merge_datasets_size(cloud_test_catalog, dogs_dataset, cats_dataset): + catalog = cloud_test_catalog.catalog + catalog.merge_datasets( + cats_dataset, + dogs_dataset, + 1, + dst_version=2, + ) + + dogs_dataset = catalog.get_dataset(dogs_dataset.name) + assert dogs_dataset.versions_values == [1, 2] + + dogs_version_1 = dogs_dataset.get_version(1) + dogs_version_2 = dogs_dataset.get_version(2) + cats_version_1 = cats_dataset.get_version(1) + + assert dogs_version_2.size + assert dogs_version_2.size == dogs_version_1.size + cats_version_1.size + + assert dogs_version_2.num_objects + assert ( + dogs_version_2.num_objects + == dogs_version_1.num_objects + cats_version_1.num_objects + ) + + +@pytest.mark.parametrize("tree", [{str(i): str(i) for i in range(50)}], indirect=True) +def test_row_random(cloud_test_catalog): + # Note: this is technically a probabilistic test, but the probability + # of accidental failure is < 1e-10 + ctc = cloud_test_catalog + catalog = ctc.catalog + catalog.index([ctc.src_uri]) + catalog.create_dataset_from_sources("test", [ctc.src_uri]) + random_values = [row["random"] for row in catalog.ls_dataset_rows("test", 1)] + + # Random values are unique + assert len(set(random_values)) == len(random_values) + + if isinstance(catalog.warehouse, SQLiteWarehouse): + RAND_MAX = 2**63 # noqa: N806 + else: + RAND_MAX = 2**64 # noqa: N806 + + # Values are drawn uniformly from range(2**63) + assert 0 <= min(random_values) < 0.4 * RAND_MAX + assert 0.6 * RAND_MAX < max(random_values) < RAND_MAX + + # Creating a new dataset preserves random values + catalog.create_dataset_from_sources("test2", [ctc.src_uri]) + random_values2 = {row["random"] for row in catalog.ls_dataset_rows("test2", 1)} + assert random_values2 == set(random_values) + + +def test_dataset_stats_registered_ds(cloud_test_catalog, dogs_dataset): + catalog = cloud_test_catalog.catalog + stats = catalog.dataset_stats(dogs_dataset.name, 1) + assert stats.num_objects == 4 + assert stats.size == 15 + rows_count = catalog.warehouse.dataset_rows_count(dogs_dataset, 1) + assert rows_count == 4 + + +@pytest.mark.parametrize("indirect", [True, False]) +def test_dataset_dependencies_registered( + listed_bucket, cloud_test_catalog, dogs_dataset, indirect +): + catalog = cloud_test_catalog.catalog + storage = catalog.get_storage(cloud_test_catalog.storage_uri) + + assert [ + dataset_dependency_asdict(d) + for d in catalog.get_dataset_dependencies( + dogs_dataset.name, 1, indirect=indirect + ) + ] == [ + { + "id": ANY, + "type": DatasetDependencyType.STORAGE, + "name": storage.uri, + "version": storage.timestamp_str, + "created_at": isoparse(storage.timestamp_str), + "dependencies": [], + } + ] diff --git a/tests/func/test_ls.py b/tests/func/test_ls.py new file mode 100644 index 000000000..175c39404 --- /dev/null +++ b/tests/func/test_ls.py @@ -0,0 +1,384 @@ +import re +from datetime import datetime +from struct import pack +from time import sleep +from typing import Any + +import msgpack +import pytest +from sqlalchemy import select + +from datachain.cli import ls +from datachain.client.local import FileClient +from tests.utils import uppercase_scheme + + +def same_lines(lines1, lines2): + def _split_lines(lines): + return [line.strip() for line in sorted(lines.split("\n"))] + + return _split_lines(lines1) == _split_lines(lines2) + + +def test_ls_no_args(cloud_test_catalog, cloud_type, capsys): + src = cloud_test_catalog.src_uri + catalog = cloud_test_catalog.catalog + catalog.index([src]) + ls([], catalog=catalog) + captured = capsys.readouterr() + if cloud_type == "file": + assert captured.out == FileClient.root_path().as_uri() + "\n" + else: + assert captured.out == f"{src}\n" + + +def test_ls_root(cloud_test_catalog, cloud_type, capsys): + src = cloud_test_catalog.src_uri + root = src[: src.find("://") + 3] + src_name = src.replace(root, "", 1) + ls([root], catalog=cloud_test_catalog.catalog) + captured = capsys.readouterr() + if cloud_type == "file": + assert not captured.out + else: + buckets = captured.out.split("\n") + assert src_name in buckets + + +def ls_sources_output(src, cloud_type): + if cloud_type == "file": + root_uri = FileClient.root_path().as_uri() + prefix = src[len(root_uri) :] + return f"""\ +{prefix}: +cats/ +description +dogs/ + +{prefix}/dogs: +dog1 +dog2 +dog3 + +{prefix}/dogs/others: +dog4 + """ + + return """\ +cats/ +dogs/ +description + +dogs/others: +dog4 + +dogs: +dog1 +dog2 +dog3 + """ + + +def test_ls_sources(cloud_test_catalog, cloud_type, capsys): + src = cloud_test_catalog.src_uri + ls([src, f"{src}/dogs/*"], catalog=cloud_test_catalog.catalog) + captured = capsys.readouterr() + assert same_lines(captured.out, ls_sources_output(src, cloud_type)) + + +def test_ls_sources_scheme_uppercased(cloud_test_catalog, cloud_type, capsys): + src = uppercase_scheme(cloud_test_catalog.src_uri) + ls([src, f"{src}/dogs/*"], catalog=cloud_test_catalog.catalog) + captured = capsys.readouterr() + assert same_lines(captured.out, ls_sources_output(src, cloud_type)) + + +def test_ls_not_found(cloud_test_catalog): + src = cloud_test_catalog.src_uri + with pytest.raises(FileNotFoundError): + ls([src, f"{src}/cats/bogus*"], catalog=cloud_test_catalog.catalog) + + +def test_ls_not_a_directory(cloud_test_catalog): + src = cloud_test_catalog.src_uri + with pytest.raises(FileNotFoundError): + ls([src, f"{src}/description/"], catalog=cloud_test_catalog.catalog) + + +def ls_glob_output(src, cloud_type): + if cloud_type == "file": + root_uri = FileClient.root_path().as_uri() + prefix = src[len(root_uri) :] + return f"""\ +{prefix}/dogs/others: +dog4 + +{prefix}/dogs: +dog1 +dog2 +dog3 + """ + + return """\ +dogs/others: +dog4 + +dogs: +dog1 +dog2 +dog3 + """ + + +def test_ls_glob_sub(cloud_test_catalog, cloud_type, capsys): + src = cloud_test_catalog.src_uri + ls([f"{src}/dogs/*"], catalog=cloud_test_catalog.catalog) + captured = capsys.readouterr() + assert same_lines(captured.out, ls_glob_output(src, cloud_type)) + + +def get_partial_indexed_paths(metastore): + p = metastore._partials + return [ + r[0] for r in metastore.db.execute(select(p.c.path_str).order_by(p.c.path_str)) + ] + + +def test_ls_partial_indexing(cloud_test_catalog, cloud_type, capsys): + metastore = cloud_test_catalog.catalog.metastore + src = cloud_test_catalog.src_uri + if cloud_type == "file": + src_metastore = metastore.clone(FileClient.root_path().as_uri()) + root_uri = FileClient.root_path().as_uri() + prefix = src[len(root_uri) :] + "/" + else: + src_metastore = metastore.clone(src) + prefix = "" + + ls([f"{src}/dogs/others/"], catalog=cloud_test_catalog.catalog) + # These sleep calls are here to ensure that capsys can fully capture the output + # and to avoid any flaky tests due to multithreading generating output out of order + sleep(0.05) + captured = capsys.readouterr() + assert get_partial_indexed_paths(src_metastore) == [f"{prefix}dogs/others/"] + assert "Listing" in captured.err + assert captured.out == "dog4\n" + + ls([f"{src}/cats/"], catalog=cloud_test_catalog.catalog) + sleep(0.05) + captured = capsys.readouterr() + assert get_partial_indexed_paths(src_metastore) == [ + f"{prefix}cats/", + f"{prefix}dogs/others/", + ] + assert "Listing" in captured.err + assert same_lines("cat1\ncat2\n", captured.out) + + ls([f"{src}/dogs/"], catalog=cloud_test_catalog.catalog) + sleep(0.05) + captured = capsys.readouterr() + assert get_partial_indexed_paths(src_metastore) == [ + f"{prefix}cats/", + f"{prefix}dogs/", + f"{prefix}dogs/others/", + ] + assert "Listing" in captured.err + assert same_lines("others/\ndog1\ndog2\ndog3\n", captured.out) + + ls([f"{src}/cats/"], catalog=cloud_test_catalog.catalog) + sleep(0.05) + captured = capsys.readouterr() + assert get_partial_indexed_paths(src_metastore) == [ + f"{prefix}cats/", + f"{prefix}dogs/", + f"{prefix}dogs/others/", + ] + assert "Listing" not in captured.err + assert same_lines("cat1\ncat2\n", captured.out) + + ls([f"{src}/"], catalog=cloud_test_catalog.catalog) + sleep(0.05) + captured = capsys.readouterr() + assert get_partial_indexed_paths(src_metastore) == [ + f"{prefix}", + f"{prefix}cats/", + f"{prefix}dogs/", + f"{prefix}dogs/others/", + ] + assert "Listing" in captured.err + assert same_lines("cats/\ndogs/\ndescription\n", captured.out) + + +class MockResponse: + def __init__(self, content, ok=True): + self.content = content + self.ok = ok + + +def mock_post(url, data=None, json=None, **kwargs): + source = json["source"] + path = re.sub(r"\w+://[^/]+/?", "", source).rstrip("/") + data = [ + { + **d, + "path_str": d["path_str"].format(src=path), + "path": d["path_str"].format(src=path).split("/"), + } + for d in REMOTE_DATA[path] + ] + return MockResponse( + content=msgpack.packb({"data": data}, default=_pack_extended_types) + ) + + +def _pack_extended_types(obj): + if isinstance(obj, datetime): + if obj.tzinfo: + data = (obj.timestamp(), int(obj.utcoffset().total_seconds())) + return msgpack.ExtType(42, pack("!dl", *data)) + data = (obj.timestamp(),) + return msgpack.ExtType(42, pack("!d", *data)) + raise TypeError(f"Unknown type: {type(obj)}") + + +ls_remote_sources_output = """\ +{src}: +cats/ +dogs/ +description + +{src}/dogs/others: +dog4 + +{src}/dogs: +dog1 +dog2 +dog3 +""" + + +def test_ls_remote_sources(cloud_type, capsys, monkeypatch): + src = f"{cloud_type}://bucket" + token = "35NmrvSlsGVxTYIglxSsBIQHRrMpi6irSSYcAL0flijOytCHc" # noqa: S105 + with monkeypatch.context() as m: + m.setattr("requests.post", mock_post) + ls( + [src, f"{src}/dogs/others", f"{src}/dogs"], + config={ + "type": "http", + "url": "http://localhost:8111/api/datachain", + "username": "datachain-team", + "token": f"isat_{token}", + }, + ) + captured = capsys.readouterr() + assert captured.out == ls_remote_sources_output.format(src=src) + + +owner_id = "a13a3ff923430363b098ce9c769e450724e74e646332b08ca6b3ac4f96dae083" +REMOTE_DATA: dict[str, list[dict[str, Any]]] = { + "": [ + { + "id": 816, + "dir_type": 1, + "name": "cats", + "etag": "", + "version": "", + "is_latest": True, + "last_modified": datetime(2023, 1, 17, 21, 39, 0, 88564), + "size": 0, + "owner_name": "", + "owner_id": "", + "path_str": "{src}/cats", + "path": [], + }, + { + "id": 825, + "dir_type": 1, + "name": "dogs", + "etag": "", + "version": "", + "is_latest": True, + "last_modified": datetime(2023, 1, 17, 21, 39, 0, 88567), + "size": 0, + "owner_name": "", + "owner_id": "", + "path_str": "{src}/dogs", + "path": [], + }, + { + "id": None, + "dir_type": 0, + "name": "description", + "etag": "20664550afa2654017377ceb266a1f82", + "version": "", + "is_latest": True, + "last_modified": datetime(2022, 2, 10, 3, 39, 9), + "size": 350496, + "owner_name": "", + "owner_id": owner_id, + "path_str": "{src}/description", + "path": [], + }, + ], + "dogs/others": [ + { + "id": None, + "dir_type": 0, + "name": "dog4", + "etag": "c4e42ce24d92bb5b4c4be9a99b237502", + "version": "", + "is_latest": True, + "last_modified": datetime(2022, 6, 28, 22, 39, 1), + "size": 32975, + "owner_name": "", + "owner_id": owner_id, + "path_str": "{src}/dogs/others/dog4", + "path": [], + }, + ], + "dogs": [ + { + "id": None, + "dir_type": 0, + "name": "dog1", + "etag": "44a632238558e0aa4c54bdb901bf9cff", + "version": "", + "is_latest": True, + "last_modified": datetime(2022, 6, 28, 22, 39, 1), + "size": 101, + "owner_name": "", + "owner_id": owner_id, + "path_str": "{src}/dogs/dog1", + "path": [], + }, + { + "id": None, + "dir_type": 0, + "name": "dog2", + "etag": "76556c960239c50e5a8f8569daf85355", + "version": "", + "is_latest": True, + "last_modified": datetime(2022, 6, 28, 22, 39, 1), + "size": 29759, + "owner_name": "", + "owner_id": owner_id, + "path_str": "{src}/dogs/dog2", + "path": [], + }, + { + "id": None, + "dir_type": 0, + "name": "dog3", + "etag": "b1c99fedcf77bf5fa62984e93db1955c", + "version": "", + "is_latest": True, + "last_modified": datetime(2022, 6, 28, 22, 39, 1), + "size": 102, + "owner_name": "", + "owner_id": owner_id, + "path_str": "{src}/dogs/dog3", + "path": [], + }, + ], +} diff --git a/tests/func/test_pull.py b/tests/func/test_pull.py new file mode 100644 index 000000000..6e5391f5b --- /dev/null +++ b/tests/func/test_pull.py @@ -0,0 +1,339 @@ +import io +import json +from datetime import datetime + +import attrs +import lz4.frame +import pandas as pd +import pytest + +from datachain.dataset import DatasetStatus +from datachain.error import DataChainError +from datachain.node import DirType +from datachain.utils import JSONSerialize +from tests.data import ENTRIES +from tests.utils import assert_row_names, skip_if_not_sqlite + + +@pytest.fixture +def dog_entries(): + return [ + attrs.asdict(e) for e in ENTRIES if e.name.startswith("dog") and not e.is_dir + ] + + +@pytest.fixture +def dog_entries_parquet_lz4(dog_entries) -> bytes: + """ + Returns dogs entries in lz4 compressed parquet format + """ + + def _adapt_row(row): + """ + Adjusting row values to match remote response + """ + adapted = {} + for k, v in row.items(): + if isinstance(v, str): + adapted[k] = v.encode("utf-8") + elif isinstance(v, datetime): + adapted[k] = v.timestamp() + elif v is None: + adapted[k] = b"" + else: + adapted[k] = v + + adapted["id"] = 1 + adapted["vtype"] = b"" + adapted["location"] = b"" + adapted["source"] = b"s3://dogs" + adapted["dir_type"] = DirType.FILE + adapted["random"] = 1 + return adapted + + dog_entries = [_adapt_row(e) for e in dog_entries] + df = pd.DataFrame.from_records(dog_entries) + buffer = io.BytesIO() + df.to_parquet(buffer, engine="auto") + + return lz4.frame.compress(buffer.getvalue()) + + +@pytest.fixture +def schema(): + return { + "id": {"type": "UInt64"}, + "vtype": {"type": "String"}, + "dir_type": {"type": "Int32"}, + "parent": {"type": "String"}, + "name": {"type": "String"}, + "etag": {"type": "String"}, + "version": {"type": "String"}, + "is_latest": {"type": "Boolean"}, + "last_modified": {"type": "DateTime"}, + "size": {"type": "Int64"}, + "owner_name": {"type": "String"}, + "owner_id": {"type": "String"}, + "random": {"type": "Int64"}, + "location": {"type": "String"}, + "source": {"type": "String"}, + } + + +@pytest.fixture +def remote_dataset_version(schema, dataset_rows): + return { + "id": 1, + "dataset_id": 1, + "version": 1, + "status": 4, + "feature_schema": {}, + "created_at": "2024-02-23T10:42:31.842944+00:00", + "finished_at": "2024-02-23T10:43:31.842944+00:00", + "error_message": "", + "error_stack": "", + "num_objects": 5, + "size": 1073741824, + "preview": json.loads(json.dumps(dataset_rows, cls=JSONSerialize)), + "script_output": "", + "schema": schema, + "sources": "", + "query_script": ( + 'from datachain.query import DatasetQuery\nDatasetQuery(path="s3://ldb-public")', + ), + "created_by_id": 1, + } + + +@pytest.fixture +def remote_dataset(remote_dataset_version, schema): + return { + "id": 1, + "name": "remote", + "description": "", + "labels": [], + "shadow": False, + "schema": schema, + "status": 4, + "feature_schema": {}, + "created_at": "2024-02-23T10:42:31.842944+00:00", + "finished_at": "2024-02-23T10:43:31.842944+00:00", + "error_message": "", + "error_stack": "", + "script_output": "", + "job_id": "f74ec414-58b7-437d-81c5-d41e5365abba", + "sources": "", + "query_script": "", + "team_id": 1, + "warehouse_id": None, + "created_by_id": 1, + "versions": [remote_dataset_version], + } + + +@pytest.fixture +def remote_config(): + return { + "url": "http://localhost:8111/api/datachain", + "username": "datachain", + "token": "isat_1LZKasZwyM46eHk6NHZZh4VCbHPRKUlQaLnYUE1bXb2U8Il0U", + } + + +@pytest.mark.parametrize("cloud_type, version_aware", [("s3", False)], indirect=True) +@pytest.mark.parametrize("dataset_uri", ["ds://dogs@v1", "ds://dogs"]) +def test_pull_dataset_success( + requests_mock, + cloud_test_catalog, + remote_config, + remote_dataset, + dog_entries_parquet_lz4, + dataset_uri, +): + skip_if_not_sqlite() + data_url = ( + "https://studio-blobvault.s3.amazonaws.com/datachain_ds_export_1_0.parquet.lz4" + ) + requests_mock.post(f'{remote_config["url"]}/dataset-info', json=remote_dataset) + requests_mock.post( + f'{remote_config["url"]}/dataset-stats', + json={"num_objects": 5, "size": 1000}, + ) + requests_mock.post(f'{remote_config["url"]}/dataset-export', json=[data_url]) + requests_mock.post( + f'{remote_config["url"]}/dataset-export-status', + json={"status": "completed"}, + ) + requests_mock.get(data_url, content=dog_entries_parquet_lz4) + catalog = cloud_test_catalog.catalog + + catalog.pull_dataset(dataset_uri, no_cp=True, remote_config=remote_config) + # trying to pull multiple times as it should work + catalog.pull_dataset(dataset_uri, no_cp=True, remote_config=remote_config) + + dataset = catalog.get_dataset("dogs") + assert dataset.versions_values == [1] + assert dataset.status == DatasetStatus.COMPLETE + assert dataset.created_at + assert dataset.finished_at + assert dataset.schema + dataset_version = dataset.get_version(1) + assert dataset_version.status == DatasetStatus.COMPLETE + assert dataset_version.created_at + assert dataset_version.finished_at + assert dataset_version.schema + assert dataset_version.num_objects == 4 + assert dataset_version.size == 15 + + assert_row_names( + catalog, + dataset, + 1, + { + "dog1", + "dog2", + "dog3", + "dog4", + }, + ) + + +@pytest.mark.parametrize("cloud_type, version_aware", [("s3", False)], indirect=True) +def test_pull_dataset_wrong_dataset_uri_format( + requests_mock, + cloud_test_catalog, + remote_config, + remote_dataset, + dog_entries_parquet_lz4, +): + skip_if_not_sqlite() + catalog = cloud_test_catalog.catalog + + with pytest.raises(DataChainError) as exc_info: + catalog.pull_dataset("wrong", no_cp=True, remote_config=remote_config) + assert str(exc_info.value) == "Error when parsing dataset uri" + + +@pytest.mark.parametrize("cloud_type, version_aware", [("s3", False)], indirect=True) +def test_pull_dataset_wrong_version( + requests_mock, + cloud_test_catalog, + remote_config, + remote_dataset, +): + skip_if_not_sqlite() + requests_mock.post( + f'{remote_config["url"]}/dataset-info', + json=remote_dataset, + ) + catalog = cloud_test_catalog.catalog + + with pytest.raises(DataChainError) as exc_info: + catalog.pull_dataset("ds://dogs@v5", no_cp=True, remote_config=remote_config) + assert str(exc_info.value) == "Dataset dogs doesn't have version 5 on server" + + +@pytest.mark.parametrize("cloud_type, version_aware", [("s3", False)], indirect=True) +def test_pull_dataset_not_found_in_remote( + requests_mock, + cloud_test_catalog, + remote_config, + remote_dataset, +): + skip_if_not_sqlite() + requests_mock.post( + f'{remote_config["url"]}/dataset-info', + status_code=404, + json={"message": "Dataset not found"}, + ) + catalog = cloud_test_catalog.catalog + + with pytest.raises(DataChainError) as exc_info: + catalog.pull_dataset("ds://dogs@v1", no_cp=True, remote_config=remote_config) + assert str(exc_info.value) == "Error from server: Dataset not found" + + +@pytest.mark.parametrize("cloud_type, version_aware", [("s3", False)], indirect=True) +def test_pull_dataset_error_on_fetching_stats( + requests_mock, + cloud_test_catalog, + remote_config, + remote_dataset, +): + skip_if_not_sqlite() + requests_mock.post( + f'{remote_config["url"]}/dataset-info', + json=remote_dataset, + ) + requests_mock.post( + f'{remote_config["url"]}/dataset-stats', + status_code=400, + json={"message": "Internal error"}, + ) + catalog = cloud_test_catalog.catalog + + with pytest.raises(DataChainError) as exc_info: + catalog.pull_dataset("ds://dogs@v1", no_cp=True, remote_config=remote_config) + assert str(exc_info.value) == "Error from server: Internal error" + + +@pytest.mark.parametrize("cloud_type, version_aware", [("s3", False)], indirect=True) +@pytest.mark.parametrize("export_status", ["failed", "removed"]) +def test_pull_dataset_exporting_dataset_failed_in_remote( + requests_mock, + cloud_test_catalog, + remote_config, + remote_dataset, + export_status, +): + skip_if_not_sqlite() + data_url = ( + "https://studio-blobvault.s3.amazonaws.com/datachain_ds_export_1_0.parquet.lz4" + ) + requests_mock.post(f'{remote_config["url"]}/dataset-info', json=remote_dataset) + requests_mock.post( + f'{remote_config["url"]}/dataset-stats', + json={"num_objects": 5, "size": 1000}, + ) + requests_mock.post(f'{remote_config["url"]}/dataset-export', json=[data_url]) + requests_mock.post( + f'{remote_config["url"]}/dataset-export-status', + json={"status": export_status}, + ) + + catalog = cloud_test_catalog.catalog + + with pytest.raises(DataChainError) as exc_info: + catalog.pull_dataset("ds://dogs@v1", no_cp=True, remote_config=remote_config) + assert str(exc_info.value) == ( + f"Error from server: Dataset export {export_status} in Studio" + ) + + +@pytest.mark.parametrize("cloud_type, version_aware", [("s3", False)], indirect=True) +def test_pull_dataset_empty_parquet( + requests_mock, + cloud_test_catalog, + remote_config, + remote_dataset, + dog_entries_parquet_lz4, +): + skip_if_not_sqlite() + data_url = ( + "https://studio-blobvault.s3.amazonaws.com/datachain_ds_export_1_0.parquet.lz4" + ) + requests_mock.post(f'{remote_config["url"]}/dataset-info', json=remote_dataset) + requests_mock.post( + f'{remote_config["url"]}/dataset-stats', + json={"num_objects": 5, "size": 1000}, + ) + requests_mock.post(f'{remote_config["url"]}/dataset-export', json=[data_url]) + requests_mock.post( + f'{remote_config["url"]}/dataset-export-status', + json={"status": "completed"}, + ) + requests_mock.get(data_url, content=b"") + catalog = cloud_test_catalog.catalog + + with pytest.raises(RuntimeError): + catalog.pull_dataset("ds://dogs@v1", no_cp=True, remote_config=remote_config) diff --git a/tests/func/test_pytorch.py b/tests/func/test_pytorch.py new file mode 100644 index 000000000..64a32bd86 --- /dev/null +++ b/tests/func/test_pytorch.py @@ -0,0 +1,69 @@ +import open_clip +import pytest +from torch import Size, Tensor +from torchvision.datasets import FakeData +from torchvision.transforms import v2 + +from datachain.lib.dc import DataChain +from datachain.lib.pytorch import PytorchDataset + + +@pytest.fixture +def fake_dataset(tmp_path, catalog): + # Create fake images in labeled dirs + data_path = tmp_path / "data" / "" + for i, (img, label) in enumerate(FakeData()): + label = str(label) + (data_path / label).mkdir(parents=True, exist_ok=True) + img.save(data_path / label / f"{i}.jpg") + + # Create dataset from images + uri = data_path.as_uri() + + return ( + DataChain.from_storage(uri, type="image") + .map(text=lambda file: file.parent.split("/")[-1], output=str) + .map(label=lambda text: int(text), output=int) + .save("fake") + ) + + +def test_pytorch_dataset(fake_dataset): + transform = v2.Compose([v2.ToTensor(), v2.Resize((64, 64))]) + tokenizer = open_clip.get_tokenizer("ViT-B-32") + pt_dataset = PytorchDataset( + name=fake_dataset.name, + version=fake_dataset.version, + transform=transform, + tokenizer=tokenizer, + ) + for img, text, label in pt_dataset: + assert isinstance(img, Tensor) + assert isinstance(text, Tensor) + assert isinstance(label, int) + assert img.size() == Size([3, 64, 64]) + + +def test_pytorch_dataset_sample(fake_dataset): + transform = v2.Compose([v2.ToTensor(), v2.Resize((64, 64))]) + pt_dataset = PytorchDataset( + name=fake_dataset.name, + version=fake_dataset.version, + transform=transform, + num_samples=700, + ) + assert len(list(pt_dataset)) == 700 + + +def test_to_pytorch(fake_dataset): + from torch.utils.data import IterableDataset + + transform = v2.Compose([v2.ToTensor(), v2.Resize((64, 64))]) + tokenizer = open_clip.get_tokenizer("ViT-B-32") + pt_dataset = fake_dataset.to_pytorch(transform=transform, tokenizer=tokenizer) + assert isinstance(pt_dataset, IterableDataset) + for img, text, label in pt_dataset: + assert isinstance(img, Tensor) + assert isinstance(text, Tensor) + assert isinstance(label, int) + assert img.size() == Size([3, 64, 64]) diff --git a/tests/func/test_query.py b/tests/func/test_query.py new file mode 100644 index 000000000..81b9e1f55 --- /dev/null +++ b/tests/func/test_query.py @@ -0,0 +1,391 @@ +import json +import os.path +from textwrap import dedent +from typing import Optional + +import dill +import pytest + +from datachain.catalog import QUERY_DATASET_PREFIX +from datachain.cli import query +from datachain.data_storage import AbstractDBMetastore, JobQueryType, JobStatus +from datachain.error import QueryScriptRunError +from tests.utils import assert_row_names + + +@pytest.fixture +def catalog_info_filepath(cloud_test_catalog_tmpfile, tmp_path): + catalog = cloud_test_catalog_tmpfile.catalog + + catalog_info = { + "catalog_init_params": catalog.get_init_params(), + "id_generator_params": catalog.id_generator.clone_params(), + "metastore_params": catalog.metastore.clone_params(), + "warehouse_params": catalog.warehouse.clone_params(), + } + catalog_info_filepath = tmp_path / "catalog-info" + with open(catalog_info_filepath, "wb") as f: + dill.dump(catalog_info, f) + + return catalog_info_filepath + + +def setup_catalog(query: str, catalog_info_filepath: str) -> str: + query_catalog_setup = f"""\ + import dill + from datachain.catalog import Catalog + + catalog_info_filepath = {str(catalog_info_filepath)!r} + with open(catalog_info_filepath, "rb") as f: + catalog_info = dill.load(f) + ( + id_generator_class, + id_generator_args, + id_generator_kwargs, + ) = catalog_info["id_generator_params"] + id_generator = id_generator_class(*id_generator_args, **id_generator_kwargs) + ( + metastore_class, + metastore_args, + metastore_kwargs, + ) = catalog_info["metastore_params"] + metastore = metastore_class(*metastore_args, **metastore_kwargs) + ( + warehouse_class, + warehouse_args, + warehouse_kwargs, + ) = catalog_info["warehouse_params"] + warehouse = warehouse_class(*warehouse_args, **warehouse_kwargs) + catalog = Catalog( + id_generator=id_generator, + metastore=metastore, + warehouse=warehouse, + **catalog_info["catalog_init_params"], + ) + """ + return dedent(query_catalog_setup + "\n" + query) + + +def get_latest_job( + metastore: AbstractDBMetastore, +) -> Optional[tuple[str, str, int, int, str, str]]: + j = metastore._jobs + + latest_jobs_query = ( + metastore._jobs_select( + j.c.id, + j.c.name, + j.c.status, + j.c.query_type, + j.c.error_message, + j.c.error_stack, + j.c.metrics, + ) + .order_by(j.c.created_at.desc()) + .limit(1) + ) + latest_jobs = list(metastore.db.execute(latest_jobs_query)) + if len(latest_jobs) == 0: + return None + return latest_jobs[0] + + +@pytest.mark.parametrize("cloud_type,version_aware", [("file", False)], indirect=True) +def test_query_cli(cloud_test_catalog_tmpfile, tmp_path, catalog_info_filepath, capsys): + catalog = cloud_test_catalog_tmpfile.catalog + src_uri = cloud_test_catalog_tmpfile.src_uri + + query_script = f"""\ + from datachain.query import DatasetQuery + + DatasetQuery({src_uri!r}, catalog=catalog) + """ + query_script = setup_catalog(query_script, catalog_info_filepath) + + filepath = tmp_path / "query_script.py" + filepath.write_text(query_script) + + ds_name = "my-dataset" + query(catalog, str(filepath), ds_name, columns=["name"]) + captured = capsys.readouterr() + + header, *rows = captured.out.splitlines() + assert header.strip() == "name" + name_rows = {row.split()[1] for row in rows} + assert name_rows == {"cat1", "cat2", "description", "dog1", "dog2", "dog3", "dog4"} + + dataset = catalog.get_dataset(ds_name) + assert dataset + result_job_id = dataset.get_version(dataset.latest_version).job_id + assert result_job_id + + latest_job = get_latest_job(catalog.metastore) + assert latest_job + + assert str(latest_job[0]) == str(result_job_id) + assert latest_job[1] == os.path.basename(filepath) + assert latest_job[2] == JobStatus.COMPLETE + assert latest_job[3] == JobQueryType.PYTHON + assert latest_job[4] == "" + assert latest_job[5] == "" + + +def test_query_cli_no_dataset_returned( + cloud_test_catalog_tmpfile, tmp_path, catalog_info_filepath, capsys +): + catalog = cloud_test_catalog_tmpfile.catalog + + query_script = """\ + from datachain.query import DatasetQuery + + DatasetQuery("test", catalog=catalog) + + print("test") + """ + query_script = setup_catalog(query_script, catalog_info_filepath) + + filepath = tmp_path / "query_script.py" + filepath.write_text(query_script) + + with pytest.raises( + QueryScriptRunError, + match="Last line in a script was not an instance of DatasetQuery", + ): + query(catalog, str(filepath), "my-dataset", columns=["name"]) + + latest_job = get_latest_job(catalog.metastore) + assert latest_job + + assert latest_job[1] == os.path.basename(filepath) + assert latest_job[2] == JobStatus.FAILED + assert latest_job[3] == JobQueryType.PYTHON + assert latest_job[4] == "Last line in a script was not an instance of DatasetQuery" + assert latest_job[5].find("datachain.error.QueryScriptRunError") + + +@pytest.mark.parametrize( + "save,save_as", + ( + (True, None), + (None, "my-dataset"), + (True, "my-dataset"), + ), +) +@pytest.mark.parametrize("save_dataset", (None, "new-dataset")) +def test_query( + save, + save_as, + save_dataset, + cloud_test_catalog_tmpfile, + tmp_path, + catalog_info_filepath, +): + catalog = cloud_test_catalog_tmpfile.catalog + src_uri = cloud_test_catalog_tmpfile.src_uri + + query_script = f"""\ + from datachain.query import DatasetQuery + ds = DatasetQuery({src_uri!r}, catalog=catalog) + if {save_dataset!r}: + ds = ds.save({save_dataset!r}) + ds + """ + query_script = setup_catalog(query_script, catalog_info_filepath) + + result = catalog.query(query_script, save=save, save_as=save_as) + if save_as: + assert result.dataset.name == save_as + assert catalog.get_dataset(save_as) + elif save_dataset: + assert result.dataset.name == save_dataset + assert catalog.get_dataset(save_dataset) + else: + assert result.dataset.name.startswith(QUERY_DATASET_PREFIX) + assert result.version == 1 + assert result.dataset.versions_values == [1] + assert result.dataset.query_script == query_script + assert_row_names( + catalog, + result.dataset, + result.version, + { + "cat1", + "cat2", + "description", + "dog1", + "dog2", + "dog3", + "dog4", + }, + ) + + +@pytest.mark.parametrize( + "params,count", + ( + (None, 7), + ({"limit": 1}, 1), + ({"limit": 5}, 5), + ), +) +def test_query_params( + params, count, cloud_test_catalog_tmpfile, tmp_path, catalog_info_filepath +): + catalog = cloud_test_catalog_tmpfile.catalog + src_uri = cloud_test_catalog_tmpfile.src_uri + + query_script = f"""\ + from datachain.query import DatasetQuery, param + + ds = DatasetQuery({src_uri!r}, catalog=catalog) + if param("limit"): + ds = ds.limit(int(param("limit"))) + ds + """ + query_script = setup_catalog(query_script, catalog_info_filepath) + + result = catalog.query(query_script, save=True, params=params) + assert ( + len(list(catalog.ls_dataset_rows(result.dataset.name, result.version))) == count + ) + + +def test_query_where_last_command_is_call_on_save_which_returns_attached_dataset( + cloud_test_catalog_tmpfile, tmp_path, catalog_info_filepath +): + """ + Testing use case where last command is call on DatasetQuery save which returns + attached instance to underlying saved dataset + """ + catalog = cloud_test_catalog_tmpfile.catalog + src_uri = cloud_test_catalog_tmpfile.src_uri + + query_script = f"""\ + from datachain.query import C, DatasetQuery + + DatasetQuery({src_uri!r}, catalog=catalog).filter(C.name.glob("dog*")).save("dogs") + """ + query_script = setup_catalog(query_script, catalog_info_filepath) + + result = catalog.query(query_script, save=True) + assert not result.dataset.name.startswith(QUERY_DATASET_PREFIX) + assert result.dataset.query_script == query_script + assert result.version == 1 + assert result.dataset.versions_values == [1] + assert_row_names( + catalog, + result.dataset, + result.version, + { + "dog1", + "dog2", + "dog3", + "dog4", + }, + ) + + +def test_query_where_last_command_is_attached_dataset_query_created_from_save( + cloud_test_catalog_tmpfile, tmp_path, catalog_info_filepath +): + """ + Testing use case where last command is instance of DatasetQuery which is + attached to underlying dataset by calling save just before + """ + catalog = cloud_test_catalog_tmpfile.catalog + src_uri = cloud_test_catalog_tmpfile.src_uri + + query_script = f"""\ + from datachain.query import C, DatasetQuery + + ds = DatasetQuery( + {src_uri!r}, catalog=catalog + ).filter(C.name.glob("dog*")).save("dogs") + ds + """ + query_script = setup_catalog(query_script, catalog_info_filepath) + + result = catalog.query(query_script, save=True) + assert result.dataset.name == "dogs" + assert result.dataset.query_script == query_script + assert result.version == 1 + assert result.dataset.versions_values == [1] + assert_row_names( + catalog, + result.dataset, + result.version, + { + "dog1", + "dog2", + "dog3", + "dog4", + }, + ) + + +def test_query_where_last_command_is_attached_dataset_query_created_from_query( + cloud_test_catalog_tmpfile, tmp_path, catalog_info_filepath +): + """ + Testing use case where last command is instance of DatasetQuery which is + attached to underlying dataset by creating query pointing to it + """ + catalog = cloud_test_catalog_tmpfile.catalog + src_uri = cloud_test_catalog_tmpfile.src_uri + + query_script = f"""\ + from datachain.query import C, DatasetQuery + + ds = DatasetQuery( + {src_uri!r}, catalog=catalog + ).filter(C.name.glob("dog*")).save("dogs") + DatasetQuery(name="dogs", version=1, catalog=catalog) + """ + query_script = setup_catalog(query_script, catalog_info_filepath) + + result = catalog.query(query_script, save=True) + assert result.dataset.name == "dogs" + assert result.dataset.query_script == query_script + assert result.version == 1 + assert result.dataset.versions_values == [1] + assert_row_names( + catalog, + result.dataset, + result.version, + { + "dog1", + "dog2", + "dog3", + "dog4", + }, + ) + + +@pytest.mark.parametrize("cloud_type,version_aware", [("file", False)], indirect=True) +def test_query_params_metrics( + cloud_test_catalog_tmpfile, tmp_path, catalog_info_filepath, capsys +): + catalog = cloud_test_catalog_tmpfile.catalog + src_uri = cloud_test_catalog_tmpfile.src_uri + + query_script = """\ + from datachain.query import DatasetQuery, metrics, param + + ds = DatasetQuery(param("url"), catalog=catalog) + + metrics.set("count", ds.count()) + + ds + """ + query_script = setup_catalog(query_script, catalog_info_filepath) + + filepath = tmp_path / "query_script.py" + filepath.write_text(query_script) + + query(catalog, str(filepath), params={"url": src_uri}) + + latest_job = get_latest_job(catalog.metastore) + assert latest_job + + assert latest_job[2] == JobStatus.COMPLETE + assert json.loads(latest_job[6]) == {"count": 7} diff --git a/tests/scripts/feature_class.py b/tests/scripts/feature_class.py new file mode 100644 index 000000000..c1889c34d --- /dev/null +++ b/tests/scripts/feature_class.py @@ -0,0 +1,17 @@ +from datachain.lib.dc import C, DataChain +from datachain.lib.feature import Feature + + +class Embedding(Feature): + value: float + + +ds_name = "feature_class" +ds = ( + DataChain.from_storage("gs://dvcx-datalakes/dogs-and-cats/") + .filter(C.name.glob("*cat*.jpg")) # type: ignore [attr-defined] + .limit(5) + .map(emd=lambda file: Embedding(value=512), output=Embedding) +) + +ds.save(ds_name) diff --git a/tests/scripts/feature_class_parallel.py b/tests/scripts/feature_class_parallel.py new file mode 100644 index 000000000..82b3d976d --- /dev/null +++ b/tests/scripts/feature_class_parallel.py @@ -0,0 +1,29 @@ +from typing import Literal, Optional + +from datachain.lib.dc import C, DataChain +from datachain.lib.feature import Feature + + +class NestedFeature(Feature): + value: str + + +class Embedding(Feature): + value: float + nested: NestedFeature = NestedFeature(value="nested_value") + literal_field: Optional[Literal["end_turn", "max_tokens", "stop_sequence"]] = None + + +# ToDO: make it parallel +ds_name = "feature_class" +ds = ( + DataChain.from_storage("gs://dvcx-datalakes/dogs-and-cats/") + .filter(C.name.glob("*cat*.jpg")) # type: ignore [attr-defined] + .limit(5) + .settings(cache=True, parallel=2) + .map(emd=lambda file: Embedding(value=512), output=Embedding) + .save(ds_name) +) + +for row in ds.results(): + print(row[5]) diff --git a/tests/scripts/name_len_normal.py b/tests/scripts/name_len_normal.py new file mode 100644 index 000000000..101a1ac8e --- /dev/null +++ b/tests/scripts/name_len_normal.py @@ -0,0 +1,27 @@ +from datachain.query import C, DatasetQuery, udf +from datachain.sql.types import Int + + +# Define the UDF: +@udf( + ("name",), # Columns consumed by the UDF. + { + "name_len": Int + }, # Signals being returned by the UDF, with the signal name and type. +) +def name_len(name): + if name.endswith(".json"): + return (-1,) + return (len(name),) + + +# Save as a new dataset. +DatasetQuery( + path="gs://dvcx-datalakes/dogs-and-cats/", + anon=True, +).filter(C.name.glob("*cat*")).add_signals(name_len, parallel=-1).order_by( # type: ignore[attr-defined] + "name" +).limit(2).save("name_len") + +# Output the contents of the new dataset. +print(DatasetQuery(name="name_len").select(C.name, C.name_len).results()) diff --git a/tests/scripts/name_len_slow.py b/tests/scripts/name_len_slow.py new file mode 100644 index 000000000..423c1a08b --- /dev/null +++ b/tests/scripts/name_len_slow.py @@ -0,0 +1,46 @@ +import sys +from time import sleep + +from datachain.query import C, DatasetQuery, udf +from datachain.sql.types import Int + +if sys.platform == "win32": + # This is needed for this process to accept a Ctrl-C event in Windows, + # when run under pytest as a subprocess. + # This is not needed when running normally from the command line. + import ctypes + + kernel32 = ctypes.WinDLL("kernel32", use_last_error=True) + + if not kernel32.SetConsoleCtrlHandler(None, False): + print("SetConsoleCtrlHandler error: ", ctypes.get_last_error(), file=sys.stderr) + + +# Define the UDF: +@udf( + ("name",), # Columns consumed by the UDF. + { + "name_len": Int + }, # Signals being returned by the UDF, with the signal name and type. +) +def name_len(name): + # This is to avoid a sleep statement in the tests, so that the end-to-end test + # knows when UDF processing has started, since we are testing canceling + # UDF processing. + # This is done to emulate a user waiting for processing that is stuck, + # and pressing Ctrl-C to cancel the query script and UDF. + print("UDF Processing Started") + # Avoid any buffering so that the end-to-end test can react immediately. + sys.stdout.flush() + # Process very slowly to emulate a stuck script. + sleep(1) + if name.endswith(".json"): + return (-1,) + return (len(name),) + + +# Save as a new dataset. +DatasetQuery( + path="gs://dvcx-datalakes/dogs-and-cats/", + anon=True, +).filter(C.name.glob("*cat*")).add_signals(name_len, parallel=1).save("name_len") # type: ignore[attr-defined] diff --git a/tests/test_cli_e2e.py b/tests/test_cli_e2e.py new file mode 100644 index 000000000..35f4845e1 --- /dev/null +++ b/tests/test_cli_e2e.py @@ -0,0 +1,219 @@ +import os +import os.path +import subprocess +from textwrap import dedent + +import pytest + +MNT_FILE_TREE = { + "01375.png": 324, + "07510.png": 308, + "08433.png": 224, + "mnist-info.txt": 0, + "readme.md": 1193, + "train": { + "1": { + "00266.png": 186, + "06810.png": 175, + "08537.png": 206, + "09396.png": 168, + "09846.png": 208, + }, + "2": { + "00422.png": 257, + "04813.png": 266, + "05508.png": 287, + "07576.png": 297, + "08747.png": 319, + }, + "3": { + "00271.png": 303, + "00577.png": 293, + "01608.png": 243, + "07194.png": 300, + "08051.png": 304, + }, + "train-info.md": 18, + }, + "val": { + "1": { + "01837.png": 182, + "05385.png": 182, + "09416.png": 162, + }, + "2": { + "02201.png": 283, + "02297.png": 274, + "08967.png": 333, + }, + "3": { + "01805.png": 345, + "06366.png": 302, + "08394.png": 329, + }, + "val-info.md": 20, + }, +} + +# Note that these commands are tested in order. +E2E_STEPS = ( + { + "command": ( + "datachain", + "ls", + "--anon", + "s3://ldb-public/remote/datasets/dogs-and-cats/", + ), + "expected": dedent( + """ + dogs-and-cats.tar.gz + dogs-and-cats.zip + license + """ + ), + "listing": True, + }, + { + "command": ( + "datachain", + "du", + "--anon", + "s3://ldb-public/remote/datasets/dogs-and-cats/", + ), + "expected": " 9.2M s3://ldb-public/remote/datasets/dogs-and-cats/\n", + }, + { + "command": ( + "datachain", + "find", + "--iname", + "*DOG*", + "--anon", + "s3://ldb-public/remote/datasets/", + ), + "expected": dedent( + """ + s3://ldb-public/remote/datasets/Stanford-dog-breeds/ + s3://ldb-public/remote/datasets/dogs-and-cats/ + s3://ldb-public/remote/datasets/dogs-and-cats/dogs-and-cats.tar.gz + s3://ldb-public/remote/datasets/dogs-and-cats/dogs-and-cats.zip + """ + ), + "listing": True, + }, + { + "command": ("datachain", "du", "--anon", "s3://ldb-public/remote/datasets/"), + "expected": " 43.3G s3://ldb-public/remote/datasets/\n", + }, + { + "command": ( + "datachain", + "cp", + "-r", + "--anon", + "s3://ldb-public/remote/datasets/mnist-tiny/", + "mnt-cp", + ), + "expected": "", + "downloading": True, + "instantiating": True, + "files": { + "mnt-cp": MNT_FILE_TREE, + }, + }, + { + "command": ( + "datachain", + "clone", + "-r", + "--anon", + "s3://ldb-public/remote/datasets/mnist-tiny/", + "mnt", + ), + "expected": "", + "downloading": True, + "instantiating": True, + "files": { + "mnt": MNT_FILE_TREE, + }, + }, + { + "command": ("datachain", "ls-datasets"), + "expected": "mnt (v1)\n", + }, + { + "command": ("datachain", "ls-datasets"), + "expected": "mnt (v1)\n", + }, + { + "command": ("datachain", "edit-dataset", "mnt", "--new-name", "mnt-new"), + "expected": "", + }, + { + "command": ("datachain", "ls-datasets"), + "expected": "mnt-new (v1)\n", + }, + { + "command": ("datachain", "rm-dataset", "mnt-new", "--version", "1"), + "expected": "", + }, + { + "command": ("datachain", "ls-datasets"), + "expected": "", + }, + { + "command": ("datachain", "gc"), + "expected": "Nothing to clean up.\n", + }, +) + + +def verify_files(files, base=""): + """Recursively validate file and directory structure.""" + for name, value in files.items(): + full_name = os.path.join(base, name) + if isinstance(value, dict): + assert os.path.isdir(full_name) + verify_files(value, full_name) + else: + assert os.path.isfile(full_name) + assert os.path.getsize(full_name) == value + + +def run_step(step): + """Run an end-to-end test step with a command and expected output.""" + result = subprocess.run( # noqa: S603 + step["command"], + shell=False, + capture_output=True, + check=True, + encoding="utf-8", + ) + if step.get("sort_expected_lines"): + assert sorted(result.stdout.split("\n")) == sorted( + step["expected"].lstrip("\n").split("\n") + ) + else: + assert result.stdout == step["expected"].lstrip("\n") + if step.get("listing"): + assert "Listing" in result.stderr + else: + assert "Listing" not in result.stderr + if step.get("downloading"): + assert "Downloading" in result.stderr + else: + assert "Downloading" not in result.stderr + if step.get("instantiating"): + assert "Instantiating" in result.stderr + else: + assert "Instantiating" not in result.stderr + files = step.get("files") + if files: + verify_files(files) + + +@pytest.mark.e2e +def test_cli_e2e(tmp_dir, catalog): + """End-to-end CLI Test""" + for step in E2E_STEPS: + run_step(step) diff --git a/tests/test_query_e2e.py b/tests/test_query_e2e.py new file mode 100644 index 000000000..14cbec2e8 --- /dev/null +++ b/tests/test_query_e2e.py @@ -0,0 +1,222 @@ +import os +import os.path +import signal +import subprocess +import sys +import textwrap +from io import TextIOWrapper +from textwrap import dedent +from threading import Thread +from typing import Callable + +import pytest + +tests_dir = os.path.dirname(os.path.abspath(__file__)) + +python_exc = sys.executable or "python3" + + +E2E_STEP_TIMEOUT_SEC = 30 + + +E2E_STEPS = ( + { + "command": ( + "datachain", + "find", + "--anon", + "--name", + "cat.1.*", + "gs://dvcx-datalakes/dogs-and-cats/", + ), + "expected": dedent( + """ + gs://dvcx-datalakes/dogs-and-cats/cat.1.jpg + gs://dvcx-datalakes/dogs-and-cats/cat.1.json + """ + ), + "listing": True, + }, + { + "command": ( + python_exc, + os.path.join(tests_dir, "scripts", "feature_class_parallel.py"), + ), + "expected_in": dedent( + """ + cat.1.jpg + cat.10.jpg + cat.100.jpg + cat.1000.jpg + cat.1001.jpg + """ + ), + }, + { + "command": ( + "datachain", + "query", + os.path.join(tests_dir, "scripts", "feature_class.py"), + "--columns", + "name,emd.value", + ), + "expected_rows": dedent( + """ + name emd__value + 1 cat.1.jpg 512.0 + 2 cat.10.jpg 512.0 + 3 cat.100.jpg 512.0 + 4 cat.1000.jpg 512.0 + 5 cat.1001.jpg 512.0 + """ + ), + }, + { + "command": ( + python_exc, + os.path.join(tests_dir, "scripts", "name_len_slow.py"), + ), + "interrupt_after": "UDF Processing Started", + "expected_in_stderr": "KeyboardInterrupt", + "expected_not_in_stderr": "semaphore", + }, + { + "command": ("datachain", "gc"), + "expected": "Nothing to clean up.\n", + }, +) + + +def watch_process_thread( + stream: TextIOWrapper, output_lines: list[str], watch_value: str, callback: Callable +) -> None: + """ + Watches either the stdout or stderr stream from a given process, + reads the output into output_lines, and watches for the given watch_value, + then calls callback once found. + """ + while (line := stream.readline()) != "": + line = line.strip() + output_lines.append(line) + if watch_value in line: + callback() + + +def communicate_and_interrupt_process( + process: subprocess.Popen, interrupt_after: str +) -> tuple[str, str]: + def interrupt_step() -> None: + if sys.platform == "win32": + # Windows has a different mechanism of sending a Ctrl-C event. + process.send_signal(signal.CTRL_C_EVENT) + else: + process.send_signal(signal.SIGINT) + + stdout_lines: list[str] = [] + stderr_lines: list[str] = [] + watch_threads = ( + Thread( + target=watch_process_thread, + name="Test-Query-E2E-Interrupt-stdout", + daemon=True, + args=[process.stdout, stdout_lines, interrupt_after, interrupt_step], + ), + Thread( + target=watch_process_thread, + name="Test-Query-E2E-Interrupt-stderr", + daemon=True, + args=[process.stderr, stderr_lines, interrupt_after, interrupt_step], + ), + ) + for t in watch_threads: + t.start() + process.wait(timeout=E2E_STEP_TIMEOUT_SEC) + return "\n".join(stdout_lines), "\n".join(stderr_lines) + + +def run_step(step): + """Run an end-to-end query test step with a command and expected output.""" + command = step["command"] + # Note that a process.returncode of -2 is the same as the shell returncode of 130 + # (canceled by KeyboardInterrupt) + interrupt_exit_code = -2 + if sys.platform == "win32": + # Windows has a different mechanism of creating a process group. + popen_args = {"creationflags": subprocess.CREATE_NEW_PROCESS_GROUP} + # This is STATUS_CONTROL_C_EXIT which is equivalent to 0xC000013A + interrupt_exit_code = 3221225786 + else: + popen_args = {"start_new_session": True} + process = subprocess.Popen( # noqa: S603 + command, + shell=False, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + **popen_args, + ) + interrupt_after = step.get("interrupt_after") + if interrupt_after: + stdout, stderr = communicate_and_interrupt_process(process, interrupt_after) + else: + stdout, stderr = process.communicate(timeout=E2E_STEP_TIMEOUT_SEC) + + if interrupt_after: + if process.returncode not in (interrupt_exit_code, 1): + print(f"Process stdout: {stdout}") + print(f"Process stderr: {stderr}") + raise RuntimeError( + "Query script failed to interrupt correctly: " + f"{process.returncode} Command: {command}" + ) + elif process.returncode != 0: + print(f"Process stdout: {stdout}") + print(f"Process stderr: {stderr}") + raise RuntimeError( + "Query script failed with exit code: " + f"{process.returncode} Command: {command}" + ) + + if step.get("sort_expected_lines"): + assert sorted(stdout.split("\n")) == sorted( + step["expected"].lstrip("\n").split("\n") + ) + elif step.get("expected_in_stderr"): + assert step["expected_in_stderr"] in stderr + if step.get("expected_not_in_stderr"): + assert step["expected_not_in_stderr"] not in stderr + elif step.get("expected_in"): + assert sorted(stdout.split("\n")) == sorted( + step["expected_in"].lstrip("\n").split("\n") + ) + elif step.get("expected_rows"): + assert _comparable_row(stdout) == _comparable_row(step["expected_rows"]) + else: + assert stdout == step["expected"].lstrip("\n") + + if step.get("listing"): + assert "Listing" in stderr + else: + assert "Listing" not in stderr + + +@pytest.mark.e2e +def test_query_e2e(tmp_dir, catalog): + """End-to-end CLI Query Test""" + for step in E2E_STEPS: + run_step(step) + + +def _comparable_row(output: str) -> str: + return "\n".join( + sorted( + [_remove_serial_index(line) for line in output.lstrip("\n").splitlines()] + ) + ) + + +def _remove_serial_index(output: str) -> str: + splits = textwrap.shorten(output, width=1000).strip().split(" ") + if splits[0].isdigit(): + return " ".join(splits[1:]) + return " ".join(splits) diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/lib/__init__.py b/tests/unit/lib/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/lib/test_arrow.py b/tests/unit/lib/test_arrow.py new file mode 100644 index 000000000..6bf0224ae --- /dev/null +++ b/tests/unit/lib/test_arrow.py @@ -0,0 +1,108 @@ +from datetime import datetime + +import pandas as pd +import pyarrow as pa +import pytest + +from datachain.lib.arrow import ( + ArrowGenerator, + _arrow_type_mapper, + schema_to_output, +) +from datachain.lib.file import File, IndexedFile + + +def test_arrow_generator(tmp_path, catalog): + ids = [12345, 67890, 34, 0xF0123] + texts = ["28", "22", "we", "hello world"] + df = pd.DataFrame({"id": ids, "text": texts}) + + name = "111.parquet" + pq_path = tmp_path / name + df.to_parquet(pq_path) + stream = File(name=name, parent=tmp_path.as_posix(), source="file:///") + stream._set_stream(catalog, caching_enabled=False) + + func = ArrowGenerator() + objs = list(func.process(stream)) + + assert len(objs) == len(ids) + for index, (o, id, text) in enumerate(zip(objs, ids, texts)): + assert isinstance(o[0], IndexedFile) + assert isinstance(o[0].file, File) + assert o[0].index == index + assert o[1] == id + assert o[2] == text + + +@pytest.mark.parametrize( + "col_type,expected", + ( + (pa.timestamp("us"), datetime), + (pa.binary(), bytes), + (pa.float32(), float), + (pa.float64(), float), + (pa.float16(), float), + (pa.int8(), int), + (pa.int16(), int), + (pa.int32(), int), + (pa.int64(), int), + (pa.uint8(), int), + (pa.bool_(), bool), + (pa.date32(), datetime), + (pa.string(), str), + (pa.large_string(), str), + (pa.struct({"x": pa.int32(), "y": pa.string()}), dict), + (pa.map_(pa.string(), pa.int32()), dict), + (pa.dictionary(pa.int64(), pa.string()), str), + (pa.list_(pa.string()), list[str]), + ), +) +def test_arrow_type_mapper(col_type, expected): + assert _arrow_type_mapper(col_type) == expected + + +def test_arrow_type_error(): + col_type = pa.union( + [pa.field("a", pa.binary(10)), pa.field("b", pa.string())], + mode=pa.lib.UnionMode_DENSE, + ) + with pytest.raises(TypeError): + _arrow_type_mapper(col_type) + + +def test_schema_to_output(): + schema = pa.schema([("some_int", pa.int32()), ("some_string", pa.string())]) + assert schema_to_output(schema) == { + "source": IndexedFile, + "some_int": int, + "some_string": str, + } + + +def test_parquet_convert_column_names(): + schema = pa.schema( + [ + ("UpperCaseCol", pa.int32()), + ("dot.notation.col", pa.int32()), + ("with-dashes", pa.int32()), + ("with spaces", pa.int32()), + ] + ) + assert list(schema_to_output(schema)) == [ + "source", + "uppercasecol", + "dotnotationcol", + "withdashes", + "withspaces", + ] + + +def test_parquet_missing_column_names(): + schema = pa.schema( + [ + ("", pa.int32()), + ("", pa.int32()), + ] + ) + assert list(schema_to_output(schema)) == ["source", "c0", "c1"] diff --git a/tests/unit/lib/test_clip.py b/tests/unit/lib/test_clip.py new file mode 100644 index 000000000..b39ac275f --- /dev/null +++ b/tests/unit/lib/test_clip.py @@ -0,0 +1,61 @@ +import open_clip +import pytest +from PIL import Image +from transformers import CLIPModel, CLIPProcessor + +from datachain.lib.clip import similarity_scores + +IMAGES = [Image.new(mode="RGB", size=(64, 64)), Image.new(mode="RGB", size=(32, 32))] +TEXTS = ["text1", "text2"] +MODEL, _, PREPROCESS = open_clip.create_model_and_transforms( + "ViT-B-32", pretrained="laion2b_s34b_b79k" +) +TOKENIZER = open_clip.get_tokenizer("ViT-B-32") + + +@pytest.mark.parametrize( + "images", + [None, Image.new(mode="RGB", size=(64, 64)), IMAGES], +) +@pytest.mark.parametrize("text", [None, "text", TEXTS]) +@pytest.mark.parametrize("prob", [True, False]) +@pytest.mark.parametrize("image_to_text", [True, False]) +def test_similarity_scores(images, text, prob, image_to_text): + if not (images or text): + with pytest.raises(ValueError): + scores = similarity_scores( + images, text, MODEL, PREPROCESS, TOKENIZER, prob, image_to_text + ) + else: + scores = similarity_scores( + images, text, MODEL, PREPROCESS, TOKENIZER, prob, image_to_text + ) + assert isinstance(scores, list) + if not images: + image_to_text = False + elif not text: + image_to_text = True + if image_to_text: + if isinstance(images, list): + assert len(scores) == len(images) + else: + assert len(scores) == 1 + elif not image_to_text: + if isinstance(text, list): + assert len(scores) == len(text) + else: + assert len(scores) == 1 + if prob: + for score in scores: + assert sum(score) == pytest.approx(1) + + +def test_similarity_scores_hf(): + model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32") + processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32") + + scores = similarity_scores( + IMAGES, TEXTS, model, processor.image_processor, processor.tokenizer + ) + assert isinstance(scores, list) + assert len(scores) == len(IMAGES) diff --git a/tests/unit/lib/test_datachain.py b/tests/unit/lib/test_datachain.py new file mode 100644 index 000000000..15a5ae96c --- /dev/null +++ b/tests/unit/lib/test_datachain.py @@ -0,0 +1,786 @@ +import datetime +import math +from collections.abc import Generator, Iterator + +import numpy as np +import pandas as pd +import pytest + +from datachain.lib.dc import C, DataChain +from datachain.lib.feature import Feature +from datachain.lib.file import File +from datachain.lib.signal_schema import ( + SignalResolvingError, + SignalResolvingTypeError, + SignalSchema, +) +from datachain.lib.udf_signature import UdfSignatureError +from datachain.lib.utils import DataChainParamsError + +DF_DATA = { + "first_name": ["Alice", "Bob", "Charlie", "David", "Eva"], + "age": [25, 30, 35, 40, 45], + "city": ["New York", "Los Angeles", "Chicago", "Houston", "Phoenix"], +} + +DF_OTHER_DATA = { + "last_name": ["Smith", "Jones"], + "country": ["USA", "Russia"], +} + + +class MyFr(Feature): + nnn: str + count: int + + +class MyNested(Feature): + label: str + fr: MyFr + + +features = [MyFr(nnn="n1", count=3), MyFr(nnn="n2", count=5), MyFr(nnn="n1", count=1)] +features_nested = [ + MyNested(fr=fr, label=f"label_{num}") for num, fr in enumerate(features) +] + + +def test_pandas_conversion(catalog): + df = pd.DataFrame(DF_DATA) + df1 = DataChain.from_pandas(df) + df1 = df1.select("first_name", "age", "city").to_pandas() + assert df1.equals(df) + + +def test_pandas_file_column_conflict(catalog): + file_records = {"name": ["aa.txt", "bb.txt", "ccc.jpg", "dd", "e.txt"]} + with pytest.raises(DataChainParamsError): + DataChain.from_pandas(pd.DataFrame(DF_DATA | file_records)) + + file_records = {"etag": [1, 2, 3, 4, 5]} + with pytest.raises(DataChainParamsError): + DataChain.from_pandas(pd.DataFrame(DF_DATA | file_records)) + + +def test_pandas_uppercase_columns(catalog): + data = { + "FirstName": ["Alice", "Bob", "Charlie", "David", "Eva"], + "Age": [25, 30, 35, 40, 45], + "City": ["New York", "Los Angeles", "Chicago", "Houston", "Phoenix"], + } + df = DataChain.from_pandas(pd.DataFrame(data)).to_pandas() + assert all(col not in df.columns for col in data) + assert all(col.lower() in df.columns for col in data) + + +def test_pandas_incorrect_column_names(catalog): + with pytest.raises(DataChainParamsError): + DataChain.from_pandas( + pd.DataFrame({"First Name": ["Alice", "Bob", "Charlie", "David", "Eva"]}) + ) + + with pytest.raises(DataChainParamsError): + DataChain.from_pandas( + pd.DataFrame({"": ["Alice", "Bob", "Charlie", "David", "Eva"]}) + ) + + with pytest.raises(DataChainParamsError): + DataChain.from_pandas( + pd.DataFrame({"First@Name": ["Alice", "Bob", "Charlie", "David", "Eva"]}) + ) + + +def test_from_features_basic(catalog): + ds = DataChain.create_empty(DataChain.DEFAULT_FILE_RECORD) + ds = ds.gen(lambda prm: [File(name="")] * 5, params="parent", output={"file": File}) + + ds_name = "my_ds" + ds.save(ds_name) + ds = DataChain(name=ds_name) + + assert isinstance(ds.feature_schema, dict) + assert isinstance(ds.signals_schema, SignalSchema) + assert ds.schema.keys() == {"file"} + assert set(ds.schema.values()) == {File} + + +def test_from_features(catalog): + ds = DataChain.create_empty(DataChain.DEFAULT_FILE_RECORD) + ds = ds.gen( + lambda prm: list(zip([File(name="")] * len(features), features)), + params="parent", + output={"file": File, "t1": MyFr}, + ) + df1 = ds.to_pandas() + + assert df1[["t1.nnn", "t1.count"]].equals( + pd.DataFrame({"t1.nnn": ["n1", "n2", "n1"], "t1.count": [3, 5, 1]}) + ) + + +def test_preserve_feature_schema(catalog): + ds = DataChain.create_empty(DataChain.DEFAULT_FILE_RECORD) + ds = ds.gen( + lambda prm: list(zip([File(name="")] * len(features), features, features)), + params="parent", + output={"file": File, "t1": MyFr, "t2": MyFr}, + ) + + ds_name = "my_ds1" + ds.save(ds_name) + ds = DataChain(name=ds_name) + + assert isinstance(ds.feature_schema, dict) + assert isinstance(ds.signals_schema, SignalSchema) + assert ds.schema.keys() == {"t1", "t2", "file"} + assert set(ds.schema.values()) == {MyFr, File} + + +def test_from_features_simple_types(catalog): + fib = [1, 1, 2, 3, 5, 8] + values = ["odd" if num % 2 else "even" for num in fib] + + ds = DataChain.from_features(fib=fib, odds=values) + + df = ds.to_pandas() + assert len(df) == len(fib) + assert df["fib"].tolist() == fib + assert df["odds"].tolist() == values + + +def test_from_features_more_simple_types(catalog): + ds_name = "my_ds_type" + DataChain.from_features( + t1=features, + num=range(len(features)), + bb=[True, True, False], + dd=[{}, {"ee": 3}, {"ww": 1, "qq": 2}], + time=[ + datetime.datetime.now(), + datetime.datetime.today(), + datetime.datetime.today(), + ], + f=[3.14, 2.72, 1.62], + ).save(ds_name) + + ds = DataChain(name=ds_name) + assert ds.schema.keys() == { + "t1", + "num", + "bb", + "dd", + "time", + "f", + } + assert set(ds.schema.values()) == { + MyFr, + int, + bool, + dict, + datetime.datetime, + float, + } + + +def test_file_list(catalog): + names = ["f1.jpg", "f1.json", "f1.txt", "f2.jpg", "f2.json"] + sizes = [1, 2, 3, 4, 5] + files = [File(name=name, size=size) for name, size in zip(names, sizes)] + + ds = DataChain.from_features(file=files) + + for i, values in enumerate(ds.iterate()): + assert values[0] == files[i] + + +def test_gen(catalog): + class _TestFr(Feature): + file: File + sqrt: float + my_name: str + + ds = DataChain.from_features(t1=features) + ds = ds.gen( + x=lambda m_fr: [ + _TestFr( + file=File(name=""), + sqrt=math.sqrt(m_fr.count), + my_name=m_fr.nnn, + ) + ], + params="t1", + output={"x": _TestFr}, + ) + + df = ds.to_pandas() + + assert df["x.my_name"].tolist() == ["n1", "n2", "n1"] + assert np.allclose(df["x.sqrt"], [math.sqrt(x) for x in [3, 5, 1]]) + with pytest.raises(KeyError): + df["x.t1.nnn"] + + +def test_map(catalog): + class _TestFr(Feature): + sqrt: float + my_name: str + + ds = DataChain.from_features(t1=features) + + df = ds.map( + x=lambda m_fr: _TestFr( + sqrt=math.sqrt(m_fr.count), + my_name=m_fr.nnn + "_suf", + ), + params="t1", + output={"x": _TestFr}, + ).to_pandas() + + assert df["x.my_name"].tolist() == ["n1_suf", "n2_suf", "n1_suf"] + assert np.allclose(df["x.sqrt"], [math.sqrt(x) for x in [3, 5, 1]]) + + +def test_agg(catalog): + class _TestFr(Feature): + f: File + cnt: int + my_name: str + + df = ( + DataChain.from_features(t1=features) + .agg( + x=lambda frs: [ + _TestFr( + f=File(name=""), + cnt=sum(f.count for f in frs), + my_name="-".join([fr.nnn for fr in frs]), + ) + ], + partition_by=C.t1.nnn, + params="t1", + output={"x": _TestFr}, + ) + .to_pandas() + ) + + assert len(df) == 2 + assert df["x.my_name"].tolist() == ["n1-n1", "n2"] + assert df["x.cnt"].tolist() == [4, 5] + + +def test_agg_two_params(catalog): + class _TestFr(Feature): + f: File + cnt: int + my_name: str + + features2 = [ + MyFr(nnn="n1", count=6), + MyFr(nnn="n2", count=10), + MyFr(nnn="n1", count=2), + ] + + ds = DataChain.from_features(t1=features, t2=features2).agg( + x=lambda frs1, frs2: [ + _TestFr( + f=File(name=""), + cnt=sum(f1.count + f2.count for f1, f2 in zip(frs1, frs2)), + my_name="-".join([fr.nnn for fr in frs1]), + ) + ], + partition_by=C.t1.nnn, + params=("t1", "t2"), + output={"x": _TestFr}, + ) + + df = ds.to_pandas() + assert len(df) == 2 + assert df["x.my_name"].tolist() == ["n1-n1", "n2"] + assert df["x.cnt"].tolist() == [12, 15] + + +def test_agg_simple_iterator(catalog): + def func(key, val) -> Iterator[tuple[File, str]]: + for i in range(val): + yield File(name=""), f"{key}_{i}" + + keys = ["a", "b", "c"] + values = [3, 1, 2] + ds = DataChain.from_features(key=keys, val=values).gen(res=func) + + df = ds.to_pandas() + res = df["res_1"].tolist() + assert res == ["a_0", "a_1", "a_2", "b_0", "c_0", "c_1"] + + +def test_agg_simple_iterator_error(catalog): + chain = DataChain.from_features(key=["a", "b", "c"]) + + with pytest.raises(UdfSignatureError): + + def func(key) -> int: + return 1 + + chain.gen(res=func) + + with pytest.raises(UdfSignatureError): + + class _MyCls(Feature): + x: int + + def func(key) -> _MyCls: # type: ignore[misc] + return _MyCls(x=2) + + chain.gen(res=func) + + with pytest.raises(UdfSignatureError): + + def func(key) -> tuple[File, str]: # type: ignore[misc] + yield None, "qq" + + chain.gen(res=func) + + +def test_agg_tuple_result_iterator(catalog): + class _ImageGroup(Feature): + name: str + size: int + + def func(key, val) -> Iterator[tuple[File, _ImageGroup]]: + n = "-".join(key) + v = sum(val) + yield File(name=n), _ImageGroup(name=n, size=v) + + keys = ["n1", "n2", "n1"] + values = [1, 5, 9] + ds = DataChain.from_features(key=keys, val=values).agg( + x=func, partition_by=C("key") + ) + + df = ds.to_pandas() + assert len(df) == 2 + assert df["x_1.name"].tolist() == ["n1-n1", "n2"] + assert df["x_1.size"].tolist() == [10, 5] + + +def test_agg_tuple_result_generator(catalog): + class _ImageGroup(Feature): + name: str + size: int + + def func(key, val) -> Generator[tuple[File, _ImageGroup], None, None]: + n = "-".join(key) + v = sum(val) + yield File(name=n), _ImageGroup(name=n, size=v) + + keys = ["n1", "n2", "n1"] + values = [1, 5, 9] + ds = DataChain.from_features(key=keys, val=values).agg( + x=func, partition_by=C("key") + ) + + df = ds.to_pandas() + assert len(df) == 2 + assert df["x_1.name"].tolist() == ["n1-n1", "n2"] + assert df["x_1.size"].tolist() == [10, 5] + + +def test_iterate(catalog): + dc = DataChain.from_features(f1=features, num=range(len(features))) + + n = 0 + for sample in dc.iterate(): + assert len(sample) == 2 + fr, num = sample + + assert isinstance(fr, MyFr) + assert isinstance(num, int) + assert num == n + assert fr == features[n] + + n += 1 + + assert n == len(features) + + +def test_iterate_nested_feature(catalog): + dc = DataChain.from_features(sign1=features_nested) + + for n, sample in enumerate(dc.iterate()): + assert len(sample) == 1 + nested = sample[0] + + assert isinstance(nested, MyNested) + assert nested == features_nested[n] + + +def test_select_feature(catalog): + dc = DataChain.from_features(my_n=features_nested) + + samples = dc.select("my_n").iterate() + n = 0 + for sample in samples: + assert sample[0] == features_nested[n] + n += 1 + assert n == len(features_nested) + + samples = dc.select("my_n.fr").iterate() + n = 0 + for sample in samples: + assert sample[0] == features[n] + n += 1 + assert n == len(features_nested) + + samples = dc.select("my_n.label", "my_n.fr.count").iterate() + n = 0 + for sample in samples: + label, count = sample + assert label == features_nested[n].label + assert count == features_nested[n].fr.count + n += 1 + assert n == len(features_nested) + + +def test_select_columns_intersection(catalog): + dc = DataChain.from_features(my_n=features_nested) + + samples = dc.select("my_n.fr", "my_n.fr.count").iterate() + n = 0 + for sample in samples: + fr, count = sample + assert fr == features_nested[n].fr + assert count == features_nested[n].fr.count + n += 1 + assert n == len(features_nested) + + +def test_select_except(catalog): + dc = DataChain.from_features(fr1=features_nested, fr2=features) + + samples = dc.select_except("fr2").iterate() + n = 0 + for sample in samples: + fr = sample[0] + assert fr == features_nested[n] + n += 1 + assert n == len(features_nested) + + +def test_select_wrong_type(catalog): + dc = DataChain.from_features(fr1=features_nested, fr2=features) + + with pytest.raises(SignalResolvingTypeError): + list(dc.select(4).iterate()) + + with pytest.raises(SignalResolvingTypeError): + list(dc.select_except(features[0]).iterate()) + + +def test_select_except_error(catalog): + dc = DataChain.from_features(fr1=features_nested, fr2=features) + + with pytest.raises(SignalResolvingError): + list(dc.select_except("not_exist", "file").iterate()) + + with pytest.raises(SignalResolvingError): + list(dc.select_except("fr1.label", "file").iterate()) + + +def test_select_restore_from_saving(catalog): + dc = DataChain.from_features(my_n=features_nested) + + name = "test_test_select_save" + dc.select("my_n.fr").save(name) + + restored = DataChain.from_dataset(name) + n = 0 + restored_sorted = sorted(restored.iterate(), key=lambda x: x[0].count) + features_sorted = sorted(features, key=lambda x: x.count) + for sample in restored_sorted: + assert sample[0] == features_sorted[n] + n += 1 + assert n == len(features_nested) + + +def test_chain_of_maps(catalog): + dc = ( + DataChain.from_features(my_n=features_nested) + .map(full_name=lambda my_n: my_n.label + "-" + my_n.fr.nnn, output=str) + .map(square=lambda my_n: my_n.fr.count**2, output=int) + ) + + signals = ["my_n", "full_name", "square"] + assert len(dc.schema) == len(signals) + for signal in signals: + assert signal in dc.schema + + preserved = dc.save() + for signal in signals: + assert signal in preserved.schema + + +def test_vector(catalog): + vector = [3.14, 2.72, 1.62] + + def get_vector(key) -> list[float]: + return vector + + ds = DataChain.from_features(key=[123]).map(emd=get_vector) + + df = ds.to_pandas() + assert np.allclose(df["emd"].tolist()[0], vector) + + +def test_vector_of_vectors(catalog): + vector = [[3.14, 2.72, 1.62], [1.0, 2.0, 3.0]] + + def get_vector(key) -> list[list[float]]: + return vector + + ds = DataChain.from_features(key=[123]).map(emd_list=get_vector) + + df = ds.to_pandas() + actual = df["emd_list"].tolist()[0] + assert len(actual) == 2 + assert np.allclose(actual[0], vector[0]) + assert np.allclose(actual[1], vector[1]) + + +def test_unsupported_output_type(catalog): + vector = [3.14, 2.72, 1.62] + + def get_vector(key) -> list[np.float64]: + return [vector] + + with pytest.raises(TypeError): + DataChain.from_features(key=[123]).map(emd=get_vector) + + +def test_collect_one(catalog): + names = ["f1.jpg", "f1.json", "f1.txt", "f2.jpg", "f2.json"] + sizes = [1, 2, 3, 4, 5] + files = [File(name=name, size=size) for name, size in zip(names, sizes)] + + scores = [0.1, 0.2, 0.3, 0.4, 0.5] + + chain = DataChain.from_features(file=files, score=scores) + + assert chain.collect_one("file") == files + assert chain.collect_one("file.name") == names + assert chain.collect_one("file.size") == sizes + assert chain.collect_one("file.source") == [""] * len(names) + assert np.allclose(chain.collect_one("score"), scores) + + for actual, expected in zip( + chain.collect("file.size", "score"), [[x, y] for x, y in zip(sizes, scores)] + ): + assert len(actual) == 2 + assert actual[0] == expected[0] + assert math.isclose(actual[1], expected[1], rel_tol=1e-7) + + +def test_default_output_type(catalog): + names = ["f1.jpg", "f1.json", "f1.txt", "f2.jpg", "f2.json"] + suffix = "-new" + + chain = DataChain.from_features(name=names).map(res1=lambda name: name + suffix) + + assert chain.collect_one("res1") == [t + suffix for t in names] + + +def test_create_model(catalog): + chain = DataChain.from_features(name=["aaa", "b", "c"], count=[1, 4, 6]) + + cls = chain.create_model("TestModel") + assert isinstance(cls, type(Feature)) + + fields = {n: f_info.annotation for n, f_info in cls.model_fields.items()} + assert fields == {"name": str, "count": int} + + +def test_parse_tabular(tmp_dir, catalog): + df = pd.DataFrame(DF_DATA) + path = tmp_dir / "test.parquet" + df.to_parquet(path) + dc = DataChain.from_storage(path.as_uri()).parse_tabular() + df1 = dc.select("first_name", "age", "city").to_pandas() + + assert df1.equals(df) + + +def test_parse_tabular_format(tmp_dir, catalog): + df = pd.DataFrame(DF_DATA) + path = tmp_dir / "test.jsonl" + path.write_text(df.to_json(orient="records", lines=True)) + dc = DataChain.from_storage(path.as_uri()).parse_tabular(format="json") + df1 = dc.select("first_name", "age", "city").to_pandas() + assert df1.equals(df) + + +def test_parse_tabular_empty(tmp_dir, catalog): + path = tmp_dir / "test.parquet" + with pytest.raises(DataChainParamsError): + DataChain.from_storage(path.as_uri()).parse_tabular() + + +def test_parse_tabular_unify_schema(tmp_dir, catalog): + df1 = pd.DataFrame(DF_DATA) + df2 = pd.DataFrame(DF_OTHER_DATA) + path1 = tmp_dir / "df1.parquet" + path2 = tmp_dir / "df2.parquet" + df1.to_parquet(path1) + df2.to_parquet(path2) + df_combined = ( + pd.concat([df1, df2], ignore_index=True) + .replace({"": None, 0: None, np.nan: None}) + .sort_values("first_name") + .reset_index(drop=True) + ) + dc = ( + DataChain.from_storage(tmp_dir.as_uri()) + .filter(C("name").glob("*.parquet")) + .parse_tabular() + ) + df = dc.select("first_name", "age", "city", "last_name", "country").to_pandas() + df = ( + df.replace({"": None, 0: None, np.nan: None}) + .sort_values("first_name") + .reset_index(drop=True) + ) + assert df.equals(df_combined) + + +def test_parse_tabular_output(tmp_dir, catalog): + df = pd.DataFrame(DF_DATA) + path = tmp_dir / "test.jsonl" + path.write_text(df.to_json(orient="records", lines=True)) + output = {"fname": str, "age": int, "loc": str} + dc = DataChain.from_storage(path.as_uri()).parse_tabular( + format="json", output=output + ) + df1 = dc.select("fname", "age", "loc").to_pandas() + df.columns = ["fname", "age", "loc"] + assert df1.equals(df) + + +def test_parse_csv(tmp_dir, catalog): + df = pd.DataFrame(DF_DATA) + path = tmp_dir / "test.csv" + df.to_csv(path) + dc = DataChain.from_storage(path.as_uri()).parse_csv() + df1 = dc.select("first_name", "age", "city").to_pandas() + assert df1.equals(df) + + +def test_parse_csv_no_header_error(tmp_dir, catalog): + df = pd.DataFrame(DF_DATA.values()).transpose() + path = tmp_dir / "test.csv" + df.to_csv(path, header=False, index=False) + with pytest.raises(DataChainParamsError): + DataChain.from_storage(path.as_uri()).parse_csv(header=False) + + +def test_parse_csv_no_header_output(tmp_dir, catalog): + df = pd.DataFrame(DF_DATA.values()).transpose() + path = tmp_dir / "test.csv" + df.to_csv(path, header=False, index=False) + dc = DataChain.from_storage(path.as_uri()).parse_csv( + header=False, output={"first_name": str, "age": int, "city": str} + ) + df1 = dc.select("first_name", "age", "city").to_pandas() + assert (df1.values != df.values).sum() == 0 + + +def test_parse_csv_no_header_column_names(tmp_dir, catalog): + df = pd.DataFrame(DF_DATA.values()).transpose() + path = tmp_dir / "test.csv" + df.to_csv(path, header=False, index=False) + dc = DataChain.from_storage(path.as_uri()).parse_csv( + header=False, column_names=["first_name", "age", "city"] + ) + df1 = dc.select("first_name", "age", "city").to_pandas() + assert (df1.values != df.values).sum() == 0 + + +def test_parse_csv_column_names_and_output(tmp_dir, catalog): + df = pd.DataFrame(DF_DATA) + path = tmp_dir / "test.csv" + df.to_csv(path) + column_names = ["fname", "age", "loc"] + output = {"fname": str, "age": int, "loc": str} + with pytest.raises(DataChainParamsError): + DataChain.from_storage(path.as_uri()).parse_csv( + column_names=column_names, output=output + ) + + +def test_parse_csv_tab_delimited(tmp_dir, catalog): + df = pd.DataFrame(DF_DATA) + path = tmp_dir / "test.csv" + df.to_csv(path, sep="\t") + dc = DataChain.from_storage(path.as_uri()).parse_csv(delimiter="\t") + df1 = dc.select("first_name", "age", "city").to_pandas() + assert df1.equals(df) + + +def test_parse_parquet_partitioned(tmp_dir, catalog): + df = pd.DataFrame(DF_DATA) + path = tmp_dir / "test.parquet" + df.to_parquet(path, partition_cols=["first_name"]) + dc = DataChain.from_storage(path.as_uri()).parse_parquet() + df1 = dc.select("first_name", "age", "city").to_pandas() + df1 = df1.sort_values("first_name").reset_index(drop=True) + assert df1.equals(df) + + +def test_parse_parquet_filter_partitions(tmp_dir, catalog): + df = pd.DataFrame(DF_DATA) + path = tmp_dir / "test.parquet" + df.to_parquet(path, partition_cols=["first_name"]) + dc = ( + DataChain.from_storage(path.as_uri()) + .filter(C("parent").glob("*first_name=Alice*")) + .parse_parquet() + ) + df1 = dc.select("first_name", "age", "city").to_pandas() + df1 = df1.sort_values("first_name").reset_index(drop=True) + assert df1.equals(df.loc[:0]) + + +@pytest.mark.parametrize("processes", [False, 2, True]) +def test_parallel(processes, catalog): + prefix = "t & " + vals = ["a", "b", "c", "d", "e", "f", "g", "h", "i"] + + res = ( + DataChain.from_features(key=vals) + .settings(parallel=processes) + .map(res=lambda key: prefix + key) + .collect_one("res") + ) + + assert res == [prefix + v for v in vals] + + +def test_exec(catalog): + names = ("f1.jpg", "f1.json", "f1.txt", "f2.jpg", "f2.json") + all_names = set() + + dc = ( + DataChain.from_features(name=names) + .map(nop=lambda name: all_names.add(name)) + .exec() + ) + assert isinstance(dc, DataChain) + assert all_names == set(names) + + +def test_extend_features(catalog): + dc = DataChain.from_features(f1=features, num=range(len(features))) + + res = dc._extend_features("select", "num") + assert isinstance(res, DataChain) + assert res.signals_schema.values == {"num": int} + + res = dc._extend_features("sum", "num") + assert res == sum(range(len(features))) diff --git a/tests/unit/lib/test_datachain_bootstrap.py b/tests/unit/lib/test_datachain_bootstrap.py new file mode 100644 index 000000000..349f71634 --- /dev/null +++ b/tests/unit/lib/test_datachain_bootstrap.py @@ -0,0 +1,92 @@ +import pytest + +from datachain.lib.dc import DataChain, DatasetPrepareError +from datachain.lib.udf import Mapper + + +class MyMapper(Mapper): + DEFAULT_VALUE = 84 + BOOTSTRAP_VALUE = 1452 + TEARDOWN_VALUE = 98763 + + def __init__(self): + super().__init__() + self.value = MyMapper.DEFAULT_VALUE + self._had_teardown = False + + def process(self, *args) -> int: + return self.value + + def setup(self): + self.value = MyMapper.BOOTSTRAP_VALUE + + def teardown(self): + self.value = MyMapper.TEARDOWN_VALUE + + +def test_udf(catalog): + vals = ["a", "b", "c", "d", "e", "f"] + chain = DataChain.from_features(key=vals) + + udf = MyMapper() + res = chain.map(res=udf).collect_one("res") + + assert res == [MyMapper.BOOTSTRAP_VALUE] * len(vals) + assert udf.value == MyMapper.TEARDOWN_VALUE + + +@pytest.mark.skip(reason="Skip until tests module will be importer for unit-tests") +def test_udf_parallel(catalog): + vals = ["a", "b", "c", "d", "e", "f"] + chain = DataChain.from_features(key=vals) + + res = chain.settings(parallel=4).map(res=MyMapper()).collect_one("res") + + assert res == [MyMapper.BOOTSTRAP_VALUE] * len(vals) + + +def test_no_bootstrap_for_callable(catalog): + class MyMapper: + def __init__(self): + self._had_bootstrap = False + self._had_teardown = False + + def __call__(self, *args): + return None + + def bootstrap(self): + self._had_bootstrap = True + + def teardown(self): + self._had_teardown = True + + udf = MyMapper() + + chain = DataChain.from_features(key=["a", "b", "c"]) + chain.map(res=udf).collect() + + assert udf._had_bootstrap is False + assert udf._had_teardown is False + + +def test_bootstrap_in_chain(): + base = 1278 + prime = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29] + + res = ( + DataChain.from_features(val=prime) + .setup(init_val=lambda: base) + .map(x=lambda val, init_val: val + init_val, output=int) + .collect_one("x") + ) + + assert res == [base + val for val in prime] + + +def test_vars_duplication_error(): + with pytest.raises(DatasetPrepareError): + ( + DataChain.from_features(val=[2, 3, 5, 7, 11, 13, 17, 19, 23, 29]) + .setup(init_val=lambda: 11, connection=lambda: 123) + .setup(init_val=lambda: 599) + ) diff --git a/tests/unit/lib/test_datachain_merge.py b/tests/unit/lib/test_datachain_merge.py new file mode 100644 index 000000000..668737652 --- /dev/null +++ b/tests/unit/lib/test_datachain_merge.py @@ -0,0 +1,198 @@ +import math +from typing import Optional + +import pytest + +from datachain.lib.dc import DataChain, DatasetMergeError +from datachain.lib.feature import Feature +from datachain.lib.signal_schema import SignalResolvingError +from datachain.sql.types import Float, String + + +class TestUser(Feature): + name: Optional[str] = None + age: Optional[int] = None + + +class TestPlayer(TestUser): + weight: Optional[float] = None + height: Optional[int] = None + + +class TestEmployee(Feature): + id: Optional[int] = None + person: TestUser + + +class TestTeamMember(Feature): + player: Optional[str] = None + sport: Optional[str] = None + weight: Optional[float] = None + height: Optional[float] = None + + +employees = [ + TestEmployee(id=151, person=TestUser(name="Alice", age=31)), + TestEmployee(id=152, person=TestUser(name="Bob", age=27)), + TestEmployee(id=153, person=TestUser(name="Charlie", age=54)), + TestEmployee(id=154, person=TestUser(name="David", age=29)), +] +team = [ + TestTeamMember(player="Alice", sport="volleyball", weight=120.3, height=5.5), + TestTeamMember(player="Charlie", sport="football", weight=200.0, height=6.0), + TestTeamMember(player="David", sport="football", weight=158.7, height=5.7), +] + + +def test_merge_objects(catalog): + ch1 = DataChain.from_features(emp=employees) + ch2 = DataChain.from_features(team=team) + ch = ch1.merge(ch2, "emp.person.name", "team.player") + + str_default = String.default_value(catalog.warehouse.db.dialect) + float_default = Float.default_value(catalog.warehouse.db.dialect) + + i = 0 + j = 0 + for items in ch.iterate(): + assert len(items) == 2 + + empl, player = items + assert isinstance(empl, TestEmployee) + assert empl == employees[i] + i += 1 + + assert isinstance(player, TestTeamMember) + if empl.person.name != "Bob": + assert player.player == team[j].player + assert player.sport == team[j].sport + assert math.isclose(player.weight, team[j].weight, rel_tol=1e-7) + assert math.isclose(player.height, team[j].height, rel_tol=1e-7) + j += 1 + else: + assert player.player == str_default + assert player.sport == str_default + assert player.weight == float_default + assert player.height == float_default + + assert i == len(employees) + assert j == len(team) + + +def test_merge_similar_objects(catalog): + new_employees = [ + TestEmployee(id=152, person=TestUser(name="Bob", age=27)), + TestEmployee(id=201, person=TestUser(name="Karl", age=18)), + TestEmployee(id=154, person=TestUser(name="David", age=29)), + ] + + ch1 = DataChain.from_features(emp=employees) + ch2 = DataChain.from_features(emp=new_employees) + + rname = "qq" + ch = ch1.merge(ch2, "emp.person.name", rname=rname) + + assert list(ch.signals_schema.values.keys()) == ["emp", rname + "emp"] + + empl = list(ch.iterate()) + assert len(empl) == 4 + assert len(empl[0]) == 2 + + ch_inner = ch1.merge(ch2, "emp.person.name", rname=rname, inner=True) + assert len(list(ch_inner.iterate())) == 2 + + +def test_merge_values(catalog): + order_ids = [11, 22, 33, 44] + order_descr = ["water", "water", "paper", "water"] + + delivery_ids = [11, 44] + delivery_time = [24.0, 16.5] + + float_default = Float.default_value(catalog.warehouse.db.dialect) + + ch1 = DataChain.from_features(id=order_ids, descr=order_descr) + ch2 = DataChain.from_features(id=delivery_ids, time=delivery_time) + + ch = ch1.merge(ch2, "id") + + assert list(ch.signals_schema.values.keys()) == ["id", "descr", "right_id", "time"] + + i = 0 + j = 0 + sorted_items_list = sorted(ch.iterate(), key=lambda x: x[0]) + for items in sorted_items_list: + assert len(items) == 4 + id, name, _right_id, time = items + + assert id == order_ids[i] + assert name == order_descr[i] + i += 1 + + if time != float_default: + assert id == delivery_ids[j] + assert time == delivery_time[j] + j += 1 + + assert i == len(order_ids) + assert j == len(delivery_ids) + + +def test_merge_multi_conditions(catalog): + order_ids = [11, 22, 33, 44] + order_name = ["water", "water", "paper", "water"] + order_descr = ["still water", "still water", "white paper", "sparkling water"] + + delivery_ids = [11, 44] + delivery_name = ["water", "unknown"] + delivery_time = [24.0, 16.5] + + ch1 = DataChain.from_features(id=order_ids, name=order_name, descr=order_descr) + ch2 = DataChain.from_features( + id=delivery_ids, d_name=delivery_name, time=delivery_time + ) + + ch = ch1.merge(ch2, ("id", "name"), ("id", "d_name")) + + res = list(ch.iterate()) + + assert len(res) == max(len(employees), len(team)) + success_ids = set() + for items in res: + if items[3]: + success_ids.add(items[0]) + + assert success_ids == {11} + + +def test_merge_errors(catalog): + ch1 = DataChain.from_features(emp=employees) + ch2 = DataChain.from_features(team=team) + + with pytest.raises(SignalResolvingError): + ch1.merge(ch2, "unknown") + + ch1.merge(ch2, ["emp.person.name"], ["team.sport"]) + ch1.merge(ch2, ["emp.person.name"], ["team.sport"]) + with pytest.raises(DatasetMergeError): + ch1.merge(ch2, ["emp.person.name"], ["team.player", "team.sport"]) + + with pytest.raises(DatasetMergeError): + ch1.merge(ch2, 33) + + with pytest.raises(DatasetMergeError): + ch1.merge(ch2, "emp.person.name", True) + + +def test_merge_with_itself(catalog): + ch = DataChain.from_features(emp=employees) + merged = ch.merge(ch, "emp.id") + + count = 0 + for left, right in merged.iterate(): + assert isinstance(left, TestEmployee) + assert isinstance(right, TestEmployee) + assert left == right == employees[count] + count += 1 + + assert count == len(employees) diff --git a/tests/unit/lib/test_feature.py b/tests/unit/lib/test_feature.py new file mode 100644 index 000000000..cca69491c --- /dev/null +++ b/tests/unit/lib/test_feature.py @@ -0,0 +1,374 @@ +from typing import ClassVar, Literal, Optional + +import pytest +from pydantic import BaseModel, Field, ValidationError + +from datachain.lib.feature import Feature +from datachain.lib.feature_registry import Registry +from datachain.lib.feature_utils import pydantic_to_feature +from datachain.lib.signal_schema import SignalSchema +from datachain.sql.types import ( + Array, + Int64, + String, +) + + +class FileBasic(Feature): + parent: str = Field(default="") + name: str + size: int = Field(default=0) + + +class TestFileInfo(FileBasic): + location: dict = Field(default={}) + + +class FileInfoEx(Feature): + f_info: TestFileInfo + type_id: int + + +class MyNestedClass(Feature): + type: int + Name: str = Field(default="test1") + + +class MyTest(Feature): + ThisIsName: str + subClass: MyNestedClass # noqa: N815 + + +def test_flatten_basic(): + vals = FileBasic(parent="hello", name="world", size=123)._flatten() + assert vals == ("hello", "world", 123) + + +def test_flatten_with_json(): + t1 = TestFileInfo(parent="prt4", name="test1", size=42, location={"ee": "rr"}) + assert t1._flatten() == ("prt4", "test1", 42, {"ee": "rr"}) + + +def test_flatten_with_empty_json(): + with pytest.raises(ValidationError): + TestFileInfo(parent="prt4", name="test1", size=42, location=None) + + +def test_flatten_with_accepted_empty_json(): + class _Test(Feature): + d: Optional[dict] + + assert _Test(d=None)._flatten() == (None,) + + +def test_flatten_nested(): + t0 = TestFileInfo(parent="sfo", name="sf", size=567, location={"42": 999}) + t1 = FileInfoEx(f_info=t0, type_id=1849) + + assert t1._flatten() == ("sfo", "sf", 567, {"42": 999}, 1849) + + +def test_flatten_list(): + t1 = TestFileInfo(parent="p1", name="n4", size=3, location={"a": "b"}) + t2 = TestFileInfo(parent="p2", name="n5", size=2, location={"c": "d"}) + + vals = t1._flatten_list([t1, t2]) + assert vals == ("p1", "n4", 3, {"a": "b"}, "p2", "n5", 2, {"c": "d"}) + + +def test_registry(): + class MyTestRndmz(Feature): + name: str + count: int + + assert Registry.get(MyTestRndmz.__name__) == MyTestRndmz + assert Registry.get(MyTestRndmz.__name__, version=1) == MyTestRndmz + Registry.remove(MyTestRndmz) + + +def test_registry_versioned(): + class MyTestXYZ(Feature): + _version: ClassVar[int] = 42 + name: str + count: int + + assert Registry.get(MyTestXYZ.__name__) == MyTestXYZ + assert Registry.get(MyTestXYZ.__name__, version=1) is None + assert Registry.get(MyTestXYZ.__name__, version=42) == MyTestXYZ + Registry.remove(MyTestXYZ) + + +def test_inheritance(): + class SubObject(Feature): + subname: str + + class SoMyTest1(Feature): + name: str + sub: SubObject + + class SoMyTest2(SoMyTest1): + pass + + try: + with pytest.raises(ValueError): + SoMyTest2() + + obj = SoMyTest2(name="name", sub=SubObject(subname="subname")) + assert obj._flatten() == ("name", "subname") + finally: + Registry.remove(SubObject) + Registry.remove(SoMyTest1) + Registry.remove(SoMyTest2) + + +def test_delimiter_in_name(): + with pytest.raises(RuntimeError): + + class _MyClass(Feature): + var__name: str + + +def test_deserialize_nested(): + class Child(Feature): + type: int + name: str = Field(default="test1") + + class Parent(Feature): + name: str + child: Child + + in_db_map = { + "name": "a1", + "child__type": 42, + "child__name": "a2", + } + + p = Parent._unflatten(in_db_map) + + assert p.name == "a1" + assert p.child.type == 42 + assert p.child.name == "a2" + + +def test_deserialize_nested_with_name_normalization(): + class ChildClass(Feature): + type: int + name: str = Field(default="test1") + + class Parent2(Feature): + name: str + childClass11: ChildClass # noqa: N815 + + in_db_map = { + "name": "name1", + "child_class11__type": 12, + "child_class11__name": "n2", + } + + p = Parent2._unflatten(in_db_map) + + assert p.name == "name1" + assert p.childClass11.type == 12 + assert p.childClass11.name == "n2" + + +def test_type_array_of_floats(): + class _Test(Feature): + d: list[float] + + dict_ = {"d": [1, 3, 5]} + t = _Test(**dict_) + assert t.d == [1, 3, 5] + + +def test_class_attr_resolver_basic(): + class _MyTest(Feature): + val1: list[float] + pp: int + + assert _MyTest.val1.name == "val1" + assert _MyTest.pp.name == "pp" + assert isinstance(_MyTest.pp.type, Int64) + assert isinstance(_MyTest.val1.type, Array) + + +def test_class_attr_resolver_shallow(): + class _MyTest(Feature): + val1: list[float] + pp: int + + assert _MyTest.val1.name == "val1" + assert _MyTest.pp.name == "pp" + assert isinstance(_MyTest.pp.type, Int64) + assert isinstance(_MyTest.val1.type, Array) + + +def test_class_attr_resolver_nested(): + assert MyTest.subClass.type.name == "sub_class__type" + assert MyTest.subClass.Name.name == "sub_class__name" + assert isinstance(MyTest.subClass.type.type, Int64) + assert isinstance(MyTest.subClass.Name.type, String) + + +def test_class_attr_resolver_nested_3levels(): + class _MyTest1(Feature): + a: int + + class _MyTest2(Feature): + b: _MyTest1 + + class _MyTest3(Feature): + c: _MyTest2 + + assert _MyTest3.c.b.a.name == "c__b__a" + assert isinstance(_MyTest3.c.b.a.type, Int64) + + +def test_class_attr_resolver_partial(): + class _MyTest1(Feature): + a: str + + class _MyTest2(Feature): + b: _MyTest1 + + class _MyTest3(Feature): + c: _MyTest2 + + assert _MyTest3.c.b.name == "c__b" + + +def test_pydantic_to_feature(): + class _MyTextBlock(BaseModel): + id: int + type: Literal["text"] + + cls = pydantic_to_feature(_MyTextBlock) + assert Feature.is_feature(cls) + + spec = SignalSchema({"val": cls}).to_udf_spec() + assert list(spec.keys()) == ["val__id", "val__type"] + assert list(spec.values()) == [Int64, String] + + +def test_pydantic_to_feature_nested(): + class _MyTextBlock(BaseModel): + id: int + type: Literal["text"] + + class _MyMessage3(BaseModel): + val1: Optional[str] + val2: _MyTextBlock + + cls = pydantic_to_feature(_MyMessage3) + assert Feature.is_feature(cls) + + spec = SignalSchema({"val": cls}).to_udf_spec() + assert list(spec) == ["val__val1", "val__val2__id", "val__val2__type"] + assert list(spec.values()) == [String, Int64, String] + + +def test_unflatten_to_json(): + class _Child(Feature): + type: int + name: str = Field(default="test1") + + class _Parent(Feature): + name: str + child: _Child + + p = _Parent(name="parent1", child=_Child(type=12, name="child1")) + + flatten = p._flatten() + assert _Parent._unflatten_to_json(flatten) == { + "name": "parent1", + "child": {"type": 12, "name": "child1"}, + } + + +def test_unflatten_to_json_list(): + class _Child(Feature): + type: int + name: str = Field(default="test1") + + class _Parent(Feature): + name: str + children: list[_Child] + + p = _Parent( + name="parent1", + children=[_Child(type=12, name="child1"), _Child(type=13, name="child2")], + ) + + flatten = p._flatten() + json = _Parent._unflatten_to_json(flatten) + assert json == { + "name": "parent1", + "children": [{"type": 12, "name": "child1"}, {"type": 13, "name": "child2"}], + } + + +def test_unflatten_to_json_dict(): + class _Child(Feature): + type: int + address: str = Field(default="test1") + + class _Parent(Feature): + name: str + children: dict[str, _Child] + + p = _Parent( + name="parent1", + children={ + "child1": _Child(type=12, address="sf"), + "child2": _Child(type=13, address="nyc"), + }, + ) + + flatten = p._flatten() + json = _Parent._unflatten_to_json(flatten) + assert json == { + "name": "parent1", + "children": { + "child1": {"type": 12, "address": "sf"}, + "child2": {"type": 13, "address": "nyc"}, + }, + } + + +def test_unflatten_to_json_list_of_int(): + class _Child(Feature): + types: list[int] + name: str = Field(default="test1") + + child1 = _Child(name="n1", types=[14]) + assert _Child._unflatten_to_json(child1._flatten()) == {"name": "n1", "types": [14]} + + child2 = _Child(name="qwe", types=[1, 2, 3, 5]) + assert _Child._unflatten_to_json(child2._flatten()) == { + "name": "qwe", + "types": [1, 2, 3, 5], + } + + +def test_unflatten_to_json_list_of_lists(): + class _Child(Feature): + type: int + name: str = Field(default="test1") + + class _Parent(Feature): + name: str + children: list[_Child] + + class _Company(Feature): + name: str + parents: list[_Parent] + + p = _Company( + name="Co", + parents=[_Parent(name="parent1", children=[_Child(type=12, name="child1")])], + ) + + assert _Company._unflatten_to_json(p._flatten()) == { + "name": "Co", + "parents": [{"name": "parent1", "children": [{"type": 12, "name": "child1"}]}], + } diff --git a/tests/unit/lib/test_feature_utils.py b/tests/unit/lib/test_feature_utils.py new file mode 100644 index 000000000..4577847e5 --- /dev/null +++ b/tests/unit/lib/test_feature_utils.py @@ -0,0 +1,106 @@ +from typing import get_args, get_origin + +import pytest + +from datachain.lib.dc import DataChain +from datachain.lib.feature_utils import FeatureToTupleError, features_to_tuples +from datachain.query.schema import Column + + +def test_basic(): + fib = [1, 1, 2, 3, 5, 8] + values = ["odd" if num % 2 else "even" for num in fib] + + typ, _partition_by, vals = features_to_tuples(fib=fib, odds=values) + + assert get_origin(typ) is tuple + assert get_args(typ) == (int, str) + assert len(vals) == len(fib) + assert type(vals[0]) is tuple + assert len(vals[0]) == 2 + assert vals[0] == (1, "odd") + assert vals[-1] == (fib[-1], values[-1]) + + +def test_e2e(catalog): + fib = [1, 1, 2, 3, 5, 8] + values = ["odd" if num % 2 else "even" for num in fib] + + dc = DataChain.from_features(fib=fib, odds=values) + + vals = list(dc.iterate()) + lst1 = [item[0] for item in vals] + lst2 = [item[1] for item in vals] + + assert lst1 == fib + assert lst2 == values + + +def test_single_value(): + fib = [1, 1, 2, 3, 5, 8] + + typ, _partition_by, vals = features_to_tuples(fib=fib) + + assert typ is int + assert vals == fib + + +def test_single_e2e(catalog): + fib = [1, 1, 2, 3, 5, 8] + + dc = DataChain.from_features(fib=fib) + + vals = list(dc.iterate()) + flattened = [item for sublist in vals for item in sublist] + + assert flattened == fib + + +def test_not_array_value_error(): + with pytest.raises(FeatureToTupleError): + DataChain.from_features(value=True) + + +def test_empty_value_list_error(): + with pytest.raises(FeatureToTupleError): + DataChain.from_features(value=[]) + + +def test_features_length_missmatch(): + with pytest.raises(FeatureToTupleError): + DataChain.from_features(value1=[1, 2, 3], value2=[1, 2, 3, 4, 5]) + + +def test_unknown_output_type(): + with pytest.raises(FeatureToTupleError): + + class UnknownFrType: + def __init__(self, val): + self.val = val + + DataChain.from_features(value1=[UnknownFrType(1), UnknownFrType(23)]) + + +def test_output_type_missmatch(): + with pytest.raises(FeatureToTupleError): + DataChain.from_features(value1=[1, 2, 3], output={"res": str}) + + +def test_output_length_missmatch(): + with pytest.raises(FeatureToTupleError): + DataChain.from_features(value1=[1, 2, 3], output={"out1": int, "out2": int}) + + +def test_output_spec_wrong_type(): + with pytest.raises(FeatureToTupleError): + DataChain.from_features(value1=[1, 2, 3], output=123) + + +def test_resolve_column(): + signal = Column("hello.world.again") + assert signal.name == "hello__world__again" + + +def test_resolve_column_attr(): + signal = Column.hello.world.again + assert signal.name == "hello__world__again" diff --git a/tests/unit/lib/test_file.py b/tests/unit/lib/test_file.py new file mode 100644 index 000000000..2f367b171 --- /dev/null +++ b/tests/unit/lib/test_file.py @@ -0,0 +1,162 @@ +import json + +import pytest +from fsspec.implementations.local import LocalFileSystem + +from datachain.cache import UniqueId +from datachain.catalog import Catalog +from datachain.lib.file import File, TextFile + + +def test_uid_missing_location(): + name = "my_name" + vtype = "vt1" + + stream = File(name=name, vtype=vtype) + assert stream.get_uid() == UniqueId("", "", name, "", 0, vtype, None) + + +def test_uid_location(): + name = "na_me" + vtype = "some_random" + loc = {"e": 42} + + stream = File(name=name, vtype=vtype, location=loc) + assert stream.get_uid() == UniqueId("", "", name, "", 0, vtype, loc) + + +def test_file_stem(): + s = File(name=".file.jpg.txt") + assert s.get_file_stem() == ".file.jpg" + + +def test_file_ext(): + s = File(name=".file.jpg.txt") + assert s.get_file_ext() == "txt" + + +def test_file_suffix(): + s = File(name=".file.jpg.txt") + assert s.get_file_suffix() == ".txt" + + +def test_full_name(): + name = ".file.jpg.txt" + f = File(name=name) + assert f.get_full_name() == name + + parent = "dir1/dir2" + f = File(name=name, parent=parent) + assert f.get_full_name() == f"{parent}/{name}" + + +def test_cache_get_path(catalog: Catalog): + stream = File(name="test.txt1", source="s3://mybkt") + stream._set_stream(catalog) + + uid = stream.get_uid() + data = b"some data is heRe" + catalog.cache.store_data(uid, data) + + path = stream.get_local_path() + assert path is not None + + with open(path, mode="rb") as f: + assert f.read() == data + + +def test_read_binary_data(tmp_path, catalog: Catalog): + file_name = "myfile" + data = b"some\x00data\x00is\x48\x65\x6c\x57\x6f\x72\x6c\x64\xff\xffheRe" + + file_path = tmp_path / file_name + with open(file_path, "wb") as fd: + fd.write(data) + + file = File(name=file_name, source=f"file://{tmp_path}") + file._set_stream(catalog, False) + assert file.read() == data + + +def test_read_binary_data_as_text(tmp_path, catalog: Catalog): + file_name = "myfile43.txt" + data = b"some\x00data\x00is\x48\x65\x6c\x57\x6f\x72\x6c\x64\xff\xffheRe" + + file_path = tmp_path / file_name + with open(file_path, "wb") as fd: + fd.write(data) + + file = TextFile(name=file_name, source=f"file://{tmp_path}") + file._set_stream(catalog, False) + try: + x = file.read() + except UnicodeDecodeError: # Unix + pass + else: # Windows + assert x != data + + +def test_read_text_data(tmp_path, catalog: Catalog): + file_name = "myfile" + data = "this is a TexT data..." + + file_path = tmp_path / file_name + with open(file_path, "w") as fd: + fd.write(data) + + file = TextFile(name=file_name, source=f"file://{tmp_path}") + file._set_stream(catalog, True) + assert file.read() == data + + +def test_cache_get_path_without_cache(): + stream = File(name="test.txt1", source="s3://mybkt") + with pytest.raises(RuntimeError): + stream.get_local_path() + + +def test_json_from_string(): + d = {"e": 12} + + file = File(name="something", location=d) + assert file.location == d + + file = File(name="something", location=None) + assert file.location is None + + file = File(name="something", location="") + assert file.location is None + + file = File(name="something", location=json.dumps(d)) + assert file.location == d + + with pytest.raises(ValueError): + File(name="something", location="{not a json}") + + +def test_file_info_jsons(): + file = File(name="something", location="") + assert file.location is None + + d = {"e": 12} + file = File(name="something", location=json.dumps(d)) + assert file.location == d + + +def test_get_path_local(catalog): + file = File(name="file", parent="dir", source="file:///") + file._catalog = catalog + assert file.get_path().replace("\\", "/").strip("/") == "dir/file" + + +@pytest.mark.parametrize("cloud_type", ["s3"], indirect=True) +def test_get_path_cloud(cloud_test_catalog): + file = File(name="file", parent="dir", source="s3://") + file._catalog = cloud_test_catalog.catalog + assert file.get_path().strip("/") == "s3:///dir/file" + + +def test_get_fs(catalog): + file = File(name="file", parent="dir", source="file:///") + file._catalog = catalog + assert isinstance(file.get_fs(), LocalFileSystem) diff --git a/tests/unit/lib/test_image.py b/tests/unit/lib/test_image.py new file mode 100644 index 000000000..b4bca5b69 --- /dev/null +++ b/tests/unit/lib/test_image.py @@ -0,0 +1,65 @@ +from PIL import Image +from torch import Tensor +from torchvision.transforms import ToTensor +from transformers import CLIPImageProcessor + +from datachain.lib.image import ( + ImageFile, + convert_image, + convert_images, +) + +IMAGE = Image.new(mode="RGB", size=(64, 64)) + + +def test_convert_image(): + converted_img = convert_image( + IMAGE, + mode="RGBA", + size=(32, 32), + transform=ToTensor(), + ) + assert isinstance(converted_img, Tensor) + assert converted_img.size() == (4, 32, 32) + + +def test_convert_image_hf(tmp_path): + transform = CLIPImageProcessor.from_pretrained("openai/clip-vit-base-patch32") + converted_img = convert_image( + IMAGE, + transform=transform, + ) + assert isinstance(converted_img, Tensor) + + +def test_image_file(tmp_path, catalog): + file_name = "img.jpg" + file_path = tmp_path / file_name + + IMAGE.save(file_path) + + file = ImageFile(name=file_name, source=f"file://{tmp_path}") + file._set_stream(catalog, caching_enabled=False) + assert isinstance(file.get_value(), Image.Image) + + +def test_convert_images(tmp_path): + file1_name = "img1.jpg" + file1_path = tmp_path / file1_name + file2_name = "img2.jpg" + file2_path = tmp_path / file2_name + + img1 = Image.new(mode="RGB", size=(64, 64)) + img2 = Image.new(mode="RGB", size=(128, 128)) + img1.save(file1_path) + img2.save(file2_path) + images = [img1, img2] + + converted_img = convert_images( + images, + mode="RGBA", + size=(32, 32), + transform=ToTensor(), + ) + assert isinstance(converted_img, Tensor) + assert converted_img.size() == (2, 4, 32, 32) diff --git a/tests/unit/lib/test_signal_schema.py b/tests/unit/lib/test_signal_schema.py new file mode 100644 index 000000000..803e5a09b --- /dev/null +++ b/tests/unit/lib/test_signal_schema.py @@ -0,0 +1,299 @@ +import json +from typing import Optional, Union + +import pytest + +from datachain.lib.feature import Feature +from datachain.lib.file import File +from datachain.lib.signal_schema import ( + SetupError, + SignalResolvingError, + SignalSchema, + SignalSchemaError, +) +from datachain.sql.types import Float, Int64, String + + +@pytest.fixture +def nested_file_schema(): + class _MyFile(File): + ref: str + nested_file: File + + schema = {"name": str, "age": float, "f": File, "my_f": _MyFile} + + return SignalSchema(schema) + + +class MyType1(Feature): + aa: int + bb: str + + +class MyType2(Feature): + name: str + deep: MyType1 + + +def test_deserialize_basic(): + stored = {"name": "str", "count": "int", "file": "File@1"} + signals = SignalSchema.deserialize(stored) + + assert len(signals.values) == 3 + assert signals.values.keys() == stored.keys() + assert list(signals.values.values()) == [str, int, File] + + +def test_deserialize_error(): + SignalSchema.deserialize({}) + + with pytest.raises(SignalSchemaError): + SignalSchema.deserialize(json.dumps({"name": "str"})) + + with pytest.raises(SignalSchemaError): + SignalSchema.deserialize({"name": [1, 2, 3]}) + + with pytest.raises(SignalSchemaError): + SignalSchema.deserialize({"name": "unknown"}) + + +def test_serialize_basic(): + schema = { + "name": str, + "age": float, + "f": File, + } + signals = SignalSchema(schema).serialize() + + assert len(signals) == 3 + assert signals["name"] == "str" + assert signals["age"] == "float" + assert signals["f"] == "File@1" + + +def test_feature_schema_serialize_optional(): + schema = { + "name": Optional[str], + "feature": Optional[MyType1], + } + signals = SignalSchema(schema).serialize() + + assert len(signals) == 2 + assert signals["name"] == "str" + assert signals["feature"] == "MyType1" + + +def test_serialize_from_column(): + signals = SignalSchema.from_column_types({"age": Float, "name": String}).values + + assert len(signals) == 2 + assert signals["name"] is str + assert signals["age"] is float + + +def test_serialize_from_column_error(): + with pytest.raises(SignalSchemaError): + SignalSchema.from_column_types({"age": Float, "wrong_type": File}) + + +def test_to_udf_spec(): + signals = SignalSchema.deserialize( + { + "age": "float", + "address": "str", + "f": "File@1", + } + ) + + spec = SignalSchema.to_udf_spec(signals) + + assert len(spec) == 2 + len(File.model_fields) + + assert "age" in spec + assert spec["age"] == Float + + assert "address" in spec + assert spec["address"] == String + + assert "f__name" in spec + assert spec["f__name"] == String + + assert "f__size" in spec + assert spec["f__size"] == Int64 + + +def test_select(): + schema = SignalSchema.deserialize( + { + "age": "float", + "address": "str", + "f": "MyType1@1", + } + ) + + new = schema.resolve("age", "f.aa", "f.bb") + assert isinstance(new, SignalSchema) + + signals = new.values + assert len(signals) == 3 + assert {"age", "f.aa", "f.bb"} == signals.keys() + assert signals["age"] is float + assert signals["f.aa"] is int + assert signals["f.bb"] is str + + +def test_select_nested_names(): + schema = SignalSchema.deserialize( + { + "address": "str", + "fr": "MyType2@1", + } + ) + + fr_signals = schema.resolve("fr.deep").values + assert "fr.deep" in fr_signals + assert fr_signals["fr.deep"] == MyType1 + + basic_signals = schema.resolve("fr.deep.aa", "fr.deep.bb").values + assert "fr.deep.aa" in basic_signals + assert "fr.deep.bb" in basic_signals + assert basic_signals["fr.deep.aa"] is int + assert basic_signals["fr.deep.bb"] is str + + +def test_select_nested_errors(): + schema = SignalSchema.deserialize( + { + "address": "str", + "fr": "MyType2@1", + } + ) + + schema = schema.resolve("fr.deep.aa", "fr.deep.bb") + + with pytest.raises(SignalResolvingError): + schema.resolve("some_random") + + with pytest.raises(SignalResolvingError): + schema.resolve("fr") + + with pytest.raises(SignalResolvingError): + schema.resolve("fr.deep") + + with pytest.raises(SignalResolvingError): + schema.resolve("fr.deep.not_exist") + + +def test_get_file_signals_basic(): + schema = { + "name": str, + "age": float, + "f": File, + } + assert list(SignalSchema(schema).get_file_signals()) == ["f"] + + +def test_get_file_signals_nested(nested_file_schema): + files = list(nested_file_schema.get_file_signals()) + assert files == ["f", "my_f", "my_f.nested_file"] + + +def test_create_model(): + class MyFr(Feature): + count: int + + spec = {"name": str, "age": float, "fr": MyFr} + cls = SignalSchema(spec).create_model("TestModel") + + assert isinstance(cls, type(Feature)) + + res = {} + for k, f_info in cls.model_fields.items(): + res[k] = f_info.annotation + + assert res == spec + + +def test_build_tree(): + spec = {"name": str, "age": float, "fr": MyType2} + lst = list(SignalSchema(spec).get_flat_tree()) + + assert lst == [ + (["name"], str, False, 0), + (["age"], float, False, 0), + (["fr"], MyType2, True, 0), + (["fr", "name"], str, False, 1), + (["fr", "deep"], MyType1, True, 1), + (["fr", "deep", "aa"], int, False, 2), + (["fr", "deep", "bb"], str, False, 2), + ] + + +def test_print_types(): + mapping = { + int: "int", + float: "float", + MyType2: "MyType2", + Optional[MyType2]: "Union[MyType2, NoneType]", + Union[str, int]: "Union[str, int]", + Union[Optional[MyType2]]: "Union[MyType2, NoneType]", + list: "list", + list[Optional[bool]]: "list[Union[bool, NoneType]]", + dict: "dict", + dict[str, Optional[MyType1]]: "dict[str, Union[MyType1, NoneType]]", + } + + for t, v in mapping.items(): + assert SignalSchema._type_to_str(t) == v + + +def test_bd_signals(): + spec = {"name": str, "age": float, "fr": MyType2} + lst = list(SignalSchema(spec).db_signals()) + + assert lst == [ + "name", + "age", + "fr__name", + "fr__deep__aa", + "fr__deep__bb", + ] + + +def test_row_to_objs(): + spec = {"name": str, "age": float, "fr": MyType2} + schema = SignalSchema(spec) + + val = MyType2(name="Fred", deep=MyType1(aa=129, bb="qwe")) + row = ("myname", 12.5, *val._flatten()) + + res = schema.row_to_objs(row) + + assert res == ["myname", 12.5, val] + + +def test_row_to_objs_setup(): + spec = {"name": str, "age": float, "init_val": int, "fr": MyType2} + setup_value = 84635 + setup = {"init_val": lambda: setup_value} + schema = SignalSchema(spec, setup) + + val = MyType2(name="Fred", deep=MyType1(aa=129, bb="qwe")) + row = ("myname", 12.5, *val._flatten()) + + res = schema.row_to_objs(row) + assert res == ["myname", 12.5, setup_value, val] + + +def test_setup_not_callable(): + spec = {"name": str, "age": float, "init_val": int, "fr": MyType2} + setup_dict = {"init_val": "asdfd"} + with pytest.raises(SetupError): + SignalSchema(spec, setup_dict) + + +def test_slice(): + schema = {"name": str, "age": float, "address": str} + keys = ["age", "name"] + sliced = SignalSchema(schema).slice(keys) + assert list(sliced.values.items()) == [("age", float), ("name", str)] diff --git a/tests/unit/lib/test_text.py b/tests/unit/lib/test_text.py new file mode 100644 index 000000000..a19a6cebb --- /dev/null +++ b/tests/unit/lib/test_text.py @@ -0,0 +1,51 @@ +import open_clip +import torch +from transformers import CLIPModel, CLIPProcessor + +from datachain.lib.file import TextFile +from datachain.lib.text import convert_text + + +def test_convert_text(): + text = "thisismytext" + tokenizer_model = "ViT-B-32" + tokenizer = open_clip.get_tokenizer(tokenizer_model) + converted_text = convert_text(text, tokenizer=tokenizer) + assert isinstance(converted_text, torch.Tensor) + + tokenizer_kwargs = {"context_length": 100} + converted_text = convert_text( + text, tokenizer=tokenizer, tokenizer_kwargs=tokenizer_kwargs + ) + assert converted_text.size() == (1, 100) + + converted_text = convert_text( + text, tokenizer=tokenizer, tokenizer_kwargs=tokenizer_kwargs + ) + model, _, _ = open_clip.create_model_and_transforms(tokenizer_model) + converted_text = convert_text(text, tokenizer=tokenizer, encoder=model.encode_text) + assert converted_text.dtype == torch.float32 + + +def test_convert_text_hf(): + text = "thisismytext" + model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32") + processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32") + converted_text = convert_text( + text, tokenizer=processor.tokenizer, encoder=model.get_text_features + ) + assert converted_text.dtype == torch.float32 + + +def test_text_file_mapper(tmp_path, catalog): + file_name = "myfile" + text = "myText" + + file_path = tmp_path / file_name + with open(file_path, "w") as fd: + fd.write(text) + + file = TextFile(name=file_name, source=f"file://{tmp_path}") + file._set_stream(catalog, caching_enabled=False) + res = file.get_value() + assert res == text diff --git a/tests/unit/lib/test_udf_signature.py b/tests/unit/lib/test_udf_signature.py new file mode 100644 index 000000000..c209b0b98 --- /dev/null +++ b/tests/unit/lib/test_udf_signature.py @@ -0,0 +1,191 @@ +from collections.abc import Sequence +from typing import Callable, Optional, Union + +import pytest + +from datachain.lib.feature import Feature, FeatureType +from datachain.lib.file import File +from datachain.lib.udf import Mapper +from datachain.lib.udf_signature import UdfSignature, UdfSignatureError + + +def get_sign( + func: Optional[Callable] = None, + params: Union[None, str, Sequence[str]] = None, + output: Union[None, FeatureType, Sequence[str], dict[str, FeatureType]] = None, + **signal_map, +): + return UdfSignature.parse("test", signal_map, func, params, output, False) + + +def func_str(p1) -> str: + return "qwe" + + +def func_tuple(p1) -> tuple[Feature, str, int]: + return File(name="n1"), "qwe", 33 + + +def func_args(*args): + return 12345 + + +def test_basic(): + sign = get_sign(s1=func_str) + + assert sign.func == func_str + assert sign.params == ["p1"] + assert sign.output_schema.values == {"s1": str} + + +def test_basic_func(): + sign1 = get_sign(s1=func_str) + sign2 = get_sign(func_str, output="s1") + sign3 = get_sign(func_str, output=["s1"]) + sign4 = get_sign(s1=func_str, params="p1") + sign5 = get_sign(s1=func_str, params=["p1"]) + + assert sign1 == sign2 + assert sign1 == sign3 + assert sign1 == sign4 + assert sign1 == sign5 + + +def test_signature_overwrite(): + sign = get_sign(s1=func_str, output={"my_sign": int}, params="some_prm") + + assert sign.func == func_str + assert sign.params == ["some_prm"] + assert sign.output_schema.values == {"my_sign": int} + + +def test_output_feature(): + sign = get_sign(s1=func_str, output={"my_sign": File}) + + assert sign.output_schema.values == {"my_sign": File} + + +def test_output_as_value(): + sign = get_sign(s1=func_str, output="my_sign") + + assert sign.func == func_str + assert sign.params == ["p1"] + assert sign.output_schema.values == {"my_sign": str} + + +def test_output_as_list(): + sign = get_sign(s1=func_str, output=["my_sign"]) + + assert sign.func == func_str + assert sign.params == ["p1"] + assert sign.output_schema.values == {"my_sign": str} + + +def test_multi_outputs_not_supported_yet(): + sign = get_sign(s1=func_tuple, output=["o1", "o2", "o3"]) + + assert sign.output_schema.values == {"o1": Feature, "o2": str, "o3": int} + + +def test_multiple_signals_error(): + with pytest.raises(UdfSignatureError): + get_sign(my_out=func_tuple, my_out2=func_str) + + with pytest.raises(UdfSignatureError): + get_sign(func_tuple, my_out=func_str) + + +def test_no_outputs(): + with pytest.raises(UdfSignatureError): + get_sign(func_tuple) + + with pytest.raises(UdfSignatureError): + get_sign() + + +def test_tuple_output_number_mismatch(): + with pytest.raises(UdfSignatureError): + get_sign(func_tuple, output=["a1", "a2", "a3", "a4", "a5"]) + + +def test_no_params(): + with pytest.raises(UdfSignatureError): + get_sign(lambda: 4) + + +def test_func_with_args(): + sign = get_sign(func_args, params=["prm1", "prm2"], output={"res": int}) + assert sign.params == ["prm1", "prm2"] + + +def test_output_type_error(): + with pytest.raises(UdfSignatureError): + get_sign(func_str, output={"res": complex}) + + with pytest.raises(UdfSignatureError): + + class TestCls: + pass + + get_sign(func_str, output={"res": TestCls}) + + +def test_feature_to_tuple_string_as_default_type(): + sign = get_sign(val1=lambda file: "asd") + assert sign.output_schema.values == {"val1": str} + + +def test_callable_class(): + class MyTest: + def __call__(self, file, p2) -> float: + return 2.72 + + sign = get_sign(s1=MyTest()) + assert sign.output_schema.values == {"s1": float} + + +def test_not_callable(): + class MyTest: + def my_func(self, file, p2) -> float: + return 2.72 + + with pytest.raises(UdfSignatureError): + get_sign(s1=MyTest()) + + with pytest.raises(UdfSignatureError): + get_sign(s1=123) + + +def test_udf_class(): + class MyTest(Mapper): + def process(self, file, p2) -> int: + return 42 + + sign = get_sign(s1=MyTest()) + + assert sign.output_schema.values == {"s1": int} + assert sign.params == ["file", "p2"] + + +def test_udf_flatten_value(): + class MyTest(Mapper): + def process(self, file, pp) -> int: + return 42 + + sign = get_sign(MyTest(), output={"res1": int}) + + assert sign.output_schema.values == {"res1": int} + + +def test_udf_flatten_feature(): + class MyData(Feature): + text: str + count: int + + class MyTest(Mapper): + def process(self, file, pp) -> MyData: + return MyData(text="asdf", count=135) + + sign = get_sign(r1=MyTest()) + + assert sign.output_schema.values == {"r1": MyData} diff --git a/tests/unit/lib/test_utils.py b/tests/unit/lib/test_utils.py new file mode 100644 index 000000000..3ddc465d5 --- /dev/null +++ b/tests/unit/lib/test_utils.py @@ -0,0 +1,58 @@ +from collections.abc import Iterable, Mapping +from typing import Literal, Optional, Union + +import pytest +from pydantic import BaseModel + +from datachain.lib.feature import Feature, convert_type_to_datachain +from datachain.sql.types import JSON, Array, String + + +class MyModel(BaseModel): + val1: str + + +class MyFeature(Feature): + val1: str + + +@pytest.mark.parametrize( + "typ,expected", + ( + (str, String), + (String, String), + (Literal["text"], String), + (dict[str, int], JSON), + (Mapping[str, int], JSON), + (Optional[str], String), + (Union[dict, list[dict]], JSON), + ), +) +def test_convert_type_to_datachain(typ, expected): + assert convert_type_to_datachain(typ) == expected + + +@pytest.mark.parametrize( + "typ,expected", + ( + (list[str], Array(String())), + (Iterable[str], Array(String())), + (list[list[str]], Array(Array(String()))), + ), +) +def test_convert_type_to_datachain_array(typ, expected): + assert convert_type_to_datachain(typ).to_dict() == expected.to_dict() + + +@pytest.mark.parametrize( + "typ", + ( + Union[str, int], + list[Union[str, int]], + MyFeature, + MyModel, + ), +) +def test_convert_type_to_datachain_error(typ): + with pytest.raises(TypeError): + convert_type_to_datachain(typ) diff --git a/tests/unit/lib/test_webdataset.py b/tests/unit/lib/test_webdataset.py new file mode 100644 index 000000000..adc7252b8 --- /dev/null +++ b/tests/unit/lib/test_webdataset.py @@ -0,0 +1,152 @@ +from tarfile import TarInfo + +import pytest + +from datachain.lib.file import File +from datachain.lib.webdataset import ( + CoreFileDuplicationError, + CoreFileNotFoundError, + UnknownFileExtensionError, + get_tar_groups, +) +from datachain.lib.webdataset_laion import WDSLaion + + +class MockTarInfo(TarInfo): + def __init__(self, name, content=b"", size=0, offset=0): + super().__init__(name) + self._content = content.encode() if isinstance(content, str) else content + self.size = size + self.offset = offset + + def isfile(self): + return True + + +class MockTar: + def __init__(self, members: list[MockTarInfo]): + self._members = members + + def getmembers(self): + return self._members + + def extractfile(self, tar_info: MockTarInfo): + class TmpReader: + def read(self): + return tar_info._content + + return TmpReader() + + +def test_webdataset_basic(): + tar_file = File(name="nnn.tar") + tar = MockTar( + [ + MockTarInfo("01.jpg"), + MockTarInfo("01.json", b'{"uid": "5678"}'), + MockTarInfo("64.jpg"), + MockTarInfo("64.json", b"{}"), + ] + ) + + groups = list(get_tar_groups(tar_file, tar, ["jpg"], WDSLaion)) + + assert len(groups) == 2 + + laion01, laion64 = groups + + assert laion01.file.name == "01.jpg" + assert laion01.file.parent == tar_file.name + assert laion01.file.location is not None + assert isinstance(laion01.file.location, list) + assert len(laion01.file.location) > 0 + + parent = laion01.file.location[0].get("parent", None) + assert parent is not None + parent_file = File(**parent) + assert parent_file == tar_file + + assert laion64.file.name == "64.jpg" + assert laion64.file.parent == tar_file.name + + assert laion01.txt is None + assert laion01.json.uid == "5678" + + assert laion64.json.uid == "" + + +def test_webdataset_empty(): + stream = File(name="nnn.tar") + tar = MockTar([]) + + groups = list(get_tar_groups(stream, tar, ["jpg"], WDSLaion)) + + assert len(groups) == 0 + + +def test_webdataset_missing_core_files(): + stream = File(name="nnn.tar") + tar = MockTar( + [ + MockTarInfo("01.txt"), + MockTarInfo("01.json", b'{"uid": "5678"}'), + MockTarInfo("64.txt"), + MockTarInfo("64.json", b"{}"), + ] + ) + + with pytest.raises(CoreFileNotFoundError): + list(get_tar_groups(stream, tar, ["NONSENSE"], WDSLaion)) + + +def test_webdataset_single_file_per_group(): + stream = File(name="nnn.tar") + tar = MockTar( + [ + MockTarInfo("01.jpg"), + MockTarInfo("64.jpg"), + MockTarInfo("64.json", b"{}"), + MockTarInfo("03.jpg"), + ] + ) + + groups = list(get_tar_groups(stream, tar, ["jpg"], WDSLaion)) + assert len(groups) == 3 + + +def test_webdataset_multiple_core_extensions(): + stream = File(name="nnn.tar") + tar = MockTar( + [ + MockTarInfo("01.png"), + MockTarInfo("64.png"), + MockTarInfo("64.json", b"{}"), + MockTarInfo("03.jpg"), + ] + ) + + groups = list(get_tar_groups(stream, tar, ["jpg", "png"], WDSLaion)) + assert len(groups) == 3 + + +def test_webdataset_core_file_duplication(): + stream = File(name="nnn.tar") + tar = MockTar( + [ + MockTarInfo("01.png"), + MockTarInfo("01.jpg"), + MockTarInfo("64.json", b"{}"), + MockTarInfo("03.jpg"), + ] + ) + + with pytest.raises(CoreFileDuplicationError): + list(get_tar_groups(stream, tar, ["jpg", "png"], WDSLaion)) + + +def test_webdataset_unknown_file_type(): + stream = File(name="nnn.tar") + tar = MockTar([MockTarInfo("01.QQQQ")]) + + with pytest.raises(UnknownFileExtensionError): + list(get_tar_groups(stream, tar, ["jpg", "png"], WDSLaion)) diff --git a/tests/unit/sql/__init__.py b/tests/unit/sql/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/sql/sqlite/__init__.py b/tests/unit/sql/sqlite/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/sql/sqlite/test_utils.py b/tests/unit/sql/sqlite/test_utils.py new file mode 100644 index 000000000..7d09c9df7 --- /dev/null +++ b/tests/unit/sql/sqlite/test_utils.py @@ -0,0 +1,15 @@ +import pytest + +from datachain.sql.sqlite.base import functions_exist + + +@pytest.mark.parametrize( + "names,expected", + [ + (["sum", "abs", "upper"], True), + (["sum", "abs", "upper", "missing_func"], False), + ([], True), + ], +) +def test_functions_exist(names, expected): + assert functions_exist(names) == expected diff --git a/tests/unit/sql/test_array.py b/tests/unit/sql/test_array.py new file mode 100644 index 000000000..5448c82c6 --- /dev/null +++ b/tests/unit/sql/test_array.py @@ -0,0 +1,20 @@ +from datachain.sql import literal, select +from datachain.sql.functions import array, string + + +def test_length(warehouse): + query = select( + array.length(["abc", "def", "g", "hi"]), + array.length([3.0, 5.0, 1.0, 6.0, 1.0]), + array.length([[1, 2, 3], [4, 5, 6]]), + ) + result = tuple(warehouse.db.execute(query)) + assert result == ((4, 5, 2),) + + +def test_length_on_split(warehouse): + query = select( + array.length(string.split(literal("abc/def/g/hi"), literal("/"))), + ) + result = tuple(warehouse.db.execute(query)) + assert result == ((4,),) diff --git a/tests/unit/sql/test_conditional.py b/tests/unit/sql/test_conditional.py new file mode 100644 index 000000000..cae3f2433 --- /dev/null +++ b/tests/unit/sql/test_conditional.py @@ -0,0 +1,51 @@ +import pytest + +from datachain.sql import column, select, values +from datachain.sql import literal as lit +from datachain.sql.functions import greatest, least + + +@pytest.mark.parametrize( + "args,expected", + [ + ([lit("abc"), lit("bcd"), lit("Abc"), lit("cd")], "cd"), + ([3, 1, 2.0, 3.1, 2.5, -1], 3.1), + ([4], 4), + ], +) +def test_greatest(warehouse, args, expected): + query = select(greatest(*args)) + result = tuple(warehouse.db.execute(query)) + assert result == ((expected,),) + + +@pytest.mark.parametrize( + "args,expected", + [ + ([lit("abc"), lit("bcd"), lit("Abc"), lit("cd")], "Abc"), + ([3, 1, 2.0, 3.1, 2.5, -1], -1), + ([4], 4), + ], +) +def test_least(warehouse, args, expected): + query = select(least(*args)) + result = tuple(warehouse.db.execute(query)) + assert result == ((expected,),) + + +@pytest.mark.parametrize( + "expr,expected", + [ + (greatest(column("a")), [(3,), (8,), (9,)]), + (least(column("a")), [(3,), (8,), (9,)]), + (least(column("a"), column("b")), [(3,), (7,), (1,)]), + ], +) +def test_conditionals_with_multiple_rows(warehouse, expr, expected): + # In particular, we want to ensure that we are avoiding sqlite's + # default behavior for `max` and `min` which is to behave as + # aggregate functions when a single argument is passed. + # See https://www.sqlite.org/lang_corefunc.html#max_scalar + query = select(expr).select_from(values([(3, 5), (8, 7), (9, 1)], ["a", "b"])) + result = list(warehouse.db.execute(query)) + assert result == expected diff --git a/tests/unit/sql/test_path.py b/tests/unit/sql/test_path.py new file mode 100644 index 000000000..3056ef3ff --- /dev/null +++ b/tests/unit/sql/test_path.py @@ -0,0 +1,74 @@ +import posixpath as pp +import re + +import pytest +from sqlalchemy import literal, select +from sqlalchemy.sql import func as f + +from datachain.sql.functions import path as sql_path + +PATHS = ["", "/", "name", "/name", "name/", "some/long/path"] +EXT_PATHS = [ + "", + "abc.txt", + "abc...txt", + "abc", + "abc/", + "some/path/abc.tar.gz", + "some/pa.th/abc", +] + + +def split_parent(path): + parent, name = f"/{path}".rsplit("/", 1) + return parent[1:], name + + +def file_stem(path): + return pp.splitext(path)[0].rstrip(".") + + +def file_ext(path): + return pp.splitext(path)[1].lstrip(".") + + +@pytest.mark.parametrize("func_base", [f.path, sql_path]) +@pytest.mark.parametrize("func_name", ["parent", "name"]) +def test_default_not_implement(func_base, func_name): + """ + Importing datachain.sql.functions.path should register a custom compiler + which raises an exception for these functions with the default + SQLAlchemy dialect. + """ + fn = getattr(func_base, func_name) + expr = fn(literal("file:///some/file/path")) + with pytest.raises(NotImplementedError, match=re.escape(f"path.{func_name}")): + expr.compile() + + +@pytest.mark.parametrize("path", PATHS) +def test_parent(warehouse, path): + query = select(f.path.parent(literal(path))) + result = tuple(warehouse.db.execute(query)) + assert result == ((split_parent(path)[0],),) + + +@pytest.mark.parametrize("path", PATHS) +def test_name(warehouse, path): + query = select(f.path.name(literal(path))) + result = tuple(warehouse.db.execute(query)) + assert result == ((split_parent(path)[1],),) + + +@pytest.mark.parametrize("path", EXT_PATHS) +def test_file_stem(warehouse, path): + query = select(sql_path.file_stem(literal(path))) + result = tuple(warehouse.db.execute(query)) + assert result == ((file_stem(path),),) + + +@pytest.mark.parametrize("path", EXT_PATHS) +def test_file_ext(warehouse, path): + query = select(sql_path.file_ext(literal(path))) + result = tuple(warehouse.db.execute(query)) + assert result == ((file_ext(path),),) diff --git a/tests/unit/sql/test_random.py b/tests/unit/sql/test_random.py new file mode 100644 index 000000000..6e486fbc7 --- /dev/null +++ b/tests/unit/sql/test_random.py @@ -0,0 +1,8 @@ +from datachain.sql import select +from datachain.sql.functions import rand + + +def test_rand(warehouse): + query = select(rand()) + result = tuple(warehouse.db.execute(query)) + assert isinstance(result[0][0], int) diff --git a/tests/unit/sql/test_selectable.py b/tests/unit/sql/test_selectable.py new file mode 100644 index 000000000..0c8ddbad2 --- /dev/null +++ b/tests/unit/sql/test_selectable.py @@ -0,0 +1,27 @@ +import pytest + +from datachain.sql import select +from datachain.sql.selectable import values + +DATA = [("a", 1.0, 100), ("b", 2.0, 200), ("c", 3.0, 300), ("d", 4.0, 400)] + + +@pytest.mark.parametrize( + "query", + [select(values(DATA)), select(values(DATA, ["letter", "float", "int"]))], +) +def test_select_values(warehouse, query): + result = list(warehouse.db.execute(query)) + assert result == DATA + + +@pytest.mark.parametrize( + "query", + [ + select("c1", "c3").select_from(values(DATA)), + select("letter", "int").select_from(values(DATA, ["letter", "float", "int"])), + ], +) +def test_select_from_values(warehouse, query): + result = list(warehouse.db.execute(query)) + assert result == [("a", 100), ("b", 200), ("c", 300), ("d", 400)] diff --git a/tests/unit/sql/test_string.py b/tests/unit/sql/test_string.py new file mode 100644 index 000000000..084261428 --- /dev/null +++ b/tests/unit/sql/test_string.py @@ -0,0 +1,23 @@ +import pytest + +from datachain.sql import literal, select +from datachain.sql.functions import string + + +def test_length(warehouse): + query = select(string.length(literal("abcdefg"))) + result = tuple(warehouse.db.execute(query)) + assert result == ((7,),) + + +@pytest.mark.parametrize( + "args,expected", + [ + ([literal("abc//def/g/hi"), literal("/")], ["abc", "", "def", "g", "hi"]), + ([literal("abc//def/g/hi"), literal("/"), 2], ["abc", "", "def/g/hi"]), + ], +) +def test_split(warehouse, args, expected): + query = select(string.split(*args)) + result = tuple(warehouse.dataset_rows_select(query)) + assert result == ((expected,),) diff --git a/tests/unit/test_asyn.py b/tests/unit/test_asyn.py new file mode 100644 index 000000000..e2499237d --- /dev/null +++ b/tests/unit/test_asyn.py @@ -0,0 +1,163 @@ +import asyncio +import functools +from collections import Counter +from contextlib import contextmanager + +import pytest +from fsspec.asyn import sync +from hypothesis import assume, given, settings +from hypothesis import strategies as st + +from datachain.asyn import AsyncMapper, OrderedMapper, get_loop + + +async def fake_io(i): + # print(f"task {i}") + await asyncio.sleep(i) + return i + + +def join_all_tasks(loop, timeout=4): + tasks = asyncio.all_tasks(loop) + if tasks: + coro = asyncio.wait(tasks, timeout=timeout) + future = asyncio.run_coroutine_threadsafe(coro, loop=loop) + try: + # Time out if any tasks are still running + future.result(timeout=timeout) + finally: + # Avoid annoying warning after a timeout + for t in asyncio.all_tasks(loop): + t.cancel() + + +@contextmanager +def mock_time(loop): + from aiotools import VirtualClock + + clock = VirtualClock() + cm = clock.patch_loop() + + async def patch(): + cm.__enter__() + + async def unpatch(): + cm.__exit__(None, None, None) + + asyncio.run_coroutine_threadsafe(patch(), loop).result() + try: + yield + finally: + asyncio.run_coroutine_threadsafe(unpatch(), loop).result() + + +@pytest.fixture +def loop(): + loop = get_loop() + with mock_time(loop): + try: + yield loop + finally: + join_all_tasks(loop) + + +@pytest.mark.parametrize("create_mapper", [AsyncMapper, OrderedMapper]) +def test_mapper_fsspec(create_mapper, loop): + n_rows = 50 + + async def process(row): + await mapper.to_thread(functools.partial(sync, loop, fake_io, row)) + return row + + mapper = create_mapper(process, range(n_rows), workers=10, loop=loop) + result = [sync(loop, fake_io, i + n_rows) for i in mapper.iterate(timeout=4)] + if mapper.order_preserving: + assert result == list(range(n_rows, 2 * n_rows)) + else: + assert set(result) == set(range(n_rows, 2 * n_rows)) + + +@pytest.mark.parametrize("create_mapper", [AsyncMapper, OrderedMapper]) +def test_mapper_generator_shutdown(create_mapper, loop): + """ + Check that throwing an exception into AsyncMapper.iterate() terminates it cleanly. + Note that finalising a generator involves throwing StopIteration into it. + """ + + async def process(row): + await mapper.to_thread(functools.partial(sync, loop, fake_io, row)) + return row + + class MyError(Exception): + pass + + mapper = create_mapper(process, range(50), workers=10, loop=loop) + iterator = mapper.iterate(timeout=4) + next(iterator) + with pytest.raises(MyError): + iterator.throw(MyError) + + +@pytest.mark.parametrize("create_mapper", [AsyncMapper, OrderedMapper]) +def test_mapper_exception_while_processing(create_mapper, loop): + async def process(row): + await mapper.to_thread(functools.partial(sync, loop, fake_io, row)) + if row == 12: + raise RuntimeError + return row + + mapper = create_mapper(process, range(50), workers=10, loop=loop) + with pytest.raises(RuntimeError): + list(mapper.iterate(timeout=4)) + + +@pytest.mark.parametrize("create_mapper", [AsyncMapper, OrderedMapper]) +@settings(deadline=None) +@given( + inputs=st.lists(st.integers(min_value=0, max_value=100), max_size=20), + workers=st.integers(min_value=1, max_value=5), +) +def test_mapper_hypothesis(inputs, workers, create_mapper): + async def process(input): + await asyncio.sleep(input) + return input + + loop = get_loop() + mapper = create_mapper(process, inputs, workers=workers, loop=loop) + with mock_time(loop): + try: + result = list(mapper.iterate(timeout=4)) + finally: + join_all_tasks(loop) + if mapper.order_preserving: + assert result == inputs + else: + assert Counter(result) == Counter(inputs) + + +@pytest.mark.parametrize("create_mapper", [AsyncMapper, OrderedMapper]) +@settings(deadline=None) +@given( + inputs=st.lists( + st.tuples(st.booleans(), st.integers(min_value=0, max_value=100)), max_size=20 + ), + workers=st.integers(min_value=1, max_value=5), +) +def test_mapper_exception_hypothesis(inputs, workers, create_mapper): + assume(any(n[0] for n in inputs)) + + async def process(input): + raising, n = input + await asyncio.sleep(n) + if raising: + raise RuntimeError + return input + + loop = get_loop() + mapper = create_mapper(process, inputs, workers=workers, loop=loop) + with mock_time(loop): + try: + with pytest.raises(RuntimeError): + list(mapper.iterate(timeout=4)) + finally: + join_all_tasks(loop) diff --git a/tests/unit/test_cache.py b/tests/unit/test_cache.py new file mode 100644 index 000000000..3643e0721 --- /dev/null +++ b/tests/unit/test_cache.py @@ -0,0 +1,56 @@ +import pytest + +from datachain.cache import DataChainCache, UniqueId + + +@pytest.fixture +def cache(tmp_path): + return DataChainCache(str(tmp_path / "cache"), str(tmp_path / "tmp")) + + +def test_simple(cache): + uid = UniqueId( + "s3://foo", "data", "bar", etag="xyz", size=3, vtype="", location=None + ) + data = b"foo" + assert not cache.contains(uid) + + cache.store_data(uid, data) + assert cache.contains(uid) + with open(cache.get_path(uid), mode="rb") as f: + assert f.read() == data + + cache.clear() + assert not cache.contains(uid) + + +def test_get_total_size(cache): + file_info = [ + ("file1", b"foo"), + ("file2", b"bar"), + ("file3", b"some file data"), + ("file4", b"more file data " * 1024), + ] + expected_total = sum(len(d) for _, d in file_info) + for name, data in file_info: + uid = UniqueId( + "s3://foo", "data", name, etag="xyz", size=3, vtype="", location=None + ) + cache.store_data(uid, data) + total = cache.get_total_size() + assert total == expected_total + + cache.clear() + empty_total = cache.get_total_size() + assert empty_total == 0 + + +def test_remove(cache): + uid = UniqueId( + "s3://bkt42", "dir1/dir2", "file84", etag="abc", size=3, vtype="", location=None + ) + cache.store_data(uid, b"some random string 679") + + assert cache.contains(uid) + cache.remove(uid) + assert not cache.contains(uid) diff --git a/tests/unit/test_catalog.py b/tests/unit/test_catalog.py new file mode 100644 index 000000000..c2ef517e8 --- /dev/null +++ b/tests/unit/test_catalog.py @@ -0,0 +1,170 @@ +from textwrap import dedent +from typing import TYPE_CHECKING + +from datachain.catalog import Catalog + +if TYPE_CHECKING: + from datachain.data_storage import AbstractWarehouse + + +def test_compile_query_script_no_feature_class(catalog): + script = dedent( + """ + from datachain.query import C, DatasetQuery, asUDF + DatasetQuery("s3://bkt/dir1") + """ + ).strip() + feature, result = catalog.compile_query_script(script, "tmpfeature") + expected = dedent( + """ + from datachain.query import C, DatasetQuery, asUDF + import datachain.query.dataset + datachain.query.dataset.query_wrapper( + DatasetQuery('s3://bkt/dir1')) + """ + ).strip() + assert feature is None + assert result == expected + + +def test_compile_query_script_with_feature_class(catalog): + script = dedent( + """ + from datachain.query import C, DatasetQuery, asUDF + from datachain.lib.feature import Feature as FromAlias + from datachain.lib.feature import Feature + import datachain.lib.feature.Feature as DirectImportedFeature + import datachain + + class NormalClass: + t = 1 + + class SFClass(FromAlias): + emb: float + + class DirectImport(DirectImportedFeature): + emb: float + + class FullImport(datachain.lib.feature.Feature): + emb: float + + class Embedding(Feature): + emb: float + + DatasetQuery("s3://bkt/dir1") + """ + ).strip() + feature, result = catalog.compile_query_script(script, "tmpfeature") + expected_feature = dedent( + """ + from datachain.query import C, DatasetQuery, asUDF + from datachain.lib.feature import Feature as FromAlias + from datachain.lib.feature import Feature + import datachain.lib.feature.Feature as DirectImportedFeature + import datachain + import datachain.query.dataset + + class SFClass(FromAlias): + emb: float + + class DirectImport(DirectImportedFeature): + emb: float + + class FullImport(datachain.lib.feature.Feature): + emb: float + + class Embedding(Feature): + emb: float + """ + ).strip() + expected_result = dedent( + """ + from datachain.query import C, DatasetQuery, asUDF + from datachain.lib.feature import Feature as FromAlias + from datachain.lib.feature import Feature + import datachain.lib.feature.Feature as DirectImportedFeature + import datachain + import datachain.query.dataset + from tmpfeature import * + + class NormalClass: + t = 1 + datachain.query.dataset.query_wrapper( + DatasetQuery('s3://bkt/dir1')) + """ + ).strip() + + assert feature == expected_feature + assert result == expected_result + + +def test_compile_query_script_with_decorator(catalog): + script = dedent( + """ + import os + from datachain.query import C, DatasetQuery, udf + from datachain.sql.types import Float, Float32, Int, String, Binary + + @udf( + params=("name", ), + output={"num": Float, "bin": Binary} + ) + def my_func1(name): + x = 3.14 + int_example = 25 + bin = int_example.to_bytes(2, "big") + return (x, bin) + + print("Test ENV = ", os.environ['TEST_ENV']) + ds = DatasetQuery("s3://dql-small/*.jpg") \ + .add_signals(my_func1) + + ds + """ + ).strip() + feature, result = catalog.compile_query_script(script, "tmpfeature") + + expected_result = dedent( + """ + import os + from datachain.query import C, DatasetQuery, udf + from datachain.sql.types import Float, Float32, Int, String, Binary + import datachain.query.dataset + + @udf(params=('name',), output={'num': Float, 'bin': Binary}) + def my_func1(name): + x = 3.14 + int_example = 25 + bin = int_example.to_bytes(2, 'big') + return (x, bin) + print('Test ENV = ', os.environ['TEST_ENV']) + ds = DatasetQuery('s3://dql-small/*.jpg').add_signals(my_func1) + datachain.query.dataset.query_wrapper( + ds) + """ + ).strip() + + assert feature is None + assert result == expected_result + + +def test_catalog_warehouse_ready_callback(mocker, warehouse, id_generator, metastore): + spy = mocker.spy(warehouse, "is_ready") + + def callback(warehouse: "AbstractWarehouse"): + assert warehouse.is_ready() + + catalog = Catalog( + id_generator, metastore, warehouse, warehouse_ready_callback=callback + ) + + spy.assert_not_called() + + _ = catalog.warehouse + + spy.assert_called_once() + spy.reset_mock() + + _ = catalog.warehouse + + spy.assert_not_called() diff --git a/tests/unit/test_catalog_loader.py b/tests/unit/test_catalog_loader.py new file mode 100644 index 000000000..d1893a894 --- /dev/null +++ b/tests/unit/test_catalog_loader.py @@ -0,0 +1,187 @@ +import os +from unittest.mock import patch + +import pytest + +from datachain.catalog.loader import ( + get_catalog, + get_distributed_class, + get_id_generator, + get_metastore, + get_warehouse, +) +from datachain.data_storage.sqlite import ( + SQLiteDatabaseEngine, + SQLiteIDGenerator, + SQLiteMetastore, + SQLiteWarehouse, +) +from datachain.storage import StorageURI + + +class DistributedClass: + def __init__(self, **kwargs): + self.kwargs = kwargs + + +def test_get_id_generator(): + db = SQLiteDatabaseEngine.from_db_file(":memory:") + + id_generator = SQLiteIDGenerator(db, table_prefix="prefix") + assert id_generator.db == db + assert id_generator._table_prefix == "prefix" + + with patch.dict(os.environ, {"DATACHAIN__ID_GENERATOR": id_generator.serialize()}): + id_generator2 = get_id_generator() + assert id_generator2 + assert isinstance(id_generator2, SQLiteIDGenerator) + assert id_generator2._db.db_file == db.db_file + assert id_generator2._table_prefix == "prefix" + assert id_generator2.clone_params() == id_generator.clone_params() + + with patch.dict(os.environ, {"DATACHAIN__ID_GENERATOR": db.serialize()}): + with pytest.raises(RuntimeError, match="instance of AbstractIDGenerator"): + get_id_generator() + + +def test_get_metastore(): + db = SQLiteDatabaseEngine.from_db_file(":memory:") + id_generator = SQLiteIDGenerator(db, table_prefix="prefix") + uri = StorageURI("s3://bucket") + partial_id = 37 + + metastore = SQLiteMetastore(id_generator, uri, partial_id, db) + assert metastore.id_generator == id_generator + assert metastore.uri == uri + assert metastore.partial_id == partial_id + assert metastore.db == db + + with patch.dict(os.environ, {"DATACHAIN__METASTORE": metastore.serialize()}): + metastore2 = get_metastore(None) + assert metastore2 + assert isinstance(metastore2, SQLiteMetastore) + assert metastore2.id_generator._db.db_file == metastore.id_generator._db.db_file + assert ( + metastore2.id_generator._table_prefix + == metastore.id_generator._table_prefix + ) + assert metastore2.uri == uri + assert metastore2.partial_id == partial_id + assert metastore2.db.db_file == db.db_file + assert metastore2.clone_params() == metastore.clone_params() + + with patch.dict(os.environ, {"DATACHAIN__METASTORE": db.serialize()}): + with pytest.raises(RuntimeError, match="instance of AbstractMetastore"): + get_metastore(None) + + +def test_get_warehouse(): + db = SQLiteDatabaseEngine.from_db_file(":memory:") + id_generator = SQLiteIDGenerator(db, table_prefix="prefix") + + warehouse = SQLiteWarehouse(id_generator, db) + assert warehouse.id_generator == id_generator + assert warehouse.db == db + + with patch.dict(os.environ, {"DATACHAIN__WAREHOUSE": warehouse.serialize()}): + warehouse2 = get_warehouse(None) + assert warehouse2 + assert isinstance(warehouse2, SQLiteWarehouse) + assert warehouse2.id_generator._db.db_file == warehouse.id_generator._db.db_file + assert ( + warehouse2.id_generator._table_prefix + == warehouse.id_generator._table_prefix + ) + assert warehouse2.db.db_file == db.db_file + assert warehouse2.clone_params() == warehouse.clone_params() + + with patch.dict(os.environ, {"DATACHAIN__WAREHOUSE": db.serialize()}): + with pytest.raises(RuntimeError, match="instance of AbstractWarehouse"): + get_warehouse(None) + + +def test_get_distributed_class(): + distributed_args = {"foo": "bar", "baz": "37", "empty": ""} + env = { + "DATACHAIN_DISTRIBUTED": "tests.unit.test_catalog_loader.DistributedClass", + "DATACHAIN_DISTRIBUTED_ARG_FOO": "bar", + "DATACHAIN_DISTRIBUTED_ARG_BAZ": "37", + "DATACHAIN_DISTRIBUTED_ARG_EMPTY": "", + } + + with patch.dict(os.environ, env): + distributed = get_distributed_class() + assert distributed + assert isinstance(distributed, DistributedClass) + assert distributed.kwargs == distributed_args + + with patch.dict(os.environ, {"DATACHAIN_DISTRIBUTED": ""}): + with pytest.raises( + RuntimeError, match="DATACHAIN_DISTRIBUTED import path is required" + ): + get_distributed_class() + + with patch.dict( + os.environ, + {"DATACHAIN_DISTRIBUTED": "tests.unit.test_catalog_loader.NonExistent"}, + ): + with pytest.raises(AttributeError, match="has no attribute 'NonExistent'"): + get_distributed_class() + + with patch.dict(os.environ, {"DATACHAIN_DISTRIBUTED": "DistributionClass"}): + with pytest.raises( + RuntimeError, match="Invalid DATACHAIN_DISTRIBUTED import path" + ): + get_distributed_class() + + +def test_get_catalog(): + db = SQLiteDatabaseEngine.from_db_file(":memory:") + id_generator = SQLiteIDGenerator(db, table_prefix="prefix") + uri = StorageURI("s3://bucket") + partial_id = 73 + metastore = SQLiteMetastore(id_generator, uri, partial_id, db) + warehouse = SQLiteWarehouse(id_generator, db) + env = { + "DATACHAIN__ID_GENERATOR": id_generator.serialize(), + "DATACHAIN__METASTORE": metastore.serialize(), + "DATACHAIN__WAREHOUSE": warehouse.serialize(), + } + + with patch.dict(os.environ, env): + catalog = get_catalog() + assert catalog + + assert catalog.id_generator + assert isinstance(catalog.id_generator, SQLiteIDGenerator) + assert catalog.id_generator._db.db_file == db.db_file + assert catalog.id_generator._table_prefix == "prefix" + assert catalog.id_generator.clone_params() == id_generator.clone_params() + + assert catalog.metastore + assert isinstance(catalog.metastore, SQLiteMetastore) + assert ( + catalog.metastore.id_generator._db.db_file + == metastore.id_generator._db.db_file + ) + assert ( + catalog.metastore.id_generator._table_prefix + == metastore.id_generator._table_prefix + ) + assert catalog.metastore.uri == uri + assert catalog.metastore.partial_id == partial_id + assert catalog.metastore.db.db_file == db.db_file + assert catalog.metastore.clone_params() == metastore.clone_params() + + assert catalog.warehouse + assert isinstance(catalog.warehouse, SQLiteWarehouse) + assert ( + catalog.warehouse.id_generator._db.db_file + == warehouse.id_generator._db.db_file + ) + assert ( + catalog.warehouse.id_generator._table_prefix + == warehouse.id_generator._table_prefix + ) + assert catalog.warehouse.db.db_file == db.db_file + assert catalog.warehouse.clone_params() == warehouse.clone_params() diff --git a/tests/unit/test_cli_parsing.py b/tests/unit/test_cli_parsing.py new file mode 100644 index 000000000..53992b108 --- /dev/null +++ b/tests/unit/test_cli_parsing.py @@ -0,0 +1,125 @@ +import logging +from argparse import ArgumentParser, ArgumentTypeError + +import pytest + +from datachain.cli import ( + TTL_HUMAN, + TTL_INT, + find_columns_type, + get_logging_level, + get_parser, + human_time_type, +) +from datachain.cli_utils import CommaSeparatedArgs, KeyValueArgs + + +def test_human_time_type(): + assert human_time_type(TTL_HUMAN) == TTL_INT + assert human_time_type("1h") == 60 * 60 + assert human_time_type("30s") == 30 + assert human_time_type("2w") == 2 * 7 * 24 * 60 * 60 + + assert human_time_type("", can_be_none=True) is None + + with pytest.raises(ArgumentTypeError): + human_time_type("bogus") + + +def test_find_columns_type(): + assert find_columns_type("") == ["path"] + assert find_columns_type("du") == ["du"] + assert find_columns_type("", default_colums_str="name") == ["name"] + assert find_columns_type("du, name,PATH") == ["du", "name", "path"] + + with pytest.raises(ArgumentTypeError): + find_columns_type("bogus") + + +def test_cli_parser(): + parser = get_parser() + + args = parser.parse_args(("ls", "s3://example-bucket/", "--ttl", "1d")) + + assert args.ttl == 24 * 60 * 60 + assert args.sources == ["s3://example-bucket/"] + + assert args.quiet == 0 + assert args.verbose == 0 + + assert get_logging_level(args) == logging.INFO + + args = parser.parse_args(("ls", "s3://example-bucket/", "-vvv")) + + assert args.quiet == 0 + assert args.verbose == 3 + + assert get_logging_level(args) == logging.DEBUG + + args = parser.parse_args(("ls", "s3://example-bucket/", "-q")) + + assert args.quiet == 1 + assert args.verbose == 0 + + assert get_logging_level(args) == logging.CRITICAL + + +@pytest.mark.parametrize( + "param,parsed", + ( + ("p1", ["p1"]), + ("p1,p2", ["p1", "p2"]), + ), +) +def test_comma_separated_args(param, parsed): + parser = ArgumentParser() + parser.add_argument("--param", default=[], action=CommaSeparatedArgs) + + args = parser.parse_args(("--param", param)) + assert args.param == parsed + + +@pytest.mark.parametrize("param", (None, "")) +def test_comma_separated_args_error(param): + parser = ArgumentParser() + parser.add_argument("--param", default=[], action=CommaSeparatedArgs) + + cmd = ["--param"] + if param: + cmd.append(param) + with pytest.raises(SystemExit): + parser.parse_args(cmd) + + +@pytest.mark.parametrize( + "params,parsed", + ( + ([], None), + (["p1=foo"], {"p1": "foo"}), + (["p1=bar", "p2=baz"], {"p1": "bar", "p2": "baz"}), + (["p1=foo", "p1=bar"], {"p1": "bar"}), + (["p1=foo", "p1=bar"], {"p1": "bar"}), + ), +) +def test_key_value_args(params, parsed): + parser = ArgumentParser() + parser.add_argument("--param", nargs=1, action=KeyValueArgs) + + cmd = [] + for p in params: + cmd.extend(["--param", p]) + + args = parser.parse_args(cmd) + assert args.param == parsed + + +@pytest.mark.parametrize("param", (None, "p1", "=", "p1=", "=foo")) +def test_key_value_args_error(param): + parser = ArgumentParser() + parser.add_argument("--param", nargs=1, action=KeyValueArgs) + + cmd = ["--param"] + if param: + cmd.append(param) + with pytest.raises(SystemExit): + parser.parse_args(cmd) diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py new file mode 100644 index 000000000..59d74029c --- /dev/null +++ b/tests/unit/test_client.py @@ -0,0 +1,136 @@ +import os +import sys +from pathlib import Path + +import pytest +from hypothesis import HealthCheck, assume, given, settings +from hypothesis import strategies as st + +from datachain.client import Client +from datachain.client.local import FileClient +from tests.utils import uppercase_scheme + +non_null_text = st.text( + alphabet=st.characters(blacklist_categories=["Cc", "Cs"]), min_size=1 +) + + +def test_bad_protocol(): + with pytest.raises(NotImplementedError): + Client.get_implementation("bogus://bucket") + + +def test_win_paths_are_recognized(): + if sys.platform != "win32": + pytest.skip() + + assert Client.get_implementation("file://C:/bucket") == FileClient + assert Client.get_implementation("file://C:\\bucket") == FileClient + assert Client.get_implementation("file://\\bucket") == FileClient + assert Client.get_implementation("file:///bucket") == FileClient + assert Client.get_implementation("C://bucket") == FileClient + assert Client.get_implementation("C:\\bucket") == FileClient + assert Client.get_implementation("\bucket") == FileClient + + +@settings(suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=None) +@given(rel_path=non_null_text) +def test_parse_url(cloud_test_catalog, rel_path, cloud_type): + if cloud_type == "file": + assume(not rel_path.startswith("/")) + bucket_uri = cloud_test_catalog.src_uri + url = f"{bucket_uri}/{rel_path}" + catalog = cloud_test_catalog.catalog + client, rel_part = catalog.parse_url(url) + if cloud_type == "file": + root_uri = FileClient.root_path().as_uri() + assert client.uri == root_uri + assert rel_part == url[len(root_uri) :] + else: + assert client.uri == bucket_uri + assert rel_part == rel_path + + +@settings(suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=None) +@given(rel_path=non_null_text) +def test_parse_url_uppercase_scheme(cloud_test_catalog, rel_path, cloud_type): + if cloud_type == "file": + assume(not rel_path.startswith("/")) + bucket_uri = cloud_test_catalog.src_uri + bucket_uri_upper = uppercase_scheme(bucket_uri) + url = f"{bucket_uri_upper}/{rel_path}" + catalog = cloud_test_catalog.catalog + client, rel_part = catalog.parse_url(url) + if cloud_type == "file": + root_uri = FileClient.root_path().as_uri() + assert client.uri == root_uri + assert rel_part == url[len(root_uri) :] + else: + assert client.uri == bucket_uri + assert rel_part == rel_path + + +@pytest.mark.parametrize("cloud_type", ["file"], indirect=True) +def test_parse_file_absolute_path_without_protocol(cloud_test_catalog): + catalog = cloud_test_catalog.catalog + working_dir = Path().absolute() + root_uri = FileClient.root_path().as_uri() + client, rel_part = catalog.parse_url(str(working_dir / Path("animals"))) + working_dir = Path().absolute() + root_uri = FileClient.root_path().as_uri() + assert client.uri == root_uri + assert rel_part == (working_dir / Path("animals")).as_uri()[len(root_uri) :] + + +@pytest.mark.parametrize("cloud_type", ["file"], indirect=True) +def test_parse_file_relative_path_multiple_dirs_back(cloud_test_catalog): + catalog = cloud_test_catalog.catalog + client, rel_part = catalog.parse_url("../../animals".replace("/", os.sep)) + root_uri = FileClient.root_path().as_uri() + assert client.uri == root_uri + assert ( + rel_part + == (Path().absolute().parents[1] / Path("animals")).as_uri()[len(root_uri) :] + ) + + +@pytest.mark.parametrize("cloud_type", ["file"], indirect=True) +@pytest.mark.parametrize("url", ["./animals".replace("/", os.sep), "animals"]) +def test_parse_file_relative_path_working_dir(cloud_test_catalog, url): + catalog = cloud_test_catalog.catalog + client, rel_part = catalog.parse_url(url) + root_uri = FileClient.root_path().as_uri() + assert client.uri == root_uri + assert rel_part == (Path().absolute() / Path("animals")).as_uri()[len(root_uri) :] + + +@pytest.mark.parametrize("cloud_type", ["file"], indirect=True) +def test_parse_file_relative_path_home_dir(cloud_test_catalog): + if sys.platform == "win32": + # home dir shortcut is not available on windows + pytest.skip() + catalog = cloud_test_catalog.catalog + client, rel_part = catalog.parse_url("~/animals") + root_uri = FileClient.root_path().as_uri() + assert client.uri == root_uri + assert rel_part == (Path().home() / Path("animals")).as_uri()[len(root_uri) :] + + +@pytest.mark.parametrize("cloud_type", ["file"], indirect=True) +def test_parse_file_path_ends_with_slash(cloud_type): + client, rel_part = Client.parse_url("./animals/".replace("/", os.sep), None, None) + root_uri = FileClient.root_path().as_uri() + assert client.uri == root_uri + assert ( + rel_part + == ((Path().absolute() / Path("animals")).as_uri())[len(root_uri) :] + "/" + ) + + +@pytest.mark.parametrize("cloud_type", ["s3", "azure", "gs"], indirect=True) +def test_parse_cloud_path_ends_with_slash(cloud_test_catalog): + uri = f"{cloud_test_catalog.src_uri}/animals/" + catalog = cloud_test_catalog.catalog + client, rel_part = catalog.parse_url(uri) + assert client.uri == cloud_test_catalog.src_uri + assert rel_part == "animals/" diff --git a/tests/unit/test_client_s3.py b/tests/unit/test_client_s3.py new file mode 100644 index 000000000..6ce45094a --- /dev/null +++ b/tests/unit/test_client_s3.py @@ -0,0 +1,81 @@ +import pytest + +from datachain.node import DirType, Node +from datachain.nodes_thread_pool import NodeChunk + + +@pytest.fixture +def nodes(): + return iter( + [ + make_size_node(11, DirType.DIR, "a", "f1", 100), + make_size_node(12, DirType.FILE, "b", "f2", 100), + make_size_node(13, DirType.FILE, "c", "f3", 100), + make_size_node(14, DirType.FILE, "d", "", 100), + make_size_node(15, DirType.FILE, "e", "f5", 100), + make_size_node(16, DirType.DIR, "f", "f6", 100), + make_size_node(17, DirType.FILE, "g", "f7", 100), + ] + ) + + +def make_size_node(node_id, dir_type, parent, name, size): + return Node( + node_id, + vtype="", + dir_type=dir_type, + parent=parent, + name=name, + size=size, + ) + + +class FakeCache: + def contains(self, _): + return False + + +def make_chunks(nodes, *args, **kwargs): + return NodeChunk(FakeCache(), "s3://foo", nodes, *args, **kwargs) + + +def test_node_bucket_the_only_item(): + bkt = make_chunks(iter([make_size_node(20, DirType.FILE, 2, "file.csv", 100)]), 201) + + result = next(bkt) + assert len(result) == 1 + assert next(bkt, None) is None + + +def test_node_bucket_the_only_item_over_limit(): + bkt = make_chunks(iter([make_size_node(20, DirType.FILE, 2, "file.csv", 100)]), 1) + + result = next(bkt) + assert len(result) == 1 + assert next(bkt, None) is None + + +def test_node_bucket_the_last_one(): + bkt = make_chunks(iter([make_size_node(20, DirType.FILE, 2, "file.csv", 100)]), 1) + + next(bkt) + with pytest.raises(StopIteration): + next(bkt) + + +def test_node_bucket_basic(nodes): + bkt = list(make_chunks(nodes, 201)) + + assert len(bkt) == 2 + assert len(bkt[0]) == 3 + assert len(bkt[1]) == 1 + + +def test_node_bucket_full_split(nodes): + bkt = list(make_chunks(nodes, 0)) + + assert len(bkt) == 4 + assert len(bkt[0]) == 1 + assert len(bkt[1]) == 1 + assert len(bkt[2]) == 1 + assert len(bkt[3]) == 1 diff --git a/tests/unit/test_data_storage.py b/tests/unit/test_data_storage.py new file mode 100644 index 000000000..73a97a28f --- /dev/null +++ b/tests/unit/test_data_storage.py @@ -0,0 +1,171 @@ +from datetime import datetime +from typing import Any + +import pytest + +from datachain.sql.types import ( + JSON, + Array, + Boolean, + DateTime, + Float, + Float32, + Float64, + Int, + String, +) +from tests.utils import DEFAULT_TREE, TARRED_TREE, create_tar_dataset + +COMPLEX_TREE: dict[str, Any] = { + **TARRED_TREE, + **DEFAULT_TREE, + "nested": {"dir": {"path": {"abc.txt": "abc"}}}, +} + + +@pytest.mark.parametrize("tree", [COMPLEX_TREE], indirect=True) +def test_dir_expansion(cloud_test_catalog, version_aware, cloud_type): + has_version = version_aware or cloud_type == "gs" + + ctc = cloud_test_catalog + catalog = ctc.catalog + src_uri = ctc.src_uri + if cloud_type == "file": + # we don't want to index things in parent directory + src_uri += "/" + + ds = create_tar_dataset(catalog, ctc.src_uri, "ds2") + dataset = catalog.get_dataset(ds.name) + st = catalog.warehouse.clone() + q = st.dataset_rows(dataset).dir_expansion() + columns = ( + "id", + "vtype", + "is_dir", + "source", + "parent", + "name", + "version", + "location", + ) + result = [dict(zip(columns, r)) for r in st.db.execute(q)] + to_compare = [ + (r["parent"], r["name"], r["vtype"], r["is_dir"], r["version"] != "") + for r in result + ] + + assert all(r["source"] == ctc.storage_uri for r in result) + if cloud_type == "file": + prefix = ctc.partial_path + "/" + prefix_root = ctc.partial_path + else: + prefix = "" + prefix_root = "" + + # Note, we have both a file and a directory entry for expanded tar files + expected = [ + (f"{prefix_root}", "animals.tar", "", 0, has_version), + (f"{prefix_root}", "animals.tar", "", 1, False), + (f"{prefix_root}", "cats", "", 1, False), + (f"{prefix_root}", "description", "", 0, has_version), + (f"{prefix_root}", "dogs", "", 1, False), + (f"{prefix_root}", "nested", "", 1, False), + (f"{prefix}animals.tar", "cats", "", 1, False), + (f"{prefix}animals.tar", "description", "tar", 0, False), + (f"{prefix}animals.tar", "dogs", "", 1, False), + (f"{prefix}animals.tar/cats", "cat1", "tar", 0, False), + (f"{prefix}animals.tar/cats", "cat2", "tar", 0, False), + (f"{prefix}animals.tar/dogs", "dog1", "tar", 0, False), + (f"{prefix}animals.tar/dogs", "dog2", "tar", 0, False), + (f"{prefix}animals.tar/dogs", "dog3", "tar", 0, False), + (f"{prefix}animals.tar/dogs", "others", "", 1, False), + (f"{prefix}animals.tar/dogs/others", "dog4", "tar", 0, False), + (f"{prefix}cats", "cat1", "", 0, has_version), + (f"{prefix}cats", "cat2", "", 0, has_version), + (f"{prefix}dogs", "dog1", "", 0, has_version), + (f"{prefix}dogs", "dog2", "", 0, has_version), + (f"{prefix}dogs", "dog3", "", 0, has_version), + (f"{prefix}dogs", "others", "", 1, False), + (f"{prefix}dogs/others", "dog4", "", 0, has_version), + (f"{prefix}nested", "dir", "", 1, False), + (f"{prefix}nested/dir", "path", "", 1, False), + (f"{prefix}nested/dir/path", "abc.txt", "", 0, has_version), + ] + + if cloud_type == "file": + # since with file listing, parent is relative path to the root of FS as + # storage uri is the root of FS, we need to add dirs to the root + prefix_split = prefix.split("/") + expected = [ + ("/".join(prefix_split[:i]), prefix_split[i], "", 1, False) + for i in range(len(prefix_split) - 1) + ] + expected + + assert to_compare == expected + + +@pytest.mark.parametrize( + "cloud_type,version_aware", + [("s3", True)], + indirect=True, +) +def test_convert_type(cloud_test_catalog): + ctc = cloud_test_catalog + catalog = ctc.catalog + warehouse = catalog.warehouse + now = datetime.now() + + def run_convert_type(value, sql_type): + return warehouse.convert_type( + value, + sql_type, + warehouse.python_type(sql_type), + type(sql_type).__name__, + "test_column", + ) + + # convert int to float + for f in [Float, Float32, Float64]: + converted = run_convert_type(1, f()) + assert converted == 1.0 + assert isinstance(converted, float) + + # types match, nothing to convert + assert run_convert_type(1, Int()) == 1 + assert run_convert_type(1.5, Float()) == 1.5 + assert run_convert_type(True, Boolean()) is True + assert run_convert_type("s", String()) == "s" + assert run_convert_type(now, DateTime()) == now + assert run_convert_type([1, 2], Array(Int)) == [1, 2] + assert run_convert_type([1.5, 2.5], Array(Float)) == [1.5, 2.5] + assert run_convert_type(["a", "b"], Array(String)) == ["a", "b"] + assert run_convert_type([[1, 2], [3, 4]], Array(Array(Int))) == [ + [1, 2], + [3, 4], + ] + + # JSON Tests + assert run_convert_type('{"a": 1}', JSON()) == '{"a": 1}' + assert run_convert_type({"a": 1}, JSON()) == '{"a": 1}' + assert run_convert_type([{"a": 1}], JSON()) == '[{"a": 1}]' + with pytest.raises(ValueError): + run_convert_type(0.5, JSON()) + + # convert array to compatible type + converted = run_convert_type([1, 2], Array(Float)) + assert converted == [1.0, 2.0] + assert all(isinstance(c, float) for c in converted) + + # convert nested array to compatible type + converted = run_convert_type([[1, 2], [3, 4]], Array(Array(Float))) + assert converted == [[1.0, 2.0], [3.0, 4.0]] + assert all(isinstance(c, float) for c in converted[0]) + assert all(isinstance(c, float) for c in converted[1]) + + # error, float to int + with pytest.raises(ValueError): + run_convert_type(1.5, Int()) + + # error, float to int in list + with pytest.raises(ValueError): + run_convert_type([1.5, 1], Array(Int)) diff --git a/tests/unit/test_database_engine.py b/tests/unit/test_database_engine.py new file mode 100644 index 000000000..3f1dc9f88 --- /dev/null +++ b/tests/unit/test_database_engine.py @@ -0,0 +1,80 @@ +import base64 +import pickle + +import pytest +from sqlalchemy import Column, Integer, Table + +from datachain.data_storage.serializer import deserialize +from datachain.data_storage.sqlite import SQLiteDatabaseEngine + + +@pytest.mark.parametrize("db_file", [":memory:", "file.db"]) +def test_init_clone(db_file): + db = SQLiteDatabaseEngine.from_db_file(db_file) + assert db.db_file == db_file + + # Test clone + db2 = db.clone() + assert isinstance(db2, SQLiteDatabaseEngine) + assert db2.db_file == db_file + + +def test_serialize(): + obj = SQLiteDatabaseEngine.from_db_file(":memory:") + + # Test serialization + serialized = obj.serialize() + assert serialized + serialized_pickled = base64.b64decode(serialized.encode()) + assert serialized_pickled + (f, args, kwargs) = pickle.loads(serialized_pickled) # noqa: S301 + assert str(f) == str(SQLiteDatabaseEngine.from_db_file) + assert args == [":memory:"] + assert kwargs == {} + + # Test deserialization + obj3 = deserialize(serialized) + assert isinstance(obj3, SQLiteDatabaseEngine) + assert obj3.db_file == ":memory:" + assert obj3.clone_params() == obj.clone_params() + + +def test_table(sqlite_db): + table = Table( + "test_table", sqlite_db.metadata, Column("id", Integer, primary_key=True) + ) + assert not sqlite_db.has_table("test_table") + assert not sqlite_db.has_table("test_table_2") + + table.create(sqlite_db.engine) + assert sqlite_db.has_table("test_table") + assert not sqlite_db.has_table("test_table_2") + + sqlite_db.rename_table("test_table", "test_table_2") + assert sqlite_db.has_table("test_table_2") + assert not sqlite_db.has_table("test_table") + + sqlite_db.drop_table(Table("test_table_2", sqlite_db.metadata)) + assert not sqlite_db.has_table("test_table") + assert not sqlite_db.has_table("test_table_2") + + +def test_table_in_transaction(sqlite_db): + table = Table( + "test_table", sqlite_db.metadata, Column("id", Integer, primary_key=True) + ) + assert not sqlite_db.has_table("test_table") + assert not sqlite_db.has_table("test_table_2") + + with sqlite_db.transaction(): + table.create(sqlite_db.engine) + assert sqlite_db.has_table("test_table") + assert not sqlite_db.has_table("test_table_2") + + sqlite_db.rename_table("test_table", "test_table_2") + assert sqlite_db.has_table("test_table_2") + assert not sqlite_db.has_table("test_table") + + sqlite_db.drop_table(Table("test_table_2", sqlite_db.metadata)) + assert not sqlite_db.has_table("test_table") + assert not sqlite_db.has_table("test_table_2") diff --git a/tests/unit/test_dataset.py b/tests/unit/test_dataset.py new file mode 100644 index 000000000..fc0dd2be6 --- /dev/null +++ b/tests/unit/test_dataset.py @@ -0,0 +1,88 @@ +from sqlalchemy import Column, DateTime +from sqlalchemy.dialects.sqlite import dialect as sqlite_dialect +from sqlalchemy.schema import CreateTable + +from datachain.data_storage.schema import DataTable +from datachain.sql.types import ( + JSON, + Array, + Binary, + Boolean, + Float, + Float32, + Float64, + Int, + Int64, + String, +) + + +def test_dataset_table_compilation(): + table = DataTable.new_table( + "ds-1", + columns=[ + Column("vtype", String, nullable=False, index=True), + Column("dir_type", Int, index=True), + Column("parent", String, index=True), + Column("name", String, nullable=False, index=True), + Column("etag", String), + Column("version", String), + Column("is_latest", Boolean), + Column("last_modified", DateTime(timezone=True)), + Column("size", Int64, nullable=False, index=True), + Column("owner_name", String), + Column("owner_id", String), + Column("location", JSON), + Column("source", String, nullable=False), + Column("score", Float, nullable=False), + Column("meta_info", JSON), + ], + ) + result = CreateTable(table, if_not_exists=True).compile(dialect=sqlite_dialect()) + + assert result.string == ( + "\n" + 'CREATE TABLE IF NOT EXISTS "ds-1" (\n' + "\tid INTEGER NOT NULL, \n" + "\trandom INTEGER DEFAULT (abs(random())) NOT NULL, \n" + "\tvtype VARCHAR NOT NULL, \n" + "\tdir_type INTEGER, \n" + "\tparent VARCHAR, \n" + "\tname VARCHAR NOT NULL, \n" + "\tetag VARCHAR, \n" + "\tversion VARCHAR, \n" + "\tis_latest BOOLEAN, \n" + "\tlast_modified DATETIME, \n" + "\tsize INTEGER NOT NULL, \n" + "\towner_name VARCHAR, \n" + "\towner_id VARCHAR, \n" + "\tlocation JSON, \n" + "\tsource VARCHAR NOT NULL, \n" + "\tscore FLOAT NOT NULL, \n" + "\tmeta_info JSON, \n" + "\tPRIMARY KEY (id)\n" + ")\n" + "\n" + ) + + +def test_schema_serialization(dataset_record): + dataset_record.schema = {"int_col": Int} + assert dataset_record.serialized_schema == {"int_col": {"type": "Int"}} + + dataset_record.schema = { + "binary_col": Binary, + "float_32_col": Float32, + } + assert dataset_record.serialized_schema == { + "binary_col": {"type": "Binary"}, + "float_32_col": {"type": "Float32"}, + } + + dataset_record.schema = {"nested_col": Array(Array(Float64))} + assert dataset_record.serialized_schema == { + "nested_col": { + "type": "Array", + "item_type": {"type": "Array", "item_type": {"type": "Float64"}}, + } + } diff --git a/tests/unit/test_dispatch.py b/tests/unit/test_dispatch.py new file mode 100644 index 000000000..cfda69e02 --- /dev/null +++ b/tests/unit/test_dispatch.py @@ -0,0 +1,53 @@ +from queue import Empty, Full +from typing import Optional + +from datachain.query.dispatch import ( + STOP_SIGNAL, + UDFDispatcher, + get_from_queue, + put_into_queue, +) + + +class MockQueue: + def __init__(self) -> None: + self.return_empty_once = True + self.return_full_once = True + self.put_signal: Optional[str] = None + self.put_count: int = 0 + + def put_nowait(self, task: str) -> None: + if self.return_full_once: + self.return_full_once = False + raise Full + assert task == STOP_SIGNAL + self.put_signal = task + self.put_count += 1 + + def get_nowait(self) -> str: + if self.return_empty_once: + self.return_empty_once = False + raise Empty + return STOP_SIGNAL + + +def test_get_from_queue(): + mock_queue = MockQueue() + + assert get_from_queue(mock_queue) == STOP_SIGNAL + + +def test_put_into_queue(): + mock_queue = MockQueue() + + assert put_into_queue(mock_queue, STOP_SIGNAL) is None + assert mock_queue.put_signal == STOP_SIGNAL + + +def test_send_stop_signal_to_workers(): + mock_queue = MockQueue() + + UDFDispatcher.send_stop_signal_to_workers(mock_queue, 8) + + assert mock_queue.put_signal == STOP_SIGNAL + assert mock_queue.put_count == 8 diff --git a/tests/unit/test_fileslice.py b/tests/unit/test_fileslice.py new file mode 100644 index 000000000..914b19327 --- /dev/null +++ b/tests/unit/test_fileslice.py @@ -0,0 +1,64 @@ +import io + +import pytest + +from datachain.client.fileslice import FileSlice + + +def test_positions(): + data = b"0123456789abcdef" + base = io.BytesIO(data) + f = FileSlice(base, 5, 5, "foo") + assert base.tell() == 0 + assert f.readable() + assert not f.writable() + assert f.seekable() + assert f.name == "foo" + + # f.seek() doesn't move the underlying stream + f.seek(0) + assert f.tell() == 0 + assert base.tell() == 0 + + assert f.read(3) == data[5:8] + assert f.tell() == 3 + assert base.tell() == 8 + + assert f.read(4) == data[8:10] + assert f.tell() == 5 + assert base.tell() == 10 + + b = bytearray(5) + f.seek(0) + f.readinto(b) + assert b == data[5:10] + + +def test_invalid_slice(): + data = b"0123456789abcdef" + base = io.BytesIO(data) + f = FileSlice(base, 10, 10, "foo") + assert f.read(4) == data[10:14] + with pytest.raises(RuntimeError): + f.read() + + +def test_close(): + data = b"0123456789abcdef" + base = io.BytesIO(data) + f = FileSlice(base, 5, 5, "foo") + assert f.closed is False + with f: + assert f.closed is False + assert f.closed is True + assert base.closed is True + + +def test_implicit_close(): + # Assumes refcounting semantics + data = b"0123456789abcdef" + base = io.BytesIO(data) + f = FileSlice(base, 5, 5, "foo") + assert base.closed is False + f = None # noqa: F841 + assert base.closed is True diff --git a/tests/unit/test_id_generator.py b/tests/unit/test_id_generator.py new file mode 100644 index 000000000..7c0a665e6 --- /dev/null +++ b/tests/unit/test_id_generator.py @@ -0,0 +1,185 @@ +import base64 +import pickle + +from sqlalchemy import select + +from datachain.data_storage.serializer import deserialize +from datachain.data_storage.sqlite import SQLiteDatabaseEngine, SQLiteIDGenerator + + +def get_rows(id_generator): + uris = id_generator.db.execute( + select(id_generator._table.c.uri, id_generator._table.c.last_id) + ).fetchall() + return set(uris) + + +def test_init(sqlite_db): + id_generator = SQLiteIDGenerator(sqlite_db) + assert id_generator.db == sqlite_db + assert id_generator._table_prefix is None + assert sqlite_db.has_table("id_generator") + assert get_rows(id_generator) == set() + + id_generator.cleanup_for_tests() + assert not sqlite_db.has_table("id_generator") + + +def test_init_empty(tmp_dir): + id_generator = SQLiteIDGenerator() + assert id_generator._table_prefix is None + assert id_generator.db + assert id_generator.db.has_table("id_generator") + assert get_rows(id_generator) == set() + + id_generator.cleanup_for_tests() + assert not id_generator.db.has_table("id_generator") + + +def test_init_with_prefix(sqlite_db): + id_generator = SQLiteIDGenerator(sqlite_db, table_prefix="foo") + assert id_generator.db == sqlite_db + assert id_generator._table_prefix == "foo" + assert sqlite_db.has_table("foo_id_generator") + assert not sqlite_db.has_table("id_generator") + assert get_rows(id_generator) == set() + + id_generator.cleanup_for_tests() + assert not sqlite_db.has_table("foo_id_generator") + + +def test_clone(id_generator): + clone = id_generator.clone() + assert clone._table_prefix == id_generator._table_prefix + assert get_rows(clone) == get_rows(id_generator) + + id_generator.init_id("foo") + clone.get_next_id("bar") + assert get_rows(id_generator) == {("foo", 0), ("bar", 1)} + assert get_rows(clone) == get_rows(id_generator) + + id_generator.cleanup_for_tests() + assert not id_generator.db.has_table("id_generator") + assert not clone.db.has_table("id_generator") + + +def test_clone_params(id_generator): + func, args, kwargs = id_generator.clone_params() + clone = func(*args, **kwargs) + assert clone._table_prefix == id_generator._table_prefix + assert get_rows(clone) == get_rows(id_generator) + + id_generator.init_id("foo") + clone.get_next_id("bar") + assert get_rows(id_generator) == {("foo", 0), ("bar", 1)} + assert get_rows(clone) == get_rows(id_generator) + + clone.cleanup_for_tests() + assert not id_generator.db.has_table("id_generator") + assert not clone.db.has_table("id_generator") + + +def test_serialize(): + db = SQLiteDatabaseEngine.from_db_file(":memory:") + + obj = SQLiteIDGenerator(db, table_prefix="prefix") + assert obj.db == db + assert obj._table_prefix == "prefix" + + # Test clone + obj2 = obj.clone() + assert isinstance(obj2, SQLiteIDGenerator) + assert obj2.db.db_file == obj.db.db_file + assert obj2._table_prefix == "prefix" + assert obj2.clone_params() == obj.clone_params() + + # Test serialization + serialized = obj.serialize() + assert serialized + serialized_pickled = base64.b64decode(serialized.encode()) + assert serialized_pickled + (f, args, kwargs) = pickle.loads(serialized_pickled) # noqa: S301 + assert str(f) == str(SQLiteIDGenerator.init_after_clone) + assert args == [] + assert str(kwargs["db_clone_params"]) == str(db.clone_params()) + assert kwargs["table_prefix"] == "prefix" + + # Test deserialization + obj3 = deserialize(serialized) + assert isinstance(obj3, SQLiteIDGenerator) + assert obj3.db.db_file == db.db_file + assert obj3._table_prefix == "prefix" + + +def test_init_id(id_generator): + assert get_rows(id_generator) == set() + + id_generator.init_id("foo") + assert get_rows(id_generator) == {("foo", 0)} + + assert id_generator.get_next_id("foo") == 1 + assert get_rows(id_generator) == {("foo", 1)} + + id_generator.init_id("foo") # second call should not raise an exception + assert get_rows(id_generator) == {("foo", 1)} + + assert id_generator.get_next_id("foo") == 2 + assert get_rows(id_generator) == {("foo", 2)} + + id_generator.init_id("bar") + assert get_rows(id_generator) == {("foo", 2), ("bar", 0)} + + +def test_get_next_id(id_generator): + assert get_rows(id_generator) == set() + + assert id_generator.get_next_id("foo") == 1 + assert get_rows(id_generator) == {("foo", 1)} + + assert id_generator.get_next_id("foo") == 2 + assert get_rows(id_generator) == {("foo", 2)} + + assert id_generator.get_next_id("bar") == 1 + assert get_rows(id_generator) == {("foo", 2), ("bar", 1)} + + +def test_get_next_ids(id_generator): + assert get_rows(id_generator) == set() + + assert id_generator.get_next_ids("foo", 3) == range(1, 4) + assert get_rows(id_generator) == {("foo", 3)} + + assert id_generator.get_next_ids("foo", 20) == range(4, 24) + assert get_rows(id_generator) == {("foo", 23)} + + assert id_generator.get_next_ids("bar", 1000) == range(1, 1001) + assert get_rows(id_generator) == {("foo", 23), ("bar", 1000)} + + +def test_delete_uri(id_generator): + assert get_rows(id_generator) == set() + + assert id_generator.get_next_ids("foo", 3) == range(1, 4) + assert get_rows(id_generator) == {("foo", 3)} + + assert id_generator.get_next_ids("bar", 1000) == range(1, 1001) + assert get_rows(id_generator) == {("foo", 3), ("bar", 1000)} + + id_generator.delete_uri("foo") + assert get_rows(id_generator) == {("bar", 1000)} + + id_generator.delete_uri("bar") + assert get_rows(id_generator) == set() + + +def test_delete_uris(id_generator): + assert get_rows(id_generator) == set() + + assert id_generator.get_next_ids("foo", 3) == range(1, 4) + assert get_rows(id_generator) == {("foo", 3)} + + assert id_generator.get_next_ids("bar", 1000) == range(1, 1001) + assert get_rows(id_generator) == {("foo", 3), ("bar", 1000)} + + id_generator.delete_uris(["foo", "bar"]) + assert get_rows(id_generator) == set() diff --git a/tests/unit/test_listing.py b/tests/unit/test_listing.py new file mode 100644 index 000000000..241b849d8 --- /dev/null +++ b/tests/unit/test_listing.py @@ -0,0 +1,140 @@ +import posixpath + +import pytest + +from datachain.catalog import Catalog +from datachain.catalog.catalog import DataSource +from datachain.node import DirType, Entry +from tests.utils import skip_if_not_sqlite + +TREE = { + "dir1": { + "d2": {None: ["file1.csv", "file2.csv"]}, + None: ["dataset.csv"], + }, + "dir2": {None: ["diagram.png"]}, + None: ["users.csv"], +} + + +def _tree_to_entries(tree: dict, path=""): + for k, v in tree.items(): + if k: + dir_path = posixpath.join(path, k) + yield from _tree_to_entries(v, dir_path) + else: + for fname in v: + yield Entry.from_file(path, fname) + + +@pytest.fixture +def listing(id_generator, metastore, warehouse): + catalog = Catalog( + id_generator=id_generator, metastore=metastore, warehouse=warehouse + ) + lst, _ = catalog.enlist_source("s3://whatever", 1234, skip_indexing=True) + lst.insert_entries(_tree_to_entries(TREE)) + lst.insert_entries_done() + return lst + + +def test_resolve_path_in_root(listing): + node = listing.resolve_path("dir1") + assert node.dir_type == DirType.DIR + assert node.is_dir + assert node.name == "dir1" + assert node.size == 0 + + +def test_path_resolving_nested(listing): + node = listing.resolve_path("dir1/d2/file2.csv") + assert node.dir_type == DirType.FILE + assert node.name == "file2.csv" + assert not node.is_dir + + node = listing.resolve_path("dir1/d2") + assert node.dir_type == DirType.DIR + assert node.is_dir + assert node.name == "d2" + + +def test_resolve_not_existing_path(listing): + with pytest.raises(FileNotFoundError): + listing.resolve_path("dir1/fake-file-name") + + +def test_resolve_root(listing): + node = listing.resolve_path("") + assert node.dir_type == DirType.DIR + assert node.is_dir + assert node.name == "" + assert node.size == 0 + + +def test_dir_ends_with_slash(listing): + node = listing.resolve_path("dir1/") + assert node.dir_type == DirType.DIR + assert node.is_dir + assert node.name == "dir1" + + +def test_file_ends_with_slash(listing): + with pytest.raises(FileNotFoundError): + listing.resolve_path("dir1/dataset.csv/") + + +def _match_filenames(nodes, expected_names): + assert len(nodes) == len(expected_names) + names = (node.name for node in nodes) + assert set(names) == set(expected_names) + + +def test_basic_expansion(listing): + nodes = listing.expand_path("*") + _match_filenames(nodes, ["dir1", "dir2", "users.csv"]) + + +def test_subname_expansion(listing): + nodes = listing.expand_path("di*/") + _match_filenames(nodes, ["dir1", "dir2"]) + + +def test_multilevel_expansion(listing): + skip_if_not_sqlite() + nodes = listing.expand_path("dir[1,2]/d*") + _match_filenames(nodes, ["dataset.csv", "diagram.png", "d2"]) + + +def test_expand_root(listing): + nodes = listing.expand_path("") + assert len(nodes) == 1 + assert nodes[0].dir_type == DirType.DIR + assert nodes[0].is_dir + + +def test_list_dir(listing): + dir1 = listing.resolve_path("dir1/") + names = listing.ls_path(dir1, ["name"]) + assert {n[0] for n in names} == {"d2", "dataset.csv"} + + +def test_list_file(listing): + file = listing.resolve_path("dir1/dataset.csv") + src = DataSource(listing, file) + results = list(src.ls(["id", "name", "dir_type"])) + assert {r[1] for r in results} == {"dataset.csv"} + assert results[0][0] == file.id + assert results[0][1] == file.name + assert results[0][2] == DirType.FILE + + +def test_subtree(listing): + dir1 = listing.resolve_path("dir1/") + nodes = listing.subtree_files(dir1) + subtree_files = ["dataset.csv", "file1.csv", "file2.csv"] + _match_filenames([nwp.n for nwp in nodes], subtree_files) + + +def test_subdirs(listing): + dirs = list(listing.get_dirs_by_parent_path("")) + _match_filenames(dirs, ["dir1", "dir2"]) diff --git a/tests/unit/test_metastore.py b/tests/unit/test_metastore.py new file mode 100644 index 000000000..113528850 --- /dev/null +++ b/tests/unit/test_metastore.py @@ -0,0 +1,55 @@ +import base64 +import pickle + +from datachain.data_storage.serializer import deserialize +from datachain.data_storage.sqlite import ( + SQLiteDatabaseEngine, + SQLiteIDGenerator, + SQLiteMetastore, +) +from datachain.storage import StorageURI + + +def test_sqlite_metastore(): + db = SQLiteDatabaseEngine.from_db_file(":memory:") + id_generator = SQLiteIDGenerator(db, table_prefix="prefix") + uri = StorageURI("s3://bucket") + + obj = SQLiteMetastore(id_generator, uri, 1, db) + assert obj.id_generator == id_generator + assert obj.uri == uri + assert obj.partial_id == 1 + assert obj.db == db + + # Test clone + obj2 = obj.clone() + assert isinstance(obj2, SQLiteMetastore) + assert obj2.id_generator.db.db_file == obj.id_generator.db.db_file + assert obj2.id_generator._table_prefix == obj.id_generator._table_prefix + assert obj2.uri == uri + assert obj2.partial_id == 1 + assert obj2.db.db_file == db.db_file + assert obj2.clone_params() == obj.clone_params() + + # Test serialization + serialized = obj.serialize() + assert serialized + serialized_pickled = base64.b64decode(serialized.encode()) + assert serialized_pickled + (f, args, kwargs) = pickle.loads(serialized_pickled) # noqa: S301 + assert str(f) == str(SQLiteMetastore.init_after_clone) + assert args == [] + assert str(kwargs["id_generator_clone_params"]) == str(id_generator.clone_params()) + assert kwargs["uri"] == uri + assert kwargs["partial_id"] == 1 + assert str(kwargs["db_clone_params"]) == str(db.clone_params()) + + # Test deserialization + obj3 = deserialize(serialized) + assert isinstance(obj3, SQLiteMetastore) + assert obj3.id_generator.db.db_file == id_generator.db.db_file + assert obj3.id_generator._table_prefix == id_generator._table_prefix + assert obj3.uri == uri + assert obj3.partial_id == 1 + assert obj3.db.db_file == db.db_file + assert obj3.clone_params() == obj.clone_params() diff --git a/tests/unit/test_query_metrics.py b/tests/unit/test_query_metrics.py new file mode 100644 index 000000000..2572b6158 --- /dev/null +++ b/tests/unit/test_query_metrics.py @@ -0,0 +1,29 @@ +import pytest + +from datachain.query import metrics + + +@pytest.mark.parametrize("value", [42, None, False, 3.14, "bar"]) +def test_query_metrics(value, mocker): + mocker.patch.dict("datachain.query.metrics.metrics", {}, clear=True) + + metrics.set("foo", value) + assert metrics.get("foo") == value + assert metrics.metrics == {"foo": value} + + +@pytest.mark.parametrize("key", [42, None, False, 3.14, [], {}, test_query_metrics]) +def test_query_metrics_bad_key(key): + with pytest.raises(TypeError, match="Key must be a string"): + metrics.set(key, 12) + + +def test_query_metrics_empty_key(): + with pytest.raises(ValueError, match="Key must not be empty"): + metrics.set("", 12) + + +@pytest.mark.parametrize("value", [[], {}, test_query_metrics]) +def test_query_metrics_bad_value(value): + with pytest.raises(TypeError, match="Value must be a string, int, float or bool"): + metrics.set("foo", value) diff --git a/tests/unit/test_query_params.py b/tests/unit/test_query_params.py new file mode 100644 index 000000000..31c53236b --- /dev/null +++ b/tests/unit/test_query_params.py @@ -0,0 +1,39 @@ +import os +from unittest.mock import patch + +import pytest + + +@pytest.mark.parametrize( + "env_var,name,value", + ( + ('{"foo":"bar"}', "foo", "bar"), + ('{"foo":"bar2"}', "foo", "bar2"), + ('{"foo":null}', "foo", None), + ("{}", "foo", None), + ("", "foo", None), + ), +) +def test_query_param(env_var, name, value): + with patch("datachain.query.params.params_cache", None): + with patch.dict(os.environ, {"DATACHAIN_QUERY_PARAMS": env_var}): + from datachain.query.params import param + + assert param(name) == value + + +@pytest.mark.parametrize("env_var", ('"foo"', "[1,2,3]", "null", "1", "true", "foo")) +def test_query_param_error(env_var): + with patch("datachain.query.params.params_cache", None): + with patch.dict(os.environ, {"DATACHAIN_QUERY_PARAMS": env_var}): + from datachain.query.params import param + + with pytest.raises(ValueError): + param("foo") + + +def test_query_param_key_error(): + from datachain.query.params import param + + with pytest.raises(TypeError): + param(12) diff --git a/tests/unit/test_serializer.py b/tests/unit/test_serializer.py new file mode 100644 index 000000000..8f59c8e6c --- /dev/null +++ b/tests/unit/test_serializer.py @@ -0,0 +1,92 @@ +import base64 +import pickle + +import pytest + +from datachain.data_storage.serializer import Serializable, deserialize + + +class MySerializableInit(Serializable): + def __init__(self, name, optional=None): + self.name = name + self.optional = optional + + def clone_params(self): + return MySerializableInit, [self.name], {"optional": self.optional} + + def get_params(self): + return self.name, self.optional + + +class MySerializableFunc(Serializable): + def __init__(self, name, optional=None): + self.name = name + self.optional = optional + + @classmethod + def from_params(cls, name, optional=None): + return cls(name, optional=optional) + + def clone_params(self): + return self.from_params, [self.name], {"optional": self.optional} + + def get_params(self): + return self.name, self.optional + + +class MySerializableNoParams(Serializable): + def clone_params(self): + return MySerializableNoParams, [], {} + + +@pytest.mark.parametrize( + "cls,call", + [ + (MySerializableInit, MySerializableInit), + (MySerializableFunc, MySerializableFunc.from_params), + ], +) +@pytest.mark.parametrize( + "name,optional", + [ + (None, None), + ("foo", None), + (None, 12), + ("bar", 24), + ], +) +def test_serializable_init(cls, call, name, optional): + obj = cls(name, optional=optional) + assert obj.clone_params() == (call, [name], {"optional": optional}) + + serialized = obj.serialize() + assert serialized + serialized_pickled = base64.b64decode(serialized.encode()) + assert serialized_pickled + (f, args, kwargs) = pickle.loads(serialized_pickled) # noqa: S301 + assert str(f) == str(call) + assert args == [name] + assert kwargs == {"optional": optional} + + obj2 = deserialize(serialized) + assert isinstance(obj2, cls) + assert obj2.name == name + assert obj2.optional == optional + assert obj2.get_params() == (name, optional) + + +def test_serializable_init_no_params(): + obj = MySerializableNoParams() + assert obj.clone_params() == (MySerializableNoParams, [], {}) + + serialized = obj.serialize() + assert serialized + serialized_pickled = base64.b64decode(serialized.encode()) + assert serialized_pickled + (f, args, kwargs) = pickle.loads(serialized_pickled) # noqa: S301 + assert f == MySerializableNoParams + assert args == [] + assert kwargs == {} + + obj2 = deserialize(serialized) + assert isinstance(obj2, MySerializableNoParams) diff --git a/tests/unit/test_session.py b/tests/unit/test_session.py new file mode 100644 index 000000000..33c680ee7 --- /dev/null +++ b/tests/unit/test_session.py @@ -0,0 +1,62 @@ +import re + +import pytest +import sqlalchemy as sa + +from datachain.error import DatasetNotFoundError +from datachain.query import DatasetQuery, Session +from datachain.sql.types import String + + +def test_ephemeral_dataset_naming(catalog): + session_name = "qwer45" + + with pytest.raises(ValueError): + Session("wrong-ds_name", catalog=catalog) + + with Session(session_name, catalog=catalog) as session: + ds_name = "my_test_ds12" + session.catalog.create_dataset(ds_name, columns=(sa.Column("name", String),)) + ds_tmp = DatasetQuery( + name=ds_name, session=session, catalog=session.catalog + ).save() + session_uuid = f"[0-9a-fA-F]{{{Session.SESSION_UUID_LEN}}}" + table_uuid = f"[0-9a-fA-F]{{{Session.TEMP_TABLE_UUID_LEN}}}" + + name_prefix = f"{Session.DATASET_PREFIX}{session_name}" + pattern = rf"^{name_prefix}_{session_uuid}_{table_uuid}$" + + assert re.match(pattern, ds_tmp.name) is not None + + +def test_global_session_naming(catalog): + session_uuid = f"[0-9a-fA-F]{{{Session.SESSION_UUID_LEN}}}" + table_uuid = f"[0-9a-fA-F]{{{Session.TEMP_TABLE_UUID_LEN}}}" + + ds_name = "qwsd" + catalog.create_dataset(ds_name, columns=(sa.Column("name", String),)) + ds_tmp = DatasetQuery(name=ds_name, catalog=catalog).save() + global_prefix = f"{Session.DATASET_PREFIX}{Session.GLOBAL_SESSION_NAME}" + pattern = rf"^{global_prefix}_{session_uuid}_{table_uuid}$" + assert re.match(pattern, ds_tmp.name) is not None + + +def test_ephemeral_dataset_lifecycle(catalog): + session_name = "asd3d4" + with Session(session_name, catalog=catalog) as session: + ds_name = "my_test_ds12" + session.catalog.create_dataset(ds_name, columns=(sa.Column("name", String),)) + ds_tmp = DatasetQuery( + name=ds_name, session=session, catalog=session.catalog + ).save() + + assert isinstance(ds_tmp, DatasetQuery) + assert ds_tmp.name != ds_name + assert ds_tmp.name.startswith(Session.DATASET_PREFIX) + assert session_name in ds_tmp.name + + ds = catalog.get_dataset(ds_tmp.name) + assert ds is not None + + with pytest.raises(DatasetNotFoundError): + catalog.get_dataset(ds_tmp.name) diff --git a/tests/unit/test_storage.py b/tests/unit/test_storage.py new file mode 100644 index 000000000..8fe67e64e --- /dev/null +++ b/tests/unit/test_storage.py @@ -0,0 +1,222 @@ +from datetime import datetime, timedelta, timezone + +import pytest +from sqlalchemy import func + +from datachain import utils +from datachain.error import StorageNotFoundError +from datachain.storage import STALE_MINUTES_LIMIT, Storage, StorageStatus, StorageURI +from tests.utils import skip_if_not_sqlite + +TS = datetime(2022, 8, 1) +EXPIRES = datetime(2022, 8, 2) + + +def test_human_time(): + assert utils.human_time_to_int("1236") == 1236 + assert utils.human_time_to_int("3h") == 3 * 60 * 60 + assert utils.human_time_to_int("2w") == 2 * 7 * 24 * 60 * 60 + assert utils.human_time_to_int("4M") == 4 * 31 * 24 * 60 * 60 + + assert utils.human_time_to_int("bla") is None + + +def test_storage(): + s = Storage("s3://foo", TS, EXPIRES) + + d = s.to_dict() + assert d.get("uri") == s.uri + + +def test_expiration_time(): + assert Storage.get_expiration_time(TS, 12344) == TS + timedelta(seconds=12344) + + +def test_adding_storage(metastore): + uri = StorageURI("s3://whatever") + with pytest.raises(StorageNotFoundError): + metastore.get_storage(uri) + + storage, _, _, _, _ = metastore.register_storage_for_indexing(uri) + cnt = next(metastore.db.execute(metastore._storages_select(func.count())), (0,)) + assert cnt[0] == 1 + + bkt = list( + metastore.db.execute( + metastore._storages_select().where(metastore._storages.c.uri == uri) + ) + ) + assert len(bkt) == 1 + + assert storage.id == bkt[0][0] + assert storage.uri == bkt[0][1] + assert storage.timestamp == bkt[0][2] + assert storage.expires == bkt[0][3] + assert storage.started_inserting_at == bkt[0][4] + assert storage.last_inserted_at == bkt[0][5] + assert storage.status == bkt[0][6] + assert storage.error_message == "" + assert storage.error_stack == "" + + +def test_storage_status(metastore): + uri = StorageURI("s3://somebucket") + + metastore.create_storage_if_not_registered(uri) + storage = metastore.get_storage(uri) + assert storage.uri == uri + assert storage.status == StorageStatus.CREATED + + ( + storage, + need_index, + in_progress, + partial_id, + partial_path, + ) = metastore.register_storage_for_indexing(uri) + assert storage.status == StorageStatus.PENDING + assert storage.uri == uri + assert storage == metastore.get_storage(uri) + assert need_index is True + assert in_progress is False + assert partial_id is None + assert partial_path is None + + ( + s2, + need_index, + in_progress, + partial_id, + partial_path, + ) = metastore.register_storage_for_indexing(uri) + assert s2.status == StorageStatus.PENDING + assert storage == s2 == metastore.get_storage(uri) + assert need_index is False + assert in_progress is True + assert partial_id is None + assert partial_path is None + + end_time = datetime.now(timezone.utc) + metastore.mark_storage_indexed(uri, StorageStatus.COMPLETE, 1000, end_time) + storage = metastore.get_storage(uri) + assert storage.status == StorageStatus.COMPLETE + + +@pytest.mark.parametrize( + "ttl", + (-1, 999999999999, 99999999999999, 9999999999999999), +) +def test_max_ttl(ttl): + uri = "s3://whatever" + expires = Storage.get_expiration_time(TS, ttl) + storage = Storage(1, uri, TS, expires) + assert storage.timestamp == TS + assert storage.expires == datetime.max + assert storage.timestamp_str # no error + assert storage.timestamp_to_local # no error + assert storage.expires_to_local # no error + + +def test_storage_without_dates(): + uri = "s3://whatever" + storage = Storage(1, uri, None, None) + assert storage.timestamp is None + assert storage.expires is None + assert storage.timestamp_str is None # no error + assert storage.timestamp_to_local is None # no error + assert storage.expires_to_local is None # no error + assert storage.to_dict() == { + "uri": uri, + "timestamp": None, + "expires": None, + } + + +def test_storage_update_last_inserted_at(metastore): + uri = StorageURI("s3://bucket_last_inserted") + metastore.create_storage_if_not_registered(uri) + metastore.update_last_inserted_at(uri) + storage = metastore.get_storage(uri) + assert storage.last_inserted_at + + +def test_stale_storage(metastore): + uri_stale = StorageURI("s3://bucket_stale") + uri_not_stale = StorageURI("s3://bucket_not_stale") + + metastore.create_storage_if_not_registered(uri_stale) + metastore.create_storage_if_not_registered(uri_not_stale) + + metastore.mark_storage_pending(metastore.get_storage(uri_stale)) + metastore.mark_storage_pending(metastore.get_storage(uri_not_stale)) + + # make storage looks stale + updates = { + "last_inserted_at": datetime.now(timezone.utc) + - timedelta(minutes=STALE_MINUTES_LIMIT + 1) + } + s = metastore._storages + metastore.db.execute(s.update().where(s.c.uri == uri_stale).values(**updates)) + + metastore.find_stale_storages() + + stale_storage = metastore.get_storage(uri_stale) + assert stale_storage.status == StorageStatus.STALE + + not_stale_storage = metastore.get_storage(uri_not_stale) + assert not_stale_storage.status == StorageStatus.PENDING + + +def test_failed_storage(metastore): + uri = StorageURI("s3://bucket") + error_message = "Internal error on indexing" + error_stack = "error" + metastore.create_storage_if_not_registered(uri) + + metastore.mark_storage_pending(metastore.get_storage(uri)) + metastore.mark_storage_indexed( + uri, + StorageStatus.FAILED, + 1000, + datetime.now(), + error_message=error_message, + error_stack=error_stack, + ) + + storage = metastore.get_storage(uri) + assert storage.status == StorageStatus.FAILED + assert storage.error_message == error_message + assert storage.error_stack == error_stack + + +def test_unlist_source( + listed_bucket, + cloud_test_catalog, + cloud_type, +): + # TODO remove when https://github.com/iterative/dvcx/pull/868 is merged + skip_if_not_sqlite() + source_uri = cloud_test_catalog.src_uri + catalog = cloud_test_catalog.catalog + _partial_id, partial_path = catalog.metastore.get_valid_partial_id( + cloud_test_catalog.storage_uri, cloud_test_catalog.partial_path + ) + storage_dataset_name = Storage.dataset_name( + cloud_test_catalog.storage_uri, partial_path + ) + + # list source + storage = catalog.get_storage(cloud_test_catalog.storage_uri) + if cloud_type == "file": + assert storage.status == StorageStatus.PARTIAL + else: + assert storage.status == StorageStatus.COMPLETE + + catalog.get_dataset(storage_dataset_name) + + # unlist source + catalog.unlist_source(source_uri) + with pytest.raises(StorageNotFoundError): + catalog.get_storage(source_uri) + # we preserve the table for dataset lineage + catalog.get_dataset(storage_dataset_name) diff --git a/tests/unit/test_udf.py b/tests/unit/test_udf.py new file mode 100644 index 000000000..39ba4b6bb --- /dev/null +++ b/tests/unit/test_udf.py @@ -0,0 +1,148 @@ +import pytest +from sqlalchemy import Integer + +from datachain.dataset import RowDict +from datachain.query import udf +from datachain.query.batch import RowBatch +from datachain.query.schema import ColumnParameter + + +def test_udf_single_signal(): + @udf(("id", "size"), {"mul": Integer}) + def t(a, b): + return (a * b,) + + row = RowDict( + id=6, + vtype="", + dir_type=1, + parent="", + name="obj", + last_modified=None, + etag="", + version="", + is_latest=True, + size=7, + owner_name="", + owner_id="", + source="", + random=1234, + location=None, + ) + result = t(None, row) + assert result[0]["mul"] == (42) + + +def test_udf_multiple_signals(): + @udf(("id", "size"), {"mul": Integer, "sum": Integer}) + def t(a, b): + return (a * b, a + b) + + row = RowDict( + id=6, + vtype="", + dir_type=1, + parent="", + name="obj", + last_modified=None, + etag="", + version="", + is_latest=True, + size=7, + owner_name="", + owner_id="", + source="", + random=1234, + location=None, + ) + result = t(None, row) + assert result[0] == {"id": 6, "mul": 42, "sum": 13} + + +def test_udf_batching(): + @udf(("id", "size"), {"mul": Integer}, batch=4) + def t(vals): + return [(a * b,) for (a, b) in vals] + + inputs = list(zip(range(1, 11), range(21, 31))) + results = [] + for size, row_id in inputs: + row = RowDict( + id=row_id, + vtype="", + dir_type=1, + parent="", + name="obj", + last_modified=None, + etag="", + version="", + is_latest=True, + size=size, + owner_name="", + owner_id="", + source="", + random=1234, + location=None, + ) + batch = RowBatch([row]) + result = t(None, batch) + if result: + assert len(result) == 1 # Matches batch size. + results.extend(result) + + assert len(results) == len(inputs) + assert results == [{"id": b, "mul": a * b} for (a, b) in inputs] + + +def test_stateful_udf(): + @udf(("size",), {"sum": Integer}, method="sum") + class MyUDF: + def __init__(self, constant): + self.constant = constant + + def sum(self, size): + return (self.constant + size,) + + udf_inst = MyUDF(5)() + inputs = range(1, 11) + results = [] + for size in inputs: + row = RowDict( + id=5, + vtype="", + dir_type=1, + parent="", + name="obj", + last_modified=None, + etag="", + version="", + is_latest=True, + size=size, + owner_name="", + owner_id="", + source="", + random=1234, + location=None, + ) + results.extend(udf_inst(None, row)) + + assert len(results) == len(inputs) + assert results == [{"id": 5, "sum": 5 + size} for size in inputs] + + +@pytest.mark.parametrize("param", ["foo", ("foo",)]) +def test_udf_api(param): + func = lambda x: x # noqa: E731 + result = udf(param, {"bar": Integer}, batch=42)(func) + assert result.func is func + assert result.properties.params == [ColumnParameter("foo")] + assert result.properties.output == {"bar": Integer} + assert result.properties.batch == 42 + + +def test_udf_error(): + with pytest.raises(TypeError): + + @udf(params=("name",), output=("name_len",)) + def name_len(name): + return len(name) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py new file mode 100644 index 000000000..12e705348 --- /dev/null +++ b/tests/unit/test_utils.py @@ -0,0 +1,180 @@ +import os +from textwrap import dedent + +import pytest + +from datachain.utils import ( + datachain_paths_join, + determine_processes, + import_object, + retry_with_backoff, + sizeof_fmt, + sql_escape_like, + suffix_to_number, +) + +DATACHAIN_TEST_PATHS = ["/file1", "file2", "/dir/file3", "dir/file4"] +DATACHAIN_EX_ROOT = ["/file1", "/file2", "/dir/file3", "/dir/file4"] +DATACHAIN_EX_SUBDIR = [ + "subdir/file1", + "subdir/file2", + "subdir/dir/file3", + "subdir/dir/file4", +] +DATACHAIN_EX_DOUBLE_SUBDIR = [ + "subdir/double/file1", + "subdir/double/file2", + "subdir/double/dir/file3", + "subdir/double/dir/file4", +] + + +@pytest.mark.parametrize( + "src,paths,expected", + ( + ("", DATACHAIN_TEST_PATHS, DATACHAIN_EX_ROOT), + ("/", DATACHAIN_TEST_PATHS, DATACHAIN_EX_ROOT), + ("/*", DATACHAIN_TEST_PATHS, DATACHAIN_EX_ROOT), + ("/file*", DATACHAIN_TEST_PATHS, DATACHAIN_EX_ROOT), + ("subdir", DATACHAIN_TEST_PATHS, DATACHAIN_EX_SUBDIR), + ("subdir/", DATACHAIN_TEST_PATHS, DATACHAIN_EX_SUBDIR), + ("subdir/*", DATACHAIN_TEST_PATHS, DATACHAIN_EX_SUBDIR), + ("subdir/file*", DATACHAIN_TEST_PATHS, DATACHAIN_EX_SUBDIR), + ("subdir/double", DATACHAIN_TEST_PATHS, DATACHAIN_EX_DOUBLE_SUBDIR), + ("subdir/double/", DATACHAIN_TEST_PATHS, DATACHAIN_EX_DOUBLE_SUBDIR), + ("subdir/double/*", DATACHAIN_TEST_PATHS, DATACHAIN_EX_DOUBLE_SUBDIR), + ("subdir/double/file*", DATACHAIN_TEST_PATHS, DATACHAIN_EX_DOUBLE_SUBDIR), + ), +) +def test_datachain_paths_join(src, paths, expected): + assert list(datachain_paths_join(src, paths)) == expected + + +@pytest.mark.parametrize( + "num,suffix,si,expected", + ( + (1, "", False, " 1"), + (536, "", False, " 536"), + (1000, "", False, "1000"), + (1000, "", True, "1.0K"), + (1000, " tests", False, "1000 tests"), + (1000, " tests", True, "1.0K tests"), + (100000, "", False, "97.7K"), + (100000, "", True, "100.0K"), + (1000000, "", True, "1.0M"), + (1000000000, "", True, "1.0G"), + (1000000000000, "", True, "1.0T"), + (1000000000000000, "", True, "1.0P"), + (1000000000000000000, "", True, "1.0E"), + (1000000000000000000000, "", True, "1.0Z"), + (1000000000000000000000000, "", True, "1.0Y"), + (1000000000000000000000000000, "", True, "1.0R"), + (1000000000000000000000000000000, "", True, "1.0Q"), + ), +) +def test_sizeof_fmt(num, suffix, si, expected): + assert sizeof_fmt(num, suffix, si) == expected + + +@pytest.mark.parametrize( + "text,expected", + ( + ("1", 1), + ("50", 50), + ("1K", 1024), + ("1k", 1024), + ("2M", 1024 * 1024 * 2), + ), +) +def test_suffix_to_number(text, expected): + assert suffix_to_number(text) == expected + + +@pytest.mark.parametrize( + "text", + ( + "", + "Bogus", + "50H", + ), +) +def test_suffix_to_number_invalid(text): + with pytest.raises(ValueError): + suffix_to_number(text) + + +@pytest.mark.parametrize( + "text,expected", + ( + ("test like", "test like"), + ("Can%t \\escape_this", "Can\\%t \\\\escape\\_this"), + ), +) +def test_sql_escape_like(text, expected): + assert sql_escape_like(text) == expected + + +def test_import_object(tmp_path): + fname = tmp_path / "foo.py" + code = """\ + def hello(): + return "Hello!" + """ + fname.write_text(dedent(code)) + func = import_object(f"{fname}:hello") + assert func() == "Hello!" + + +def test_import_object_relative(tmp_path, monkeypatch): + fname = tmp_path / "foo.py" + code = """\ + def hello(): + return "Hello!" + """ + fname.write_text(dedent(code)) + monkeypatch.chdir(tmp_path) + func = import_object("foo.py:hello") + assert func() == "Hello!" + + +def test_retry_with_backoff(): + called = 0 + retries = 2 + + @retry_with_backoff(retries=retries, backoff_sec=0.05) + def func_with_exception(): + nonlocal called + called += 1 + raise RuntimeError("Error") + + with pytest.raises(RuntimeError): + func_with_exception() + assert called == retries + 1 + + called = 0 # resetting called + + @retry_with_backoff(retries=retries, backoff_sec=0.05) + def func_ok(): + nonlocal called + called += 1 + + func_ok() + assert called == 1 + + +@pytest.mark.parametrize( + "parallel,settings,expected", + ( + (None, None, False), + (None, "-1", True), + (None, "0", False), + (None, "5", 5), + (-1, "5", True), + (0, "5", False), + (10, "5", 10), + ), +) +def test_determine_processes(parallel, settings, expected): + if settings is not None: + os.environ["DATACHAIN_SETTINGS_PARALLEL"] = settings + assert determine_processes(parallel) == expected diff --git a/tests/unit/test_warehouse.py b/tests/unit/test_warehouse.py new file mode 100644 index 000000000..b1b27a255 --- /dev/null +++ b/tests/unit/test_warehouse.py @@ -0,0 +1,63 @@ +import base64 +import pickle + +from datachain.data_storage.serializer import deserialize +from datachain.data_storage.sqlite import ( + SQLiteDatabaseEngine, + SQLiteIDGenerator, + SQLiteWarehouse, +) + + +def test_serialize(): + db = SQLiteDatabaseEngine.from_db_file(":memory:") + id_generator = SQLiteIDGenerator(db, table_prefix="prefix") + + obj = SQLiteWarehouse(id_generator, db) + assert obj.id_generator == id_generator + assert obj.db == db + + # Test clone + obj2 = obj.clone() + assert isinstance(obj2, SQLiteWarehouse) + assert obj2.id_generator.db.db_file == obj.id_generator.db.db_file + assert obj2.id_generator._table_prefix == obj.id_generator._table_prefix + assert obj2.db.db_file == db.db_file + assert obj2.clone_params() == obj.clone_params() + + # Test serialization + serialized = obj.serialize() + assert serialized + serialized_pickled = base64.b64decode(serialized.encode()) + assert serialized_pickled + (f, args, kwargs) = pickle.loads(serialized_pickled) # noqa: S301 + assert str(f) == str(SQLiteWarehouse.init_after_clone) + assert args == [] + assert str(kwargs["id_generator_clone_params"]) == str(id_generator.clone_params()) + assert str(kwargs["db_clone_params"]) == str(db.clone_params()) + + # Test deserialization + obj3 = deserialize(serialized) + assert isinstance(obj3, SQLiteWarehouse) + assert obj3.id_generator.db.db_file == id_generator.db.db_file + assert obj3.id_generator._table_prefix == id_generator._table_prefix + assert obj3.db.db_file == db.db_file + assert obj3.clone_params() == obj.clone_params() + + +def test_is_temp_table_name(warehouse): + assert warehouse.is_temp_table_name("tmp_vc12F") is True + assert warehouse.is_temp_table_name("udf_jh653") is True + assert warehouse.is_temp_table_name("ds_shadow_12345") is True + assert warehouse.is_temp_table_name("old_ds_shadow") is True + assert warehouse.is_temp_table_name("ds_my_dataset") is False + assert warehouse.is_temp_table_name("src_my_bucket") is False + assert warehouse.is_temp_table_name("ds_ds_my_query_script_1_1") is False + + +def test_dataset_stats_no_table(cloud_test_catalog, dogs_dataset): + catalog = cloud_test_catalog.catalog + catalog.warehouse.drop_dataset_rows_table(dogs_dataset, 1) + num_objects, size = catalog.warehouse.dataset_stats(dogs_dataset, 1) + assert num_objects is None + assert size is None diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 000000000..b127af55d --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,270 @@ +import dataclasses +import io +import math +import os +import tarfile +from string import printable +from tarfile import DIRTYPE, TarInfo +from time import sleep, time +from typing import Any, Callable, Optional + +import pytest + +from datachain.catalog.catalog import Catalog +from datachain.dataset import DatasetDependency, DatasetRecord +from datachain.query import C, DatasetQuery +from datachain.query.builtins import index_tar +from datachain.storage import StorageStatus + + +def make_index(catalog, src: str, entries, ttl: int = 1234): + lst, _ = catalog.enlist_source(src, ttl, skip_indexing=True) + lst.insert_entries(entries) + lst.insert_entries_done() + lst.metastore.mark_storage_indexed( + src, + StorageStatus.COMPLETE, + ttl=ttl, + prefix="", + partial_id=lst.metastore.partial_id, + ) + + +DEFAULT_TREE: dict[str, Any] = { + "description": "Cats and Dogs", + "cats": { + "cat1": "meow", + "cat2": "mrow", + }, + "dogs": { + "dog1": "woof", + "dog2": "arf", + "dog3": "bark", + "others": {"dog4": "ruff"}, + }, +} +NUM_TREE = {f"{i:06d}": f"{i}" for i in range(1024)} + + +def instantiate_tree(path, tree): + for key, value in tree.items(): + if isinstance(value, str): + (path / key).write_text(value) + elif isinstance(value, bytes): + (path / key).write_bytes(value) + elif isinstance(value, dict): + (path / key).mkdir() + instantiate_tree(path / key, value) + else: + raise TypeError(f"{value=}") + + +def tree_from_path(path, binary=False): + tree = {} + for child in path.iterdir(): + if child.is_dir(): + tree[child.name] = tree_from_path(child, binary) + else: # noqa: PLR5501 + if binary: + tree[child.name] = child.read_bytes() + else: + tree[child.name] = child.read_text() + return tree + + +def uppercase_scheme(uri: str) -> str: + """ + Makes scheme (or protocol) of an url uppercased + e.g s3://bucket_name -> S3://bucket_name + """ + return f'{uri.split(":")[0].upper()}:{":".join(uri.split(":")[1:])}' + + +def make_tar(tree) -> bytes: + with io.BytesIO() as tmp: + with tarfile.open(fileobj=tmp, mode="w") as archive: + write_tar(tree, archive) + return tmp.getvalue() + + +def write_tar(tree, archive, curr_dir=""): + for key, value in tree.items(): + name = f"{curr_dir}/{key}" if curr_dir else key + if isinstance(value, str): + value = value.encode("utf-8") + if isinstance(value, bytes): + info = TarInfo(name) + info.size = len(value) + f = io.BytesIO(value) + archive.addfile(info, f) + elif isinstance(value, dict): + info = TarInfo(name) + info.type = DIRTYPE + archive.addfile(info, io.BytesIO()) + write_tar(value, archive, name) + + +TARRED_TREE: dict[str, Any] = {"animals.tar": make_tar(DEFAULT_TREE)} + + +def create_tar_dataset(catalog, uri: str, ds_name: str) -> DatasetQuery: + """ + Create a dataset from a storage location containing tar archives and other files. + + The resulting dataset contains both the original files (as regular objects) + and the tar members (as v-objects). + """ + ds1 = DatasetQuery(path=uri, catalog=catalog) + tar_entries = ds1.filter(C("name").glob("*.tar")).generate(index_tar) + return ds1.filter(~C("name").glob("*.tar")).union(tar_entries).save(ds_name) + + +def skip_if_not_sqlite(): + if os.environ.get("DATACHAIN_METASTORE") or os.environ.get("DATACHAIN_WAREHOUSE"): + pytest.skip("This test is not supported on other data storages") + + +WEBFORMAT_TREE: dict[str, Any] = { + "f1.raw": "raw data", + "f1.json": '{"similarity": 0.001, "md5": "deadbeef"}', + "f2.raw": "raw data", + "f2.json": '{"similarity": 0.005, "md5": "foobar"}', +} + + +def text_embedding(text: str) -> list[float]: + """ + Compute a simple text embedding based on character counts. + + These aren't the most meaningful, but will produce a 100-element + vector of floats between 0 and 1 where texts with similar + character counts will have similar embeddings. Useful for writing + unit tests without loading a heavy ML model. + """ + emb = dict.fromkeys(printable, 0.01) + for c in text: + try: + emb[c] += 1.0 + except KeyError: + pass + # sqeeze values between 0 and 1 with an adjusted sigmoid function + return [2.0 / (1.0 + math.e ** (-x)) - 1.0 for x in emb.values()] + + +SIMPLE_DS_QUERY_RECORDS = [ + { + "parent": "", + "name": "description", + "vtype": "", + "dir_type": 0, + "is_latest": 1, + "size": 13, + }, + { + "parent": "cats", + "name": "cat1", + "vtype": "", + "dir_type": 0, + "is_latest": 1, + "size": 4, + }, + { + "parent": "cats", + "name": "cat2", + "vtype": "", + "dir_type": 0, + "is_latest": 1, + "size": 4, + }, + { + "parent": "dogs", + "name": "dog1", + "vtype": "", + "dir_type": 0, + "is_latest": 1, + "size": 4, + }, + { + "parent": "dogs", + "name": "dog2", + "vtype": "", + "dir_type": 0, + "is_latest": 1, + "size": 3, + }, + { + "parent": "dogs", + "name": "dog3", + "vtype": "", + "dir_type": 0, + "is_latest": 1, + "size": 4, + }, + { + "parent": "dogs/others", + "name": "dog4", + "vtype": "", + "dir_type": 0, + "is_latest": 1, + "size": 4, + }, +] + + +def get_simple_ds_query(path, catalog): + return ( + DatasetQuery(path=path, catalog=catalog) + .select(C.parent, C.name, C.vtype, C.dir_type, C.is_latest, C.size) + .order_by(C.source, C.parent, C.name) + ) + + +def dataset_dependency_asdict( + dep: Optional[DatasetDependency], +) -> Optional[dict[str, Any]]: + """ + Converting to dict with making sure we don't have any additional fields + that could've been added with subclasses + """ + if not dep: + return None + parsed = { + k: dataclasses.asdict(dep)[k] for k in DatasetDependency.__dataclass_fields__ + } + + if dep.dependencies: + parsed["dependencies"] = [ + dataset_dependency_asdict(d) for d in dep.dependencies + ] + + return parsed + + +def wait_for_condition( + callback: Callable, + message: str, + check_interval: float = 0.01, + timeout: float = 1.0, +) -> Any: + start_time = time() + while time() - start_time < timeout: + result = callback() + if result: + return result + sleep(check_interval) + raise TimeoutError(f"Timeout expired while waiting for: {message}") + + +def assert_row_names( + catalog: Catalog, dataset: DatasetRecord, version: int, expected_names: set +) -> None: + dataset_rows = catalog.ls_dataset_rows(dataset.name, version, limit=20) + assert dataset_rows + preview = dataset.get_version(version).preview + assert preview + + assert ( + {r["name"] for r in dataset_rows} + == {r.get("name") for r in preview} + == expected_names + )

    Mw6^Lr|7+8B@9(C6$j$4zsxE$wo@nVQWk zeY~RMSo@fHd8ZXGIA=YMZPi@+d8Fe=g}q}M=DM}hMb&WAAlIUPRoUh~XWx{TYHD~! zru8`6XQNi0_T6chlCO4ga_C`UZEYQxA2}=`t897xku3k3u#cTruF@>M5$m|?!8f(H zsPJN42L5I*kYT*+$p+80R zZ_pIrB{uoZH%*x`7=gVk5n`v(UakIo#Es-Lk2J#{n{9I6NBr_D``P&J@Z@#+{o3zQ za8c`b;j?P)jrKJ+2ev*+GfL{ruxoF-tLo)!_4ZPdYm5hE zT48&-YGcJL3QmEbVl^ns?9iNXVGnVfE8TxK2BBX>G)q zaE*bnr_D9h)o(8x`1;Ko<6vJS)8z#N#{8Jwp)@@rGUB>rj%%s5Y1OY^$@$;hwI3(s zZgh$*6L(?6;PPP2`a}M#->|_nEaK4pl@>4fSVkr$+YJI4A$fnnIlJr5rX6aBTG+Us za`d>~(L&w$kh|YtU`qIP<(UVSbEX^{T=wachWhBwrrvp_Cv58;dKmiu&c3wF)^^dd zWphj1(rRA02MmsQcI(;Y9bamFQlG~^x^Eh}d_`1#=!`GAKeBsT{273rqCceT_uloc zr_}OR`FmH@UsY5cGbX`MdA@7f;|1~GU8Clj>Xr_3H0Yil*YC~es_Q>$u7B0pV;Fu%#?Bah-ohg^F6Wo?vUFx)`>)y1J-QrEmOi&G3aY4PG$ zy#D(r{ff=rfA>gmojnkBfYto@$zM#b-upe+<>%a}-0#H+I%b-`>c^$!#1;SevM)Vu ze#e@i@NkEw*^rUtKYn!l6W~5FG%jCSZ(_FA zBjw}Aaqo(p-z>6tsUPuv)by^qHV-I&aedCOI_156Ld*Jm_H{2?_V0=f7pLf06)x)a zKb*0T{+5Hg5DD|216`JHmU->JwD{@bb`-l#r?SYZ(JsLT33^caWU;FIP+gJI~7kW7aKN?g0++ga4K74k)#hM4YbvO{;C=GJ%iGM$>CyR7O=|3C7 z_j8}zx3Su?kwd)0Jl^*FHue3f8`u7L3jB2ax+>t#*=kEZR!~p=s^V?Cfrn1b*_rtD z>-1-UPM=jf>i~MHOwK&>XwUy2W@o;9%t1AStxdbny?4-m$y3YbExc0ObiViYzzF#o z|NlSrCT9nP@TUua!}IoZ(b{4Ef0OTj{(Jq{{~k3TF=2~~(W26V*1xiSi|>3tR^6;_ zWA1zXPx|Kdo4Z_JP-Sc};>Y0pxcTlUN`KDGTYhs&Ubn6($BrIcZ$9Nx;o~{JCn8ro z3j5^lI_+11x@Fk$_oV8_&r8NE8oklRs`;M@{jM#u+MTx2N{G6Ub5(6Z3y=DCd5Jr8 z?iGLgwL0g_#Vyy4{@StZe(^Vj9||uwZ%%03(DLZ~DFxp?j!O(;G$Zfm{s=GHSOE^Unib0{%o;gjKgrv z+N-{$SwC7huF8G4^zwBBx2)wYUOpJ}W^j)HHNOWRTWdre@OZHCyQ@;o(=mZvHGI74 zx3A7u%vzjhHuF_vtB3@Lk!slwnmYI`uG?Vi;+WgDwauvV?xFU7xBOui(kRQtoNZTJiJO3LFFW0`iu2-a4xN&$U{OqsMfuWfn6+A3g-8!|FsvE44 za`p9>fWuYGbUl(}?;FzXTLS`^+(T_b_u?^pjH}z#C##@l=;h)DJVuDhq9Q_S@{SkV zM4#j5S3UN@hJ^p-sE|9couocj@e8FL0NqyWsL8Oa*s_BZ%DTE*QXSOUH$AiSF&2FE$D^+PylrgWvR>hQVRoUqmYFT>W9Os^^Ys z<~MiyPku8vx0^8X3_uJT#DL#|9_ea-kL#dVwJg5+%7yWWxeWWLTKB5-_3-rF_aqB# zu<3)l9x7Rm=gh(nq032M8a+QU!F3o#23klg-v7DQrz0cj*W9W!nC#&jAl=(yS>cLo znL(~=6*c$|?~RCvNU%!T{U#9M-KKl0TylqeKD-Wv5hH=V zua^BvU|NsQE#obyWxcA~zPhbCV(o5W9*{CAD4cZ<-RmCOIIs+n-hH%BE9dM+%v%9( z%*@7BL**kL@2X8R17KHC^U#vlUUiF`lW&@N(zWJORAnEBK6iqr9qok0Kd zq|>0@Vrb2moO{7)5yD7Z91h?sDLx8GI~2Ld2+y`4SQWF$*Yy$MD?Fe~P-rg(V2HIC zJoiY)+>N7GL=1RaQBhG1QkFdQtBm$9(u#THV7uWl`c;-0NI%5~!4p~*JD~zw3c@Nt z@akeiAVcO4h z6Z#vz-7^ydzo}-w+@`j+W?i$EH(%Khq88mBMu9J{Irl;t1p_b+bDC*EY}xxPTMAc-bcQXA zg7?K)7AUCr+57=-&}(YifTwkN)*(@MefYg&+B$5m)P4 zbdBs&P>^VIJ@^DO|0BTbgvKQH8|I&gOk1_d!}P_9D9f~lxz3K32Tboj8kyeu>SLd@ zV*Fb@k*R?AX`~+*Yg)Zww$fWqlo$x=#q|~`@FFHzSo3sh*Xod^V=D;gNPA>2gyI%Z{UFuJoVX^3PO}M`vog;Mp?m2EmN3hbqZ4dG)lC zwPT*nWe6rDL}m?TGzwP0lX?H#Tz{CUr!fw5p%A}}U;gvU zYg!9#?ep0 zFz^d#?ds}EZYn7$kqPRyZF_39rHsRC^=?MSAs7wR2@r~}VPPQfSM#Jjw{7Et6q0us z2IAj_nnkMcBnlgiK$8Sw%+W{3PyNVFmTe&+YV#t^tjy7^8nG=#sA#|G|M-e% zYp&CE=c2!2>@(Z?c1P}qo2fhlpe8B`xOX}s8Rumn2a&G$FeoS_Wo9ZP=Yc?zpLpkv ztO4Mg3vUC&Q9Fip2s}8{_Ree9w$Ja+JceSPPV_8j)urjjZF1$i06C%b;+>>oQn%KH ziG3c%8%PHDbHwAXc{*?j&@f(cad9s%Fd@uGk%pDO1v3%5zu%%o0mqI_!BYj9IubEj zyM{cr!l6Lc@`PPoo2_&VTeD`(a`0)=n$^wq5Z2vJoH#*_0Q;w4L*#eYCR!ls+S=L) zrMD54Ls0QikOvh(KfjwA^@o4+{RDu!~oXm~viXW`quB1Bo8RBEb+fGSI1O@h<3R@lAl)^hy(`LlBW-ppk-z%QbaIKH9+C&cXJ?E;NWx4gIa=uy#ib|JXQ z7$xTYOtSM{eop&V^0?=U3t*_}%y`-5--uD~7E!80K_j!eOc666i9ZHhCW7W;Y!3pM z?MZRz`OANL-(FmB<_MmJnQ9t)TlVk3BlR2VaOBz{$Z1qNMaU;PnyfdW#N>8}xROy? z$SF!!8Aj%t5;_Wtizn_^8ET~A3$PC?Df|f$6XhA|VVMTv2yK3QVS|oe%Vy1*-HeYH zMG6!~%KiH-kaiNf;GBZo55`2$xsAnNfw%HtW~A9P#_CqCT*)0-7;Vmjk={Shyw{lK zs6jXm2uI!&78>yV=0;fuqh?_OBBSlFL4ywAaQvyPO!MAwOf3MhC~SEj^lx{g9d!Iy zIR}#7bcccQo8#Fl`m)i66LHuuSWjD{y#g3Gr%1jN0t$2UV|wpFt_Nv*=5adiaH)H#@GGPd_VFciV8WR(EQI&1xY|woze4~^D4_82Sf@7BQ2^5rVf=k zUntRejp8pOcFL4Q6t3LANr6USs&Y{Y72JPuj^hZ3p}@$Ia!4g0a5v!Klx-H!De$E@ z=n#TNW@eN4svIQ5h5U?A>-Z7;jM(CAESk!4hDNXA=|FbL`V0P$oc#|UeoiU14m7%m zc<|B5QEAYt%re|vp}XcyQd1Z#KZd{2si_eR$%k;T)Di>-uGINCA+3;r38TeJRzeVm zJeL{-^#;W3_Pu-eet7qH>=F+|FATL#j5Nb$dLrilodaGj&L@aJ`9~J?IJ{+c=vinT znGC6`abXZ2J{TcMV_(CnmzB$>{&DXr*PdW*RrB+w==o*b3BKz~>0s6^pc?;;Kgl;W6{BP z9%842I1Pt~<0LM69GK*e1)E!#=^_P|ktAj`0Kih&Z(6}=FCh~T)Onnxt?K5lkIFumA1qS5%fJR(9n2BX9#xuQ@LkX#}eZ>Apm0%&_g| zni4hg18<$9ED0B~U&h8ucALQAD`MuPyZdKzH>GGE)vLhD(x=VWdRPdTz!) zXp~rG6ilngJcu_1J(W@t`4)X=Y|W+cVyUe9yaH0ASofo~`pH0+ zIPUU9$O{BI+0_dbPkAn@HW|#30dOJ-3)uD}!xi&&-gGs~1$H4x29GT(OFunso1{Mx zprB#pfMY5-KG^-oXMXwSMeH)54v|56K0Vwn zW-)W();Z!khf#=Iy zj1EssPC|%X%^S(S>iCaST5(!x>Mqojvj6)KV`fC0WVALEBT0#gTgfZv8Dy0Yj3Oa{ zY2BT4uKYp~Oo%RlpDdFk{Pc_FD=p^LFk-+g7b#OJb)={KN|6Z=(?4M5$RQdH8#YYN11VF6Cl4RSnnI=!#oU-!BAk#I z(ol^UM;n6^&>|wtzhgIK^riHF2TslkGu?~+`hs=5A>}-2ils?DxFBKVLU(MVe;ZPx z9cpGGEo~-KGG7S~{DFt`Mb0iR%ER7Mn8>4n7#aO^^>5|v-+33Z=8Op+gw+pOZz0D5 zc@yND>?7q`QDE?R5f;imL$n7rs523(k%&>a_e8bAQpf(M$%!~tZcVHfs+3H{vwO>ql!@EX7j}le2J^$Pq=sJZ$6glnoLaU5?7I&tB%uXbs!)y{F!00F@ag*E2|2A zd)dudRCbAR1C?PbABu`tqfX)`3ZrZ5%2bGb82^jxb08muYD?T$P~OSP+@APy5Ts?b zp|qu?RTArDyw*rv$`6#}?dSU`D>MaQSV%}f+aWf>5()@Wlb8jB`=pl=ZHzHjNx^Jt zLjCWN=m!h=;Ypk(f`0Ux_cPJs0XLmNQ^$Qco7Ax5fgo#{q30FbWrq27nxl<$Qnax9 z@~<=B4MZ!72M#cEjv^G~x9Op-WqOiEqw zPNGj0oo94xEIQT&hnF$d?!iAq9W04O+UFtASWEnD`|cJrUglPaNgm50)Jp$a|U0fQ&}q z(2NEPeU2C*@#}G@%Sh@oa-`+H)F#q;ki9U5Lir_Vni%(pM-`q8A}!>NokppPi^J?F z>ruad71S4&mZm{a1D~)6pyPBuou=Huj3j3Fb|9QWaXf*VgoZSjA1KW&LyhQR4?w8} z8X56305nA)PhEh}QtBJbH16<-v(cHO__E<2<=)Y#OMxOw(5Fn(Wwjcux3Tqp3FQx9O9UB7|8M|(_^}hId=gOLF~j?!ptlMF9(Kdg2}cb6QJZ%8265eWm(ycBvW<@ zp(0|9acB9am@A1rCGDYzrx873eQdpaKtrLDYU?tz3FQjcbnHc@*B3FP+=&qLdZD;g^2 z-hKOCq9_^E%7-~=?wRaKp>Vv!2tE3`$^UIN!ba>O_8?S;l5+v}3V$%>s&V5=(twFx zuXX!9->S4;uU|he;?uJmE)Mjm$-E^a_4YF0k8+VaI*rjdz6pkBg@M<`->JW!1tlo; zhp}F#X%{6AW$X~z3ED<5;n_9Ht}K+ks&%rJ@@bZJ8E0MMUq*F>xP)bh~AcZ zgqn1v`}Z?rjTR1F{#0a~R0V4Dx^)@7u@g;|G@DF95v$F3dIdvqtOO&=?vJ*)7A|@uAigy5lSD6Hmj7?obbxm6+%fT?zV(o{VLB?+- zm@$3_4k?aNT+ND%$=xLXlL*Dqh=M7Q8CsOZI+Pzs{n<(tqi)G#_{mL_Ie0Eq;ojzN zf-xMm;m63bOi_+yUE~xUs&v4IW5*7rKWH5oV#O|b~v;^3gm>6X#?7B#f z=UvhVU%PQb2LM{;Cs}1gg~-&5StoZHs;BsSCo(=-O?vVt+-Dw5A~}XG+otyMPZT4*`~4fbDkfXn>LslC~Dul zjh(>PZ{Na%46i2>>SdF0Q644PIbHnuD@C~TRXPMy^6|LEWy)Vd%W1l-n3JY&~|H=f{{%uAFd;^ihsnr(I2fQ+39jX;77 zsK_@8C@R+}^yUpv4T_MI+NqEVn#vcka3Eih&hZJ-W{LQOHtEW_m{`9Vg_{isn;SKg zmf`D3!L7raP!0-SJteR)r+18U4&{8kxg0yw21c^*FZ0APlgMbeV7B{hg{ zN=A2SUk7dc=~Q0!73*k>CxWWHVbU17#K!|5Ya-<=$8OS@jDhTx=bjuxPt6=7vs6Ku z_RIOHj_YZFhd zD<8niu+5?(SFmgjJwKbEY@_SHfqi+$hB|aW42eQsU|>Wsum`my3goY>o%O1_ObnHW zBQhk~7G7kZlYQ0R)+5s;KVw1Y#rPfR`z*grilCs?eR}pZ_|yz&s_pz`Uu@r40(g?l zWg3XL8bVS!F~+0owcr!@0EAFC24$Y26^eObeTT^nZhpqusqMs@!7RKOPKg3~8!2MY zsKosyox**Abft8mm%qt81<+49{g$jvB8TinUk{pqGYDS=babQLO69ER`74UMc}Jy((huURq+tH%OCLt7*EXk(!X~6OWOxp8PWg-yBipcjC$MUvC+O|uUi0lyv zOtAR4xC+_`CZ)duzCsH_!HUZg#u^gp7$ZT=BrXUStgg!(fG8#D0jLQ1aO>-;ULecw zuA?*CkN=U%o=sM?Sly)Vq*Esa6J~uG zC0e{{RsDo3;oWC(>WL@HHmwwN^5+Fwnfg%)p-DD~G!rm%?_NbfHEQ&tw{IsbT(~e; z%P#AiwAJhc-AX)^NfOe=^^Mh+IxViGsixlEMVO^l*@j6$q=W0HmpwH0;R)Si=ce_C z9qD6fM`P~~_PK8o{O_v~Oa1u`fP2h*3%X73`76ZV{~w})=}BGnvu&7`gp(|{f(L6^;N$Hu0?+3{LhD~35_{WCmwY($n~&qc<)kERXk)RLnJ)->TqV`R=ZQf^27 zeVUe%5%u{QLkMDfdR3z0MlfEGr~5`}^cYYqTqe z%BaBjus?E>Sc~@{85j#;Qx|Y-E;nVw+7DZJ|J~S$OTE4Xa+s$}6O-Dk+r}+~6f&aZ z0-M}<@26qDMjGM3?+OpsOMqtt<;ouOkEXap#1Wb3zoQZF6_UKi62yJK}XIXv^} zh4~{X{A8yc_az04O}ty^o)yZyy3TFV`EyB0zj@vk2TGyjC=z%s?GvgpryA^jx47)*lq`SU*qD-2oGPW{C0KI)#- zk#vOo;6Ya*wUuYZv}ttfR$G^n-I?BRb^CHjjzPVKRfyvom z;DZdonbN~eiY6aQaUaDs+>s%eg9R3oa6xx?rzA0fyr|o_s!Xt~`Tog5@;Fr;l6`9b zL0wH*YsuMJk$*!OM+}(zyPamhE{NoTVum72rg~TCN;6BjFDNkwNKH%2n_nn3f@DpK za8WYAbpX;5%*7fDA!h?Rf~G_r#3r)t07OA7{1{!J^_6)(5M06s1W-3pAPC1HyMVbC z-69kg4FaW{1$;#2!ih5IMJbviir+nk7vKqizGQ?-Rw!?=T)88OIw4a`vEa94bdzGC zH%47NJ%Ju_7`_ZFP^f<(W?w_$Pptm$IH< zQ<$ho6$*qQo*1mlf!dW3=*yQin_PHdA48`j8LxCC)Fe#;L?eHb$TT5sIDiG|SMz!N z>(N$bW_?p?=BT$9e?7tk6cn~9xUk+%@|gSGY+Ztp?9diz7bq?26sWGZ5Nr%PebsH+ zAs_igST#N2nIF{bhE*P zjt+sF#9M;%1=>UR?nHq!L?wOvn)^39a)EdnS5{>I%TW;D1)$UBEnBuv0KG~+wwp8w z5@3u`Ay}vMRGONlZH~PDy;#Rj;84Uqv}k)O9Kk?-5=gIecc3<`0?Knk>b51@2lZ7~;ns*fC}E2`>+|owo-jlR z)6UXFto`&-lun}2qdC~Ud$-cI6=NPX-%|{BcF{glgX(oMWYomue8QphUf|+N@7KJU z=r=Q@mzvu9TNcw_E$@ovUq)Go1Qj%n%ny%+7!tJ$T@_=m1iPOQEh4mAo_(ngRC>_O zo3)3`Wol$z?8cP|*m6jssu21IYVn5H|vE#;V?X=*7>>Xq~xfsyUJY^6iHvg^`ynr?$aOgM=k^j6(>%bFiw9+R%m z&yDIzQ%*Xwb}Q)vKDdJr0R0Z7Qos1i&7w2sZ;bvOkbNYwq(x{GgD*2Cj5CQaUohd* zo|l?6w--0WZ?l`?X!URA7Z-XMwKpEO;-zxahRVS^55DqAUwO%Yz~GAC(SG+=PEqgB z+{Ugux2|m?B3$z z8}+X02jM^rR=E#YtmMS+>aw~y4tcWj7_O*&Y;HQJqHG_g!;%&V0j>-uo0=+bPUz;J zey+3O8W^#bH^Nq5aEk;sk^n3QW@XpZZ(}piX5t|re6f6*KR;lp(ef|-ls2vH@Ad)^ zFZw`)cC0n14zqwboZZHL=_lS?`Gxf_SEU`Gp@^iqK)z6J5ivy=;Vs|=p zY&ngLjT52IROa0Jm>uYLhY7lJ2r3;yny}Bkr^@C};Qcjk4^6gDt!TCV=Uk2F&I@(@ zI}f#Q&l>7hY!}B?Z|2CqqTCYLJ8&F;Dw@kGD?RZENtFNN1a}-f)3i%OcC)5E{vEX% zf?SB|!H@K|8L4kq--DsUPBDAtZODi`>(A){WRErr>c4@Vl;cu0Xzd3hs1sTEbsbbi zypecI;@F>KTqc%p73P-IPlyAT5r?2TrYKH*!bS|tWohkTQU(Kk$wqm8L!@nktJy2W zyIkA^WYL(<%n@F+aHuf;8t|_0b4@DVQ#>RY=7%OCXx%JN`KL*vhVpJCkH99~vpqWh zoa%t-Q>RwnsSF+SPuP!lp&=(9>`~UdhVDYv$Ffdn5oMi>V#^c2yON(p1$*<`15Lb7 zj$(A~k1N^!bs1~UX%yu=)2*4ZrZVqY#>U*=*CFF&?iyn+_V7Z&VWanF$*o}RAJ3mb z;W7D$Eqa^p4mSJ0AGG!JnKjx^Wa@bA?jqH&tV(n0!m>W>tjqxR;&>>DNrPE)w!dUYEIv>|{lffj#<2Xm+ggvl2L~r4H0gOUH+;K6 z53cK9G^}F9hb1Rt)u#ZAmv^;|&0DR=y>G@F?X^pFT_;dU)^c`F(-8+O6y^GrPsYoe zz@J9AogTZoc&hKoL)jp%x?+ty?{ooj7ahpKCOz67_!zWTtML6PkCc_MJ}-`s9`WCR z^=f{t(1#+wZ!@`zY&{ryBYTA|K5craA|}=Uu8T2m57G1bod3!~@%cx(LDe{5=Ob0u zyshecgwiIUfs&;_REg*7`G|15bU+oj0#}d&@|D$2*#Xc)+cn zBb-y@ik+hR9vO2*&m{5rLfq#O#AbvqS{<#N8d5HeB09ZK`Dc}=BpfbW-^Q zFkvSsGl6gLZ*N!t2|V>RFA}aioQ+aol-dcWMzi$Gn`}-vh2w(_8{$bL9OS4Yp&$5q zue;U%Y{u|=hK^@wN)O**&l6Nths~*;-hQI@sPE<-w5=b4R1PoP-RmbCCy<~T?z`lN zG2r3@qf#&)O(N2h&x<@2wH9(?n|t0yhxCvgE%)hi0LUf!@yGrpK8XW+`km>j%fi}- zxdHyQM*veVuE^GbTS{dI*%i0PIAKr{uEywreM{F~dn$rw2bmqo>5#c5`ix#5(s!|} zk>Jq2$H}h`n?gqcv4e;)4o|bCo9XJ ziqj8QS0EM6^S_Yr+l>uIf1d9AwQYDfJa<{{K`Po%5`451Zqi7SLrQSatCoq4e>I}|YNwU4n+9h6(Ks?vFB)q3BL@u6-&V>o$&ckA!rhnSKO=dU0N4o=1J%3AWoEUj*!80llOg^6Wms zq~WCzmoWm!!#+^FuOFTm-j4k1oT{2ifxW+bbE{2NnesgI+)5+Y$6G%dQQH5JUQtWP zU*X6R$*7CO0%(63eE>yKV~Zo#DFMVUUlf4Qsj@5Oc!&0u^y6RCmo7c0yNeF%ANp*) zF5>5YV$=8RDgWNZ`eir@2n$$Llo6z}#s^Z*WL&IsF3DG2Q~E*Os`6&;@5?#but(Fh zu5af(i*$^b^_Wv}jgLylAARm}@%#4)nT|;ciQR9Sc{zU-A8tUQN`zb8ZJPBPE9eiHJ(z z+v{zug?|*xV&mv;$fQJXfylgZ-^F34Yg?Qf;`^lZ=!%*VDa-UjE_j>f#1@AE(?LE$}b3SA8X!45ce;I7GjKEO4xqkdN;?MM)K+3yT1 znD)a-Z{Lg3{T83bPUwfmF~>8c?rfO%o9|!dCOFvJKYdiCxhF`q2SyR|j2tt6eYo;$ zW)XGp^TX4RrUlz(AF}Nno69bj7v8m@3O;z0{#nHwb8P;{n6cf*vKEgQ!{IMGe^BKM zI{=HS+)%hY&-;|QC5X3DQ{#5iS{v(S{>Y4eXjL#owTo_pZM1dNQT`@&G*zn3aMgoSIk)IvSm+1c$Lbn<-N8z(&*&rk(VSu4@?K(k$U0#$uY7WXzRR-+@?t_JXL;-(c4e3Bj^2mo|gw|JhzVO$f~_NZ^j<6ZM3xk4+{%c z8m!>?-aA86f67!B@P78q4TGt3)mRgtqJ3pm_{X8=ygO(5PggwQqHJ4R=)pt6E?Fq8`Yl^QLUcyH0C+>9# zS$X1o(JUpgIsaO{5t7-VyzThFgDI9_lPAoXZ88#}lBtE^KzG|BMc7JKK1jt*Hl7W8oWjV*}_pXdbU zl&xd=!O)^TBkI|D!A_yYHqN@khB<5sR4KyLbJCeM(yDD>GfMMa(lRWH&5gNc5VlY8 zuWdZ(MVGT%onL%0ZhYm-xwh79J=!{UtfETS0siD579FUR`TP41t1;P=l;p8r&L(+|DTV?xUAnur&rnZM@_8-?kntf%6^kJ!hbsQkB}=bU=G z^A*>RU*sSfSG1Zl>>o}zKUydpj{S>LM!LsM>#5Xg7YnD4cl}tto+{(`C+~AUA15*b zc7FfFm@y_Qj5)9%5Rd9@iKo+d@d?y|V}wJw{2LRbno zILoEO!ruqBeBSEPphx>wJ*Ks7)aYDRz{SS4Pjlv88Kx6Da(#kYuHhA>*~%|I+)mvS zbY~^yw@gjMVKM8Do?dsOb<^ndPwnD_UglrSP!#+mzg?AZ=4=heYmmx z&Y}U{`)+(-r~IJF4KsHbk;f@#e%5VHZ_Zs`Y=;87O;LH^_sMH5GuP39ELVu1H+Sgy zSdCcIZJxAv*xnNX9i30V;!g(l`&G6)T>a3(k7e=4dQW|NsU-CAB#(}DH^UBxrWynd z(@J_Xkd7V|-~Ovx-j#%G*8O8!VZY|z2fyqCBFw^%1kV}jHm>)h8EY3z`bT+2U!SE) zh6LT5&ZFeTDxx4fV z)WI5?YWNO!Jj!F0IZdZOi=h^tOQV`on)r>E@&mGN~oN@EK%Cbz?NvWAW zw!@e951TwRsPu<>)kxoFbn)x@&2`*{9(VBI%i|wwA5BvpAM#=KMB{yj9G;}K%Wykt z-$l8}R#31b(#uos37(3nGja0XrRfF+Nm}2SH)%tqLz%_Ly4fM_YgqV&q?NJt!6WN3 z^qVnQNQhP`v%{=_YKKdZRvYIY@_L~ zTbIj~N2#$lq0{iwPcOxHnG+TISMG=F{o5-f-<&m6BkXZ#ZScy06Msy+(%)!**2mfo z^SfDi_jWg495ivk$WB>K84fXI+D!Y*!lC0Lrti3?-zh)KZg}9*+1~FezF*mY#6RQz zQT0_}RkdBTAKl&EB^}b;NDG2=NJ@8ugmfd_A>G}LG)PHR+?MvYXYKjq`n>Hd^&Tt}M~*nX`S{#MeLmi#w?cU6^@s1XC0STTmm&Wq`;uIlsA zr<#G0(q6{(RY#qt-lsOJI8sxt`k%H`y#l-KPWFU4+UoY^^eo?>TY4!cPh=fL#w^Xt ze^MwbEd{(1`&>d#r%Sa}FYR>4m8Y4j%Zn>j^l>N*P0Nb(*oVLOJF0vZw9@lOzxw&O z`TXug&AjlEIa^P6s1ay!%gOV}6e2|fmih|R1Ou0NKM9#EjfRVoe_K8&%*_?MLR3c) zXxl1!+3;_uT1bC5xclK%ZDg?9Bcx~Lr~Gs_dv{W%yQ;HvS~8J2cVj4F&K2278Udon7pl)SiDm#G)? zx-#{4f^!W<`a9ma*;N;{x`RSjdE|ZiTX^NRy}IoNaxbojR+(50Qbx%)&T95q{E6?kG_+pTA3TyA58yX`&%jw+pP$U}kzjL?Rae7G2T6Wg z-`ecM*|&~4BUDM^P?slW&+e?OWinp;M*}xSzW>PKA+vohFm6y9uE)-z`cAh4W7hhP z9H5>K^;bN)+gF8u9zQvGc}k;0hleA1ynd$x%^ndZSgNjPK0lLM1j=rH8Tf6+$(-d^ zcH$;XL02>(=84|f3h_QyQyt8^q35nPXP+Gid8rNhv}p1a9Q?kR7ej|Wkr_Ob@ZvwU zOC#NXxi_D+GeF^oqs$vLG#4Qe%HQf9%xa%fuTLfd_Kw|f_GuZ;`VS}#19W>y;;#D< zDHnY)uDrDJZ~B}8kecRwnXS9uEK~m6WQ20YfjWo8c4{GR;g3$+~j{k3t!0)eixC;*_?%2X6K#mLfq#u2WZGw zd{g?zs!}ew_@MHk(13+p$il2RO(^WZ0D3~5W#$3;wG+3oFZ%`GsO95?V?2{9bHI}9 zZJcp;`c@^+WUD53A^P1sEpJU$PTJiN^QPF7pEpGN44!UJ+g;?$Jn8>i1m|PaR){0k z05fz-8F#z@8z8!3@d#!SN-RGOf-_8zo^&NQb0my-2yZa)`M9f`tt@VFNKD z1{5si*k8Y8SMsf*OU8+k$N$lOp8|m4atCf|t=>JWU&b}$@RR}(h@$@O?WYyNMTQ}; zBx0e^$JPk-D(dY}`BRd%N<7H0s`9xNYgToO4*a?p`Gk| zB|(q`g*UUXBn3B7+yLgV_ot7wSlR0`N=lj;sCdF%cFB#>91Jb7%TuMz1?XBD`+u0p zwBlWJ6Sbu?Xrc?vSfvKOBr(g4a zdjH4fbKeq~hSPQ2AO_-hE~qfjmTNwRYMbRjBApNicbYlLQ`YsjVcsC8ww72cu4b87 zA;TUOFUyUO+lrdG5$LqN1UHO%oTIk7jhg=%^SijTnyhtRJW|(4q=2V9u+-xP{2bsk zb7DwIM8YNH(f>%oxmrPb+lZ*r*tp`MBLC}1!**JX)qGe>WY4cRvJ{k`R|6-n-c!sa1kp~L1y5rtfw^Bq}*X9Q1j=Lanh1kf)e8y_@fLCwmVZB>;q zksA^TD~cG>ymflu{rDhr?=2iIAJMWuEe9fDr#lifcK6@*S)RM>`mbkBkph>zyDKmB zS%uP)V$jfO=|b(#hjr~W*Pjj#OXH{!+E&H7I-ydzU1(P?JuPfNOi{7S?XavjGjz5V zp~ZQ&*D#NF@hiPbMNbVOKveq!|6Zoh_ZcNVthz;F(^$-s;&(d-=DSx9$z(IfR^r%_ z87z1&o@geQ-!(a`hh$A2&XRwK4*G{_%-oUU{Rambl+pz!lj-u$G~bCPdwE5y_WyWT zPW1>XUiWKid_fbZm`*);JN)esyHypd^uOCe_`ANgG~kBl4<>3ziv8mv?=dRuEt&Xy zhTxyKdnf~+y6`wE)IRG*i%CRyXbCaby1u0+oI0gXhxO} zVcPdE^XtA7K4>dS8xtoc=59ajYge!8D?H)Rh+a;PY_rbR*Yr)!8JeM4d6PK@72kY2nkBrZl zIgH3*A$$*5I8oZ%!YVGS0=lWO=-IN~-p1Q0S>?b_%4=z{Cnn;A28Z4yTA%|J5tCo# zL=U#(nxN1=)uE#dW+LA=%z3}=43;lf2Om|YP4vGB;N=fT=7`GYd>7eMoe;pi!ObH{Ou|>n5bB&y6FOgbovD1C!oz(|EMl>H`L=Z>YPxSOF4| zn7C8^_zd;$GEt!k_T*9xS}^RpNF+sz5OP2wLUKSd!$Y%?lVTg8Q4I#-=(Ww{N({cC zQ(9^3ow}g9*v2AIQc{sa`3HXWUG^&GMkVpX+W*B!;-@w=6#A{6ELEGw@C8PtBemQ? z<4?s~bU?4o_3=~)h6I|FYWL2EzbOt|4@NI{_YrT?I#P(H5XYxERdq3dv<9!=^+ zj2e=#)JmxDX-YAw**VS_k-gQwrTkMzAB~zp!P1f)ZLA|AZ&UminI9pr1q6#)Ekof7 zVm%sJzNZeIS}%M^*Z)!QgVmU}(+Bn@ez&DQU1eMl*XnJXX;uXT#-53IR_(y%(7E+#neWF!GHqVPQ=687;Xv2(hOhi#Lr3`01SVPEn;GFE ztf0`f$ApW55E+a*0WA_ma{Mren=NIl8m*HI4qwQk$ZSzwVsUgylf_@ZDGoB+Ca^mn zZ#mzUjQ}lT3M#4yu+i}eA2FaI5_$lH#804C3(9FAaa6oU{FVp0aPR--yFL8fx&s2; zdzTFJ4#&+GgeX{>SgF2SC)R71NLId|$8|n+lIY&dWe9u@cj!P^WEyNlOhlN2)b)^Iy#Gyr3JC>P zW4|Ue%MZSB|0rI!LA00De0(WA&#q870tCP${IB!q%vCNA^XxxR{encCeFb8HG~v;` zPEG?S6)KFMi>dSe{fh-_!X4iKJagS*Z{tb&!*N-RoTA)b>_$j#z2~NEyrhx_DqS|O z|Iaffvzt2uhsP3gqo}%H(1>_27{G8ENl0&23&fhXyw}N+)c%U+SnIfWKMJd@{cOW( zi<1&S?g-pD)>$}EXd+O>j-w8%uV-o2z4(vIgDB4QyFG9J5?Lo0=yf`tqKnT=C7A($ znkeved|TIbTIbuk;K0%mqc-S&TEgf5Afbl+{c0ksw`NNTKpOSGIyfl8Df2A4jk(cL z<-slP{g`5CF)w=HMNA(MK>dBO^9v(DYdSlgI9R`W?n`W}3H4>lPY)u=MSy zltQR>vydj*xOQI<4VwhOba+v-2_4EQEVNj+S=|hc5Y{)Ib>FD^Sr%f~eS8j%S_GIt zV!qwaO8gtn|A8V*(vC_21ves5g}!)BOqJ4qr>@Qspw{Yh>c1EE!Ts=PdbX}x96DOc3=ZaRv|X0e zl43|qH2sm73~;64+kk&%l#goDsz284$CUhccWFzl-l<}zwzqqOxRV{0Dp4N_4!OsK zV`g@HV9S!4k&~$5Qll&?=pGt+4*K_FyBIb*0o9t-$1EzI|i6NyrDOvo#4CMbVZ0O?){GBLU(Y0DXyrXz(xco$F)tu$t$Wdg8I7ye+D={gsZmRuVx>EW>=@d z0~b9qXBQKfG{;P9{_AH&17~m$m#w=AS!>_(;(Js0PZ*z>nSr8cE`4l#M&qbU0}5&z z7C@}n@WB?~s&YwMK!FG-5_X2gSsgu3_ZyXOPdqidWBEiZu%qH z{N+>;qnwh0+28J5UDt~S;9CrM^bh_WsDJD5zmtVZ;svq#?FLU&72Ego9f?4M8#u2Z za5kdur;}tUZA6BVoazSGcd_1-YIIGf_eVq}B{OrN-fEg}AcQ3P`~0~q!@A{$NMC;q zwCdX|i9EyZy*n%r38Vwjr(Y>sz9(Qo$b7F!?^Z;xVv6EkU*H^;6sagx2i;5cU|8}*3xNjmBT}F@p!p)6g8mut04J{B5{{x@V+we?$uy`Q zI1HWl;V;;+Nzn)}th8j>bv=c%c46=MF^YM9|>x@qQ)!VkLXfy4M$yDjaob(b!!vQg!LyL_7r+7 zo}pT8s$xnJ3knO2*axTR->Yb95+Nk5EhP?bE=$6xY|l*anc$MdF0bFwouZ>TIHd+@~8;s$<6Sp|EH776MRjTRsA z;^fZYqmCw40?HH^?W`+~RTkC@o3hB0@wMC3L(o?75JRVgIQ%ZVLNA66JY8;cGkWsO zz{Ow|kTjD~HG%ZU?cG2Z?ut44;~ltuCXuuwOI)QP5JMjxXYUXmNuJa3WxiD+V^O?m z#Tz-#{{%*7;r>XO-6aNp!$jZnLfzZaZvExltrl9J! zBl}am(9EBvxbcGZwfS&NKH{p|PuEx4d=)!nV_O;(UR+(Ts-~yPl6O(1L?t3q^Lc|Z zI!x*`yV^375bWzsZ&M>`#t#%}Mfn~MZxoGsCr7xeS^HaMHO&n;x3EI+a&R>d%SwlHjq-SPGMFMenZSarL*$;JPtzC;N{hvRW*IB zU%h*~b-sPtwY84;?;RD&9OQuM1MKUTx9?RyH)pm!5<)_VIA{>j=7)*0b>9Xs2T6!A zcelX9_}RPXYO&lM~hejX+6N6%9V}sc}ChG=y9dpV^AL=k30{ ziy}^hzGrvj@u2)yCPJ#) z1e+}#hABz*xH-@$V1-CFKC-Pd+K~J=F7G*yI5v!=>H=eH@!jm4x{NWLv>1BMz+Qe{ zIkr}kuVyB{SJR_!)nMTK)?&j%xVVcc&A8&4$a_6^di?%Xl`0x)J{1h)Mio6(CHnd! z-G(ao`umcSQc7Y(|D3;;&T6V373lHr6?YhFSC%vtN%ZY59*WCK!dAS{W3;rAES-7& z-f+EEM&)x$jK8k?d=y%bf`urP>NX2|ZqSAvD_p=Yp;eP?!j^)F(E1NL>JH$hCYUv! zjZKxP5n@F%v_!e;J@$a;UU=aRyVJCVtinj45V75x{m2Y+2eCDS`6|Ci%E z?CJN04QBpgpi?#()~Pywj!>TdTUlh6{ZEkn5?mmyE+Z66%Y&40U|xMe<&%w20W^KJ z7MO!EKui1itG`r$=k|0&q%yk)xD9d;CDI#XqhP4zGXqizGl`WF`2FermdhF&nF==t~(ooMy? z|D~+?pBRfPOH9T#WWdyqxSSi6 zs$Cel+X7z$Vc9)brertPZ6c;<7ewX1opn7%D-!vEvi_9c5ZfaPapbV|?xd^8N~Ys- z>LpP}_t7z*=oOpG{CwScO$gt{ECRBKGq#ajbPJEC1D?-yYra=nyZ*nR)$RCFcljl* z6GO^B?eHsU^B3AZJx4wXnds?5Q|@jz1K*Lpy0hJ%ZW{4F;v>JVhs{zn`U#Y(aiQ?% zg^^2M`gdI(i*wZ0^6f;*6*EezVp|~8+mF82bQmEpXFCXpfDaZZI$P@$c@#)YBKlLg z0a|-^Hp{B|aI%}19o^P7iK!lu%6p(snAw^;M{}+lSwo_uo2?I&JxxkVR)!xlt@P|>`$gj0>KLT1(Ioiy1%Kg6 zL#VGmADGXsdu^LrI98G-VNdFWg8RZHB~G6*R&6tb`)>5ZzJLY&FvGCy$AaT*p7NsF z_DHe5;Dn6$;3@KtAJxYWS%vcrk2G5E4u>~nqN%7D>4Ldo1I1N)P+0`LDC_nnz8p7y zfe&&$xts9d;Kq3pp8BYYjtsdB8Xv){B&1^oKnTkwDy0RZri|@82mYZ@7^WOhpRPRo=28EGM$Ow;YNL*FPpzHq`}L8^wJia+ zD?6y4nwp_KA{HmKf2mf0Sal>3fc*dyPxBeCkS^T6YdGyp9c#|Gz+hyuLH62cNsdd2 z9jlALt#)1gs!{cT2yAPQYz%1!-#;|?W}B0pa0%J0guya{Z^Qe;hwyW550HR)O5<2d z@shqeydXdOimZD-5wLdMS{E?=>u8uDcyr_a2RpO20}$Y>ed5ll%jkGNA7W_YaHK)= zCE-m_2(PKlA3$|5-(pV`38v~{x})d?Ucm!pUlK7=N%5%_#QOZ9wx#h>MOAZMcW zi=0sQ!w0kJexzpa5f_jk$K@PK(o(6SBgl!V_x!F51o3V&FHtRa&1c5i{+g{{oM#)~ z1+P}NtQT(?yKGxo^nuk%QNHv=|WMscZJb9@?*d8GGKG$dIG|Js5l)zu_6*zIK}E zGu&T^eU%Xl3md>w{WwtkWnpU29Y!qjk|jLSLrN7LMvQYJM>6t}o_rK@LnU^p^eb~< zDdi}9aAQB(6jB3s)qgD&EFVlXnTZiO-wf{JQZo8+c%HGedFZx_VZIg+Lxod*;p$~2 z`lF_!C#5Zor#`7C1pxzNpoT>wfP^5AN#1n(^IbX&w$#^(goQ`lvPd=TAAj6rvY=FA zEEy|yLPKF8VwU?J&opQ5;u%8ozEL-krUM{HE3dsC+wKchKwi!eVgNr6{Ragt8!pSX z@>|sc+qlQ#yAtR6&nBHuhe!eb|7nyF{A{p;wxL*A64sKoO`NxI)D-)@HuYzHYJKoh z`AA$1`^zC=u6(+x+D|pENf0y1M`YJd8;kdFPyT2$q4?0o{f<6dvR|&Vf+Pp5gGj{_ z1OqYc`?&!>JfXnB6SKqdrns!QFv`Kp`aJZR35zu>?FYGdnvu~D7S3%tPdZ`0%No|} z9NRc7?q!i_Kbc1A#%N#4T(p%>m7c&tEYw{t|3RBtZ#Y5EADWUM)BBas`ggV#9x3h| zCOnDeb75{Ju2vFy|DU^tGPR(trALv?o`pX};gp6oq2&**%-O!x0rHhCS=+PMO3T;g zqFmi0rgnALbIC&!qY>9X$=RIybFYbb|4SGi%cI+^^FF|YMJ15_wBwo7UK>hqCp;84 zNQM@q)#%PJDfCKD8WRGCD=6jp3tupbG^y(bHX{_lG%@XLWW0%L4B|@gf~4?Rfbqj{3A!LYntfJ)*lg7X$dLSykAk} zt5nDNct zn`lOG(__BM_VO17^{e0S60l8^6*g=f>#$l1;43Z#FRhP>3nu&#Ph3Kyw^74O8R=&= zG&gSCU$Ob0OH{C-d%eLlIMJVSX)gT5%h5^ROcuw>)(uN%&WBQ~gT*Gg+?=4_@64&P zil;6OzPBzHj`+0%wlvIXK~Xy0u{?@OvJ|&QpuhLCdepjpnrk`Z8H$C%2xt0&xv@vq zxfcwk*gEXGiZru@n~4O+nqLvU$H!M3-uf!8bjvmEO;8x?T?1DdpAbHe!LE57>2*F` zA{DN)NAwq{gThXqG-)uWFh*jdjb|J(J}N!IPonBuEXqul`1K0BH{WwhXw@8ZUptUj zx8Y0YWvODb5sWLOdlG??5lr2%Hgo1)cK+k_dmtI)NFmg&CHcpqx46l+5 z=$Tmo3=@}_s+fF@+`8*JXkBA}0i$U@eNhC&|8pED?|I1@B5R zq#r8aoG?G$clcLpSy?GZXaqH#zF^XkKqu0o0u#EQrUUNjg4A?B`8U=mzHzMU*1K*V|r+evmx7v|p zkETuW`tH}jfK?=||A|iH)y-a6^K-Ceq=*k>Jhs`XklW=zX$+s|GXgd3Psm`Qq`UM( zQOR0d>nEHLP6)rckXCJ;33maHhmM-ho3YekCpwgf+Ilw3mG>r>mtN==mU-@bsDwm< z3nUKCCu>-3RGo9Y)M|RF(pr`XxDgoXLZo7mOybm9g@(&j1%>9Zu_<8j-+%ga@2$A) zd=gBK4-X54jboi#GZiszOWS2Gq5#Ci1l+29Pu!?!=xSVMl((Ia!upKz^PJ%laXzJt zNrQu*@FQdC!x#UkqxnsHemOZssU#Z2rk4*A9m%uXk^mVgz0_0H5O-3m+kK!KU$L#f2MQ`H(M2@`Au}KJeOb%7=>!_>#IPkDFE80sBe_~v)f4d&W-92EiUOxM zDG7-VL4%((I`#_6+>hgxFjaKf@gZBie5vSW92~rv4oyEMTuwyDsGA#g&BW;r83Xj3 z^5)pXk*{O8SbTa{vPz15Grwg$7L{rpTQ{5sz&8;<@}&``_};(GB@PkRd$PPQ;tpS^ zL1pc*p>gWqM~AOh(bVb7F`}YhZckJcd6WF0&3|yA0SK9Ei3{87W7A5tvB?E^(r2#~C?jmc z!fTC7M;O`g$(y{;FcYR#Q4}`pLrK#8el}M67yNKL?Hxng9Y`)-_j2G6?WmzsA-EVcRV|6 z!FtjWBgUbhO-ItgO68W;BC8AyjfkNm->0>!Yy5~!ok6Kv44{MqY*KlxwYd4J)J6c)Ffi{X>kQ_)%s|8Une7 zp~`E1F%c|X;WHEbaAJr~gNic`+b?&##w(xKysQXD_5Bi!@=?u~3k}%uBV}{{JL`JE zHSlW;SDWBXcS~jhj9f-Fgj%n+m@79fg7^KL&S!HGC{};$2cza|J+`F5xw5>8#|c%I zHJSddHguE~Q#CB(ph}^|2bd-oqX$oDXqXdM+Oseom?ZB#_8JW?JYNb5iVPe0EsD!% zyWINGo0`x8_=15i{FMtqj^TKr^?P}S!ianXuqt;ByqVUk^@$J?GXFuY`(5GmNk{jd zMp7dz)wp0q*`jf$e{U!CsaZT8ikBRofMmjDWMaY#JbfCDyruu&2Ci_&-R{!AI|X$0KsD1qUW_@IvE(3`eb_eJdvYyA>tJ zuXyi1M4+SKe#9iU{(L1QQlT?icyol$YmBYc(vso+2;FL8Mima}n;jEilWuX*5SxOn ztRM3V_X97N!yg*u+!k(z99ow*RJqR=(iQ z5uW#(nKk=#)su2G9v6D`z?t3RFPrF8ErK*)znyL~H;@#gW|C7CxU$*V-QAvg-z2bM z4=)9g&tCiArzoS77{Aaw(KAw?{nT|h7c%+x&V0m5(xe<|8x=NegjA#i;2@~b%i1cD zu3lcoQF|nRW_fu+&9lpvC3n+`{>{M18Io}iI?=-e#f!U9Dw~h9P@`}?YG(eRPcP-vV&=OJOd+RS461zuJfPx51{Hq$QeG$$tr z^&#Nzp`M?z7Bf70q#^|_Ov=rmvi+VhbhuypMa);D7YQdLlwU)tMBaj$c^OHSsY0TK z1LW}Z`$Nt;B_#AnI(KRHeNOpUvOTGmdFIa-UYtTB>L}%mFyj+io*#+!r;5lF^~t(y zpTnNBeArT%yz8HZ^xchxx!a&LjVWufLAWZ4ale@``T1qQe9Yh)8N{goWd$_scyt-B ze$=#d7l%%hmpg-GaVhcQdNwFBq+XDFy50!OzN=$ItX;=$U&=xT)kY0X#+^lWi|304T?LBVsNa38O_GxC(V$Xm)6RuJ|o0c;22U+qlr^$H2+OH-B9kk0jwq zf-h?F$g24KNNd;mNSQ~Di442XX^1|*?#Nhq53S)-AD^7{h&EheS%3eseRntxsLW{I z3uTVfegv*0!}m{1`_(HPw)LWdlomsbx`zFp#9N{LGH%w}uV^~#L_x@=GjOlv3o*k)pXbDb_HtMH3F-23fD)B1BAY*i6Pc2%lX;d*Zzn3K~i z6W;B+Tie(u%7QS__q&i1)_!GC()8Zuovhx_b?9?uGIQTaH7~9#1&CjJl&y)@?seV7 z?idmaHU6=C`DkrqpYeA7&RNZ4#ANm22^Y_Mkm$_V{Y7VKIfZq++pD&{nP>9yVFz7%;PNL&GF5h1!lv@I0OZOy%Ud2x)uPqnJDN*vjRGb3IR)vcB1;sz^ zl36Iw;m;2Napxiy?{jf}EVakm*q$-E~~pL=YIVaIHRJUOhl;k6+tk~(iO|J%q> zIP--X8k>m5r;LhC%?unr6;1kbBNGTem$)<5X-eq!dSn!jMJ(w0mhol|2p-CcFS%bc zQMr3+}dW1+HG%d2;tVBihay-8_WJFi$(cNT!g}?smqWVAr0E^`!f|QTZAz0f&~= zmAj#>?On9|mX;_N#-tkK3vuzjUiMU_@aAa(2Gd zmp0e(z4|8pgsH23UsaaUP>Xcr;Rx`HYIos%hD>eAm#VmnIDzDfndXoEc;Xs zR}~wOJ$;6JzMx0^gUXmamOdJ@%>9M?X?l8Ps9K(NYj^pST#)03)YV#1O)jYz`hkTV zsMLfJ(JE}$vTI&VwK>TiC-)Y#6-}#X_LU!?ISm&xx?B9h`hlwnO4|yBN&t^8%8Xgd zl-S6Hj5On9DGJm%>rBQ-j>-F3k_lti)w2=!G({BYT;IsC8|db)0b5 zw-qvCO5943$}i)UP3^QTd+gYHxBZy|^wgIoBo>&X((*ULwfQXoI*Of{fd~Zn7-=An z{maR3cPh0GK00)J>u&F3wH>RluP-nPj^oW2x;(y{>+Ll!eo+z7e?&0!wnJxjj(vS7 z_P+7=9C4JKcuus*u7bHRbC3&zC9 zzjiyJE}cvYh^ZoDIIw)KH%Bw5)DlztgaZa71z-2}&Tp>v$U3gd{~M1&v2fu(&3yIp zy#8Pa7E!ConBZl4IxON$0zB@}ftm6+wADW_c-7ZiCg%CBhVbOcnColY4tIyU;y2fL2wf+@DGmOcA6 zAtv`H=8@>+*jbV9)0+uDhy{M>b@_xYm123{UWAzJ==I)U(M~O>42C$Z^`5v=Swe#Z z;K6hJLp15TJmrrZ-(^r$b=`vov%G@?UPNj<`sPrfTfUQM_Tg@+1*r;Mza7&wYcIX(L*kUN7hRN<5_ALHqnc!!>O48Cms z@M`uxeoy>*@E@zl`-n~k24o_>Z~#wSOvq1izD6@xuQvN5sGZUX3dUBXqhb-__7r#( zVi0flwN-Tt7qZSLq-RHr?&Or##)qk$n%au$od4z*K$(RJ95And;9A|2HdY-O7zdQ*}h zD6pmKYP~r2H^cf5lg8(;SaAUxz8jd^=a3&Z+iYq?3Gk zvq!=mAt3`D_%q#CY)?JBX~C2;Pqts0)YbYI!#{?{jY?@mp#RxF;N|JT>_yu)`%z~a z4yEwg9m8(iH>n|;+4MOm-NMGQ5NvRW*>l*pZc*^o7*>rce8c;39;QxcWyX^wQ>5Hk(LKZrOl5HX>by!?5s{z+VL+$bzQj(Lzp7UmH5gP-i<( z)U2!Id3)%g!B%?b9+SQs475gM|q?JsB?<{QTR+G(F_ z_Ni<;eGomUWE@KKGUl^<5M!9Ok|t3yJG+1N7|u@7@qO>@gR``;{4yDRa=pJ85vLiw zTlT)p!p>&PWI%OHSom6De|(M1?{CFk_U?tbKDF0^{kxG5sSm$>py6N~M@bS6j`wpw z82iusv+<@RXQvIwIPIOc(Ph(ca3|gAd&Es9GaIRBa2r%;4FtPbxnFbuE|CVYd11bnG;?aqLiHxQlwtg5B|E3Eb-?fM(J z(MFv0%dS;;Od=aE4`kai@IZ_^G&F*@gGFIy%gMbGFk>DE9<^7Oa+AceUwnk#7nP-4 z93$OpKQF$n(k8KL%f1#fvZo-cf6{QAu}L@(VM|EB*=c{CWkMz66@SZ9lnGmX6-= zhm9{cUA#1{v+Xp^$em_OPsK@$c^$p?yiqbaZs^MGs7M^SSnd!e%G8z4a!t~@0 zQDRvF{T;f__Q70N#20c92~EwbHC*E9PSvY6JPpf~i-!vxXC~of-A6{t0fQ54GcbDq zEHOPX&yHiKk4V}zJLV?36S{;(2u9+%SH(ou5d;?@(E5 z)O($H$Coy-B%FF!5_#OQ{BwH<@V>D7eo0+ho&ajYqvPw~!Ls}srcti2 zQb%ez{&V&elDG;<;ARUvXY=!2xFqQ@GTM$Fq7&o@XwOZ(K22A+i0hlp0{uqYys3*V z*NSpK&Bd8C^_>C z2NIO?Az5cwD1VAvkg+=McZ*R+`4db|n9bV#q7+!ocqbCQmP6NQF6ia)&jJDt9_nZL3t1gQ1aTmg#|MQ&ad&lWzP?~gBD~Sq_KjveB``i8PrY%` zOyIS}3qEPOPMOs<#a)0|QL%n@F5@5pk$9yIDS1Xt^xzK6}#WMR{>9Jd(ww5#Wgg|Dg zTLDz|zjm=H$LsrG61X17_EPo|#B-(y55Mqg+y6c~qljM-38ZL|2#ts-%)92zBex%q zn#Uj{75m`HPmrp0ynaJP;+neIOLe_J7jazf9~30R=Il<2;FB;RnAZD{rE_SNsqyz* z8E)UgYHOry5Pc>ePL5%C%=;DRS2R(~z`D4FBsI`V4J~F<5H57$u6>g};ONSu=)6SzUn(s;&;-b#Z6GMx(%>V_*FxQkTPI2Q|O0BQXBUcVKqp zy|X(_e%|%%I67WR;sEH(KYJj6hW>|7n`^?XNT$3@3@rzD_zFhfFmalUr5pijQNYZB zVRx)GtSQ=0yAPP`4i2B6p@zD=sTsl$Oi)NZ=qbJjk;M;Oytphre7SfFTU$yl!GOb3 zwG4Vz3CW(x&B>IbK@1!s`Hx+~;99B}Xa+trs++wj@@~P=*ZJMCEa~Wkg+}(8O#gLk zTj=;ApdGbxLV-I&6+K!-EGFp;Oo#MuR-gY2d^-`FWTH;-QxX+-bJX+l@`qYX@H>4w zt=BI}C~_$jvTpxlfP$T#o&C1D6h@Bd>{<^3(|GB=P)3=AEWXf%X3`9LMu=_^w$P=t znSDiCrlkLrG=Ag@sXh6rmc6|27O{Hnsf6d{VEyer)eDl6jOSzu0Vc}v@=8|WaPW^E zH@Q~DQ0-XvaW3-2Fs&eeK%8b21tmu?=-gww46gy#vACjSXFnJ6QbteC$_hzJY7mUe z({T$!B*dgr!N48Nu);JgOMLkvl#78u%wL_n*_(fTN;LV>l`Up(fq&DW7@8M4y3_mB zCFeh12nb!r2l~gKKQZyKXERlfMMxo=iIq@KmjlT^9IrIOEN<~n+N$jfH;&GzcQc;a zdo@4YGMJd4l5;U-h(?gT`ukgOY4`Stv+v)~53NdcWoL`L$z?NF&tb`6 zA%cQ4PhhuzCyj~Ni4fjM@HmV6&Eucayhb1HE;i3UzgIO1H(xe%yc-n+7=#yHE-r4Q z(4jMw=%k~*tr>Rjpb#Z-VR{CxmBnJ$xGk~mM}_90CTsQ-Ov!=;tkJ3fW1M~}Y_&{V z)kyb`4=&~?>Jw`X$4jk(R>44IIk?AAvS@D0wdBLpP>Mwr$DdUk!{9+GDoaV*RCi!; zk{U)uOb0e&!otH$r-jrG6458)`a(j_R`JM0kX+NTpdrXa$iT#Hyv8SW*?7>fQq}eS zd9=##WdtXqv@*B-ns>2}rN`tSdN63soe!^Fg;6YPqJSJ;{?~B?WS1D(3(2i%CqhC^ z`7Vug!{7Q~U(}8ZP%k_1e@e7gv9E4=8%j~Yg*7$3IGH!#*t}#63k{bJVOSwXAHm%OJ|>is(Tz8o)!;){Ju-C^h7^bm8|%If z4=&hnN77>9{G?fn6#hy#edsKk7hSm>pGO`*zIjnBU7J^+8gp%{1mr^Unpc~l5jyl^ zl&a2X`&u8xb>uk#)!9$7aZQu~7?+xLffBOLPBYssD@4X5maV`h0||7=_{Tf*-psqr zBlCj#=Fp)2PWXVJ0QnQQ+DN$S<>PXL2Lci6HNYDQ+#3jp@l!IRAXhjnF}0(*_eOE+ zo)ggar#Sy5fwa6_$a3-Us7RW9H&SAsu&1TY-Jfdn{#GvoPZUbooI#2$B~~_NT(@{m zzZQ$tK?(yABO_KeLxD(%_fCZ)O+hvuH+kkeIU-PeBX$+wDE%l~G_!F5KSbIdh6P$!JRxsr(GO83p6tWtvWY4Js`)$zxay4vd z$NCMU)VH7ao4GUhUS|Bm2cBP? zvt#Xb&e_Y;XQ9;Ql7&zlm2UQFm`R#T>|yU&{?+mhU#1GKW!t0gokx@UP`k!c*wV3J-Q>6$jEy8YV^>?k?t!@E-gR#5)D>5ud2(JA7^seX?Ow=_2dLVhNPdeiJZu(x>BYt zNV%O7vG|V+rL8FL`GpUhIEi&U{zWPaWBH8aQ(|ozL_sy7x3?>LLn(u5-@L5htNbwG zp{wd1)xF)eYNu?sMGQOx`Be71{GUDj?GpxVe)U>e6Qni`Z+cLIqCZD2&l~cc?FN2Z z>(Z~WyPU+aP-nK+^G1ekhh|nwlZ$sYHniC<60wmGP}~aX_YhQ9{~eW?peEY+DUi+>jB z)}y3~Mo^+ilYII*Nx|!LgXPhC4mThwcsXv$NrhfuKp@d>bdp(>`>^djA4t)MvV_!< zQB-PU0?D&&S$w3hs{n}vnjWu-yYem5&2`0xkuvbRg_bXOVvum+SBPfRmH9+xWdlp@ z9Pz66Y6M^n453NeYwO|B$)GmVoTe%@wIMgsH;>JJtz*{CQq<#qxNLCYz7oPlZhJ4Y zN6cRwU!&*c^EHv!%YIpo!SpXhEdg@prfo^j4=rnEq>lkhQB6_O(mlE{P7%WdY#G%F z=sw@C^Dh_k^Um$Ah#LR(r-MM84)-7PE*xc z%qw%SNcs~B6AZVvF4c^k<8^s{0e%i*wL^8$IP|YSkSH0R#b!MYw3(hp-8&U^;xBV@f)LOqLU{}S z<8*;&M4xuCA}bZ@bGe{LnSx##Si$I zTY4C1DK6fL{g6OTgCRzg&7h*e)cU>g<+d~Ryvt+y!uMw#M}eTCMB^|_6HZ8Gu=h() zmhLX$&2t)=B~@c+Bl$i1mL+S!XB5%r!=^nH+$rD=YawxkL((M5!^*bx$YA=sZhtAu zI|2rlLi*mB?Czuo1#gSWMwmM7P7bdBz=N1xv3KPR+^~O1r&!x{lel>IugAhZpAZ^hsymw2E5jv_R--Q=8D0YCyl^_oCFv;n(^Mm=E&qrmElPt&hM& zjrjhS-EJgD;4eY;c-4UeId>su&1+iaUH28+BDpbqa@Lh5=e5Y#&s~uzZDb;D{!}jx z8_$2%OW&_r38>d@zV;xP&~s+>3;09Xpwqb5jy^k|SzPszv|Hi1cxHBH8j+rB7%OS> zpMR(^f8_^`E4pVJklDV5B(_Grl~+?=u&LcU9U9ShgvO<)Xb(guI=xeb-nq=yd&}Ej za(*UFE}gOT4?udm?EuRjfJw8yxGB~b`uds^f9v67WhM9jD79$k4%Pv&M4!`aZ@)tE zwI@mEcKd>1PF1GgpD&Vk8j%~IPq>Vls{KAW#Dg08;|E+S{{r13k~ZR z_FbUoy*iNuimAkH8T>tl1qa?pn%hdF8AB+OMGu~7v-(ozucUYQ%)LFk;J*K%dfW+U zk)Akcx6L3xiYh^Sh>x5-6hw*gjQd%s>71ZgdwY`Ge3O5d0bKL3p^})x1 zNx{F4)brNYK4Z{)s>6R@$MXvFF9xpFL$;*9U8_#nBM-gQL$et09|_57OQJDQv&w}5 z0OBAG`^)+csKGO{LDBi;; zZ>=*Hg6~UT@yh5KRZm|6FKj2ZlG($F=~$J~LyU=AEcf#n_h?k)B$14AZ~SMK zJoYTgOaNR-+j)0lGFZ>Gk4 z+MUAFQfClwB@yxV{yn#R-7>2s5jBKDE!swfPx0ir(`Z1`` zGqrBFZW(4}&PjS&Vm|Z>e9IZ+FC|%_ziobb*AL`Ja-ay^ZN9!E;Zx~@H)L9c6!;N` z1gGtdvz+L#dFfON8CxM`wFz6iXXUNJu^)aTkEp15gXHFS-_HIpZcVcM^lfkL+r??R zyzcD{;>TvN`;C%`KX_R}qTh4d82I$w77H3*27{W{O88j!P*Wa|7PHEkbg}T7WJ|Cr zle0PcW&|uV&OJspH8-W?{zmWZ@=uGO)cnk2mdN91=QXU!4%g_R1s{}HkKEk8b?fG4 zE7jRWRqfxI$f^8pjIPWbn#x;Ls2P+@LG^?V+x7q6IA3WwZ&Tl)*2HGX@D2L<0R=6n zNI93G$xoX8==1Y`M#C{?r>EUUPhzm22cx-&493tW{T9eeKW$tS+U1j)68oBwk+GNX z!eFTJQPS1yv)@)k&~5UeN{fEa2QQ5E`M|puCn+`eFr)W^SRcI7>494x1Om2|VL z5PH5RWO0Js{y2N^NvC6>>1yt3=+)U{uQUOo+iX$~$QH6|=8Yz*i%lI%-Fgiqn@+j; zq{6W^m9#5780Y1#1Gtzq_NRidY#ZL>RLbuMGNP5A%36kf2ZBe^R-Z}*6pv>MxMVNw zs0FI6vR>p3`1?=&ovb1aA-e3j)T&6lS@xidAb*iGvaXlUV^;dnztm&jApVJV`-Gn7 z&x3TLvzi1{n~SEK40e(965*TJxIT@8u|x%z25aks@YM|ELUt_{vgwwH)sXlFhuFQH z*VgPt&%GZ|2uX@Yc`wnUC@wlEYbnWZI&CZEQyu>JCf?b_VbBAjndPy)c+CbUr}O&H zk~Rh&`q%n@Ici*2{~~u8-IJ)fgokI-;OWr3VIEE{)IM55U#9<4yE$o(_HkgdgsrHs zE}O^kI(?3FbM)FjOoQ%txy!w6RdaJ4GCqgMSanNw?NEOS$-^4mn#0)~RFBi+iOa?X z`Z;gIw~_Zyg+)7yoqu-(jP)_HIu27;ag9|`Xn(2xZp87_^|wipPIYkI(|7C+t;`m) zN9gKt5$6@}l>e|Sc3yj~|H^-gCvp+%yG0hhH6J*$O!sBLuXv5M=J7er7rC_W140*C zWp-SCZX#0S)vWjggq#~!&OLuUXxTvU1G%oQKb_-lvG()Au_$+z14G1#g?qz?KN z!B_VB`PV~fBvo?)Xngu&s+-r2C4tZ0+XyK64|<}Wr#~lLz4zWeAp7wpBP*bMSb3BL z+SO!DE(F~{I(L%8R{X?-O;fMq$yL`K!Qr%WzeuMnm=Uk5;(a%_+pMRakV0j~hm3)< ztwY!Su~Zet8yCUrL;lScb{@MLl5ntGG==a$@?@UH7tc$0 z6jc>l+g%TP2K(~96nvIRk9gNL%7{|STk0&5JxBTjwx%h*f4f%A8cN2W*Z~pBVSQlg zhp7A4FvBUSho`*^@mGzBj_=1g{`ffqtgSSgxRu1M~I5mPZzMm9&+q zHC!o+&BFo|0$aLIRhn1xT+z?nw$>^K|4Pps$!b^1WjkIx)Ac&Q326L>w)%Ll>x6MrqaN zMow#^#!1S_pWq0S{S$s4Ut+NFy=1caT*IpQ%1YI|<#MXjjdr=qt9P29Fp>NGz0Nmn ze#a&>QS7hZzWuQ|v3)=>pnp;{#T$&~D_AP7RqZOM8~wTpR_@E+?&zFQ@b>>uD?GE= zqja*G3Ju_&a!pb#)T|-U#6~v%ZL>Ny+ba~`F`*~__U8{SkEc##7fvR&S3heOxQKDY zqvS}(fggy`tTP~pfv^^QU^mSFZt1|Rw&23i=n|noGiMlYN%!MkTCZ2t=c#}*yOe}UQ9^>zk58so^oy5 zpMKKUZbRUI!@_pkueX1BP)NZ%(4ougl1l1yn~I75bF{(ziZ9;X3g}+%ek3eoZ#?*R zT0)f?X&Al2bN%7VhP#Z^`TCl)&Al5L%HdaZxkcfoNPo3LeR9 z8NTmtMB!&~P&Aq*CEURE)r-0PWipX?KZc;|nT{VnRxI7tg1cFVvgDZjq&(V99z9sz z_F`K;{AW7!h4sdDRjr-cz^6ik(<3_K@qMhfSg4}dw9xH#ve^|+L(lmx^&MK;2PeC8 zZ=AJ0dXF{AT|DD1((PT@*TW{HXv1SW`r;a*9?REPI=y9DsSQ-}^8Qf4oA=lABZD04 zAqrZ&?u7SZKHaDAiv2d*uMG0cXjUFJ+M6d3#^u&A(g1 zkIrxV&0T$Hw#HcXYx5%(%wUto<+-|&zrJ1&;cN6}gCiewpXBykXxgc%&j&+1iG{Yv z=Ui54Kj&5EyvSo_c*63$=(X`v60W1hT%z~!Y_wdg0_C)j~JBErT5U6+f>Gm^Glo?G(77UWvFx(OSwyml z$KQudBv3ssZ|!GO?ONvj$?^-zP4y>h;TMl+IG;py!(y5S><3!y)39AxxQ&}ki-$`R zKEVu|7r&EsNFA*Hjlys(exzL-e*gLyrDo2R?P>GXy(;Za6}BedDf7kM(wP;)H3wdsv%{}>U5r+ zIryu1ROo;P=T-72@D^D}@2&@(31w0e*|W~Xg+fS5xpdS_0TY2?Z!hT|yPEte1pf5f z#mrB^V*~3^5(0%&*SJApj|hTSgus&W%-iz3DcNrO(HUYoMHlD)j`6y;l;$m`7CSNY zp?eOv9P7HJJ85s7;m=^241~gIV}$r2wanf@MqB8`+2a6A3UN3ouc3FtV^w!ZA+FSF zjxL(VGm46G8<>utguX9fyVJ3zg%gr!jzsc_;FXEQ2{+&zaXrl0EySH0QyT5{ne$V@>Jy*}) zc5Ac0k4|T7T*aqNjD|B+Oq$P^o*Eb=|ElVCM1Y=x;Fn*Y_HWNr#W+1o3i1_~aOG=o zw#uuo5B}%HyXG4#T!N*4_WG`C!%d$w?lU>7wh_;N`qdv7k6#wOuEW4iJnGCl;A3Dc zp8n-6{WJ?xlIF5TKDn6iCajd6>2B4ubr2C3NhOI-`r7zxq_$FTk8@xTk^1lRh?{KV zm$*_`6x3^f^<9%0DZ5ekeiYvxG8q; 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 OLfO;R&t;ucLK6T{=<-|u literal 0 HcmV?d00001 diff --git a/examples/computer_vision/fashion_product_images/static/images/dataset-3.png b/examples/computer_vision/fashion_product_images/static/images/dataset-3.png new file mode 100644 index 0000000000000000000000000000000000000000..3b3170727590f85d09f09628b6443bb133842826 GIT binary patch literal 154158 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