fleche 0.22.2__tar.gz → 0.22.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fleche-0.22.2 → fleche-0.22.4}/.github/workflows/test-minimum-deps.yml +6 -1
- {fleche-0.22.2 → fleche-0.22.4}/.github/workflows/tests.yml +23 -0
- {fleche-0.22.2 → fleche-0.22.4}/.gitignore +4 -0
- fleche-0.22.4/.release-please-manifest.json +3 -0
- {fleche-0.22.2 → fleche-0.22.4}/CHANGELOG.md +14 -0
- {fleche-0.22.2 → fleche-0.22.4}/PKG-INFO +2 -2
- {fleche-0.22.2 → fleche-0.22.4}/agents/DEVELOPING.md +129 -53
- {fleche-0.22.2 → fleche-0.22.4}/docs/dev/function_profile.rst +5 -0
- {fleche-0.22.2 → fleche-0.22.4}/docs/dev/ssh_cache.rst +3 -3
- {fleche-0.22.2 → fleche-0.22.4}/docs/digests/digest_equivalence.rst +69 -16
- {fleche-0.22.2 → fleche-0.22.4/docs}/notebooks/ConcurrentExecution.ipynb +2 -2
- {fleche-0.22.2 → fleche-0.22.4}/docs/parallel_execution.rst +32 -37
- {fleche-0.22.2 → fleche-0.22.4}/docs/storage/configuration.rst +29 -16
- {fleche-0.22.2 → fleche-0.22.4}/docs/usage/helpers.rst +10 -1
- {fleche-0.22.2 → fleche-0.22.4}/docs/usage/lazy_call.rst +8 -0
- {fleche-0.22.2 → fleche-0.22.4}/docs/usage/query.rst +36 -1
- {fleche-0.22.2/docs → fleche-0.22.4}/notebooks/ConcurrentExecution.ipynb +2 -2
- {fleche-0.22.2 → fleche-0.22.4}/pyproject.toml +1 -1
- fleche-0.22.4/src/fleche/_hashing.py +60 -0
- fleche-0.22.4/src/fleche/_numbers.py +283 -0
- fleche-0.22.4/src/fleche/_numpy.py +45 -0
- fleche-0.22.4/src/fleche/_pandas.py +114 -0
- {fleche-0.22.2 → fleche-0.22.4}/src/fleche/_version.py +3 -3
- {fleche-0.22.2 → fleche-0.22.4}/src/fleche/call.py +3 -1
- {fleche-0.22.2 → fleche-0.22.4}/src/fleche/digest.py +27 -66
- {fleche-0.22.2 → fleche-0.22.4}/src/fleche.egg-info/PKG-INFO +2 -2
- {fleche-0.22.2 → fleche-0.22.4}/src/fleche.egg-info/SOURCES.txt +4 -0
- {fleche-0.22.2 → fleche-0.22.4}/src/fleche.egg-info/requires.txt +1 -1
- {fleche-0.22.2 → fleche-0.22.4}/src/fleche.egg-info/scm_file_list.json +4 -0
- fleche-0.22.4/src/fleche.egg-info/scm_version.json +8 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/conftest.py +7 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/caches/test_cache.py +9 -9
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/caches/test_operation_context_cache.py +1 -1
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/caches/test_redigest.py +4 -2
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/caches/test_size_limited.py +1 -1
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/call/test_dehydrate.py +22 -27
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/call/test_prepared_call.py +56 -1
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/digest/test_digest.py +448 -118
- fleche-0.22.4/tests/unit/digest/test_hashing.py +46 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/fleche/test_fleche.py +24 -1
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/fleche/test_query_iterator.py +15 -30
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/storage/test_thread_safe.py +29 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/test_pickle.py +1 -1
- fleche-0.22.2/.release-please-manifest.json +0 -3
- fleche-0.22.2/src/fleche/_pandas.py +0 -110
- fleche-0.22.2/src/fleche.egg-info/scm_version.json +0 -8
- {fleche-0.22.2 → fleche-0.22.4}/.claude/CLAUDE.md +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/.claude/settings.json +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/.github/dependabot.yml +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/.github/workflows/benchmarks-main.yml +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/.github/workflows/benchmarks.yml +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/.github/workflows/ci-failure-summary.yml +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/.github/workflows/claude.yaml +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/.github/workflows/perf-triage.yml +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/.github/workflows/pypi-publish.yml +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/.github/workflows/reflow-agent-docs.yml +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/.github/workflows/reflow_agent_docs.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/.github/workflows/release-please.yml +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/.github/workflows/rendernb.yml +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/.github/workflows/ty.yml +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/.github/workflows/updatebenchmarks.yml +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/.readthedocs.yaml +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/AGENTS.md +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/LICENSE +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/README.md +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/agents/USAGE.md +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/benchmarks/benchmark_digest.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/benchmarks/benchmark_integration.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/benchmarks/benchmark_storage.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/benchmarks/compare_results.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/benchmarks/profile_digest_types.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/benchmarks/results.csv +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/benchmarks/run_benchmarks.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/benchmarks/utils.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/docs/_static/custom.css +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/docs/conf.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/docs/dev/call_lifecycle.rst +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/docs/dev/custom_digests.rst +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/docs/dev/extending_destructurer.rst +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/docs/dev/sql_test_backends.rst +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/docs/dev/storage_hierarchy.rst +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/docs/digests/digests_as_args.rst +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/docs/digests/entry_points.rst +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/docs/figures/cache_sequence.svg +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/docs/figures/dedup_forest.svg +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/docs/figures/gen_diagrams.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/docs/figures/gen_sequence.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/docs/figures/remaining_depth.svg +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/docs/figures/special_cases.svg +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/docs/figures/storage_hierarchy.dot +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/docs/figures/storage_hierarchy.svg +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/docs/figures/storage_mro.dot +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/docs/figures/storage_mro.svg +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/docs/figures/storage_sequence.svg +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/docs/index.rst +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/docs/installation.rst +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/docs/notebooks/CacheStack.ipynb +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/docs/notebooks/Caches.ipynb +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/docs/notebooks/Destructuring.ipynb +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/docs/notebooks/ExtraMethods.ipynb +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/docs/notebooks/FiveMinuteTour.ipynb +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/docs/notebooks/GettingStarted.ipynb +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/docs/notebooks/SecureStorage.ipynb +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/docs/notebooks/StorageBackends.ipynb +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/docs/notebooks/TransferWorkflow.ipynb +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/docs/storage/cache_stack.rst +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/docs/storage/destructuring.rst +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/docs/storage/security.rst +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/docs/usage/tldr.rst +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/notebooks/CacheStack.ipynb +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/notebooks/Caches.ipynb +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/notebooks/Destructuring.ipynb +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/notebooks/ExtraMethods.ipynb +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/notebooks/FiveMinuteTour.ipynb +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/notebooks/GettingStarted.ipynb +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/notebooks/SecureStorage.ipynb +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/notebooks/StorageBackends.ipynb +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/notebooks/TransferWorkflow.ipynb +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/release-please-config.json +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/setup.cfg +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/src/fleche/__init__.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/src/fleche/__main__.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/src/fleche/_attrs.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/src/fleche/caches.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/src/fleche/config.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/src/fleche/executor.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/src/fleche/metadata.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/src/fleche/query.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/src/fleche/remote.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/src/fleche/security.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/src/fleche/state.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/src/fleche/storage/__init__.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/src/fleche/storage/bagofholding_file.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/src/fleche/storage/base.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/src/fleche/storage/destructuring.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/src/fleche/storage/file.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/src/fleche/storage/memory.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/src/fleche/storage/pickle_file.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/src/fleche/storage/sql.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/src/fleche/storage/thread_safe.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/src/fleche/storage/void.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/src/fleche/wrapper.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/src/fleche.egg-info/dependency_links.txt +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/src/fleche.egg-info/top_level.txt +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/__init__.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/fixtures.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/integration/__init__.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/integration/test_hash_code_integration.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/integration/test_integration.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/integration/test_methods.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/integration/test_notebooks.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/integration/test_parallel_execution.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/integration/test_remote.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/integration/test_wrapper_query_integration.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/regression/__init__.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/regression/test_builtin_signature.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/regression/test_issue_1004.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/regression/test_issue_217.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/regression/test_issue_297.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/regression/test_issue_319.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/regression/test_issue_352.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/regression/test_issue_451.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/regression/test_issue_452.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/regression/test_issue_485.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/regression/test_issue_578.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/regression/test_issue_592.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/regression/test_issue_691.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/regression/test_sql_concurrent_save.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/regression/test_sql_non_sqlite_backends.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/regression/test_sql_table_uniqueness.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/regression/test_wrap_executor_cloudpickle_lock.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/smoke/__init__.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/smoke/test_optional_deps.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/strategies.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/__init__.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/caches/__init__.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/caches/test_cache_pool.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/caches/test_cache_stack.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/caches/test_expand_shrink.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/caches/test_filter.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/caches/test_gc.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/caches/test_lazy_call.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/caches/test_readonly_cache.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/caches/test_refreshing_cache.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/call/__init__.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/call/test_code_digest.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/call/test_matches.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/call/test_partial_binding.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/config/__init__.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/config/test_cache_from_config.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/config/test_cache_to_config.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/config/test_config.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/config/test_storage_to_config.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/digest/__init__.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/digest/test_attrs.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/digest/test_attrs_optional_dep.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/digest/test_digest_methods.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/digest/test_entry_points.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/fleche/__init__.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/fleche/test_bound_wrapper.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/fleche/test_dataclass_input.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/fleche/test_decorator_attributes.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/fleche/test_digest_args.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/fleche/test_futures.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/fleche/test_hash_code.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/fleche/test_ignore_digest.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/fleche/test_process_args.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/fleche/test_processpool.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/fleche/test_rerun.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/fleche/test_signature_binding.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/fleche/test_type_hints.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/fleche/test_workdir.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/metadata/__init__.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/metadata/test_metadata.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/storage/__init__.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/storage/test_bagofholding_file.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/storage/test_destructuring_dataclasses.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/storage/test_destructuring_storage.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/storage/test_file_storage.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/storage/test_mixins.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/storage/test_operation_context.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/storage/test_optional_deps.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/storage/test_overwrite.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/storage/test_pickle_file.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/storage/test_secure_storage.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/storage/test_short_digest.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/storage/test_sql_digest_types.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/storage/test_sql_evict.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/storage/test_sql_pragmas.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/storage/test_sql_query.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/storage/test_sql_url_coercion.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/storage/test_storage.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/storage/test_transform.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/storage/test_void.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/test_cache_sticky.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/test_executor.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/test_main.py +0 -0
- {fleche-0.22.2 → fleche-0.22.4}/tests/unit/test_remote.py +0 -0
|
@@ -35,7 +35,12 @@ jobs:
|
|
|
35
35
|
fetch-depth: 0
|
|
36
36
|
- uses: astral-sh/setup-uv@v5
|
|
37
37
|
if: needs.changes.outputs.run == 'true'
|
|
38
|
-
|
|
38
|
+
# Pin the interpreter to the oldest Python allowed by requires-python
|
|
39
|
+
# (keep in sync with pyproject.toml). Without it, uv picks the runner's
|
|
40
|
+
# system Python, which drifts with the image (ubuntu-26.04 ships 3.14),
|
|
41
|
+
# and the floors may have no wheels there, so they get built from source.
|
|
42
|
+
# uv downloads a managed 3.11 if the image lacks one.
|
|
43
|
+
- run: uv venv --python 3.11
|
|
39
44
|
if: needs.changes.outputs.run == 'true'
|
|
40
45
|
# --resolution lowest-direct pins every direct dependency to the lowest
|
|
41
46
|
# version its bound allows, exercising the floors declared in pyproject.
|
|
@@ -62,6 +62,29 @@ jobs:
|
|
|
62
62
|
if-no-files-found: ignore
|
|
63
63
|
retention-days: 90
|
|
64
64
|
|
|
65
|
+
executorlib:
|
|
66
|
+
needs: changes
|
|
67
|
+
if: needs.changes.outputs.run == 'true'
|
|
68
|
+
runs-on: ubuntu-latest
|
|
69
|
+
steps:
|
|
70
|
+
- uses: actions/checkout@v5
|
|
71
|
+
- name: Set up Python
|
|
72
|
+
uses: actions/setup-python@v6
|
|
73
|
+
with:
|
|
74
|
+
python-version: "3.12"
|
|
75
|
+
cache: pip
|
|
76
|
+
- name: Setup
|
|
77
|
+
shell: bash -l {0}
|
|
78
|
+
run: |
|
|
79
|
+
pip install -e .[tests,executorlib]
|
|
80
|
+
- name: Run executorlib tests
|
|
81
|
+
shell: bash -l {0}
|
|
82
|
+
env:
|
|
83
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
84
|
+
run: |
|
|
85
|
+
# The main build job doesn't install executorlib, so it skips these.
|
|
86
|
+
pytest tests/integration/test_parallel_execution.py -k executorlib -v -rs
|
|
87
|
+
|
|
65
88
|
sql-backends:
|
|
66
89
|
needs: changes
|
|
67
90
|
if: needs.changes.outputs.run == 'true'
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.22.4](https://github.com/pmrv/fleche/compare/0.22.3...0.22.4) (2026-11-06)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **call:** keep an explicit __version__ when the module cannot be imported ([#1028](https://github.com/pmrv/fleche/issues/1028)) ([f47005c](https://github.com/pmrv/fleche/commit/f47005c572d6c5eed65f6a28fab96b58bebef815))
|
|
9
|
+
|
|
10
|
+
## [0.22.3](https://github.com/pmrv/fleche/compare/0.22.2...0.22.3) (2026-11-06)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **digest:** digest numbers by exact value through a downcast tree ([#1020](https://github.com/pmrv/fleche/issues/1020)) ([4daccb3](https://github.com/pmrv/fleche/commit/4daccb3f34bf9f6b87620105a1f7e8bfde11155f))
|
|
16
|
+
|
|
3
17
|
## [0.22.2](https://github.com/pmrv/fleche/compare/0.22.1...0.22.2) (2026-10-23)
|
|
4
18
|
|
|
5
19
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fleche
|
|
3
|
-
Version: 0.22.
|
|
3
|
+
Version: 0.22.4
|
|
4
4
|
Summary: Hierachical, persistant caches in python
|
|
5
5
|
Author-email: Marvin Poul <pmrv@posteo.de>
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -43,7 +43,7 @@ Requires-Dist: attrs>=21.3; extra == "tests"
|
|
|
43
43
|
Provides-Extra: executorlib
|
|
44
44
|
Requires-Dist: executorlib; extra == "executorlib"
|
|
45
45
|
Provides-Extra: ty
|
|
46
|
-
Requires-Dist: ty==0.0.
|
|
46
|
+
Requires-Dist: ty==0.0.95; extra == "ty"
|
|
47
47
|
Dynamic: license-file
|
|
48
48
|
|
|
49
49
|
# fleche
|
|
@@ -130,11 +130,11 @@ Values and calls are stored separately so call records are queryable without des
|
|
|
130
130
|
`Hook`, `add_hook`, `get_hooks`, `load_entry_points`;
|
|
131
131
|
`Indigestible`;
|
|
132
132
|
`DIGEST_LENGTH=64`.
|
|
133
|
-
Built-in types are inline `match` cases in `_digest_bytes`, ordered roughly hot-path first: `int` (`bool ⊂ int`) → `Digest` (returns `value.encode()` so `digest(Digest("abc")) == "abc"`) → `str` → `None` → `
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
133
|
+
Built-in types are inline `match` cases in `_digest_bytes`, ordered roughly hot-path first: `int` (`bool ⊂ int`) → `Digest` (returns `value.encode()` so `digest(Digest("abc")) == "abc"`) → `str` → `None` → `datetime.timedelta` / `np.timedelta64` (must precede `Number`, as numpy registers `timedelta64` `Integral`;
|
|
134
|
+
refused with `Indigestible` until a digest honours their cross-type, cross-unit `==` — the old `total_seconds()` float merged unequal `pd.Timedelta` nanoseconds;
|
|
135
|
+
`datetime.timezone` digests its offset as exact microseconds instead) → `Number` (`float`, `complex`, `Decimal`, `Fraction`, plus `np.integer`/`np.floating` which satisfy `numbers.Number` automatically;
|
|
136
|
+
handed to `_numbers.digest_number(value, _digest_bytes)`, see `_numbers.py` below, and `Indigestible` when it returns `None`;
|
|
137
|
+
This replaced `hash(value)`, a lossy encoding under which distinct numbers collided) → `bytes` → numpy (`case _ if _numpy.is_numpy(value)`, which returns `_numpy.digest_numpy(value, _digest_bytes)`: `np.ndarray` and `np.bool_`, see `_numpy.py` below) → pandas (`case _ if _pandas.is_pandas(value)`, which returns `_pandas.digest_pandas(value, _digest_bytes)`: `pd.DataFrame` / `pd.Series` / `pd.Index` / `pd.NA`;
|
|
138
138
|
must precede `Iterable` — DataFrame iterates over column names only, so the `Iterable` arm collides any two DataFrames with the same columns regardless of values;
|
|
139
139
|
DataFrame folds in columns + per-column dtype + index, Series folds in name + dtype + index, Index folds in name + dtype;
|
|
140
140
|
values are mixed via `pd.util.hash_pandas_object(...).values.tobytes()` — `index=False` on the DataFrame/Series arms (the Index arm passes no kwarg since Index has no index) because `hash_pandas_object` only mixes per-element value bytes and ignores name/dtype/index metadata, so we hand-roll those.
|
|
@@ -161,13 +161,45 @@ Values and calls are stored separately so call records are queryable without des
|
|
|
161
161
|
bound methods are their own arm (`types.MethodType`: receiver + underlying function, so `obj1.m` and `obj2.m` differ;
|
|
162
162
|
a class receiver is named via `digest_class`), so there is no second entry point — `call._code_digest` reduces a bound method to `__func__` (the receiver already arrives as a call argument) and goes through `digest`;
|
|
163
163
|
`digest_class(cls)` names a class `module.QualName` and is shared by the built-in-`type` arm and the `__class__` cell.
|
|
164
|
-
`_new_hash(salt)`
|
|
165
|
-
|
|
166
|
-
|
|
164
|
+
`_new_hash(salt)` (imported from `_hashing.new_hash`) starts every salted running hash.
|
|
165
|
+
- `_hashing.py` — the hash primitive, kept out of `digest.py` so the arm modules it imports (`_numbers`, `_numpy`, `_pandas`) can share it without importing it back.
|
|
166
|
+
`new_hash(salt)` starts a salted SHA256;
|
|
167
|
+
`running_hash(salt)` is a context manager over it whose `.result` is the finished digest in `_digest_bytes`'s wire format (hex as bytes) once the block exits, and raises `RuntimeError` if read earlier, so an arm module never spells out `hexdigest().encode()` itself.
|
|
168
|
+
The primitive lives only here and the encoding only here and in `digest.py`'s own `hexdigest()` returns, never in an arm module: the #440 raw-bytes switch touches those, and patching `_hashing.hashlib` swaps every digest's hash (`test_redigest`, `test_hashing`).
|
|
169
|
+
`running_hash` costs about 0.3 µs per use, so `digest.py`'s own hot path keeps the bare `_new_hash` + `hexdigest()`.
|
|
170
|
+
- `_numbers.py` — the number digest arm, kept out of `digest.py`;
|
|
171
|
+
`digest_bytes` is passed in because `digest.py` imports it;
|
|
172
|
+
it hashes through `_hashing.running_hash` (`_salted`), never `hashlib` directly.
|
|
173
|
+
`digest_number` walks `NUMBER_TREE` (`int` → {`numbers.Integral`, `float` → {`complex` → `numbers.Complex`, `Fraction` → {`Decimal`, `numbers.Real`}}};
|
|
174
|
+
a complex with zero imaginary part first drops to its real part) from the root down to the value's own node and digests it as the first type whose converter holds it exactly, so equal values digest alike unless they sit on different branches below that type (none do in the current tree).
|
|
175
|
+
A value's node is the most derived tree type its type subclasses (`_path`, cached per type), registered ABCs included: a type in another's MRO is always its superclass, so one `issubclass` rule covers real subclasses and numpy scalars, which are only *registered* with the `numbers` ABCs — `np.int64` lands on `Integral`,
|
|
176
|
+
`np.float32`/`np.longdouble` on `Real` (below `float` and `Fraction`),
|
|
177
|
+
`np.complex64` on `Complex`, while `np.float64`/`np.complex128` subclass `float`/`complex`.
|
|
178
|
+
`_hold(value)` returns the node that holds a value and the converted value;
|
|
179
|
+
`test_branch_choice_follows_exact_value` checks its choice against exact arithmetic, since the converters go through numpy for numpy scalars.
|
|
180
|
+
`np.float32`/`np.longdouble` on `Real` (below `float` and `Fraction`), `np.complex64` on `Complex`, while `np.float64`/`np.complex128` subclass `float`/`complex`.
|
|
181
|
+
ABC nodes have no converter or encoder: they only place what sits under them, and `Decimal` never holds a value either (every finite `Decimal` is an exact `Fraction`).
|
|
182
|
+
A value nothing on its path holds gets `None`, which `digest.py` raises as `Indigestible`.
|
|
183
|
+
Converters must refuse inexact conversions themselves rather than rely on a trailing `==`: numpy compares `Fraction` to `np.longdouble` by rounding the `Fraction` to double, and an `np.longdouble` to an `int` through `str` (subject to `sys.int_max_str_digits`).
|
|
184
|
+
So `_as_int` decides on `as_integer_ratio` / `numerator`+`denominator` (also avoiding a quadratic `int()` round trip for `Decimal("1e1000000")`), falling back to `math.trunc` for a foreign `Real`, which promises `__trunc__` but neither `__int__` nor `as_integer_ratio`;
|
|
185
|
+
Encoders salt with the bare type name of the node (`_salted(float, …)`), like every generic arm;
|
|
186
|
+
a user type sharing the name and feeding the same bytes would collide, which is accepted as for all other types.
|
|
187
|
+
`_is_nan` skips `Rational`s and treats `OverflowError` as not-NaN, since `cmath.isnan` converts to float and `Fraction(10**400)` overflows it.
|
|
188
|
+
The promise, worded in `digest_equivalence.rst`: non-NaN numbers digest equal iff they hold the same exact value — numpy's `==` (NEP 50 down-casts, `Fraction` rounding) is not the reference.
|
|
189
|
+
`int` subclasses (`bool`, `IntEnum`) never reach the tree: the `int` arm catches them under their own type name, unlike `float` subclasses, which downcast.
|
|
190
|
+
The tree is drawn in the code comment and in `docs/digests/digest_equivalence.rst`;
|
|
191
|
+
no test checks the drawings, so update both by hand whenever `NUMBER_TREE` changes.
|
|
192
|
+
A NaN is digested by its bits exactly as the `float` (or, with an imaginary part, `complex`) node would encode them, whatever its type, so `np.float64("nan")` meets `float("nan")` and the sign survives;
|
|
193
|
+
`Decimal` NaNs (including `sNaN`, which `cmath.isnan` rejects) by `str()` under the `Decimal` salt.
|
|
194
|
+
Equal-value results of different numeric types share one value-storage entry, and whichever saved last wins the return type — intended, not a bug.
|
|
167
195
|
- `_attrs.py` — Internal `is_attrs_instance` / `field_items` helpers gating optional `attrs` support;
|
|
168
196
|
safe to import without `attrs` installed.
|
|
197
|
+
- `_numpy.py` — the `np.ndarray` and `np.bool_` digest arm (`is_numpy` / `digest_numpy(value, digest_bytes)`), plus `is_timedelta64` for `digest.py`'s timedelta refusal, so `digest.py` no longer imports numpy;
|
|
198
|
+
arrays digest dtype + shape + `tobytes`, structured dtypes each field name + field view recursively (`dtype.str` is only `|V<itemsize>` and padding bytes are uninitialised), other `dtype.hasobject` arrays every element of `ravel().tolist()` (their bytes are PyObject pointers);
|
|
199
|
+
`np.bool_` delegates to the builtin `bool` (`np.bool_ ∉ Number` since numpy 1.20).
|
|
200
|
+
numpy *number* scalars are not handled here but by `_numbers`.
|
|
169
201
|
- `_pandas.py` — Internal digest arms for pandas objects: `is_pandas` gates the one pandas case in `_digest_bytes`,
|
|
170
|
-
`digest_pandas` digests DataFrame / Series / Index / MultiIndex / `pd.NA` with the same salt
|
|
202
|
+
`digest_pandas` digests DataFrame / Series / Index / MultiIndex / `pd.NA` with the same bare-type-name salt as `_digest_bytes`, through `_hashing.running_hash`, and `needs_element_digests` / `element_digests` handle object columns (#1004);
|
|
171
203
|
`digest_pandas` and `element_digests` take `digest._digest_bytes` as an argument, since `digest.py` imports the module.
|
|
172
204
|
- `call.py` — `Call` dataclass (`from_call`, `to_lookup_key`, `.stash(values)→DigestedCall` writes args+result to value storage / `.digest()→DigestedCall` digests without saving);
|
|
173
205
|
`DigestedCall` (storage representation: arguments and result are `Digest` keys;
|
|
@@ -404,7 +436,7 @@ Values and calls are stored separately so call records are queryable without des
|
|
|
404
436
|
best-effort — non-Linux or an unreadable `/proc/mounts` reports "not network";
|
|
405
437
|
network types are `nfs`/`nfs4`/`cifs`/`smb*`/`ceph*`/`glusterfs`/`9p`/`lustre`/`gpfs`/`panfs`/`afs`/`afpfs`;
|
|
406
438
|
a warning is logged when WAL is skipped).
|
|
407
|
-
`:memory:` skips `journal_mode` entirely.
|
|
439
|
+
`:memory:` skips `journal_mode` entirely — the check is `engine.url.database in (None, ":memory:")`, because SQLAlchemy percent-encodes `:memory:` when rendering the URL, so a string match on the rendered URL never fires (a dead short-circuit until PR #980 fixed it).
|
|
408
440
|
The schema is three tables: `calls`, `arguments` (one row per arg, `UNIQUE(call_key, name)`, ordered by `position`), `metadata` (one JSON blob per metadata namespace, `UNIQUE(call_key, name)`).
|
|
409
441
|
`arguments.call_key`/`metadata.call_key` are FKs with `ondelete="CASCADE"`, so `_evict` is a single `session.execute(delete(CallModel).where(...))` (SQLAlchemy DSL, no raw SQL or ORM materialisation) and lets the DB clean up child rows (SQLite needs the `foreign_keys=ON` PRAGMA the dialect listener installs;
|
|
410
442
|
Postgres/MySQL enforce FK cascades natively).
|
|
@@ -757,6 +789,18 @@ Git history is the changelog.
|
|
|
757
789
|
Planned refactor moves built-in type handlers into an internal dispatch registry that mirrors the user `Hook` list;
|
|
758
790
|
composes with #834.
|
|
759
791
|
|
|
792
|
+
- **Enum digests** (#1022, survey filed 2026-10-24 — nothing decided;
|
|
793
|
+
probed on the #1020 branch).
|
|
794
|
+
Current behaviour: plain `Enum` members are `Indigestible`;
|
|
795
|
+
`IntEnum`/`StrEnum`/mixin members digest through their mixin's arm under the bare class `__name__` (two same-named classes sharing a value collide;
|
|
796
|
+
float/`Decimal` mixins go through the number logic).
|
|
797
|
+
Two bugs the survey found: `Flag` members — and `Flag` classes passed as values — raise `RecursionError` through the `Iterable` arm on 3.11+ (a Flag member iterates and yields itself;
|
|
798
|
+
`wrapper.py` only falls back on `Indigestible`, so a decorated `f(Perm.R)` *raises* instead of running uncached), and destructuring storage mishandles mixin enums (a `tuple`-mixin member fails `ValueMemory.save` with `ValueError`;
|
|
799
|
+
a dataclass-mixin member loads back as a bogus non-member).
|
|
800
|
+
Designs surveyed: digesting the full `{member_name: value}` mapping makes every member's digest depend on the whole class (drifts across Python versions — `http.HTTPStatus` has 62 members on 3.11, 66 on 3.13 — one indigestible sibling poisons all members, ~50× cost);
|
|
801
|
+
the leaner alternative digests class `__name__` + canonical member name (`None` for Flag pseudo-members) + `_value_` under one enum salt, placed before the whole `match` (float/tuple/date/dataclass mixins), trading away rename-relocatability.
|
|
802
|
+
Any enum arm changes `IntEnum`/`IntFlag`/`StrEnum`/mixin digests, and relocation only ever helps arguments — a stored result still unpickles through the old module path.
|
|
803
|
+
|
|
760
804
|
**Open refactor proposals** (small, well-scoped, no PRs yet)
|
|
761
805
|
|
|
762
806
|
*Source:*
|
|
@@ -876,24 +920,14 @@ Git history is the changelog.
|
|
|
876
920
|
Composes with #883/#905 and #333.
|
|
877
921
|
- #883 — `DestructuringMixin._raw_sub_digests` hardcodes a `match` over the three built-in `Digested` types, so a subclass registered via `register_destructurer` reports zero children: `Cache.gc()` can evict values reachable only through it (silent data loss) and `count_reuses()` under-counts.
|
|
878
922
|
Fix in flight as PR #905 (below).
|
|
879
|
-
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
The `Number` arm digests via `hash(value)`, a lossy 61-bit residue, so distinct numbers share cache keys and produce wrong hits end to end: `digest(-1.0) == digest(-2.0)` (CPython reserves `-1` as an error sentinel),
|
|
888
|
-
`digest(0.5) == digest(2**60)` (rational hashing mod `2**61 - 1`),
|
|
889
|
-
`inf` vs `314159`,
|
|
890
|
-
`np.uint64(2**63)` vs `4`, complex via the `hash(a) + 1000003*hash(b)` formula;
|
|
891
|
-
digests are also platform-dependent (32-bit builds hash mod `2**31 - 1`).
|
|
892
|
-
The Hypothesis pair tests in `digest/test_digest.py` can't draw these coincidences, and the complex test's oracle restates the bug (`hash(x) == hash(y)` ⇒ equal digests) — regression tests need explicit `@example`s.
|
|
893
|
-
A second, separable finding: `digest(3) == digest(3.0)` by design, so equal-value different-type *results* share one value-storage entry and whichever saved last wins the return type.
|
|
894
|
-
The PR comment carries a validated `_digest_number` prototype (digest the exact rational via `as_integer_ratio`, dedicated salts for rational/complex/inf, NaN keeps `struct.pack`;
|
|
895
|
-
full suite green) pending maintainer decisions on unknown `Number` types (raise `Indigestible` vs keep `hash()`) and on result-type erasure.
|
|
896
|
-
The fix invalidates number-bearing keys — pre-1.0 that is a `fix:` per [Commit messages](#commit-messages), and it is worth coordinating with the blake2b switchover #615, which invalidates every key anyway.
|
|
923
|
+
- `test-minimum-deps` is red on every run since 2026-10-23, also with no repo change: `ubuntu-latest` moved from ubuntu-24.04 (Python 3.12) to ubuntu-26.04 (Python 3.14, gcc 15), the job's bare `uv venv` uses that system Python, and `--resolution lowest-direct` selects pandas 2.2.0, which has no cp314 wheel and whose Cython 3.0.5 output does not compile under gcc 15.
|
|
924
|
+
The fix is in flight as PR #1026 (below): `uv venv --python 3.11`, the `requires-python` floor, so the job tests the dependency floors on the oldest supported Python and stops drifting with the runner image.
|
|
925
|
+
No issue filed yet.
|
|
926
|
+
Until it merges, a red `test-minimum-deps` on a `src/`-touching PR is likewise not that PR's failure.
|
|
927
|
+
Running the job on uv's managed 3.11 exposed a real cache-correctness bug, fixed in flight as PR #1028 (below): when `VersionInfo.of` raises `ModuleNotFoundError`,
|
|
928
|
+
`FunctionProfile.of`'s fallback set `version = None`, dropping the function's own `__version__` (the one `fleche(version=...)` sets) — so two versions of a function from an unimportable module shared one cache key and the second was served the first one's result.
|
|
929
|
+
Merge order: #1028 first, then rebase #1026 (its `test_fleche_with_version` failure under uv's Python — whose standalone builds ship no stdlib `test` package, so the test's `__module__ = "test"` import genuinely fails — needs the fix).
|
|
930
|
+
|
|
897
931
|
|
|
898
932
|
**Feature requests**
|
|
899
933
|
|
|
@@ -906,6 +940,13 @@ Git history is the changelog.
|
|
|
906
940
|
|
|
907
941
|
**Field reports**
|
|
908
942
|
|
|
943
|
+
- #1019 — config lookup misses inside executorlib *file-backend* jobs (`SlurmClusterExecutor`, `TestClusterExecutor`) when `cache_directory` sits behind a symlink: the job chdirs into its task folder,
|
|
944
|
+
`getcwd()` returns the physical path, the upward `fleche.toml` walk never reaches the project directory, and fleche silently (INFO-level) falls back to a transient memory cache.
|
|
945
|
+
Inside an MPI/collective job the miss then recomputes on *every rank*.
|
|
946
|
+
Workarounds that hit: `wrap_executor` in the driver (the `BoundWrapper` carries the cache config into the payload), or resolving cached values in the driver and passing them in — the latter is the only option that also avoids per-rank recompute on a genuinely cold call.
|
|
947
|
+
Asks in the issue: document the collective-job pattern, make the fallback loud (WARN/raise when a parent had a persistent cache), and a `FLECHE_CONFIG`-style explicit config location (prototype on local branch `claude/fleche-config-env-var`, not pushed;
|
|
948
|
+
related to #868 and #568).
|
|
949
|
+
|
|
909
950
|
- #942 — mine it before building anything in this area: migrating ~20k cached calls across a module rename under the default `hash_module=True`.
|
|
910
951
|
The call index was rewritten via `dataclasses.replace(digested_call, module=new)` + `.to_lookup_key()` (reusing fleche's own key derivation), run add-only first against a backup copy, verified by a recompute-counter test (a `contains()` check would pass on a silent recompute), then `--replace` + `VACUUM` (SQLite does not shrink on delete).
|
|
911
952
|
Value storage is untouched — a module rename changes call keys only.
|
|
@@ -915,20 +956,60 @@ Git history is the changelog.
|
|
|
915
956
|
|
|
916
957
|
- #887 — `BoundWrapper.__reduce__` pre-serialises `func` into a standalone bytes payload (plain `pickle` by reference when importable, `cloudpickle` by value otherwise, a clear `TypeError` when cloudpickle is missing), so it survives `spawn`/`forkserver` pools;
|
|
917
958
|
closes #840, regression pin `tests/regression/test_issue_840.py`.
|
|
918
|
-
- #
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
Setup creates database and user `fleche` with a random password and a free port;
|
|
926
|
-
the URL is written back as `url` into the declaring `fleche.toml` via tomlkit (comments survive), and a readable `DATADIR/fleche.url` takes precedence over it;
|
|
927
|
-
to support the write-back, the config loader now records which file each top-level table came from.
|
|
928
|
-
New CLI `python -m fleche db {postgres,mariadb} DATADIR` runs the server in the foreground (`--cache NAME` records the server into config, refusing in-memory values, stacks with several persistent layers, pools and SSH caches) and `python -m fleche db stop DATADIR` shuts it down;
|
|
929
|
-
`serve` writes host+pid to `DATADIR/fleche.server` before starting, and autostart refuses a datadir another host is serving (shared filesystems).
|
|
930
|
-
New extras `fleche[postgres]` / `fleche[mariadb]` (sqlalchemy + driver + tomlkit).
|
|
959
|
+
- #1014 / #1017 / #1018 (opened 2026-10-21/22) — fleche-hosted Postgres/MariaDB call storage, a stack of three PRs, each based on the previous.
|
|
960
|
+
**#1014** (base): new CLI `python -m fleche db {postgres,mariadb} DATADIR` sets up `DATADIR` on first run (database and user `fleche`, random password kept in `DATADIR/fleche.url` mode 0600, a free port remembered across restarts) and runs the server in the foreground;
|
|
961
|
+
`python -m fleche db stop DATADIR` stops it via the host+pid that `serve` records in `DATADIR/fleche.server`.
|
|
962
|
+
Config side: `calls.type = "postgres"` / `"mariadb"` plus `calls.datadir` (resolved relative to the declaring `fleche.toml`, like `root`) builds a plain `Sql` on the URL from `DATADIR/fleche.url`, so the password is never copied;
|
|
963
|
+
nothing listening raises an error naming the start command.
|
|
964
|
+
New extras `fleche[postgres]` / `fleche[mariadb]`;
|
|
965
|
+
URLs name their driver explicitly because SQLAlchemy 2.1 changed what driver a bare `postgresql://` selects.
|
|
931
966
|
Rides with two `Sql` concurrency fixes hit with 8 writers on a fresh server database — `create_all`'s check-then-create schema race retries, and `save` retries deadlock-aborted transactions (MariaDB error 1213, Postgres SQLSTATEs `40P01`/`40001`) — and a test-hygiene fix (`_test_missing_dep` now restores the storage registry it used to leave broken for later tests).
|
|
967
|
+
**#1017** (on #1014): `calls.autostart = true` — when nothing is listening, the first process launches `python -m fleche db …` detached (own session, logging to `<datadir>.log`) and waits until ready;
|
|
968
|
+
a `FileLock` on `<datadir>.lock` makes racing processes wait for the winner, so exactly one server starts, and it outlives the process.
|
|
969
|
+
Autostart never *creates* a database — a missing or uninitialized datadir is a `DbServerError` naming the setup command — and refuses a datadir another host is serving (shared filesystems).
|
|
970
|
+
**#1018** (on #1017): the URL is written back as `url` into the declaring `fleche.toml` via tomlkit (comments survive;
|
|
971
|
+
the config loader records which file each top-level table came from to support this), and `db --cache NAME` records the server into config (refusing in-memory values;
|
|
972
|
+
stacks with persistent layers, pools and SSH caches).
|
|
973
|
+
- #1020 (opened 2026-10-23) — fixes the numeric digest collisions found on PR #990 (`hash()` made `digest(-1.0) == digest(-2.0)` and `digest(0.5) == digest(2**60)`): `_digest_bytes`'s `Number` arm stops routing through lossy `hash()` and instead digests each number as the first type on an explicit downcast tree (`int → float → {complex, Fraction → Decimal}`) that holds its exact value, so non-NaN numbers digest equal **iff** they are the same exact value (`1 == 1.0 == 1+0j` as `int`;
|
|
974
|
+
`0.5 == Fraction(1, 2) == Decimal("0.5")` as `float`), with converters deciding exactness themselves (`as_integer_ratio`, `math.trunc`) rather than trusting numpy's NEP-50 `==`;
|
|
975
|
+
a `Number` nothing on its path holds exactly raises `Indigestible`, never `hash()`.
|
|
976
|
+
NaNs digest by their bits, whatever their type;
|
|
977
|
+
timedeltas (`datetime.timedelta`, `pd.Timedelta`, `np.timedelta64`) now raise `Indigestible` until one digest can honour cross-type equality without unit loss (`total_seconds()` collided distinct `pd.Timedelta`s) — PR #1023 (below), stacked on this one, carries that digest;
|
|
978
|
+
`datetime.timezone` digests its offset as exact microseconds.
|
|
979
|
+
Also moves the number arm into a private `fleche._numbers` module and the numpy arms into `fleche._numpy` (the `_pandas` pattern), and extracts a shared `fleche._hashing` module (`new_hash(salt)` + a `running_hash` context manager) that all three use.
|
|
980
|
+
Persisted keys involving non-integral numbers, `±inf`, complex, huge ints, NaNs or timezones change (`Cache.redigest()` migrates;
|
|
981
|
+
plain `int` keys are unchanged).
|
|
982
|
+
Supersedes #990 (closed unmerged 2026-10-24;
|
|
983
|
+
the numeric-digest investigation referenced under **Bugs** stays readable in its review comments);
|
|
984
|
+
overlaps #1000's NaN-arm pins, so one of the two needs a rebase.
|
|
985
|
+
- #1023 (opened 2026-10-24, stacked on #1020 — retargets to `main` once that merges) — digests each library's timedeltas by **exact length**, lifting #1020's `Indigestible` refusal: `datetime.timedelta` as exact microseconds (beside the other datetime arms in `digest.py`),
|
|
986
|
+
`pd.Timedelta` as exact nanoseconds via `to_timedelta64()` (`.value` would overflow;
|
|
987
|
+
the arm sits in `_pandas` ahead of the `datetime.timedelta` arm it subclasses),
|
|
988
|
+
`np.timedelta64` as exact attoseconds for every fixed unit,
|
|
989
|
+
`W` to `as` (in `_numpy`, ahead of `Number` — numpy registers it as `Integral`).
|
|
990
|
+
Libraries deliberately digest apart even where they compare equal (merging would force a unit-lossy common denominator), units within one library merge, and a timedelta never digests like a number;
|
|
991
|
+
`Y`/`M` digest as months, the unitless `np.timedelta64(5)` as its count, NaT alike in every unit — numpy's cross-unit `==` can wrap int64, so it is not the reference.
|
|
992
|
+
`datetime.timezone` digests its offset through the `datetime.timedelta` arm;
|
|
993
|
+
timedelta64 *arrays*/Series are unchanged (dtype + bytes).
|
|
994
|
+
`np.datetime64` stays `Indigestible`.
|
|
995
|
+
- #1024 (opened 2026-10-24) — Python 3.15 support: `requires-python` → `>=3.11,<3.16`, 3.15 in the `tests.yml` matrix with `allow-prereleases: true`.
|
|
996
|
+
The one real breakage: on 3.15 `threading.RLock` is a plain function, so `_factory = threading.RLock` bound as a method and `_PicklableRLock()` raised `TypeError` — took down every `SerializingMixin` storage and `SizeLimitedCache` (~36 failures);
|
|
997
|
+
the lock factories are now real `@staticmethod`s.
|
|
998
|
+
The 3.15 job builds h5py from source (no cp315 wheels yet;
|
|
999
|
+
drop that step once wheels publish) and is **blocked**: `pyiron-snippets` and `bagofholding` both declare `Requires-Python <3.15` on every release, so pip refuses them on 3.15 (options in a PR comment).
|
|
1000
|
+
Also carries the `ty` fix for the pandas-3.1 `pd.util` regression (fixed by #1020) as its own commit, and moves `test_fleche_with_version` off `__module__ = "test"` (only resolves when the stdlib `test` package is installed — uv's standalone builds leave it out).
|
|
1001
|
+
- #1026 (opened 2026-10-25) — pins `test-minimum-deps`'s venv to Python 3.11 via `uv venv --python 3.11` (see **Bugs**: the job is red since the runner image moved to ubuntu-26.04, whose system Python 3.14 has no pandas 2.2.0 wheel and fails its source build under gcc 15);
|
|
1002
|
+
keep the pin in sync with the `requires-python` floor.
|
|
1003
|
+
The rejected alternative was raising the numpy/pandas floors for every user just to get cp314 wheels;
|
|
1004
|
+
the 3.12–3.14 interpreters stay covered with current deps by the `tests.yml` matrix.
|
|
1005
|
+
Blocked on #1028 (below) — merge that first, then rebase.
|
|
1006
|
+
- #1028 (opened 2026-10-28) — `FunctionProfile.of` keeps an explicit `__version__` when the function's module cannot be imported: the `ModuleNotFoundError` fallback in `src/fleche/call.py` set `version = None` instead of `getattr(func, "__version__", None)`, so two versions of a function from an unimportable module shared one cache key (see **Bugs**).
|
|
1007
|
+
Surfaced by #1026's uv-managed Python, where `test_fleche_with_version`'s mock module name `"test"` stops resolving (uv's standalone CPython ships no stdlib `test` package) and the fallback actually runs.
|
|
1008
|
+
New regression test uses a module name that never exists, so it covers the fallback on every interpreter.
|
|
1009
|
+
Its own `test-minimum-deps` check stays red until #1026's pin lands — the two merge in that order: #1028, then #1026 rebased.
|
|
1010
|
+
- #1032 (dependabot, opened 2026-11-04, replacing #1030, closed unmerged the same day) — bumps the pinned `ty` type-checker (the `ty` extra, which the CI job installs) 0.0.89 → 0.0.95.
|
|
1011
|
+
Its `ty` run fails with the same three pandas-3.1 `call-non-callable` diagnostics as `main` before #1020, so the bump neither causes nor clears the known red;
|
|
1012
|
+
`test-minimum-deps` is red as on every PR until #1026 lands, and the 3.11–3.14 test matrix is green.
|
|
932
1013
|
- #786 (draft) — a process-wide cache of read-only `h5py.File` handles for multi-bag files (bounded MRU of 32, `(inode, mtime_ns, size)` invalidation, `locking=False`, writers close the cached handle under the per-bag lock);
|
|
933
1014
|
multi-bag `contains` drops from ~240 µs to ~29 µs.
|
|
934
1015
|
`evict` deliberately untouched;
|
|
@@ -944,24 +1025,20 @@ Git history is the changelog.
|
|
|
944
1025
|
Needs a rebase onto `tldr.rst`/`helpers.rst` as they are on `main`.
|
|
945
1026
|
- #934 — SSH config + destructuring list.
|
|
946
1027
|
Needs a rebase onto `ssh_cache.rst` as #993 left it.
|
|
947
|
-
- #965 — usage: scope the lazy-return claim to `load()`/`query()`.
|
|
948
|
-
- #989 — parallel_execution: an MPI-decorated call runs once per rank cold and serves one rank's record warm.
|
|
949
|
-
- #990 — digests: the numeric-digest mechanism explanation;
|
|
950
|
-
superseded by its own review — the maintainer judged the rewrite the wrong level ("warrants deeper investigation") and the follow-up investigation on the PR found the digest *code* at fault (see the numeric-digest bullet under **Bugs**), so expect it to close in favour of a code fix rather than merge.
|
|
951
1028
|
|
|
952
|
-
#
|
|
953
|
-
|
|
1029
|
+
#990 (digests: the numeric-digest mechanism explanation) closed unmerged 2026-10-24 as superseded — its review investigation found the digest *code* at fault, and PR #1020 (above) carries the code fix plus the docs promise.
|
|
1030
|
+
#932 needs a rebase: #989 (merged 2026-10-22) changed `parallel_execution.rst` under it.
|
|
954
1031
|
- Test-only coverage sweeps: #939 (pins the `Intent.READ` no-op fast path in both lock mixins;
|
|
955
|
-
drops
|
|
1032
|
+
drops tests strictly weaker than a sibling — rebased 2026-10-23, where three of its eight digest-smoke removals turned out to have already landed via the merged #980 and are now no-ops;
|
|
956
1033
|
converts `test_local_function_digests_same_as_module_level` into `test_nesting_changes_a_function_digest` — `CO_NESTED` is in `co_flags`, so lifting a helper out of an enclosing function invalidates its cached calls under `hash_code=True`;
|
|
957
1034
|
pinned as observed, flip it there if ruled a bug), #955 (pins the deferred-future abandon arm in `wrapper.py`;
|
|
958
|
-
collapses five single-property tests in `test_dehydrate.py`), #
|
|
959
|
-
drops 11 vacuous digest smokes), #1000 (pins the NaN `struct.pack` arm deterministically — two *simultaneously live* same-value NaNs must share a digest, which `hash()`-routing would break since CPython's NaN hash is address-derived;
|
|
1035
|
+
collapses five single-property tests in `test_dehydrate.py`), #1000 (pins the NaN `struct.pack` arm deterministically — two *simultaneously live* same-value NaNs must share a digest, which `hash()`-routing would break since CPython's NaN hash is address-derived;
|
|
960
1036
|
covers the `<d` `Decimal`-NaN sub-arm and the `np.bool_` → builtin-`bool` delegation;
|
|
961
1037
|
narrows the Hypothesis float/complex pair tests to `allow_nan=False`, killing their self-fulfilling NaN oracle branches;
|
|
962
1038
|
drops four digest tests subsumed *in assertion* by siblings, after a per-test-arc ablation confirmed 89% of tests have zero unique arcs — reaffirming the collective-redundancy lesson below;
|
|
963
1039
|
also `.gitignore`s `pytest --cov`'s `.coverage` litter;
|
|
964
|
-
the PR body flags the next deterministic-coverage picks, led by `bagofholding_file.py`'s seed-dependent `SaveError` arm
|
|
1040
|
+
the PR body flags the next deterministic-coverage picks, led by `bagofholding_file.py`'s seed-dependent `SaveError` arm;
|
|
1041
|
+
rebased 2026-10-23 and re-measured — note PR #1020 replaces the NaN arm it pins, so one of the two needs a rebase).
|
|
965
1042
|
|
|
966
1043
|
**Lessons and standing decisions**
|
|
967
1044
|
|
|
@@ -986,8 +1063,7 @@ Git history is the changelog.
|
|
|
986
1063
|
an addon package (`fleche-mpi` or similar) is the preferred home, and the close-out comment on #870 is its reference.
|
|
987
1064
|
Branch `claude/fleche-mpi-executor-caching-zxjazw` keeps the helper, 13 subprocess-isolated integration tests, and `docs/mpi_execution.rst`.
|
|
988
1065
|
Findings that stand regardless: lookup keys carry no rank, so all ranks contend for one record — warm runs flip the `[result, None]` return shape, and divergent per-rank hit/miss decisions make some ranks skip the body's collectives (silently wrong, or a hang).
|
|
989
|
-
|
|
990
|
-
`wrap_executor`'s cold/warm return-shape asymmetry is still undocumented in `docs/parallel_execution.rst` (#989 adds the per-rank warning).
|
|
1066
|
+
User docs stay silent on MPI-native functions: fleche makes no promises either way, so #1037 removed the per-rank caveat #989 had added to `docs/parallel_execution.rst`, and the executorlib example uses task parallelism (`max_workers`) rather than `resource_dict={"cores": N}`.
|
|
991
1067
|
- Optional skip-saving of input arguments (#170), corruption-tolerant loads (#93), method-state restoration (#99), metadata-into-value-storage (#82) — parked for milestone 2.0.
|
|
992
1068
|
- Refactors flagged `backburner`: `_digest` dispatch registry (#333), `DigestedCall`/`LazyCall` consolidation (#404).
|
|
993
1069
|
- Terminology shift "cache" → "stash" and unifying "hash"/"digest" on "digest" (#350) — targeted for 1.0, not started.
|
|
@@ -34,3 +34,8 @@ correctly without special-casing at call sites.
|
|
|
34
34
|
``FunctionProfile`` and populate it inside ``FunctionProfile.of``. Downstream
|
|
35
35
|
code then reads it from the profile instead of calling introspection APIs
|
|
36
36
|
directly.
|
|
37
|
+
|
|
38
|
+
See :doc:`/usage/helpers` ("Per-Function Static Caching") for the
|
|
39
|
+
user-facing behavior and caveats — including the "mutating ``func`` after
|
|
40
|
+
the first call isn't picked up" gotcha and how to clear the cache via
|
|
41
|
+
``_profile.cache_clear()``.
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
======================
|
|
5
5
|
|
|
6
6
|
This page is a tour of :mod:`fleche.remote` for contributors hacking on the
|
|
7
|
-
cross-machine cache. End-user
|
|
8
|
-
:doc:`/storage/configuration
|
|
9
|
-
together.
|
|
7
|
+
cross-machine cache. End-user configuration lives in
|
|
8
|
+
:doc:`/storage/configuration` (see its ``type = "ssh"`` section); this page
|
|
9
|
+
explains *how* the pieces fit together.
|
|
10
10
|
|
|
11
11
|
Security model
|
|
12
12
|
--------------
|
|
@@ -9,24 +9,77 @@ hold the same numeric value, and ``digest((1, 2)) != digest([1, 2])`` because th
|
|
|
9
9
|
container type matters. This page documents the deliberate equivalences ``fleche``
|
|
10
10
|
guarantees, and the boundaries you should keep in mind.
|
|
11
11
|
|
|
12
|
-
The digest function is content-based: it walks each value
|
|
13
|
-
|
|
14
|
-
representation that is stable across processes and Python versions (where the
|
|
12
|
+
The digest function is content-based: it walks each value and folds in its type
|
|
13
|
+
and a representation that is stable across processes and Python versions (where the
|
|
15
14
|
underlying object's representation allows it).
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
16
|
+
Numbers
|
|
17
|
+
-------
|
|
18
|
+
|
|
19
|
+
Numbers digest by their value, not their type: ``1``, ``1.0``, ``1+0j`` and
|
|
20
|
+
``Fraction(1)`` all digest alike. To find that value, ``fleche`` walks a fixed
|
|
21
|
+
tree of number types:
|
|
22
|
+
|
|
23
|
+
.. code-block:: text
|
|
24
|
+
|
|
25
|
+
int
|
|
26
|
+
├── numbers.Integral
|
|
27
|
+
└── float
|
|
28
|
+
├── complex
|
|
29
|
+
│ └── numbers.Complex
|
|
30
|
+
└── fractions.Fraction
|
|
31
|
+
├── decimal.Decimal
|
|
32
|
+
└── numbers.Real
|
|
33
|
+
|
|
34
|
+
A number's place in the tree is its class or, for a type the tree does not
|
|
35
|
+
name, the :mod:`numbers` ABC the type belongs to; that is where numpy scalars
|
|
36
|
+
such as ``np.int64`` or ``np.float32`` sit. The digest walks from ``int`` down
|
|
37
|
+
to that place and tries to convert the number to each type it passes. The
|
|
38
|
+
first type that holds the number exactly digests it: ``0.5`` is no ``int`` but
|
|
39
|
+
an exact ``float``, and ``Fraction(1, 3)`` is neither, so it digests as a
|
|
40
|
+
``Fraction``. A complex number with no imaginary part digests as its real
|
|
41
|
+
part. A number that no type on its path holds exactly is
|
|
42
|
+
:exc:`~fleche.digest.Indigestible` unless a hook handles it.
|
|
43
|
+
|
|
44
|
+
Two numbers therefore digest equal exactly when they hold the same exact value.
|
|
45
|
+
Three kinds of value are not digested as numbers:
|
|
46
|
+
|
|
47
|
+
* ``bool`` and other ``int`` subclasses, such as ``IntEnum`` members, digest
|
|
48
|
+
as their own type, so ``digest(True) != digest(1)``.
|
|
49
|
+
* NaNs equal nothing, so they digest by their bit pattern: NaNs with the same
|
|
50
|
+
bits digest alike whatever their type.
|
|
51
|
+
* Timedeltas (``datetime.timedelta``, ``pd.Timedelta`` and ``np.timedelta64``)
|
|
52
|
+
are refused with :exc:`~fleche.digest.Indigestible` for now, although numpy
|
|
53
|
+
counts ``np.timedelta64`` as a ``numbers.Integral``. A hook registered with
|
|
54
|
+
:func:`~fleche.digest.add_hook` can digest them meanwhile.
|
|
55
|
+
|
|
56
|
+
For example:
|
|
57
|
+
|
|
58
|
+
.. code-block:: python
|
|
59
|
+
|
|
60
|
+
digest(1) == digest(1.0) == digest(1 + 0j) == digest(Fraction(1)) # as int
|
|
61
|
+
digest(0.5) == digest(np.float32(0.5)) == digest(Decimal("0.5")) # as float
|
|
62
|
+
digest(Fraction(1, 10)) == digest(Decimal("0.1")) # as Fraction
|
|
63
|
+
digest(-1.0) != digest(-2.0)
|
|
64
|
+
digest(0.1) != digest(Decimal("0.1")) # the float is not exactly 1/10
|
|
65
|
+
digest(True) != digest(1)
|
|
66
|
+
|
|
67
|
+
A number is converted to a lower type only when the conversion is lossless, so
|
|
68
|
+
``digest(a) == digest(b)`` exactly when ``a`` and ``b`` are numerically equal.
|
|
69
|
+
For standard library types this agrees with ``==``, but third party types do not
|
|
70
|
+
always: numpy casts a Python float down to the precision of a numpy scalar
|
|
71
|
+
before comparing (so ``np.float32(0.1) == 0.1``), rounds a ``Fraction`` to
|
|
72
|
+
double, and compares a huge ``np.longdouble`` with an ``int`` through their
|
|
73
|
+
string forms. Following that ``==`` would let unequal values share a cache
|
|
74
|
+
entry, so ``fleche`` goes by the exact value instead: ``np.float32(0.1)``
|
|
75
|
+
stores 0.100000001490116…, and digests apart from ``0.1``.
|
|
76
|
+
|
|
77
|
+
Other types
|
|
78
|
+
-----------
|
|
79
|
+
|
|
80
|
+
Every other value digests by its type as well as its content, so
|
|
81
|
+
``digest((1, 2)) != digest([1, 2])``: a tuple and a list never digest alike,
|
|
82
|
+
whatever their elements.
|
|
30
83
|
|
|
31
84
|
Dataclasses and ``attrs`` classes
|
|
32
85
|
---------------------------------
|
|
@@ -454,11 +454,11 @@
|
|
|
454
454
|
"\n",
|
|
455
455
|
"```python\n",
|
|
456
456
|
"with cache(shared_cache):\n",
|
|
457
|
-
" with SingleNodeExecutor() as executor
|
|
457
|
+
" with SingleNodeExecutor(max_workers=4) as executor: # 4 tasks at once\n",
|
|
458
458
|
" wrap_executor(executor)\n",
|
|
459
459
|
" future = executor.submit(\n",
|
|
460
460
|
" expensive, 5,\n",
|
|
461
|
-
" resource_dict={\"
|
|
461
|
+
" resource_dict={\"threads_per_core\": 2}, # goes to SingleNodeExecutor.submit\n",
|
|
462
462
|
" )\n",
|
|
463
463
|
" future.result() # goes to expensive\n",
|
|
464
464
|
"```\n",
|