pax_global_header00006660000000000000000000000064151624126450014520gustar00rootroot0000000000000052 comment=41c726586cfef55de0c8474923d2734f1529d223 qlustered-deepdiff-41c7265/000077500000000000000000000000001516241264500155515ustar00rootroot00000000000000qlustered-deepdiff-41c7265/.bumpversion.cfg000066400000000000000000000004511516241264500206610ustar00rootroot00000000000000[bumpversion] current_version = 9.0.0 commit = False tag = False tag_name = {new_version} [bumpversion:file:pyproject.toml] [bumpversion:file:README.md] [bumpversion:file:CITATION.cff] [bumpversion:file:docs/index.rst] [bumpversion:file:docs/conf.py] [bumpversion:file:deepdiff/__init__.py] qlustered-deepdiff-41c7265/.coveragerc000066400000000000000000000001211516241264500176640ustar00rootroot00000000000000[report] omit = */python?.?/* */site-packages/nose/* *__init__* qlustered-deepdiff-41c7265/.direnvrc.example000066400000000000000000000006321516241264500210210ustar00rootroot00000000000000function load_venv () { ACTUAL_VENV_PATH="$HOME/.venvs/$1" if [ -d "$ACTUAL_VENV_PATH" ] && [ -f "$ACTUAL_VENV_PATH/bin/activate" ]; then echo "direnv: Activating $ACTUAL_VENV_PATH..." source "$ACTUAL_VENV_PATH/bin/activate" export UV_PROJECT_ENVIRONMENT="$ACTUAL_VENV_PATH" else echo "direnv: Virtual environment at $ACTUAL_VENV_PATH not found or is incomplete." fi } qlustered-deepdiff-41c7265/.envrc.example000066400000000000000000000000171516241264500203170ustar00rootroot00000000000000load_venv deep qlustered-deepdiff-41c7265/.github/000077500000000000000000000000001516241264500171115ustar00rootroot00000000000000qlustered-deepdiff-41c7265/.github/FUNDING.yml000066400000000000000000000000431516241264500207230ustar00rootroot00000000000000github: [seperman] ko_fi: seperman qlustered-deepdiff-41c7265/.github/ISSUE_TEMPLATE/000077500000000000000000000000001516241264500212745ustar00rootroot00000000000000qlustered-deepdiff-41c7265/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000013421516241264500237660ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- Please checkout the [F.A.Q](https://zepworks.com/deepdiff/current/faq.html) page before creating a bug ticket to make sure it is not already addressed. **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior **Expected behavior** A clear and concise description of what you expected to happen. **OS, DeepDiff version and Python version (please complete the following information):** - OS: [e.g. Ubuntu] - Version [e.g. 20LTS] - Python Version [e.g. 3.10.12] - DeepDiff Version [e.g. 5.8.0] **Additional context** Add any other context about the problem here. qlustered-deepdiff-41c7265/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000011231516241264500250160ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. qlustered-deepdiff-41c7265/.github/workflows/000077500000000000000000000000001516241264500211465ustar00rootroot00000000000000qlustered-deepdiff-41c7265/.github/workflows/main.yaml000066400000000000000000000033001516241264500227520ustar00rootroot00000000000000name: CI on: push: { branches: [master, dev] } pull_request: { branches: [master, dev] } jobs: build: runs-on: ubuntu-latest env: DEFAULT_PYTHON: '3.14' strategy: matrix: python-version: ['3.10','3.11','3.12','3.13','3.14'] architecture: ['x64'] steps: - uses: actions/checkout@v3 - name: Setup Python uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} architecture: ${{ matrix.architecture }} cache: pip cache-dependency-path: pyproject.toml - name: Install nox run: pip install nox==2025.5.1 - name: Lint with flake8 if: ${{ matrix.python-version == '3.14' }} run: | nox -s flake8 -- deepdiff --count --select=E9,F63,F7,F82 --show-source --statistics nox -s flake8 -- deepdiff --count --exit-zero --max-complexity=26 --max-line-length=250 --statistics - name: Test with pytest (no coverage) if: ${{ matrix.python-version != '3.14' }} run: | nox -s pytest-${{ matrix.python-version }} -- --benchmark-disable tests/ - name: Test with pytest (+ coverage) if: ${{ matrix.python-version == '3.14' }} run: | nox -s pytest-${{ matrix.python-version }} -- \ --benchmark-disable \ --cov-report=xml \ --cov=deepdiff \ tests/ --runslow - name: Upload coverage if: ${{ matrix.python-version == '3.14' }} uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} file: coverage.xml env_vars: OS,PYTHON fail_ci_if_error: true qlustered-deepdiff-41c7265/.gitignore000066400000000000000000000015401516241264500175410ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] .pytest_cache/ # C extensions *.so # Distribution / packaging .Python env/ .venv build/ develop-eggs/ dist/ downloads/ eggs/ lib/ lib64/ parts/ sdist/ var/ no_upload/ *.egg-info/ .installed.cfg *.egg # 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/ .coverage .cache nosetests.xml coverage.xml # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/_build/ # PyBuilder target/ # OS-specific spam .DS_Store # Editor / IDE files *.swp .idea/ .~lock* .python-version* temp* # env file .env pyrightconfig.json # direnv file .envrc qlustered-deepdiff-41c7265/AGENTS.md000077700000000000000000000000001516241264500203262CLAUDE.mdustar00rootroot00000000000000qlustered-deepdiff-41c7265/AUTHORS.md000066400000000000000000000201601516241264500172170ustar00rootroot00000000000000# Authors Authors in order of the timeline of their contributions: - [Sep Dehpour (Seperman)](http://www.zepworks.com) - [Victor Hahn Castell](http://hahncastell.de) for the tree view and major contributions: - [nfvs](https://github.com/nfvs) for Travis-CI setup script. - [brbsix](https://github.com/brbsix) for initial Py3 porting. - [WangFenjin](https://github.com/WangFenjin) for unicode support. - [timoilya](https://github.com/timoilya) for comparing list of sets when ignoring order. - [Bernhard10](https://github.com/Bernhard10) for significant digits comparison. - [b-jazz](https://github.com/b-jazz) for PEP257 cleanup, Standardize on full names, fixing line endings. - [finnhughes](https://github.com/finnhughes) for fixing __slots__ - [moloney](https://github.com/moloney) for Unicode vs. Bytes default - [serv-inc](https://github.com/serv-inc) for adding help(deepdiff) - [movermeyer](https://github.com/movermeyer) for updating docs - [maxrothman](https://github.com/maxrothman) for search in inherited class attributes - [maxrothman](https://github.com/maxrothman) for search for types/objects - [MartyHub](https://github.com/MartyHub) for exclude regex paths - [sreecodeslayer](https://github.com/sreecodeslayer) for DeepSearch match_string - Brian Maissy [brianmaissy](https://github.com/) for weakref fix, enum tests - Bartosz Borowik [boba-2](https://github.com/boba-2) for Exclude types fix when ignoring order - Brian Maissy [brianmaissy](https://github.com/brianmaissy) for fixing classes which inherit from classes with slots didn't have all of their slots compared - Juan Soler [Soleronline](https://github.com/Soleronline) for adding ignore_type_number - [mthaddon](https://github.com/mthaddon) for adding timedelta diffing support - [Necrophagos](https://github.com/Necrophagos) for Hashing of the number 1 vs. True - [gaal-dev](https://github.com/gaal-dev) for adding exclude_obj_callback - Ivan Piskunov [van-ess0](https://github.com/van-ess0) for deprecation warning enhancement. - Michał Karaś [MKaras93](https://github.com/MKaras93) for the pretty view - Christian Kothe [chkothe](https://github.com/chkothe) for the basic support for diffing numpy arrays - [Timothy](https://github.com/timson) for truncate_datetime - [d0b3rm4n](https://github.com/d0b3rm4n) for bugfix to not apply format to non numbers. - [MyrikLD](https://github.com/MyrikLD) for Bug Fix NoneType in ignore type groups - Stian Jensen [stianjensen](https://github.com/stianjensen) for improving ignoring of NoneType in diff - Florian Klien [flowolf](https://github.com/flowolf) for adding math_epsilon - Tim Klein [timjklein36](https://github.com/timjklein36) for retaining the order of multiple dictionary items added via Delta. - Wilhelm Schürmann [wbsch](https://github.com/wbsch) for fixing the typo with yml files. - [lyz-code](https://github.com/lyz-code) for adding support for regular expressions in DeepSearch and strict_checking feature in DeepSearch. - [dtorres-sf](https://github.com/dtorres-sf) for adding the option for custom compare function - Tony Wang [Tony-Wang](https://github.com/Tony-Wang) for bugfix: verbose_level==0 should disable values_changes. - Sun Ao [eggachecat](https://github.com/eggachecat) for adding custom operators. - Sun Ao [eggachecat](https://github.com/eggachecat) for adding ignore_order_func. - [SlavaSkvortsov](https://github.com/SlavaSkvortsov) for fixing unprocessed key error. - Håvard Thom [havardthom](https://github.com/havardthom) for adding UUID support. - Dhanvantari Tilak [Dhanvantari](https://github.com/Dhanvantari) for Bug-Fix: `TypeError in _get_numbers_distance() when ignore_order = True`. - Yael Mintz [yaelmi3](https://github.com/yaelmi3) for detailed pretty print when verbose_level=2. - Mikhail Khviyuzov [mskhviyu](https://github.com/mskhviyu) for Exclude obj callback strict. - [dtorres-sf](https://github.com/dtorres-sf) for the fix for diffing using iterable_compare_func with nested objects. - [Enric Pou](https://github.com/epou) for bug fix of ValueError when using Decimal 0.x - [Uwe Fladrich](https://github.com/uwefladrich) for fixing bug when diff'ing non-sequence iterables - [Michal Ozery-Flato](https://github.com/michalozeryflato) for setting equal_nan=ignore_nan_inequality in the call for np.array_equal - [martin-kokos](https://github.com/martin-kokos) for using Pytest's tmp_path fixture instead of /tmp/ - Håvard Thom [havardthom](https://github.com/havardthom) for adding include_obj_callback and include_obj_callback_strict. - [Noam Gottlieb](https://github.com/noamgot) for fixing a corner case where numpy's `np.float32` nans are not ignored when using `ignore_nan_equality`. - [maggelus](https://github.com/maggelus) for the bugfix deephash for paths. - [maggelus](https://github.com/maggelus) for the bugfix deephash compiled regex. - [martin-kokos](https://github.com/martin-kokos) for fixing the tests dependent on toml. - [kor4ik](https://github.com/kor4ik) for the bugfix for `include_paths` for nested dictionaries. - [martin-kokos](https://github.com/martin-kokos) for using tomli and tomli-w for dealing with tomli files. - [Alex Sauer-Budge](https://github.com/amsb) for the bugfix for `datetime.date`. - [William Jamieson](https://github.com/WilliamJamieson) for [NumPy 2.0 compatibility](https://github.com/seperman/deepdiff/pull/422) - [Leo Sin](https://github.com/leoslf) for Supporting Python 3.12 in the build process - [sf-tcalhoun](https://github.com/sf-tcalhoun) for fixing "Instantiating a Delta with a flat_dict_list unexpectedly mutates the flat_dict_list" - [dtorres-sf](https://github.com/dtorres-sf) for fixing iterable moved items when iterable_compare_func is used. - [Florian Finkernagel](https://github.com/TyberiusPrime) for pandas and polars support. - Mathis Chenuet [artemisart](https://github.com/artemisart) for fixing slots classes comparison and PR review. - Sherjeel Shabih [sherjeelshabih](https://github.com/sherjeelshabih) for fixing the issue where the key deep_distance is not returned when both compared items are equal #510 - [Aaron D. Marasco](https://github.com/AaronDMarasco) for adding `prefix` option to `pretty()` - [Juergen Skrotzky](https://github.com/Jorgen-VikingGod) for adding empty `py.typed` - [Mate Valko](https://github.com/vmatt) for fixing the issue so we lower only if clean_key is instance of str via #504 - [jlaba](https://github.com/jlaba) for fixing #493 include_paths, when only certain keys are included via #499 - [Doron Behar](https://github.com/doronbehar) for fixing DeepHash for numpy booleans via #496 - [Aaron D. Marasco](https://github.com/AaronDMarasco) for adding print() options which allows a user-defined string (or callback function) to prefix every output when using the pretty() call. - [David Hotham](https://github.com/dimbleby) for relaxing orderly-set dependency via #486 - [dtorres-sf](https://github.com/dtorres-sf) for the fix for moving nested tables when using iterable_compare_func. - [Jim Cipar](https://github.com/jcipar) for the fix recursion depth limit when hashing numpy.datetime64 - [Enji Cooper](https://github.com/ngie-eign) for converting legacy setuptools use to pyproject.toml - [Diogo Correia](https://github.com/diogotcorreia) for reporting security vulnerability in Delta and DeepDiff that could allow remote code execution. - [am-periphery](https://github.com/am-periphery) for reporting CVE-2026-33155: denial-of-service via crafted pickle payloads triggering massive memory allocation. - [echan5](https://github.com/echan5) for adding callable `group_by` support. - [yannrouillard](https://github.com/yannrouillard) for fixing colored view display when all list items are removed. - [tpvasconcelos](https://github.com/tpvasconcelos) for fixing `__slots__` handling for objects with `__getattr__`. - [devin13cox](https://github.com/devin13cox) for always using t1 path for reporting. - [vitalis89](https://github.com/vitalis89) for fixing `ignore_keys` issue in `detailed__dict__`. - [ljames8](https://github.com/ljames8) for fixing logarithmic similarity type hint. - [srini047](https://github.com/srini047) for fixing README typo. - [Nagato-Yuzuru](https://github.com/Nagato-Yuzuru) for colored view tests. - [akshat62](https://github.com/akshat62) for adding Fraction numeric support. qlustered-deepdiff-41c7265/CHANGELOG.md000066400000000000000000000422651516241264500173730ustar00rootroot00000000000000# DeepDiff Change log - v9-0-0 - migration note: - `to_dict()` and `to_json()` now accept a `verbose_level` parameter and always return a usable text-view dict. When the original view is `'tree'`, they default to `verbose_level=2` for full detail. The old `view_override` parameter is removed. To get the previous results, you will need to pass the explicit verbose_level to `to_json` and `to_dict` if you are using the tree view. - Dropping support for Python 3.9 - Support for python 3.14 - Added support for callable `group_by` thanks to [echan5](https://github.com/echan5) - Added `FlatDeltaDict` TypedDict for `to_flat_dicts` return type - Fixed colored view display when all list items are removed thanks to [yannrouillard](https://github.com/yannrouillard) - Fixed `hasattr()` swallowing `AttributeError` in `__slots__` handling for objects with `__getattr__` thanks to [tpvasconcelos](https://github.com/tpvasconcelos) - Fixed `ignore_order=True` missing int-vs-float type changes - Always use t1 path for reporting thanks to [devin13cox](https://github.com/devin13cox) - Fixed `_convert_oversized_ints` failing on NamedTuples - Fixed orjson `TypeError` for integers exceeding 64-bit range - Fixed parameter bug in `to_flat_dicts` where `include_action_in_path` and `report_type_changes` were not being passed through - Fixed `ignore_keys` issue in `detailed__dict__` thanks to [vitalis89](https://github.com/vitalis89) - Fixed logarithmic similarity type hint thanks to [ljames8](https://github.com/ljames8) - Added `Fraction` numeric support thanks to [akshat62](https://github.com/akshat62) - v8-6-2 - Security fix (CVE-2026-33155): Prevent denial-of-service via crafted pickle payloads that trigger massive memory allocation through the REDUCE opcode. Size-sensitive callables like `bytes()` and `bytearray()` are now wrapped to reject allocations exceeding 128 MB. - v8-6-1 - Patched security vulnerability in the Delta class which was vulnerable to class pollution via its constructor, and when combined with a gadget available in DeltaDiff itself, it could lead to Denial of Service and Remote Code Execution (via insecure Pickle deserialization). - v8-6-0 - Added Colored View thanks to @mauvilsa - Added support for applying deltas to NamedTuple thanks to @paulsc - Fixed test_delta.py with Python 3.14 thanks to @Romain-Geissler-1A - Added python property serialization to json - Added ip address serialization - Switched to UV from pip - Added Claude.md - Added uuid hashing thanks to @akshat62 - Added `ignore_uuid_types` flag to DeepDiff to avoid type reports when comparing UUID and string. - Added comprehensive type hints across the codebase (multiple commits for better type safety) - Added support for memoryview serialization - Added support for bytes serialization (non-UTF8 compatible) - Fixed bug where group_by with numbers would leak type info into group path reports - Fixed bug in `_get_clean_to_keys_mapping` without explicit significant digits - Added support for python dict key serialization - Enhanced support for IP address serialization with safe module imports - Added development tooling improvements (pyright config, .envrc example) - Updated documentation and development instructions - v8-5-0 - Updating deprecated pydantic calls - Switching to pyproject.toml - Fix for moving nested tables when using iterable_compare_func. - Fix recursion depth limit when hashing numpy.datetime64 - Moving from legacy setuptools use to pyproject.toml - v8-4-2 - fixes the type hints for the base - fixes summarize so if json dumps fails, we can still get a repr of the results - adds ipaddress support - v8-4-1 - Adding BaseOperatorPlus base class for custom operators - default_timezone can be passed now to set your default timezone to something other than UTC. - New summarization algorithm that produces valid json - Better type hint support - Breaking change in DeepHash where we raise Exception instead of logging if we can't hash a value. - Added the log_stacktrace parameter to DeepDiff. When True, it will log the stacktrace along with the error. - v8-3-0 - Fixed some static typing issues - Added the summarize module for better repr of nested values - v8-2-0 - Small optimizations so we don't load functions that are not needed - Updated the minimum version of Orderly-set - Normalize all datetimes into UTC. Assume timezone naive datetimes are UTC. - v8-1-0 - Removing deprecated lines from setup.py - Added `prefix` option to `pretty()` - Fixes hashing of numpy boolean values. - Fixes __slots__ comparison when the attribute doesn't exist. - Relaxing orderly-set reqs - Added Python 3.13 support - Only lower if clean_key is instance of str #504 - Fixes issue where the key deep_distance is not returned when both compared items are equal #510 - Fixes exclude_paths fails to work in certain cases - exclude_paths fails to work #509 - Fixes to_json() method chokes on standard json.dumps() kwargs such as sort_keys - to_dict() method chokes on standard json.dumps() kwargs #490 - Fixes accessing the affected_root_keys property on the diff object returned by DeepDiff fails when one of the dicts is empty - Fixes accessing the affected_root_keys property on the diff object returned by DeepDiff fails when one of the dicts is empty #508 - v8-0-1 - Bugfix. Numpy should be optional. - v8-0-0 - With the introduction of `threshold_to_diff_deeper`, the values returned are different than in previous versions of DeepDiff. You can still get the older values by setting `threshold_to_diff_deeper=0`. However to signify that enough has changed in this release that the users need to update the parameters passed to DeepDiff, we will be doing a major version update. - [x] `use_enum_value=True` makes it so when diffing enum, we use the enum's value. It makes it so comparing an enum to a string or any other value is not reported as a type change. - [x] `threshold_to_diff_deeper=float` is a number between 0 and 1. When comparing dictionaries that have a small intersection of keys, we will report the dictionary as a `new_value` instead of reporting individual keys changed. If you set it to zero, you get the same results as DeepDiff 7.0.1 and earlier, which means this feature is disabled. The new default is 0.33 which means if less that one third of keys between dictionaries intersect, report it as a new object. - [x] Deprecated `ordered-set` and switched to `orderly-set`. The `ordered-set` package was not being maintained anymore and starting Python 3.6, there were better options for sets that ordered. I forked one of the new implementations, modified it, and published it as `orderly-set`. - [x] Added `use_log_scale:bool` and `log_scale_similarity_threshold:float`. They can be used to ignore small changes in numbers by comparing their differences in logarithmic space. This is different than ignoring the difference based on significant digits. - [x] json serialization of reversed lists. - [x] Fix for iterable moved items when `iterable_compare_func` is used. - [x] Pandas and Polars support - v7-0-1 - Fixes the translation between Difflib opcodes and Delta flat rows. - v7-0-0 - When verbose=2, return `new_path` when the `path` and `new_path` are different (for example when ignore_order=True and the index of items have changed). - Dropping support for Python 3.7 - Introducing serialize to flat rows for delta objects. - fixes the issue with hashing `datetime.date` objects where it treated them as numbers instead of dates (fixes #445). - upgrading orjson to the latest version - Fix for bug when diffing two lists with ignore_order and providing compare_func - Fixes "Wrong diff on list of strings" #438 - Supporting Python 3.12 in the build process by [Leo Sin](https://github.com/leoslf) - Fixes "Instantiating a Delta with a flat_dict_list unexpectedly mutates the flat_dict_list" #457 by [sf-tcalhoun](https://github.com/sf-tcalhoun) - Fixes "Error on Delta With None Key and Removed Item from List" #441 - Fixes "Error when comparing two nested dicts with 2 added fields" #450 - Fixes "Error when subtracting Delta from a dictionary" #443 - v6-7-1 - Support for subtracting delta objects when iterable_compare_func is used. - Better handling of force adding a delta to an object. - Fix for [`Can't compare dicts with both single and double quotes in keys`](https://github.com/seperman/deepdiff/issues/430) - Updated docs for Inconsistent Behavior with math_epsilon and ignore_order = True - v6-7-0 - Delta can be subtracted from other objects now. - verify_symmetry is deprecated. Use bidirectional instead. - always_include_values flag in Delta can be enabled to include values in the delta for every change. - Fix for Delta.__add__ breaks with esoteric dict keys. - You can load a delta from the list of flat dictionaries. - v6-6-1 - Fix for [DeepDiff raises decimal exception when using significant digits](https://github.com/seperman/deepdiff/issues/426) - Introducing group_by_sort_key - Adding group_by 2D. For example `group_by=['last_name', 'zip_code']` - v6-6-0 - Numpy 2.0 support - Adding [Delta.to_flat_dicts](https://zepworks.com/deepdiff/current/serialization.html#delta-serialize-to-flat-dictionaries) - v6-5-0 - Adding [`parse_path`](https://github.com/seperman/deepdiff/pull/419) - v6-4-1 - Bugfix: Keep Numpy Optional - v6-4-0 - [Add Ignore List Order Option to DeepHash](https://github.com/seperman/deepdiff/pull/403) by [Bobby Morck](https://github.com/bmorck) - [pyyaml to 6.0.1 to fix cython build problems](https://github.com/seperman/deepdiff/pull/406) by [Robert Bo Davis](https://github.com/robert-bo-davis) - [Precompiled regex simple diff](https://github.com/seperman/deepdiff/pull/413) by [cohml](https://github.com/cohml) - New flag: `zip_ordered_iterables` for forcing iterable items to be compared one by one. - v6-3-1 - Bugfix deephash for paths by [maggelus](https://github.com/maggelus) - Bugfix deephash compiled regex [maggelus](https://github.com/maggelus) - Fix tests dependent on toml by [martin-kokos](https://github.com/martin-kokos) - Bugfix for `include_paths` for nested dictionaries by [kor4ik](https://github.com/kor4ik) - Use tomli and tomli-w for dealing with tomli files by [martin-kokos](https://github.com/martin-kokos) - Bugfix for `datetime.date` by [Alex Sauer-Budge](https://github.com/amsb) - v6-3-0 - `PrefixOrSuffixOperator`: This operator will skip strings that are suffix or prefix of each other. - `include_obj_callback` and `include_obj_callback_strict` are added by [Håvard Thom](https://github.com/havardthom). - Fixed a corner case where numpy's `np.float32` nans are not ignored when using `ignore_nan_equality` by [Noam Gottlieb](https://github.com/noamgot) - `orjson` becomes optional again. - Fix for `ignore_type_in_groups` with numeric values so it does not report number changes when the number types are different. - v6-2-3 - Switching to Orjson for serialization to improve the performance. - Setting `equal_nan=ignore_nan_inequality` in the call for `np.array_equal` - Using Pytest's tmp_path fixture instead of `/tmp/` - v6-2-2 - Enum test fix for python 3.11 - Adding support for dateutils rrules - v6-2-1 - Removed the print statements. - v6-2-0 - Major improvement in the diff report for lists when items are all hashable and the order of items is important. - v6-1-0 - DeepDiff.affected_paths can be used to get the list of all paths where a change, addition, or deletion was reported for. - DeepDiff.affected_root_keys can be used to get the list of all paths where a change, addition, or deletion was reported for. - Bugfix: ValueError when using Decimal 0.x #339 by [Enric Pou](https://github.com/epou) - Serialization of UUID - v6-0-0 - [Exclude obj callback strict](https://github.com/seperman/deepdiff/pull/320/files) parameter is added to DeepDiff by Mikhail Khviyuzov [mskhviyu](https://github.com/mskhviyu). - A fix for diffing using `iterable_compare_func` with nested objects by [dtorres-sf](https://github.com/dtorres-sf) who originally contributed this feature. - v5-7-0: - https://github.com/seperman/deepdiff/pull/284 Bug-Fix: TypeError in _get_numbers_distance() when ignore_order = True by @Dhanvantari - https://github.com/seperman/deepdiff/pull/280 Add support for UUIDs by @havardthom - Major bug in delta when it comes to iterable items added or removed is investigated by @uwefladrich and resolved by @seperman - v5-6-0: Adding custom operators, and ignore_order_func. Bugfix: verbose_level==0 should disable values_changes. Bugfix: unprocessed key error. - v5-5-0: adding iterable_compare_func for DeepDiff, adding output_format of list for path() in tree view. - v5-4-0: adding strict_checking for numbers in DeepSearch. - v5-3-0: add support for regular expressions in DeepSearch. - v5-2-3: Retaining the order of multiple dictionary items added via Delta. Fixed the typo with yml files in deep cli. Fixing Grep RecursionError where using non UTF-8 character. Allowing kwargs to be passed to to_json method. - v5-2-2: Fixed Delta serialization when None type is present. - v5-2-0: Removed Murmur3 as the preferred hashing method. Using SHA256 by default now. Added commandline for deepdiff. Added group_by. Added math_epsilon. Improved ignoring of NoneType. - v5-0-2: Bug Fix NoneType in ignore type groups https://github.com/seperman/deepdiff/issues/207 - v5-0-1: Bug fix to not apply format to non numbers. - v5-0-0: Introducing the Delta object, Improving Numpy support, Fixing tuples comparison when ignore_order=True, Dramatically improving the results when ignore_order=True by running in passes, Introducing pretty print view, deep_distance, purge, progress logging, cache and truncate_datetime. - v4-3-3: Adds support for datetime.time - v4-3-2: Deprecation Warning Enhancement - v4-3-1: Fixing the issue with exclude_path and hash calculations when dictionaries were inside iterables. https://github.com/seperman/deepdiff/issues/174 - v4-3-0: adding exclude_obj_callback - v4-2-0: .json property is finally removed. Fix for Py3.10. Dropping support for EOL Python 3.4. Ignoring private keys when calculating hashes. For example __init__ is not a part of hash calculation anymore. Fix for #166 Problem with comparing lists, with an boolean as element. - v4-0-9: Fixing the bug for hashing custom unhashable objects - v4-0-8: Adding ignore_nan_inequality for float('nan') - v4-0-7: Hashing of the number 1 vs. True - v4-0-6: found a tiny bug in Python formatting of numbers in scientific notation. Added a workaround. - v4-0-5: Fixing number diffing. Adding number_format_notation and number_to_string_func. - v4-0-4: Adding ignore_string_case and ignore_type_subclasses - v4-0-3: Adding versionbump tool for release - v4-0-2: Fixing installation issue where rst files are missing. - v4-0-1: Fixing installation Tarball missing requirements.txt . DeepDiff v4+ should not show up as pip installable for Py2. Making Murmur3 installation optional. - v4-0-0: Ending Python 2 support, Adding more functionalities and documentation for DeepHash. Switching to Pytest for testing. Switching to Murmur3 128bit for hashing. Fixing classes which inherit from classes with slots didn't have all of their slots compared. Renaming ContentHash to DeepHash. Adding exclude by path and regex path to DeepHash. Adding ignore_type_in_groups. Adding match_string to DeepSearch. Adding Timedelta object diffing. - v3-5-0: Exclude regex path - v3-3-0: Searching for objects and class attributes - v3-2-2: Adding help(deepdiff) - v3-2-1: Fixing hash of None - v3-2-0: Adding grep for search: object | grep(item) - v3-1-3: Unicode vs. Bytes default fix - v3-1-2: NotPresent Fix when item is added or removed. - v3-1-1: Bug fix when item value is None (#58) - v3-1-0: Serialization to/from json - v3-0-0: Introducing Tree View - v2-5-3: Bug fix on logging for content hash. - v2-5-2: Bug fixes on content hash. - v2-5-0: Adding ContentHash module to fix ignore_order once and for all. - v2-1-0: Adding Deep Search. Now you can search for item in an object. - v2-0-0: Exclusion patterns better coverage. Updating docs. - v1-8-0: Exclusion patterns. - v1-7-0: Deep Set comparison. - v1-6-0: Unifying key names. i.e newvalue is new_value now. For backward compatibility, newvalue still works. - v1-5-0: Fixing ignore order containers with unordered items. Adding significant digits when comparing decimals. Changes property is deprecated. - v1-1-0: Changing Set, Dictionary and Object Attribute Add/Removal to be reported as Set instead of List. Adding Pypy compatibility. - v1-0-2: Checking for ImmutableMapping type instead of dict - v1-0-1: Better ignore order support - v1-0-0: Restructuring output to make it more useful. This is NOT backward compatible. - v0-6-1: Fixing iterables with unhashable when order is ignored - v0-6-0: Adding unicode support - v0-5-9: Adding decimal support - v0-5-8: Adding ignore order of unhashables support - v0-5-7: Adding ignore order support - v0-5-6: Adding slots support - v0-5-5: Adding loop detection qlustered-deepdiff-41c7265/CITATION.cff000066400000000000000000000004331516241264500174430ustar00rootroot00000000000000cff-version: 1.2.0 message: "If you use this software, please cite it as below." authors: - family-names: "Dehpour" given-names: "Sep" orcid: "https://orcid.org/0009-0009-5828-4345" title: "DeepDiff" version: 9.0.0 date-released: 2026 url: "https://github.com/seperman/deepdiff" qlustered-deepdiff-41c7265/CLAUDE.md000066400000000000000000000072761516241264500170440ustar00rootroot00000000000000# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project Overview DeepDiff is a Python library for deep comparison, searching, and hashing of Python objects. It provides: - **DeepDiff**: Deep difference detection between objects - **DeepSearch**: Search for objects within other objects - **DeepHash**: Content-based hashing for any object - **Delta**: Git-like diff objects that can be applied to other objects - **CLI**: Command-line interface via `deep` command ## Development Commands ### Setup ```bash # Install with all development dependencies uv pip install -e ".[cli,coverage,dev,docs,static,test]" # OR using uv (recommended) uv sync --all-extras ``` **Virtual Environment**: Activate with `source ~/.venvs/deep/bin/activate` before running tests or Python commands ### Testing ```bash # Run tests with coverage source ~/.venvs/deep/bin/activate && pytest --cov=deepdiff --cov-report term-missing # Run tests including slow ones source ~/.venvs/deep/bin/activate && pytest --cov=deepdiff --runslow # Run single test file source ~/.venvs/deep/bin/activate && pytest tests/test_diff_text.py # Run tests across multiple Python versions. No need to use this unless getting ready for creating a new build source ~/.venvs/deep/bin/activate && nox -s pytest ``` ### **Type Checking with Pyright:** Always use this pattern for type checking: ```bash source ~/.venvs/deep/bin/activate && pyright {file_path} ``` Examples: - `source ~/.venvs/deep/bin/activate && pyright deepdiff/diff.py` - Type check specific file - `source ~/.venvs/deep/bin/activate && pyright deepdiff/` - Type check entire module - `source ~/.venvs/deep/bin/activate && pyright .` - Type check entire repo ### Common Pitfalls to Avoid 1. **Forgetting Virtual Environment**: ALWAYS activate venv before ANY Python command: ```bash source ~/.venvs/deep/bin/activate ``` 2. **Running pytest without venv**: This will cause import errors. Always use: ```bash source ~/.venvs/deep/bin/activate && pytest ``` 3. **Running module commands without venv**: Commands like `capi run`, `cettings shell`, etc. all require venv to be activated first 4. **Using `pip` instead of `uv pip`**: This project uses `uv` for package management. Always use `uv pip` instead of `pip`. ### Slow quality checks only to run before creating a build ```bash # Linting (max line length: 120) nox -s flake8 # Type checking nox -s mypy # Run all quality checks nox ``` ## Architecture ### Core Structure - **deepdiff/diff.py**: Main DeepDiff implementation (most complex component) - **deepdiff/deephash.py**: DeepHash functionality - **deepdiff/base.py**: Shared base classes and functionality - **deepdiff/model.py**: Core data structures and result objects - **deepdiff/helper.py**: Utility functions and type definitions - **deepdiff/delta.py**: Delta objects for applying changes ### Key Patterns - **Inheritance**: `Base` class provides common functionality with mixins - **Result Objects**: Different result formats (`ResultDict`, `TreeResult`, `TextResult`) - **Path Navigation**: Consistent path notation for nested object access - **Performance**: LRU caching and numpy array optimization ### Testing - Located in `/tests/` directory - Organized by functionality (e.g., `test_diff_text.py`, `test_hash.py`) - Aims for ~100% test coverage - Uses pytest with comprehensive fixtures ## Development Notes - **Python Support**: 3.10+ and PyPy3 - **Main Branch**: `master` (PRs typically go to `dev` branch) - **Build System**: Modern `pyproject.toml` with `flit_core` - **Dependencies**: Core dependency is `orderly-set>=5.4.1,<6` - **CLI Tool**: Available as `deep` command after installation with `[cli]` extra qlustered-deepdiff-41c7265/LICENSE000066400000000000000000000021661516241264500165630ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2014 - 2026 Sep Dehpour (Seperman) and contributors getqluster.com zepworks.com Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. qlustered-deepdiff-41c7265/MANIFEST.in000066400000000000000000000007121516241264500173070ustar00rootroot00000000000000include LICENSE include AUTHORS include CHANGELOG include *.rst include deepdiff/*.rst include *.txt include *.sh include pytest.ini include *.py exclude uv.lock recursive-include docs/ *.rst recursive-include docs/ *.png recursive-include tests *.csv recursive-include tests *.json recursive-include tests *.pickle recursive-include tests *.py recursive-include tests *.toml recursive-include tests *.yaml global-exclude __pycache__ global-exclude *.py[co] qlustered-deepdiff-41c7265/README.md000066400000000000000000000120071516241264500170300ustar00rootroot00000000000000# DeepDiff v 9.0.0 ![Downloads](https://img.shields.io/pypi/dm/deepdiff.svg?style=flat) ![Python Versions](https://img.shields.io/pypi/pyversions/deepdiff.svg?style=flat) ![License](https://img.shields.io/pypi/l/deepdiff.svg?version=latest) [![Build Status](https://github.com/seperman/deepdiff/workflows/Unit%20Tests/badge.svg)](https://github.com/seperman/deepdiff/actions) [![codecov](https://codecov.io/gh/seperman/deepdiff/branch/master/graph/badge.svg?token=KkHZ3siA3m)](https://codecov.io/gh/seperman/deepdiff) **DeepDiff is now part of [Qluster](/qluster).** *If you're building workflows around data validation and correction, [Qluster](/qluster) gives your team a structured way to manage rules, review failures, approve fixes, and reuse decisions—without building the entire system from scratch.* ## Modules - [DeepDiff](https://zepworks.com/deepdiff/current/diff.html): Deep Difference of dictionaries, iterables, strings, and ANY other object. - [DeepSearch](https://zepworks.com/deepdiff/current/dsearch.html): Search for objects within other objects. - [DeepHash](https://zepworks.com/deepdiff/current/deephash.html): Hash any object based on their content. - [Delta](https://zepworks.com/deepdiff/current/delta.html): Store the difference of objects and apply them to other objects. - [Extract](https://zepworks.com/deepdiff/current/extract.html): Extract an item from a nested Python object using its path. - [commandline](https://zepworks.com/deepdiff/current/commandline.html): Use DeepDiff from commandline. Tested on Python 3.10+ and PyPy3. - **[Documentation](https://zepworks.com/deepdiff/9.0.0/)** ## What is new? Please check the [ChangeLog](CHANGELOG.md) file for the detailed information. DeepDiff 9-0-0 - migration note: - `to_dict()` and `to_json()` now accept a `verbose_level` parameter and always return a usable text-view dict. When the original view is `'tree'`, they default to `verbose_level=2` for full detail. The old `view_override` parameter is removed. To get the previous results, you will need to pass the explicit verbose_level to `to_json` and `to_dict` if you are using the tree view. - Dropping support for Python 3.9 - Support for python 3.14 - Added support for callable `group_by` thanks to @echan5 - Added `FlatDeltaDict` TypedDict for `to_flat_dicts` return type - Fixed colored view display when all list items are removed thanks to @yannrouillard - Fixed `hasattr()` swallowing `AttributeError` in `__slots__` handling for objects with `__getattr__` thanks to @tpvasconcelos - Fixed `ignore_order=True` missing int-vs-float type changes - Always use t1 path for reporting thanks to @devin13cox - Fixed `_convert_oversized_ints` failing on NamedTuples - Fixed orjson `TypeError` for integers exceeding 64-bit range - Fixed parameter bug in `to_flat_dicts` where `include_action_in_path` and `report_type_changes` were not being passed through - Fixed `ignore_keys` issue in `detailed__dict__` thanks to @vitalis89 - Fixed logarithmic similarity type hint thanks to @ljames8 - Added `Fraction` numeric support thanks to @akshat62 ## Installation ### Install from PyPi: `pip install deepdiff` If you want to use DeepDiff from commandline: `pip install "deepdiff[cli]"` If you want to improve the performance of DeepDiff with certain functionalities such as improved json serialization: `pip install "deepdiff[optimize]"` Install optional packages: - [yaml](https://pypi.org/project/PyYAML/) - [tomli](https://pypi.org/project/tomli/) (python 3.10 and older) and [tomli-w](https://pypi.org/project/tomli-w/) for writing - [clevercsv](https://pypi.org/project/clevercsv/) for more robust CSV parsing - [orjson](https://pypi.org/project/orjson/) for speed and memory optimized parsing - [pydantic](https://pypi.org/project/pydantic/) # Documentation # ChangeLog Please take a look at the [CHANGELOG](CHANGELOG.md) file. # Survey :mega: **Please fill out our [fast 5-question survey](https://forms.gle/E6qXexcgjoKnSzjB8)** so that we can learn how & why you use DeepDiff, and what improvements we should make. Thank you! :dancers: # Local dev 1. Clone the repo 2. Switch to the dev branch 3. Create your own branch 4. Install dependencies - Method 1: Use [`uv`](https://github.com/astral-sh/uv) to install the dependencies: `uv sync --all-extras`. - Method 2: Use pip: `pip install -e ".[cli,coverage,dev,docs,static,test]"` 5. Build `uv build` # Contribute 1. Please make your PR against the dev branch 2. Please make sure that your PR has tests. Since DeepDiff is used in many sensitive data driven projects, we strive to maintain around 100% test coverage on the code. Please run `pytest --cov=deepdiff --runslow` to see the coverage report. Note that the `--runslow` flag will run some slow tests too. In most cases you only want to run the fast tests which so you won't add the `--runslow` flag. Or to see a more user friendly version, please run: `pytest --cov=deepdiff --cov-report term-missing --runslow`. Thank you! # Authors Please take a look at the [AUTHORS](AUTHORS.md) file. qlustered-deepdiff-41c7265/conftest.py000066400000000000000000000072241516241264500177550ustar00rootroot00000000000000import sys import os import json import pytest sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), 'tests'))) FIXTURES_DIR = os.path.join(os.path.dirname(__file__), 'tests/fixtures/') def pytest_addoption(parser): parser.addoption( "--runslow", action="store_true", default=False, help="run slow tests" ) def pytest_configure(config): config.addinivalue_line("markers", "slow: mark test as slow to run") def pytest_collection_modifyitems(config, items): if config.getoption("--runslow"): # --runslow given in cli: do not skip slow tests return skip_slow = pytest.mark.skip(reason="need --runslow option to run") for item in items: if "slow" in item.keywords: item.add_marker(skip_slow) @pytest.fixture(scope='class') def nested_a_t1(): with open(os.path.join(FIXTURES_DIR, 'nested_a_t1.json')) as the_file: return json.load(the_file) @pytest.fixture(scope='class') def nested_a_t2(): with open(os.path.join(FIXTURES_DIR, 'nested_a_t2.json')) as the_file: return json.load(the_file) @pytest.fixture(scope='class') def nested_a_result(): with open(os.path.join(FIXTURES_DIR, 'nested_a_result.json')) as the_file: return json.load(the_file) @pytest.fixture(scope='function') def compounds(): with open(os.path.join(FIXTURES_DIR, 'compounds.json')) as the_file: return json.load(the_file) @pytest.fixture(scope='class') def nested_a_affected_paths(): return { 'root[0][0][2][0][1]', 'root[0][1][1][1][5]', 'root[0][2][1]', 'root[1][1][2][0][1]', 'root[1][2][0]', 'root[1][2][0][1][5]', 'root[1][0][2][2][3]', 'root[0][0][1][0][0]', 'root[0][1][0][2][3]', 'root[0][3][0][2][3]', 'root[0][3][1][0][2]', 'root[1][1][1][0][0]', 'root[1][0][1][2][1]', 'root[1][0][2][1][2]', 'root[1][3][0][2][3]', 'root[1][3][1][0][2]', 'root[1][2][0][2]', 'root[1][0][2][0][1]', 'root[0][3][2][0][1]', 'root[0][3][2][1][0]', 'root[1][3][1][1]', 'root[1][2][1][1][0]', 'root[1][2][1][0]', 'root[1][0][0][0][2]', 'root[1][3][2][1][0]', 'root[1][0][0][1][1]', 'root[0][1][2][0]', 'root[0][1][2][1][0]', 'root[0][2][0][1][2]', 'root[1][3][0][1]', 'root[0][3][1][1]', 'root[1][2][0][0][2]', 'root[1][3][2][0][1]', 'root[1][0][1][0]', 'root[1][2][0][0][0]', 'root[1][0][0][0][1]', 'root[1][3][2][2][2]', 'root[0][1][1][2][1]', 'root[0][1][1][2][2]', 'root[0][2][0][0][2]', 'root[0][2][0][0][3]', 'root[0][3][1][2][1]', 'root[0][3][1][2][2]', 'root[1][2][1][2][3]', 'root[1][0][0][1][2]', 'root[1][0][0][2][1]', 'root[1][3][1][2][1]', 'root[1][3][1][2][2]' } @pytest.fixture(scope='class') def nested_b_t1(): with open(os.path.join(FIXTURES_DIR, 'nested_b_t1.json')) as the_file: return json.load(the_file) @pytest.fixture(scope='class') def nested_b_t2(): with open(os.path.join(FIXTURES_DIR, 'nested_b_t2.json')) as the_file: return json.load(the_file) @pytest.fixture(scope='class') def nested_b_result(): with open(os.path.join(FIXTURES_DIR, 'nested_b_result.json')) as the_file: return json.load(the_file) @pytest.fixture(scope='class') def compare_func_t1(): with open(os.path.join(FIXTURES_DIR, 'compare_func_t1.json')) as the_file: return json.load(the_file) @pytest.fixture(scope='class') def compare_func_t2(): with open(os.path.join(FIXTURES_DIR, 'compare_func_t2.json')) as the_file: return json.load(the_file) @pytest.fixture(scope='class') def compare_func_result1(): with open(os.path.join(FIXTURES_DIR, 'compare_func_result1.json')) as the_file: return json.load(the_file) qlustered-deepdiff-41c7265/deepdiff/000077500000000000000000000000001516241264500173175ustar00rootroot00000000000000qlustered-deepdiff-41c7265/deepdiff/__init__.py000066400000000000000000000007361516241264500214360ustar00rootroot00000000000000"""This module offers the DeepDiff, DeepSearch, grep, Delta and DeepHash classes.""" # flake8: noqa __version__ = '9.0.0' import logging if __name__ == '__main__': logging.basicConfig(format='%(asctime)s %(levelname)8s %(message)s') from .diff import DeepDiff as DeepDiff from .search import DeepSearch as DeepSearch, grep as grep from .deephash import DeepHash as DeepHash from .delta import Delta as Delta from .path import extract as extract, parse_path as parse_path qlustered-deepdiff-41c7265/deepdiff/anyset.py000066400000000000000000000036731516241264500212050ustar00rootroot00000000000000from deepdiff.deephash import DeepHash from deepdiff.helper import dict_, SetOrdered class AnySet: """ Any object can be in this set whether hashable or not. Note that the current implementation has memory leak and keeps traces of objects in itself even after popping. However one the AnySet object is deleted, all those traces will be gone too. """ def __init__(self, items=None): self._set = SetOrdered() self._hashes = dict_() self._hash_to_objects = dict_() if items: for item in items: self.add(item) def add(self, item): try: self._set.add(item) except TypeError: hashes_obj = DeepHash(item, hashes=self._hashes) hash_ = hashes_obj[item] if hash_ not in self._hash_to_objects: self._hash_to_objects[hash_] = item def __contains__(self, item): try: result = item in self._set except TypeError: hashes_obj = DeepHash(item, hashes=self._hashes) hash_ = hashes_obj[item] result = hash_ in self._hash_to_objects return result def pop(self): if self._set: return self._set.pop() else: return self._hash_to_objects.pop(next(iter(self._hash_to_objects))) def __eq__(self, other): set_part, hashes_to_objs_part = other return (self._set == set_part and self._hash_to_objects == hashes_to_objs_part) __req__ = __eq__ def __repr__(self): return "< AnySet {}, {} >".format(self._set, self._hash_to_objects) __str__ = __repr__ def __len__(self): return len(self._set) + len(self._hash_to_objects) def __iter__(self): for item in self._set: yield item for item in self._hash_to_objects.values(): yield item def __bool__(self): return bool(self._set or self._hash_to_objects) qlustered-deepdiff-41c7265/deepdiff/base.py000066400000000000000000000052651516241264500206130ustar00rootroot00000000000000import uuid from typing import List, Optional, Union, Tuple, Any, Type from deepdiff.helper import strings, numbers, SetOrdered DEFAULT_SIGNIFICANT_DIGITS_WHEN_IGNORE_NUMERIC_TYPES = 12 TYPE_STABILIZATION_MSG = 'Unable to stabilize the Numpy array {} due to {}. Please set ignore_order=False.' class Base: numbers = numbers strings = strings def get_significant_digits(self, significant_digits: Optional[int], ignore_numeric_type_changes: bool) -> Optional[int]: if significant_digits is not None and significant_digits < 0: raise ValueError( "significant_digits must be None or a non-negative integer") if significant_digits is None: if ignore_numeric_type_changes: significant_digits = DEFAULT_SIGNIFICANT_DIGITS_WHEN_IGNORE_NUMERIC_TYPES return significant_digits def get_ignore_types_in_groups(self, ignore_type_in_groups: Optional[Union[List[Any], Tuple[Any, ...]]], ignore_string_type_changes: bool, ignore_numeric_type_changes: bool, ignore_type_subclasses: bool, ignore_uuid_types: bool = False) -> List[Union[SetOrdered, Tuple[Type[Any], ...]]]: if ignore_type_in_groups: if isinstance(ignore_type_in_groups[0], type): ignore_type_in_groups = [ignore_type_in_groups] else: ignore_type_in_groups = [] result = [] for item_group in ignore_type_in_groups: new_item_group = SetOrdered() for item in item_group: item = type(item) if item is None or not isinstance(item, type) else item new_item_group.add(item) result.append(new_item_group) ignore_type_in_groups = result if ignore_string_type_changes and self.strings not in ignore_type_in_groups: ignore_type_in_groups.append(SetOrdered(self.strings)) if ignore_numeric_type_changes and self.numbers not in ignore_type_in_groups: ignore_type_in_groups.append(SetOrdered(self.numbers)) if ignore_uuid_types: # Create a group containing both UUID and str types uuid_str_group = SetOrdered([uuid.UUID, str]) if uuid_str_group not in ignore_type_in_groups: ignore_type_in_groups.append(uuid_str_group) if not ignore_type_subclasses: # is_instance method needs tuples. When we look for subclasses, we need them to be tuples ignore_type_in_groups = list(map(tuple, ignore_type_in_groups)) return ignore_type_in_groups qlustered-deepdiff-41c7265/deepdiff/colored_view.py000066400000000000000000000135751516241264500223650ustar00rootroot00000000000000import json import os from ast import literal_eval from importlib.util import find_spec from typing import Any, Dict from deepdiff.model import TextResult, TreeResult if os.name == "nt" and find_spec("colorama"): import colorama colorama.init() # ANSI color codes RED = '\033[31m' GREEN = '\033[32m' RESET = '\033[0m' class ColoredView: """A view that shows JSON with color-coded differences.""" def __init__(self, t2: Any, tree_result: TreeResult, compact: bool = False): self.t2 = t2 self.tree = tree_result self.compact = compact self.diff_paths = self._collect_diff_paths() def _collect_diff_paths(self) -> Dict[str, str]: """Collect all paths that have differences and their types.""" text_result = TextResult(tree_results=self.tree, verbose_level=2) diff_paths = {} for diff_type, items in text_result.items(): if not items: continue try: iter(items) except TypeError: continue for path, item in items.items(): if diff_type in ("values_changed", "type_changes"): changed_path = item.get("new_path") or path diff_paths[changed_path] = ("changed", item["old_value"], item["new_value"]) elif diff_type in ("dictionary_item_added", "iterable_item_added", "set_item_added"): diff_paths[path] = ("added", None, item) elif diff_type in ("dictionary_item_removed", "iterable_item_removed", "set_item_removed"): diff_paths[path] = ("removed", item, None) return diff_paths def _format_value(self, value: Any) -> str: """Format a value for display.""" if isinstance(value, bool): return 'true' if value else 'false' elif isinstance(value, str): return f'"{value}"' elif isinstance(value, (dict, list, tuple)): return json.dumps(value) else: return str(value) def _get_path_removed(self, path: str) -> dict: """Get all removed items for a given path.""" removed = {} for key, value in self.diff_paths.items(): if value[0] == 'removed' and key.startswith(path + "["): key_suffix = key[len(path):] if key_suffix.count("[") == 1 and key_suffix.endswith("]"): removed[literal_eval(key_suffix[1:-1])] = value[1] return removed def _has_differences(self, path_prefix: str) -> bool: """Check if a path prefix has any differences under it.""" return any(diff_path.startswith(path_prefix + "[") for diff_path in self.diff_paths) def _colorize_json(self, obj: Any, path: str = 'root', indent: int = 0) -> str: """Recursively colorize JSON based on differences, with pretty-printing.""" INDENT = ' ' current_indent = INDENT * indent next_indent = INDENT * (indent + 1) if path in self.diff_paths and path not in self._colorize_skip_paths: diff_type, old, new = self.diff_paths[path] if diff_type == 'changed': return f"{RED}{self._format_value(old)}{RESET} -> {GREEN}{self._format_value(new)}{RESET}" elif diff_type == 'added': return f"{GREEN}{self._format_value(new)}{RESET}" elif diff_type == 'removed': return f"{RED}{self._format_value(old)}{RESET}" if isinstance(obj, (dict, list)) and self.compact and not self._has_differences(path): return '{...}' if isinstance(obj, dict) else '[...]' if isinstance(obj, dict): if not obj: return '{}' items = [] for key, value in obj.items(): new_path = f"{path}['{key}']" if isinstance(key, str) else f"{path}[{key}]" if new_path in self.diff_paths and self.diff_paths[new_path][0] == 'added': # Colorize both key and value for added fields items.append(f'{next_indent}{GREEN}"{key}": {self._colorize_json(value, new_path, indent + 1)}{RESET}') else: items.append(f'{next_indent}"{key}": {self._colorize_json(value, new_path, indent + 1)}') for key, value in self._get_path_removed(path).items(): new_path = f"{path}['{key}']" if isinstance(key, str) else f"{path}[{key}]" items.append(f'{next_indent}{RED}"{key}": {self._colorize_json(value, new_path, indent + 1)}{RESET}') return '{\n' + ',\n'.join(items) + f'\n{current_indent}' + '}' elif isinstance(obj, (list, tuple)): removed_map = self._get_path_removed(path) if not obj and not removed_map: return '[]' for index in removed_map: self._colorize_skip_paths.add(f"{path}[{index}]") items = [] remove_index = 0 for index, value in enumerate(obj): while remove_index == next(iter(removed_map), None): items.append(f'{next_indent}{RED}{self._format_value(removed_map.pop(remove_index))}{RESET}') remove_index += 1 items.append(f'{next_indent}{self._colorize_json(value, f"{path}[{index}]", indent + 1)}') remove_index += 1 for value in removed_map.values(): items.append(f'{next_indent}{RED}{self._format_value(value)}{RESET}') return '[\n' + ',\n'.join(items) + f'\n{current_indent}' + ']' else: return self._format_value(obj) def __str__(self) -> str: """Return the colorized, pretty-printed JSON string.""" self._colorize_skip_paths = set() return self._colorize_json(self.t2) def __iter__(self): """Make the view iterable by yielding the tree results.""" yield from self.tree.items() qlustered-deepdiff-41c7265/deepdiff/commands.py000066400000000000000000000242231516241264500214750ustar00rootroot00000000000000import click import sys from decimal import Decimal from pprint import pprint from deepdiff.diff import ( DeepDiff, CUTOFF_DISTANCE_FOR_PAIRS_DEFAULT, CUTOFF_INTERSECTION_FOR_PAIRS_DEFAULT, logger ) from deepdiff import Delta, DeepSearch, extract as deep_extract from deepdiff.serialization import load_path_content, save_content_to_path try: import orjson except ImportError: orjson = None @click.group() def cli(): """A simple command line tool.""" pass # pragma: no cover. @cli.command() @click.argument('t1', type=click.Path(exists=True, resolve_path=True)) @click.argument('t2', type=click.Path(exists=True, resolve_path=True)) @click.option('--cutoff-distance-for-pairs', required=False, default=CUTOFF_DISTANCE_FOR_PAIRS_DEFAULT, type=float, show_default=True) @click.option('--cutoff-intersection-for-pairs', required=False, default=CUTOFF_INTERSECTION_FOR_PAIRS_DEFAULT, type=float, show_default=True) @click.option('--cache-size', required=False, default=0, type=int, show_default=True) @click.option('--cache-tuning-sample-size', required=False, default=0, type=int, show_default=True) @click.option('--cache-purge-level', required=False, default=1, type=click.IntRange(0, 2), show_default=True) @click.option('--create-patch', is_flag=True, show_default=True) @click.option('--exclude-paths', required=False, type=str, show_default=False, multiple=True) @click.option('--exclude-regex-paths', required=False, type=str, show_default=False, multiple=True) @click.option('--math-epsilon', required=False, type=Decimal, show_default=False) @click.option('--get-deep-distance', is_flag=True, show_default=True) @click.option('--group-by', required=False, type=str, show_default=False, multiple=False) @click.option('--ignore-order', is_flag=True, show_default=True) @click.option('--ignore-string-type-changes', is_flag=True, show_default=True) @click.option('--ignore-numeric-type-changes', is_flag=True, show_default=True) @click.option('--ignore-type-subclasses', is_flag=True, show_default=True) @click.option('--ignore-string-case', is_flag=True, show_default=True) @click.option('--ignore-nan-inequality', is_flag=True, show_default=True) @click.option('--include-private-variables', is_flag=True, show_default=True) @click.option('--log-frequency-in-sec', required=False, default=0, type=int, show_default=True) @click.option('--max-passes', required=False, default=10000000, type=int, show_default=True) @click.option('--max_diffs', required=False, default=None, type=int, show_default=True) @click.option('--threshold-to-diff-deeper', required=False, default=0.33, type=float, show_default=False) @click.option('--number-format-notation', required=False, type=click.Choice(['f', 'e'], case_sensitive=True), show_default=True, default="f") @click.option('--progress-logger', required=False, type=click.Choice(['info', 'error'], case_sensitive=True), show_default=True, default="info") @click.option('--report-repetition', is_flag=True, show_default=True) @click.option('--significant-digits', required=False, default=None, type=int, show_default=True) @click.option('--truncate-datetime', required=False, type=click.Choice(['second', 'minute', 'hour', 'day'], case_sensitive=True), show_default=True, default=None) @click.option('--verbose-level', required=False, default=1, type=click.IntRange(0, 2), show_default=True) @click.option('--view', required=False, type=click.Choice(['tree', 'colored', 'colored_compact'], case_sensitive=True), show_default=True, default='tree') @click.option('--debug', is_flag=True, show_default=False) def diff( *args, **kwargs ): """ Deep Diff Commandline Deep Difference of content in files. It can read csv, tsv, json, yaml, and toml files. T1 and T2 are the path to the files to be compared with each other. """ debug = kwargs.pop('debug') kwargs['ignore_private_variables'] = not kwargs.pop('include_private_variables') kwargs['progress_logger'] = logger.info if kwargs['progress_logger'] == 'info' else logger.error create_patch = kwargs.pop('create_patch') t1_path = kwargs.pop("t1") t2_path = kwargs.pop("t2") t1_extension = t1_path.split('.')[-1] t2_extension = t2_path.split('.')[-1] if "view" in kwargs and kwargs["view"] is None: kwargs.pop("view") for name, t_path, t_extension in [('t1', t1_path, t1_extension), ('t2', t2_path, t2_extension)]: try: kwargs[name] = load_path_content(t_path, file_type=t_extension) except Exception as e: # pragma: no cover. if debug: # pragma: no cover. raise # pragma: no cover. else: # pragma: no cover. sys.exit(str(f"Error when loading {name}: {e}")) # pragma: no cover. # if (t1_extension != t2_extension): if t1_extension in {'csv', 'tsv'}: kwargs['t1'] = [dict(i) for i in kwargs['t1']] if t2_extension in {'csv', 'tsv'}: kwargs['t2'] = [dict(i) for i in kwargs['t2']] if create_patch: # Disabling logging progress since it will leak into stdout kwargs['log_frequency_in_sec'] = 0 try: diff = DeepDiff(**kwargs) except Exception as e: # pragma: no cover. No need to test this. sys.exit(str(e)) # pragma: no cover. No need to test this. if create_patch: try: delta = Delta(diff) except Exception as e: # pragma: no cover. if debug: # pragma: no cover. raise # pragma: no cover. else: # pragma: no cover. sys.exit(f"Error when loading the patch (aka delta): {e}") # pragma: no cover. # printing into stdout sys.stdout.buffer.write(delta.dumps()) else: try: if kwargs["view"] in {'colored', 'colored_compact'}: print(diff) else: print(diff.to_json(indent=2)) except Exception: pprint(diff, indent=2) @cli.command() @click.argument('path', type=click.Path(exists=True, resolve_path=True)) @click.argument('delta_path', type=click.Path(exists=True, resolve_path=True)) @click.option('--backup', '-b', is_flag=True, show_default=True) @click.option('--raise-errors', is_flag=True, show_default=True) @click.option('--debug', is_flag=True, show_default=False) def patch( path, delta_path, backup, raise_errors, debug ): """ Deep Patch Commandline Patches a file based on the information in a delta file. The delta file can be created by the deep diff command and passing the --create-patch argument. Deep Patch is similar to Linux's patch command. The difference is that it is made for patching data. It can read csv, tsv, json, yaml, and toml files. """ try: delta = Delta(delta_path=delta_path, raise_errors=raise_errors) except Exception as e: # pragma: no cover. if debug: # pragma: no cover. raise # pragma: no cover. else: # pragma: no cover. sys.exit(str(f"Error when loading the patch (aka delta) {delta_path}: {e}")) # pragma: no cover. extension = path.split('.')[-1] try: content = load_path_content(path, file_type=extension) except Exception as e: # pragma: no cover. sys.exit(str(f"Error when loading {path}: {e}")) # pragma: no cover. result = delta + content try: save_content_to_path(result, path, file_type=extension, keep_backup=backup) except Exception as e: # pragma: no cover. if debug: # pragma: no cover. raise # pragma: no cover. else: # pragma: no cover. sys.exit(str(f"Error when saving {path}: {e}")) # pragma: no cover. @cli.command() @click.argument('item', required=True, type=str) @click.argument('path', type=click.Path(exists=True, resolve_path=True)) @click.option('--ignore-case', '-i', is_flag=True, show_default=True) @click.option('--exact-match', is_flag=True, show_default=True) @click.option('--exclude-paths', required=False, type=str, show_default=False, multiple=True) @click.option('--exclude-regex-paths', required=False, type=str, show_default=False, multiple=True) @click.option('--verbose-level', required=False, default=1, type=click.IntRange(0, 2), show_default=True) @click.option('--debug', is_flag=True, show_default=False) def grep(item, path, debug, **kwargs): """ Deep Grep Commandline Grep through the contents of a file and find the path to the item. It can read csv, tsv, json, yaml, and toml files. """ kwargs['case_sensitive'] = not kwargs.pop('ignore_case') kwargs['match_string'] = kwargs.pop('exact_match') try: content = load_path_content(path) except Exception as e: # pragma: no cover. if debug: # pragma: no cover. raise # pragma: no cover. else: # pragma: no cover. sys.exit(str(f"Error when loading {path}: {e}")) # pragma: no cover. try: result = DeepSearch(content, item, **kwargs) except Exception as e: # pragma: no cover. if debug: # pragma: no cover. raise # pragma: no cover. else: # pragma: no cover. sys.exit(str(f"Error when running deep search on {path}: {e}")) # pragma: no cover. pprint(result, indent=2) @cli.command() @click.argument('path_inside', required=True, type=str) @click.argument('path', type=click.Path(exists=True, resolve_path=True)) @click.option('--debug', is_flag=True, show_default=False) def extract(path_inside, path, debug): """ Deep Extract Commandline Extract an item from a file based on the path that is passed. It can read csv, tsv, json, yaml, and toml files. """ try: content = load_path_content(path) except Exception as e: # pragma: no cover. if debug: # pragma: no cover. raise # pragma: no cover. else: # pragma: no cover. sys.exit(str(f"Error when loading {path}: {e}")) # pragma: no cover. try: result = deep_extract(content, path_inside) except Exception as e: # pragma: no cover. if debug: # pragma: no cover. raise # pragma: no cover. else: # pragma: no cover. sys.exit(str(f"Error when running deep search on {path}: {e}")) # pragma: no cover. pprint(result, indent=2) qlustered-deepdiff-41c7265/deepdiff/deephash.py000066400000000000000000000746271516241264500214720ustar00rootroot00000000000000#!/usr/bin/env python import logging import datetime import uuid from typing import Union, Optional, Any, List, TYPE_CHECKING, Dict, Tuple, Set, Callable, Generator from collections.abc import Iterable, MutableMapping from collections import defaultdict from hashlib import sha1, sha256 from pathlib import Path from enum import Enum import re from deepdiff.helper import (strings, numbers, only_numbers, times, unprocessed, not_hashed, add_to_frozen_set, convert_item_or_items_into_set_else_none, get_doc, ipranges, convert_item_or_items_into_compiled_regexes_else_none, get_id, type_is_subclass_of_type_group, type_in_type_group, number_to_string, datetime_normalize, KEY_TO_VAL_STR, get_truncate_datetime, dict_, add_root_to_paths, PydanticBaseModel) from deepdiff.base import Base if TYPE_CHECKING: from pytz.tzinfo import BaseTzInfo import numpy as np # Type aliases for better readability HashableType = Union[str, int, float, bytes, bool, tuple, frozenset, type(None)] HashResult = Union[str, Any] # Can be string hash or unprocessed marker HashTuple = Tuple[HashResult, int] # (hash_result, count) HashesDict = Dict[Any, Union[HashTuple, List[Any]]] # Special case for UNPROCESSED_KEY PathType = Union[str, List[str], Set[str]] RegexType = Union[str, re.Pattern[str], List[Union[str, re.Pattern[str]]]] NumberToStringFunc = Callable[..., str] # More flexible for different number_to_string implementations try: import pandas except ImportError: pandas = False # type: ignore try: import polars except ImportError: polars = False # type: ignore try: import numpy as np booleanTypes: Tuple[type, ...] = (bool, np.bool_) # type: ignore except ImportError: booleanTypes = (bool,) # type: ignore logger: logging.Logger = logging.getLogger(__name__) UNPROCESSED_KEY: object = object() EMPTY_FROZENSET: frozenset = frozenset() INDEX_VS_ATTRIBUTE: Tuple[str, str] = ('[%s]', '.%s') HASH_LOOKUP_ERR_MSG: str = '{} is not one of the hashed items.' def sha256hex(obj: Union[str, bytes]) -> str: """Use Sha256 as a cryptographic hash.""" if isinstance(obj, str): obj = obj.encode('utf-8') return sha256(obj).hexdigest() def sha1hex(obj: Union[str, bytes]) -> str: """Use Sha1 as a cryptographic hash.""" if isinstance(obj, str): obj = obj.encode('utf-8') return sha1(obj).hexdigest() default_hasher: Callable[[Union[str, bytes]], str] = sha256hex def combine_hashes_lists(items: List[List[str]], prefix: Union[str, bytes]) -> str: """ Combines lists of hashes into one hash This can be optimized in future. It needs to work with both murmur3 hashes (int) and sha256 (str) Although murmur3 is not used anymore. """ if isinstance(prefix, bytes): prefix = prefix.decode('utf-8') hashes_bytes = b'' for item in items: # In order to make sure the order of hashes in each item does not affect the hash # we resort them. hashes_bytes += (''.join(map(str, sorted(item))) + '--').encode('utf-8') return prefix + str(default_hasher(hashes_bytes)) class BoolObj(Enum): TRUE = 1 FALSE = 0 def prepare_string_for_hashing( obj: Union[str, bytes, memoryview], ignore_string_type_changes: bool = False, ignore_string_case: bool = False, encodings: Optional[List[str]] = None, ignore_encoding_errors: bool = False, ) -> str: """ Clean type conversions """ original_type = obj.__class__.__name__ # https://docs.python.org/3/library/codecs.html#codecs.decode errors_mode = 'ignore' if ignore_encoding_errors else 'strict' if isinstance(obj, memoryview): obj = obj.tobytes() if isinstance(obj, bytes): err = None encodings = ['utf-8'] if encodings is None else encodings encoded = False for encoding in encodings: try: obj = obj.decode(encoding, errors=errors_mode) encoded = True break except UnicodeDecodeError as er: err = er if not encoded and err is not None: obj_decoded = obj.decode('utf-8', errors='ignore') # type: ignore start = max(err.start - 20, 0) start_prefix = '' if start > 0: start_prefix = '...' end = err.end + 20 end_suffix = '...' if end >= len(obj): end = len(obj) end_suffix = '' raise UnicodeDecodeError( err.encoding, err.object, err.start, err.end, f"{err.reason} in '{start_prefix}{obj_decoded[start:end]}{end_suffix}'. Please either pass ignore_encoding_errors=True or pass the encoding via encodings=['utf-8', '...']." ) from None if not ignore_string_type_changes: obj = KEY_TO_VAL_STR.format(original_type, obj) if ignore_string_case: obj = obj.lower() return str(obj) doc = get_doc('deephash_doc.rst') class DeepHash(Base): __doc__ = doc # Class attributes hashes: Dict[Any, Any] exclude_types_tuple: Tuple[type, ...] ignore_repetition: bool exclude_paths: Optional[Set[str]] include_paths: Optional[Set[str]] exclude_regex_paths: Optional[List[re.Pattern[str]]] hasher: Callable[[Union[str, bytes]], str] use_enum_value: bool default_timezone: Union[datetime.timezone, "BaseTzInfo"] significant_digits: Optional[int] truncate_datetime: Optional[str] number_format_notation: str ignore_type_in_groups: Any ignore_string_type_changes: bool ignore_numeric_type_changes: bool ignore_string_case: bool exclude_obj_callback: Optional[Callable[[Any, str], bool]] apply_hash: bool type_check_func: Callable[[type, Any], bool] number_to_string: Any ignore_private_variables: bool encodings: Optional[List[str]] ignore_encoding_errors: bool ignore_iterable_order: bool custom_operators: Optional[List[Any]] def __init__(self, obj: Any, *, apply_hash: bool = True, custom_operators: Optional[List[Any]] = None, default_timezone: Union[datetime.timezone, "BaseTzInfo"] = datetime.timezone.utc, encodings: Optional[List[str]] = None, exclude_obj_callback: Optional[Callable[[Any, str], bool]] = None, exclude_paths: Optional[PathType] = None, exclude_regex_paths: Optional[RegexType] = None, exclude_types: Optional[Union[List[type], Set[type], Tuple[type, ...]]] = None, hasher: Optional[Callable[[Union[str, bytes]], str]] = None, hashes: Optional[Union[Dict[Any, Any], "DeepHash"]] = None, ignore_encoding_errors: bool = False, ignore_iterable_order: bool = True, ignore_numeric_type_changes: bool = False, ignore_private_variables: bool = True, ignore_repetition: bool = True, ignore_string_case: bool = False, ignore_string_type_changes: bool = False, ignore_type_in_groups: Any = None, ignore_type_subclasses: bool = False, ignore_uuid_types: bool = False, include_paths: Optional[PathType] = None, number_format_notation: str = "f", number_to_string_func: Optional[NumberToStringFunc] = None, parent: str = "root", significant_digits: Optional[int] = None, truncate_datetime: Optional[str] = None, use_enum_value: bool = False, **kwargs) -> None: if kwargs: raise ValueError( ("The following parameter(s) are not valid: %s\n" "The valid parameters are obj, hashes, exclude_types, significant_digits, truncate_datetime," "exclude_paths, include_paths, exclude_regex_paths, hasher, ignore_repetition, " "number_format_notation, apply_hash, ignore_type_in_groups, ignore_string_type_changes, " "ignore_numeric_type_changes, ignore_type_subclasses, ignore_string_case, ignore_uuid_types, " "number_to_string_func, ignore_private_variables, parent, use_enum_value, default_timezone " "encodings, ignore_encoding_errors") % ', '.join(kwargs.keys())) if isinstance(hashes, MutableMapping): self.hashes = hashes elif isinstance(hashes, DeepHash): self.hashes = hashes.hashes else: self.hashes = dict_() exclude_types = set() if exclude_types is None else set(exclude_types) self.exclude_types_tuple = tuple(exclude_types) # we need tuple for checking isinstance self.ignore_repetition = ignore_repetition self.exclude_paths = add_root_to_paths(convert_item_or_items_into_set_else_none(exclude_paths)) self.include_paths = add_root_to_paths(convert_item_or_items_into_set_else_none(include_paths)) self.exclude_regex_paths = convert_item_or_items_into_compiled_regexes_else_none(exclude_regex_paths) self.hasher = default_hasher if hasher is None else hasher self.hashes[UNPROCESSED_KEY] = [] # type: ignore self.use_enum_value = use_enum_value self.default_timezone = default_timezone self.significant_digits = self.get_significant_digits(significant_digits, ignore_numeric_type_changes) self.truncate_datetime = get_truncate_datetime(truncate_datetime) self.number_format_notation = number_format_notation self.ignore_type_in_groups = self.get_ignore_types_in_groups( ignore_type_in_groups=ignore_type_in_groups, ignore_string_type_changes=ignore_string_type_changes, ignore_numeric_type_changes=ignore_numeric_type_changes, ignore_type_subclasses=ignore_type_subclasses, ignore_uuid_types=ignore_uuid_types, ) self.ignore_string_type_changes = ignore_string_type_changes self.ignore_numeric_type_changes = ignore_numeric_type_changes self.ignore_string_case = ignore_string_case self.exclude_obj_callback = exclude_obj_callback # makes the hash return constant size result if true # the only time it should be set to False is when # testing the individual hash functions for different types of objects. self.apply_hash = apply_hash self.type_check_func = type_in_type_group if ignore_type_subclasses else type_is_subclass_of_type_group # self.type_check_func = type_is_subclass_of_type_group if ignore_type_subclasses else type_in_type_group self.number_to_string = number_to_string_func or number_to_string self.ignore_private_variables = ignore_private_variables self.encodings = encodings self.ignore_encoding_errors = ignore_encoding_errors self.ignore_iterable_order = ignore_iterable_order self.custom_operators = custom_operators self._hash(obj, parent=parent, parents_ids=frozenset({get_id(obj)})) if self.hashes[UNPROCESSED_KEY]: logger.warning("Can not hash the following items: {}.".format(self.hashes[UNPROCESSED_KEY])) else: del self.hashes[UNPROCESSED_KEY] sha256hex: Callable[[Union[str, bytes]], str] = sha256hex sha1hex: Callable[[Union[str, bytes]], str] = sha1hex def __getitem__(self, obj: Any, extract_index: Optional[int] = 0) -> Any: return self._getitem(self.hashes, obj, extract_index=extract_index, use_enum_value=self.use_enum_value, ignore_numeric_type_changes=self.ignore_numeric_type_changes) @staticmethod def _get_slots_dict(obj: Any) -> Dict[str, Any]: """Get a dict of initialized slot attributes. Uses object.__getattribute__ to check each slot directly, bypassing __getattr__. For uninitialized slots on classes that define __getattr__, falls back to getattr — letting it raise if the object is truly broken. """ result = {} has_getattr = hasattr(type(obj), '__getattr__') for slot in obj.__slots__: try: result[slot] = object.__getattribute__(obj, slot) except AttributeError: if has_getattr: # The slot isn't initialized, but the class defines __getattr__. # Try the normal getattr to let __getattr__ provide a value or # raise — if it raises, we propagate to fail the strategy. result[slot] = getattr(obj, slot) return result @staticmethod def _getitem(hashes: Dict[Any, Any], obj: Any, extract_index: Optional[int] = 0, use_enum_value: bool = False, ignore_numeric_type_changes: bool = False) -> Any: """ extract_index is zero for hash and 1 for count and None to get them both. To keep it backward compatible, we only get the hash by default so it is set to zero by default. """ key = obj if obj is True: key = BoolObj.TRUE elif obj is False: key = BoolObj.FALSE elif use_enum_value and isinstance(obj, Enum): key = obj.value key = DeepHash._make_hash_key_for_lookup(key, ignore_numeric_type_changes=ignore_numeric_type_changes) result_n_count: Tuple[Any, int] = (None, 0) # type: ignore try: result_n_count = hashes[key] except (TypeError, KeyError): key = get_id(obj) try: result_n_count = hashes[key] except KeyError: raise KeyError(HASH_LOOKUP_ERR_MSG.format(obj)) from None if obj is UNPROCESSED_KEY: extract_index = None return result_n_count if extract_index is None else result_n_count[extract_index] def __contains__(self, obj: Any) -> bool: key = self._make_hash_key(obj) result = False try: result = key in self.hashes except (TypeError, KeyError): result = False if not result: result = get_id(obj) in self.hashes return result def get(self, key: Any, default: Any = None, extract_index: Optional[int] = 0) -> Any: """ Get method for the hashes dictionary. It can extract the hash for a given key that is already calculated when extract_index=0 or the count of items that went to building the object when extract_index=1. """ return self.get_key(self.hashes, key, default=default, extract_index=extract_index, ignore_numeric_type_changes=self.ignore_numeric_type_changes) @staticmethod def get_key(hashes: Dict[Any, Any], key: Any, default: Any = None, extract_index: Optional[int] = 0, use_enum_value: bool = False, ignore_numeric_type_changes: bool = False) -> Any: """ get_key method for the hashes dictionary. It can extract the hash for a given key that is already calculated when extract_index=0 or the count of items that went to building the object when extract_index=1. """ try: result = DeepHash._getitem(hashes, key, extract_index=extract_index, use_enum_value=use_enum_value, ignore_numeric_type_changes=ignore_numeric_type_changes) except KeyError: result = default return result @staticmethod def _unwrap_hash_key(key: Any) -> Any: """Unwrap a (type, value) hash key back to the original value for public API.""" if isinstance(key, tuple) and len(key) == 2 and isinstance(key[0], type) and isinstance(key[1], only_numbers): return key[1] return key def _get_objects_to_hashes_dict(self, extract_index: Optional[int] = 0) -> Dict[Any, Any]: """ A dictionary containing only the objects to hashes, or a dictionary of objects to the count of items that went to build them. extract_index=0 for hashes and extract_index=1 for counts. """ result = dict_() for key, value in self.hashes.items(): key = self._unwrap_hash_key(key) if key is UNPROCESSED_KEY: result[key] = value else: result[key] = value[extract_index] return result def __eq__(self, other: Any) -> bool: if isinstance(other, DeepHash): return self.hashes == other.hashes else: # We only care about the hashes return self._get_objects_to_hashes_dict() == other __req__ = __eq__ def __repr__(self) -> str: """ Hide the counts since it will be confusing to see them when they are hidden everywhere else. """ from deepdiff.summarize import summarize return summarize(self._get_objects_to_hashes_dict(extract_index=0), max_length=500) def __str__(self) -> str: return str(self._get_objects_to_hashes_dict(extract_index=0)) def __bool__(self) -> bool: return bool(self.hashes) def keys(self) -> Any: return [self._unwrap_hash_key(k) for k in self.hashes.keys()] def values(self) -> Generator[Any, None, None]: return (i[0] for i in self.hashes.values()) # Just grab the item and not its count def items(self) -> Generator[Tuple[Any, Any], None, None]: return ((self._unwrap_hash_key(i), v[0]) for i, v in self.hashes.items()) def _prep_obj(self, obj: Any, parent: str, parents_ids: frozenset = EMPTY_FROZENSET, is_namedtuple: bool = False, is_pydantic_object: bool = False) -> HashTuple: """prepping objects""" original_type = type(obj) if not isinstance(obj, type) else obj obj_to_dict_strategies = [] if is_namedtuple: obj_to_dict_strategies.append(lambda o: o._asdict()) elif is_pydantic_object: obj_to_dict_strategies.append(lambda o: {k: v for (k, v) in o.__dict__.items() if v !="model_fields_set"}) else: obj_to_dict_strategies.append(lambda o: o.__dict__) if hasattr(obj, "__slots__"): obj_to_dict_strategies.append(lambda o: DeepHash._get_slots_dict(o)) else: import inspect obj_to_dict_strategies.append(lambda o: dict(inspect.getmembers(o, lambda m: not inspect.isroutine(m)))) for get_dict in obj_to_dict_strategies: try: d = get_dict(obj) break except AttributeError: pass else: self.hashes[UNPROCESSED_KEY].append(obj) # type: ignore return (unprocessed, 0) obj = d result, counts = self._prep_dict(obj, parent=parent, parents_ids=parents_ids, print_as_attribute=True, original_type=original_type) result = "nt{}".format(result) if is_namedtuple else "obj{}".format(result) return result, counts def _skip_this(self, obj: Any, parent: str) -> bool: skip = False if self.exclude_paths and parent in self.exclude_paths: skip = True if self.include_paths and parent != 'root': if parent not in self.include_paths: skip = True for prefix in self.include_paths: if parent.startswith(prefix): skip = False break elif self.exclude_regex_paths and any( [exclude_regex_path.search(parent) for exclude_regex_path in self.exclude_regex_paths]): # type: ignore skip = True elif self.exclude_types_tuple and isinstance(obj, self.exclude_types_tuple): skip = True elif self.exclude_obj_callback and self.exclude_obj_callback(obj, parent): skip = True return skip def _prep_dict(self, obj: Union[Dict[Any, Any], MutableMapping], parent: str, parents_ids: frozenset = EMPTY_FROZENSET, print_as_attribute: bool = False, original_type: Optional[type] = None) -> HashTuple: result = [] counts = 1 key_text = "%s{}".format(INDEX_VS_ATTRIBUTE[print_as_attribute]) for key, item in obj.items(): counts += 1 # ignore private variables if self.ignore_private_variables and isinstance(key, str) and key.startswith('__'): continue key_formatted = "'%s'" % key if not print_as_attribute and isinstance(key, strings) else key key_in_report = key_text % (parent, key_formatted) key_hash, _ = self._hash(key, parent=key_in_report, parents_ids=parents_ids) if not key_hash: continue item_id = get_id(item) if (parents_ids and item_id in parents_ids) or self._skip_this(item, parent=key_in_report): continue parents_ids_added = add_to_frozen_set(parents_ids, item_id) hashed, count = self._hash(item, parent=key_in_report, parents_ids=parents_ids_added) hashed = KEY_TO_VAL_STR.format(key_hash, hashed) result.append(hashed) counts += count result.sort() result = ';'.join(result) if print_as_attribute: type_ = original_type or type(obj) type_str = type_.__name__ for type_group in self.ignore_type_in_groups: if self.type_check_func(type_, type_group): type_str = ','.join(map(lambda x: x.__name__, type_group)) break else: type_str = 'dict' return "{}:{{{}}}".format(type_str, result), counts def _prep_iterable(self, obj: Iterable[Any], parent: str, parents_ids: frozenset = EMPTY_FROZENSET) -> HashTuple: counts = 1 result = defaultdict(int) for i, item in enumerate(obj): new_parent = "{}[{}]".format(parent, i) if self._skip_this(item, parent=new_parent): continue item_id = get_id(item) if parents_ids and item_id in parents_ids: continue parents_ids_added = add_to_frozen_set(parents_ids, item_id) hashed, count = self._hash(item, parent=new_parent, parents_ids=parents_ids_added) # counting repetitions result[hashed] += 1 counts += count if self.ignore_repetition: result = list(result.keys()) else: result = [ '{}|{}'.format(i, v) for i, v in result.items() ] result = map(str, result) # making sure the result items are string so join command works. if self.ignore_iterable_order: result = sorted(result) result = ','.join(result) result = KEY_TO_VAL_STR.format(type(obj).__name__, result) return result, counts def _prep_bool(self, obj: bool) -> BoolObj: return BoolObj.TRUE if obj else BoolObj.FALSE def _prep_path(self, obj: Path) -> str: type_ = obj.__class__.__name__ return KEY_TO_VAL_STR.format(type_, obj) def _prep_number(self, obj: Union[int, float, complex]) -> str: type_ = "number" if self.ignore_numeric_type_changes else obj.__class__.__name__ if self.significant_digits is not None: obj = self.number_to_string(obj, significant_digits=self.significant_digits, number_format_notation=self.number_format_notation) # type: ignore return KEY_TO_VAL_STR.format(type_, obj) def _prep_ipranges(self, obj) -> str: type_ = 'iprange' obj = str(obj) return KEY_TO_VAL_STR.format(type_, obj) def _prep_datetime(self, obj: datetime.datetime) -> str: type_ = 'datetime' obj = datetime_normalize(self.truncate_datetime, obj, default_timezone=self.default_timezone) return KEY_TO_VAL_STR.format(type_, obj) def _prep_date(self, obj: datetime.date) -> str: type_ = 'datetime' # yes still datetime but it doesn't need normalization return KEY_TO_VAL_STR.format(type_, obj) def _prep_tuple(self, obj: tuple, parent: str, parents_ids: frozenset) -> HashTuple: # Checking to see if it has _fields. Which probably means it is a named # tuple. try: obj._asdict # type: ignore # It must be a normal tuple except AttributeError: result, counts = self._prep_iterable(obj=obj, parent=parent, parents_ids=parents_ids) # We assume it is a namedtuple then else: result, counts = self._prep_obj(obj, parent, parents_ids=parents_ids, is_namedtuple=True) return result, counts def _make_hash_key(self, obj: Any) -> Any: """ Create a key for the hashes dict that distinguishes numeric types. In Python, 1 == 1.0 and hash(1) == hash(1.0), so int and float values collide as dict keys. When ignore_numeric_type_changes is False, we wrap numeric objects as (type, value) tuples so that each type gets its own cache entry and its own hash. """ if not self.ignore_numeric_type_changes and isinstance(obj, only_numbers): return (type(obj), obj) return obj @staticmethod def _make_hash_key_for_lookup(obj: Any, ignore_numeric_type_changes: bool = False) -> Any: """Static version of _make_hash_key for use in static accessor methods.""" if not ignore_numeric_type_changes and isinstance(obj, only_numbers): return (type(obj), obj) return obj def _hash(self, obj: Any, parent: str, parents_ids: frozenset = EMPTY_FROZENSET) -> HashTuple: """The main hash method""" counts = 1 if self.custom_operators is not None: for operator in self.custom_operators: func = getattr(operator, 'normalize_value_for_hashing', None) if func is None: raise NotImplementedError(f"{operator.__class__.__name__} needs to define a normalize_value_for_hashing method to be compatible with ignore_order=True or iterable_compare_func.".format(operator)) else: obj = func(parent, obj) if isinstance(obj, booleanTypes): obj = self._prep_bool(obj) result = None elif self.use_enum_value and isinstance(obj, Enum): obj = obj.value else: result = not_hashed hash_key = self._make_hash_key(obj) try: result, counts = self.hashes[hash_key] except (TypeError, KeyError): pass else: return result, counts if self._skip_this(obj, parent): return None, 0 elif obj is None: result = 'NONE' elif isinstance(obj, strings): result = prepare_string_for_hashing( obj, ignore_string_type_changes=self.ignore_string_type_changes, ignore_string_case=self.ignore_string_case, encodings=self.encodings, ignore_encoding_errors=self.ignore_encoding_errors, ) elif isinstance(obj, Path): result = self._prep_path(obj) elif isinstance(obj, times): result = self._prep_datetime(obj) # type: ignore elif isinstance(obj, datetime.date): result = self._prep_date(obj) elif isinstance(obj, numbers): # type: ignore result = self._prep_number(obj) elif isinstance(obj, ipranges): result = self._prep_ipranges(obj) elif isinstance(obj, uuid.UUID): # Handle UUID objects (including uuid6.UUID) by using their integer value result = str(obj.int) elif isinstance(obj, MutableMapping): result, counts = self._prep_dict(obj=obj, parent=parent, parents_ids=parents_ids) elif isinstance(obj, tuple): result, counts = self._prep_tuple(obj=obj, parent=parent, parents_ids=parents_ids) elif (pandas and isinstance(obj, pandas.DataFrame)): # type: ignore def gen(): # type: ignore yield ('dtype', obj.dtypes) # type: ignore yield ('index', obj.index) # type: ignore yield from obj.items() # type: ignore # which contains (column name, series tuples) result, counts = self._prep_iterable(obj=gen(), parent=parent, parents_ids=parents_ids) elif (polars and isinstance(obj, polars.DataFrame)): # type: ignore def gen(): yield from obj.columns # type: ignore yield from list(obj.schema.items()) # type: ignore yield from obj.rows() # type: ignore result, counts = self._prep_iterable(obj=gen(), parent=parent, parents_ids=parents_ids) elif isinstance(obj, Iterable): result, counts = self._prep_iterable(obj=obj, parent=parent, parents_ids=parents_ids) elif obj == BoolObj.TRUE or obj == BoolObj.FALSE: result = 'bool:true' if obj is BoolObj.TRUE else 'bool:false' elif isinstance(obj, PydanticBaseModel): result, counts = self._prep_obj(obj=obj, parent=parent, parents_ids=parents_ids, is_pydantic_object=True) else: result, counts = self._prep_obj(obj=obj, parent=parent, parents_ids=parents_ids) if result is not_hashed: # pragma: no cover self.hashes[UNPROCESSED_KEY].append(obj) # type: ignore elif result is unprocessed: pass elif self.apply_hash: if isinstance(obj, strings): result_cleaned = result else: result_cleaned = prepare_string_for_hashing( str(result), ignore_string_type_changes=self.ignore_string_type_changes, ignore_string_case=self.ignore_string_case) result = self.hasher(result_cleaned) # It is important to keep the hash of all objects. # The hashes will be later used for comparing the objects. # Object to hash when possible otherwise ObjectID to hash try: self.hashes[hash_key] = (result, counts) except TypeError: obj_id = get_id(obj) self.hashes[obj_id] = (result, counts) return result, counts if __name__ == "__main__": # pragma: no cover import doctest doctest.testmod() qlustered-deepdiff-41c7265/deepdiff/delta.py000066400000000000000000001632731516241264500207760ustar00rootroot00000000000000import copy import logging from typing import List, Dict, IO, Callable, Set, Union, Optional, Any, cast from functools import partial, cmp_to_key from collections.abc import Mapping from copy import deepcopy from deepdiff import DeepDiff from deepdiff.serialization import pickle_load, pickle_dump from deepdiff.helper import ( strings, numbers, np_ndarray, np_array_factory, numpy_dtypes, get_doc, not_found, numpy_dtype_string_to_type, dict_, Opcode, FlatDeltaRow, FlatDeltaDict, UnkownValueCode, FlatDataAction, OPCODE_TAG_TO_FLAT_DATA_ACTION, FLAT_DATA_ACTION_TO_OPCODE_TAG, SetOrdered, ) from deepdiff.path import ( _path_to_elements, _get_nested_obj, _get_nested_obj_and_force, GET, GETATTR, check_elem, parse_path, stringify_path, ) from deepdiff.anyset import AnySet from deepdiff.summarize import summarize logger = logging.getLogger(__name__) VERIFICATION_MSG = 'Expected the old value for {} to be {} but it is {}. Error found on: {}. You may want to set force=True, especially if this delta is created by passing flat_rows_list or flat_dict_list' ELEM_NOT_FOUND_TO_ADD_MSG = 'Key or index of {} is not found for {} for setting operation.' TYPE_CHANGE_FAIL_MSG = 'Unable to do the type change for {} from to type {} due to {}' VERIFY_BIDIRECTIONAL_MSG = ('You have applied the delta to an object that has ' 'different values than the original object the delta was made from.') FAIL_TO_REMOVE_ITEM_IGNORE_ORDER_MSG = 'Failed to remove index[{}] on {}. It was expected to be {} but got {}' DELTA_NUMPY_OPERATOR_OVERRIDE_MSG = ( 'A numpy ndarray is most likely being added to a delta. ' 'Due to Numpy override the + operator, you can only do: delta + ndarray ' 'and NOT ndarray + delta') BINIARY_MODE_NEEDED_MSG = "Please open the file in the binary mode and pass to Delta by passing 'b' in open(..., 'b'): {}" DELTA_AT_LEAST_ONE_ARG_NEEDED = 'At least one of the diff, delta_path or delta_file arguments need to be passed.' INVALID_ACTION_WHEN_CALLING_GET_ELEM = 'invalid action of {} when calling _get_elem_and_compare_to_old_value' INVALID_ACTION_WHEN_CALLING_SIMPLE_SET_ELEM = 'invalid action of {} when calling _simple_set_elem_value' INVALID_ACTION_WHEN_CALLING_SIMPLE_DELETE_ELEM = 'invalid action of {} when calling _simple_set_elem_value' UNABLE_TO_GET_ITEM_MSG = 'Unable to get the item at {}: {}' UNABLE_TO_GET_PATH_MSG = 'Unable to get the item at {}' INDEXES_NOT_FOUND_WHEN_IGNORE_ORDER = 'Delta added to an incompatible object. Unable to add the following items at the specific indexes. {}' NUMPY_TO_LIST = 'NUMPY_TO_LIST' NOT_VALID_NUMPY_TYPE = "{} is not a valid numpy type." doc = get_doc('delta.rst') class DeltaError(ValueError): """ Delta specific errors """ pass class DeltaNumpyOperatorOverrideError(ValueError): """ Delta Numpy Operator Override Error """ pass class Delta: __doc__ = doc def __init__( self, diff: Union[DeepDiff, Mapping, str, bytes, None]=None, delta_path: Optional[str]=None, delta_file: Optional[IO]=None, delta_diff: Optional[dict]=None, flat_dict_list: Optional[List[Dict]]=None, flat_rows_list: Optional[List[FlatDeltaRow]]=None, deserializer: Callable=pickle_load, log_errors: bool=True, mutate: bool=False, raise_errors: bool=False, safe_to_import: Optional[Set[str]]=None, serializer: Callable=pickle_dump, verify_symmetry: Optional[bool]=None, bidirectional: bool=False, always_include_values: bool=False, iterable_compare_func_was_used: Optional[bool]=None, force: bool=False, fill: Any=not_found, ): # for pickle deserializer: if hasattr(deserializer, '__code__') and 'safe_to_import' in set(deserializer.__code__.co_varnames): _deserializer = deserializer else: def _deserializer(obj, safe_to_import=None): result = deserializer(obj) if result.get('_iterable_opcodes'): _iterable_opcodes = {} for path, op_codes in result['_iterable_opcodes'].items(): _iterable_opcodes[path] = [] for op_code in op_codes: _iterable_opcodes[path].append( Opcode( **op_code ) ) result['_iterable_opcodes'] = _iterable_opcodes return result self._reversed_diff = None if verify_symmetry is not None: logger.warning( "DeepDiff Deprecation: use bidirectional instead of verify_symmetry parameter." ) bidirectional = verify_symmetry self.bidirectional = bidirectional if bidirectional: self.always_include_values = True # We need to include the values in bidirectional deltas else: self.always_include_values = always_include_values if diff is not None: if isinstance(diff, DeepDiff): self.diff = diff._to_delta_dict(directed=not bidirectional, always_include_values=self.always_include_values) elif isinstance(diff, Mapping): self.diff = diff elif isinstance(diff, strings): self.diff = _deserializer(diff, safe_to_import=safe_to_import) elif delta_path: with open(delta_path, 'rb') as the_file: content = the_file.read() self.diff = _deserializer(content, safe_to_import=safe_to_import) elif delta_diff: self.diff = delta_diff elif delta_file: try: content = delta_file.read() except UnicodeDecodeError as e: raise ValueError(BINIARY_MODE_NEEDED_MSG.format(e)) from None self.diff = _deserializer(content, safe_to_import=safe_to_import) elif flat_dict_list: # Use copy to preserve original value of flat_dict_list in calling module self.diff = self._from_flat_dicts(copy.deepcopy(flat_dict_list)) elif flat_rows_list: self.diff = self._from_flat_rows(copy.deepcopy(flat_rows_list)) else: raise ValueError(DELTA_AT_LEAST_ONE_ARG_NEEDED) self.mutate = mutate self.raise_errors = raise_errors self.log_errors = log_errors self._numpy_paths = self.diff.get('_numpy_paths', False) # When we create the delta from a list of flat dictionaries, details such as iterable_compare_func_was_used get lost. # That's why we allow iterable_compare_func_was_used to be explicitly set. self._iterable_compare_func_was_used = self.diff.get('_iterable_compare_func_was_used', iterable_compare_func_was_used) self.serializer = serializer self.deserializer = deserializer self.force = force self.fill = fill if force: self.get_nested_obj = _get_nested_obj_and_force else: self.get_nested_obj = _get_nested_obj self.reset() def __repr__(self): return "".format(summarize(self.diff, max_length=100)) def reset(self): self.post_process_paths_to_convert = dict_() def __add__(self, other): if isinstance(other, numbers) and self._numpy_paths: # type: ignore raise DeltaNumpyOperatorOverrideError(DELTA_NUMPY_OPERATOR_OVERRIDE_MSG) if self.mutate: self.root = other else: self.root = deepcopy(other) self._do_pre_process() self._do_values_changed() self._do_set_item_added() self._do_set_item_removed() self._do_type_changes() # NOTE: the remove iterable action needs to happen BEFORE # all the other iterables to match the reverse of order of operations in DeepDiff self._do_iterable_opcodes() self._do_iterable_item_removed() self._do_iterable_item_added() self._do_ignore_order() self._do_dictionary_item_added() self._do_dictionary_item_removed() self._do_attribute_added() self._do_attribute_removed() self._do_post_process() other = self.root # removing the reference to other del self.root self.reset() return other __radd__ = __add__ def __rsub__(self, other): if self._reversed_diff is None: self._reversed_diff = self._get_reverse_diff() self.diff, self._reversed_diff = self._reversed_diff, self.diff result = self.__add__(other) self.diff, self._reversed_diff = self._reversed_diff, self.diff return result def _raise_or_log(self, msg, level='error'): if self.log_errors: getattr(logger, level)(msg) if self.raise_errors: raise DeltaError(msg) def _do_verify_changes(self, path, expected_old_value, current_old_value): if self.bidirectional and expected_old_value != current_old_value: if isinstance(path, str): path_str = path else: path_str = stringify_path(path, root_element=('', GETATTR)) self._raise_or_log(VERIFICATION_MSG.format( path_str, expected_old_value, current_old_value, VERIFY_BIDIRECTIONAL_MSG)) def _get_elem_and_compare_to_old_value( self, obj, path_for_err_reporting, expected_old_value, elem=None, action=None, forced_old_value=None, next_element=None, ): try: check_elem(elem) except ValueError as error: self._raise_or_log(UNABLE_TO_GET_ITEM_MSG.format(path_for_err_reporting, error)) return not_found # if forced_old_value is not None: try: if action == GET: current_old_value = obj[elem] elif action == GETATTR: current_old_value = getattr(obj, elem) # type: ignore else: raise DeltaError(INVALID_ACTION_WHEN_CALLING_GET_ELEM.format(action)) except (KeyError, IndexError, AttributeError, TypeError) as e: if self.force: if forced_old_value is None: if next_element is None or isinstance(next_element, str): _forced_old_value = {} else: _forced_old_value = [] else: _forced_old_value = forced_old_value if action == GET: if isinstance(obj, list): if isinstance(elem, int) and elem < len(obj): obj[elem] = _forced_old_value else: obj.append(_forced_old_value) else: obj[elem] = _forced_old_value elif action == GETATTR: setattr(obj, elem, _forced_old_value) # type: ignore return _forced_old_value current_old_value = not_found if isinstance(path_for_err_reporting, (list, tuple)): path_for_err_reporting = '.'.join([i[0] for i in path_for_err_reporting]) if self.bidirectional: self._raise_or_log(VERIFICATION_MSG.format( path_for_err_reporting, expected_old_value, current_old_value, e)) else: self._raise_or_log(UNABLE_TO_GET_PATH_MSG.format( path_for_err_reporting)) return current_old_value def _simple_set_elem_value(self, obj, path_for_err_reporting, elem=None, value=None, action=None): """ Set the element value directly on an object """ try: if action == GET: try: obj[elem] = value except IndexError: if elem == len(obj): obj.append(value) elif self.fill is not not_found and elem > len(obj): while len(obj) < elem: if callable(self.fill): obj.append(self.fill(obj, value, path_for_err_reporting)) else: obj.append(self.fill) obj.append(value) else: self._raise_or_log(ELEM_NOT_FOUND_TO_ADD_MSG.format(elem, path_for_err_reporting)) elif action == GETATTR: setattr(obj, elem, value) # type: ignore else: raise DeltaError(INVALID_ACTION_WHEN_CALLING_SIMPLE_SET_ELEM.format(action)) except (KeyError, IndexError, AttributeError, TypeError) as e: self._raise_or_log('Failed to set {} due to {}'.format(path_for_err_reporting, e)) def _coerce_obj(self, parent, obj, path, parent_to_obj_elem, parent_to_obj_action, elements, to_type, from_type): """ Coerce obj and mark it in post_process_paths_to_convert for later to be converted back. Also reassign it to its parent to replace the old object. """ self.post_process_paths_to_convert[elements[:-1]] = {'old_type': to_type, 'new_type': from_type} # If this function is going to ever be used to convert numpy arrays, uncomment these lines: # if from_type is np_ndarray: # obj = obj.tolist() # else: obj = to_type(obj) if parent: # Making sure that the object is re-instated inside the parent especially if it was immutable # and we had to turn it into a mutable one. In such cases the object has a new id. self._simple_set_elem_value(obj=parent, path_for_err_reporting=path, elem=parent_to_obj_elem, value=obj, action=parent_to_obj_action) return obj def _set_new_value(self, parent, parent_to_obj_elem, parent_to_obj_action, obj, elements, path, elem, action, new_value): """ Set the element value on an object and if necessary convert the object to the proper mutable type """ if isinstance(obj, tuple): # Check if it's a NamedTuple and use _replace() to generate a new copy with the change if hasattr(obj, '_fields') and hasattr(obj, '_replace'): if action == GETATTR: obj = obj._replace(**{elem: new_value}) if parent: self._simple_set_elem_value(obj=parent, path_for_err_reporting=path, elem=parent_to_obj_elem, value=obj, action=parent_to_obj_action) return else: # Regular tuple - convert this object back to a tuple later obj = self._coerce_obj( parent, obj, path, parent_to_obj_elem, parent_to_obj_action, elements, to_type=list, from_type=tuple) if elem != 0 and self.force and isinstance(obj, list) and len(obj) == 0: # it must have been a dictionary obj = {} self._simple_set_elem_value(obj=parent, path_for_err_reporting=path, elem=parent_to_obj_elem, value=obj, action=parent_to_obj_action) self._simple_set_elem_value(obj=obj, path_for_err_reporting=path, elem=elem, value=new_value, action=action) def _simple_delete_elem(self, obj, path_for_err_reporting, elem=None, action=None): """ Delete the element directly on an object """ try: if action == GET: del obj[elem] elif action == GETATTR: del obj.__dict__[elem] else: raise DeltaError(INVALID_ACTION_WHEN_CALLING_SIMPLE_DELETE_ELEM.format(action)) except (KeyError, IndexError, AttributeError) as e: self._raise_or_log('Failed to set {} due to {}'.format(path_for_err_reporting, e)) def _del_elem(self, parent, parent_to_obj_elem, parent_to_obj_action, obj, elements, path, elem, action): """ Delete the element value on an object and if necessary convert the object to the proper mutable type """ obj_is_new = False if isinstance(obj, tuple): # convert this object back to a tuple later self.post_process_paths_to_convert[elements[:-1]] = {'old_type': list, 'new_type': tuple} obj = list(obj) obj_is_new = True self._simple_delete_elem(obj=obj, path_for_err_reporting=path, elem=elem, action=action) if obj_is_new and parent: # Making sure that the object is re-instated inside the parent especially if it was immutable # and we had to turn it into a mutable one. In such cases the object has a new id. self._simple_set_elem_value(obj=parent, path_for_err_reporting=path, elem=parent_to_obj_elem, value=obj, action=parent_to_obj_action) def _do_iterable_item_added(self): iterable_item_added = self.diff.get('iterable_item_added', {}) iterable_item_moved = self.diff.get('iterable_item_moved') # First we need to create a placeholder for moved items. # This will then get replaced below after we go through added items. # Without this items can get double added because moved store the new_value and does not need item_added replayed if iterable_item_moved: added_dict = {v["new_path"]: None for k, v in iterable_item_moved.items()} iterable_item_added.update(added_dict) if iterable_item_added: self._do_item_added(iterable_item_added, insert=True) if iterable_item_moved: added_dict = {v["new_path"]: v["value"] for k, v in iterable_item_moved.items()} self._do_item_added(added_dict, insert=False) def _do_dictionary_item_added(self): dictionary_item_added = self.diff.get('dictionary_item_added') if dictionary_item_added: self._do_item_added(dictionary_item_added, sort=False) def _do_attribute_added(self): attribute_added = self.diff.get('attribute_added') if attribute_added: self._do_item_added(attribute_added) @staticmethod def _sort_key_for_item_added(path_and_value): elements = _path_to_elements(path_and_value[0]) # Example elements: [(4.3, 'GET'), ('b', 'GETATTR'), ('a3', 'GET')] # We only care about the values in the elements not how to get the values. return [i[0] for i in elements] @staticmethod def _sort_comparison(left, right): """ We use sort comparison instead of _sort_key_for_item_added when we run into comparing element types that can not be compared with each other, such as None to None. Or integer to string. """ # Example elements: [(4.3, 'GET'), ('b', 'GETATTR'), ('a3', 'GET')] # We only care about the values in the elements not how to get the values. left_path = [i[0] for i in _path_to_elements(left[0], root_element=None)] right_path = [i[0] for i in _path_to_elements(right[0], root_element=None)] try: if left_path < right_path: return -1 elif left_path > right_path: return 1 else: return 0 except TypeError: if len(left_path) > len(right_path): left_path = left_path[:len(right_path)] elif len(right_path) > len(left_path): right_path = right_path[:len(left_path)] for l_elem, r_elem in zip(left_path, right_path): if type(l_elem) != type(r_elem) or type(l_elem) in None: l_elem = str(l_elem) r_elem = str(r_elem) try: if l_elem < r_elem: return -1 elif l_elem > r_elem: return 1 except TypeError: continue return 0 def _do_item_added(self, items, sort=True, insert=False): if sort: # sorting items by their path so that the items with smaller index # are applied first (unless `sort` is `False` so that order of # added items is retained, e.g. for dicts). try: items = sorted(items.items(), key=self._sort_key_for_item_added) except TypeError: items = sorted(items.items(), key=cmp_to_key(self._sort_comparison)) else: items = items.items() for path, new_value in items: elem_and_details = self._get_elements_and_details(path) if elem_and_details: elements, parent, parent_to_obj_elem, parent_to_obj_action, obj, elem, action = elem_and_details else: continue # pragma: no cover. Due to cPython peephole optimizer, this line doesn't get covered. https://github.com/nedbat/coveragepy/issues/198 # Insert is only true for iterables, make sure it is a valid index. if(insert and elem < len(obj)): # type: ignore obj.insert(elem, None) # type: ignore self._set_new_value(parent, parent_to_obj_elem, parent_to_obj_action, obj, elements, path, elem, action, new_value) def _do_values_changed(self): values_changed = self.diff.get('values_changed') if values_changed: self._do_values_or_type_changed(values_changed) def _do_type_changes(self): type_changes = self.diff.get('type_changes') if type_changes: self._do_values_or_type_changed(type_changes, is_type_change=True) def _do_post_process(self): if self.post_process_paths_to_convert: # Example: We had converted some object to be mutable and now we are converting them back to be immutable. # We don't need to check the change because it is not really a change that was part of the original diff. self._do_values_or_type_changed(self.post_process_paths_to_convert, is_type_change=True, verify_changes=False) def _do_pre_process(self): if self._numpy_paths and ('iterable_item_added' in self.diff or 'iterable_item_removed' in self.diff): preprocess_paths = dict_() for path, type_ in self._numpy_paths.items(): # type: ignore preprocess_paths[path] = {'old_type': np_ndarray, 'new_type': list} try: type_ = numpy_dtype_string_to_type(type_) except Exception as e: self._raise_or_log(NOT_VALID_NUMPY_TYPE.format(e)) continue # pragma: no cover. Due to cPython peephole optimizer, this line doesn't get covered. https://github.com/nedbat/coveragepy/issues/198 self.post_process_paths_to_convert[path] = {'old_type': list, 'new_type': type_} if preprocess_paths: self._do_values_or_type_changed(preprocess_paths, is_type_change=True) def _get_elements_and_details(self, path): try: elements = _path_to_elements(path) if len(elements) > 1: elements_subset = elements[:-2] if len(elements_subset) != len(elements): next_element = elements[-2][0] next2_element = elements[-1][0] else: next_element = None parent = self.get_nested_obj(obj=self, elements=elements_subset, next_element=next_element) parent_to_obj_elem, parent_to_obj_action = elements[-2] obj = self._get_elem_and_compare_to_old_value( obj=parent, path_for_err_reporting=path, expected_old_value=None, elem=parent_to_obj_elem, action=parent_to_obj_action, next_element=next2_element) # type: ignore else: # parent = self # obj = self.root # parent_to_obj_elem = 'root' # parent_to_obj_action = GETATTR parent = parent_to_obj_elem = parent_to_obj_action = None obj = self # obj = self.get_nested_obj(obj=self, elements=elements[:-1]) elem, action = elements[-1] # type: ignore check_elem(elem) except Exception as e: self._raise_or_log(UNABLE_TO_GET_ITEM_MSG.format(path, e)) return None else: if obj is not_found: return None return elements, parent, parent_to_obj_elem, parent_to_obj_action, obj, elem, action def _do_values_or_type_changed(self, changes, is_type_change=False, verify_changes=True): compare_func_was_used = self.diff.get('_iterable_compare_func_was_used', False) for path, value in changes.items(): # When iterable_compare_func is used, DiffLevel.path() inverts use_t2 for # moved items (see model.py DiffLevel.path). This means dict keys here are # actually t2 paths and new_path holds the t1 path. Apply at t1 so we # don't access indices that don't exist yet or modify the wrong item. apply_path = value['new_path'] if (compare_func_was_used and value.get('new_path')) else path elem_and_details = self._get_elements_and_details(apply_path) if elem_and_details: elements, parent, parent_to_obj_elem, parent_to_obj_action, obj, elem, action = elem_and_details else: continue # pragma: no cover. Due to cPython peephole optimizer, this line doesn't get covered. https://github.com/nedbat/coveragepy/issues/198 expected_old_value = value.get('old_value', not_found) current_old_value = self._get_elem_and_compare_to_old_value( obj=obj, path_for_err_reporting=path, expected_old_value=expected_old_value, elem=elem, action=action) if current_old_value is not_found: continue # pragma: no cover. I have not been able to write a test for this case. But we should still check for it. # With type change if we could have originally converted the type from old_value # to new_value just by applying the class of the new_value, then we might not include the new_value # in the delta dictionary. That is defined in Model.DeltaResult._from_tree_type_changes if is_type_change and 'new_value' not in value: try: new_type = value['new_type'] # in case of Numpy we pass the ndarray plus the dtype in a tuple if new_type in numpy_dtypes: new_value = np_array_factory(current_old_value, new_type) else: new_value = new_type(current_old_value) except Exception as e: self._raise_or_log(TYPE_CHANGE_FAIL_MSG.format(obj[elem], value.get('new_type', 'unknown'), e)) # type: ignore continue else: new_value = value['new_value'] self._set_new_value(parent, parent_to_obj_elem, parent_to_obj_action, obj, elements, path, elem, action, new_value) if verify_changes: self._do_verify_changes(path, expected_old_value, current_old_value) def _do_item_removed(self, items): """ Handle removing items. """ # Sorting the iterable_item_removed in reverse order based on the paths. # So that we delete a bigger index before a smaller index try: sorted_item = sorted(items.items(), key=self._sort_key_for_item_added, reverse=True) except TypeError: sorted_item = sorted(items.items(), key=cmp_to_key(self._sort_comparison), reverse=True) for path, expected_old_value in sorted_item: elem_and_details = self._get_elements_and_details(path) if elem_and_details: elements, parent, parent_to_obj_elem, parent_to_obj_action, obj, elem, action = elem_and_details else: continue # pragma: no cover. Due to cPython peephole optimizer, this line doesn't get covered. https://github.com/nedbat/coveragepy/issues/198 look_for_expected_old_value = False current_old_value = not_found try: if action == GET: current_old_value = obj[elem] # type: ignore elif action == GETATTR: current_old_value = getattr(obj, elem) look_for_expected_old_value = current_old_value != expected_old_value except (KeyError, IndexError, AttributeError, TypeError): look_for_expected_old_value = True if look_for_expected_old_value and isinstance(obj, list) and not self._iterable_compare_func_was_used: # It may return None if it doesn't find it elem = self._find_closest_iterable_element_for_index(obj, elem, expected_old_value) if elem is not None: current_old_value = expected_old_value if current_old_value is not_found or elem is None: continue self._del_elem(parent, parent_to_obj_elem, parent_to_obj_action, obj, elements, path, elem, action) self._do_verify_changes(path, expected_old_value, current_old_value) def _find_closest_iterable_element_for_index(self, obj, elem, expected_old_value): closest_elem = None closest_distance = float('inf') for index, value in enumerate(obj): dist = abs(index - elem) if dist > closest_distance: break if value == expected_old_value and dist < closest_distance: closest_elem = index closest_distance = dist return closest_elem def _do_iterable_opcodes(self): _iterable_opcodes = self.diff.get('_iterable_opcodes', {}) if _iterable_opcodes: for path, opcodes in _iterable_opcodes.items(): transformed = [] # elements = _path_to_elements(path) elem_and_details = self._get_elements_and_details(path) if elem_and_details: elements, parent, parent_to_obj_elem, parent_to_obj_action, obj, elem, action = elem_and_details if parent is None: parent = self obj = self.root parent_to_obj_elem = 'root' parent_to_obj_action = GETATTR else: continue # pragma: no cover. Due to cPython peephole optimizer, this line doesn't get covered. https://github.com/nedbat/coveragepy/issues/198 # import pytest; pytest.set_trace() obj = self.get_nested_obj(obj=self, elements=elements) is_obj_tuple = isinstance(obj, tuple) for opcode in opcodes: if opcode.tag == 'replace': # Replace items in list a[i1:i2] with b[j1:j2] transformed.extend(opcode.new_values) elif opcode.tag == 'delete': # Delete items from list a[i1:i2], so we do nothing here continue elif opcode.tag == 'insert': # Insert items from list b[j1:j2] into the new list transformed.extend(opcode.new_values) elif opcode.tag == 'equal': # Items are the same in both lists, so we add them to the result transformed.extend(obj[opcode.t1_from_index:opcode.t1_to_index]) # type: ignore if is_obj_tuple: obj = tuple(obj) # type: ignore # Making sure that the object is re-instated inside the parent especially if it was immutable # and we had to turn it into a mutable one. In such cases the object has a new id. self._simple_set_elem_value(obj=parent, path_for_err_reporting=path, elem=parent_to_obj_elem, value=obj, action=parent_to_obj_action) else: obj[:] = transformed # type: ignore # obj = self.get_nested_obj(obj=self, elements=elements) # for def _do_iterable_item_removed(self): iterable_item_removed = self.diff.get('iterable_item_removed', {}) iterable_item_moved = self.diff.get('iterable_item_moved') if iterable_item_moved: # These will get added back during items_added removed_dict = {k: v["value"] for k, v in iterable_item_moved.items()} iterable_item_removed.update(removed_dict) if iterable_item_removed: self._do_item_removed(iterable_item_removed) def _do_dictionary_item_removed(self): dictionary_item_removed = self.diff.get('dictionary_item_removed') if dictionary_item_removed: self._do_item_removed(dictionary_item_removed) def _do_attribute_removed(self): attribute_removed = self.diff.get('attribute_removed') if attribute_removed: self._do_item_removed(attribute_removed) def _do_set_item_added(self): items = self.diff.get('set_item_added') if items: self._do_set_or_frozenset_item(items, func='union') def _do_set_item_removed(self): items = self.diff.get('set_item_removed') if items: self._do_set_or_frozenset_item(items, func='difference') def _do_set_or_frozenset_item(self, items, func): for path, value in items.items(): elements = _path_to_elements(path) parent = self.get_nested_obj(obj=self, elements=elements[:-1]) elem, action = elements[-1] obj = self._get_elem_and_compare_to_old_value( parent, path_for_err_reporting=path, expected_old_value=None, elem=elem, action=action, forced_old_value=set()) new_value = getattr(obj, func)(value) if hasattr(parent, '_fields') and hasattr(parent, '_replace'): # Handle parent NamedTuple by creating a new instance with _replace(). Will not work with nested objects. new_parent = parent._replace(**{elem: new_value}) self.root = new_parent else: self._simple_set_elem_value(parent, path_for_err_reporting=path, elem=elem, value=new_value, action=action) def _do_ignore_order_get_old(self, obj, remove_indexes_per_path, fixed_indexes_values, path_for_err_reporting): """ A generator that gets the old values in an iterable when the order was supposed to be ignored. """ old_obj_index = -1 max_len = len(obj) - 1 while old_obj_index < max_len: old_obj_index += 1 current_old_obj = obj[old_obj_index] if current_old_obj in fixed_indexes_values: continue if old_obj_index in remove_indexes_per_path: expected_obj_to_delete = remove_indexes_per_path.pop(old_obj_index) if current_old_obj == expected_obj_to_delete: continue else: self._raise_or_log(FAIL_TO_REMOVE_ITEM_IGNORE_ORDER_MSG.format( old_obj_index, path_for_err_reporting, expected_obj_to_delete, current_old_obj)) yield current_old_obj def _do_ignore_order(self): """ 't1': [5, 1, 1, 1, 6], 't2': [7, 1, 1, 1, 8], 'iterable_items_added_at_indexes': { 'root': { 0: 7, 4: 8 } }, 'iterable_items_removed_at_indexes': { 'root': { 4: 6, 0: 5 } } """ fixed_indexes = self.diff.get('iterable_items_added_at_indexes', dict_()) remove_indexes = self.diff.get('iterable_items_removed_at_indexes', dict_()) paths = SetOrdered(fixed_indexes.keys()) | SetOrdered(remove_indexes.keys()) for path in paths: # type: ignore # In the case of ignore_order reports, we are pointing to the container object. # Thus we add a [0] to the elements so we can get the required objects and discard what we don't need. elem_and_details = self._get_elements_and_details("{}[0]".format(path)) if elem_and_details: _, parent, parent_to_obj_elem, parent_to_obj_action, obj, _, _ = elem_and_details else: continue # pragma: no cover. Due to cPython peephole optimizer, this line doesn't get covered. https://github.com/nedbat/coveragepy/issues/198 # copying both these dictionaries since we don't want to mutate them. fixed_indexes_per_path = fixed_indexes.get(path, dict_()).copy() remove_indexes_per_path = remove_indexes.get(path, dict_()).copy() fixed_indexes_values = AnySet(fixed_indexes_per_path.values()) new_obj = [] # Numpy's NdArray does not like the bool function. if isinstance(obj, np_ndarray): there_are_old_items = obj.size > 0 else: there_are_old_items = bool(obj) old_item_gen = self._do_ignore_order_get_old( obj, remove_indexes_per_path, fixed_indexes_values, path_for_err_reporting=path) while there_are_old_items or fixed_indexes_per_path: new_obj_index = len(new_obj) if new_obj_index in fixed_indexes_per_path: new_item = fixed_indexes_per_path.pop(new_obj_index) new_obj.append(new_item) elif there_are_old_items: try: new_item = next(old_item_gen) except StopIteration: there_are_old_items = False else: new_obj.append(new_item) else: # pop a random item from the fixed_indexes_per_path dictionary self._raise_or_log(INDEXES_NOT_FOUND_WHEN_IGNORE_ORDER.format(fixed_indexes_per_path)) new_item = fixed_indexes_per_path.pop(next(iter(fixed_indexes_per_path))) new_obj.append(new_item) if isinstance(obj, tuple): new_obj = tuple(new_obj) # Making sure that the object is re-instated inside the parent especially if it was immutable # and we had to turn it into a mutable one. In such cases the object has a new id. self._simple_set_elem_value(obj=parent, path_for_err_reporting=path, elem=parent_to_obj_elem, value=new_obj, action=parent_to_obj_action) def _get_reverse_diff(self): if not self.bidirectional: raise ValueError('Please recreate the delta with bidirectional=True') SIMPLE_ACTION_TO_REVERSE = { 'iterable_item_added': 'iterable_item_removed', 'iterable_items_added_at_indexes': 'iterable_items_removed_at_indexes', 'attribute_added': 'attribute_removed', 'set_item_added': 'set_item_removed', 'dictionary_item_added': 'dictionary_item_removed', } # Adding the reverse of the dictionary for key in list(SIMPLE_ACTION_TO_REVERSE.keys()): SIMPLE_ACTION_TO_REVERSE[SIMPLE_ACTION_TO_REVERSE[key]] = key r_diff = {} for action, info in self.diff.items(): reverse_action = SIMPLE_ACTION_TO_REVERSE.get(action) if reverse_action: r_diff[reverse_action] = info elif action == 'values_changed': r_diff[action] = {} for path, path_info in info.items(): reverse_path = path_info['new_path'] if path_info.get('new_path') else path r_diff[action][reverse_path] = { 'new_value': path_info['old_value'], 'old_value': path_info['new_value'] } elif action == 'type_changes': r_diff[action] = {} for path, path_info in info.items(): reverse_path = path_info['new_path'] if path_info.get('new_path') else path r_diff[action][reverse_path] = { 'old_type': path_info['new_type'], 'new_type': path_info['old_type'], } if 'new_value' in path_info: r_diff[action][reverse_path]['old_value'] = path_info['new_value'] if 'old_value' in path_info: r_diff[action][reverse_path]['new_value'] = path_info['old_value'] elif action == 'iterable_item_moved': r_diff[action] = {} for path, path_info in info.items(): old_path = path_info['new_path'] r_diff[action][old_path] = { 'new_path': path, 'value': path_info['value'], } elif action == '_iterable_opcodes': r_diff[action] = {} for path, op_codes in info.items(): r_diff[action][path] = [] for op_code in op_codes: tag = op_code.tag tag = {'delete': 'insert', 'insert': 'delete'}.get(tag, tag) new_op_code = Opcode( tag=tag, t1_from_index=op_code.t2_from_index, t1_to_index=op_code.t2_to_index, t2_from_index=op_code.t1_from_index, t2_to_index=op_code.t1_to_index, new_values=op_code.old_values, old_values=op_code.new_values, ) r_diff[action][path].append(new_op_code) return r_diff def dump(self, file): """ Dump into file object """ # Small optimization: Our internal pickle serializer can just take a file object # and directly write to it. However if a user defined serializer is passed # we want to make it compatible with the expectation that self.serializer(self.diff) # will give the user the serialization and then it can be written to # a file object when using the dump(file) function. param_names_of_serializer = set(self.serializer.__code__.co_varnames) if 'file_obj' in param_names_of_serializer: self.serializer(self.diff, file_obj=file) else: file.write(self.dumps()) def dumps(self): """ Return the serialized representation of the object as a bytes object, instead of writing it to a file. """ return self.serializer(self.diff) def to_dict(self): return dict(self.diff) def _flatten_iterable_opcodes(self, _parse_path): """ Converts op_codes to FlatDeltaRows """ result = [] for path, op_codes in self.diff['_iterable_opcodes'].items(): for op_code in op_codes: result.append( FlatDeltaRow( path=_parse_path(path), action=OPCODE_TAG_TO_FLAT_DATA_ACTION[op_code.tag], value=op_code.new_values, old_value=op_code.old_values, type=type(op_code.new_values), old_type=type(op_code.old_values), new_path=None, t1_from_index=op_code.t1_from_index, t1_to_index=op_code.t1_to_index, t2_from_index=op_code.t2_from_index, t2_to_index=op_code.t2_to_index, ) ) return result @staticmethod def _get_flat_row(action, info, _parse_path, keys_and_funcs, report_type_changes=True): for path, details in info.items(): row = {'path': _parse_path(path), 'action': action} for key, new_key, func in keys_and_funcs: if key in details: if func: row[new_key] = func(details[key]) else: row[new_key] = details[key] if report_type_changes: if 'value' in row and 'type' not in row: row['type'] = type(row['value']) if 'old_value' in row and 'old_type' not in row: row['old_type'] = type(row['old_value']) yield FlatDeltaRow(**row) @staticmethod def _from_flat_rows(flat_rows_list: List[FlatDeltaRow]): flat_dict_list = (i._asdict() for i in flat_rows_list) return Delta._from_flat_dicts(flat_dict_list) @staticmethod def _from_flat_dicts(flat_dict_list): """ Create the delta's diff object from the flat_dict_list """ result = {} FLATTENING_NEW_ACTION_MAP = { 'unordered_iterable_item_added': 'iterable_items_added_at_indexes', 'unordered_iterable_item_removed': 'iterable_items_removed_at_indexes', } for flat_dict in flat_dict_list: index = None action = flat_dict.get("action") path = flat_dict.get("path") value = flat_dict.get('value') new_path = flat_dict.get('new_path') old_value = flat_dict.get('old_value', UnkownValueCode) if not action: raise ValueError("Flat dict need to include the 'action'.") if path is None: raise ValueError("Flat dict need to include the 'path'.") if action in FLATTENING_NEW_ACTION_MAP: action = FLATTENING_NEW_ACTION_MAP[action] index = path.pop() if action in { FlatDataAction.attribute_added, FlatDataAction.attribute_removed, }: root_element = ('root', GETATTR) else: root_element = ('root', GET) if isinstance(path, str): path_str = path else: path_str = stringify_path(path, root_element=root_element) # We need the string path if new_path and new_path != path: new_path = stringify_path(new_path, root_element=root_element) else: new_path = None if action not in result: result[action] = {} if action in { 'iterable_items_added_at_indexes', 'iterable_items_removed_at_indexes', }: if path_str not in result[action]: result[action][path_str] = {} result[action][path_str][index] = value elif action in { FlatDataAction.set_item_added, FlatDataAction.set_item_removed }: if path_str not in result[action]: result[action][path_str] = set() result[action][path_str].add(value) elif action in { FlatDataAction.dictionary_item_added, FlatDataAction.dictionary_item_removed, FlatDataAction.attribute_removed, FlatDataAction.attribute_added, FlatDataAction.iterable_item_added, FlatDataAction.iterable_item_removed, }: result[action][path_str] = value elif action == 'values_changed': if old_value == UnkownValueCode: result[action][path_str] = {'new_value': value} else: result[action][path_str] = {'new_value': value, 'old_value': old_value} elif action == 'type_changes': type_ = flat_dict.get('type', UnkownValueCode) old_type = flat_dict.get('old_type', UnkownValueCode) result[action][path_str] = {'new_value': value} for elem, elem_value in [ ('new_type', type_), ('old_type', old_type), ('old_value', old_value), ]: if elem_value != UnkownValueCode: result[action][path_str][elem] = elem_value elif action == FlatDataAction.iterable_item_moved: result[action][path_str] = {'value': value} elif action in { FlatDataAction.iterable_items_inserted, FlatDataAction.iterable_items_deleted, FlatDataAction.iterable_items_replaced, FlatDataAction.iterable_items_equal, }: if '_iterable_opcodes' not in result: result['_iterable_opcodes'] = {} if path_str not in result['_iterable_opcodes']: result['_iterable_opcodes'][path_str] = [] result['_iterable_opcodes'][path_str].append( Opcode( tag=FLAT_DATA_ACTION_TO_OPCODE_TAG[action], # type: ignore t1_from_index=flat_dict.get('t1_from_index'), t1_to_index=flat_dict.get('t1_to_index'), t2_from_index=flat_dict.get('t2_from_index'), t2_to_index=flat_dict.get('t2_to_index'), new_values=flat_dict.get('value'), old_values=flat_dict.get('old_value'), ) ) if new_path: result[action][path_str]['new_path'] = new_path return result def to_flat_dicts(self, include_action_in_path=False, report_type_changes=True) -> List[FlatDeltaDict]: """ Returns a flat list of actions that is easily machine readable. For example: {'iterable_item_added': {'root[3]': 5, 'root[2]': 3}} Becomes: [ {'path': [3], 'value': 5, 'action': 'iterable_item_added'}, {'path': [2], 'value': 3, 'action': 'iterable_item_added'}, ] **Parameters** include_action_in_path : Boolean, default=False When False, we translate DeepDiff's paths like root[3].attribute1 into a [3, 'attribute1']. When True, we include the action to retrieve the item in the path: [(3, 'GET'), ('attribute1', 'GETATTR')] Note that the "action" here is the different than the action reported by to_flat_dicts. The action here is just about the "path" output. report_type_changes : Boolean, default=True If False, we don't report the type change. Instead we report the value change. Example: t1 = {"a": None} t2 = {"a": 1} dump = Delta(DeepDiff(t1, t2)).dumps() delta = Delta(dump) assert t2 == delta + t1 flat_result = delta.to_flat_dicts() flat_expected = [{'path': ['a'], 'action': 'type_changes', 'value': 1, 'new_type': int, 'old_type': type(None)}] assert flat_expected == flat_result flat_result2 = delta.to_flat_dicts(report_type_changes=False) flat_expected2 = [{'path': ['a'], 'action': 'values_changed', 'value': 1}] **List of actions** Here are the list of actions that the flat dictionary can return. iterable_item_added iterable_item_removed iterable_item_moved values_changed type_changes set_item_added set_item_removed dictionary_item_added dictionary_item_removed attribute_added attribute_removed """ return cast(List[FlatDeltaDict], [ i._asdict() for i in self.to_flat_rows(include_action_in_path=include_action_in_path, report_type_changes=report_type_changes) ]) def to_flat_rows(self, include_action_in_path=False, report_type_changes=True) -> List[FlatDeltaRow]: """ Just like to_flat_dicts but returns FlatDeltaRow Named Tuples """ result = [] if include_action_in_path: _parse_path = partial(parse_path, include_actions=True) else: _parse_path = parse_path if report_type_changes: keys_and_funcs = [ ('value', 'value', None), ('new_value', 'value', None), ('old_value', 'old_value', None), ('new_type', 'type', None), ('old_type', 'old_type', None), ('new_path', 'new_path', _parse_path), ] else: if not self.always_include_values: raise ValueError( "When converting to flat dictionaries, if report_type_changes=False and there are type changes, " "you must set the always_include_values=True at the delta object creation. Otherwise there is nothing to include." ) keys_and_funcs = [ ('value', 'value', None), ('new_value', 'value', None), ('old_value', 'old_value', None), ('new_path', 'new_path', _parse_path), ] FLATTENING_NEW_ACTION_MAP = { 'iterable_items_added_at_indexes': 'unordered_iterable_item_added', 'iterable_items_removed_at_indexes': 'unordered_iterable_item_removed', } for action, info in self.diff.items(): if action == '_iterable_opcodes': result.extend(self._flatten_iterable_opcodes(_parse_path=_parse_path)) continue if action.startswith('_'): continue if action in FLATTENING_NEW_ACTION_MAP: new_action = FLATTENING_NEW_ACTION_MAP[action] for path, index_to_value in info.items(): path = _parse_path(path) for index, value in index_to_value.items(): path2 = path.copy() if include_action_in_path: path2.append((index, 'GET')) # type: ignore else: path2.append(index) if report_type_changes: row = FlatDeltaRow(path=path2, value=value, action=new_action, type=type(value)) # type: ignore else: row = FlatDeltaRow(path=path2, value=value, action=new_action) # type: ignore result.append(row) elif action in {'set_item_added', 'set_item_removed'}: for path, values in info.items(): path = _parse_path(path) for value in values: if report_type_changes: row = FlatDeltaRow(path=path, value=value, action=action, type=type(value)) else: row = FlatDeltaRow(path=path, value=value, action=action) result.append(row) elif action == 'dictionary_item_added': for path, value in info.items(): path = _parse_path(path) if isinstance(value, dict) and len(value) == 1: new_key = next(iter(value)) path.append(new_key) value = value[new_key] elif isinstance(value, (list, tuple)) and len(value) == 1: value = value[0] path.append(0) # type: ignore action = 'iterable_item_added' elif isinstance(value, set) and len(value) == 1: value = value.pop() action = 'set_item_added' if report_type_changes: row = FlatDeltaRow(path=path, value=value, action=action, type=type(value)) # type: ignore else: row = FlatDeltaRow(path=path, value=value, action=action) # type: ignore result.append(row) elif action in { 'dictionary_item_removed', 'iterable_item_added', 'iterable_item_removed', 'attribute_removed', 'attribute_added' }: for path, value in info.items(): path = _parse_path(path) if report_type_changes: row = FlatDeltaRow(path=path, value=value, action=action, type=type(value)) else: row = FlatDeltaRow(path=path, value=value, action=action) result.append(row) elif action == 'type_changes': if not report_type_changes: action = 'values_changed' for row in self._get_flat_row( action=action, info=info, _parse_path=_parse_path, keys_and_funcs=keys_and_funcs, report_type_changes=report_type_changes, ): result.append(row) else: for row in self._get_flat_row( action=action, info=info, _parse_path=_parse_path, keys_and_funcs=keys_and_funcs, report_type_changes=report_type_changes, ): result.append(row) return result if __name__ == "__main__": # pragma: no cover import doctest doctest.testmod() qlustered-deepdiff-41c7265/deepdiff/diff.py000077500000000000000000002747671516241264500206330ustar00rootroot00000000000000#!/usr/bin/env python # In order to run the docstrings: # python3 -m deepdiff.diff # You might need to run it many times since dictionaries come in different orders # every time you run the docstrings. # However the docstring expects it in a specific order in order to pass! import difflib import logging import types import datetime import uuid from enum import Enum from copy import deepcopy from math import isclose as is_close from typing import List, Dict, Callable, Union, Any, Pattern, Tuple, Optional, Set, FrozenSet, TYPE_CHECKING, Protocol, Literal from collections.abc import Mapping, Iterable, Sequence from collections import defaultdict from inspect import getmembers from itertools import zip_longest from functools import lru_cache from deepdiff.helper import (strings, bytes_type, numbers, uuids, ListItemRemovedOrAdded, notpresent, IndexedHash, unprocessed, add_to_frozen_set, basic_types, convert_item_or_items_into_set_else_none, get_type, convert_item_or_items_into_compiled_regexes_else_none, type_is_subclass_of_type_group, type_in_type_group, get_doc, number_to_string, datetime_normalize, KEY_TO_VAL_STR, booleans, np_ndarray, np_floating, get_numpy_ndarray_rows, RepeatedTimer, TEXT_VIEW, TREE_VIEW, DELTA_VIEW, COLORED_VIEW, COLORED_COMPACT_VIEW, detailed__dict__, add_root_to_paths, np, get_truncate_datetime, dict_, CannotCompare, ENUM_INCLUDE_KEYS, PydanticBaseModel, Opcode, SetOrdered, ipranges) from deepdiff.serialization import SerializationMixin from deepdiff.distance import DistanceMixin, logarithmic_similarity from deepdiff.model import ( RemapDict, ResultDict, TextResult, TreeResult, DiffLevel, DictRelationship, AttributeRelationship, REPORT_KEYS, SubscriptableIterableRelationship, NonSubscriptableIterableRelationship, SetRelationship, NumpyArrayRelationship, CUSTOM_FIELD, FORCE_DEFAULT, ) from deepdiff.deephash import DeepHash, combine_hashes_lists from deepdiff.base import Base from deepdiff.lfucache import LFUCache, DummyLFU from deepdiff.colored_view import ColoredView if TYPE_CHECKING: from pytz.tzinfo import BaseTzInfo logger = logging.getLogger(__name__) MAX_PASSES_REACHED_MSG = ( 'DeepDiff has reached the max number of passes of {}. ' 'You can possibly get more accurate results by increasing the max_passes parameter.') MAX_DIFFS_REACHED_MSG = ( 'DeepDiff has reached the max number of diffs of {}. ' 'You can possibly get more accurate results by increasing the max_diffs parameter.') notpresent_indexed = IndexedHash(indexes=[0], item=notpresent) doc = get_doc('diff_doc.rst') PROGRESS_MSG = "DeepDiff {} seconds in progress. Pass #{}, Diff #{}" def _report_progress(_stats: Dict[str, Any], progress_logger: Callable[[str], None], duration: float) -> None: """ Report the progress every few seconds. """ progress_logger(PROGRESS_MSG.format(duration, _stats[PASSES_COUNT], _stats[DIFF_COUNT])) DISTANCE_CACHE_HIT_COUNT = 'DISTANCE CACHE HIT COUNT' DIFF_COUNT = 'DIFF COUNT' PASSES_COUNT = 'PASSES COUNT' MAX_PASS_LIMIT_REACHED = 'MAX PASS LIMIT REACHED' MAX_DIFF_LIMIT_REACHED = 'MAX DIFF LIMIT REACHED' DISTANCE_CACHE_ENABLED = 'DISTANCE CACHE ENABLED' PREVIOUS_DIFF_COUNT = 'PREVIOUS DIFF COUNT' PREVIOUS_DISTANCE_CACHE_HIT_COUNT = 'PREVIOUS DISTANCE CACHE HIT COUNT' CANT_FIND_NUMPY_MSG = 'Unable to import numpy. This must be a bug in DeepDiff since a numpy array is detected.' INVALID_VIEW_MSG = "view parameter must be one of 'text', 'tree', 'delta', 'colored' or 'colored_compact'. But {} was passed." CUTOFF_RANGE_ERROR_MSG = 'cutoff_distance_for_pairs needs to be a positive float max 1.' VERBOSE_LEVEL_RANGE_MSG = 'verbose_level should be 0, 1, or 2.' PURGE_LEVEL_RANGE_MSG = 'cache_purge_level should be 0, 1, or 2.' _ENABLE_CACHE_EVERY_X_DIFF = '_ENABLE_CACHE_EVERY_X_DIFF' model_fields_set = frozenset(["model_fields_set"]) # What is the threshold to consider 2 items to be pairs. Only used when ignore_order = True. CUTOFF_DISTANCE_FOR_PAIRS_DEFAULT = 0.3 # What is the threshold to calculate pairs of items between 2 iterables. # For example 2 iterables that have nothing in common, do not need their pairs to be calculated. CUTOFF_INTERSECTION_FOR_PAIRS_DEFAULT = 0.7 DEEPHASH_PARAM_KEYS = ( 'exclude_types', 'exclude_paths', 'include_paths', 'exclude_regex_paths', 'hasher', 'significant_digits', 'number_format_notation', 'ignore_string_type_changes', 'ignore_numeric_type_changes', 'ignore_uuid_types', 'use_enum_value', 'ignore_type_in_groups', 'ignore_type_subclasses', 'ignore_string_case', 'exclude_obj_callback', 'ignore_private_variables', 'encodings', 'ignore_encoding_errors', 'default_timezone', 'custom_operators', ) class DeepDiffProtocol(Protocol): t1: Any t2: Any cutoff_distance_for_pairs: float use_log_scale: bool log_scale_similarity_threshold: float view: str math_epsilon: Optional[float] class DeepDiff(ResultDict, SerializationMixin, DistanceMixin, DeepDiffProtocol, Base): __doc__ = doc CACHE_AUTO_ADJUST_THRESHOLD = 0.25 def __init__(self, t1: Any, t2: Any, _original_type: Optional[Any]=None, cache_purge_level: int=1, cache_size: int=0, cache_tuning_sample_size: int=0, custom_operators: Optional[List[Any]] =None, cutoff_distance_for_pairs: float=CUTOFF_DISTANCE_FOR_PAIRS_DEFAULT, cutoff_intersection_for_pairs: float=CUTOFF_INTERSECTION_FOR_PAIRS_DEFAULT, default_timezone:Union[datetime.timezone, "BaseTzInfo"]=datetime.timezone.utc, encodings: Optional[List[str]]=None, exclude_obj_callback: Optional[Callable]=None, exclude_obj_callback_strict: Optional[Callable]=None, exclude_paths: Union[str, List[str], Set[str], FrozenSet[str], None]=None, exclude_regex_paths: Union[str, List[str], Pattern[str], List[Pattern[str]], None]=None, exclude_types: Optional[List[type]]=None, get_deep_distance: bool=False, group_by: Union[str, Tuple[str, str], Callable, None]=None, group_by_sort_key: Union[str, Callable, None]=None, hasher: Optional[Callable]=None, hashes: Optional[Dict[Any, Any]]=None, ignore_encoding_errors: bool=False, ignore_nan_inequality: bool=False, ignore_numeric_type_changes: bool=False, ignore_order: bool=False, ignore_order_func: Optional[Callable]=None, ignore_private_variables: bool=True, ignore_string_case: bool=False, ignore_string_type_changes: bool=False, ignore_type_in_groups: Optional[List[Tuple[Any, ...]]]=None, ignore_type_subclasses: bool=False, ignore_uuid_types: bool=False, include_obj_callback: Optional[Callable]=None, include_obj_callback_strict: Optional[Callable]=None, include_paths: Union[str, List[str], None]=None, iterable_compare_func: Optional[Callable]=None, log_frequency_in_sec: int=0, log_scale_similarity_threshold: float=0.1, log_stacktrace: bool=False, math_epsilon: Optional[float]=None, max_diffs: Optional[int]=None, max_passes: int=10000000, number_format_notation: Literal["f", "e"]="f", number_to_string_func: Optional[Callable]=None, progress_logger: Callable[[str], None]=logger.info, report_repetition: bool=False, significant_digits: Optional[int]=None, threshold_to_diff_deeper: float = 0.33, truncate_datetime: Optional[str]=None, use_enum_value: bool=False, use_log_scale: bool=False, verbose_level: int=1, view: str=TEXT_VIEW, zip_ordered_iterables: bool=False, _parameters: Optional[Dict[str, Any]]=None, _shared_parameters: Optional[Dict[str, Any]]=None, **kwargs): super().__init__() if kwargs: raise ValueError(( "The following parameter(s) are not valid: %s\n" "The valid parameters are ignore_order, report_repetition, significant_digits, " "number_format_notation, exclude_paths, include_paths, exclude_types, exclude_regex_paths, ignore_type_in_groups, " "ignore_string_type_changes, ignore_numeric_type_changes, ignore_type_subclasses, ignore_uuid_types, truncate_datetime, " "ignore_private_variables, ignore_nan_inequality, number_to_string_func, verbose_level, " "view, hasher, hashes, max_passes, max_diffs, zip_ordered_iterables, " "cutoff_distance_for_pairs, cutoff_intersection_for_pairs, log_frequency_in_sec, cache_size, " "cache_tuning_sample_size, get_deep_distance, group_by, group_by_sort_key, cache_purge_level, log_stacktrace," "math_epsilon, iterable_compare_func, use_enum_value, _original_type, threshold_to_diff_deeper, default_timezone " "ignore_order_func, custom_operators, encodings, ignore_encoding_errors, use_log_scale, log_scale_similarity_threshold " "_parameters and _shared_parameters.") % ', '.join(kwargs.keys())) if _parameters: self.__dict__.update(_parameters) else: self.custom_operators = custom_operators or [] self.ignore_order = ignore_order self.ignore_order_func = ignore_order_func ignore_type_in_groups = ignore_type_in_groups or [] if numbers == ignore_type_in_groups or numbers in ignore_type_in_groups: ignore_numeric_type_changes = True self.ignore_numeric_type_changes = ignore_numeric_type_changes if strings == ignore_type_in_groups or strings in ignore_type_in_groups: ignore_string_type_changes = True # Handle ignore_uuid_types - check if uuid+str group is already in ignore_type_in_groups uuid_str_group = (uuids[0], str) if uuid_str_group == ignore_type_in_groups or uuid_str_group in ignore_type_in_groups: ignore_uuid_types = True self.ignore_uuid_types = ignore_uuid_types self.use_enum_value = use_enum_value self.log_scale_similarity_threshold = log_scale_similarity_threshold self.use_log_scale = use_log_scale self.default_timezone = default_timezone self.log_stacktrace = log_stacktrace self.threshold_to_diff_deeper = threshold_to_diff_deeper self.ignore_string_type_changes = ignore_string_type_changes self.ignore_type_in_groups = self.get_ignore_types_in_groups( ignore_type_in_groups=ignore_type_in_groups, ignore_string_type_changes=ignore_string_type_changes, ignore_numeric_type_changes=ignore_numeric_type_changes, ignore_type_subclasses=ignore_type_subclasses, ignore_uuid_types=ignore_uuid_types) self.report_repetition = report_repetition self.exclude_paths = add_root_to_paths(convert_item_or_items_into_set_else_none(exclude_paths)) self.include_paths = add_root_to_paths(convert_item_or_items_into_set_else_none(include_paths)) self.exclude_regex_paths = convert_item_or_items_into_compiled_regexes_else_none(exclude_regex_paths) self.exclude_types = set(exclude_types) if exclude_types else None self.exclude_types_tuple = tuple(exclude_types) if exclude_types else None # we need tuple for checking isinstance self.ignore_type_subclasses = ignore_type_subclasses self.type_check_func = type_in_type_group if ignore_type_subclasses else type_is_subclass_of_type_group self.ignore_string_case = ignore_string_case self.exclude_obj_callback = exclude_obj_callback self.exclude_obj_callback_strict = exclude_obj_callback_strict self.include_obj_callback = include_obj_callback self.include_obj_callback_strict = include_obj_callback_strict self.number_to_string = number_to_string_func or number_to_string self.iterable_compare_func = iterable_compare_func self.zip_ordered_iterables = zip_ordered_iterables self.ignore_private_variables = ignore_private_variables self.ignore_nan_inequality = ignore_nan_inequality self.hasher = hasher self.cache_tuning_sample_size = cache_tuning_sample_size self.group_by = group_by if callable(group_by_sort_key): self.group_by_sort_key = group_by_sort_key elif group_by_sort_key: def _group_by_sort_key(x): return x[group_by_sort_key] self.group_by_sort_key = _group_by_sort_key else: self.group_by_sort_key = None self.encodings = encodings self.ignore_encoding_errors = ignore_encoding_errors self.significant_digits = self.get_significant_digits(significant_digits, ignore_numeric_type_changes) self.math_epsilon = math_epsilon if self.math_epsilon is not None and self.ignore_order: logger.warning("math_epsilon in conjunction with ignore_order=True is only used for flat object comparisons. Custom math_epsilon will not have an effect when comparing nested objects.") self.truncate_datetime = get_truncate_datetime(truncate_datetime) self.number_format_notation = number_format_notation if verbose_level in {0, 1, 2}: self.verbose_level = verbose_level else: raise ValueError(VERBOSE_LEVEL_RANGE_MSG) if cache_purge_level not in {0, 1, 2}: raise ValueError(PURGE_LEVEL_RANGE_MSG) self.view = view # Setting up the cache for dynamic programming. One dictionary per instance of root of DeepDiff running. self.max_passes = max_passes self.max_diffs = max_diffs self.cutoff_distance_for_pairs = float(cutoff_distance_for_pairs) self.cutoff_intersection_for_pairs = float(cutoff_intersection_for_pairs) if self.cutoff_distance_for_pairs < 0 or self.cutoff_distance_for_pairs > 1: raise ValueError(CUTOFF_RANGE_ERROR_MSG) # _Parameters are the clean _parameters to initialize DeepDiff with so we avoid all the above # cleaning functionalities when running DeepDiff recursively. # However DeepHash has its own set of _parameters that are slightly different than DeepDIff. # DeepDiff _parameters are transformed to DeepHash _parameters via _get_deephash_params method. self.progress_logger = progress_logger self.cache_size = cache_size _parameters = self.__dict__.copy() _parameters['group_by'] = None # overwriting since these parameters will be passed on to other passes. if log_stacktrace: self.log_err = logger.exception else: self.log_err = logger.error # Non-Root if _shared_parameters: self.is_root = False self._shared_parameters = _shared_parameters self.__dict__.update(_shared_parameters) # We are in some pass other than root progress_timer = None # Root else: self.is_root = True # Caching the DeepDiff results for dynamic programming self._distance_cache = LFUCache(cache_size) if cache_size else DummyLFU() self._stats = { PASSES_COUNT: 0, DIFF_COUNT: 0, DISTANCE_CACHE_HIT_COUNT: 0, PREVIOUS_DIFF_COUNT: 0, PREVIOUS_DISTANCE_CACHE_HIT_COUNT: 0, MAX_PASS_LIMIT_REACHED: False, MAX_DIFF_LIMIT_REACHED: False, DISTANCE_CACHE_ENABLED: bool(cache_size), } self.hashes = dict_() if hashes is None else hashes self._numpy_paths = dict_() # if _numpy_paths is None else _numpy_paths self.group_by_keys = set() # Track keys that originated from group_by operations self._shared_parameters = { 'hashes': self.hashes, '_stats': self._stats, '_distance_cache': self._distance_cache, 'group_by_keys': self.group_by_keys, '_numpy_paths': self._numpy_paths, _ENABLE_CACHE_EVERY_X_DIFF: self.cache_tuning_sample_size * 10, } if log_frequency_in_sec: # Creating a progress log reporter that runs in a separate thread every log_frequency_in_sec seconds. progress_timer = RepeatedTimer(log_frequency_in_sec, _report_progress, self._stats, progress_logger) else: progress_timer = None self._parameters = _parameters self.deephash_parameters = self._get_deephash_params() self.tree = TreeResult() self._iterable_opcodes = {} if group_by and self.is_root: try: original_t1 = t1 t1 = self._group_iterable_to_dict(t1, group_by, item_name='t1') except (KeyError, ValueError): pass else: try: t2 = self._group_iterable_to_dict(t2, group_by, item_name='t2') except (KeyError, ValueError): t1 = original_t1 self.t1 = t1 self.t2 = t2 try: root = DiffLevel(t1, t2, verbose_level=self.verbose_level) # _original_type is only used to pass the original type of the data. Currently only used for numpy arrays. # The reason is that we convert the numpy array to python list and then later for distance calculations # we convert only the the last dimension of it into numpy arrays. self._diff(root, parents_ids=frozenset({id(t1)}), _original_type=_original_type) if get_deep_distance and view in {TEXT_VIEW, TREE_VIEW}: self.tree['deep_distance'] = self._get_rough_distance() self.tree.remove_empty_keys() view_results = self._get_view_results(self.view) if isinstance(view_results, ColoredView): self.update(view_results.tree) self._colored_view = view_results else: self.update(view_results) finally: if self.is_root: if cache_purge_level: del self._distance_cache del self.hashes del self._shared_parameters del self._parameters for key in (PREVIOUS_DIFF_COUNT, PREVIOUS_DISTANCE_CACHE_HIT_COUNT, DISTANCE_CACHE_ENABLED): del self._stats[key] if progress_timer: duration = progress_timer.stop() self._stats['DURATION SEC'] = duration logger.info('stats {}'.format(self.get_stats())) if cache_purge_level == 2: self.__dict__.clear() def _get_deephash_params(self): result = {key: self._parameters[key] for key in DEEPHASH_PARAM_KEYS} result['ignore_repetition'] = not self.report_repetition result['number_to_string_func'] = self.number_to_string return result def _report_result(self, report_type, change_level, local_tree=None): """ Add a detected change to the reference-style result dictionary. report_type will be added to level. (We'll create the text-style report from there later.) :param report_type: A well defined string key describing the type of change. Examples: "set_item_added", "values_changed" :param change_level: A DiffLevel object describing the objects in question in their before-change and after-change object structure. :local_tree: None """ if not self._skip_this(change_level): change_level.report_type = report_type tree = self.tree if local_tree is None else local_tree tree[report_type].add(change_level) def custom_report_result(self, report_type, level, extra_info=None): """ Add a detected change to the reference-style result dictionary. report_type will be added to level. (We'll create the text-style report from there later.) :param report_type: A well defined string key describing the type of change. Examples: "set_item_added", "values_changed" :param parent: A DiffLevel object describing the objects in question in their before-change and after-change object structure. :param extra_info: A dict that describe this result :rtype: None """ if not self._skip_this(level): level.report_type = report_type level.additional[CUSTOM_FIELD] = extra_info self.tree[report_type].add(level) @staticmethod def _dict_from_slots(object: Any) -> Dict[str, Any]: def unmangle(attribute: str) -> str: if attribute.startswith('__') and attribute != '__weakref__': return '_{type}{attribute}'.format( type=type(object).__name__, attribute=attribute ) return attribute all_slots = [] if isinstance(object, type): mro = object.__mro__ # pragma: no cover. I have not been able to write a test for this case. But we still check for it. else: mro = object.__class__.__mro__ for type_in_mro in mro: slots = getattr(type_in_mro, '__slots__', None) if slots: if isinstance(slots, strings): all_slots.append(slots) else: all_slots.extend(slots) return {i: getattr(object, key) for i in all_slots if hasattr(object, key := unmangle(i))} def _diff_enum(self, level: Any, parents_ids: FrozenSet[int]=frozenset(), local_tree: Optional[Any]=None) -> None: t1 = detailed__dict__(level.t1, include_keys=ENUM_INCLUDE_KEYS) t2 = detailed__dict__(level.t2, include_keys=ENUM_INCLUDE_KEYS) self._diff_dict( level, parents_ids, print_as_attribute=True, override=True, override_t1=t1, override_t2=t2, local_tree=local_tree, ) def _diff_obj(self, level: Any, parents_ids: FrozenSet[int]=frozenset(), is_namedtuple: bool=False, local_tree: Optional[Any]=None, is_pydantic_object: bool=False) -> None: """Difference of 2 objects""" processing_error = False t1: Optional[Dict[str, Any]] = None t2: Optional[Dict[str, Any]] = None try: if is_namedtuple: t1 = level.t1._asdict() t2 = level.t2._asdict() elif is_pydantic_object: t1 = detailed__dict__(level.t1, ignore_private_variables=self.ignore_private_variables, ignore_keys=model_fields_set) t2 = detailed__dict__(level.t2, ignore_private_variables=self.ignore_private_variables, ignore_keys=model_fields_set) elif all('__dict__' in dir(t) for t in level): t1 = detailed__dict__(level.t1, ignore_private_variables=self.ignore_private_variables) t2 = detailed__dict__(level.t2, ignore_private_variables=self.ignore_private_variables) elif all('__slots__' in dir(t) for t in level): t1 = self._dict_from_slots(level.t1) t2 = self._dict_from_slots(level.t2) else: t1 = {k: v for k, v in getmembers(level.t1) if not callable(v)} t2 = {k: v for k, v in getmembers(level.t2) if not callable(v)} except AttributeError: processing_error = True if processing_error is True or t1 is None or t2 is None: self._report_result('unprocessed', level, local_tree=local_tree) return self._diff_dict( level, parents_ids, print_as_attribute=True, override=True, override_t1=t1, override_t2=t2, local_tree=local_tree, ) def _skip_this(self, level: Any) -> bool: """ Check whether this comparison should be skipped because one of the objects to compare meets exclusion criteria. :rtype: bool """ level_path = level.path() skip = False if self.exclude_paths and level_path in self.exclude_paths: skip = True if self.include_paths and level_path != 'root': if level_path not in self.include_paths: skip = True for prefix in self.include_paths: if prefix in level_path or level_path in prefix: skip = False break elif self.exclude_regex_paths and any( [exclude_regex_path.search(level_path) for exclude_regex_path in self.exclude_regex_paths]): skip = True elif self.exclude_types_tuple and \ (isinstance(level.t1, self.exclude_types_tuple) or isinstance(level.t2, self.exclude_types_tuple)): skip = True elif self.exclude_obj_callback and \ (self.exclude_obj_callback(level.t1, level_path) or self.exclude_obj_callback(level.t2, level_path)): skip = True elif self.exclude_obj_callback_strict and \ (self.exclude_obj_callback_strict(level.t1, level_path) and self.exclude_obj_callback_strict(level.t2, level_path)): skip = True elif self.include_obj_callback and level_path != 'root': skip = True if (self.include_obj_callback(level.t1, level_path) or self.include_obj_callback(level.t2, level_path)): skip = False elif self.include_obj_callback_strict and level_path != 'root': skip = True if (self.include_obj_callback_strict(level.t1, level_path) and self.include_obj_callback_strict(level.t2, level_path)): skip = False return skip def _skip_this_key(self, level: Any, key: Any) -> bool: # if include_paths is not set, than treet every path as included if self.include_paths is None: return False if "{}['{}']".format(level.path(), key) in self.include_paths: return False if level.path() in self.include_paths: # matches e.g. level+key root['foo']['bar']['veg'] include_paths ["root['foo']['bar']"] return False for prefix in self.include_paths: if "{}['{}']".format(level.path(), key) in prefix: # matches as long the prefix is longer than this object key # eg.: level+key root['foo']['bar'] matches prefix root['foo']['bar'] from include paths # level+key root['foo'] matches prefix root['foo']['bar'] from include_paths # level+key root['foo']['bar'] DOES NOT match root['foo'] from include_paths This needs to be handled afterwards return False # check if a higher level is included as a whole (=without any sublevels specified) # matches e.g. level+key root['foo']['bar']['veg'] include_paths ["root['foo']"] # but does not match, if it is level+key root['foo']['bar']['veg'] include_paths ["root['foo']['bar']['fruits']"] up = level.up while up is not None: if up.path() in self.include_paths: return False up = up.up return True def _get_clean_to_keys_mapping(self, keys: Any, level: Any) -> Dict[Any, Any]: """ Get a dictionary of cleaned value of keys to the keys themselves. This is mainly used to transform the keys when the type changes of keys should be ignored. TODO: needs also some key conversion for groups of types other than the built-in strings and numbers. """ result = dict_() for key in keys: if self.ignore_string_type_changes and isinstance(key, bytes): clean_key = key.decode('utf-8') elif self.ignore_string_type_changes and isinstance(key, memoryview): clean_key = key.tobytes().decode('utf-8') elif self.use_enum_value and isinstance(key, Enum): clean_key = key.value elif isinstance(key, numbers): # Skip type prefixing for keys that originated from group_by operations if hasattr(self, 'group_by_keys') and key in self.group_by_keys: if self.significant_digits is None: clean_key = key else: clean_key = self.number_to_string(key, significant_digits=self.significant_digits, number_format_notation=self.number_format_notation) # type: ignore # type: ignore else: type_ = "number" if self.ignore_numeric_type_changes else key.__class__.__name__ if self.significant_digits is None: clean_key = key else: clean_key = self.number_to_string(key, significant_digits=self.significant_digits, number_format_notation=self.number_format_notation) # type: ignore # type: ignore clean_key = KEY_TO_VAL_STR.format(type_, clean_key) else: clean_key = key if self.ignore_string_case and isinstance(clean_key, str): clean_key = clean_key.lower() if clean_key in result: logger.warning(('{} and {} in {} become the same key when ignore_numeric_type_changes' 'or ignore_numeric_type_changes are set to be true.').format( key, result[clean_key], level.path())) else: result[clean_key] = key return result def _diff_dict( self, level: Any, parents_ids: FrozenSet[int]=frozenset([]), print_as_attribute: bool=False, override: bool=False, override_t1: Optional[Any]=None, override_t2: Optional[Any]=None, local_tree: Optional[Any]=None, ) -> None: """Difference of 2 dictionaries""" if override: # for special stuff like custom objects and named tuples we receive preprocessed t1 and t2 # but must not spoil the chain (=level) with it t1 = override_t1 t2 = override_t2 else: t1 = level.t1 t2 = level.t2 if print_as_attribute: item_added_key = "attribute_added" item_removed_key = "attribute_removed" rel_class = AttributeRelationship else: item_added_key = "dictionary_item_added" item_removed_key = "dictionary_item_removed" rel_class = DictRelationship if self.ignore_private_variables: t1_keys = SetOrdered([key for key in t1 if not(isinstance(key, str) and key.startswith('__')) and not self._skip_this_key(level, key)]) t2_keys = SetOrdered([key for key in t2 if not(isinstance(key, str) and key.startswith('__')) and not self._skip_this_key(level, key)]) else: t1_keys = SetOrdered([key for key in t1 if not self._skip_this_key(level, key)]) t2_keys = SetOrdered([key for key in t2 if not self._skip_this_key(level, key)]) if self.ignore_string_type_changes or self.ignore_numeric_type_changes or self.ignore_string_case: t1_clean_to_keys = self._get_clean_to_keys_mapping(keys=t1_keys, level=level) t2_clean_to_keys = self._get_clean_to_keys_mapping(keys=t2_keys, level=level) t1_keys = SetOrdered(t1_clean_to_keys.keys()) t2_keys = SetOrdered(t2_clean_to_keys.keys()) else: t1_clean_to_keys = t2_clean_to_keys = None t_keys_intersect = t2_keys & t1_keys t_keys_added = t2_keys - t_keys_intersect t_keys_removed = t1_keys - t_keys_intersect if self.threshold_to_diff_deeper: if self.exclude_paths: t_keys_union = {f"{level.path()}[{repr(key)}]" for key in (t2_keys | t1_keys)} t_keys_union -= self.exclude_paths t_keys_union_len = len(t_keys_union) else: t_keys_union_len = len(t2_keys | t1_keys) if t_keys_union_len > 1 and len(t_keys_intersect) / t_keys_union_len < self.threshold_to_diff_deeper: self._report_result('values_changed', level, local_tree=local_tree) return for key in t_keys_added: if self._count_diff() is StopIteration: return key = t2_clean_to_keys[key] if t2_clean_to_keys else key change_level = level.branch_deeper( notpresent, t2[key], child_relationship_class=rel_class, child_relationship_param=key, child_relationship_param2=key, ) self._report_result(item_added_key, change_level, local_tree=local_tree) for key in t_keys_removed: if self._count_diff() is StopIteration: return # pragma: no cover. This is already covered for addition. key = t1_clean_to_keys[key] if t1_clean_to_keys else key change_level = level.branch_deeper( t1[key], notpresent, child_relationship_class=rel_class, child_relationship_param=key, child_relationship_param2=key, ) self._report_result(item_removed_key, change_level, local_tree=local_tree) for key in t_keys_intersect: # key present in both dicts - need to compare values if self._count_diff() is StopIteration: return # pragma: no cover. This is already covered for addition. key1 = t1_clean_to_keys[key] if t1_clean_to_keys else key key2 = t2_clean_to_keys[key] if t2_clean_to_keys else key item_id = id(t1[key1]) if parents_ids and item_id in parents_ids: continue parents_ids_added = add_to_frozen_set(parents_ids, item_id) # Go one level deeper next_level = level.branch_deeper( t1[key1], t2[key2], child_relationship_class=rel_class, child_relationship_param=key, child_relationship_param2=key, ) self._diff(next_level, parents_ids_added, local_tree=local_tree) def _diff_set(self, level: Any, local_tree: Optional[Any]=None) -> None: """Difference of sets""" t1_hashtable = self._create_hashtable(level, 't1') t2_hashtable = self._create_hashtable(level, 't2') t1_hashes = set(t1_hashtable.keys()) t2_hashes = set(t2_hashtable.keys()) hashes_added = t2_hashes - t1_hashes hashes_removed = t1_hashes - t2_hashes items_added = [t2_hashtable[i].item for i in hashes_added] items_removed = [t1_hashtable[i].item for i in hashes_removed] for item in items_added: if self._count_diff() is StopIteration: return # pragma: no cover. This is already covered for addition. change_level = level.branch_deeper( notpresent, item, child_relationship_class=SetRelationship) self._report_result('set_item_added', change_level, local_tree=local_tree) for item in items_removed: if self._count_diff() is StopIteration: return # pragma: no cover. This is already covered for addition. change_level = level.branch_deeper( item, notpresent, child_relationship_class=SetRelationship) self._report_result('set_item_removed', change_level, local_tree=local_tree) @staticmethod def _iterables_subscriptable(t1: Any, t2: Any) -> bool: try: if getattr(t1, '__getitem__') and getattr(t2, '__getitem__'): return True else: # pragma: no cover return False # should never happen except AttributeError: return False def _diff_iterable(self, level: Any, parents_ids: FrozenSet[int]=frozenset(), _original_type: Optional[type]=None, local_tree: Optional[Any]=None) -> None: """Difference of iterables""" if (self.ignore_order_func and self.ignore_order_func(level)) or self.ignore_order: self._diff_iterable_with_deephash(level, parents_ids, _original_type=_original_type, local_tree=local_tree) else: self._diff_iterable_in_order(level, parents_ids, _original_type=_original_type, local_tree=local_tree) def _compare_in_order( self, level, t1_from_index=None, t1_to_index=None, t2_from_index=None, t2_to_index=None ) -> List[Tuple[Tuple[int, int], Tuple[Any, Any]]]: """ Default compare if `iterable_compare_func` is not provided. This will compare in sequence order. """ if t1_from_index is None: return [((i, i), (x, y)) for i, (x, y) in enumerate( zip_longest( level.t1, level.t2, fillvalue=ListItemRemovedOrAdded))] else: t1_chunk = level.t1[t1_from_index:t1_to_index] t2_chunk = level.t2[t2_from_index:t2_to_index] return [((i + t1_from_index, i + t2_from_index), (x, y)) for i, (x, y) in enumerate( zip_longest( t1_chunk, t2_chunk, fillvalue=ListItemRemovedOrAdded))] def _get_matching_pairs( self, level, t1_from_index=None, t1_to_index=None, t2_from_index=None, t2_to_index=None ) -> List[Tuple[Tuple[int, int], Tuple[Any, Any]]]: """ Given a level get matching pairs. This returns list of two tuples in the form: [ (t1 index, t2 index), (t1 item, t2 item) ] This will compare using the passed in `iterable_compare_func` if available. Default it to compare in order """ if self.iterable_compare_func is None: # Match in order if there is no compare function provided return self._compare_in_order( level, t1_from_index=t1_from_index, t1_to_index=t1_to_index, t2_from_index=t2_from_index, t2_to_index=t2_to_index, ) try: matches = [] y_matched = set() y_index_matched = set() for i, x in enumerate(level.t1): x_found = False for j, y in enumerate(level.t2): if(j in y_index_matched): # This ensures a one-to-one relationship of matches from t1 to t2. # If y this index in t2 has already been matched to another x # it cannot have another match, so just continue. continue if(self.iterable_compare_func(x, y, level)): deep_hash = DeepHash(y, hashes=self.hashes, apply_hash=True, **self.deephash_parameters, ) y_index_matched.add(j) y_matched.add(deep_hash[y]) matches.append(((i, j), (x, y))) x_found = True break if(not x_found): matches.append(((i, -1), (x, ListItemRemovedOrAdded))) for j, y in enumerate(level.t2): deep_hash = DeepHash(y, hashes=self.hashes, apply_hash=True, **self.deephash_parameters, ) if(deep_hash[y] not in y_matched): matches.append(((-1, j), (ListItemRemovedOrAdded, y))) return matches except CannotCompare: return self._compare_in_order( level, t1_from_index=t1_from_index, t1_to_index=t1_to_index, t2_from_index=t2_from_index, t2_to_index=t2_to_index ) def _diff_iterable_in_order(self, level, parents_ids=frozenset(), _original_type=None, local_tree=None): # We're handling both subscriptable and non-subscriptable iterables. Which one is it? subscriptable = self._iterables_subscriptable(level.t1, level.t2) if subscriptable: child_relationship_class = SubscriptableIterableRelationship else: child_relationship_class = NonSubscriptableIterableRelationship if ( not self.zip_ordered_iterables and isinstance(level.t1, Sequence) and isinstance(level.t2, Sequence) and self._all_values_basic_hashable(level.t1) and self._all_values_basic_hashable(level.t2) and self.iterable_compare_func is None ): local_tree_pass = TreeResult() opcodes_with_values = self._diff_ordered_iterable_by_difflib( level, parents_ids=parents_ids, _original_type=_original_type, child_relationship_class=child_relationship_class, local_tree=local_tree_pass, ) # Sometimes DeepDiff's old iterable diff does a better job than DeepDiff if len(local_tree_pass) > 1: local_tree_pass2 = TreeResult() self._diff_by_forming_pairs_and_comparing_one_by_one( level, parents_ids=parents_ids, _original_type=_original_type, child_relationship_class=child_relationship_class, local_tree=local_tree_pass2, ) if len(local_tree_pass) >= len(local_tree_pass2): local_tree_pass = local_tree_pass2 else: self._iterable_opcodes[level.path(force=FORCE_DEFAULT)] = opcodes_with_values for report_type, levels in local_tree_pass.items(): if levels: self.tree[report_type] |= levels else: self._diff_by_forming_pairs_and_comparing_one_by_one( level, parents_ids=parents_ids, _original_type=_original_type, child_relationship_class=child_relationship_class, local_tree=local_tree, ) def _all_values_basic_hashable(self, iterable: Iterable[Any]) -> bool: """ Are all items basic hashable types? Or there are custom types too? """ # We don't want to exhaust a generator if isinstance(iterable, types.GeneratorType): return False for item in iterable: if not isinstance(item, basic_types): return False return True def _diff_by_forming_pairs_and_comparing_one_by_one( self, level, local_tree, parents_ids=frozenset(), _original_type=None, child_relationship_class=None, t1_from_index=None, t1_to_index=None, t2_from_index=None, t2_to_index=None, ): for (i, j), (x, y) in self._get_matching_pairs( level, t1_from_index=t1_from_index, t1_to_index=t1_to_index, t2_from_index=t2_from_index, t2_to_index=t2_to_index ): if self._count_diff() is StopIteration: return # pragma: no cover. This is already covered for addition. reference_param1 = i reference_param2 = j if y is ListItemRemovedOrAdded: # item removed completely change_level = level.branch_deeper( x, notpresent, child_relationship_class=child_relationship_class, child_relationship_param=reference_param1, child_relationship_param2=reference_param2, ) self._report_result('iterable_item_removed', change_level, local_tree=local_tree) elif x is ListItemRemovedOrAdded: # new item added change_level = level.branch_deeper( notpresent, y, child_relationship_class=child_relationship_class, child_relationship_param=reference_param1, child_relationship_param2=reference_param2, ) self._report_result('iterable_item_added', change_level, local_tree=local_tree) else: # check if item value has changed if (i != j and ((x == y) or self.iterable_compare_func)): # Item moved change_level = level.branch_deeper( x, y, child_relationship_class=child_relationship_class, child_relationship_param=reference_param1, child_relationship_param2=reference_param2 ) self._report_result('iterable_item_moved', change_level, local_tree=local_tree) if self.iterable_compare_func: # Mark additional context denoting that we have moved an item. # This will allow for correctly setting paths relative to t2 when using an iterable_compare_func level.additional["moved"] = True else: continue item_id = id(x) if parents_ids and item_id in parents_ids: continue parents_ids_added = add_to_frozen_set(parents_ids, item_id) # Go one level deeper next_level = level.branch_deeper( x, y, child_relationship_class=child_relationship_class, child_relationship_param=reference_param1, child_relationship_param2=reference_param2 ) self._diff(next_level, parents_ids_added, local_tree=local_tree) def _diff_ordered_iterable_by_difflib( self, level, local_tree, parents_ids=frozenset(), _original_type=None, child_relationship_class=None, ): seq = difflib.SequenceMatcher(isjunk=None, a=level.t1, b=level.t2, autojunk=False) opcodes = seq.get_opcodes() opcodes_with_values = [] # TODO: this logic should be revisted so we detect reverse operations # like when a replacement happens at index X and a reverse replacement happens at index Y # in those cases we have a "iterable_item_moved" operation. for tag, t1_from_index, t1_to_index, t2_from_index, t2_to_index in opcodes: if tag == 'equal': opcodes_with_values.append(Opcode( tag, t1_from_index, t1_to_index, t2_from_index, t2_to_index, )) continue # print('{:7} t1[{}:{}] --> t2[{}:{}] {!r:>8} --> {!r}'.format( # tag, t1_from_index, t1_to_index, t2_from_index, t2_to_index, level.t1[t1_from_index:t1_to_index], level.t2[t2_from_index:t2_to_index])) opcodes_with_values.append(Opcode( tag, t1_from_index, t1_to_index, t2_from_index, t2_to_index, old_values = level.t1[t1_from_index: t1_to_index], new_values = level.t2[t2_from_index: t2_to_index], )) if tag == 'replace': self._diff_by_forming_pairs_and_comparing_one_by_one( level, local_tree=local_tree, parents_ids=parents_ids, _original_type=_original_type, child_relationship_class=child_relationship_class, t1_from_index=t1_from_index, t1_to_index=t1_to_index, t2_from_index=t2_from_index, t2_to_index=t2_to_index, ) elif tag == 'delete': for index, x in enumerate(level.t1[t1_from_index:t1_to_index]): change_level = level.branch_deeper( x, notpresent, child_relationship_class=child_relationship_class, child_relationship_param=index + t1_from_index, child_relationship_param2=index + t1_from_index, ) self._report_result('iterable_item_removed', change_level, local_tree=local_tree) elif tag == 'insert': for index, y in enumerate(level.t2[t2_from_index:t2_to_index]): change_level = level.branch_deeper( notpresent, y, child_relationship_class=child_relationship_class, child_relationship_param=index + t2_from_index, child_relationship_param2=index + t2_from_index, ) self._report_result('iterable_item_added', change_level, local_tree=local_tree) return opcodes_with_values def _diff_str(self, level, local_tree=None): """Compare strings""" if self.ignore_string_case: level.t1 = level.t1.lower() level.t2 = level.t2.lower() if type(level.t1) == type(level.t2) and level.t1 == level.t2: # NOQA return # do we add a diff for convenience? do_diff = True t1_str = level.t1 t2_str = level.t2 if isinstance(level.t1, memoryview): try: t1_str = level.t1.tobytes().decode('ascii') except UnicodeDecodeError: do_diff = False elif isinstance(level.t1, bytes_type): try: t1_str = level.t1.decode('ascii') except UnicodeDecodeError: do_diff = False if isinstance(level.t2, memoryview): try: t2_str = level.t2.tobytes().decode('ascii') except UnicodeDecodeError: do_diff = False elif isinstance(level.t2, bytes_type): try: t2_str = level.t2.decode('ascii') except UnicodeDecodeError: do_diff = False if isinstance(level.t1, Enum): t1_str = level.t1.value if isinstance(level.t2, Enum): t2_str = level.t2.value if t1_str == t2_str: return if do_diff: if '\n' in t1_str or isinstance(t2_str, str) and '\n' in t2_str: diff = difflib.unified_diff( t1_str.splitlines(), t2_str.splitlines(), lineterm='') diff = list(diff) if diff: level.additional['diff'] = '\n'.join(diff) self._report_result('values_changed', level, local_tree=local_tree) def _diff_tuple(self, level, parents_ids, local_tree=None): # Checking to see if it has _fields. Which probably means it is a named # tuple. try: level.t1._asdict # It must be a normal tuple except AttributeError: self._diff_iterable(level, parents_ids, local_tree=local_tree) # We assume it is a namedtuple then else: self._diff_obj(level, parents_ids, is_namedtuple=True, local_tree=local_tree) def _add_hash(self, hashes, item_hash, item, i): if item_hash in hashes: hashes[item_hash].indexes.append(i) else: hashes[item_hash] = IndexedHash(indexes=[i], item=item) def _create_hashtable(self, level, t): """Create hashtable of {item_hash: (indexes, item)}""" obj = getattr(level, t) local_hashes = dict_() for (i, item) in enumerate(obj): try: parent = "{}[{}]".format(level.path(), i) # Note: in the DeepDiff we only calculate the hash of items when we have to. # So self.hashes does not include hashes of all objects in t1 and t2. # It only includes the ones needed when comparing iterables. # The self.hashes dictionary gets shared between different runs of DeepHash # So that any object that is already calculated to have a hash is not re-calculated. deep_hash = DeepHash( item, hashes=self.hashes, parent=parent, apply_hash=True, **self.deephash_parameters, ) except UnicodeDecodeError as err: err.reason = f"Can not produce a hash for {level.path()}: {err.reason}" raise except NotImplementedError: raise # except Exception as e: # pragma: no cover # logger.error("Can not produce a hash for %s." # "Not counting this object.\n %s" % # (level.path(), e)) else: try: item_hash = deep_hash[item] except KeyError: pass else: if item_hash is unprocessed: # pragma: no cover self.log_err("Item %s was not processed while hashing " "thus not counting this object." % level.path()) else: self._add_hash(hashes=local_hashes, item_hash=item_hash, item=item, i=i) # Also we hash the iterables themselves too so that we can later create cache keys from those hashes. DeepHash( obj, hashes=self.hashes, parent=level.path(), apply_hash=True, **self.deephash_parameters, ) return local_hashes @staticmethod @lru_cache(maxsize=2028) def _get_distance_cache_key(added_hash, removed_hash): key1, key2 = (added_hash, removed_hash) if added_hash > removed_hash else (removed_hash, added_hash) if isinstance(key1, int): # If the hash function produces integers we convert them to hex values. # This was used when the default hash function was Murmur3 128bit which produces integers. key1 = hex(key1).encode('utf-8') key2 = hex(key2).encode('utf-8') elif isinstance(key1, str): key1 = key1.encode('utf-8') key2 = key2.encode('utf-8') return key1 + b'--' + key2 + b'dc' def _get_rough_distance_of_hashed_objs( self, added_hash, removed_hash, added_hash_obj, removed_hash_obj, _original_type=None): # We need the rough distance between the 2 objects to see if they qualify to be pairs or not _distance = cache_key = None if self._stats[DISTANCE_CACHE_ENABLED]: cache_key = self._get_distance_cache_key(added_hash, removed_hash) if cache_key in self._distance_cache: self._stats[DISTANCE_CACHE_HIT_COUNT] += 1 _distance = self._distance_cache.get(cache_key) if _distance is None: # We can only cache the rough distance and not the actual diff result for reuse. # The reason is that we have modified the parameters explicitly so they are different and can't # be used for diff reporting diff = DeepDiff( removed_hash_obj.item, added_hash_obj.item, _parameters=self._parameters, _shared_parameters=self._shared_parameters, view=DELTA_VIEW, _original_type=_original_type, iterable_compare_func=self.iterable_compare_func, ) _distance = diff._get_rough_distance() if cache_key and self._stats[DISTANCE_CACHE_ENABLED]: self._distance_cache.set(cache_key, value=_distance) return _distance def _get_most_in_common_pairs_in_iterables( self, hashes_added, hashes_removed, t1_hashtable, t2_hashtable, parents_ids, _original_type): """ Get the closest pairs between items that are removed and items that are added. returns a dictionary of hashes that are closest to each other. The dictionary is going to be symmetrical so any key will be a value too and otherwise. Note that due to the current reporting structure in DeepDiff, we don't compare an item that was added to an item that is in both t1 and t2. For example [{1, 2}, {4, 5, 6}] [{1, 2}, {1, 2, 3}] is only compared between {4, 5, 6} and {1, 2, 3} even though technically {1, 2, 3} is just one item different than {1, 2} Perhaps in future we can have a report key that is item duplicated and modified instead of just added. """ cache_key = None if self._stats[DISTANCE_CACHE_ENABLED]: cache_key = combine_hashes_lists(items=[hashes_added, hashes_removed], prefix='pairs_cache') if cache_key in self._distance_cache: return self._distance_cache.get(cache_key).copy() # A dictionary of hashes to distances and each distance to an ordered set of hashes. # It tells us about the distance of each object from other objects. # And the objects with the same distances are grouped together in an ordered set. # It also includes a "max" key that is just the value of the biggest current distance in the # most_in_common_pairs dictionary. def defaultdict_orderedset(): return defaultdict(SetOrdered) most_in_common_pairs = defaultdict(defaultdict_orderedset) pairs = dict_() pre_calced_distances = None if hashes_added and hashes_removed and np and len(hashes_added) > 1 and len(hashes_removed) > 1: # pre-calculates distances ONLY for 1D arrays whether an _original_type # was explicitly passed or a homogeneous array is detected. # Numpy is needed for this optimization. pre_calced_distances = self._precalculate_numpy_arrays_distance( hashes_added, hashes_removed, t1_hashtable, t2_hashtable, _original_type) if hashes_added and hashes_removed \ and self.iterable_compare_func \ and len(hashes_added) > 0 and len(hashes_removed) > 0: pre_calced_distances = self._precalculate_distance_by_custom_compare_func( hashes_added, hashes_removed, t1_hashtable, t2_hashtable, _original_type) for added_hash in hashes_added: for removed_hash in hashes_removed: added_hash_obj = t2_hashtable[added_hash] removed_hash_obj = t1_hashtable[removed_hash] # Loop is detected if id(removed_hash_obj.item) in parents_ids: continue _distance = None if pre_calced_distances: _distance = pre_calced_distances.get("{}--{}".format(added_hash, removed_hash)) if _distance is None: _distance = self._get_rough_distance_of_hashed_objs( added_hash, removed_hash, added_hash_obj, removed_hash_obj, _original_type) # Left for future debugging # print(f'{Fore.RED}distance of {added_hash_obj.item} and {removed_hash_obj.item}: {_distance}{Style.RESET_ALL}') # Discard potential pairs that are too far. if _distance >= self.cutoff_distance_for_pairs: continue pairs_of_item = most_in_common_pairs[added_hash] pairs_of_item[_distance].add(removed_hash) used_to_hashes = set() distances_to_from_hashes = defaultdict(SetOrdered) for from_hash, distances_to_to_hashes in most_in_common_pairs.items(): # del distances_to_to_hashes['max'] for dist in distances_to_to_hashes: distances_to_from_hashes[dist].add(from_hash) for dist in sorted(distances_to_from_hashes.keys()): from_hashes = distances_to_from_hashes[dist] while from_hashes: from_hash = from_hashes.pop() if from_hash not in used_to_hashes: to_hashes = most_in_common_pairs[from_hash][dist] while to_hashes: to_hash = to_hashes.pop() if to_hash not in used_to_hashes: used_to_hashes.add(from_hash) used_to_hashes.add(to_hash) # Left for future debugging: # print(f'{bcolors.FAIL}Adding {t2_hashtable[from_hash].item} as a pairs of {t1_hashtable[to_hash].item} with distance of {dist}{bcolors.ENDC}') pairs[from_hash] = to_hash inverse_pairs = {v: k for k, v in pairs.items()} pairs.update(inverse_pairs) if cache_key and self._stats[DISTANCE_CACHE_ENABLED]: self._distance_cache.set(cache_key, value=pairs) return pairs.copy() def _diff_iterable_with_deephash(self, level, parents_ids, _original_type=None, local_tree=None): """Diff of hashable or unhashable iterables. Only used when ignoring the order.""" full_t1_hashtable = self._create_hashtable(level, 't1') full_t2_hashtable = self._create_hashtable(level, 't2') t1_hashes = SetOrdered(full_t1_hashtable.keys()) t2_hashes = SetOrdered(full_t2_hashtable.keys()) hashes_added = t2_hashes - t1_hashes hashes_removed = t1_hashes - t2_hashes # Deciding whether to calculate pairs or not. if (len(hashes_added) + len(hashes_removed)) / (len(full_t1_hashtable) + len(full_t2_hashtable) + 1) > self.cutoff_intersection_for_pairs: get_pairs = False else: get_pairs = True # reduce the size of hashtables if self.report_repetition: t1_hashtable = full_t1_hashtable t2_hashtable = full_t2_hashtable else: t1_hashtable = {k: v for k, v in full_t1_hashtable.items() if k in hashes_removed} t2_hashtable = {k: v for k, v in full_t2_hashtable.items() if k in hashes_added} if self._stats[PASSES_COUNT] < self.max_passes and get_pairs: self._stats[PASSES_COUNT] += 1 pairs = self._get_most_in_common_pairs_in_iterables( hashes_added, hashes_removed, t1_hashtable, t2_hashtable, parents_ids, _original_type) elif get_pairs: if not self._stats[MAX_PASS_LIMIT_REACHED]: self._stats[MAX_PASS_LIMIT_REACHED] = True logger.warning(MAX_PASSES_REACHED_MSG.format(self.max_passes)) pairs = dict_() else: pairs = dict_() def get_other_pair(hash_value, in_t1=True): """ Gets the other paired indexed hash item to the hash_value in the pairs dictionary in_t1: are we looking for the other pair in t1 or t2? """ if in_t1: hashtable = t1_hashtable the_other_hashes = hashes_removed else: hashtable = t2_hashtable the_other_hashes = hashes_added other = pairs.pop(hash_value, notpresent) if other is notpresent: other = notpresent_indexed else: # The pairs are symmetrical. # removing the other direction of pair # so it does not get used. del pairs[other] the_other_hashes.remove(other) other = hashtable[other] return other if self.report_repetition: for hash_value in hashes_added: if self._count_diff() is StopIteration: return # pragma: no cover. This is already covered for addition (when report_repetition=False). other = get_other_pair(hash_value) item_id = id(other.item) indexes = t2_hashtable[hash_value].indexes if other.item is notpresent else other.indexes # When we report repetitions, we want the child_relationship_param2 only if there is no repetition. # Because when there is a repetition, we report it in a different way (iterable_items_added_at_indexes for example). # When there is no repetition, we want child_relationship_param2 so that we report the "new_path" correctly. if len(t2_hashtable[hash_value].indexes) == 1: index2 = t2_hashtable[hash_value].indexes[0] else: index2 = None for i in indexes: change_level = level.branch_deeper( other.item, t2_hashtable[hash_value].item, child_relationship_class=SubscriptableIterableRelationship, child_relationship_param=i, child_relationship_param2=index2, ) if other.item is notpresent: self._report_result('iterable_item_added', change_level, local_tree=local_tree) else: parents_ids_added = add_to_frozen_set(parents_ids, item_id) self._diff(change_level, parents_ids_added, local_tree=local_tree) for hash_value in hashes_removed: if self._count_diff() is StopIteration: return # pragma: no cover. This is already covered for addition. other = get_other_pair(hash_value, in_t1=False) item_id = id(other.item) # When we report repetitions, we want the child_relationship_param2 only if there is no repetition. # Because when there is a repetition, we report it in a different way (iterable_items_added_at_indexes for example). # When there is no repetition, we want child_relationship_param2 so that we report the "new_path" correctly. if other.item is notpresent or len(other.indexes > 1): index2 = None else: index2 = other.indexes[0] for i in t1_hashtable[hash_value].indexes: change_level = level.branch_deeper( t1_hashtable[hash_value].item, other.item, child_relationship_class=SubscriptableIterableRelationship, child_relationship_param=i, child_relationship_param2=index2, ) if other.item is notpresent: self._report_result('iterable_item_removed', change_level, local_tree=local_tree) else: # I was not able to make a test case for the following 2 lines since the cases end up # getting resolved above in the hashes_added calcs. However I am leaving these 2 lines # in case things change in future. parents_ids_added = add_to_frozen_set(parents_ids, item_id) # pragma: no cover. self._diff(change_level, parents_ids_added, local_tree=local_tree) # pragma: no cover. items_intersect = t2_hashes.intersection(t1_hashes) for hash_value in items_intersect: t1_indexes = t1_hashtable[hash_value].indexes t2_indexes = t2_hashtable[hash_value].indexes t1_indexes_len = len(t1_indexes) t2_indexes_len = len(t2_indexes) if t1_indexes_len != t2_indexes_len: # this is a repetition change! # create "change" entry, keep current level untouched to handle further changes repetition_change_level = level.branch_deeper( t1_hashtable[hash_value].item, t2_hashtable[hash_value].item, # nb: those are equal! child_relationship_class=SubscriptableIterableRelationship, child_relationship_param=t1_hashtable[hash_value] .indexes[0]) repetition_change_level.additional['repetition'] = RemapDict( old_repeat=t1_indexes_len, new_repeat=t2_indexes_len, old_indexes=t1_indexes, new_indexes=t2_indexes) self._report_result('repetition_change', repetition_change_level, local_tree=local_tree) else: for hash_value in hashes_added: if self._count_diff() is StopIteration: return other = get_other_pair(hash_value) item_id = id(other.item) index = t2_hashtable[hash_value].indexes[0] if other.item is notpresent else other.indexes[0] index2 = t2_hashtable[hash_value].indexes[0] change_level = level.branch_deeper( other.item, t2_hashtable[hash_value].item, child_relationship_class=SubscriptableIterableRelationship, child_relationship_param=index, child_relationship_param2=index2, ) if other.item is notpresent: self._report_result('iterable_item_added', change_level, local_tree=local_tree) else: parents_ids_added = add_to_frozen_set(parents_ids, item_id) self._diff(change_level, parents_ids_added, local_tree=local_tree) for hash_value in hashes_removed: if self._count_diff() is StopIteration: return # pragma: no cover. This is already covered for addition. other = get_other_pair(hash_value, in_t1=False) item_id = id(other.item) index = t1_hashtable[hash_value].indexes[0] index2 = t1_hashtable[hash_value].indexes[0] if other.item is notpresent else other.indexes[0] change_level = level.branch_deeper( t1_hashtable[hash_value].item, other.item, child_relationship_class=SubscriptableIterableRelationship, child_relationship_param=index, child_relationship_param2=index2, ) if other.item is notpresent: self._report_result('iterable_item_removed', change_level, local_tree=local_tree) else: # Just like the case when report_repetition = True, these lines never run currently. # However they will stay here in case things change in future. parents_ids_added = add_to_frozen_set(parents_ids, item_id) # pragma: no cover. self._diff(change_level, parents_ids_added, local_tree=local_tree) # pragma: no cover. def _diff_booleans(self, level, local_tree=None): if level.t1 != level.t2: self._report_result('values_changed', level, local_tree=local_tree) def _diff_numbers(self, level, local_tree=None, report_type_change=True): """Diff Numbers""" if report_type_change: t1_type = "number" if self.ignore_numeric_type_changes else level.t1.__class__.__name__ t2_type = "number" if self.ignore_numeric_type_changes else level.t2.__class__.__name__ else: t1_type = t2_type = '' if self.use_log_scale: if not logarithmic_similarity(level.t1, level.t2, threshold=self.log_scale_similarity_threshold): self._report_result('values_changed', level, local_tree=local_tree) elif self.math_epsilon is not None: if not is_close(level.t1, level.t2, abs_tol=self.math_epsilon): self._report_result('values_changed', level, local_tree=local_tree) elif self.significant_digits is None: if level.t1 != level.t2: self._report_result('values_changed', level, local_tree=local_tree) else: # Bernhard10: I use string formatting for comparison, to be consistent with usecases where # data is read from files that were previously written from python and # to be consistent with on-screen representation of numbers. # Other options would be abs(t1-t2)<10**-self.significant_digits # or math.is_close (python3.5+) # Note that abs(3.25-3.251) = 0.0009999999999998899 < 0.001 # Note also that "{:.3f}".format(1.1135) = 1.113, but "{:.3f}".format(1.11351) = 1.114 # For Decimals, format seems to round 2.5 to 2 and 3.5 to 4 (to closest even number) t1_s = self.number_to_string(level.t1, significant_digits=self.significant_digits, number_format_notation=self.number_format_notation) # type: ignore t2_s = self.number_to_string(level.t2, significant_digits=self.significant_digits, number_format_notation=self.number_format_notation) # type: ignore t1_s = KEY_TO_VAL_STR.format(t1_type, t1_s) t2_s = KEY_TO_VAL_STR.format(t2_type, t2_s) if t1_s != t2_s: self._report_result('values_changed', level, local_tree=local_tree) def _diff_ipranges(self, level, local_tree=None): """Diff IP ranges""" if str(level.t1) != str(level.t2): self._report_result('values_changed', level, local_tree=local_tree) def _diff_datetime(self, level, local_tree=None): """Diff DateTimes""" level.t1 = datetime_normalize(self.truncate_datetime, level.t1, default_timezone=self.default_timezone) level.t2 = datetime_normalize(self.truncate_datetime, level.t2, default_timezone=self.default_timezone) if level.t1 != level.t2: self._report_result('values_changed', level, local_tree=local_tree) def _diff_time(self, level, local_tree=None): """Diff DateTimes""" if self.truncate_datetime: level.t1 = datetime_normalize(self.truncate_datetime, level.t1, default_timezone=self.default_timezone) level.t2 = datetime_normalize(self.truncate_datetime, level.t2, default_timezone=self.default_timezone) if level.t1 != level.t2: self._report_result('values_changed', level, local_tree=local_tree) def _diff_uuids(self, level, local_tree=None): """Diff UUIDs""" if level.t1.int != level.t2.int: self._report_result('values_changed', level, local_tree=local_tree) def _diff_numpy_array(self, level, parents_ids=frozenset(), local_tree=None): """Diff numpy arrays""" if level.path() not in self._numpy_paths: self._numpy_paths[level.path()] = get_type(level.t2).__name__ if np is None: # This line should never be run. If it is ever called means the type check detected a numpy array # which means numpy module needs to be available. So np can't be None. raise ImportError(CANT_FIND_NUMPY_MSG) # pragma: no cover if (self.ignore_order_func and not self.ignore_order_func(level)) or not self.ignore_order: # fast checks if self.significant_digits is None: if np.array_equal(level.t1, level.t2, equal_nan=self.ignore_nan_inequality): return # all good else: try: np.testing.assert_almost_equal(level.t1, level.t2, decimal=self.significant_digits) except TypeError: np.array_equal(level.t1, level.t2, equal_nan=self.ignore_nan_inequality) except AssertionError: pass # do detailed checking below else: return # all good # compare array meta-data _original_type = level.t1.dtype if level.t1.shape != level.t2.shape: # arrays are converted to python lists so that certain features of DeepDiff can apply on them easier. # They will be converted back to Numpy at their final dimension. level.t1 = level.t1.tolist() level.t2 = level.t2.tolist() self._diff_iterable(level, parents_ids, _original_type=_original_type, local_tree=local_tree) else: # metadata same -- the difference is in the content shape = level.t1.shape dimensions = len(shape) if dimensions == 1: self._diff_iterable(level, parents_ids, _original_type=_original_type, local_tree=local_tree) elif (self.ignore_order_func and self.ignore_order_func(level)) or self.ignore_order: # arrays are converted to python lists so that certain features of DeepDiff can apply on them easier. # They will be converted back to Numpy at their final dimension. level.t1 = level.t1.tolist() level.t2 = level.t2.tolist() self._diff_iterable_with_deephash(level, parents_ids, _original_type=_original_type, local_tree=local_tree) else: for (t1_path, t1_row), (t2_path, t2_row) in zip( get_numpy_ndarray_rows(level.t1, shape), get_numpy_ndarray_rows(level.t2, shape)): new_level = level.branch_deeper( t1_row, t2_row, child_relationship_class=NumpyArrayRelationship, child_relationship_param=t1_path, child_relationship_param2=t2_path, ) self._diff_iterable_in_order(new_level, parents_ids, _original_type=_original_type, local_tree=local_tree) def _diff_types(self, level, local_tree=None): """Diff types""" level.report_type = 'type_changes' self._report_result('type_changes', level, local_tree=local_tree) def _count_diff(self): if (self.max_diffs is not None and self._stats[DIFF_COUNT] > self.max_diffs): if not self._stats[MAX_DIFF_LIMIT_REACHED]: self._stats[MAX_DIFF_LIMIT_REACHED] = True logger.warning(MAX_DIFFS_REACHED_MSG.format(self.max_diffs)) return StopIteration self._stats[DIFF_COUNT] += 1 if self.cache_size and self.cache_tuning_sample_size: self._auto_tune_cache() def _auto_tune_cache(self): take_sample = (self._stats[DIFF_COUNT] % self.cache_tuning_sample_size == 0) if self.cache_tuning_sample_size: if self._stats[DISTANCE_CACHE_ENABLED]: if take_sample: self._auto_off_cache() # Turn on the cache once in a while elif self._stats[DIFF_COUNT] % self._shared_parameters[_ENABLE_CACHE_EVERY_X_DIFF] == 0: self.progress_logger('Re-enabling the distance and level caches.') # decreasing the sampling frequency self._shared_parameters[_ENABLE_CACHE_EVERY_X_DIFF] *= 10 self._stats[DISTANCE_CACHE_ENABLED] = True if take_sample: for key in (PREVIOUS_DIFF_COUNT, PREVIOUS_DISTANCE_CACHE_HIT_COUNT): self._stats[key] = self._stats[key[9:]] def _auto_off_cache(self): """ Auto adjust the cache based on the usage """ if self._stats[DISTANCE_CACHE_ENABLED]: angle = (self._stats[DISTANCE_CACHE_HIT_COUNT] - self._stats['PREVIOUS {}'.format(DISTANCE_CACHE_HIT_COUNT)]) / (self._stats[DIFF_COUNT] - self._stats[PREVIOUS_DIFF_COUNT]) if angle < self.CACHE_AUTO_ADJUST_THRESHOLD: self._stats[DISTANCE_CACHE_ENABLED] = False self.progress_logger('Due to minimal cache hits, {} is disabled.'.format('distance cache')) def _use_custom_operator(self, level): """ For each level we check all custom operators. If any one of them was a match for the level, we run the diff of the operator. If the operator returned True, the operator must have decided these objects should not be compared anymore. It might have already reported their results. In that case the report will appear in the final results of this diff. Otherwise basically the 2 objects in the level are being omitted from the results. """ for operator in self.custom_operators: if operator.match(level): prevent_default = operator.give_up_diffing(level=level, diff_instance=self) if prevent_default: return True return False def _diff(self, level, parents_ids=frozenset(), _original_type=None, local_tree=None): """ The main diff method **parameters** level: the tree level or tree node parents_ids: the ids of all the parent objects in the tree from the current node. _original_type: If the objects had an original type that was different than what currently exists in the level.t1 and t2 """ if self._count_diff() is StopIteration: return if self._use_custom_operator(level): return if level.t1 is level.t2: return if self._skip_this(level): return report_type_change = True if get_type(level.t1) != get_type(level.t2): for type_group in self.ignore_type_in_groups: if self.type_check_func(level.t1, type_group) and self.type_check_func(level.t2, type_group): report_type_change = False break if self.use_enum_value and isinstance(level.t1, Enum): level.t1 = level.t1.value report_type_change = False if self.use_enum_value and isinstance(level.t2, Enum): level.t2 = level.t2.value report_type_change = False if report_type_change: self._diff_types(level, local_tree=local_tree) return # This is an edge case where t1=None or t2=None and None is in the ignore type group. if level.t1 is None or level.t2 is None: self._report_result('values_changed', level, local_tree=local_tree) return if self.ignore_nan_inequality and isinstance(level.t1, (float, np_floating)) and str(level.t1) == str(level.t2) == 'nan': return if isinstance(level.t1, booleans): self._diff_booleans(level, local_tree=local_tree) elif isinstance(level.t1, strings): # Special handling when comparing string with UUID and ignore_uuid_types is True if self.ignore_uuid_types and isinstance(level.t2, uuids): try: # Convert string to UUID for comparison t1_uuid = uuid.UUID(level.t1) if t1_uuid.int != level.t2.int: self._report_result('values_changed', level, local_tree=local_tree) except (ValueError, AttributeError): # If string is not a valid UUID, report as changed self._report_result('values_changed', level, local_tree=local_tree) else: self._diff_str(level, local_tree=local_tree) elif isinstance(level.t1, datetime.datetime): self._diff_datetime(level, local_tree=local_tree) elif isinstance(level.t1, ipranges): self._diff_ipranges(level, local_tree=local_tree) elif isinstance(level.t1, (datetime.date, datetime.timedelta, datetime.time)): self._diff_time(level, local_tree=local_tree) elif isinstance(level.t1, uuids): # Special handling when comparing UUID with string and ignore_uuid_types is True if self.ignore_uuid_types and isinstance(level.t2, str): try: # Convert string to UUID for comparison t2_uuid = uuid.UUID(level.t2) if level.t1.int != t2_uuid.int: self._report_result('values_changed', level, local_tree=local_tree) except (ValueError, AttributeError): # If string is not a valid UUID, report as changed self._report_result('values_changed', level, local_tree=local_tree) else: self._diff_uuids(level, local_tree=local_tree) elif isinstance(level.t1, numbers): self._diff_numbers(level, local_tree=local_tree, report_type_change=report_type_change) elif isinstance(level.t1, Mapping): self._diff_dict(level, parents_ids, local_tree=local_tree) elif isinstance(level.t1, tuple): self._diff_tuple(level, parents_ids, local_tree=local_tree) elif isinstance(level.t1, (set, frozenset, SetOrdered)): self._diff_set(level, local_tree=local_tree) elif isinstance(level.t1, np_ndarray): self._diff_numpy_array(level, parents_ids, local_tree=local_tree) elif isinstance(level.t1, PydanticBaseModel): self._diff_obj(level, parents_ids, local_tree=local_tree, is_pydantic_object=True) elif isinstance(level.t1, Iterable): self._diff_iterable(level, parents_ids, _original_type=_original_type, local_tree=local_tree) elif isinstance(level.t1, Enum): self._diff_enum(level, parents_ids, local_tree=local_tree) else: self._diff_obj(level, parents_ids) def _get_view_results(self, view, verbose_level=None): """ Get the results based on the view """ result = self.tree if not self.report_repetition: # and self.is_root: result.mutual_add_removes_to_become_value_changes() if view == TREE_VIEW: pass elif view == TEXT_VIEW: effective_verbose_level = verbose_level if verbose_level is not None else self.verbose_level result = TextResult(tree_results=self.tree, verbose_level=effective_verbose_level) result.remove_empty_keys() elif view == DELTA_VIEW: result = self._to_delta_dict(report_repetition_required=False) elif view == COLORED_VIEW: result = ColoredView(t2=self.t2, tree_result=self.tree, compact=False) elif view == COLORED_COMPACT_VIEW: result = ColoredView(t2=self.t2, tree_result=self.tree, compact=True) else: raise ValueError(INVALID_VIEW_MSG.format(view)) return result @staticmethod def _get_key_for_group_by(row, group_by, item_name): """ Get the key value to group a row by, using the specified group_by parameter. Example >>> row = {'first': 'John', 'middle': 'Joe', 'last': 'Smith'} >>> DeepDiff._get_key_for_group_by(row, 'first', 't1') 'John' >>> nested_row = {'id': 123, 'demographics': {'names': {'first': 'John', 'middle': 'Joe', 'last': 'Smith'}}} >>> group_by = lambda x: x['demographics']['names']['first'] >>> DeepDiff._get_key_for_group_by(nested_row, group_by, 't1') 'John' Args: row (dict): The dictionary (row) to extract the group by key from. group_by (str or callable): The key name or function to call to get to the key value to group by. item_name (str): The name of the item, used for error messages. Returns: str: The key value to group by. Raises: KeyError: If the specified key is not found in the row. """ try: if callable(group_by): return group_by(row) return row.pop(group_by) except KeyError: logger.error("Unable to group {} by {}. The key is missing in {}".format(item_name, group_by, row)) raise def _group_iterable_to_dict(self, item, group_by, item_name): """ Convert a list of dictionaries into a dictionary of dictionaries where the key is the value of the group_by key in each dictionary. """ group_by_level2 = None if isinstance(group_by, (list, tuple)): group_by_level1 = group_by[0] if len(group_by) > 1: group_by_level2 = group_by[1] else: group_by_level1 = group_by if isinstance(item, Iterable) and not isinstance(item, Mapping): result = {} item_copy = deepcopy(item) for row in item_copy: if isinstance(row, Mapping): key1 = self._get_key_for_group_by(row, group_by_level1, item_name) # Track keys created by group_by to avoid type prefixing later if hasattr(self, 'group_by_keys'): self.group_by_keys.add(key1) if group_by_level2: key2 = self._get_key_for_group_by(row, group_by_level2, item_name) # Track level 2 keys as well if hasattr(self, 'group_by_keys'): self.group_by_keys.add(key2) if key1 not in result: result[key1] = {} if self.group_by_sort_key: if key2 not in result[key1]: result[key1][key2] = [] result_key1_key2 = result[key1][key2] if row not in result_key1_key2: result_key1_key2.append(row) else: result[key1][key2] = row else: if self.group_by_sort_key: if key1 not in result: result[key1] = [] if row not in result[key1]: result[key1].append(row) else: result[key1] = row else: msg = "Unable to group {} by {} since the item {} is not a dictionary.".format(item_name, group_by_level1, row) logger.error(msg) raise ValueError(msg) if self.group_by_sort_key: if group_by_level2: for key1, row1 in result.items(): for key2, row in row1.items(): row.sort(key=self.group_by_sort_key) else: for key, row in result.items(): row.sort(key=self.group_by_sort_key) return result msg = "Unable to group {} by {}".format(item_name, group_by) logger.error(msg) raise ValueError(msg) def get_stats(self): """ Get some stats on internals of the DeepDiff run. """ return self._stats @property def affected_paths(self): """ Get the list of paths that were affected. Whether a value was changed or they were added or removed. Example >>> from pprint import pprint >>> t1 = {1: 1, 2: 2, 3: [3], 4: 4} >>> t2 = {1: 1, 2: 4, 3: [3, 4], 5: 5, 6: 6} >>> ddiff = DeepDiff(t1, t2) >>> pprint(ddiff, indent=4) { 'dictionary_item_added': ['root[5]', 'root[6]'], 'dictionary_item_removed': ['root[4]'], 'iterable_item_added': {'root[3][1]': 4}, 'values_changed': {'root[2]': {'new_value': 4, 'old_value': 2}}} >>> sorted(ddiff.affected_paths) ['root[2]', 'root[3][1]', 'root[4]', 'root[5]', 'root[6]'] >>> sorted(ddiff.affected_root_keys) [2, 3, 4, 5, 6] """ result = SetOrdered() for key in REPORT_KEYS: value = self.get(key) if value: if isinstance(value, SetOrdered): result |= value else: result |= SetOrdered(value.keys()) return result @property def affected_root_keys(self): """ Get the list of root keys that were affected. Whether a value was changed or they were added or removed. Example >>> from pprint import pprint >>> t1 = {1: 1, 2: 2, 3: [3], 4: 4} >>> t2 = {1: 1, 2: 4, 3: [3, 4], 5: 5, 6: 6} >>> ddiff = DeepDiff(t1, t2) >>> pprint(ddiff, indent=4) { 'dictionary_item_added': ['root[5]', 'root[6]'], 'dictionary_item_removed': ['root[4]'], 'iterable_item_added': {'root[3][1]': 4}, 'values_changed': {'root[2]': {'new_value': 4, 'old_value': 2}}} >>> sorted(ddiff.affected_paths) ['root[2]', 'root[3][1]', 'root[4]', 'root[5]', 'root[6]'] >>> sorted(ddiff.affected_root_keys) [2, 3, 4, 5, 6] """ result = SetOrdered() for key in REPORT_KEYS: value = self.tree.get(key) if value: if isinstance(value, SetOrdered): values_list = value else: values_list = value.keys() for item in values_list: root_key = item.get_root_key() if root_key is not notpresent: result.add(root_key) return result def __str__(self): if hasattr(self, '_colored_view') and self.view in {COLORED_VIEW, COLORED_COMPACT_VIEW}: return str(self._colored_view) return super().__str__() if __name__ == "__main__": # pragma: no cover import doctest doctest.testmod() qlustered-deepdiff-41c7265/deepdiff/distance.py000066400000000000000000000336141516241264500214720ustar00rootroot00000000000000import math import datetime from typing import TYPE_CHECKING, Callable, Protocol, Any, Union, Optional from deepdiff.deephash import DeepHash from deepdiff.helper import ( DELTA_VIEW, numbers, strings, add_to_frozen_set, not_found, only_numbers, np, np_float64, time_to_seconds, cartesian_product_numpy, np_ndarray, np_array_factory, get_homogeneous_numpy_compatible_type_of_seq, dict_, CannotCompare, NumberType) from collections.abc import Mapping, Iterable if TYPE_CHECKING: from deepdiff.diff import DeepDiffProtocol class DistanceProtocol(DeepDiffProtocol, Protocol): hashes: dict deephash_parameters: dict ignore_numeric_type_changes: bool iterable_compare_func: Optional[Callable] math_epsilon: Optional[float] cutoff_distance_for_pairs: float def __get_item_rough_length(self, item, parent:str="root") -> float: ... def _to_delta_dict( self, directed: bool = True, report_repetition_required: bool = True, always_include_values: bool = False, ) -> dict: ... def __calculate_item_deephash(self, item: Any) -> None: ... DISTANCE_CALCS_NEEDS_CACHE = "Distance calculation can not happen once the cache is purged. Try with _cache='keep'" class DistanceMixin: def _get_rough_distance(self: "DistanceProtocol"): """ Gives a numeric value for the distance of t1 and t2 based on how many operations are needed to convert one to the other. This is a similar concept to the Levenshtein Edit Distance but for the structured data and it is designed to be between 0 and 1. A distance of zero means the objects are equal and a distance of 1 is very far. Note: The distance calculation formula is subject to change in future. Use the distance results only as a way of comparing the distances of pairs of items with other pairs rather than an absolute distance such as the one provided by Levenshtein edit distance. Info: The current algorithm is based on the number of operations that are needed to convert t1 to t2 divided by the number of items that make up t1 and t2. """ _distance = get_numeric_types_distance( self.t1, self.t2, max_=self.cutoff_distance_for_pairs, use_log_scale=self.use_log_scale, log_scale_similarity_threshold=self.log_scale_similarity_threshold) if _distance is not not_found: return _distance item = self if self.view == DELTA_VIEW else self._to_delta_dict(report_repetition_required=False) diff_length = _get_item_length(item) if diff_length == 0: return 0 t1_len = self.__get_item_rough_length(self.t1) t2_len = self.__get_item_rough_length(self.t2) return diff_length / (t1_len + t2_len) def __get_item_rough_length(self: "DistanceProtocol", item, parent='root'): """ Get the rough length of an item. It is used as a part of calculating the rough distance between objects. **parameters** item: The item to calculate the rough length for parent: It is only used for DeepHash reporting purposes. Not really useful here. """ if not hasattr(self, 'hashes'): raise RuntimeError(DISTANCE_CALCS_NEEDS_CACHE) length = DeepHash.get_key(self.hashes, key=item, default=None, extract_index=1, ignore_numeric_type_changes=self.ignore_numeric_type_changes) if length is None: self.__calculate_item_deephash(item) length = DeepHash.get_key(self.hashes, key=item, default=None, extract_index=1, ignore_numeric_type_changes=self.ignore_numeric_type_changes) return length def __calculate_item_deephash(self: "DistanceProtocol", item: Any) -> None: DeepHash( item, hashes=self.hashes, parent='root', apply_hash=True, **self.deephash_parameters, ) def _precalculate_distance_by_custom_compare_func( self: "DistanceProtocol", hashes_added, hashes_removed, t1_hashtable, t2_hashtable, _original_type): pre_calced_distances = dict_() for added_hash in hashes_added: for removed_hash in hashes_removed: try: is_close_distance = self.iterable_compare_func(t2_hashtable[added_hash].item, t1_hashtable[removed_hash].item) except CannotCompare: pass else: if is_close_distance: # an arbitrary small distance if math_epsilon is not defined distance = self.math_epsilon or 0.000001 else: distance = 1 pre_calced_distances["{}--{}".format(added_hash, removed_hash)] = distance return pre_calced_distances def _precalculate_numpy_arrays_distance( self: "DistanceProtocol", hashes_added, hashes_removed, t1_hashtable, t2_hashtable, _original_type): # We only want to deal with 1D arrays. if isinstance(t2_hashtable[next(iter(hashes_added))].item, (np_ndarray, list)): return pre_calced_distances = dict_() added = [t2_hashtable[k].item for k in hashes_added] removed = [t1_hashtable[k].item for k in hashes_removed] if _original_type is None: added_numpy_compatible_type = get_homogeneous_numpy_compatible_type_of_seq(added) removed_numpy_compatible_type = get_homogeneous_numpy_compatible_type_of_seq(removed) if added_numpy_compatible_type and added_numpy_compatible_type == removed_numpy_compatible_type: _original_type = added_numpy_compatible_type if _original_type is None: return added = np_array_factory(added, dtype=_original_type) removed = np_array_factory(removed, dtype=_original_type) pairs = cartesian_product_numpy(added, removed) pairs_transposed = pairs.T distances = _get_numpy_array_distance( pairs_transposed[0], pairs_transposed[1], max_=self.cutoff_distance_for_pairs, use_log_scale=self.use_log_scale, log_scale_similarity_threshold=self.log_scale_similarity_threshold, ) i = 0 for added_hash in hashes_added: for removed_hash in hashes_removed: pre_calced_distances["{}--{}".format(added_hash, removed_hash)] = distances[i] i += 1 return pre_calced_distances def _get_item_length(item, parents_ids=frozenset([])): """ Get the number of operations in a diff object. It is designed mainly for the delta view output but can be used with other dictionary types of view outputs too. """ length = 0 if isinstance(item, Mapping): for key, subitem in item.items(): # dedupe the repetition report so the number of times items have shown up does not affect the distance. if key in {'iterable_items_added_at_indexes', 'iterable_items_removed_at_indexes'}: new_subitem = dict_() for path_, indexes_to_items in subitem.items(): used_value_ids = set() new_indexes_to_items = dict_() for k, v in indexes_to_items.items(): v_id = id(v) if v_id not in used_value_ids: used_value_ids.add(v_id) new_indexes_to_items[k] = v new_subitem[path_] = new_indexes_to_items subitem = new_subitem # internal keys such as _numpy_paths should not count towards the distance. # old_type and old_value are metadata about the previous state, not additional operations. if isinstance(key, strings) and (key.startswith('_') or key == 'deep_distance' or key == 'new_path' or key == 'old_type' or key == 'old_value'): continue item_id = id(subitem) if parents_ids and item_id in parents_ids: continue parents_ids_added = add_to_frozen_set(parents_ids, item_id) length += _get_item_length(subitem, parents_ids_added) elif isinstance(item, numbers): length = 1 elif isinstance(item, strings): length = 1 elif isinstance(item, Iterable): for subitem in item: item_id = id(subitem) if parents_ids and item_id in parents_ids: continue parents_ids_added = add_to_frozen_set(parents_ids, item_id) length += _get_item_length(subitem, parents_ids_added) elif isinstance(item, type): # it is a class length = 1 else: if hasattr(item, '__dict__'): for subitem in item.__dict__: item_id = id(subitem) parents_ids_added = add_to_frozen_set(parents_ids, item_id) length += _get_item_length(subitem, parents_ids_added) return length def _get_numbers_distance(num1, num2, max_=1, use_log_scale=False, log_scale_similarity_threshold=0.1): """ Get the distance of 2 numbers. The output is a number between 0 to the max. The reason is the When max is returned means the 2 numbers are really far, and 0 means they are equal. """ if num1 == num2: return 0 if use_log_scale: distance = logarithmic_distance(num1, num2) if distance < 0: return 0 return distance if not isinstance(num1, float): num1 = float(num1) if not isinstance(num2, float): num2 = float(num2) # Since we have a default cutoff of 0.3 distance when # getting the pairs of items during the ingore_order=True # calculations, we need to make the divisor of comparison very big # so that any 2 numbers can be chosen as pairs. divisor = (num1 + num2) / max_ if divisor == 0: return max_ try: return min(max_, abs((num1 - num2) / divisor)) except Exception: # pragma: no cover. I don't think this line will ever run but doesn't hurt to leave it. return max_ # pragma: no cover def _numpy_div(a, b, replace_inf_with=1): max_array = np.full(shape=a.shape, fill_value=replace_inf_with, dtype=np_float64) result = np.divide(a, b, out=max_array, where=b != 0, dtype=np_float64) # wherever 2 numbers are the same, make sure the distance is zero. This is mainly for 0 divided by zero. result[a == b] = 0 return result # To deal with numbers close to zero MATH_LOG_OFFSET = 1e-10 def numpy_apply_log_keep_sign(array, offset=MATH_LOG_OFFSET): # Calculate the absolute value and add the offset abs_plus_offset = np.abs(array) + offset # Calculate the logarithm log_values = np.log(abs_plus_offset) # Apply the original signs to the log values signed_log_values = np.copysign(log_values, array) return signed_log_values def logarithmic_similarity(a: NumberType, b: NumberType, threshold: float=0.1) -> bool: """ A threshold of 0.1 translates to about 10.5% difference. A threshold of 0.5 translates to about 65% difference. A threshold of 0.05 translates to about 5.1% difference. """ return logarithmic_distance(a, b) < threshold def logarithmic_distance(a: NumberType, b: NumberType) -> float: # Apply logarithm to the absolute values and consider the sign a = float(a) b = float(b) log_a = math.copysign(math.log(abs(a) + MATH_LOG_OFFSET), a) log_b = math.copysign(math.log(abs(b) + MATH_LOG_OFFSET), b) return abs(log_a - log_b) def _get_numpy_array_distance(num1, num2, max_=1, use_log_scale=False, log_scale_similarity_threshold=0.1): """ Get the distance of 2 numbers. The output is a number between 0 to the max. The reason is the When max is returned means the 2 numbers are really far, and 0 means they are equal. """ # Since we have a default cutoff of 0.3 distance when # getting the pairs of items during the ingore_order=True # calculations, we need to make the divisor of comparison very big # so that any 2 numbers can be chosen as pairs. if use_log_scale: num1 = numpy_apply_log_keep_sign(num1) num2 = numpy_apply_log_keep_sign(num2) divisor = (num1 + num2) / max_ result = _numpy_div((num1 - num2), divisor, replace_inf_with=max_) distance_array = np.clip(np.absolute(result), 0, max_) if use_log_scale: distance_array[distance_array < log_scale_similarity_threshold] = 0 return distance_array def _get_datetime_distance(date1, date2, max_, use_log_scale, log_scale_similarity_threshold): return _get_numbers_distance(date1.timestamp(), date2.timestamp(), max_) def _get_date_distance(date1, date2, max_, use_log_scale, log_scale_similarity_threshold): return _get_numbers_distance(date1.toordinal(), date2.toordinal(), max_) def _get_timedelta_distance(timedelta1, timedelta2, max_, use_log_scale, log_scale_similarity_threshold): return _get_numbers_distance(timedelta1.total_seconds(), timedelta2.total_seconds(), max_) def _get_time_distance(time1, time2, max_, use_log_scale, log_scale_similarity_threshold): return _get_numbers_distance(time_to_seconds(time1), time_to_seconds(time2), max_) TYPES_TO_DIST_FUNC = [ (only_numbers, _get_numbers_distance), (datetime.datetime, _get_datetime_distance), (datetime.date, _get_date_distance), (datetime.timedelta, _get_timedelta_distance), (datetime.time, _get_time_distance), ] def get_numeric_types_distance(num1, num2, max_, use_log_scale=False, log_scale_similarity_threshold=0.1): for type_, func in TYPES_TO_DIST_FUNC: if isinstance(num1, type_) and isinstance(num2, type_): return func(num1, num2, max_, use_log_scale, log_scale_similarity_threshold) return not_found qlustered-deepdiff-41c7265/deepdiff/docstrings/000077500000000000000000000000001516241264500214765ustar00rootroot00000000000000qlustered-deepdiff-41c7265/deepdiff/docstrings/authors.rst000066400000000000000000000216561516241264500237270ustar00rootroot00000000000000:doc:`/index` Authors ======= Authors in order of the timeline of their contributions: - `Sep Dehpour (Seperman)`_ - `Victor Hahn Castell`_ for the tree view and major contributions: - `nfvs`_ for Travis-CI setup script. - `brbsix`_ for initial Py3 porting. - `WangFenjin`_ for unicode support. - `timoilya`_ for comparing list of sets when ignoring order. - `Bernhard10`_ for significant digits comparison. - `b-jazz`_ for PEP257 cleanup, Standardize on full names, fixing line endings. - `finnhughes`_ for fixing **slots** - `moloney`_ for Unicode vs. Bytes default - `serv-inc`_ for adding help(deepdiff) - `movermeyer`_ for updating docs - `maxrothman`_ for search in inherited class attributes - `maxrothman`_ for search for types/objects - `MartyHub`_ for exclude regex paths - `sreecodeslayer`_ for DeepSearch match_string - Brian Maissy `brianmaissy`_ for weakref fix, enum tests - Bartosz Borowik `boba-2`_ for Exclude types fix when ignoring order - Brian Maissy `brianmaissy `__ for fixing classes which inherit from classes with slots didn’t have all of their slots compared - Juan Soler `Soleronline`_ for adding ignore_type_number - `mthaddon`_ for adding timedelta diffing support - `Necrophagos`_ for Hashing of the number 1 vs. True - `gaal-dev`_ for adding exclude_obj_callback - Ivan Piskunov `van-ess0`_ for deprecation warning enhancement. - Michał Karaś `MKaras93`_ for the pretty view - Christian Kothe `chkothe`_ for the basic support for diffing numpy arrays - `Timothy`_ for truncate_datetime - `d0b3rm4n`_ for bugfix to not apply format to non numbers. - `MyrikLD`_ for Bug Fix NoneType in ignore type groups - Stian Jensen `stianjensen`_ for improving ignoring of NoneType in diff - Florian Klien `flowolf`_ for adding math_epsilon - Tim Klein `timjklein36`_ for retaining the order of multiple dictionary items added via Delta. - Wilhelm Schürmann\ `wbsch`_ for fixing the typo with yml files. - `lyz-code`_ for adding support for regular expressions in DeepSearch and strict_checking feature in DeepSearch. - `dtorres-sf`_ for adding the option for custom compare function - Tony Wang `Tony-Wang`_ for bugfix: verbose_level==0 should disable values_changes. - Sun Ao `eggachecat`_ for adding custom operators. - Sun Ao `eggachecat`_ for adding ignore_order_func. - `SlavaSkvortsov`_ for fixing unprocessed key error. - Håvard Thom `havardthom`_ for adding UUID support. - Dhanvantari Tilak `Dhanvantari`_ for Bug-Fix: ``TypeError in _get_numbers_distance() when ignore_order = True``. - Yael Mintz `yaelmi3`_ for detailed pretty print when verbose_level=2. - Mikhail Khviyuzov `mskhviyu`_ for Exclude obj callback strict. - `dtorres-sf`_ for the fix for diffing using iterable_compare_func with nested objects. - `Enric Pou `__ for bug fix of ValueError when using Decimal 0.x - `Uwe Fladrich `__ for fixing bug when diff'ing non-sequence iterables - `Michal Ozery-Flato `__ for setting equal_nan=ignore_nan_inequality in the call for np.array_equal - `martin-kokos `__ for using Pytest’s tmp_path fixture instead of /tmp/ - Håvard Thom `havardthom `__ for adding include_obj_callback and include_obj_callback_strict. - `Noam Gottlieb `__ for fixing a corner case where numpy’s ``np.float32`` nans are not ignored when using ``ignore_nan_equality``. - `maggelus `__ for the bugfix deephash for paths. - `maggelus `__ for the bugfix deephash compiled regex. - `martin-kokos `__ for fixing the tests dependent on toml. - `kor4ik `__ for the bugfix for ``include_paths`` for nested dictionaries. - `martin-kokos `__ for using tomli and tomli-w for dealing with tomli files. - `Alex Sauer-Budge `__ for the bugfix for ``datetime.date``. - `William Jamieson `__ for `NumPy 2.0 compatibility `__ - `Leo Sin `__ for Supporting Python 3.12 in the build process - `sf-tcalhoun `__ for fixing “Instantiating a Delta with a flat_dict_list unexpectedly mutates the flat_dict_list” - `dtorres-sf `__ for fixing iterable moved items when iterable_compare_func is used. - `Florian Finkernagel `__ for pandas and polars support. - Mathis Chenuet `artemisart `__ for fixing slots classes comparison and PR review. - Sherjeel Shabih `sherjeelshabih `__ for fixing the issue where the key deep_distance is not returned when both compared items are equal #510 - `Juergen Skrotzky `__ for adding empty ``py.typed`` - `Mate Valko `__ for fixing the issue so we lower only if clean_key is instance of str via #504 - `jlaba `__ for fixing #493 include_paths, when only certain keys are included via #499 - `Doron Behar `__ for fixing DeepHash for numpy booleans via #496 - `Aaron D. Marasco `__ for adding print() options which allows a user-defined string (or callback function) to prefix every output when using the pretty() call. - `David Hotham `__ for relaxing orderly-set dependency via #486 - `dtorres-sf `__ for the fix for moving nested tables when using iterable_compare_func. - `Jim Cipar `__ for the fix recursion depth limit when hashing numpy.datetime64 - `Enji Cooper `__ for converting legacy setuptools use to pyproject.toml - `Diogo Correia `__ for reporting security vulnerability in Delta and DeepDiff that could allow remote code execution. - `am-periphery `__ for reporting CVE-2026-33155: denial-of-service via crafted pickle payloads triggering massive memory allocation. - `echan5 `__ for adding callable ``group_by`` support. - `yannrouillard `__ for fixing colored view display when all list items are removed. - `tpvasconcelos `__ for fixing ``__slots__`` handling for objects with ``__getattr__``. - `devin13cox `__ for always using t1 path for reporting. - `vitalis89 `__ for fixing ``ignore_keys`` issue in ``detailed__dict__``. - `ljames8 `__ for fixing logarithmic similarity type hint. - `srini047 `__ for fixing README typo. - `Nagato-Yuzuru `__ for colored view tests. - `akshat62 `__ for adding Fraction numeric support. .. _Sep Dehpour (Seperman): http://www.zepworks.com .. _Victor Hahn Castell: http://hahncastell.de .. _nfvs: https://github.com/nfvs .. _brbsix: https://github.com/brbsix .. _WangFenjin: https://github.com/WangFenjin .. _timoilya: https://github.com/timoilya .. _Bernhard10: https://github.com/Bernhard10 .. _b-jazz: https://github.com/b-jazz .. _finnhughes: https://github.com/finnhughes .. _moloney: https://github.com/moloney .. _serv-inc: https://github.com/serv-inc .. _movermeyer: https://github.com/movermeyer .. _maxrothman: https://github.com/maxrothman .. _MartyHub: https://github.com/MartyHub .. _sreecodeslayer: https://github.com/sreecodeslayer .. _brianmaissy: https://github.com/ .. _boba-2: https://github.com/boba-2 .. _Soleronline: https://github.com/Soleronline .. _mthaddon: https://github.com/mthaddon .. _Necrophagos: https://github.com/Necrophagos .. _gaal-dev: https://github.com/gaal-dev .. _van-ess0: https://github.com/van-ess0 .. _MKaras93: https://github.com/MKaras93 .. _chkothe: https://github.com/chkothe .. _Timothy: https://github.com/timson .. _d0b3rm4n: https://github.com/d0b3rm4n .. _MyrikLD: https://github.com/MyrikLD .. _stianjensen: https://github.com/stianjensen .. _flowolf: https://github.com/flowolf .. _timjklein36: https://github.com/timjklein36 .. _wbsch: https://github.com/wbsch .. _lyz-code: https://github.com/lyz-code .. _dtorres-sf: https://github.com/dtorres-sf .. _Tony-Wang: https://github.com/Tony-Wang .. _eggachecat: https://github.com/eggachecat .. _SlavaSkvortsov: https://github.com/SlavaSkvortsov .. _havardthom: https://github.com/havardthom .. _Dhanvantari: https://github.com/Dhanvantari .. _yaelmi3: https://github.com/yaelmi3 .. _mskhviyu: https://github.com/mskhviyu Thank you for contributing to DeepDiff! Back to :doc:`/index` qlustered-deepdiff-41c7265/deepdiff/docstrings/basics.rst000066400000000000000000000344211516241264500235000ustar00rootroot00000000000000:doc:`/index` Basics ====== Importing >>> from deepdiff import DeepDiff >>> from pprint import pprint Same object returns empty >>> t1 = {1:1, 2:2, 3:3} >>> t2 = t1 >>> print(DeepDiff(t1, t2)) {} Type of an item has changed >>> t1 = {1:1, 2:2, 3:3} >>> t2 = {1:1, 2:"2", 3:3} >>> pprint(DeepDiff(t1, t2), indent=2) { 'type_changes': { 'root[2]': { 'new_type': , 'new_value': '2', 'old_type': , 'old_value': 2}}} Value of an item has changed >>> t1 = {1:1, 2:2, 3:3} >>> t2 = {1:1, 2:4, 3:3} >>> pprint(DeepDiff(t1, t2, verbose_level=0), indent=2) {'values_changed': {'root[2]': {'new_value': 4, 'old_value': 2}}} Item added and/or removed >>> t1 = {1:1, 3:3, 4:4} >>> t2 = {1:1, 3:3, 5:5, 6:6} >>> ddiff = DeepDiff(t1, t2) >>> pprint (ddiff) {'dictionary_item_added': [root[5], root[6]], 'dictionary_item_removed': [root[4]]} Set verbose level to 2 in order to see the added or removed items with their values >>> t1 = {1:1, 3:3, 4:4} >>> t2 = {1:1, 3:3, 5:5, 6:6} >>> ddiff = DeepDiff(t1, t2, verbose_level=2) >>> pprint(ddiff, indent=2) { 'dictionary_item_added': {'root[5]': 5, 'root[6]': 6}, 'dictionary_item_removed': {'root[4]': 4}} Set verbose level to 2 includes new_path when the path has changed for a report between t1 and t2: >>> t1 = [1, 3] >>> t2 = [3, 2] >>> >>> >>> diff = DeepDiff(t1, t2, ignore_order=True, verbose_level=2) >>> pprint(diff) {'values_changed': {'root[0]': {'new_path': 'root[1]', 'new_value': 2, 'old_value': 1}}} String difference >>> t1 = {1:1, 2:2, 3:3, 4:{"a":"hello", "b":"world"}} >>> t2 = {1:1, 2:4, 3:3, 4:{"a":"hello", "b":"world!"}} >>> ddiff = DeepDiff(t1, t2) >>> pprint (ddiff, indent = 2) { 'values_changed': { 'root[2]': {'new_value': 4, 'old_value': 2}, "root[4]['b']": { 'new_value': 'world!', 'old_value': 'world'}}} String difference 2 >>> t1 = {1:1, 2:2, 3:3, 4:{"a":"hello", "b":"world!\nGoodbye!\n1\n2\nEnd"}} >>> t2 = {1:1, 2:2, 3:3, 4:{"a":"hello", "b":"world\n1\n2\nEnd"}} >>> ddiff = DeepDiff(t1, t2) >>> pprint (ddiff, indent = 2) { 'values_changed': { "root[4]['b']": { 'diff': '--- \n' '+++ \n' '@@ -1,5 +1,4 @@\n' '-world!\n' '-Goodbye!\n' '+world\n' ' 1\n' ' 2\n' ' End', 'new_value': 'world\n1\n2\nEnd', 'old_value': 'world!\n' 'Goodbye!\n' '1\n' '2\n' 'End'}}} >>> >>> print (ddiff['values_changed']["root[4]['b']"]["diff"]) --- +++ @@ -1,5 +1,4 @@ -world! -Goodbye! +world 1 2 End List difference >>> t1 = {1:1, 2:2, 3:3, 4:{"a":"hello", "b":[1, 2, 3, 4]}} >>> t2 = {1:1, 2:2, 3:3, 4:{"a":"hello", "b":[1, 2]}} >>> ddiff = DeepDiff(t1, t2) >>> pprint (ddiff, indent = 2) {'iterable_item_removed': {"root[4]['b'][2]": 3, "root[4]['b'][3]": 4}} List that contains dictionary: >>> t1 = {1:1, 2:2, 3:3, 4:{"a":"hello", "b":[1, 2, {1:1, 2:2}]}} >>> t2 = {1:1, 2:2, 3:3, 4:{"a":"hello", "b":[1, 2, {1:3}]}} >>> ddiff = DeepDiff(t1, t2) >>> pprint (ddiff, indent = 2) { 'dictionary_item_removed': [root[4]['b'][2][2]], 'values_changed': {"root[4]['b'][2][1]": {'new_value': 3, 'old_value': 1}}} Sets: >>> t1 = {1, 2, 8} >>> t2 = {1, 2, 3, 5} >>> ddiff = DeepDiff(t1, t2) >>> pprint(ddiff) {'set_item_added': [root[3], root[5]], 'set_item_removed': [root[8]]} Named Tuples: >>> from collections import namedtuple >>> Point = namedtuple('Point', ['x', 'y']) >>> t1 = Point(x=11, y=22) >>> t2 = Point(x=11, y=23) >>> pprint (DeepDiff(t1, t2)) {'values_changed': {'root.y': {'new_value': 23, 'old_value': 22}}} Custom objects: >>> class ClassA(object): ... a = 1 ... def __init__(self, b): ... self.b = b ... >>> t1 = ClassA(1) >>> t2 = ClassA(2) >>> >>> pprint(DeepDiff(t1, t2)) {'values_changed': {'root.b': {'new_value': 2, 'old_value': 1}}} Object attribute added: >>> t2.c = "new attribute" >>> pprint(DeepDiff(t1, t2)) {'attribute_added': [root.c], 'values_changed': {'root.b': {'new_value': 2, 'old_value': 1}}} Datetime DeepDiff converts all datetimes into UTC. If a datetime is timezone naive, we assume it is in UTC too. That is different than what Python does. Python assumes your timezone naive datetime is in your local timezone. >>> from deepdiff import DeepDiff >>> from datetime import datetime, timezone >>> d1 = datetime(2020, 8, 31, 13, 14, 1) >>> d2 = datetime(2020, 8, 31, 13, 14, 1, tzinfo=timezone.utc) >>> d1 == d2 False >>> DeepDiff(d1, d2) {} .. note:: All the examples above use the default :ref:`text_view_label`. If you want traversing functionality in the results, use the :ref:`tree_view_label`. You just need to set view='tree' to get it in tree form. .. _group_by_label: Group By -------- group_by can be used when dealing with the list of dictionaries. It converts them from lists to a single dictionary with the key defined by group_by. The common use case is when reading data from a flat CSV, and the primary key is one of the columns in the CSV. We want to use the primary key instead of the CSV row number to group the rows. The group_by can do 2D group_by by passing a list of 2 keys. It is also possible to have a callable group_by, which can be used to access keys in more nested data structures. For example: >>> [ ... {'id': 'AA', 'name': 'Joe', 'last_name': 'Nobody'}, ... {'id': 'BB', 'name': 'James', 'last_name': 'Blue'}, ... {'id': 'CC', 'name': 'Mike', 'last_name': 'Apple'}, ... ] Becomes: >>> t1 = { ... 'AA': {'name': 'Joe', 'last_name': 'Nobody'}, ... 'BB': {'name': 'James', 'last_name': 'Blue'}, ... 'CC': {'name': 'Mike', 'last_name': 'Apple'}, ... } With that in mind, let's take a look at the following: >>> from deepdiff import DeepDiff >>> t1 = [ ... {'id': 'AA', 'name': 'Joe', 'last_name': 'Nobody'}, ... {'id': 'BB', 'name': 'James', 'last_name': 'Blue'}, ... {'id': 'CC', 'name': 'Mike', 'last_name': 'Apple'}, ... ] >>> >>> t2 = [ ... {'id': 'AA', 'name': 'Joe', 'last_name': 'Nobody'}, ... {'id': 'BB', 'name': 'James', 'last_name': 'Brown'}, ... {'id': 'CC', 'name': 'Mike', 'last_name': 'Apple'}, ... ] >>> >>> DeepDiff(t1, t2) {'values_changed': {"root[1]['last_name']": {'new_value': 'Brown', 'old_value': 'Blue'}}} Now we use group_by='id': >>> DeepDiff(t1, t2, group_by='id') {'values_changed': {"root['BB']['last_name']": {'new_value': 'Brown', 'old_value': 'Blue'}}} .. note:: group_by actually changes the structure of the t1 and t2. You can see this by using the tree view: >>> diff = DeepDiff(t1, t2, group_by='id', view='tree') >>> diff {'values_changed': []} >>> diff['values_changed'][0] >>> diff['values_changed'][0].up >>> diff['values_changed'][0].up.up >>> diff['values_changed'][0].up.up.t1 {'AA': {'name': 'Joe', 'last_name': 'Nobody'}, 'BB': {'name': 'James', 'last_name': 'Blue'}, 'CC': {'name': 'Mike', 'last_name': 'Apple'}} 2D Example: >>> from pprint import pprint >>> from deepdiff import DeepDiff >>> >>> t1 = [ ... {'id': 'AA', 'name': 'Joe', 'last_name': 'Nobody'}, ... {'id': 'BB', 'name': 'James', 'last_name': 'Blue'}, ... {'id': 'BB', 'name': 'Jimmy', 'last_name': 'Red'}, ... {'id': 'CC', 'name': 'Mike', 'last_name': 'Apple'}, ... ] >>> >>> t2 = [ ... {'id': 'AA', 'name': 'Joe', 'last_name': 'Nobody'}, ... {'id': 'BB', 'name': 'James', 'last_name': 'Brown'}, ... {'id': 'CC', 'name': 'Mike', 'last_name': 'Apple'}, ... ] >>> >>> diff = DeepDiff(t1, t2, group_by=['id', 'name']) >>> pprint(diff) {'dictionary_item_removed': [root['BB']['Jimmy']], 'values_changed': {"root['BB']['James']['last_name']": {'new_value': 'Brown', 'old_value': 'Blue'}}} Callable group_by Example: >>> from deepdiff import DeepDiff >>> >>> t1 = [ ... {'id': 'AA', 'demographics': {'names': {'first': 'Joe', 'middle': 'John', 'last': 'Nobody'}}}, ... {'id': 'BB', 'demographics': {'names': {'first': 'James', 'middle': 'Joyce', 'last': 'Blue'}}}, ... {'id': 'CC', 'demographics': {'names': {'first': 'Mike', 'middle': 'Mark', 'last': 'Apple'}}}, ... ] >>> >>> t2 = [ ... {'id': 'AA', 'demographics': {'names': {'first': 'Joe', 'middle': 'John', 'last': 'Nobody'}}}, ... {'id': 'BB', 'demographics': {'names': {'first': 'James', 'middle': 'Joyce', 'last': 'Brown'}}}, ... {'id': 'CC', 'demographics': {'names': {'first': 'Mike', 'middle': 'Charles', 'last': 'Apple'}}}, ... ] >>> >>> diff = DeepDiff(t1, t2, group_by=lambda x: x['demographics']['names']['first']) >>> pprint(diff) {'values_changed': {"root['James']['demographics']['names']['last']": {'new_value': 'Brown', 'old_value': 'Blue'}, "root['Mike']['demographics']['names']['middle']": {'new_value': 'Charles', 'old_value': 'Mark'}}} .. _group_by_sort_key_label: Group By - Sort Key ------------------- group_by_sort_key is used to define how dictionaries are sorted if multiple ones fall under one group. When this parameter is used, group_by converts the lists of dictionaries into a dictionary of keys to lists of dictionaries. Then, group_by_sort_key is used to sort between the list. For example, there are duplicate id values. If we only use group_by='id', one of the dictionaries with id of 'BB' will overwrite the other. However, if we also set group_by_sort_key='name', we keep both dictionaries with the id of 'BB'. Example: >>> [{'id': 'AA', 'int_id': 2, 'last_name': 'Nobody', 'name': 'Joe'}, ... {'id': 'BB', 'int_id': 20, 'last_name': 'Blue', 'name': 'James'}, ... {'id': 'BB', 'int_id': 3, 'last_name': 'Red', 'name': 'Jimmy'}, ... {'id': 'CC', 'int_id': 4, 'last_name': 'Apple', 'name': 'Mike'}] Becomes: >>> {'AA': [{'int_id': 2, 'last_name': 'Nobody', 'name': 'Joe'}], ... 'BB': [{'int_id': 20, 'last_name': 'Blue', 'name': 'James'}, ... {'int_id': 3, 'last_name': 'Red', 'name': 'Jimmy'}], ... 'CC': [{'int_id': 4, 'last_name': 'Apple', 'name': 'Mike'}]} Example of using group_by_sort_key >>> t1 = [ ... {'id': 'AA', 'name': 'Joe', 'last_name': 'Nobody', 'int_id': 2}, ... {'id': 'BB', 'name': 'James', 'last_name': 'Blue', 'int_id': 20}, ... {'id': 'BB', 'name': 'Jimmy', 'last_name': 'Red', 'int_id': 3}, ... {'id': 'CC', 'name': 'Mike', 'last_name': 'Apple', 'int_id': 4}, ... ] >>> >>> t2 = [ ... {'id': 'AA', 'name': 'Joe', 'last_name': 'Nobody', 'int_id': 2}, ... {'id': 'BB', 'name': 'James', 'last_name': 'Brown', 'int_id': 20}, ... {'id': 'CC', 'name': 'Mike', 'last_name': 'Apple', 'int_id': 4}, ... ] >>> >>> diff = DeepDiff(t1, t2, group_by='id', group_by_sort_key='name') >>> >>> pprint(diff) {'iterable_item_removed': {"root['BB'][1]": {'int_id': 3, 'last_name': 'Red', 'name': 'Jimmy'}}, 'values_changed': {"root['BB'][0]['last_name']": {'new_value': 'Brown', 'old_value': 'Blue'}}} .. _default_timezone_label: Default Time Zone ----------------- default_timezone defines the default timezone. If a datetime is timezone naive, which means it doesn't have a timezone, we assume the datetime is in this timezone. Also any datetime that has a timezone will be converted to this timezone so the datetimes can be compared properly all in the same timezone. Note that Python's default behavior assumes the default timezone is your local timezone. DeepDiff's default is UTC, not your local time zone. Note that if we change the default_timezone, the output timezone changes accordingly >>> from deepdiff import DeepDiff >>> import pytz >>> from datetime import date, datetime, time, timezone >>> dt_utc = datetime(2025, 2, 3, 12, 0, 0, tzinfo=pytz.utc) # UTC timezone >>> dt_utc2 = datetime(2025, 2, 3, 11, 0, 0, tzinfo=pytz.utc) # UTC timezone >>> dt_ny = dt_utc.astimezone(pytz.timezone('America/New_York')) >>> dt_ny2 = dt_utc2.astimezone(pytz.timezone('America/New_York')) >>> diff = DeepDiff(dt_ny, dt_ny2) >>> diff {'values_changed': {'root': {'new_value': datetime.datetime(2025, 2, 3, 11, 0, tzinfo=datetime.timezone.utc), 'old_value': datetime.datetime(2025, 2, 3, 12, 0, tzinfo=datetime.timezone.utc)}}} >>> diff2 = DeepDiff(dt_ny, dt_ny2, default_timezone=pytz.timezone('America/New_York')) >>> diff2 {'values_changed': {'root': {'new_value': datetime.datetime(2025, 2, 3, 6, 0, tzinfo=), 'old_value': datetime.datetime(2025, 2, 3, 7, 0, tzinfo=)}}} Back to :doc:`/index` qlustered-deepdiff-41c7265/deepdiff/docstrings/changelog.rst000066400000000000000000000433331516241264500241650ustar00rootroot00000000000000:doc:`/index` Changelog ========= DeepDiff Changelog - v9-0-0 - migration note: - `to_dict()` and `to_json()` now accept a `verbose_level` parameter and always return a usable text-view dict. When the original view is `'tree'`, they default to `verbose_level=2` for full detail. The old `view_override` parameter is removed. To get the previous results, you will need to pass the explicit verbose_level to `to_json` and `to_dict` if you are using the tree view. - Dropping support for Python 3.9 - Support for python 3.14 - Added support for callable ``group_by`` thanks to `echan5 `__ - Added ``FlatDeltaDict`` TypedDict for ``to_flat_dicts`` return type - Fixed colored view display when all list items are removed thanks to `yannrouillard `__ - Fixed ``hasattr()`` swallowing ``AttributeError`` in ``__slots__`` handling for objects with ``__getattr__`` thanks to `tpvasconcelos `__ - Fixed ``ignore_order=True`` missing int-vs-float type changes - Always use t1 path for reporting thanks to `devin13cox `__ - Fixed ``_convert_oversized_ints`` failing on NamedTuples - Fixed orjson ``TypeError`` for integers exceeding 64-bit range - Fixed parameter bug in ``to_flat_dicts`` where ``include_action_in_path`` and ``report_type_changes`` were not being passed through - Fixed ``ignore_keys`` issue in ``detailed__dict__`` thanks to `vitalis89 `__ - Fixed logarithmic similarity type hint thanks to `ljames8 `__ - Added ``Fraction`` numeric support thanks to `akshat62 `__ - v8-6-2 - Security fix (CVE-2026-33155): Prevent denial-of-service via crafted pickle payloads that trigger massive memory allocation through the REDUCE opcode. Size-sensitive callables like ``bytes()`` and ``bytearray()`` are now wrapped to reject allocations exceeding 128 MB. - v8-6-1 - Patched security vulnerability in the Delta class which was vulnerable to class pollution via its constructor, and when combined with a gadget available in DeltaDiff itself, it could lead to Denial of Service and Remote Code Execution (via insecure Pickle deserialization). - v8-6-0 - Added Colored View thanks to @mauvilsa - Added support for applying deltas to NamedTuple thanks to @paulsc - Fixed test_delta.py with Python 3.14 thanks to @Romain-Geissler-1A - Added python property serialization to json - Added ip address serialization - Switched to UV from pip - Added Claude.md - Added uuid hashing thanks to @akshat62 - Added ``ignore_uuid_types`` flag to DeepDiff to avoid type reports when comparing UUID and string. - Added comprehensive type hints across the codebase (multiple commits for better type safety) - Added support for memoryview serialization - Added support for bytes serialization (non-UTF8 compatible) - Fixed bug where group_by with numbers would leak type info into group path reports - Fixed bug in ``_get_clean_to_keys_mapping`` without explicit significant digits - Added support for python dict key serialization - Enhanced support for IP address serialization with safe module imports - Added development tooling improvements (pyright config, .envrc example) - Updated documentation and development instructions - v8-5-0 - Updating deprecated pydantic calls - Switching to pyproject.toml - Fix for moving nested tables when using iterable_compare_func. by - Fix recursion depth limit when hashing numpy.datetime64 - Moving from legacy setuptools use to pyproject.toml - v8-4-2 - fixes the type hints for the base - fixes summarize so if json dumps fails, we can still get a repr of the results - adds ipaddress support - v8-4-1 - Adding BaseOperatorPlus base class for custom operators - default_timezone can be passed now to set your default timezone to something other than UTC. - New summarization algorithm that produces valid json - Better type hint support - Breaking change in DeepHash where we raise Exception instead of logging if we can't hash a value. - Added the log_stacktrace parameter to DeepDiff. When True, it will log the stacktrace along with the error. - v8-3-0 - Fixed some static typing issues - Added the summarize module for better repr of nested values - v8-2-0 - Small optimizations so we don't load functions that are not needed - Updated the minimum version of Orderly-set - Normalize all datetimes into UTC. Assume timezone naive datetimes are UTC. - v8-1-0 - Removing deprecated lines from setup.py - Added ``prefix`` option to ``pretty()`` - Fixes hashing of numpy boolean values. - Fixes **slots** comparison when the attribute doesn’t exist. - Relaxing orderly-set reqs - Added Python 3.13 support - Only lower if clean_key is instance of str - Fixes issue where the key deep_distance is not returned when both compared items are equal - Fixes exclude_paths fails to work in certain cases - exclude_paths fails to work - Fixes to_json() method chokes on standard json.dumps() kwargs such as sort_keys - to_dict() method chokes on standard json.dumps() kwargs - Fixes accessing the affected_root_keys property on the diff object returned by DeepDiff fails when one of the dicts is empty - Fixes accessing the affected_root_keys property on the diff object returned by DeepDiff fails when one of the dicts is empty - v8-0-1 - Bugfix. Numpy should be optional. - v8-0-0 - With the introduction of `threshold_to_diff_deeper`, the values returned are different than in previous versions of DeepDiff. You can still get the older values by setting `threshold_to_diff_deeper=0`. However to signify that enough has changed in this release that the users need to update the parameters passed to DeepDiff, we will be doing a major version update. - `use_enum_value=True` makes it so when diffing enum, we use the enum's value. It makes it so comparing an enum to a string or any other value is not reported as a type change. - `threshold_to_diff_deeper=float` is a number between 0 and 1. When comparing dictionaries that have a small intersection of keys, we will report the dictionary as a `new_value` instead of reporting individual keys changed. If you set it to zero, you get the same results as DeepDiff 7.0.1 and earlier, which means this feature is disabled. The new default is 0.33 which means if less that one third of keys between dictionaries intersect, report it as a new object. - Deprecated `ordered-set` and switched to `orderly-set`. The `ordered-set` package was not being maintained anymore and starting Python 3.6, there were better options for sets that ordered. I forked one of the new implementations, modified it, and published it as `orderly-set`. - Added `use_log_scale:bool` and `log_scale_similarity_threshold:float`. They can be used to ignore small changes in numbers by comparing their differences in logarithmic space. This is different than ignoring the difference based on significant digits. - json serialization of reversed lists. - Fix for iterable moved items when `iterable_compare_func` is used. - Pandas and Polars support - v7-0-1 - Fixes the translation between Difflib opcodes and Delta flat rows. - v7-0-0 - When verbose=2, return ``new_path`` when the ``path`` and ``new_path`` are different (for example when ignore_order=True and the index of items have changed). - Dropping support for Python 3.7 - Introducing serialize to flat rows for delta objects. - fixes the issue with hashing ``datetime.date`` objects where it treated them as numbers instead of dates (fixes #445). - upgrading orjson to the latest version - Fix for bug when diffing two lists with ignore_order and providing compare_func - Fixes “Wrong diff on list of strings” #438 - Supporting Python 3.12 in the build process by `Leo Sin `__ - Fixes “Instantiating a Delta with a flat_dict_list unexpectedly mutates the flat_dict_list” #457 by `sf-tcalhoun `__ - Fixes “Error on Delta With None Key and Removed Item from List” #441 - Fixes “Error when comparing two nested dicts with 2 added fields” #450 - Fixes “Error when subtracting Delta from a dictionary” #443 - v6-7-1 - Support for subtracting delta objects when iterable_compare_func is used. - Better handling of force adding a delta to an object. - Fix for ```Can't compare dicts with both single and double quotes in keys`` `__ - Updated docs for Inconsistent Behavior with math_epsilon and ignore_order = True - v6-7-0 - Delta can be subtracted from other objects now. - verify_symmetry is deprecated. Use bidirectional instead. - always_include_values flag in Delta can be enabled to include values in the delta for every change. - Fix for Delta.\__add\_\_ breaks with esoteric dict keys. - v6-6-1 - Fix for `DeepDiff raises decimal exception when using significant digits `__ - Introducing group_by_sort_key - Adding group_by 2D. For example ``group_by=['last_name', 'zip_code']`` - v6-6-0 - Numpy 2.0 support - Adding `Delta.to_flat_dicts `__ - v6-5-0 - Adding ```parse_path`` `__ - v6-4-1 - Bugfix: Keep Numpy Optional - v6-4-0 - `Add Ignore List Order Option to DeepHash `__ by `Bobby Morck `__ - `pyyaml to 6.0.1 to fix cython build problems `__ by `Robert Bo Davis `__ - `Precompiled regex simple diff `__ by `cohml `__ - New flag: ``zip_ordered_iterables`` for forcing iterable items to be compared one by one. - v6-3-1 - Bugfix deephash for paths by `maggelus `__ - Bugfix deephash compiled regex `maggelus `__ - Fix tests dependent on toml by `martin-kokos `__ - Bugfix for ``include_paths`` for nested dictionaries by `kor4ik `__ - Use tomli and tomli-w for dealing with tomli files by `martin-kokos `__ - Bugfix for ``datetime.date`` by `Alex Sauer-Budge `__ - v6-3-0 - ``PrefixOrSuffixOperator``: This operator will skip strings that are suffix or prefix of each other. - ``include_obj_callback`` and ``include_obj_callback_strict`` are added by `Håvard Thom `__. - Fixed a corner case where numpy’s ``np.float32`` nans are not ignored when using ``ignore_nan_equality`` by `Noam Gottlieb `__ - ``orjson`` becomes optional again. - Fix for ``ignore_type_in_groups`` with numeric values so it does not report number changes when the number types are different. - v6-2-3 - Switching to Orjson for serialization to improve the performance. - Setting ``equal_nan=ignore_nan_inequality`` in the call for ``np.array_equal`` - Using Pytest’s tmp_path fixture instead of ``/tmp/`` - v6-2-2 - Enum test fix for python 3.11 - Adding support for dateutils rrules - v6-2-1 - Removed the print statements. - v6-2-0 - Major improvement in the diff report for lists when items are all hashable and the order of items is important. - v6-1-0 - DeepDiff.affected_paths can be used to get the list of all paths where a change, addition, or deletion was reported for. - DeepDiff.affected_root_keys can be used to get the list of all paths where a change, addition, or deletion was reported for. - Bugfix: ValueError when using Decimal 0.x #339 by `Enric Pou `__ - Serialization of UUID - v6-0-0 - `Exclude obj callback strict `__ parameter is added to DeepDiff by Mikhail Khviyuzov `mskhviyu `__. - A fix for diffing using ``iterable_compare_func`` with nested objects by `dtorres-sf `__ who originally contributed this feature. - v5-7-0: - https://github.com/seperman/deepdiff/pull/284 Bug-Fix: TypeError in \_get_numbers_distance() when ignore_order = True by @Dhanvantari - https://github.com/seperman/deepdiff/pull/280 Add support for UUIDs by @havardthom - Major bug in delta when it comes to iterable items added or removed is investigated by @uwefladrich and resolved by @seperman - v5-6-0: Adding custom operators, and ignore_order_func. Bugfix: verbose_level==0 should disable values_changes. Bugfix: unprocessed key error. - v5-5-0: adding iterable_compare_func for DeepDiff, adding output_format of list for path() in tree view. - v5-4-0: adding strict_checking for numbers in DeepSearch. - v5-3-0: add support for regular expressions in DeepSearch. - v5-2-3: Retaining the order of multiple dictionary items added via Delta. Fixed the typo with yml files in deep cli. Fixing Grep RecursionError where using non UTF-8 character. Allowing kwargs to be passed to to_json method. - v5-2-2: Fixed Delta serialization when None type is present. - v5-2-0: Removed Murmur3 as the preferred hashing method. Using SHA256 by default now. Added commandline for deepdiff. Added group_by. Added math_epsilon. Improved ignoring of NoneType. - v5-0-2: Bug Fix NoneType in ignore type groups https://github.com/seperman/deepdiff/issues/207 - v5-0-1: Bug fix to not apply format to non numbers. - v5-0-0: Introducing the Delta object, Improving Numpy support, Fixing tuples comparison when ignore_order=True, Dramatically improving the results when ignore_order=True by running in passes, Introducing pretty print view, deep_distance, purge, progress logging, cache and truncate_datetime. - v4-3-3: Adds support for datetime.time - v4-3-2: Deprecation Warning Enhancement - v4-3-1: Fixing the issue with exclude_path and hash calculations when dictionaries were inside iterables. https://github.com/seperman/deepdiff/issues/174 - v4-3-0: adding exclude_obj_callback - v4-2-0: .json property is finally removed. Fix for Py3.10. Dropping support for EOL Python 3.4. Ignoring private keys when calculating hashes. For example __init__ is not a part of hash calculation anymore. Fix for #166 Problem with comparing lists, with an boolean as element. - v4-1-0: .json property is finally removed. - v4-0-9: Fixing the bug for hashing custom unhashable objects - v4-0-8: Adding ignore_nan_inequality for float('nan') - v4-0-7: Hashing of the number 1 vs. True - v4-0-6: found a tiny bug in Python formatting of numbers in scientific notation. Added a workaround. - v4-0-5: Fixing number diffing. Adding number_format_notation and number_to_string_func. - v4-0-4: Adding ignore_string_case and ignore_type_subclasses - v4-0-3: Adding versionbump tool for release - v4-0-2: Fixing installation issue where rst files are missing. - v4-0-1: Fixing installation Tarball missing requirements.txt . DeepDiff v4+ should not show up as pip installable for Py2. Making Murmur3 installation optional. - v4-0-0: Ending Python 2 support, Adding more functionalities and documentation for DeepHash. Switching to Pytest for testing. Switching to Murmur3 128bit for hashing. Fixing classes which inherit from classes with slots didn't have all of their slots compared. Renaming ContentHash to DeepHash. Adding exclude by path and regex path to DeepHash. Adding ignore_type_in_groups. Adding match_string to DeepSearch. Adding Timedelta object diffing. - v3-5-0: Exclude regex path - v3-3-0: Searching for objects and class attributes - v3-2-2: Adding help(deepdiff) - v3-2-1: Fixing hash of None - v3-2-0: Adding grep for search: object | grep(item) - v3-1-3: Unicode vs. Bytes default fix - v3-1-2: NotPresent Fix when item is added or removed. - v3-1-1: Bug fix when item value is None (#58) - v3-1-0: Serialization to/from json - v3-0-0: Introducing Tree View - v2-5-3: Bug fix on logging for content hash. - v2-5-2: Bug fixes on content hash. - v2-5-0: Adding ContentHash module to fix ignore_order once and for all. - v2-1-0: Adding Deep Search. Now you can search for item in an object. - v2-0-0: Exclusion patterns better coverage. Updating docs. - v1-8-0: Exclusion patterns. - v1-7-0: Deep Set comparison. - v1-6-0: Unifying key names. i.e newvalue is new_value now. For backward compatibility, newvalue still works. - v1-5-0: Fixing ignore order containers with unordered items. Adding significant digits when comparing decimals. Changes property is deprecated. - v1-1-0: Changing Set, Dictionary and Object Attribute Add/Removal to be reported as Set instead of List. Adding Pypy compatibility. - v1-0-2: Checking for ImmutableMapping type instead of dict - v1-0-1: Better ignore order support - v1-0-0: Restructuring output to make it more useful. This is NOT backward compatible. - v0-6-1: Fixing iterables with unhashable when order is ignored - v0-6-0: Adding unicode support - v0-5-9: Adding decimal support - v0-5-8: Adding ignore order for unhashables support - v0-5-7: Adding ignore order support - v0-5-6: Adding slots support - v0-5-5: Adding loop detection Back to :doc:`/index` qlustered-deepdiff-41c7265/deepdiff/docstrings/colored_view.rst000066400000000000000000000057661516241264500247270ustar00rootroot00000000000000.. _colored_view_label: Colored View ============ The `ColoredView` feature in `deepdiff` provides a human-readable, color-coded JSON output of the differences between two objects. This feature is particularly useful for visualizing changes in a clear and intuitive manner. - **Color-Coded Differences:** - **Added Elements:** Shown in green. - **Removed Elements:** Shown in red. - **Changed Elements:** The old value is shown in red, and the new value is shown in green. Usage ----- To use the `ColoredView`, simply pass the `COLORED_VIEW` option to the `DeepDiff` function: .. code-block:: python from deepdiff import DeepDiff from deepdiff.helper import COLORED_VIEW t1 = {"name": "John", "age": 30, "scores": [1, 2, 3], "address": {"city": "New York", "zip": "10001"}} t2 = {"name": "John", "age": 31, "scores": [1, 2, 4], "address": {"city": "Boston", "zip": "10001"}, "new": "value"} diff = DeepDiff(t1, t2, view=COLORED_VIEW) print(diff) Or from command line: .. code-block:: bash deep diff --view colored t1.json t2.json The output will look something like this: .. raw:: html
    {
      "name": "John",
      "age": 30 -> 31,
      "scores": [
        1,
        2,
        3 -> 4
      ],
      "address": {
        "city": "New York" -> "Boston",
        "zip": "10001"
      },
      "new": "value"
    }
    
Colored Compact View -------------------- For a more concise output, especially with deeply nested objects where many parts are unchanged, the `ColoredView` with the compact option can be used. This view is similar but collapses unchanged nested dictionaries to `{...}` and unchanged lists/tuples to `[...]`. To use the compact option do: .. code-block:: python from deepdiff import DeepDiff from deepdiff.helper import COLORED_COMPACT_VIEW t1 = {"name": "John", "age": 30, "scores": [1, 2, 3], "address": {"city": "New York", "zip": "10001"}} t2 = {"name": "John", "age": 31, "scores": [1, 2, 4], "address": {"city": "New York", "zip": "10001"}, "new": "value"} diff = DeepDiff(t1, t2, view=COLORED_COMPACT_VIEW) print(diff) Or from command line: .. code-block:: bash deep diff --view colored_compact t1.json t2.json The output will look something like this: .. raw:: html
    {
      "name": "John",
      "age": 30 -> 31,
      "scores": [
        1,
        2,
        3 -> 4
      ],
      "address": {...},
      "new": "value"
    }
    
qlustered-deepdiff-41c7265/deepdiff/docstrings/commandline.rst000066400000000000000000000202761516241264500245250ustar00rootroot00000000000000:doc:`/index` Command Line ============ `New in DeepDiff 5.2.0` DeepDiff provides commandline interface to a subset of functionality that it provides through its Python API. The commands are: - :ref:`deep_diff_command` - :ref:`deep_grep_command` - :ref:`deep_extract_command` - :ref:`deep_patch_command` .. _deep_diff_command: deep diff command ----------------- Run .. code:: bash $ deep diff to get the options: .. code-block:: bash $ deep diff --help Usage: deep diff [OPTIONS] T1 T2 Deep Diff Commandline Deep Difference of content in files. It can read csv, tsv, json, yaml, and toml files. T1 and T2 are the path to the files to be compared with each other. Options: --cutoff-distance-for-pairs FLOAT [default: 0.3] --cutoff-intersection-for-pairs FLOAT [default: 0.7] --cache-size INTEGER [default: 0] --cache-tuning-sample-size INTEGER [default: 0] --cache-purge-level INTEGER RANGE [default: 1] --create-patch [default: False] --exclude-paths TEXT --exclude-regex-paths TEXT --math-epsilon DECIMAL --get-deep-distance [default: False] --group-by TEXT --ignore-order [default: False] --ignore-string-type-changes [default: False] --ignore-numeric-type-changes [default: False] --ignore-type-subclasses [default: False] --ignore-string-case [default: False] --ignore-nan-inequality [default: False] --include-private-variables [default: False] --log-frequency-in-sec INTEGER [default: 0] --max-passes INTEGER [default: 10000000] --max_diffs INTEGER --number-format-notation [f|e] [default: f] --progress-logger [info|error] [default: info] --report-repetition [default: False] --significant-digits INTEGER --truncate-datetime [second|minute|hour|day] --verbose-level INTEGER RANGE [default: 1] --view [-|colored|colored_compact] [default: -] Format for displaying differences. --help Show this message and exit. Example usage: Let's imagine we have t1.csv and t2.csv: .. csv-table:: t1.csv :file: ../tests/fixtures/t1.csv :header-rows: 1 .. csv-table:: t2.csv :file: ../tests/fixtures/t2.csv :header-rows: 1 We can run: .. code-block:: bash $ deep diff t1.csv t2.csv --ignore-order {'values_changed': {"root[2]['zip']": {'new_value': 90002, 'old_value': 90001}}} As you can see here the path to the item that is being changed is `root[2]['zip']` which is ok but what if we assume last names are unique and group by last_name? .. code-block:: bash $ deep diff t1.csv t2.csv --ignore-order --group-by last_name { 'values_changed': { "root['Molotov']['zip']": { 'new_value': 90002, 'old_value': 90001}}} The path is perhaps more readable now: `root['Molotov']['zip']`. It is more clear that the zip code of Molotov has changed. .. Note:: The parameters in the deep diff commandline are a subset of those in :ref:`deepdiff_label` 's Python API. To output in a specific format, for example the colored compact view (see :doc:`colored_view` for output details): .. code-block:: bash $ deep diff t1.json t2.json --view colored_compact .. _deep_grep_command: deep grep command ----------------- Run .. code:: bash $ deep grep to get the options: .. code-block:: bash $ deep grep --help Usage: deep grep [OPTIONS] ITEM PATH Deep Grep Commandline Grep through the contents of a file and find the path to the item. It can read csv, tsv, json, yaml, and toml files. Options: -i, --ignore-case [default: False] --exact-match [default: False] --exclude-paths TEXT --exclude-regex-paths TEXT --verbose-level INTEGER RANGE [default: 1] --help Show this message and exit. .. csv-table:: t1.csv :file: ../tests/fixtures/t1.csv :header-rows: 1 .. code-block:: bash $ deep grep --ignore-case james t1.csv {'matched_values': ["root[2]['first_name']"]} .. _deep_extract_command: deep extract command -------------------- Run .. code:: bash $ deep extract to get the options: .. code-block:: bash $ deep extract --help Usage: deep extract [OPTIONS] PATH_INSIDE PATH Deep Extract Commandline Extract an item from a file based on the path that is passed. It can read csv, tsv, json, yaml, and toml files. Options: --help Show this message and exit. .. csv-table:: t1.csv :file: ../tests/fixtures/t1.csv :header-rows: 1 .. code-block:: bash $ deep extract "root[2]['first_name']" t1.csv 'James' .. _deep_patch_command: deep patch command ------------------ Run .. code:: bash $ deep patch --help to get the options: .. code-block:: text $ deep patch --help Usage: deep patch [OPTIONS] PATH DELTA_PATH Deep Patch Commandline Patches a file based on the information in a delta file. The delta file can be created by the deep diff command and passing the --create-patch argument. Deep Patch is similar to Linux's patch command. The difference is that it is made for patching data. It can read csv, tsv, json, yaml, and toml files. Options: -b, --backup [default: False] --raise-errors [default: False] --help Show this message and exit. Imagine if we have the following files: .. csv-table:: t1.csv :file: ../tests/fixtures/t1.csv :header-rows: 1 .. csv-table:: t2.csv :file: ../tests/fixtures/t2.csv :header-rows: 1 First we need to create a "delta" file which represents the difference between the 2 files. .. code-block:: bash $ deep diff t1.csv t2.csv --ignore-order {'values_changed': {"root[2]['zip']": {'new_value': 90002, 'old_value': 90001}}} We create the delta by using the deep diff command and passing the `--create-patch` argument. However since we are using `--ignore-order`, `deep diff` will ask us to also use `--report-repetition`: .. code-block:: bash deep diff t1.csv t2.csv --ignore-order --report-repetition --create-patch =}values_changed}root[2]['zip']} new_valueJ_sss.% Note that the delta is not human readable. It is meant for us to pass it into a file: .. code-block:: bash deep diff t1.csv t2.csv --ignore-order --report-repetition --create-patch > patch1.pickle Now this delta file is ready to be applied by the `deep patch` command to any json, csv, toml or yaml file! It is expecting the structure of the file to be similar to the one in the csv file though. Let's look at this yaml file: `another.yaml` .. code-block:: yaml --- - first_name: Joe last_name: Nobody zip: 90011 - first_name: Jack last_name: Doit zip: 22222 - first_name: Sara last_name: Stanley zip: 11111 All that our delta knows is that `root[2]['zip']` has changed to `90002`. Let's apply the delta: .. code-block:: bash deep patch --backup another.yaml patch1.pickle --raise-errors And looking at the `another.yaml` file, the zip code is indeed updated! .. code-block:: yaml - first_name: Joe last_name: Nobody zip: 90011 - first_name: Jack last_name: Doit zip: 22222 - first_name: Sara last_name: Stanley zip: 90002 As you can see the formatting of the yaml file is changed. This is due to the fact that DeepDiff loads the file into a Python dictionary, modifies it and then writes it back to disk. During this operation, the file loses its original formatting. .. note:: The deep patch command only provides a subset of what DeepDiff's :ref:`delta_label`'s Python API provides. The deep patch command is minimalistic and is designed to have a similar interface to Linux's patch command rather than DeepDiff's :ref:`delta_label`. Back to :doc:`/index` qlustered-deepdiff-41c7265/deepdiff/docstrings/custom.rst000066400000000000000000000357611516241264500235560ustar00rootroot00000000000000:doc:`/index` Customized Diff =============== .. _iterable_compare_func_label: Iterable Compare Func --------------------- New in DeepDiff 5.5.0 There are times that we want to guide DeepDiff as to what items to compare with other items. In such cases we can pass a `iterable_compare_func` that takes a function pointer to compare two items. The function takes three parameters (x, y, level) and should return `True` if it is a match, `False` if it is not a match or raise `CannotCompare` if it is unable to compare the two. For example take the following objects: Now let's define a compare_func that takes 3 parameters: x, y and level. >>> from deepdiff import DeepDiff >>> from deepdiff.helper import CannotCompare >>> >>> t1 = [ ... { ... 'id': 1, ... 'value': [1] ... }, ... { ... 'id': 2, ... 'value': [7, 8, 1] ... }, ... { ... 'id': 3, ... 'value': [7, 8], ... }, ... ] >>> >>> t2 = [ ... { ... 'id': 2, ... 'value': [7, 8] ... }, ... { ... 'id': 3, ... 'value': [7, 8, 1], ... }, ... { ... 'id': 1, ... 'value': [1] ... }, ... ] >>> >>> DeepDiff(t1, t2) {'values_changed': {"root[0]['id']": {'new_value': 2, 'old_value': 1}, "root[0]['value'][0]": {'new_value': 7, 'old_value': 1}, "root[1]['id']": {'new_value': 3, 'old_value': 2}, "root[2]['id']": {'new_value': 1, 'old_value': 3}, "root[2]['value'][0]": {'new_value': 1, 'old_value': 7}}, 'iterable_item_added': {"root[0]['value'][1]": 8}, 'iterable_item_removed': {"root[2]['value'][1]": 8}} As you can see the results are different. Now items with the same ids are compared with each other. >>> def compare_func(x, y, level=None): ... try: ... return x['id'] == y['id'] ... except Exception: ... raise CannotCompare() from None ... >>> DeepDiff(t1, t2, iterable_compare_func=compare_func) {'iterable_item_added': {"root[2]['value'][2]": 1}, 'iterable_item_removed': {"root[1]['value'][2]": 1}} If we set the verbose_level=2, we can see more details. >>> DeepDiff(t1, t2, iterable_compare_func=compare_func, verbose_level=2) {'iterable_item_added': {"root[2]['value'][2]": 1}, 'iterable_item_removed': {"root[1]['value'][2]": 1}, 'iterable_item_moved': {'root[0]': {'new_path': 'root[2]', 'value': {'id': 1, 'value': [1]}}, 'root[1]': {'new_path': 'root[0]', 'value': {'id': 2, 'value': [7, 8]}}, 'root[2]': {'new_path': 'root[1]', 'value': {'id': 3, 'value': [7, 8, 1]}}}} We can also use the level parameter. Levels are explained in the :ref:`tree_view_label`. For example you could use the level object to further determine if the 2 objects should be matches or not. >>> t1 = { ... 'path1': [], ... 'path2': [ ... { ... 'id': 1, ... 'value': [1] ... }, ... { ... 'id': 2, ... 'value': [7, 8, 1] ... }, ... ] ... } >>> >>> t2 = { ... 'path1': [{'pizza'}], ... 'path2': [ ... { ... 'id': 2, ... 'value': [7, 8, 1] ... }, ... { ... 'id': 1, ... 'value': [1, 2] ... }, ... ] ... } >>> >>> >>> def compare_func2(x, y, level): ... if (not isinstance(x, dict) or not isinstance(y, dict)): ... raise CannotCompare ... if(level.path() == "root['path2']"): ... if (x["id"] == y["id"]): ... return True ... return False ... >>> >>> DeepDiff(t1, t2, iterable_compare_func=compare_func2) {'iterable_item_added': {"root['path1'][0]": {'pizza'}, "root['path2'][0]['value'][1]": 2}} .. note:: The level parameter of the iterable_compare_func is only used when ignore_order=False which is the default value for ignore_order. .. _custom_operators_label: Custom Operators ---------------- Whether two objects are different or not largely depends on the context. For example, apples and bananas are the same if you are considering whether they are fruits or not. In that case, you can pass a *custom_operators* for the job. Custom operators give you a lot of power. In the following examples, we explore various use cases such as: - Making DeepDiff report the L2 Distance of items - Only include specific paths in diffing - Making DeepDiff stop diffing once we find the first diff. You can use one of the predefined custom operators that come with DeepDiff. Or you can define one yourself. Built-In Custom Operators .. _prefix_or_suffix_operator_label: PrefixOrSuffixOperator ...................... This operator will skip strings that are suffix or prefix of each other. For example when this operator is used, the two strings of "joe" and "joe's car" will not be reported as different. >>> from deepdiff import DeepDiff >>> from deepdiff.operator import PrefixOrSuffixOperator >>> t1 = { ... "key1": ["foo", "bar's food", "jack", "joe"] ... } >>> t2 = { ... "key1": ["foo", "bar", "jill", "joe'car"] ... } >>> >>> DeepDiff(t1, t2) {'values_changed': {"root['key1'][1]": {'new_value': 'bar', 'old_value': "bar's food"}, "root['key1'][2]": {'new_value': 'jill', 'old_value': 'jack'}, "root['key1'][3]": {'new_value': "joe'car", 'old_value': 'joe'}}} >>> DeepDiff(t1, t2, custom_operators=[ ... PrefixOrSuffixOperator() ... ]) >>> {'values_changed': {"root['key1'][2]": {'new_value': 'jill', 'old_value': 'jack'}}} Define A Custom Operator ------------------------ To define a custom operator, you just need to inherit *BaseOperator* or *BaseOperatorPlus*. - *BaseOperatorPlus* is our new base operator that can be subclassed and provides the structure to build any custom operator. - *BaseOperator* is our older base class for creating custom operators. It was designed mainly for simple string based regex comparison. Base Operator Plus .................. *BaseOperatorPlus* is our new base operator that can be subclassed and provides the structure to build any custom operator. .. code-block:: python class BaseOperatorPlus(metaclass=ABCMeta): @abstractmethod def match(self, level) -> bool: """ Given a level which includes t1 and t2 in the tree view, is this operator a good match to compare t1 and t2? If yes, we will run the give_up_diffing to compare t1 and t2 for this level. """ pass @abstractmethod def give_up_diffing(self, level, diff_instance: "DeepDiff") -> bool: """ Given a level which includes t1 and t2 in the tree view, and the "distance" between l1 and l2. do we consider t1 and t2 to be equal or not. The distance is a number between zero to one and is calculated by DeepDiff to measure how similar objects are. """ @abstractmethod def normalize_value_for_hashing(self, parent: Any, obj: Any) -> Any: """ You can use this function to normalize values for ignore_order=True For example, you may want to turn all the words to be lowercase. Then you return obj.lower() """ pass **Example 1: We don't care about the exact GUID values. As long as pairs of strings match GUID regex, we want them to be considered as equals** >>> import re ... from typing import Any ... from deepdiff import DeepDiff ... from deepdiff.operator import BaseOperatorPlus ... ... ... d1 = { ... "Name": "SUB_OBJECT_FILES", ... "Values": { ... "Value": [ ... "{f254498b-b752-4f35-bef5-6f1844b61eb7}", ... "{7fb2a550-1849-45c0-b273-9aa5e4eb9f2b}", ... "{a9cbecc0-21dc-49ce-8b2c-d36352dae139}" ... ] ... } ... } ... ... d2 = { ... "Name": "SUB_OBJECT_FILES", ... "Values": { ... "Value": [ ... "{e5d18917-1a2c-4abe-b601-8ec002629953}", ... "{ea71ba1f-1339-4fae-bc28-a9ce9b8a8c67}", ... "{66bb6192-9cd2-4074-8be1-f2ac52877c70}", ... ] ... } ... } ... ... ... class RemoveGUIDsOperator(BaseOperatorPlus): ... _pattern = r"[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}" ... _substitute = "guid" ... ... def match(self, level) -> bool: ... return isinstance(level.t1, str) and isinstance(level.t2, str) ... ... @classmethod ... def _remove_pattern(cls, t: str): ... return re.sub(cls._pattern, cls._substitute, t) ... ... def give_up_diffing(self, level, diff_instance): ... t1 = self._remove_pattern(level.t1) ... t2 = self._remove_pattern(level.t2) ... return t1 == t2 ... ... def normalize_value_for_hashing(self, parent: Any, obj: Any) -> Any: ... """ ... Used for ignore_order=True ... """ ... if isinstance(obj, str): ... return self._remove_pattern(obj) ... return obj ... ... ... operator = RemoveGUIDsOperator() ... >>> diff1 = DeepDiff(d1, d2, custom_operators=[operator], log_stacktrace=True) ... diff1 {} >>> diff2 = DeepDiff(d1, d2, ignore_order=True, custom_operators=[operator], log_stacktrace=True) ... diff2 {} Base Operator ............. *BaseOperator* is our older base class for creating custom operators. It was designed mainly for simple string based regex comparison. .. code-block:: python class BaseOperator: def __init__(self, regex_paths:Optional[List[str]]=None, types:Optional[List[type]]=None): if regex_paths: self.regex_paths = convert_item_or_items_into_compiled_regexes_else_none(regex_paths) else: self.regex_paths = None self.types = types def match(self, level) -> bool: if self.regex_paths: for pattern in self.regex_paths: matched = re.search(pattern, level.path()) is not None if matched: return True if self.types: for type_ in self.types: if isinstance(level.t1, type_) and isinstance(level.t2, type_): return True return False def give_up_diffing(self, level, diff_instance) -> bool: raise NotImplementedError('Please implement the diff function.') **Example 2: An operator that mapping L2:distance as diff criteria and reports the distance** >>> import math >>> >>> from typing import List >>> from deepdiff import DeepDiff >>> from deepdiff.operator import BaseOperator >>> >>> >>> class L2DistanceDifferWithPreventDefault(BaseOperator): ... def __init__(self, regex_paths: List[str], distance_threshold: float): ... super().__init__(regex_paths) ... self.distance_threshold = distance_threshold ... def _l2_distance(self, c1, c2): ... return math.sqrt( ... (c1["x"] - c2["x"]) ** 2 + (c1["y"] - c2["y"]) ** 2 ... ) ... def give_up_diffing(self, level, diff_instance): ... l2_distance = self._l2_distance(level.t1, level.t2) ... if l2_distance > self.distance_threshold: ... diff_instance.custom_report_result('distance_too_far', level, { ... "l2_distance": l2_distance ... }) ... return True ... >>> >>> t1 = { ... "coordinates": [ ... {"x": 5, "y": 5}, ... {"x": 8, "y": 8} ... ] ... } >>> >>> t2 = { ... "coordinates": [ ... {"x": 6, "y": 6}, ... {"x": 88, "y": 88} ... ] ... } >>> DeepDiff(t1, t2, custom_operators=[L2DistanceDifferWithPreventDefault( ... ["^root\\['coordinates'\\]\\[\\d+\\]$"], ... 1 ... )]) {'distance_too_far': {"root['coordinates'][0]": {'l2_distance': 1.4142135623730951}, "root['coordinates'][1]": {'l2_distance': 113.13708498984761}}} **Example 3: If the objects are subclasses of a certain type, only compare them if their list attributes are not equal sets** >>> class CustomClass: ... def __init__(self, d: dict, l: list): ... self.dict = d ... self.dict['list'] = l ... >>> >>> custom1 = CustomClass(d=dict(a=1, b=2), l=[1, 2, 3]) >>> custom2 = CustomClass(d=dict(c=3, d=4), l=[1, 2, 3, 2]) >>> custom3 = CustomClass(d=dict(a=1, b=2), l=[1, 2, 3, 4]) >>> >>> >>> class ListMatchOperator(BaseOperator): ... def give_up_diffing(self, level, diff_instance): ... if set(level.t1.dict['list']) == set(level.t2.dict['list']): ... return True ... >>> >>> DeepDiff(custom1, custom2, custom_operators=[ ... ListMatchOperator(types=[CustomClass]) ... ]) {} >>> >>> >>> DeepDiff(custom2, custom3, custom_operators=[ ... ListMatchOperator(types=[CustomClass]) ... ]) {'dictionary_item_added': [root.dict['a'], root.dict['b']], 'dictionary_item_removed': [root.dict['c'], root.dict['d']], 'values_changed': {"root.dict['list'][3]": {'new_value': 4, 'old_value': 2}}} >>> **Example 4: Only diff certain paths** >>> from deepdiff import DeepDiff >>> class MyOperator: ... def __init__(self, include_paths): ... self.include_paths = include_paths ... def match(self, level) -> bool: ... return True ... def give_up_diffing(self, level, diff_instance) -> bool: ... return level.path() not in self.include_paths ... >>> >>> t1 = {'a': [10, 11], 'b': [20, 21], 'c': [30, 31]} >>> t2 = {'a': [10, 22], 'b': [20, 33], 'c': [30, 44]} >>> >>> DeepDiff(t1, t2, custom_operators=[ ... MyOperator(include_paths="root['a'][1]") ... ]) {'values_changed': {"root['a'][1]": {'new_value': 22, 'old_value': 11}}} **Example 5: Give up further diffing once the first diff is found** Sometimes all you care about is that there is a difference between 2 objects and not all the details of what exactly is different. In that case you may want to stop diffing as soon as the first diff is found. >>> from deepdiff import DeepDiff >>> class MyOperator: ... def match(self, level) -> bool: ... return True ... def give_up_diffing(self, level, diff_instance) -> bool: ... return any(diff_instance.tree.values()) ... >>> t1 = [[1, 2], [3, 4], [5, 6]] >>> t2 = [[1, 3], [3, 5], [5, 7]] >>> >>> DeepDiff(t1, t2, custom_operators=[ ... MyOperator() ... ]) {'values_changed': {'root[0][1]': {'new_value': 3, 'old_value': 2}}} Back to :doc:`/index` qlustered-deepdiff-41c7265/deepdiff/docstrings/deep_distance.rst000066400000000000000000000144531516241264500250260ustar00rootroot00000000000000:doc:`/index` .. _deep_distance_label: Deep Distance ============= Deep Distance is the distance between 2 objects. It is a floating point number between 0 and 1. Deep Distance in concept is inspired by `Levenshtein Edit Distance `_. At its core, the Deep Distance is the number of operations needed to convert one object to the other divided by the sum of the sizes of the 2 objects capped at 1. Note that unlike Levenshtein Distance, the Deep Distance is based on the number of operations and NOT the “minimum” number of operations to convert one object to the other. The number is highly dependent on the granularity of the diff results. And the granularity is controlled by the parameters passed to DeepDiff. .. _get_deep_distance_label: Get Deep Distance ----------------- get_deep_distance: Boolean, default = False get_deep_distance will get you the deep distance between objects. The distance is a number between 0 and 1 where zero means there is no diff between the 2 objects and 1 means they are very different. Note that this number should only be used to compare the similarity of 2 objects and nothing more. The algorithm for calculating this number may or may not change in the future releases of DeepDiff. The value of Deep Distance will show up in the result diff object's deep_distance key. >>> from deepdiff import DeepDiff >>> DeepDiff(10.0, 10.1, get_deep_distance=True) {'values_changed': {'root': {'new_value': 10.1, 'old_value': 10.0}}, 'deep_distance': 0.0014925373134328302} >>> DeepDiff(10.0, 100.1, get_deep_distance=True) {'values_changed': {'root': {'new_value': 100.1, 'old_value': 10.0}}, 'deep_distance': 0.24550408719346048} >>> DeepDiff(10.0, 1000.1, get_deep_distance=True) {'values_changed': {'root': {'new_value': 1000.1, 'old_value': 10.0}}, 'deep_distance': 0.29405999405999406} >>> DeepDiff([1], [1], get_deep_distance=True) {} >>> DeepDiff([1], [1, 2], get_deep_distance=True) {'iterable_item_added': {'root[1]': 2}, 'deep_distance': 0.2} >>> DeepDiff([1], [1, 2, 3], get_deep_distance=True) {'iterable_item_added': {'root[1]': 2, 'root[2]': 3}, 'deep_distance': 0.3333333333333333} >>> DeepDiff([[2, 1]], [[1, 2, 3]], ignore_order=True, get_deep_distance=True) {'iterable_item_added': {'root[0][2]': 3}, 'deep_distance': 0.1111111111111111} .. _distance_and_diff_granularity_label: Distance And Diff Granularity ----------------------------- .. note:: Deep Distance of objects are highly dependent on the diff object that is produced. A diff object that is more granular will give more accurate Deep Distance value too. Let's use the following 2 deeply nested objects as an example. If you ignore the order of items, they are very similar and only differ in a few elements. We will run 2 diffs and ask for the deep distance. The only difference between the below 2 diffs is that in the first one the :ref:`cutoff_intersection_for_pairs_label` is not passed so the default value of 0.3 is used while in the other one cutoff_intersection_for_pairs=1 is used which forces extra pass calculations. >>> from pprint import pprint >>> t1 = [ ... { ... "key3": [[[[[[[[[[1, 2, 4, 5]]], [[[8, 7, 3, 5]]]]]]]]]], ... "key4": [7, 8] ... }, ... { ... "key5": "val5", ... "key6": "val6" ... } ... ] >>> >>> t2 = [ ... { ... "key5": "CHANGE", ... "key6": "val6" ... }, ... { ... "key3": [[[[[[[[[[1, 3, 5, 4]]], [[[8, 8, 1, 5]]]]]]]]]], ... "key4": [7, 8] ... } ... ] We don't pass cutoff_intersection_for_pairs in the first diff. >>> diff1=DeepDiff(t1, t2, ignore_order=True, cache_size=5000, get_deep_distance=True) >>> pprint(diff1) {'deep_distance': 0.36363636363636365, 'values_changed': {'root[0]': {'new_value': {'key5': 'CHANGE', 'key6': 'val6'}, 'old_value': {'key3': [[[[[[[[[[1, 2, 4, 5]]], [[[8, 7, 3, 5]]]]]]]]]], 'key4': [7, 8]}}, 'root[1]': {'new_value': {'key3': [[[[[[[[[[1, 3, 5, 4]]], [[[8, 8, 1, 5]]]]]]]]]], 'key4': [7, 8]}, 'old_value': {'key5': 'val5', 'key6': 'val6'}}}} Note that the stats show that only 5 set of objects were compared with each other according to the DIFF COUNT: >>> diff1.get_stats() {'PASSES COUNT': 0, 'DIFF COUNT': 5, 'DISTANCE CACHE HIT COUNT': 0, 'MAX PASS LIMIT REACHED': False, 'MAX DIFF LIMIT REACHED': False} Let's pass cutoff_intersection_for_pairs=1 to enforce pass calculations. As you can see the results are way more granular and the deep distance value is way more accurate now. >>> diff2=DeepDiff(t1, t2, ignore_order=True, cache_size=5000, cutoff_intersection_for_pairs=1, get_deep_distance=True) >>> from pprint import pprint >>> pprint(diff2) {'deep_distance': 0.06060606060606061, 'iterable_item_removed': {"root[0]['key3'][0][0][0][0][0][0][1][0][0][1]": 7}, 'values_changed': {"root[0]['key3'][0][0][0][0][0][0][0][0][0][1]": {'new_value': 3, 'old_value': 2}, "root[0]['key3'][0][0][0][0][0][0][1][0][0][2]": {'new_value': 1, 'old_value': 3}, "root[1]['key5']": {'new_value': 'CHANGE', 'old_value': 'val5'}}} As you can see now way more calculations have happened behind the scene. Instead of only 5 set of items being compared with each other, we have 306 items that are compared with each other in 110 passes. >>> diff2.get_stats() {'PASSES COUNT': 110, 'DIFF COUNT': 306, 'DISTANCE CACHE HIT COUNT': 0, 'MAX PASS LIMIT REACHED': False, 'MAX DIFF LIMIT REACHED': False} Back to :doc:`/index` qlustered-deepdiff-41c7265/deepdiff/docstrings/deephash.rst000066400000000000000000000002401516241264500240050ustar00rootroot00000000000000:doc:`/index` DeepHash ======== .. toctree:: :maxdepth: 3 .. automodule:: deepdiff.deephash .. autoclass:: DeepHash :members: Back to :doc:`/index` qlustered-deepdiff-41c7265/deepdiff/docstrings/deephash_doc.rst000066400000000000000000000430211516241264500246360ustar00rootroot00000000000000:orphan: **DeepHash** DeepHash calculates the hash of objects based on their contents in a deterministic way. This way 2 objects with the same content should have the same hash. The main usage of DeepHash is to calculate the hash of otherwise unhashable objects. For example you can use DeepHash to calculate the hash of a set or a dictionary! At the core of it, DeepHash is a deterministic serialization of your object into a string so it can be passed to a hash function. By default it uses SHA256. You have the option to pass any other hashing function to be used instead. **Import** >>> from deepdiff import DeepHash **Parameters** obj : any object, The object to be hashed based on its content. apply_hash: Boolean, default = True DeepHash at its core is doing deterministic serialization of objects into strings. Then it hashes the string. The only time you want the apply_hash to be False is if you want to know what the string representation of your object is BEFORE it gets hashed. exclude_types: list, default = None List of object types to exclude from hashing. exclude_paths: list, default = None List of paths to exclude from the report. If only one item, you can pass it as a string instead of a list containing only one path. include_paths: list, default = None List of the only paths to include in the report. If only one item, you can pass it as a string. exclude_regex_paths: list, default = None List of string regex paths or compiled regex paths objects to exclude from the report. If only one item, you can pass it as a string instead of a list containing only one regex path. exclude_obj_callback function, default = None A function that takes the object and its path and returns a Boolean. If True is returned, the object is excluded from the results, otherwise it is included. This is to give the user a higher level of control than one can achieve via exclude_paths, exclude_regex_paths or other means. encodings: List, default = None Character encodings to iterate through when we convert bytes into strings. You may want to pass an explicit list of encodings in your objects if you start getting UnicodeDecodeError from DeepHash. Also check out ignore_encoding_errors if you can get away with ignoring these errors and don't want to bother with an explicit list of encodings but it will come at the price of slightly less accuracy of the final results. Example: encodings=["utf-8", "latin-1"] hashes: dictionary, default = empty dictionary A dictionary of {object or object id: object hash} to start with. Any object that is encountered and it is already in the hashes dictionary or its id is in the hashes dictionary, will re-use the hash that is provided by this dictionary instead of re-calculating its hash. This is typically used when you have a series of objects to be hashed and there might be repeats of the same object. hasher: function. default = DeepHash.sha256hex hasher is the hashing function. The default is DeepHash.sha256hex. But you can pass another hash function to it if you want. For example a cryptographic hash function or Python's builtin hash function. All it needs is a function that takes the input in string format and returns the hash. You can use it by passing: hasher=hash for Python's builtin hash. The following alternative is already provided: - hasher=DeepHash.sha1hex Note that prior to DeepDiff 5.2, Murmur3 was the default hash function. But Murmur3 is removed from DeepDiff dependencies since then. ignore_repetition: Boolean, default = True If repetitions in an iterable should cause the hash of iterable to be different. Note that the deepdiff diffing functionality lets this to be the default at all times. But if you are using DeepHash directly, you can set this parameter. ignore_type_in_groups Ignore type changes between members of groups of types. For example if you want to ignore type changes between float and decimals etc. Note that this is a more granular feature. Most of the times the shortcuts provided to you are enough. The shortcuts are ignore_string_type_changes which by default is False and ignore_numeric_type_changes which is by default False. You can read more about those shortcuts in this page. ignore_type_in_groups gives you more control compared to the shortcuts. For example lets say you have specifically str and byte datatypes to be ignored for type changes. Then you have a couple of options: 1. Set ignore_string_type_changes=True which is the default. 2. Set ignore_type_in_groups=[(str, bytes)]. Here you are saying if we detect one type to be str and the other one bytes, do not report them as type change. It is exactly as passing ignore_type_in_groups=[DeepDiff.strings] or ignore_type_in_groups=DeepDiff.strings . Now what if you want also typeA and typeB to be ignored when comparing agains each other? 1. ignore_type_in_groups=[DeepDiff.strings, (typeA, typeB)] 2. or ignore_type_in_groups=[(str, bytes), (typeA, typeB)] ignore_string_type_changes: Boolean, default = True string type conversions should not affect the hash output when this is set to True. For example "Hello" and b"Hello" should produce the same hash. By setting it to True, both the string and bytes of hello return the same hash. ignore_numeric_type_changes: Boolean, default = False numeric type conversions should not affect the hash output when this is set to True. For example 10, 10.0 and Decimal(10) should produce the same hash. When ignore_numeric_type_changes is set to True, all numbers are converted to strings with the precision of significant_digits parameter and number_format_notation notation. If no significant_digits is passed by the user, a default value of 12 is used. ignore_type_subclasses Use ignore_type_subclasses=True so when ignoring type (class), the subclasses of that class are ignored too. ignore_string_case Whether to be case-sensitive or not when comparing strings. By settings ignore_string_case=False, strings will be compared case-insensitively. ignore_private_variables: Boolean, default = True Whether to exclude the private variables in the calculations or not. It only affects variables that start with double underscores (__). ignore_encoding_errors: Boolean, default = False If you want to get away with UnicodeDecodeError without passing explicit character encodings, set this option to True. If you want to make sure the encoding is done properly, keep this as False and instead pass an explicit list of character encodings to be considered via the encodings parameter. ignore_iterable_order: Boolean, default = True If order of items in an iterable should not cause the hash of the iterable to be different. number_format_notation : string, default="f" number_format_notation is what defines the meaning of significant digits. The default value of "f" means the digits AFTER the decimal point. "f" stands for fixed point. The other option is "e" which stands for exponent notation or scientific notation. significant_digits : int >= 0, default=None By default the significant_digits compares only that many digits AFTER the decimal point. However you can set override that by setting the number_format_notation="e" which will make it mean the digits in scientific notation. Important: This will affect ANY number comparison when it is set. Note: If ignore_numeric_type_changes is set to True and you have left significant_digits to the default of None, it gets automatically set to 12. The reason is that normally when numbers from 2 different types are compared, instead of comparing the values, we only report the type change. However when ignore_numeric_type_changes=True, in order compare numbers from different types to each other, we need to convert them all into strings. The significant_digits will be used to make sure we accurately convert all the numbers into strings in order to report the changes between them. Internally it uses "{:.Xf}".format(Your Number) to compare numbers where X=significant_digits when the number_format_notation is left as the default of "f" meaning fixed point. Note that "{:.3f}".format(1.1135) = 1.113, but "{:.3f}".format(1.11351) = 1.114 For Decimals, Python's format rounds 2.5 to 2 and 3.5 to 4 (to the closest even number) When you set the number_format_notation="e", we use "{:.Xe}".format(Your Number) where X=significant_digits. truncate_datetime: string, default = None Can take value one of 'second', 'minute', 'hour', 'day' and truncate with this value datetime objects before hashing it **Returns** A dictionary of {item: item hash}. If your object is nested, it will build hashes of all the objects it contains too. .. note:: DeepHash output is not like conventional hash functions. It is a dictionary of object IDs to their hashes. This happens because DeepHash calculates the hash of the object and any other objects found within the object in a recursive manner. If you only need the hash of the object you are passing, all you need to do is to do: >>> from deepdiff import DeepHash >>> obj = {1: 2, 'a': 'b'} >>> DeepHash(obj)[obj] # doctest: +SKIP **Examples** Let's say you have a dictionary object. >>> from deepdiff import DeepHash >>> obj = {1: 2, 'a': 'b'} If you try to hash it: >>> hash(obj) Traceback (most recent call last): File "", line 1, in TypeError: unhashable type: 'dict' But with DeepHash: >>> from deepdiff import DeepHash >>> obj = {1: 2, 'a': 'b'} >>> DeepHash(obj) # doctest: +SKIP So what is exactly the hash of obj in this case? DeepHash is calculating the hash of the obj and any other object that obj contains. The output of DeepHash is a dictionary of object IDs to their hashes. In order to get the hash of obj itself, you need to use the object (or the id of object) to get its hash: >>> hashes = DeepHash(obj) >>> hashes[obj] 'bf5478de322aa033da36bf3bcf9f0599e13a520773f50c6eb9f2487377a7929b' Which you can write as: >>> hashes = DeepHash(obj)[obj] At first it might seem weird why DeepHash(obj)[obj] but remember that DeepHash(obj) is a dictionary of hashes of all other objects that obj contains too. If you prefer to use another hashing algorithm, you can pass it using the hasher parameter. If you do a deep copy of the obj, it should still give you the same hash: >>> from copy import deepcopy >>> obj2 = deepcopy(obj) >>> DeepHash(obj2)[obj2] 'bf5478de322aa033da36bf3bcf9f0599e13a520773f50c6eb9f2487377a7929b' Note that by default DeepHash will include string type differences. So if your strings were bytes: >>> obj3 = {1: 2, b'a': b'b'} >>> DeepHash(obj3)[obj3] '71db3231177d49f78b52a356ca206e6179417b681604d00ed703a077049e3300' But if you want the same hash if string types are different, set ignore_string_type_changes to True: >>> DeepHash(obj3, ignore_string_type_changes=True)[obj3] 'e60c2befb84be625037c75e1e26d0bfc85a0ffc1f3cde9500f68f6eac55e5ad6' ignore_numeric_type_changes is by default False too. >>> from decimal import Decimal >>> obj1 = {4:10} >>> obj2 = {4.0: Decimal(10.0)} >>> DeepHash(obj1)[4] == DeepHash(obj2)[4.0] False But by setting it to True, we can get the same hash. >>> DeepHash(obj1, ignore_numeric_type_changes=True)[4] == DeepHash(obj2, ignore_numeric_type_changes=True)[4.0] True number_format_notation: String, default = "f" number_format_notation is what defines the meaning of significant digits. The default value of "f" means the digits AFTER the decimal point. "f" stands for fixed point. The other option is "e" which stands for exponent notation or scientific notation. ignore_string_type_changes: Boolean, default = True By setting it to True, both the string and bytes of hello return the same hash. >>> DeepHash(b'hello', ignore_string_type_changes=True)[b'hello'] '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824' >>> DeepHash('hello', ignore_string_type_changes=True)['hello'] '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824' ignore_numeric_type_changes: Boolean, default = False For example if significant_digits=5, 1.1, Decimal(1.1) are both converted to 1.10000 That way they both produce the same hash. >>> t1 = {1: 1, 2: 2.22} >>> DeepHash(t1)[1] 'c1800a30c736483f13615542e7096f7973631fef8ca935ee1ed9f35fb06fd44e' >>> DeepHash(t1, ignore_numeric_type_changes=True)[1] == DeepHash(t1, ignore_numeric_type_changes=True)[1.0] True You can pass a list of tuples or list of lists if you have various type groups. When t1 and t2 both fall under one of these type groups, the type change will be ignored. DeepDiff already comes with 2 groups: DeepDiff.strings and DeepDiff.numbers . If you want to pass both: >>> from deepdiff import DeepDiff >>> ignore_type_in_groups = [DeepDiff.strings, DeepDiff.numbers] ignore_type_in_groups example with custom objects: >>> class Burrito: ... bread = 'flour' ... def __init__(self): ... self.spicy = True ... >>> >>> class Taco: ... bread = 'flour' ... def __init__(self): ... self.spicy = True ... >>> >>> burrito = Burrito() >>> taco = Taco() >>> >>> burritos = [burrito] >>> tacos = [taco] >>> >>> d1 = DeepHash(burritos, ignore_type_in_groups=[(Taco, Burrito)]) >>> d2 = DeepHash(tacos, ignore_type_in_groups=[(Taco, Burrito)]) >>> d1[burrito] == d2[taco] True ignore_type_subclasses Use ignore_type_subclasses=True so when ignoring type (class), the subclasses of that class are ignored too. >>> from deepdiff import DeepHash >>> >>> class ClassB: ... def __init__(self, x): ... self.x = x ... def __repr__(self): ... return "obj b" ... >>> >>> class ClassC(ClassB): ... def __repr__(self): ... return "obj c" ... >>> obj_b = ClassB(1) >>> obj_c = ClassC(1) >>> >>> # By default, subclasses are considered part of the type group. ... # ignore_type_in_groups=[(ClassB, )] matches ClassC too since it's a subclass. ... hashes_b = DeepHash(obj_b, ignore_type_in_groups=[(ClassB, )]) >>> hashes_c = DeepHash(obj_c, ignore_type_in_groups=[(ClassB, )]) >>> hashes_b[obj_b] == hashes_c[obj_c] True >>> >>> # With ignore_type_subclasses=True, only exact type matches count. ... # ClassC no longer matches (ClassB, ) group, so hashes differ. ... hashes_b = DeepHash(obj_b, ignore_type_in_groups=[(ClassB, )], ignore_type_subclasses=True) >>> hashes_c = DeepHash(obj_c, ignore_type_in_groups=[(ClassB, )], ignore_type_subclasses=True) >>> hashes_b[obj_b] != hashes_c[obj_c] True ignore_string_case Whether to be case-sensitive or not when comparing strings. By settings ignore_string_case=False, strings will be compared case-insensitively. >>> from deepdiff import DeepHash >>> DeepHash('hello')['hello'] == DeepHash('heLLO')['heLLO'] False >>> DeepHash('hello', ignore_string_case=True)['hello'] == DeepHash('heLLO', ignore_string_case=True)['heLLO'] True exclude_obj_callback function, default = None A function that takes the object and its path and returns a Boolean. If True is returned, the object is excluded from the results, otherwise it is included. This is to give the user a higher level of control than one can achieve via exclude_paths, exclude_regex_paths or other means. >>> def exclude_obj_callback(obj, path): ... return True if isinstance(obj, str) and obj in ('x', 'y') else False ... >>> dic1 = {"x": 1, "y": 2, "z": 3} >>> t1 = [dic1] >>> t1_hash = DeepHash(t1, exclude_obj_callback=exclude_obj_callback) >>> >>> dic2 = {"z": 3} >>> t2 = [dic2] >>> t2_hash = DeepHash(t2, exclude_obj_callback=exclude_obj_callback) >>> >>> t1_hash[t1] == t2_hash[t2] True number_format_notation : string, default="f" When numbers are converted to the string, you have the choices between "f" as fixed point and "e" as scientific notation: >>> t1=10002 >>> t2=10004 >>> t1_hash = DeepHash(t1, significant_digits=3, number_format_notation="f") >>> t2_hash = DeepHash(t2, significant_digits=3, number_format_notation="f") >>> >>> t1_hash[t1] == t2_hash[t2] False >>> >>> >>> # Now we use the scientific notation ... t1_hash = DeepHash(t1, significant_digits=3, number_format_notation="e") >>> t2_hash = DeepHash(t2, significant_digits=3, number_format_notation="e") >>> >>> t1_hash[t1] == t2_hash[t2] True Defining your own number_to_string_func Lets say you want the hash of numbers below 100 to be the same for some reason. >>> from deepdiff import DeepHash >>> from deepdiff.helper import number_to_string >>> def custom_number_to_string(number, *args, **kwargs): ... number = 100 if number < 100 else number ... return number_to_string(number, *args, **kwargs) ... >>> t1 = [10, 12, 100000] >>> t2 = [50, 63, 100021] >>> t1_hash = DeepHash(t1, significant_digits=3, number_format_notation="e", number_to_string_func=custom_number_to_string) >>> t2_hash = DeepHash(t2, significant_digits=3, number_format_notation="e", number_to_string_func=custom_number_to_string) >>> t1_hash[t1] == t2_hash[t2] True So both lists produced the same hash thanks to the low significant digits for 100000 vs 100021 and also the custom_number_to_string that converted all numbers below 100 to be 100! qlustered-deepdiff-41c7265/deepdiff/docstrings/delta.rst000066400000000000000000000521471516241264500233320ustar00rootroot00000000000000.. _delta_label: Delta ===== DeepDiff Delta is a directed delta that when applied to t1 can yield t2 where delta is the difference between t1 and t2. Delta objects are like git commits but for structured data. You can convert the diff results into Delta objects, store the deltas, and later apply to other objects. .. note:: If you plan to generate Delta objects from the DeepDiff result, and ignore_order=True, you need to also set the report_repetition=True. **Parameters** diff : Delta dictionary, Delta dump payload or a DeepDiff object, default=None. :ref:`delta_diff_label` is the content to be loaded. delta_path : String, default=None. :ref:`delta_path_label` is the local path to the delta dump file to be loaded delta_file : File Object, default=None. :ref:`delta_file_label` is the file object containing the delta data. delta_diff : Delta diff, default=None. This is a slightly different diff than the output of DeepDiff. When Delta object is initiated from the DeepDiff output, it transforms the diff into a slightly different structure that is more suitable for delta. You can find that object via delta.diff. It is the same object that is serialized when you create a delta dump. If you already have the delta_diff object, you can pass it to Delta via the delta_diff parameter. flat_dict_list : List of flat dictionaries, default=None, :ref:`flat_dict_list_label` can be used to load the delta object from a list of flat dictionaries. .. note:: You need to pass only one of the diff, delta_path, or delta_file parameters. deserializer : Deserializer function, default=pickle_load :ref:`delta_deserializer_label` is the function to deserialize the delta content. The default is the pickle_load function that comes with DeepDiff. serializer : Serializer function, default=pickle_dump :ref:`delta_serializer_label` is the function to serialize the delta content into a format that can be stored. The default is the pickle_dump function that comes with DeepDiff. log_errors : Boolean, default=True Whether to log the errors or not when applying the delta object. raise_errors : Boolean, default=False :ref:`raise_errors_label` Whether to raise errors or not when applying a delta object. mutate : Boolean, default=False. :ref:`delta_mutate_label` defines whether to mutate the original object when adding the delta to it or not. Note that this parameter is not always successful in mutating. For example if your original object is an immutable type such as a frozenset or a tuple, mutation will not succeed. Hence it is recommended to keep this parameter as the default value of False unless you are sure that you do not have immutable objects. There is a small overhead of doing deepcopy on the original object when mutate=False. If performance is a concern and modifying the original object is not a big deal, set the mutate=True but always reassign the output back to the original object. safe_to_import : Set, default=None. :ref:`delta_safe_to_import_label` is a set of modules that needs to be explicitly white listed to be loaded Example: {'mymodule.MyClass', 'decimal.Decimal'} Note that this set will be added to the basic set of modules that are already white listed. The set of what is already white listed can be found in deepdiff.serialization.SAFE_TO_IMPORT bidirectional : Boolean, default=False :ref:`delta_verify_symmetry_label` is used to verify that the original value of items are the same as when the delta was created. Note that in order for this option to work, the delta object will need to store more data and thus the size of the object will increase. Let's say that the diff object says root[0] changed value from X to Y. If you create the delta with the default value of bidirectional=False, then what delta will store is root[0] = Y. And if this delta was applied to an object that has any root[0] value, it will still set the root[0] to Y. However if bidirectional=True, then the delta object will store also that the original value of root[0] was X and if you try to apply the delta to an object that has root[0] of any value other than X, it will notify you. force : Boolean, default=False :ref:`delta_force_label` is used to force apply a delta to objects that have a different structure than what the delta was originally created from. always_include_values : Boolean, default=False :ref:`always_include_values_label` is used to make sure the delta objects includes the values that were changed. Sometime Delta tries to be efficient not include the values when it can get away with it. By setting this parameter to True, you ensure that the Delta object will include the values. .. _delta_fill: fill : Any, default=No Fill This is only relevant if `force` is set. This parameter only applies when force is set and trying to fill an existing array. If the index of the array being applied is larger than the length of the array this value will be used to fill empty spaces of the array to extend it in order to add the new value. If this parameter is not set, the items will get dropped and the array not extended. If this parameter is set with a callable function, it will get called each time a fill item is needed. It will be provided with three arguments: first argument is the array being filled, second argument is the value that is being added to the array, the third argument is the path that is being added. Example function: `def fill(obj, value, path): return "Camry" if "car" in path else None` **Returns** A delta object that can be added to t1 to recreate t2. Delta objects can contain the following vocabulary: iterable_item_added iterable_item_moved iterable_item_removed set_item_added set_item_removed dictionary_item_added dictionary_item_removed attribute_added attribute_removed type_changes values_changed iterable_items_added_at_indexes iterable_items_removed_at_indexes .. _delta_diff_label: Diff to load in Delta --------------------- diff : Delta dictionary, Delta dump payload or a DeepDiff object, default=None. diff is the content to be loaded. >>> from deepdiff import DeepDiff, Delta >>> from pprint import pprint >>> >>> t1 = [1, 2, 3] >>> t2 = ['a', 2, 3, 4] >>> diff = DeepDiff(t1, t2) >>> diff {'type_changes': {'root[0]': {'old_type': , 'new_type': , 'old_value': 1, 'new_value': 'a'}}, 'iterable_item_added': {'root[3]': 4}} >>> delta = Delta(diff) >>> delta # doctest: +SKIP Applying the delta object to t1 will yield t2: >>> t1 + delta ['a', 2, 3, 4] >>> t1 + delta == t2 True If we want to subtract a delta, we need to create a bidirectional delta: >>> delta = Delta(diff, bidirectional=True) >>> t2 - delta [1, 2, 3] >>> t2 - delta == t1 True Now let's dump the delta object so we can store it. >>> dump = delta.dumps() >>> >>> dump # doctest: +SKIP The dumps() function gives us the serialized content of the delta in the form of bytes. We could store it however we want. Or we could use the dump(file_object) to write the dump to the file_object instead. But before we try the dump(file_object) method, let's create a new Delta object and reapply it to t1 and see if we still get t2: >>> delta2 = Delta(dump) >>> t1 + delta2 == t2 True >>> .. _delta_path_label: Delta Path parameter -------------------- Ok now we can try the dumps(file_object). It does what you expect: >>> with open('/tmp/delta1', 'wb') as dump_file: ... delta.dump(dump_file) ... And we use the delta_path parameter to load the delta >>> delta3 = Delta(delta_path='/tmp/delta1') It still gives us the same result when applied. >>> t1 + delta3 == t2 True .. _delta_file_label: Delta File parameter -------------------- You can also pass a file object containing the delta dump: >>> with open('/tmp/delta1', 'rb') as dump_file: ... delta4 = Delta(delta_file=dump_file) ... >>> t1 + delta4 == t2 True .. _flat_dict_list_label: Flat Dict List -------------- You can create a delta object from the list of flat dictionaries that are produced via :ref:`to_flat_dicts_label`. Read more on :ref:`delta_from_flat_dicts_label`. .. _flat_rows_list_label: Flat Rows List -------------- You can create a delta object from the list of flat dictionaries that are produced via :ref:`delta_to_flat_rows_label`. Read more on :ref:`delta_to_flat_rows_label`. .. _delta_deserializer_label: Delta Deserializer ------------------ DeepDiff by default uses a restricted Python pickle function to deserialize the Delta dumps. Read more about :ref:`delta_dump_safety_label`. The user of Delta can decide to switch the serializer and deserializer to their custom ones. The serializer and deserializer parameters can be used exactly for that reason. The best way to come up with your own serializer and deserializer is to take a look at the `pickle_dump and pickle_load functions in the serializer module `_ .. _delta_json_deserializer_label: Json Deserializer for Delta ``````````````````````````` If all you deal with are Json serializable objects, you can use json for serialization. >>> from deepdiff import DeepDiff, Delta >>> from deepdiff.serialization import json_dumps, json_loads >>> t1 = {"a": 1} >>> t2 = {"a": 2} >>> >>> diff = DeepDiff(t1, t2) >>> delta = Delta(diff, serializer=json_dumps) >>> dump = delta.dumps() >>> dump '{"values_changed":{"root[\'a\']":{"new_value":2}}}' >>> delta_reloaded = Delta(dump, deserializer=json_loads) >>> t2 == delta_reloaded + t1 True .. note:: Json is very limited and easily you can get to deltas that are not json serializable. You will probably want to extend the Python's Json serializer to support your needs. >>> import json >>> t1 = {"a": 1} >>> t2 = {"a": None} >>> diff = DeepDiff(t1, t2) >>> diff {'type_changes': {"root['a']": {'old_type': , 'new_type': , 'old_value': 1, 'new_value': None}}} >>> Delta(diff, serializer=json.dumps) # doctest: +SKIP >>> delta = Delta(diff, serializer=json.dumps) >>> dump = delta.dumps() # doctest: +ELLIPSIS Traceback (most recent call last): ... TypeError: Object of type type is not JSON serializable... .. _delta_serializer_label: Delta Serializer ---------------- DeepDiff uses pickle to serialize delta objects by default. Please take a look at the :ref:`delta_deserializer_label` for more information. .. _to_flat_dicts_label: Delta Serialize To Flat Dictionaries ------------------------------------ Read about :ref:`delta_to_flat_dicts_label` .. _delta_dump_safety_label: Delta Dump Safety ----------------- Delta by default uses Python's pickle to serialize and deserialize. While the unrestricted use of pickle is not safe as noted in the `pickle's documentation `_ , DeepDiff's Delta is written with extra care to `restrict the globals `_ and hence mitigate this security risk. In fact only a few Python object types are allowed by default. The user of DeepDiff can pass additional types using the :ref:`delta_safe_to_import_label` to allow further object types that need to be allowed. .. _delta_mutate_label: Delta Mutate parameter ---------------------- mutate : Boolean, default=False. delta_mutate defines whether to mutate the original object when adding the delta to it or not. Note that this parameter is not always successful in mutating. For example if your original object is an immutable type such as a frozenset or a tuple, mutation will not succeed. Hence it is recommended to keep this parameter as the default value of False unless you are sure that you do not have immutable objects. There is a small overhead of doing deepcopy on the original object when mutate=False. If performance is a concern and modifying the original object is not a big deal, set the mutate=True but always reassign the output back to the original object. For example: >>> t1 = [1, 2, [3, 5, 6]] >>> t2 = [2, 3, [3, 6, 8]] >>> diff = DeepDiff(t1, t2, ignore_order=True, report_repetition=True) >>> diff {'values_changed': {'root[0]': {'new_value': 3, 'old_value': 1}, 'root[2][1]': {'new_value': 8, 'old_value': 5}}} >>> delta = Delta(diff) >>> delta # doctest: +SKIP Note that we can apply delta to objects different than the original objects they were made from: >>> t3 = ["a", 2, [3, "b", "c"]] >>> t3 + delta [3, 2, [3, 8, 'c']] If we check t3, it is still the same as the original value of t3: >>> t3 ['a', 2, [3, 'b', 'c']] Now let's make the delta with mutate=True >>> delta2 = Delta(diff, mutate=True) >>> t3 + delta2 [3, 2, [3, 8, 'c']] >>> t3 [3, 2, [3, 8, 'c']] Applying the delta to t3 mutated the t3 itself in this case! .. _delta_and_numpy_label: Delta and Numpy --------------- >>> from deepdiff import DeepDiff, Delta >>> import numpy as np >>> t1 = np.array([1, 2, 3, 5]) >>> t2 = np.array([2, 2, 7, 5]) >>> diff = DeepDiff(t1, t2) >>> diff {'values_changed': {'root[0]': {'new_value': np.int64(2), 'old_value': np.int64(1)}, 'root[2]': {'new_value': np.int64(7), 'old_value': np.int64(3)}}} >>> delta = Delta(diff) .. note:: When applying delta to Numpy arrays, make sure to put the delta object first and the numpy array second. This is because Numpy array overrides the + operator and thus DeepDiff's Delta won't be able to be applied. >>> t1 + delta Traceback (most recent call last): File "", line 1, in raise DeltaNumpyOperatorOverrideError(DELTA_NUMPY_OPERATOR_OVERRIDE_MSG) deepdiff.delta.DeltaNumpyOperatorOverrideError: A numpy ndarray is most likely being added to a delta. Due to Numpy override the + operator, you can only do: delta + ndarray and NOT ndarray + delta Let's put the delta first then: >>> delta + t1 array([2, 2, 7, 5]) >>> delta + t2 == t2 array([ True, True, True, True]) .. note:: You can apply a delta that was created from normal Python objects to Numpy arrays. But it is not recommended. .. _raise_errors_label: Delta Raise Errors parameter ---------------------------- raise_errors : Boolean, default=False Whether to raise errors or not when applying a delta object. >>> from deepdiff import DeepDiff, Delta >>> t1 = [1, 2, [3, 5, 6]] >>> t2 = [2, 3, [3, 6, 8]] >>> diff = DeepDiff(t1, t2, ignore_order=True, report_repetition=True) >>> delta = Delta(diff, raise_errors=False) Now let's apply the delta to a very different object: >>> t3 = [1, 2, 3, 5] >>> t4 = t3 + delta # doctest: +SKIP We get a log message that it was unable to get the item at root[2][1]. We get the message since by default log_errors=True Let's see what t4 is now: >>> t4 # doctest: +SKIP [3, 2, 3, 5] So the delta was partially applied on t3. Now let's set the raise_errors=True >>> delta2 = Delta(diff, raise_errors=True) >>> >>> t3 + delta2 # doctest: +ELLIPSIS Traceback (most recent call last): ... deepdiff.delta.DeltaError: Unable to get the item at root[2][1] .. _delta_safe_to_import_label: Delta Safe To Import parameter ------------------------------ safe_to_import : Set, default=None. safe_to_import is a set of modules that needs to be explicitly white listed to be loaded Example: {'mymodule.MyClass', 'decimal.Decimal'} Note that this set will be added to the basic set of modules that are already white listed. As noted in :ref:`delta_dump_safety_label` and :ref:`delta_deserializer_label`, DeepDiff's Delta takes safety very seriously and thus limits the globals that can be deserialized when importing. However on occasions that you need a specific type (class) that needs to be used in delta objects, you need to pass it to the Delta via safe_to_import parameter. The set of what is already white listed can be found in deepdiff.serialization.SAFE_TO_IMPORT At the time of writing this document, this list consists of: >>> from deepdiff.serialization import SAFE_TO_IMPORT >>> from pprint import pprint >>> pprint(SAFE_TO_IMPORT) # doctest: +SKIP frozenset({'builtins.None', 'builtins.bin', 'builtins.bool', ...}) If you want to pass any other argument to safe_to_import, you will need to put the full path to the type as it appears in the sys.modules For example let's say you have a package call mypackage and has a module called mymodule. If you check the sys.modules, the address to this module must be mypackage.mymodule. In order for Delta to be able to serialize this object via pickle, first of all it has to be `picklable `_. >>> diff = DeepDiff(t1, t2) >>> delta = Delta(diff) >>> dump = delta.dumps() The dump at this point is serialized via Pickle and can be written to disc if needed. Later when you want to load this dump, by default Delta will block you from importing anything that is NOT in deepdiff.serialization.SAFE_TO_IMPORT . In fact it will show you this error message when trying to load this dump: deepdiff.serialization.ForbiddenModule: Module 'builtins.type' is forbidden. You need to explicitly pass it by passing a safe_to_import parameter In order to let Delta know that this specific module is safe to import, you will need to pass it to Delta during loading of this dump: >>> delta = Delta(dump, safe_to_import={'mypackage.mymodule'}) .. note :: If you pass a custom deserializer to Delta, DeepDiff will pass safe_to_import parameter to the custom deserializer if that deserializer takes safe_to_import as a parameter in its definition. For example if you just use json.loads as deserializer, the safe_to_import items won't be passed to it since json.loads does not have such a parameter. .. _delta_verify_symmetry_label: Delta Verify Symmetry parameter ------------------------------- bidirectional : Boolean, default=False bidirectional is used to to include all the required information so that we can use the delta object both for addition and subtraction. It will also check that the object you are adding the delta to, has the same values as the original object that the delta was created from. It complains if the object is not what it expected to be. >>> from deepdiff import DeepDiff, Delta >>> t1 = [1] >>> t2 = [2] >>> t3 = [3] >>> >>> diff = DeepDiff(t1, t2) >>> >>> delta2 = Delta(diff, raise_errors=False, bidirectional=True) >>> t4 = delta2 + t3 # doctest: +SKIP >>> t4 # doctest: +SKIP [2] And if you had set raise_errors=True, then it would have raised the error in addition to logging it. .. _delta_force_label: Delta Force ----------- force : Boolean, default=False force is used to force apply a delta to objects that have a different structure than what the delta was originally created from. >>> from deepdiff import DeepDiff, Delta >>> t1 = { ... 'x': { ... 'y': [1, 2, 3] ... }, ... 'q': { ... 'r': 'abc', ... } ... } >>> >>> t2 = { ... 'x': { ... 'y': [1, 2, 3, 4] ... }, ... 'q': { ... 'r': 'abc', ... 't': 0.5, ... } ... } >>> >>> diff = DeepDiff(t1, t2) >>> diff {'dictionary_item_added': ["root['q']['t']"], 'iterable_item_added': {"root['x']['y'][3]": 4}} >>> delta = Delta(diff) >>> {} + delta # doctest: +SKIP {} Once we set the force to be True >>> delta = Delta(diff, force=True) >>> {} + delta {'x': {'y': {3: 4}}, 'q': {'t': 0.5}} Notice that the force attribute does not know the original object at ['x']['y'] was supposed to be a list, so it assumes it was a dictionary. .. _always_include_values_label: Always Include Values --------------------- always_include_values is used to make sure the delta objects includes the values that were changed. Sometime Delta tries to be efficient not include the values when it can get away with it. By setting this parameter to True, you ensure that the Delta object will include the values. For example, when the type of an object changes, if we can easily convert from one type to the other, the Delta object does not include the values: >>> from deepdiff import DeepDiff, Delta >>> diff = DeepDiff(t1=[1, 2], t2=[1, '2']) >>> diff {'type_changes': {'root[1]': {'old_type': , 'new_type': , 'old_value': 2, 'new_value': '2'}}} >>> delta=Delta(diff) >>> delta # doctest: +SKIP As you can see the delta object does not include the values that were changed. Now let's pass always_include_values=True: >>> delta=Delta(diff, always_include_values=True) >>> delta.diff {'type_changes': {'root[1]': {'old_type': , 'new_type': , 'new_value': '2'}}} If we want to make sure the old values stay with delta, we pass bidirectional=True. By doing so we can also use the delta object to subtract from other objects. >>> delta=Delta(diff, always_include_values=True, bidirectional=True) >>> delta.diff {'type_changes': {'root[1]': {'old_type': , 'new_type': , 'old_value': 2, 'new_value': '2'}}} qlustered-deepdiff-41c7265/deepdiff/docstrings/diff.rst000066400000000000000000000005511516241264500231410ustar00rootroot00000000000000:doc:`/index` .. _deepdiff_label: DeepDiff ======== .. automodule:: deepdiff.diff .. autoclass:: DeepDiff :members: .. toctree:: :maxdepth: 3 basics custom deep_distance exclude_paths ignore_order ignore_types_or_values numbers optimizations other serialization stats troubleshoot view Back to :doc:`/index` qlustered-deepdiff-41c7265/deepdiff/docstrings/diff_doc.rst000066400000000000000000000414711516241264500237740ustar00rootroot00000000000000:orphan: .. |qluster_link| raw:: html Qluster .. admonition:: DeepDiff is now part of |qluster_link|. *If you're building workflows around data validation and correction,* `Qluster `__ *gives your team a structured way to manage rules, review failures, approve fixes, and reuse decisions—without building the entire system from scratch.* DeepDiff Module =============== Deep Difference of dictionaries, iterables, strings and almost any other object. It will recursively look for all the changes. **Parameters** t1 : A dictionary, list, string or any python object that has __dict__ or __slots__ This is the first item to be compared to the second item t2 : dictionary, list, string or almost any python object that has __dict__ or __slots__ The second item is to be compared to the first one cutoff_distance_for_pairs : 1 >= float >= 0, default=0.3 :ref:`cutoff_distance_for_pairs_label` What is the threshold to consider 2 items as pairs. Note that it is only used when ignore_order = True. cutoff_intersection_for_pairs : 1 >= float >= 0, default=0.7 :ref:`cutoff_intersection_for_pairs_label` What is the threshold to calculate pairs of items between 2 iterables. For example 2 iterables that have nothing in common, do not need their pairs to be calculated. Note that it is only used when ignore_order = True. cache_size : int >= 0, default=0 :ref:`cache_size_label` Cache size to be used to improve the performance. A cache size of zero means it is disabled. Using the cache_size can dramatically improve the diff performance especially for the nested objects at the cost of more memory usage. cache_purge_level: int, 0, 1, or 2. default=1 :ref:`cache_purge_level` defines what objects in DeepDiff should be deleted to free the memory once the diff object is calculated. If this value is set to zero, most of the functionality of the diff object is removed and the most memory is released. A value of 1 preserves all the functionalities of the diff object. A value of 2 also preserves the cache and hashes that were calculated during the diff calculations. In most cases the user does not need to have those objects remained in the diff unless for investigation purposes. cache_tuning_sample_size : int >= 0, default = 0 :ref:`cache_tuning_sample_size_label` This is an experimental feature. It works hands in hands with the :ref:`cache_size_label`. When cache_tuning_sample_size is set to anything above zero, it will sample the cache usage with the passed sample size and decide whether to use the cache or not. And will turn it back on occasionally during the diffing process. This option can be useful if you are not sure if you need any cache or not. However you will gain much better performance with keeping this parameter zero and running your diff with different cache sizes and benchmarking to find the optimal cache size. custom_operators : BaseOperator subclasses, default = None :ref:`custom_operators_label` if you are considering whether they are fruits or not. In that case, you can pass a *custom_operators* for the job. default_timezone : datetime.timezone subclasses or pytz datetimes, default = datetime.timezone.utc :ref:`default_timezone_label` defines the default timezone. If a datetime is timezone naive, which means it doesn't have a timezone, we assume the datetime is in this timezone. Also any datetime that has a timezone will be converted to this timezone so the datetimes can be compared properly all in the same timezone. Note that Python's default behavior assumes the default timezone is your local timezone. DeepDiff's default is UTC, not your local time zone. encodings: List, default = None :ref:`encodings_label` Character encodings to iterate through when we convert bytes into strings. You may want to pass an explicit list of encodings in your objects if you start getting UnicodeDecodeError from DeepHash. Also check out :ref:`ignore_encoding_errors_label` if you can get away with ignoring these errors and don't want to bother with an explicit list of encodings but it will come at the price of slightly less accuracy of the final results. Example: encodings=["utf-8", "latin-1"] exclude_paths: list, default = None :ref:`exclude_paths_label` List of paths to exclude from the report. If only one item, you can path it as a string. exclude_regex_paths: list, default = None :ref:`exclude_regex_paths_label` List of string regex paths or compiled regex paths objects to exclude from the report. If only one item, you can pass it as a string or regex compiled object. exclude_types: list, default = None :ref:`exclude_types_label` List of object types to exclude from the report. exclude_obj_callback: function, default = None :ref:`exclude_obj_callback_label` A function that takes the object and its path and returns a Boolean. If True is returned, the object is excluded from the results, otherwise it is included. This is to give the user a higher level of control than one can achieve via exclude_paths, exclude_regex_paths or other means. exclude_obj_callback_strict: function, default = None :ref:`exclude_obj_callback_strict_label` A function that works the same way as exclude_obj_callback, but excludes elements from the result only if the function returns True for both elements. include_paths: list, default = None :ref:`include_paths_label` List of the only paths to include in the report. If only one item is in the list, you can pass it as a string. include_obj_callback: function, default = None :ref:`include_obj_callback_label` A function that takes the object and its path and returns a Boolean. If True is returned, the object is included in the results, otherwise it is excluded. This is to give the user a higher level of control than one can achieve via include_paths. include_obj_callback_strict: function, default = None :ref:`include_obj_callback_strict_label` A function that works the same way as include_obj_callback, but includes elements in the result only if the function returns True for both elements. get_deep_distance: Boolean, default = False :ref:`get_deep_distance_label` will get you the deep distance between objects. The distance is a number between 0 and 1 where zero means there is no diff between the 2 objects and 1 means they are very different. Note that this number should only be used to compare the similarity of 2 objects and nothing more. The algorithm for calculating this number may or may not change in the future releases of DeepDiff. group_by: String or a list of size 2, default=None :ref:`group_by_label` can be used when dealing with the list of dictionaries. It converts them from lists to a single dictionary with the key defined by group_by. The common use case is when reading data from a flat CSV, and the primary key is one of the columns in the CSV. We want to use the primary key instead of the CSV row number to group the rows. The group_by can do 2D group_by by passing a list of 2 keys. group_by_sort_key: String or a function :ref:`group_by_sort_key_label` is used to define how dictionaries are sorted if multiple ones fall under one group. When this parameter is used, group_by converts the lists of dictionaries into a dictionary of keys to lists of dictionaries. Then, :ref:`group_by_sort_key_label` is used to sort between the list. hasher: default = DeepHash.sha256hex Hash function to be used. If you don't want SHA256, you can use your own hash function by passing hasher=hash. This is for advanced usage and normally you don't need to modify it. ignore_order : Boolean, default=False :ref:`ignore_order_label` ignores order of elements when comparing iterables (lists) Normally ignore_order does not report duplicates and repetition changes. In order to report repetitions, set report_repetition=True in addition to ignore_order=True ignore_order_func : Function, default=None :ref:`ignore_order_func_label` Sometimes single *ignore_order* parameter is not enough to do a diff job, you can use *ignore_order_func* to determine whether the order of certain paths should be ignored ignore_string_type_changes: Boolean, default = False :ref:`ignore_string_type_changes_label` Whether to ignore string type changes or not. For example b"Hello" vs. "Hello" are considered the same if ignore_string_type_changes is set to True. ignore_numeric_type_changes: Boolean, default = False :ref:`ignore_numeric_type_changes_label` Whether to ignore numeric type changes or not. For example 10 vs. 10.0 are considered the same if ignore_numeric_type_changes is set to True. ignore_type_in_groups: Tuple or List of Tuples, default = None :ref:`ignore_type_in_groups_label` ignores types when t1 and t2 are both within the same type group. ignore_type_subclasses: Boolean, default = False :ref:`ignore_type_subclasses_label` ignore type (class) changes when dealing with the subclasses of classes that were marked to be ignored. .. Note:: ignore_type_subclasses was incorrectly doing the reverse of its job up until DeepDiff 6.7.1 Please make sure to flip it in your use cases, when upgrading from older versions to 7.0.0 or above. ignore_uuid_types: Boolean, default = False :ref:`ignore_uuid_types_label` Whether to ignore UUID vs string type differences when comparing. When set to True, comparing a UUID object with its string representation will not report as a type change. ignore_string_case: Boolean, default = False :ref:`ignore_string_case_label` Whether to be case-sensitive or not when comparing strings. By setting ignore_string_case=True, strings will be compared case-insensitively. ignore_nan_inequality: Boolean, default = False :ref:`ignore_nan_inequality_label` Whether to ignore float('nan') inequality in Python. ignore_private_variables: Boolean, default = True :ref:`ignore_private_variables_label` Whether to exclude the private variables in the calculations or not. It only affects variables that start with double underscores (__). ignore_encoding_errors: Boolean, default = False :ref:`ignore_encoding_errors_label` If you want to get away with UnicodeDecodeError without passing explicit character encodings, set this option to True. If you want to make sure the encoding is done properly, keep this as False and instead pass an explicit list of character encodings to be considered via the :ref:`encodings_label` parameter. zip_ordered_iterables: Boolean, default = False :ref:`zip_ordered_iterables_label`: When comparing ordered iterables such as lists, DeepDiff tries to find the smallest difference between the two iterables to report. That means that items in the two lists are not paired individually in the order of appearance in the iterables. Sometimes, that is not the desired behavior. Set this flag to True to make DeepDiff pair and compare the items in the iterables in the order they appear. iterable_compare_func: :ref:`iterable_compare_func_label`: There are times that we want to guide DeepDiff as to what items to compare with other items. In such cases we can pass a iterable_compare_func that takes a function pointer to compare two items. The function takes three parameters (x, y, level) and should return True if it is a match, False if it is not a match or raise CannotCompare if it is unable to compare the two. log_frequency_in_sec: Integer, default = 0 :ref:`log_frequency_in_sec_label` How often to log the progress. The default of 0 means logging progress is disabled. If you set it to 20, it will log every 20 seconds. This is useful only when running DeepDiff on massive objects that will take a while to run. If you are only dealing with small objects, keep it at 0 to disable progress logging. log_scale_similarity_threshold: float, default = 0.1 :ref:`use_log_scale_label` along with :ref:`log_scale_similarity_threshold_label` can be used to ignore small changes in numbers by comparing their differences in logarithmic space. This is different than ignoring the difference based on significant digits. log_stacktrace: Boolean, default = False If True, we log the stacktrace when logging errors. Otherwise we only log the error message. max_passes: Integer, default = 10000000 :ref:`max_passes_label` defined the maximum number of passes to run on objects to pin point what exactly is different. This is only used when ignore_order=True. A new pass is started each time 2 iterables are compared in a way that every single item that is different from the first one is compared to every single item that is different in the second iterable. max_diffs: Integer, default = None :ref:`max_diffs_label` defined the maximum number of diffs to run on objects to pin point what exactly is different. This is only used when ignore_order=True math_epsilon: Decimal, default = None :ref:`math_epsilon_label` uses Python's built in Math.isclose. It defines a tolerance value which is passed to math.isclose(). Any numbers that are within the tolerance will not report as being different. Any numbers outside of that tolerance will show up as different. number_format_notation : string, default="f" :ref:`number_format_notation_label` is what defines the meaning of significant digits. The default value of "f" means the digits AFTER the decimal point. "f" stands for fixed point. The other option is "e" which stands for exponent notation or scientific notation. number_to_string_func : function, default=None :ref:`number_to_string_func_label` is an advanced feature to give the user the full control into overriding how numbers are converted to strings for comparison. The default function is defined in https://github.com/seperman/deepdiff/blob/master/deepdiff/helper.py and is called number_to_string. You can define your own function to do that. progress_logger: log function, default = logger.info :ref:`progress_logger_label` defines what logging function to use specifically for progress reporting. This function is only used when progress logging is enabled which happens by setting log_frequency_in_sec to anything above zero. report_repetition : Boolean, default=False :ref:`report_repetition_label` reports repetitions when set True It only works when ignore_order is set to True too. significant_digits : int >= 0, default=None :ref:`significant_digits_label` defines the number of digits AFTER the decimal point to be used in the comparison. However you can override that by setting the number_format_notation="e" which will make it mean the digits in scientific notation. truncate_datetime: string, default = None :ref:`truncate_datetime_label` can take value one of 'second', 'minute', 'hour', 'day' and truncate with this value datetime objects before hashing it threshold_to_diff_deeper: float, default = 0.33 :ref:`threshold_to_diff_deeper_label` is a number between 0 and 1. When comparing dictionaries that have a small intersection of keys, we will report the dictionary as a new_value instead of reporting individual keys changed. If you set it to zero, you get the same results as DeepDiff 7.0.1 and earlier, which means this feature is disabled. The new default is 0.33 which means if less that one third of keys between dictionaries intersect, report it as a new object. use_enum_value: Boolean, default=False :ref:`use_enum_value_label` makes it so when diffing enum, we use the enum's value. It makes it so comparing an enum to a string or any other value is not reported as a type change. use_log_scale: Boolean, default=False :ref:`use_log_scale_label` along with :ref:`log_scale_similarity_threshold_label` can be used to ignore small changes in numbers by comparing their differences in logarithmic space. This is different than ignoring the difference based on significant digits. verbose_level: 2 >= int >= 0, default = 1 Higher verbose level shows you more details. For example verbose level 1 shows what dictionary item are added or removed. And verbose level 2 shows the value of the items that are added or removed too. view: string, default = text :ref:`view_label` Views are different "formats" of results. Each view comes with its own features. The choices are text (the default) and tree. The text view is the original format of the results. The tree view allows you to traverse through the tree of results. So you can traverse through the tree and see what items were compared to what. **Returns** A DeepDiff object that has already calculated the difference of the 2 items. The format of the object is chosen by the view parameter. **Supported data types** int, string, unicode, dictionary, list, tuple, set, frozenset, OrderedDict, NamedTuple, Numpy, custom objects and more! .. Note:: |:mega:| **Please fill out our** `fast 5-question survey `__ so that we can learn how & why you use DeepDiff, and what improvements we should make. Thank you! |:dancers:| qlustered-deepdiff-41c7265/deepdiff/docstrings/dsearch.rst000066400000000000000000000003361516241264500236430ustar00rootroot00000000000000:doc:`/index` .. _deepsearch_label: DeepSearch ========== .. toctree:: :maxdepth: 3 .. automodule:: deepdiff.search .. autoclass:: grep :members: .. autoclass:: DeepSearch :members: Back to :doc:`/index` qlustered-deepdiff-41c7265/deepdiff/docstrings/exclude_paths.rst000066400000000000000000000070141516241264500250620ustar00rootroot00000000000000:doc:`/index` .. _exclude_paths_label: Exclude Paths ============= Exclude part of your object tree from comparison. use exclude_paths and pass a set or list of paths to exclude, if only one item is being passed, then just put it there as a string. No need to pass it as a list then. Example >>> t1 = {"for life": "vegan", "ingredients": ["no meat", "no eggs", "no dairy"]} >>> t2 = {"for life": "vegan", "ingredients": ["veggies", "tofu", "soy sauce"]} >>> print (DeepDiff(t1, t2, exclude_paths="root['ingredients']")) # one item pass it as a string {} >>> print (DeepDiff(t1, t2, exclude_paths=["root['ingredients']", "root['ingredients2']"])) # multiple items pass as a list or a set. {} Also for root keys you don't have to pass as "root['key']". You can instead just pass the key: Example >>> t1 = {"for life": "vegan", "ingredients": ["no meat", "no eggs", "no dairy"]} >>> t2 = {"for life": "vegan", "ingredients": ["veggies", "tofu", "soy sauce"]} >>> print (DeepDiff(t1, t2, exclude_paths="ingredients")) # one item pass it as a string {} >>> print (DeepDiff(t1, t2, exclude_paths=["ingredients", "ingredients2"])) # multiple items pass as a list or a set. {} .. _include_paths_label: Include Paths ============= Only include this part of your object tree in the comparison. Use include_paths and pass a set or list of paths to limit diffing to only those paths. If only one item is being passed, just put it there as a string—no need to pass it as a list then. Example >>> t1 = {"for life": "vegan", "ingredients": ["no meat", "no eggs", "no dairy"]} >>> t2 = {"for life": "vegan", "ingredients": ["veggies", "tofu", "soy sauce"]} >>> print (DeepDiff(t1, t2, include_paths="root['for life']")) # one item pass it as a string {} >>> print (DeepDiff(t1, t2, include_paths=["for life", "ingredients2"])) # multiple items pass as a list or a set and you don't need to pass the full path when dealing with root keys. So instead of "root['for life']" you can pass "for life" {} When passing include_paths, all the children of that path will be included too. Example >>> t1 = { ... "foo": {"bar": "potato"}, ... "ingredients": ["no meat", "no eggs", "no dairy"] ... } >>> t2 = { ... "foo": {"bar": "banana"}, ... "ingredients": ["bread", "cheese"] ... } >>> DeepDiff(t1, t2, include_paths="foo") {'values_changed': {"root['foo']['bar']": {'new_value': 'banana', 'old_value': 'potato'}}} .. _exclude_regex_paths_label: Exclude Regex Paths ------------------- You can also exclude using regular expressions by using `exclude_regex_paths` and pass a set or list of path regexes to exclude. The items in the list could be raw regex strings or compiled regex objects. >>> import re >>> t1 = [{'a': 1, 'b': 2}, {'c': 4, 'b': 5}] >>> t2 = [{'a': 1, 'b': 3}, {'c': 4, 'b': 5}] >>> print(DeepDiff(t1, t2, exclude_regex_paths=r"root\[\d+\]\['b'\]")) {} >>> exclude_path = re.compile(r"root\[\d+\]\['b'\]") >>> print(DeepDiff(t1, t2, exclude_regex_paths=[exclude_path])) {} example 2: >>> t1 = {'a': [1, 2, [3, {'foo1': 'bar'}]]} >>> t2 = {'a': [1, 2, [3, {'foo2': 'bar'}]]} >>> DeepDiff(t1, t2, exclude_regex_paths="\['foo.'\]") # since it is one item in exclude_regex_paths, you don't have to put it in a list or a set. {} Tip: DeepDiff is using re.search on the path. So if you want to force it to match from the beginning of the path, add `^` to the beginning of regex. Back to :doc:`/index` qlustered-deepdiff-41c7265/deepdiff/docstrings/extract.rst000066400000000000000000000002051516241264500236770ustar00rootroot00000000000000:doc:`/index` .. _extract_label: Extract ======= .. automodule:: deepdiff.path .. autofunction:: extract Back to :doc:`/index` qlustered-deepdiff-41c7265/deepdiff/docstrings/faq.rst000066400000000000000000000153551516241264500230100ustar00rootroot00000000000000:doc:`/index` F.A.Q ===== .. |qluster_link| raw:: html Qluster .. admonition:: DeepDiff is now part of |qluster_link|. *If you're building workflows around data validation and correction,* `Qluster `__ *gives your team a structured way to manage rules, review failures, approve fixes, and reuse decisions—without building the entire system from scratch.* .. Note:: |:mega:| **Please fill out our** `fast 5-question survey `__ so that we can learn how & why you use DeepDiff, and what improvements we should make. Thank you! |:dancers:| Q: DeepDiff report is not precise when ignore_order=True -------------------------------------------------------- >>> from deepdiff import DeepDiff >>> from pprint import pprint >>> t1 = [ ... { ... "key": "some/pathto/customers/foo/", ... "flags": 0, ... "value": "" ... }, ... { ... "key": "some/pathto/customers/foo/account_number", ... "flags": 0, ... "value": "somevalue1" ... } ... ] >>> >>> t2 = [ ... { ... "key": "some/pathto/customers/foo/account_number", ... "flags": 0, ... "value": "somevalue2" ... }, ... { ... "key": "some/pathto/customers/foo/", ... "flags": 0, ... "value": "new" ... } ... ] >>> >>> pprint(DeepDiff(t1, t2)) {'values_changed': {"root[0]['key']": {'new_value': 'some/pathto/customers/foo/account_number', 'old_value': 'some/pathto/customers/foo/'}, "root[0]['value']": {'new_value': 'somevalue2', 'old_value': ''}, "root[1]['key']": {'new_value': 'some/pathto/customers/foo/', 'old_value': 'some/pathto/customers/foo/account_number'}, "root[1]['value']": {'new_value': 'new', 'old_value': 'somevalue1'}}} **Answer** This is explained in :ref:`cutoff_distance_for_pairs_label` and :ref:`cutoff_intersection_for_pairs_label` Bump up these 2 parameters to 1 and you get what you want: >>> pprint(DeepDiff(t1, t2, ignore_order=True, cutoff_distance_for_pairs=1, cutoff_intersection_for_pairs=1)) {'values_changed': {"root[0]['value']": {'new_value': 'new', 'old_value': ''}, "root[1]['value']": {'new_value': 'somevalue2', 'old_value': 'somevalue1'}}} Q: The report of changes in a nested dictionary is too granular --------------------------------------------------------------- **Answer** Use :ref:`threshold_to_diff_deeper_label` >>> from deepdiff import DeepDiff >>> t1 = {"veggie": "carrots"} >>> t2 = {"meat": "carrots"} >>> >>> DeepDiff(t1, t2, threshold_to_diff_deeper=0) {'dictionary_item_added': ["root['meat']"], 'dictionary_item_removed': ["root['veggie']"]} >>> DeepDiff(t1, t2, threshold_to_diff_deeper=0.33) {'values_changed': {'root': {'new_value': {'meat': 'carrots'}, 'old_value': {'veggie': 'carrots'}}}} Q: TypeError: Object of type type is not JSON serializable ---------------------------------------------------------- I'm trying to serialize the DeepDiff results into json and I'm getting the TypeError. >>> diff=DeepDiff(1, "a") >>> diff {'type_changes': {'root': {'old_type': , 'new_type': , 'old_value': 1, 'new_value': 'a'}}} >>> json.dumps(diff) Traceback (most recent call last): File "", line 1, in File ".../json/__init__.py", line 231, in dumps return _default_encoder.encode(obj) File ".../json/encoder.py", line 199, in encode chunks = self.iterencode(o, _one_shot=True) File ".../json/encoder.py", line 257, in iterencode return _iterencode(o, 0) File ".../json/encoder.py", line 179, in default raise TypeError(f'Object of type {o.__class__.__name__} ' TypeError: Object of type type is not JSON serializable **Answer** In order to serialize DeepDiff results into json, use to_json() >>> diff.to_json() '{"type_changes": {"root": {"old_type": "int", "new_type": "str", "old_value": 1, "new_value": "a"}}}' Q: How do I parse DeepDiff result paths? ---------------------------------------- **Answer** Use parse_path: >>> from deepdiff import parse_path >>> parse_path("root[1][2]['age']") [1, 2, 'age'] >>> parse_path("root[1][2]['age']", include_actions=True) [{'element': 1, 'action': 'GET'}, {'element': 2, 'action': 'GET'}, {'element': 'age', 'action': 'GET'}] >>> >>> parse_path("root['joe'].age") ['joe', 'age'] >>> parse_path("root['joe'].age", include_actions=True) [{'element': 'joe', 'action': 'GET'}, {'element': 'age', 'action': 'GETATTR'}] Or use the tree view so you can use path(output_format='list'): >>> t1 = {1:1, 2:2, 3:3, 4:{"a":"hello", "b":[1, 2, 3, 4]}} >>> t2 = {1:1, 2:2, 3:3, 4:{"a":"hello", "b":[1, 2]}} >>> ddiff = DeepDiff(t1, t2, view='tree') >>> ddiff {'iterable_item_removed': [, ]} >>> # Note that the iterable_item_removed is a set. In this case it has 2 items in it. >>> # One way to get one item from the set is to convert it to a list >>> # And then get the first item of the list: >>> removed = list(ddiff['iterable_item_removed'])[0] >>> removed >>> >>> parent = removed.up >>> parent >>> parent.path() # gives you the string representation of the path "root[4]['b']" >>> parent.path(output_format='list') # gives you the list of keys and attributes that make up the path [4, 'b'] Q: Why my datetimes are reported in UTC? ---------------------------------------- **Answer** DeepDiff converts all datetimes into UTC. If a datetime is timezone naive, we assume it is in UTC too. That is different than what Python does. Python assumes your timezone naive datetime is in your local timezone. However, you can override it to any other time zone such as your :ref:`default_timezone_label`. >>> from deepdiff import DeepDiff >>> from datetime import datetime, timezone >>> d1 = datetime(2020, 8, 31, 13, 14, 1) >>> d2 = datetime(2020, 8, 31, 13, 14, 1, tzinfo=timezone.utc) >>> d1 == d2 False >>> DeepDiff(d1, d2) {} >>> d3 = d2.astimezone(pytz.timezone('America/New_York')) >>> DeepDiff(d1, d3) {} >>> d1 == d3 False --------- Back to :doc:`/index` qlustered-deepdiff-41c7265/deepdiff/docstrings/ignore_order.rst000066400000000000000000000340101516241264500247040ustar00rootroot00000000000000:doc:`/index` .. _ignore_order_label: Ignore Order ============ DeepDiff by default compares objects in the order that it iterates through them in iterables. In other words if you have 2 lists, then the first item of the lists are compared to each other, then the 2nd items and so on. That makes DeepDiff be able to run in linear time. However, there are often times when you don't care about the order in which the items have appeared. In such cases DeepDiff needs to do way more work in order to find the differences. There are a couple of parameters provided to you to have full control over. List difference with ignore_order=False which is the default: >>> t1 = {1:1, 2:2, 3:3, 4:{"a":"hello", "b":[1, 2, 3]}} >>> t2 = {1:1, 2:2, 3:3, 4:{"a":"hello", "b":[1, 3, 2, 3]}} >>> ddiff = DeepDiff(t1, t2) >>> pprint (ddiff, indent = 2) { 'iterable_item_added': {"root[4]['b'][3]": 3}, 'values_changed': { "root[4]['b'][1]": {'new_value': 3, 'old_value': 2}, "root[4]['b'][2]": {'new_value': 2, 'old_value': 3}}} Ignore Order ------------ List difference ignoring order or duplicates: (with the same dictionaries as above) >>> t1 = {1:1, 2:2, 3:3, 4:{"a":"hello", "b":[1, 2, 3]}} >>> t2 = {1:1, 2:2, 3:3, 4:{"a":"hello", "b":[1, 3, 2, 3]}} >>> ddiff = DeepDiff(t1, t2, ignore_order=True) >>> print (ddiff) {} .. _ignore_order_func_label: Dynamic Ignore Order -------------------- Sometimes single *ignore_order* parameter is not enough to do a diff job, you can use *ignore_order_func* to determine whether the order of certain paths should be ignored List difference ignoring order with *ignore_order_func* >>> t1 = {"set": [1,2,3], "list": [1,2,3]} >>> t2 = {"set": [3,2,1], "list": [3,2,1]} >>> ddiff = DeepDiff(t1, t2, ignore_order_func=lambda level: "set" in level.path()) >>> print (ddiff) { 'values_changed': { "root['list'][0]": {'new_value': 3, 'old_value': 1}, "root['list'][2]": {'new_value': 1, 'old_value': 3}}} Ignoring order when certain word in the path >>> from deepdiff import DeepDiff >>> t1 = {'a': [1, 2], 'b': [3, 4]} >>> t2 = {'a': [2, 1], 'b': [4, 3]} >>> DeepDiff(t1, t2, ignore_order=True) {} >>> def ignore_order_func(level): ... return 'a' in level.path() ... >>> DeepDiff(t1, t2, ignore_order=True, ignore_order_func=ignore_order_func) {'values_changed': {"root['b'][0]": {'new_value': 4, 'old_value': 3}, "root['b'][1]": {'new_value': 3, 'old_value': 4}}} .. _report_repetition_label: Reporting Repetitions --------------------- List difference ignoring order and reporting repetitions: >>> from deepdiff import DeepDiff >>> from pprint import pprint >>> t1 = [1, 3, 1, 4] >>> t2 = [4, 4, 1] >>> ddiff = DeepDiff(t1, t2, ignore_order=True, report_repetition=True) >>> pprint(ddiff, indent=2) { 'iterable_item_removed': {'root[1]': 3}, 'repetition_change': { 'root[0]': { 'new_indexes': [2], 'new_repeat': 1, 'old_indexes': [0, 2], 'old_repeat': 2, 'value': 1}, 'root[3]': { 'new_indexes': [0, 1], 'new_repeat': 2, 'old_indexes': [3], 'old_repeat': 1, 'value': 4}}} .. _max_passes_label: Max Passes ---------- max_passes: Integer, default = 10000000 Maximum number of passes to run on objects to pin point what exactly is different. This is only used when ignore_order=True If you have deeply nested objects, DeepDiff needs to run multiple passes in order to pin point the difference. That can dramatically increase the time spent to find the difference. You can control the maximum number of passes that can be run via the max_passes parameter. .. note:: The definition of pass is whenever 2 iterable objects are being compared with each other and deepdiff decides to compare every single element of one iterable with every single element of the other iterable. Refer to :ref:`cutoff_distance_for_pairs_label` and :ref:`cutoff_intersection_for_pairs_label` for more info on how DeepDiff decides to start a new pass. Max Passes Example >>> from pprint import pprint >>> from deepdiff import DeepDiff >>> >>> t1 = [ ... { ... 'key3': [[[[[1, 2, 4, 5]]]]], ... 'key4': [7, 8], ... }, ... { ... 'key5': 'val5', ... 'key6': 'val6', ... }, ... ] >>> >>> t2 = [ ... { ... 'key5': 'CHANGE', ... 'key6': 'val6', ... }, ... { ... 'key3': [[[[[1, 3, 5, 4]]]]], ... 'key4': [7, 8], ... }, ... ] >>> >>> for max_passes in (1, 2, 62, 65): ... diff = DeepDiff(t1, t2, ignore_order=True, max_passes=max_passes, verbose_level=2) ... print('-\n----- Max Passes = {} -----'.format(max_passes)) ... pprint(diff) ... DeepDiff has reached the max number of passes of 1. You can possibly get more accurate results by increasing the max_passes parameter. - ----- Max Passes = 1 ----- {'values_changed': {'root[0]': {'new_value': {'key5': 'CHANGE', 'key6': 'val6'}, 'old_value': {'key3': [[[[[1, 2, 4, 5]]]]], 'key4': [7, 8]}}, 'root[1]': {'new_value': {'key3': [[[[[1, 3, 5, 4]]]]], 'key4': [7, 8]}, 'old_value': {'key5': 'val5', 'key6': 'val6'}}}} DeepDiff has reached the max number of passes of 2. You can possibly get more accurate results by increasing the max_passes parameter. - ----- Max Passes = 2 ----- {'values_changed': {"root[0]['key3'][0]": {'new_value': [[[[1, 3, 5, 4]]]], 'old_value': [[[[1, 2, 4, 5]]]]}, "root[1]['key5']": {'new_value': 'CHANGE', 'old_value': 'val5'}}} DeepDiff has reached the max number of passes of 62. You can possibly get more accurate results by increasing the max_passes parameter. - ----- Max Passes = 62 ----- {'values_changed': {"root[0]['key3'][0][0][0][0]": {'new_value': [1, 3, 5, 4], 'old_value': [1, 2, 4, 5]}, "root[1]['key5']": {'new_value': 'CHANGE', 'old_value': 'val5'}}} DeepDiff has reached the max number of passes of 65. You can possibly get more accurate results by increasing the max_passes parameter. - ----- Max Passes = 65 ----- {'values_changed': {"root[0]['key3'][0][0][0][0][1]": {'new_value': 3, 'old_value': 2}, "root[1]['key5']": {'new_value': 'CHANGE', 'old_value': 'val5'}}} .. note:: If there are potential passes left to be run and the max_passes value is reached, DeepDiff will issue a warning. However the most accurate result might have already been found when there are still potential passes left to be run. For example in the above example at max_passes=64, DeepDiff finds the optimal result however it has one more pass to go before it has run all the potential passes. Hence just for the sake of example we are using max_passes=65 as an example of a number that doesn't issue warnings. .. note:: Also take a look at :ref:`max_passes_label` .. _cutoff_distance_for_pairs_label: Cutoff Distance For Pairs ------------------------- cutoff_distance_for_pairs : 1 >= float >= 0, default=0.3 What is the threshold to consider 2 items as potential pairs. Note that it is only used when ignore_order = True. cutoff_distance_for_pairs in combination with :ref:`cutoff_intersection_for_pairs_label` are the parameters that decide whether 2 objects to be paired with each other during ignore_order=True algorithm or not. Note that these parameters are mainly used for nested iterables. For example by going from the default of cutoff_distance_for_pairs=0.3 to 0.1, we have essentially disallowed the 1.0 and 20.0 to be paired with each other. As you can see, DeepDiff has decided that the :ref:`deep_distance_label` of 1.0 and 20.0 to be around 0.27. Since that is way above cutoff_distance_for_pairs of 0.1, the 2 items are not paired. As a result the lists containing the 2 numbers are directly compared with each other: >>> from deepdiff import DeepDiff >>> t1 = [[1.0]] >>> t2 = [[20.0]] >>> DeepDiff(t1, t2, ignore_order=True, cutoff_distance_for_pairs=0.3) {'values_changed': {'root[0][0]': {'new_value': 20.0, 'old_value': 1.0}}} >>> DeepDiff(t1, t2, ignore_order=True, cutoff_distance_for_pairs=0.1) {'values_changed': {'root[0]': {'new_value': [20.0], 'old_value': [1.0]}}} >>> DeepDiff(1.0, 20.0, get_deep_distance=True) {'values_changed': {'root': {'new_value': 20.0, 'old_value': 1.0}}, 'deep_distance': 0.2714285714285714} .. _cutoff_intersection_for_pairs_label: Cutoff Intersection For Pairs ----------------------------- cutoff_intersection_for_pairs : 1 >= float >= 0, default=0.7 What is the threshold to calculate pairs of items between 2 iterables. For example 2 iterables that have nothing in common, do not need their pairs to be calculated. Note that it is only used when ignore_order = True. Behind the scene DeepDiff takes the :ref:`deep_distance_label` of objects when running ignore_order=True. The distance is between zero and 1. A distance of zero means the items are equal. A distance of 1 means they are 100% different. When comparing iterables, the cutoff_intersection_for_pairs is used to decide whether to compare every single item in each iterable with every single item in the other iterable or not. If the distance between the 2 iterables is equal or bigger than the cutoff_intersection_for_pairs, then the 2 iterables items are only compared as added or removed items and NOT modified items. However, if the distance between 2 iterables is below the cutoff, every single item from each iterable will be compared to every single item from the other iterable to find the closest "pair" of each item. .. note:: The process of comparing every item to the other is very expensive so :ref:`cutoff_intersection_for_pairs_label` in combination with :ref:`cutoff_distance_for_pairs_label` is used to give acceptable results with much higher speed. With a low cutoff_intersection_for_pairs, the 2 iterables above will be considered too far off from each other to get the individual pairs of items. So numbers that are not only related to each other via their positions in the lists and not their values are paired together in the results. >>> t1 = [1.0, 2.0, 3.0, 4.0, 5.0] >>> t2 = [5.0, 3.01, 1.2, 2.01, 4.0] >>> >>> DeepDiff(t1, t2, ignore_order=True, cutoff_intersection_for_pairs=0.1) {'values_changed': {'root[1]': {'new_value': 3.01, 'old_value': 2.0}, 'root[2]': {'new_value': 1.2, 'old_value': 3.0}}, 'iterable_item_added': {'root[3]': 2.01}, 'iterable_item_removed': {'root[0]': 1.0}} With the cutoff_intersection_for_pairs of 0.7 (which is the default value), the 2 iterables will be considered close enough to get pairs of items between the 2. So 2.0 and 2.01 are paired together for example. >>> t1 = [1.0, 2.0, 3.0, 4.0, 5.0] >>> t2 = [5.0, 3.01, 1.2, 2.01, 4.0] >>> >>> DeepDiff(t1, t2, ignore_order=True, cutoff_intersection_for_pairs=0.7) {'values_changed': {'root[2]': {'new_value': 3.01, 'old_value': 3.0}, 'root[0]': {'new_value': 1.2, 'old_value': 1.0}, 'root[1]': {'new_value': 2.01, 'old_value': 2.0}}} As an example of how much this parameter can affect the results in deeply nested objects, please take a look at :ref:`distance_and_diff_granularity_label`. .. _iterable_compare_func_label2: Iterable Compare Func --------------------- New in DeepDiff 5.5.0 There are times that we want to guide DeepDiff as to what items to compare with other items. In such cases we can pass a `iterable_compare_func` that takes a function pointer to compare two items. The function takes three parameters (x, y, level) and should return `True` if it is a match, `False` if it is not a match or raise `CannotCompare` if it is unable to compare the two. For example take the following objects: >>> from deepdiff import DeepDiff >>> from deepdiff.helper import CannotCompare >>> >>> t1 = [ ... { ... 'id': 1, ... 'value': [1] ... }, ... { ... 'id': 2, ... 'value': [7, 8, 1] ... }, ... { ... 'id': 3, ... 'value': [7, 8], ... }, ... ] >>> >>> t2 = [ ... { ... 'id': 2, ... 'value': [7, 8] ... }, ... { ... 'id': 3, ... 'value': [7, 8, 1], ... }, ... { ... 'id': 1, ... 'value': [1] ... }, ... ] >>> >>> DeepDiff(t1, t2, ignore_order=True) {'values_changed': {"root[2]['id']": {'new_value': 2, 'old_value': 3}, "root[1]['id']": {'new_value': 3, 'old_value': 2}}} Now let's define a compare_func that takes 3 parameters: x, y and level. >>> def compare_func(x, y, level=None): ... try: ... return x['id'] == y['id'] ... except Exception: ... raise CannotCompare() from None ... >>> DeepDiff(t1, t2, ignore_order=True, iterable_compare_func=compare_func) {'iterable_item_added': {"root[2]['value'][2]": 1}, 'iterable_item_removed': {"root[1]['value'][2]": 1}} As you can see the results are different. Now items with the same ids are compared with each other. .. note:: The level parameter of the iterable_compare_func is only used when ignore_order=False. Back to :doc:`/index` qlustered-deepdiff-41c7265/deepdiff/docstrings/ignore_types_or_values.rst000066400000000000000000000435021516241264500270220ustar00rootroot00000000000000:doc:`/index` Ignore Types Or Values ====================== DeepDiff provides numerous functionalities for the user to be able to define what paths, item types etc. to be included or ignored during the diffing process. As an example, you may have a type change in your objects: Type change >>> t1 = {1:1, 2:2, 3:3, 4:{"a":"hello", "b":[1, 2, 3]}} >>> t2 = {1:1, 2:2, 3:3, 4:{"a":"hello", "b":"world\n\n\nEnd"}} >>> ddiff = DeepDiff(t1, t2) >>> pprint (ddiff, indent = 2) { 'type_changes': { "root[4]['b']": { 'new_type': , 'new_value': 'world\n\n\nEnd', 'old_type': , 'old_value': [1, 2, 3]}}} And if you don't care about the value of items that have changed type, you can set verbose level to 0 >>> t1 = {1:1, 2:2, 3:3} >>> t2 = {1:1, 2:"2", 3:3} >>> pprint(DeepDiff(t1, t2, verbose_level=0), indent=2) { 'type_changes': { 'root[2]': { 'new_type': , 'old_type': }}} But what if you did not care about the integer becoming a string with the same value? What if you didn't want 2 -> "2" to be considered a type or value change? Throughout this page you will find different examples of functionalities that can help you achieve what you want. .. _exclude_types_label: Exclude Types ------------- exclude_types: list, default = None List of object types to exclude from the report. Exclude certain types from comparison: >>> l1 = logging.getLogger("test") >>> l2 = logging.getLogger("test2") >>> t1 = {"log": l1, 2: 1337} >>> t2 = {"log": l2, 2: 1337} >>> print(DeepDiff(t1, t2, exclude_types={logging.Logger})) {} .. _ignore_string_type_changes_label: Ignore String Type Changes -------------------------- ignore_string_type_changes: Boolean, default = False Whether to ignore string type changes or not. For example b"Hello" vs. "Hello" are considered the same if ignore_string_type_changes is set to True. >>> DeepDiff(b'hello', 'hello', ignore_string_type_changes=True) {} >>> DeepDiff(b'hello', 'hello') {'type_changes': {'root': {'old_type': , 'new_type': , 'old_value': b'hello', 'new_value': 'hello'}}} .. _ignore_numeric_type_changes_label: Ignore Numeric Type Changes --------------------------- ignore_numeric_type_changes: Boolean, default = False Whether to ignore numeric type changes or not. For example 10 vs. 10.0 are considered the same if ignore_numeric_type_changes is set to True. Example with Decimal >>> from decimal import Decimal >>> from deepdiff import DeepDiff >>> >>> t1 = Decimal('10.01') >>> t2 = 10.01 >>> >>> DeepDiff(t1, t2) {'type_changes': {'root': {'old_type': , 'new_type': , 'old_value': Decimal('10.01'), 'new_value': 10.01}}} >>> DeepDiff(t1, t2, ignore_numeric_type_changes=True) {} Note that this parameter only works for comparing numbers with numbers. If you compare a number to a string value of the number, this parameter does not solve your problem: Example with Fraction >>> from fractions import Fraction >>> from deepdiff import DeepDiff >>> >>> t1 = Fraction(1, 2) >>> t2 = 0.5 >>> >>> DeepDiff(t1, t2) {'type_changes': {'root': {'old_type': , 'new_type': , 'old_value': Fraction(1, 2), 'new_value': 0.5}}} >>> DeepDiff(t1, t2, ignore_numeric_type_changes=True) {} Example: >>> t1 = Decimal('10.01') >>> t2 = "10.01" >>> >>> DeepDiff(t1, t2, ignore_numeric_type_changes=True) {'type_changes': {'root': {'old_type': , 'new_type': , 'old_value': Decimal('10.01'), 'new_value': '10.01'}}} If you face repeated patterns of comparing numbers to string values of numbers, you will want to preprocess your input to convert the strings into numbers before feeding it into DeepDiff. .. _ignore_type_in_groups_label: Ignore Type In Groups --------------------- ignore_type_in_groups: Tuple or List of Tuples, default = None Ignore type changes between members of groups of types. For example if you want to ignore type changes between float and decimals etc. Note that this is a more granular feature. While this feature is production ready for strings and numbers, it is still experimental with other custom lists of types, Hence it is recommended to use the shortcuts provided to you which are :ref:`ignore_string_type_changes_label` and :ref:`ignore_numeric_type_changes_label` unless you have a specific need beyond those 2 cases and you need do define your own ignore_type_in_groups. For example lets say you have specifically str and byte datatypes to be ignored for type changes. Then you have a couple of options: 1. Set ignore_string_type_changes=True. 2. Or set ignore_type_in_groups=[(str, bytes)]. Here you are saying if we detect one type to be str and the other one bytes, do not report them as type change. It is exactly as passing ignore_type_in_groups=[DeepDiff.strings] or ignore_type_in_groups=DeepDiff.strings . Now what if you want also typeA and typeB to be ignored when comparing against each other? 1. ignore_type_in_groups=[DeepDiff.strings, (typeA, typeB)] 2. or ignore_type_in_groups=[(str, bytes), (typeA, typeB)] Note: The example below shows you have to use this feature. For enum types, however, you can just use :ref:`use_enum_value_label` Example: Ignore Enum to string comparison >>> from deepdiff import DeepDiff >>> from enum import Enum >>> class MyEnum1(Enum): ... book = "book" ... cake = "cake" ... >>> DeepDiff("book", MyEnum1.book) {'type_changes': {'root': {'old_type': , 'new_type': , 'old_value': 'book', 'new_value': }}} >>> DeepDiff("book", MyEnum1.book, ignore_type_in_groups=[(Enum, str)]) {} Example: Ignore Type Number - Dictionary that contains float and integer. Note that this is exactly the same as passing ignore_numeric_type_changes=True. >>> from deepdiff import DeepDiff >>> from pprint import pprint >>> t1 = {1: 1, 2: 2.22} >>> t2 = {1: 1.0, 2: 2.22} >>> ddiff = DeepDiff(t1, t2) >>> pprint(ddiff, indent=2) { 'type_changes': { 'root[1]': { 'new_type': , 'new_value': 1.0, 'old_type': , 'old_value': 1}}} >>> ddiff = DeepDiff(t1, t2, ignore_type_in_groups=DeepDiff.numbers) >>> pprint(ddiff, indent=2) {} Example: Ignore Type Number - List that contains float and integer. Note that this is exactly the same as passing ignore_numeric_type_changes=True. >>> from deepdiff import DeepDiff >>> from pprint import pprint >>> t1 = [1, 2, 3] >>> t2 = [1.0, 2.0, 3.0] >>> ddiff = DeepDiff(t1, t2) >>> pprint(ddiff, indent=2) { 'type_changes': { 'root[0]': { 'new_type': , 'new_value': 1.0, 'old_type': , 'old_value': 1}, 'root[1]': { 'new_type': , 'new_value': 2.0, 'old_type': , 'old_value': 2}, 'root[2]': { 'new_type': , 'new_value': 3.0, 'old_type': , 'old_value': 3}}} >>> ddiff = DeepDiff(t1, t2, ignore_type_in_groups=DeepDiff.numbers) >>> pprint(ddiff, indent=2) {} You can pass a list of tuples or list of lists if you have various type groups. When t1 and t2 both fall under one of these type groups, the type change will be ignored. DeepDiff already comes with 2 groups: DeepDiff.strings and DeepDiff.numbers . If you want to pass both: >>> ignore_type_in_groups = [DeepDiff.strings, DeepDiff.numbers] ignore_type_in_groups example with custom objects: >>> class Burrito: ... bread = 'flour' ... def __init__(self): ... self.spicy = True ... >>> >>> class Taco: ... bread = 'flour' ... def __init__(self): ... self.spicy = True ... >>> >>> burrito = Burrito() >>> taco = Taco() >>> >>> burritos = [burrito] >>> tacos = [taco] >>> >>> DeepDiff(burritos, tacos, ignore_type_in_groups=[(Taco, Burrito)], ignore_order=True) {} .. note:: You can pass list of tuples of types to ignore_type_in_groups or you can put actual values in the tuples and ignore_type_in_groups will extract the type from them. The example below has used (1, 1.0) instead of (int, float), Ignoring string to None comparison: >>> from deepdiff import DeepDiff >>> import datetime >>> >>> t1 = [1, 2, 3, 'a', None] >>> t2 = [1.0, 2.0, 3.3, b'a', 'hello'] >>> DeepDiff(t1, t2, ignore_type_in_groups=[(1, 1.0), (None, str, bytes)]) {'values_changed': {'root[2]': {'new_value': 3.3, 'old_value': 3}}} >>> Ignoring datetime to string comparison >>> now = datetime.datetime(2020, 5, 5) >>> t1 = [1, 2, 3, 'a', now] >>> t2 = [1, 2, 3, 'a', 'now'] >>> DeepDiff(t1, t2, ignore_type_in_groups=[(str, bytes, datetime.datetime)]) {'values_changed': {'root[4]': {'new_value': 'now', 'old_value': datetime.datetime(2020, 5, 5, 0, 0)}}} .. _ignore_type_subclasses_label: Ignore Type Subclasses ---------------------- ignore_type_subclasses: Boolean, default = False Use ignore_type_subclasses=True so when ignoring type (class), the subclasses of that class are ignored too. .. Note:: ignore_type_subclasses was incorrectly doing the reverse of its job up until DeepDiff 6.7.1 Please make sure to flip it in your use cases, when upgrading from older versions to 7.0.0 or above. >>> from deepdiff import DeepDiff >>> class ClassA: ... def __init__(self, x, y): ... self.x = x ... self.y = y ... >>> class ClassB: ... def __init__(self, x): ... self.x = x ... >>> class ClassC(ClassB): ... pass ... >>> obj_a = ClassA(1, 2) >>> obj_c = ClassC(3) >>> >>> DeepDiff(obj_a, obj_c, ignore_type_in_groups=[(ClassA, ClassB)], ignore_type_subclasses=True) {'type_changes': {'root': {'old_type': , 'new_type': , 'old_value': <__main__.ClassA object at 0x10076a2e8>, 'new_value': <__main__.ClassC object at 0x10082f630>}}} >>> >>> DeepDiff(obj_a, obj_c, ignore_type_in_groups=[(ClassA, ClassB)], ignore_type_subclasses=False) {'values_changed': {'root.x': {'new_value': 3, 'old_value': 1}}, 'attribute_removed': [root.y]} .. _ignore_uuid_types_label: Ignore UUID Types ------------------ ignore_uuid_types: Boolean, default = False Whether to ignore UUID vs string type differences when comparing. When set to True, comparing a UUID object with its string representation will not report as a type change. Without ignore_uuid_types: >>> import uuid >>> from deepdiff import DeepDiff >>> test_uuid = uuid.UUID('12345678-1234-5678-1234-567812345678') >>> uuid_str = '12345678-1234-5678-1234-567812345678' >>> DeepDiff(test_uuid, uuid_str) {'type_changes': {'root': {'old_type': , 'new_type': , 'old_value': UUID('12345678-1234-5678-1234-567812345678'), 'new_value': '12345678-1234-5678-1234-567812345678'}}} With ignore_uuid_types=True: >>> DeepDiff(test_uuid, uuid_str, ignore_uuid_types=True) {} This works in both directions: >>> DeepDiff(uuid_str, test_uuid, ignore_uuid_types=True) {} The parameter works with nested structures like dictionaries and lists: >>> dict1 = {'id': test_uuid, 'name': 'test'} >>> dict2 = {'id': uuid_str, 'name': 'test'} >>> DeepDiff(dict1, dict2, ignore_uuid_types=True) {} Note that if the UUID and string represent different values, it will still report as a value change: >>> different_uuid = uuid.UUID('87654321-4321-8765-4321-876543218765') >>> DeepDiff(different_uuid, uuid_str, ignore_uuid_types=True) {'values_changed': {'root': {'old_value': UUID('87654321-4321-8765-4321-876543218765'), 'new_value': '12345678-1234-5678-1234-567812345678'}}} This parameter can be combined with other ignore flags: >>> data1 = {'id': test_uuid, 'name': 'TEST', 'count': 42} >>> data2 = {'id': uuid_str, 'name': 'test', 'count': 42.0} >>> DeepDiff(data1, data2, ignore_uuid_types=True, ignore_string_case=True, ignore_numeric_type_changes=True) {} .. _ignore_string_case_label: Ignore String Case ------------------ ignore_string_case: Boolean, default = False Whether to be case-sensitive or not when comparing strings. By settings ignore_string_case=False, strings will be compared case-insensitively. >>> DeepDiff(t1='Hello', t2='heLLO') {'values_changed': {'root': {'new_value': 'heLLO', 'old_value': 'Hello'}}} >>> DeepDiff(t1='Hello', t2='heLLO', ignore_string_case=True) {} Ignore Nan Inequality --------------------- ignore_nan_inequality: Boolean, default = False Read more at :ref:`ignore_nan_inequality_label` Whether to ignore float('nan') inequality in Python. .. _ignore_private_variables_label: Ignore Private Variables ------------------------ ignore_private_variables: Boolean, default = True Whether to exclude the private variables in the calculations or not. It only affects variables that start with double underscores (__). .. _exclude_obj_callback_label: Exclude Obj Callback -------------------- exclude_obj_callback: function, default = None A function that takes the object and its path and returns a Boolean. If True is returned, the object is excluded from the results, otherwise it is included. This is to give the user a higher level of control than one can achieve via exclude_paths, exclude_regex_paths or other means. >>> def exclude_obj_callback(obj, path): ... return True if "skip" in path or isinstance(obj, int) else False ... >>> t1 = {"x": 10, "y": "b", "z": "c", "skip_1": 0} >>> t2 = {"x": 12, "y": "b", "z": "c", "skip_2": 0} >>> DeepDiff(t1, t2, exclude_obj_callback=exclude_obj_callback) {} .. _exclude_obj_callback_strict_label: Exclude Obj Callback Strict --------------------------- exclude_obj_callback_strict: function, default = None A function that works the same way as exclude_obj_callback, but excludes elements from the result only if the function returns True for both elements >>> def exclude_obj_callback_strict(obj, path): ... return True if isinstance(obj, int) and obj > 10 else False ... >>> t1 = {"x": 10, "y": "b", "z": "c"} >>> t2 = {"x": 12, "y": "b", "z": "c"} >>> DeepDiff(t1, t2, exclude_obj_callback=exclude_obj_callback_strict) {} >>> DeepDiff(t1, t2, exclude_obj_callback_strict=exclude_obj_callback_strict) {'values_changed': {"root['x']": {'new_value': 12, 'old_value': 10}}} .. _include_obj_callback_label: Include Obj Callback -------------------- include_obj_callback: function, default = None A function that takes the object and its path and returns a Boolean. If True is returned, the object is included in the results, otherwise it is excluded. This is to give the user a higher level of control than one can achieve via include_paths. >>> def include_obj_callback(obj, path): ... return True if "include" in path or isinstance(obj, int) else False ... >>> t1 = {"x": 10, "y": "b", "z": "c", "include_me": "a"} >>> t2 = {"x": 10, "y": "b", "z": "c", "include_me": "b"} >>> DeepDiff(t1, t2, include_obj_callback=include_obj_callback) {'values_changed': {"root['include_me']": {'new_value': "b", 'old_value': "a"}}} .. _include_obj_callback_strict_label: Include Obj Callback Strict --------------------------- include_obj_callback_strict: function, default = None A function that works the same way as include_obj_callback, but includes elements in the result only if the function returns True for both elements. >>> def include_obj_callback_strict(obj, path): ... return True if isinstance(obj, int) and obj > 10 else False ... >>> t1 = {"x": 10, "y": "b", "z": "c"} >>> t2 = {"x": 12, "y": "b", "z": "c"} >>> DeepDiff(t1, t2, include_obj_callback=include_obj_callback_strict) {'values_changed': {"root['x']": {'new_value': 12, 'old_value': 10}}} >>> DeepDiff(t1, t2, include_obj_callback_strict=include_obj_callback_strict) {} .. _truncate_datetime_label: Truncate Datetime ----------------- truncate_datetime: string, default = None truncate_datetime can take value one of 'second', 'minute', 'hour', 'day' and truncate with this value datetime objects before hashing it >>> import datetime >>> from deepdiff import DeepDiff >>> d1 = {'a': datetime.datetime(2020, 5, 17, 22, 15, 34, 913070)} >>> d2 = {'a': datetime.datetime(2020, 5, 17, 22, 15, 39, 296583)} >>> DeepDiff(d1, d2, truncate_datetime='minute') {} .. _use_enum_value_label: Use Enum Value -------------- use_enum_value: Boolean, default=False Makes it so when diffing enum, we use the enum's value. It makes it so comparing an enum to a string or any other value is not reported as a type change. >>> from enum import Enum >>> from deepdiff import DeepDiff >>> >>> class MyEnum2(str, Enum): ... book = "book" ... cake = "cake" ... >>> DeepDiff("book", MyEnum2.book) {'type_changes': {'root': {'old_type': , 'new_type': , 'old_value': 'book', 'new_value': }}} >>> DeepDiff("book", MyEnum2.book, use_enum_value=True) {} Back to :doc:`/index` qlustered-deepdiff-41c7265/deepdiff/docstrings/index.rst000066400000000000000000000120651516241264500233430ustar00rootroot00000000000000.. DeepDiff documentation master file, created by sphinx-quickstart on Mon Jul 20 06:06:44 2015. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. DeepDiff 9.0.0 documentation! ============================= .. |qluster_link| raw:: html Qluster .. admonition:: DeepDiff is now part of |qluster_link|. *If you're building workflows around data validation and correction,* `Qluster `__ *gives your team a structured way to manage rules, review failures, approve fixes, and reuse decisions—without building the entire system from scratch.* ******* Modules ******* The DeepDiff library includes the following modules: - **DeepDiff** For Deep Difference of 2 objects. :doc:`/diff` It returns the deep difference of python objects. It can also be used to take the distance between objects. :doc:`/deep_distance` - **DeepSearch** Search for objects within other objects. :doc:`/dsearch` - **DeepHash** Hash any object based on their content even if they are not "hashable" in Python's eyes. :doc:`/deephash` - **Delta** Delta of objects that can be applied to other objects. Imagine git commits but for structured data. :doc:`/delta` - **Extract** For extracting a path from an object :doc:`/extract` - **Commandline** Most of the above functionality is also available via the commandline module :doc:`/commandline` *********** What Is New *********** DeepDiff 9-0-0 -------------- - migration note: - `to_dict()` and `to_json()` now accept a `verbose_level` parameter and always return a usable text-view dict. When the original view is `'tree'`, they default to `verbose_level=2` for full detail. The old `view_override` parameter is removed. To get the previous results, you will need to pass the explicit verbose_level to `to_json` and `to_dict` if you are using the tree view. - Dropping support for Python 3.9 - Support for python 3.14 - Added support for callable ``group_by`` thanks to `echan5 `__ - Added ``FlatDeltaDict`` TypedDict for ``to_flat_dicts`` return type - Fixed colored view display when all list items are removed thanks to `yannrouillard `__ - Fixed ``hasattr()`` swallowing ``AttributeError`` in ``__slots__`` handling for objects with ``__getattr__`` thanks to `tpvasconcelos `__ - Fixed ``ignore_order=True`` missing int-vs-float type changes - Always use t1 path for reporting thanks to `devin13cox `__ - Fixed ``_convert_oversized_ints`` failing on NamedTuples - Fixed orjson ``TypeError`` for integers exceeding 64-bit range - Fixed parameter bug in ``to_flat_dicts`` where ``include_action_in_path`` and ``report_type_changes`` were not being passed through - Fixed ``ignore_keys`` issue in ``detailed__dict__`` thanks to `vitalis89 `__ - Fixed logarithmic similarity type hint thanks to `ljames8 `__ - Added ``Fraction`` numeric support thanks to `akshat62 `__ ********* Tutorials ********* Tutorials can be found on `Zepworks blog `_ ************ Installation ************ Install from PyPi:: pip install deepdiff If you want to use DeepDiff from commandline:: pip install "deepdiff[cli]" If you want to improve the performance of DeepDiff with certain processes such as json serialization:: pip install "deepdiff[optimize]" Read about DeepDiff optimizations at :ref:`optimizations_label` Importing --------- .. code:: python >>> from deepdiff import DeepDiff # For Deep Difference of 2 objects >>> from deepdiff import grep, DeepSearch # For finding if item exists in an object >>> from deepdiff import DeepHash # For hashing objects based on their contents >>> from deepdiff import Delta # For creating delta of objects that can be applied later to other objects. >>> from deepdiff import extract # For extracting a path from an object .. note:: if you want to use DeepDiff via commandline, make sure to run:: pip install "deepdiff[cli]" Then you can access the commands via: - DeepDiff .. code:: bash $ deep diff --help - Delta .. code:: bash $ deep patch --help - grep .. code:: bash $ deep grep --help - extract .. code:: bash $ deep extract --help Supported data types -------------------- int, string, unicode, dictionary, list, tuple, set, frozenset, OrderedDict, NamedTuple, Numpy, custom objects and more! References ========== .. toctree:: :maxdepth: 4 diff dsearch deephash delta extract colored_view commandline changelog authors faq support Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` qlustered-deepdiff-41c7265/deepdiff/docstrings/numbers.rst000066400000000000000000000222151516241264500237050ustar00rootroot00000000000000:doc:`/index` Numbers ======= When dealing with numbers, DeepDiff provides the following functionalities: .. _significant_digits_label: Significant Digits ------------------ significant_digits : int >= 0, default=None significant_digits defines the number of digits AFTER the decimal point to be used in the comparison. However you can override that by setting the number_format_notation="e" which will make it mean the digits in scientific notation. .. note:: Setting significant_digits will affect ANY number comparison. If ignore_numeric_type_changes is set to True and you have left significant_digits to the default of None, it gets automatically set to 55. The reason is that normally when numbers from 2 different types are compared, instead of comparing the values, we only report the type change. However when ignore_numeric_type_changes=True, in order compare numbers from different types to each other, we need to convert them all into strings. The significant_digits will be used to make sure we accurately convert all the numbers into strings in order to report the changes between them. .. note:: significant_digits by default uses "{:.Xf}".format(Your Number) behind the scene to compare numbers where X=significant_digits when the number_format_notation is left as the default of "f" meaning fixed point. As a side note, please pay attention that adding digits to your floating point can result in small differences in the results. For example: "{:.3f}".format(1.1135) = 1.113, but "{:.3f}".format(1.11351) = 1.114 For Decimals, Python's format rounds 2.5 to 2 and 3.5 to 4 (to the closest even number) .. note:: To override what significant digits mean and switch it to scientific notation, use number_format_notation="e" Behind the scene that switches DeepDiff to use "{:.Xe}".format(Your Number) where X=significant_digits. **Examples:** Approximate decimals comparison (Significant digits after the point): >>> t1 = Decimal('1.52') >>> t2 = Decimal('1.57') >>> DeepDiff(t1, t2, significant_digits=0) {} >>> DeepDiff(t1, t2, significant_digits=1) {'values_changed': {'root': {'new_value': Decimal('1.57'), 'old_value': Decimal('1.52')}}} Approximate fractions comparison (Significant digits after the point): >>> from fractions import Fraction >>> t1 = Fraction(22, 7) # 3.142857... >>> t2 = Fraction(355, 113) # 3.141592... >>> DeepDiff(t1, t2, significant_digits=2) {} >>> DeepDiff(t1, t2, significant_digits=3) {'values_changed': {'root': {'new_value': Fraction(355, 113), 'old_value': Fraction(22, 7)}}} Approximate float comparison (Significant digits after the point): >>> t1 = [ 1.1129, 1.3359 ] >>> t2 = [ 1.113, 1.3362 ] >>> pprint(DeepDiff(t1, t2, significant_digits=3)) {} >>> pprint(DeepDiff(t1, t2)) {'values_changed': {'root[0]': {'new_value': 1.113, 'old_value': 1.1129}, 'root[1]': {'new_value': 1.3362, 'old_value': 1.3359}}} >>> pprint(DeepDiff(1.23*10**20, 1.24*10**20, significant_digits=1)) {'values_changed': {'root': {'new_value': 1.24e+20, 'old_value': 1.23e+20}}} .. _number_format_notation_label: Number Format Notation ---------------------- number_format_notation : string, default="f" number_format_notation is what defines the meaning of significant digits. The default value of "f" means the digits AFTER the decimal point. "f" stands for fixed point. The other option is "e" which stands for exponent notation or scientific notation. **Examples:** Approximate number comparison (significant_digits after the decimal point in scientific notation) >>> DeepDiff(1024, 1020, significant_digits=2, number_format_notation="f") # default is "f" {'values_changed': {'root': {'new_value': 1020, 'old_value': 1024}}} >>> DeepDiff(1024, 1020, significant_digits=2, number_format_notation="e") {} .. _number_to_string_func_label: Number To String Function ------------------------- number_to_string_func : function, default=None In many cases DeepDiff converts numbers to strings in order to compare them. For example when ignore_order=True, when significant digits parameter is defined or when the ignore_numeric_type_changes=True. In its simplest form, the number_to_string_func is "{:.Xf}".format(Your Number) where X is the significant digits and the number_format_notation is left as the default of "f" meaning fixed point. The number_to_string_func parameter gives the user the full control into overriding how numbers are converted to strings for comparison. The default function is defined in https://github.com/seperman/deepdiff/blob/master/deepdiff/helper.py and is called number_to_string. You can define your own custom function instead of the default one in the helper module. Defining your own number_to_string_func Lets say you want the numbers comparison happen only for numbers above 100 for some reason. >>> from deepdiff import DeepDiff >>> from deepdiff.helper import number_to_string >>> def custom_number_to_string(number, *args, **kwargs): ... number = 100 if number < 100 else number ... return number_to_string(number, *args, **kwargs) ... >>> t1 = [10, 12, 100000] >>> t2 = [50, 63, 100021] >>> DeepDiff(t1, t2, significant_digits=3, number_format_notation="e") {'values_changed': {'root[0]': {'new_value': 50, 'old_value': 10}, 'root[1]': {'new_value': 63, 'old_value': 12}}} >>> >>> DeepDiff(t1, t2, significant_digits=3, number_format_notation="e", ... number_to_string_func=custom_number_to_string) {} Ignore Numeric Type Changes --------------------------- ignore_numeric_type_changes: Boolean, default = False read more at :ref:`ignore_numeric_type_changes_label` .. _ignore_nan_inequality_label: Ignore Nan Inequality --------------------- ignore_nan_inequality: Boolean, default = False Whether to ignore float('nan') inequality in Python. Note that this is a cPython "feature". Some versions of Pypy3 have nan==nan where in cPython nan!=nan >>> float('nan') == float('nan') False >>> DeepDiff(float('nan'), float('nan')) {'values_changed': {'root': {'new_value': nan, 'old_value': nan}}} >>> DeepDiff(float('nan'), float('nan'), ignore_nan_inequality=True) {} .. _math_epsilon_label: Math Epsilon ------------ math_epsilon: Decimal, default = None math_epsilon uses Python's built in Math.isclose. It defines a tolerance value which is passed to math.isclose(). Any numbers that are within the tolerance will not report as being different. Any numbers outside of that tolerance will show up as different. For example for some sensor data derived and computed values must lie in a certain range. It does not matter that they are off by e.g. 1e-5. To check against that the math core module provides the valuable isclose() function. It evaluates the being close of two numbers to each other, with reference to an epsilon (abs_tol). This is superior to the format function, as it evaluates the mathematical representation and not the string representation. Example with Decimal: >>> from decimal import Decimal >>> d1 = {"a": Decimal("7.175")} >>> d2 = {"a": Decimal("7.174")} >>> DeepDiff(d1, d2, math_epsilon=0.01) {} Example with Fraction: >>> from fractions import Fraction >>> d1 = {"a": Fraction(7175, 1000)} >>> d2 = {"a": Fraction(7174, 1000)} >>> DeepDiff(d1, d2, math_epsilon=0.01) {} .. note:: math_epsilon cannot currently handle the hashing of values, which is done when :ref:`ignore_order_label` is True. .. _use_log_scale_label: Use Log Scale ------------- use_log_scale: Boolean, default=False use_log_scale along with :ref:`log_scale_similarity_threshold_label` can be used to ignore small changes in numbers by comparing their differences in logarithmic space. This is different than ignoring the difference based on significant digits. >>> from deepdiff import DeepDiff >>> t1 = {'foo': 110, 'bar': 306} >>> t2 = {'foo': 140, 'bar': 298} >>> >>> DeepDiff(t1, t2) {'values_changed': {"root['foo']": {'new_value': 140, 'old_value': 110}, "root['bar']": {'new_value': 298, 'old_value': 306}}} >>> DeepDiff(t1, t2, use_log_scale=True, log_scale_similarity_threshold=0.01) {'values_changed': {"root['foo']": {'new_value': 140, 'old_value': 110}, "root['bar']": {'new_value': 298, 'old_value': 306}}} >>> DeepDiff(t1, t2, use_log_scale=True, log_scale_similarity_threshold=0.1) {'values_changed': {"root['foo']": {'new_value': 140, 'old_value': 110}}} >>> DeepDiff(t1, t2, use_log_scale=True, log_scale_similarity_threshold=0.3) {} .. _log_scale_similarity_threshold_label: Log Scale Similarity Threshold ------------------------------ log_scale_similarity_threshold: float, default = 0.1 :ref:`use_log_scale_label` along with log_scale_similarity_threshold can be used to ignore small changes in numbers by comparing their differences in logarithmic space. This is different than ignoring the difference based on significant digits. See the example above. Performance Improvement of Numbers diffing ------------------------------------------ Take a look at :ref:`diffing_numbers_optimizations_label` Back to :doc:`/index` qlustered-deepdiff-41c7265/deepdiff/docstrings/optimizations.rst000066400000000000000000000336341516241264500251520ustar00rootroot00000000000000:doc:`/index` .. _optimizations_label: Optimizations ============= If you are dealing with large nested objects and ignore_order=True, chances are DeepDiff takes a while to calculate the diff. Here are some tips that may help you with optimizations and progress report. Optimized Libraries ------------------- If you dump DeepDiff or Delta objects as json, you can improve the performance by installing orjson. DeepDiff will automatically use orjson instead of Python's built-in json library to do json serialization. pip install "deepdiff[optimize]" Max Passes ---------- :ref:`max_passes_label` comes with the default of 10000000. If you don't need to exactly pinpoint the difference and you can get away with getting a less granular report, you can reduce the number of passes. It is recommended to get a diff of your objects with the defaults max_passes and take a look at the stats by running :ref:`get_stats_label` before deciding to reduce this number. In many cases reducing this number does not yield faster results. A new pass is started each time 2 iterables are compared in a way that every single item that is different from the first one is compared to every single item that is different in the second iterable. .. _max_diffs_label: Max Diffs --------- max_diffs: Integer, default = None max_diffs defined the maximum number of diffs to run on objects to pin point what exactly is different. This is only used when ignore_order=True. Every time 2 individual items are compared a diff is counted. The default value of None means there is no limit in the number of diffs that will take place. Any positive integer can make DeepDiff stop doing the calculations upon reaching that max_diffs count. You can run diffs and then :ref:`get_stats_label` to see how many diffs and passes have happened. >>> from deepdiff import DeepDiff >>> diff=DeepDiff(1, 2) >>> diff {'values_changed': {'root': {'new_value': 2, 'old_value': 1}}} >>> diff.get_stats() {'PASSES COUNT': 0, 'DIFF COUNT': 1, 'DISTANCE CACHE HIT COUNT': 0, 'MAX PASS LIMIT REACHED': False, 'MAX DIFF LIMIT REACHED': False} >>> diff=DeepDiff([[1,2]], [[2,3,1]]) >>> diff.get_stats() {'PASSES COUNT': 0, 'DIFF COUNT': 8, 'DISTANCE CACHE HIT COUNT': 0, 'MAX PASS LIMIT REACHED': False, 'MAX DIFF LIMIT REACHED': False} >>> diff=DeepDiff([[1,2]], [[2,3,1]], ignore_order=True) >>> diff.get_stats() {'PASSES COUNT': 3, 'DIFF COUNT': 6, 'DISTANCE CACHE HIT COUNT': 0, 'MAX PASS LIMIT REACHED': False, 'MAX DIFF LIMIT REACHED': False} .. note:: Compare :ref:`max_diffs_label` with :ref:`max_passes_label` .. _cache_size_label: Cache Size ---------- cache_size : int >= 0, default=0 Cache size to be used to improve the performance. A cache size of zero means it is disabled. Using the cache_size can dramatically improve the diff performance especially for the nested objects at the cost of more memory usage. However if cache hits rate is very low, having a cache actually reduces the performance. Cache Examples -------------- For example lets take a look at the performance of the benchmark_deeply_nested_a in the `DeepDiff-Benchmark repo `_ . No Cache ^^^^^^^^ With the no cache option we have the following stats: {'PASSES COUNT': 11234, 'DIFF COUNT': 107060, 'DISTANCE CACHE HIT COUNT': 0, 'MAX PASS LIMIT REACHED': False, 'MAX DIFF LIMIT REACHED': False, 'DURATION SEC': 10} Yes it has taken 10 seconds to do the diff! .. figure:: _static/benchmark_deeply_nested_a__3.8__ignore_order=True__cache_size=0__cache_tuning_sample_size=0__cutoff_intersection_for_pairs=1.png :alt: cache_size=0 cache_size=0 Cache Size 500 ^^^^^^^^^^^^^^ With a cache size of 500, we are doing the same diff in 2.5 seconds! And the memory usage has not changed. It is still hovering around 100Mb. {'PASSES COUNT': 3960, 'DIFF COUNT': 19469, 'DISTANCE CACHE HIT COUNT': 11847, 'MAX PASS LIMIT REACHED': False, 'MAX DIFF LIMIT REACHED': False, 'DURATION SEC': 2} As you can see the number of passes and diff counts have gone down and instead the distance cache hit count has gone up. .. figure:: _static/benchmark_deeply_nested_a__3.8__ignore_order=True__cache_size=500__cache_tuning_sample_size=0__cutoff_intersection_for_pairs=1.png :alt: cache_size=500 cache_size=500 Cache Size 500 and Cache Tuning Sample Size 500 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ With a cache size of 500, we set the :ref:`cache_tuning_sample_size_label` to be 500 too. And we have a slight improvement. we are doing the same diff in 2 seconds now. And the memory usage has not changed. It is still hovering around 100Mb. {'PASSES COUNT': 3960, 'DIFF COUNT': 19469, 'DISTANCE CACHE HIT COUNT': 11847, 'MAX PASS LIMIT REACHED': False, 'MAX DIFF LIMIT REACHED': False, 'DURATION SEC': 2} As you can see in this case none of the stats have changed compared to the previous stats. .. figure:: _static/benchmark_deeply_nested_a__3.8__ignore_order=True__cache_size=500__cache_tuning_sample_size=500__cutoff_intersection_for_pairs=1.png :alt: cache_size=500 cache_tuning_sample_size=500 cache_size=500 cache_tuning_sample_size=500 Cache Size of 5000 ^^^^^^^^^^^^^^^^^^ Let's pay a little attention to our stats. Particularly to 'DISTANCE CACHE HIT COUNT': 11847 and the fact that the memory usage has not changed so far. What if we bump the cache_size to 5000 and disable cache_tuning_sample_size? {'PASSES COUNT': 1486, 'DIFF COUNT': 6637, 'DISTANCE CACHE HIT COUNT': 3440, 'MAX PASS LIMIT REACHED': False, 'MAX DIFF LIMIT REACHED': False, 'DURATION SEC': 0} We get the result calculated below 1 second! And the memory usage is only slightly above 100Mb. .. figure:: _static/benchmark_deeply_nested_a__3.8__ignore_order=True__cache_size=5000__cache_tuning_sample_size=0__cutoff_intersection_for_pairs=1.png :alt: cache_size=5000 cache_size=5000 .. _cache_tuning_sample_size_label: Cache Tuning Sample Size ------------------------ cache_tuning_sample_size : int >= 0, default = 0 cache_tuning_sample_size is an experimental feature. It works hands in hands with the :ref:`cache_size_label`. When cache_tuning_sample_size is set to anything above zero, it will sample the cache usage with the passed sample size and decide whether to use the cache or not. And will turn it back on occasionally during the diffing process. This option can be useful if you are not sure if you need any cache or not. However you will gain much better performance with keeping this parameter zero and running your diff with different cache sizes and benchmarking to find the optimal cache size. .. note:: A good start with cache_tuning_sample_size is to set it to the size of your cache. .. _diffing_numbers_optimizations_label: Optimizations for Diffing Numbers --------------------------------- If you are diffing lists of python numbers, you could get performance improvement just by installing numpy. DeepDiff will use Numpy to improve the performance behind the scene. For example lets take a look at the performance of the benchmark_array_no_numpy vs. benchmark_numpy_array in the `DeepDiff-Benchmark repo `_. In this specific test, we have 2 lists of numbers that have nothing in common: `mat1 `_ and `mat2 `_ . No Cache and No Numpy ^^^^^^^^^^^^^^^^^^^^^ With the no cache option and no Numpy installed we have the following stats: {'PASSES COUNT': 1, 'DIFF COUNT': 439944, 'DISTANCE CACHE HIT COUNT': 0, 'MAX PASS LIMIT REACHED': False, 'MAX DIFF LIMIT REACHED': False, 'DURATION SEC': 30} Yes it has taken 30 seconds to do the diff! .. figure:: _static/benchmark_array_no_numpy__3.8__ignore_order=True__cache_size=0__cache_tuning_sample_size=0__cutoff_intersection_for_pairs=1.png :alt: cache_size=0 and no Numpy cache_size=0 and no Numpy Cache Size 10000 and No Numpy ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ What if we increase the cache size to 10000? {'PASSES COUNT': 1, 'DIFF COUNT': 439944, 'DISTANCE CACHE HIT COUNT': 0, 'MAX PASS LIMIT REACHED': False, 'MAX DIFF LIMIT REACHED': False, 'DURATION SEC': 35} Not only it didn't help, it increased the diff time by 15%!! Worse, if you look at the stats you see that the cache hit count is zero. This has happened since the 2 lists of items have nothing in common and hence caching the results does not improve the performance. .. figure:: _static/benchmark_array_no_numpy__3.8__ignore_order=True__cache_size=10000__cache_tuning_sample_size=0__cutoff_intersection_for_pairs=1.png :alt: cache_size=10000 and no Numpy cache_size=10000 and no Numpy No Cache and Numpy ^^^^^^^^^^^^^^^^^^ Let's install Numpy now. Set the cache_size=0 and run the diff again. Yay, the same diff is done in 5 seconds! {'PASSES COUNT': 1, 'DIFF COUNT': 1348, 'DISTANCE CACHE HIT COUNT': 0, 'MAX PASS LIMIT REACHED': False, 'MAX DIFF LIMIT REACHED': False, 'DURATION SEC': 5} As you can see the memory usage has gone up from around 500Mb to around 630Mb. .. figure:: _static/benchmark_numpy_array__3.8__ignore_order=True__cache_size=0__cache_tuning_sample_size=0__cutoff_intersection_for_pairs=1.png :alt: Numpy but no cache Numpy but no cache Pypy ---- If you are diffing big blobs of data that do not mainly include numbers, you may gain some performance improvement by running DeepDiff on Pypy3 instead of cPython. For example lets take a look at the performance of the benchmark_big_jsons in the `DeepDiff-Benchmark repo `_. First we will run it on cPython 3.8: It takes around 17.5 seconds and 40Mb of memory: .. figure:: _static/benchmark_big_jsons__3.8__ignore_order=True__cache_size=0__cache_tuning_sample_size=0__max_diffs=300000__max_passes=40000__cutoff_intersection_for_pairs=1.png :alt: Nested blob of text diffed in Python3.8 Nested blob of text diffed in Python3.8 And then we run it in Pypy3.6-7.3.0. It takes 12 seconds now but around 110Mb of memory. .. figure:: _static/benchmark_big_jsons__pypy3.6__ignore_order=True__cache_size=0__cache_tuning_sample_size=0__max_diffs=300000__max_passes=40000__cutoff_intersection_for_pairs=1.png :alt: Nested blob of text diffed in Pypy3.6-7.3.0 Nested blob of text diffed in Pypy3.6-7.3.0 .. note:: Note that if you are diffing numbers, and have Numpy installed as recommended, cPython will have a better performance than Pypy. But if you are diffing blobs of mixed strings and some numbers, Pypy will have a better CPU performance and worse memory usage. Cutoff Intersection For Pairs ----------------------------- :ref:`cutoff_intersection_for_pairs_label` which is only used when ignore_order=True can have a huge affect on the granularity of the results and the performance. A value of zero essentially stops DeepDiff from doing passes while a value of 1 forced DeepDiff to do passes on iterables even when they are very different. Running passes is an expensive operation. As an example of how much this parameter can affect the results in deeply nested objects, please take a look at :ref:`distance_and_diff_granularity_label`. .. _cache_purge_level: Cache Purge Level ----------------- cache_purge_level: int, 0, 1, or 2. default=1 cache_purge_level defines what objects in DeepDiff should be deleted to free the memory once the diff object is calculated. If this value is set to zero, most of the functionality of the diff object is removed and the most memory is released. A value of 1 preserves all the functionalities of the diff object. A value of 2 also preserves the cache and hashes that were calculated during the diff calculations. In most cases the user does not need to have those objects remained in the diff unless for investigation purposes. .. _zip_ordered_iterables_label: Zip Ordered Iterables --------------------- zip_ordered_iterables: Boolean, default = False When comparing ordered iterables such as lists, DeepDiff tries to find the smallest difference between the two iterables to report. That means that items in the two lists are not paired individually in the order of appearance in the iterables. Sometimes, that is not the desired behavior. Set this flag to True to make DeepDiff pair and compare the items in the iterables in the order they appear. >>> from pprint import pprint >>> from deepdiff import DeepDiff >>> t1 = ["a", "b", "d", "e"] >>> t2 = ["a", "b", "c", "d", "e"] >>> DeepDiff(t1, t2) {'iterable_item_added': {'root[2]': 'c'}} When this flag is set to True and ignore_order=False, diffing will be faster. >>> diff=DeepDiff(t1, t2, zip_ordered_iterables=True) >>> pprint(diff) {'iterable_item_added': {'root[4]': 'e'}, 'values_changed': {'root[2]': {'new_value': 'c', 'old_value': 'd'}, 'root[3]': {'new_value': 'd', 'old_value': 'e'}}} .. _threshold_to_diff_deeper_label: Threshold To Diff Deeper ------------------------ threshold_to_diff_deeper: float, default = 0.33 threshold_to_diff_deeper is a number between 0 and 1. When comparing dictionaries that have a small intersection of keys, we will report the dictionary as a new_value instead of reporting individual keys changed. If you set it to zero, you get the same results as DeepDiff 7.0.1 and earlier, which means this feature is disabled. The new default is 0.33 which means if less that one third of keys between dictionaries intersect, report it as a new object. >>> from deepdiff import DeepDiff >>> t1 = {"veggie": "carrots"} >>> t2 = {"meat": "carrots"} >>> >>> DeepDiff(t1, t2, threshold_to_diff_deeper=0) {'dictionary_item_added': ["root['meat']"], 'dictionary_item_removed': ["root['veggie']"]} >>> DeepDiff(t1, t2, threshold_to_diff_deeper=0.33) {'values_changed': {'root': {'new_value': {'meat': 'carrots'}, 'old_value': {'veggie': 'carrots'}}}} Back to :doc:`/index` qlustered-deepdiff-41c7265/deepdiff/docstrings/other.rst000066400000000000000000000062171516241264500233570ustar00rootroot00000000000000:doc:`/index` Other Parameters ================ .. _encodings_label: Encodings --------- significant_digits : int >= 0, default=None Character encodings to iterate through when we convert bytes into strings. You may want to pass an explicit list of encodings in your objects if you start getting UnicodeDecodeError from DeepHash. Also check out :ref:`ignore_encoding_errors_label` if you can get away with ignoring these errors and don't want to bother with an explicit list of encodings but it will come at the price of slightly less accuracy of the final results. Example: encodings=["utf-8", "latin-1"] The reason the decoding of bytes to string is needed is that when `ignore_order = True` we calculate the hash of the objects in order to facilitate in diffing them. In order to calculate the hash, we serialize all objects into strings. During the serialization we may encounter issues with character encodings. **Examples:** Comparing bytes that have non UTF-8 encoding: >>> from deepdiff import DeepDiff >>> item = b"\xbc cup of flour" >>> DeepDiff([b'foo'], [item], ignore_order=True) Traceback (most recent call last): raise UnicodeDecodeError( UnicodeDecodeError: 'utf-8' codec can't decode byte 0xbc in position 0: Can not produce a hash for root: invalid start byte in 'p of flo...'. Please either pass ignore_encoding_errors=True or pass the encoding via encodings=['utf-8', '...']. Let's try to pass both 'utf-8' and 'latin-1' as encodings to be tried: >>> DeepDiff([b'foo'], [item], encodings=['utf-8', 'latin-1'], ignore_order=True) {'values_changed': {'root[0]': {'new_value': b'\xbc cup of flour', 'old_value': b'foo'}}} .. _ignore_encoding_errors_label: Ignore Encoding Errors ---------------------- ignore_encoding_errors: Boolean, default = False If you want to get away with UnicodeDecodeError without passing explicit character encodings, set this option to True. If you want to make sure the encoding is done properly, keep this as False and instead pass an explicit list of character encodings to be considered via the encodings parameter. We can generally get the same results as above example if we just pass `ignore_encoding_errors=True`. However it comes at the cost of less accuracy of the results. >>> DeepDiff([b'foo'], [b"\xbc cup of flour"], ignore_encoding_errors=True, ignore_order=True) {'values_changed': {'root[0]': {'new_value': b'\xbc cup of flour', 'old_value': b'foo'}}} For example if we replace `foo` with ` cup of flour`, we have bytes that are only different in the problematic character. Ignoring that character means DeepDiff will consider these 2 strings to be equal since their hash becomes the same. Note that we only hash items when `ignore_order=True`. >>> DeepDiff([b" cup of flour"], [b"\xbc cup of flour"], ignore_encoding_errors=True, ignore_order=True) {} But if we had passed the proper encoding, it would have detected that these 2 bytes are different: >>> DeepDiff([b" cup of flour"], [b"\xbc cup of flour"], encodings=['latin-1'], ignore_order=True) {'values_changed': {'root[0]': {'new_value': b'\xbc cup of flour', 'old_value': b' cup of flour'}}} Back to :doc:`/index` qlustered-deepdiff-41c7265/deepdiff/docstrings/search_doc.rst000066400000000000000000000052001516241264500243170ustar00rootroot00000000000000:orphan: grep is a more user friendly interface for DeepSearch. It takes exactly the same arguments as DeepSearch except that you pipe the object into it instead of passing it as a parameter. It works just like grep in linux shell! **Parameters** item : The item to search for verbose_level : int >= 0, default = 1. Verbose level one shows the paths of found items. Verbose level 2 shows the path and value of the found items. exclude_paths: list, default = None. List of paths to exclude from the report. exclude_types: list, default = None. List of object types to exclude from the report. case_sensitive: Boolean, default = False match_string: Boolean, default = False If True, the value of the object or its children have to exactly match the item. If False, the value of the item can be a part of the value of the object or its children use_regexp: Boolean, default = False strict_checking: Boolean, default = True If True, it will check the type of the object to match, so when searching for '1234', it will NOT match the int 1234. Currently this only affects the numeric values searching. **Examples** Importing >>> from deepdiff import grep >>> from pprint import pprint Search in list for string >>> obj = ["long somewhere", "string", 0, "somewhere great!"] >>> item = "somewhere" >>> ds = obj | grep(item) >>> print(ds) {'matched_values': ['root[0]', 'root[3]']} Search in nested data for string >>> obj = ["something somewhere", {"long": "somewhere", "string": 2, 0: 0, "somewhere": "around"}] >>> item = "somewhere" >>> ds = obj | grep(item, verbose_level=2) >>> pprint(ds, indent=2) { 'matched_paths': {"root[1]['somewhere']": 'around'}, 'matched_values': { 'root[0]': 'something somewhere', "root[1]['long']": 'somewhere'}} You can also use regular expressions >>> obj = ["something here", {"long": "somewhere", "someone": 2, 0: 0, "somewhere": "around"}] >>> ds = obj | grep("some.*", use_regexp=True) >>> pprint(ds, indent=2) { 'matched_paths': ["root[1]['someone']", "root[1]['somewhere']"], 'matched_values': ['root[0]', "root[1]['long']"]} Change strict_checking to False to match numbers in strings and vice versa: >>> obj = {"long": "somewhere", "num": 1123456, 0: 0, "somewhere": "around"} >>> item = "1234" >>> result = {"matched_values": {"root['num']"}} >>> ds = obj | grep(item, verbose_level=1, use_regexp=True) >>> pprint(ds) {} >>> >>> ds = obj | grep(item, verbose_level=1, use_regexp=True, strict_checking=False) >>> pprint(ds) {'matched_values': ["root['num']"]} qlustered-deepdiff-41c7265/deepdiff/docstrings/serialization.rst000066400000000000000000000270601516241264500251120ustar00rootroot00000000000000:doc:`/index` .. _serialization_label: Serialization ============= .. _to_dict_label: To Dict ------- In order to convert the DeepDiff object into a normal Python dictionary, use the to_dict() method. The result is always a text-view dictionary regardless of the original view used to create the DeepDiff object. **Parameters** verbose_level: int, default=None Override the verbose_level for the serialized output. When None, the behavior depends on the original view: - If the original view is 'text', the verbose_level from DeepDiff initialization is used. - If the original view is 'tree', verbose_level=2 is used to provide the most detailed output. Valid values are 0, 1, or 2. Example: >>> t1 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": [1, 2, 3]}} >>> t2 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": "world\n\n\nEnd"}} >>> ddiff = DeepDiff(t1, t2) >>> ddiff.to_dict() {'type_changes': {"root[4]['b']": {'old_type': , 'new_type': , 'old_value': [1, 2, 3], 'new_value': 'world\n\n\nEnd'}}} When the original view is 'tree', to_dict() defaults to verbose_level=2 for the most detailed output: Example: >>> t1 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": [1, 2, 3]}} >>> t2 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": "world\n\n\nEnd"}} >>> ddiff = DeepDiff(t1, t2, view='tree') >>> ddiff.to_dict() {'type_changes': {"root[4]['b']": {'old_type': , 'new_type': , 'old_value': [1, 2, 3], 'new_value': 'world\n\n\nEnd'}}} You can also override the verbose_level: Example: >>> ddiff = DeepDiff(t1, t2, view='tree') >>> ddiff.to_dict(verbose_level=0) {'type_changes': {"root[4]['b']": {'old_type': , 'new_type': }}} .. _to_json_label: To Json ------- Dump json of the text view. In order to do safe json serialization, use the to_json() method. **Parameters** default_mapping : dictionary(optional), a dictionary of mapping of different types to json types. by default DeepDiff converts certain data types. For example Decimals into floats so they can be exported into json. If you have a certain object type that the json serializer can not serialize it, please pass the appropriate type conversion through this dictionary. verbose_level: int, default=None Override the verbose_level for the serialized output. Same behavior as to_dict(). kwargs: Any other kwargs you pass will be passed on to Python's json.dumps() Example 1 Serialize custom objects: >>> class A: ... pass ... >>> class B: ... pass ... >>> t1 = A() >>> t2 = B() >>> ddiff = DeepDiff(t1, t2) >>> ddiff.to_json() TypeError: We do not know how to convert <__main__.A object at 0x10648> of type for json serialization. Please pass the default_mapping parameter with proper mapping of the object to a basic python type. >>> default_mapping = {A: lambda x: 'obj A', B: lambda x: 'obj B'} >>> ddiff.to_json(default_mapping=default_mapping) '{"type_changes": {"root": {"old_type": "A", "new_type": "B", "old_value": "obj A", "new_value": "obj B"}}}' Example 2: >>> t1 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": [1, 2, 3]}} >>> t2 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": "world\n\n\nEnd"}} >>> ddiff = DeepDiff(t1, t2, view='tree') >>> ddiff.to_json() '{"type_changes": {"root[4][\'b\']": {"old_type": "list", "new_type": "str", "old_value": [1, 2, 3], "new_value": "world\\n\\n\\nEnd"}}}' .. _to_json_pickle_label: To Json Pickle -------------- If you want the original DeepDiff object to be serialized with all the bells and whistles, you can use the to_json_pickle() and from_json_pickle() in order to serialize and deserialize its results into json. Note that json_pickle is unsafe and json pickle dumps from untrusted sources should never be loaded. It is recommended not to use this serialization unless you have to. .. note:: You need to install the `jsonpickle `_ package to use the to_json_pickle() method. Serialize and then deserialize back to deepdiff >>> t1 = {1: 1, 2: 2, 3: 3} >>> t2 = {1: 1, 2: "2", 3: 3} >>> ddiff = DeepDiff(t1, t2) >>> jsoned = ddiff.to_json_pickle() >>> jsoned '{"type_changes": {"root[2]": {"new_type": {"py/type": "builtins.str"}, "new_value": "2", "old_type": {"py/type": "builtins.int"}, "old_value": 2}}}' >>> ddiff_new = DeepDiff.from_json_pickle(jsoned) >>> ddiff == ddiff_new True .. _from_json_pickle_label: From Json Pickle ---------------- Load the diff object from the json pickle dump. Take a look at the above :ref:`to_json_pickle_label` for an example. .. _delta_to_flat_rows_label: Delta Serialize To Flat Rows ---------------------------- Sometimes, it is desired to serialize a :ref:`delta_label` object to a list of flat rows. For example, to store them in relation databases. In that case, you can use the Delta.to_flat_rows to achieve the desired outcome. The rows are named tuples and can be converted to dictionaries using `._asdict()` >>> from pprint import pprint >>> from deepdiff import DeepDiff, Delta >>> t1 = {"key1": "value1"} >>> t2 = {"field2": {"key2": "value2"}} >>> diff = DeepDiff(t1, t2, verbose_level=2) >>> pprint(diff, indent=2) { 'dictionary_item_added': {"root['field2']": {'key2': 'value2'}}, 'dictionary_item_removed': {"root['key1']": 'value1'}} >>> delta = Delta(diff, bidirectional=True) >>> flat_rows = delta.to_flat_rows() >>> pprint(flat_rows, indent=2) [ FlatDeltaRow(path=['field2', 'key2'], action='dictionary_item_added', value='value2'), FlatDeltaRow(path=['key1'], action='dictionary_item_removed', value='value1')] .. note:: When converting a delta to flat rows, nested dictionaries that have single keys in them are flattened too. Notice that the diff object says { 'dictionary_item_added': {"root['field2']": {'key2': 'value2'}} but the flat row is: FlatDeltaRow(path=['field2', 'key2'], action='dictionary_item_added', value='value2') That means, when you recreate the delta from the flat rows, you need to set force=True to apply the delta: >>> t1 + delta == t2 True >>> t2 - delta == t1 True >>> delta2 = Delta(flat_rows_list=flat_rows, bidirectional=True) >>> t1 + delta2 == t2 Expected the old value for root['field2']['key2'] to be None but it is not found. Error found on: 'field2' False. You may want to set force=True, especially if this delta is created by passing flat_rows_list or flat_dict_list >>> t1 + delta {'field2': {'key2': 'value2'}} >>> t1 + delta2 {} >>> delta2 = Delta(flat_rows_list=flat_rows, bidirectional=True, force=True) # We need to set force=True >>> t1 + delta2 {'field2': {'key2': 'value2'}} >>> Flat Row Specs: class FlatDataAction(str, enum.Enum): values_changed = 'values_changed' type_changes = 'type_changes' set_item_added = 'set_item_added' set_item_removed = 'set_item_removed' dictionary_item_added = 'dictionary_item_added' dictionary_item_removed = 'dictionary_item_removed' iterable_item_added = 'iterable_item_added' iterable_item_removed = 'iterable_item_removed' iterable_item_moved = 'iterable_item_moved' iterable_items_inserted = 'iterable_items_inserted' # opcode iterable_items_deleted = 'iterable_items_deleted' # opcode iterable_items_replaced = 'iterable_items_replaced' # opcode iterable_items_equal = 'iterable_items_equal' # opcode attribute_removed = 'attribute_removed' attribute_added = 'attribute_added' unordered_iterable_item_added = 'unordered_iterable_item_added' unordered_iterable_item_removed = 'unordered_iterable_item_removed' UnkownValueCode = 'unknown___' class FlatDeltaRow(NamedTuple): path: List action: FlatDataAction value: Optional[Any] = UnkownValueCode old_value: Optional[Any] = UnkownValueCode type: Optional[Any] = UnkownValueCode old_type: Optional[Any] = UnkownValueCode new_path: Optional[List] = None t1_from_index: Optional[int] = None t1_to_index: Optional[int] = None t2_from_index: Optional[int] = None t2_to_index: Optional[int] = None .. _delta_to_flat_dicts_label: Delta Serialize To Flat Dictionaries ------------------------------------ Sometimes, it is desired to serialize a :ref:`delta_label` object to a list of flat dictionaries. For example, to store them in relation databases. In that case, you can use the Delta.to_flat_dicts to achieve the desired outcome. Since None is a valid value, we use a special hard-coded string to signify "unknown": 'unknown___' .. note:: Many new keys are added to the flat dicts in DeepDiff 7.0.0 You may want to use :ref:`delta_to_flat_rows_label` instead of flat dicts. For example: >>> from pprint import pprint >>> from deepdiff import DeepDiff, Delta >>> t1 = {"key1": "value1"} >>> t2 = {"field2": {"key2": "value2"}} >>> diff = DeepDiff(t1, t2, verbose_level=2) >>> pprint(diff, indent=2) { 'dictionary_item_added': {"root['field2']": {'key2': 'value2'}}, 'dictionary_item_removed': {"root['key1']": 'value1'}} >>> delta = Delta(diff, bidirectional=True) >>> flat_dicts = delta.to_flat_dicts() >>> pprint(flat_dicts, indent=2) [ { 'action': 'dictionary_item_added', 'new_path': None, 'old_type': 'unknown___', 'old_value': 'unknown___', 'path': ['field2', 'key2'], 't1_from_index': None, 't1_to_index': None, 't2_from_index': None, 't2_to_index': None, 'type': 'unknown___', 'value': 'value2'}, { 'action': 'dictionary_item_removed', 'new_path': None, 'old_type': 'unknown___', 'old_value': 'unknown___', 'path': ['key1'], 't1_from_index': None, 't1_to_index': None, 't2_from_index': None, 't2_to_index': None, 'type': 'unknown___', 'value': 'value1'}] Example 2: >>> t3 = ["A", "B"] >>> t4 = ["A", "B", "C", "D"] >>> diff = DeepDiff(t3, t4, verbose_level=2) >>> pprint(diff, indent=2) {'iterable_item_added': {'root[2]': 'C', 'root[3]': 'D'}} >>> >>> delta = Delta(diff, bidirectional=True) >>> flat_dicts = delta.to_flat_dicts() >>> pprint(flat_dicts, indent=2) [ { 'action': 'iterable_item_added', 'new_path': None, 'old_type': 'unknown___', 'old_value': 'unknown___', 'path': [2], 't1_from_index': None, 't1_to_index': None, 't2_from_index': None, 't2_to_index': None, 'type': 'unknown___', 'value': 'C'}, { 'action': 'iterable_item_added', 'new_path': None, 'old_type': 'unknown___', 'old_value': 'unknown___', 'path': [3], 't1_from_index': None, 't1_to_index': None, 't2_from_index': None, 't2_to_index': None, 'type': 'unknown___', 'value': 'D'}] .. _delta_from_flat_dicts_label: Delta Load From Flat Dictionaries ------------------------------------ >>> from deepdiff import DeepDiff, Delta >>> t3 = ["A", "B"] >>> t4 = ["A", "B", "C", "D"] >>> diff = DeepDiff(t3, t4, verbose_level=2) >>> delta = Delta(diff, bidirectional=True) >>> flat_dicts = delta.to_flat_dicts() >>> >>> delta2 = Delta(flat_dict_list=flat_dicts) >>> t3 + delta == t4 True Back to :doc:`/index` qlustered-deepdiff-41c7265/deepdiff/docstrings/stats.rst000066400000000000000000000056461516241264500234010ustar00rootroot00000000000000:doc:`/index` .. _stats_n_logging_label: Stats and Logging ================= .. _log_frequency_in_sec_label: Log Frequency In Sec -------------------- log_frequency_in_sec: Integer, default = 0 How often to log the progress. The default of 0 means logging progress is disabled. If you set it to 20, it will log every 20 seconds. This is useful only when running DeepDiff on massive objects that will take a while to run. If you are only dealing with small objects, keep it at 0 to disable progress logging. For example we have run a diff on 2 nested objects that took 2 seconds to get the results. By passing the log_frequency_in_sec=1, we get the following in the logs: >>> DeepDiff(t1, t2, log_frequency_in_sec=1) INFO:deepdiff.diff:DeepDiff 1 seconds in progress. Pass #1634, Diff #8005 INFO:deepdiff.diff:DeepDiff 2 seconds in progress. Pass #3319, Diff #16148 INFO:deepdiff.diff:stats {'PASSES COUNT': 3960, 'DIFF COUNT': 19469, 'DISTANCE CACHE HIT COUNT': 11847, 'MAX PASS LIMIT REACHED': False, 'MAX DIFF LIMIT REACHED': False, 'DURATION SEC': 2} .. note:: The default python logger will omit the info logs. You can either set the logging filter to include info logs or pass a different logger via :ref:`progress_logger_label` >>> import logging >>> logging.basicConfig(level=logging.INFO) .. _progress_logger_label: Progress Logger --------------- progress_logger: log function, default = logger.info What logging function to use specifically for progress reporting. This function is only used when progress logging is enabled by setting log_frequency_in_sec to anything above zero. The function that is passed as the progress_logger needs to be thread safe. For example you can pass progress_logger=logger.warning to the example above and everything is logged as warning level: >>> DeepDiff(t1, t2, log_frequency_in_sec=1, progress_logger=logger.warning) WARNING:deepdiff.diff:DeepDiff 1 seconds in progress. Pass #1634, Diff #8005 WARNING:deepdiff.diff:DeepDiff 2 seconds in progress. Pass #3319, Diff #16148 WARNING:deepdiff.diff:stats {'PASSES COUNT': 3960, 'DIFF COUNT': 19469, 'DISTANCE CACHE HIT COUNT': 11847, 'MAX PASS LIMIT REACHED': False, 'MAX DIFF LIMIT REACHED': False, 'DURATION SEC': 2} .. _get_stats_label: Get Stats --------- You can run the get_stats() method on a diff object to get some stats on the object. For example: >>> from pprint import pprint >>> from deepdiff import DeepDiff >>> >>> t1 = [ ... [1, 2, 3, 9], [9, 8, 5, 9] ... ] >>> >>> t2 = [ ... [1, 2, 4, 10], [4, 2, 5] ... ] >>> >>> diff = DeepDiff(t1, t2, ignore_order=True, cache_size=5000, cutoff_intersection_for_pairs=1) >>> pprint(diff.get_stats()) {'DIFF COUNT': 37, 'DISTANCE CACHE HIT COUNT': 0, 'MAX DIFF LIMIT REACHED': False, 'MAX PASS LIMIT REACHED': False, 'PASSES COUNT': 7} Back to :doc:`/index` qlustered-deepdiff-41c7265/deepdiff/docstrings/support.rst000066400000000000000000000013601516241264500237440ustar00rootroot00000000000000:doc:`/index` Support ======= .. |qluster_link| raw:: html Qluster .. admonition:: DeepDiff is now part of |qluster_link|. *If you're building workflows around data validation and correction,* `Qluster `__ *gives your team a structured way to manage rules, review failures, approve fixes, and reuse decisions—without building the entire system from scratch.* Thank you for using DeepDiff! If you find a bug, please create a ticket on our `GitHub repo `__ We are **available for consulting** if you need immediate help or custom implementations of DeepDiff. You can reach us via filling up `this form `__ Back to :doc:`/index` qlustered-deepdiff-41c7265/deepdiff/docstrings/troubleshoot.rst000066400000000000000000000011521516241264500247600ustar00rootroot00000000000000:doc:`/index` .. _troubleshoot_label: Troubleshoot ============ Murmur3 Installation ~~~~~~~~~~~~~~~~~~~~ NOTE: Murmur3 was removed from DeepDiff 5.2.0 If you are running into this issue, you are using an older version of DeepDiff. `Failed to build mmh3 when installing DeepDiff` DeepDiff prefers to use Murmur3 for hashing. However you have to manually install murmur3 by running: `pip install mmh3` On MacOS Mojave, some users experience difficulty when installing Murmur3. The problem can be solved by running: `xcode-select --install` And then running `pip install mmh3` Back to :doc:`/index` qlustered-deepdiff-41c7265/deepdiff/docstrings/view.rst000066400000000000000000000350211516241264500232030ustar00rootroot00000000000000:doc:`/index` .. _view_label: View ==== You have the options of text view and tree view. The main difference is that the tree view has the capabilities to traverse the objects to see what objects were compared to what other objects. While the view options decide the format of the output that is mostly machine readable, regardless of the view you choose, you can get a more human readable output by using the pretty() method. DeepDiff also offers other specialized views such as the :doc:`colored_view` (which includes a compact variant) and :doc:`delta` view for specific use cases. .. _text_view_label: Text View --------- Text view is the default view of DeepDiff. It is simpler than tree view. Example of using the text view. >>> from decimal import Decimal >>> from deepdiff import DeepDiff >>> t1 = {1:1, 3:3, 4:4} >>> t2 = {1:1, 3:3, 5:5, 6:6} >>> ddiff = DeepDiff(t1, t2) >>> print(ddiff) {'dictionary_item_added': [root[5], root[6]], 'dictionary_item_removed': [root[4]]} So for example ddiff['dictionary_item_added'] is a set of string results. That's why this view is called the text view. You can get this view by default or by passing `view='text'`. .. _tree_view_label: Tree View --------- The tree view provides you with tree objects that you can traverse through to find the parents of the objects that are diffed and the actual objects that are being diffed. This view is very useful when dealing with nested objects. Note that tree view always returns results in the form of Python sets. You can traverse through the tree elements! .. note:: The Tree view is just a different representation of the diffed data. Behind the scene, DeepDiff creates the tree view first and then converts it to textual representation for the text view. **Tree View Interface** .. code:: text +---------------------------------------------------------------+ | | | parent(t1) parent node parent(t2) |----level | + ^ + | +------|--------------------------|---------------------|-------+ | | | up | | Child | | | ChildRelationship | Relationship | | | | down | | | +------|----------------------|-------------------------|-------+ | v v v | | child(t1) child node child(t2) |----level | | +---------------------------------------------------------------+ :up: Move up to the parent node aka parent level :down: Move down to the child node aka child level :path(): Get the path to the current node in string representation, path(output_format='list') gives you the path in list representation. path(use_t2=True) gives you the path to t2. :t1: The first item in the current node that is being diffed :t2: The second item in the current node that is being diffed :additional: Additional information about the node i.e. repetition :repetition: Shortcut to get the repetition report The tree view allows you to have more than mere textual representaion of the diffed objects. It gives you the actual objects (t1, t2) throughout the tree of parents and children. **Examples for Tree View** .. note:: Set view='tree' in order to get the results in tree view. Value of an item has changed (Tree View) >>> from deepdiff import DeepDiff >>> from pprint import pprint >>> t1 = {1:1, 2:2, 3:3} >>> t2 = {1:1, 2:4, 3:3} >>> ddiff_verbose0 = DeepDiff(t1, t2, verbose_level=0, view='tree') >>> ddiff_verbose0 {'values_changed': []} >>> >>> ddiff_verbose1 = DeepDiff(t1, t2, verbose_level=1, view='tree') >>> ddiff_verbose1 {'values_changed': []} >>> set_of_values_changed = ddiff_verbose1['values_changed'] >>> # since set_of_values_changed includes only one item in a set >>> # in order to get that one item we can: >>> (changed,) = set_of_values_changed >>> changed # Another way to get this is to do: changed=list(set_of_values_changed)[0] >>> changed.t1 2 >>> changed.t2 4 >>> # You can traverse through the tree, get to the parents! >>> changed.up List difference (Tree View) >>> t1 = {1:1, 2:2, 3:3, 4:{"a":"hello", "b":[1, 2, 3, 4]}} >>> t2 = {1:1, 2:2, 3:3, 4:{"a":"hello", "b":[1, 2]}} >>> ddiff = DeepDiff(t1, t2, view='tree') >>> ddiff {'iterable_item_removed': [, ]} >>> # Note that the iterable_item_removed is a set. In this case it has 2 items in it. >>> # One way to get one item from the set is to convert it to a list >>> # And then get the first item of the list: >>> removed = list(ddiff['iterable_item_removed'])[0] >>> removed >>> >>> parent = removed.up >>> parent >>> parent.path() # gives you the string representation of the path "root[4]['b']" >>> parent.path(output_format='list') # gives you the list of keys and attributes that make up the path [4, 'b'] >>> parent.t1 [1, 2, 3, 4] >>> parent.t2 [1, 2] >>> parent.up >>> parent.up.up >>> parent.up.up.t1 {1: 1, 2: 2, 3: 3, 4: {'a': 'hello', 'b': [1, 2, 3, 4]}} >>> parent.up.up.t1 == t1 # It is holding the original t1 that we passed to DeepDiff True List difference 2 (Tree View) >>> t1 = {1:1, 2:2, 3:3, 4:{"a":"hello", "b":[1, 2, 3]}} >>> t2 = {1:1, 2:2, 3:3, 4:{"a":"hello", "b":[1, 3, 2, 3]}} >>> ddiff = DeepDiff(t1, t2, view='tree') >>> pprint(ddiff, indent = 2) { 'iterable_item_added': [], 'values_changed': [, ]} >>> >>> # Note that iterable_item_added is a set with one item. >>> # So in order to get that one item from it, we can do: >>> >>> (added,) = ddiff['iterable_item_added'] >>> added >>> added.up.up >>> added.up.up.path() 'root[4]' >>> added.up.up.path(output_format='list') # gives you the list of keys and attributes that make up the path [4] >>> added.up.up.down >>> >>> # going up twice and then down twice gives you the same node in the tree: >>> added.up.up.down.down == added True List difference ignoring order but reporting repetitions (Tree View) >>> t1 = [1, 3, 1, 4] >>> t2 = [4, 4, 1] >>> ddiff = DeepDiff(t1, t2, ignore_order=True, report_repetition=True, view='tree') >>> pprint(ddiff, indent=2) { 'iterable_item_removed': [], 'repetition_change': [, ]} >>> >>> # repetition_change is a set with 2 items. >>> # in order to get those 2 items, we can do the following. >>> # or we can convert the set to list and get the list items. >>> # or we can iterate through the set items >>> >>> (repeat1, repeat2) = ddiff['repetition_change'] >>> repeat1 # the default verbosity is set to 1. >>> # The actual data regarding the repetitions can be found in the repetition attribute: >>> repeat1.repetition {'old_repeat': 1, 'new_repeat': 2, 'old_indexes': [3], 'new_indexes': [0, 1]} >>> >>> # If you change the verbosity, you will see less: >>> ddiff = DeepDiff(t1, t2, ignore_order=True, report_repetition=True, view='tree', verbose_level=0) >>> ddiff {'repetition_change': [, ], 'iterable_item_removed': []} >>> (repeat1, repeat2) = ddiff['repetition_change'] >>> repeat1 >>> >>> # But the verbosity level does not change the actual report object. >>> # It only changes the textual representaion of the object. We get the actual object here: >>> repeat1.repetition {'old_repeat': 1, 'new_repeat': 2, 'old_indexes': [3], 'new_indexes': [0, 1]} >>> repeat1.t1 4 >>> repeat1.t2 4 >>> repeat1.up List that contains dictionary (Tree View) >>> t1 = {1:1, 2:2, 3:3, 4:{"a":"hello", "b":[1, 2, {1:1, 2:2}]}} >>> t2 = {1:1, 2:2, 3:3, 4:{"a":"hello", "b":[1, 2, {1:3}]}} >>> ddiff = DeepDiff(t1, t2, view='tree') >>> pprint (ddiff, indent = 2) { 'dictionary_item_removed': [], 'values_changed': []} Sets (Tree View): >>> t1 = {1, 2, 8} >>> t2 = {1, 2, 3, 5} >>> ddiff = DeepDiff(t1, t2, view='tree') >>> print(ddiff) {'set_item_removed': [], 'set_item_added': [, ]} >>> # grabbing one item from set_item_removed set which has one item only >>> (item,) = ddiff['set_item_removed'] >>> item.up >>> item.up.t1 == t1 True Named Tuples (Tree View): >>> from collections import namedtuple >>> Point = namedtuple('Point', ['x', 'y']) >>> t1 = Point(x=11, y=22) >>> t2 = Point(x=11, y=23) >>> print(DeepDiff(t1, t2, view='tree')) {'values_changed': []} Custom objects (Tree View): >>> class ClassA(object): ... a = 1 ... def __init__(self, b): ... self.b = b ... >>> t1 = ClassA(1) >>> t2 = ClassA(2) >>> >>> print(DeepDiff(t1, t2, view='tree')) {'values_changed': []} Object attribute added (Tree View): >>> t2.c = "new attribute" >>> pprint(DeepDiff(t1, t2, view='tree')) {'attribute_added': [], 'values_changed': []} Approximate decimals comparison (Significant digits after the point) (Tree View): >>> t1 = Decimal('1.52') >>> t2 = Decimal('1.57') >>> DeepDiff(t1, t2, significant_digits=0, view='tree') {} >>> ddiff = DeepDiff(t1, t2, significant_digits=1, view='tree') >>> ddiff {'values_changed': []} >>> (change1,) = ddiff['values_changed'] >>> change1 >>> change1.t1 Decimal('1.52') >>> change1.t2 Decimal('1.57') >>> change1.path() 'root' Approximate float comparison (Significant digits after the point) (Tree View): >>> t1 = [ 1.1129, 1.3359 ] >>> t2 = [ 1.113, 1.3362 ] >>> ddiff = DeepDiff(t1, t2, significant_digits=3, view='tree') >>> ddiff {} >>> ddiff = DeepDiff(t1, t2, view='tree') >>> pprint(ddiff, indent=2) { 'values_changed': [, ]} >>> ddiff = DeepDiff(1.23*10**20, 1.24*10**20, significant_digits=1, view='tree') >>> ddiff {'values_changed': []} pretty() method --------------- Use the pretty method for human readable output. This is regardless of what view you have used to generate the results. >>> from deepdiff import DeepDiff >>> t1={1,2,4} >>> t2={2,3} >>> print(DeepDiff(t1, t2).pretty()) Item root[3] added to set. Item root[4] removed from set. Item root[1] removed from set. The pretty method has an optional parameter ``prefix`` that allows a prefix string before every output line (*e.g.* for logging): >>> from deepdiff import DeepDiff >>> t1={1,2,4} >>> t2={2,3} >>> print(DeepDiff(t1, t2).pretty(prefix='Diff: ')) Diff: Item root[3] added to set. Diff: Item root[4] removed from set. Diff: Item root[1] removed from set. The ``prefix`` may also be a callable function. This function must accept ``**kwargs``; as of this version, the only parameter is ``diff`` but the signature allows for future expansion. The ``diff`` given will be the ``DeepDiff`` that ``pretty`` was called on; this allows interesting capabilities such as: .. code:: python >>> from deepdiff import DeepDiff >>> t1={1,2,4} >>> t2={2,3} >>> def callback(**kwargs): ... """Helper function using a hidden variable on the diff that tracks which count prints next""" ... kwargs['diff']._diff_count = 1 + getattr(kwargs['diff'], '_diff_count', 0) ... return f"Diff #{kwargs['diff']._diff_count}: " ... >>> print(DeepDiff(t1, t2).pretty(prefix=callback)) Diff #1: Item root[3] added to set. Diff #2: Item root[4] removed from set. Diff #3: Item root[1] removed from set. Text view vs. Tree view vs. pretty() method ----------------------------------------------- Views are just different format of results. Each comes with its own set of features. At the end of the day the user can choose the right format based on the use case. - The text view is the default format of the results. It is the format that is the most suitable if you don't need to know the traversal history of the objects being compared. - The tree view allows you to traverse back and forth through the tree and see what objects were compared to what other objects. - The pretty() method is not a view. All the views are dictionaries. The pretty() method spits out a string output of what has changed and is designed to be human readable. For example >>> from deepdiff import DeepDiff >>> t1={1,2,4} >>> t2={2,3} Text view (default) >>> DeepDiff(t1, t2) # same as view='text' {'set_item_removed': [root[4], root[1]], 'set_item_added': [root[3]]} Tree view >>> tree = DeepDiff(t1, t2, view='tree') >>> tree {'set_item_removed': [, ], 'set_item_added': []} >>> tree['set_item_added'][0] >>> tree['set_item_added'][0].t2 3 Pretty method. Regardless of what view was used, you can use the "pretty()" method to get a human readable output. >>> print(DeepDiff(t1, t2).pretty()) Item root[3] added to set. Item root[4] removed from set. Item root[1] removed from set. Back to :doc:`/index` qlustered-deepdiff-41c7265/deepdiff/helper.py000066400000000000000000000667251516241264500211700ustar00rootroot00000000000000import sys import re import os import datetime import uuid import logging import warnings import string import time import enum import ipaddress from typing import NamedTuple, Any, List, Optional, Dict, Union, TYPE_CHECKING, Tuple, Iterable, Iterator, Set, FrozenSet, Callable, Pattern, Type, TypeVar, Generic, Literal, overload, TypedDict from collections.abc import Mapping, Sequence, Generator from ast import literal_eval from decimal import Decimal, localcontext, InvalidOperation as InvalidDecimalOperation from fractions import Fraction from itertools import repeat from orderly_set import StableSetEq as SetOrderedBase # median: 1.0867 s for cache test, 5.63s for all tests from threading import Timer if TYPE_CHECKING: from pytz.tzinfo import BaseTzInfo class np_type: pass class pydantic_base_model_type: pass class SetOrdered(SetOrderedBase): def __repr__(self) -> str: return str(list(self)) try: import numpy as np except ImportError: # pragma: no cover. The case without Numpy is tested locally only. np = None # pragma: no cover. np_array_factory = 'numpy not available' # pragma: no cover. np_ndarray = np_type # pragma: no cover. np_bool_ = np_type # pragma: no cover. np_int8 = np_type # pragma: no cover. np_int16 = np_type # pragma: no cover. np_int32 = np_type # pragma: no cover. np_int64 = np_type # pragma: no cover. np_uint8 = np_type # pragma: no cover. np_uint16 = np_type # pragma: no cover. np_uint32 = np_type # pragma: no cover. np_uint64 = np_type # pragma: no cover. np_intp = np_type # pragma: no cover. np_uintp = np_type # pragma: no cover. np_float32 = np_type # pragma: no cover. np_float64 = np_type # pragma: no cover. np_double = np_type # pragma: no cover. np_floating = np_type # pragma: no cover. np_complex64 = np_type # pragma: no cover. np_complex128 = np_type # pragma: no cover. np_cdouble = np_type # pragma: no cover. np_complexfloating = np_type # pragma: no cover. np_datetime64 = np_type # pragma: no cover. else: np_array_factory = np.array np_ndarray = np.ndarray np_bool_ = np.bool_ np_int8 = np.int8 np_int16 = np.int16 np_int32 = np.int32 np_int64 = np.int64 np_uint8 = np.uint8 np_uint16 = np.uint16 np_uint32 = np.uint32 np_uint64 = np.uint64 np_intp = np.intp np_uintp = np.uintp np_float32 = np.float32 np_float64 = np.float64 np_double = np.double # np.float_ is an alias for np.double and is being removed by NumPy 2.0 np_floating = np.floating np_complex64 = np.complex64 np_complex128 = np.complex128 np_cdouble = np.cdouble # np.complex_ is an alias for np.cdouble and is being removed by NumPy 2.0 np_complexfloating = np.complexfloating np_datetime64 = np.datetime64 numpy_numbers: Tuple[Type[Any], ...] = ( np_int8, np_int16, np_int32, np_int64, np_uint8, np_uint16, np_uint32, np_uint64, np_intp, np_uintp, np_float32, np_float64, np_double, np_floating, np_complex64, np_complex128, np_cdouble,) numpy_complex_numbers: Tuple[Type[Any], ...] = ( np_complexfloating, np_complex64, np_complex128, np_cdouble, ) numpy_dtypes: Set[Type[Any]] = set(numpy_numbers) numpy_dtypes.add(np_bool_) # type: ignore numpy_dtypes.add(np_datetime64) # type: ignore numpy_dtype_str_to_type: Dict[str, Type[Any]] = { item.__name__: item for item in numpy_dtypes } try: from pydantic.main import BaseModel as PydanticBaseModel # type: ignore except ImportError: PydanticBaseModel = pydantic_base_model_type logger = logging.getLogger(__name__) py_major_version = sys.version_info.major py_minor_version = sys.version_info.minor py_current_version: Decimal = Decimal("{}.{}".format(py_major_version, py_minor_version)) py2 = py_major_version == 2 py3 = py_major_version == 3 py4 = py_major_version == 4 NUMERICS: FrozenSet[str] = frozenset(string.digits) class EnumBase(str, enum.Enum): def __repr__(self) -> str: """ We need to add a single quotes so we can easily copy the value when we do ipdb. """ return f"'{self.name}'" def __str__(self) -> str: return self.name def _int_or_zero(value: str) -> int: """ Tries to extract some number from a string. 12c becomes 12 """ try: return int(value) except Exception: result = [] for char in value: if char in NUMERICS: result.append(char) if result: return int(''.join(result)) return 0 def get_semvar_as_integer(version: str) -> int: """ Converts: '1.23.5' to 1023005 """ version_parts = version.split('.') if len(version_parts) > 3: version_parts = version_parts[:3] elif len(version_parts) < 3: version_parts.extend(['0'] * (3 - len(version_parts))) return sum([10**(i * 3) * _int_or_zero(v) for i, v in enumerate(reversed(version_parts))]) # we used to use OrderedDictPlus when dictionaries in Python were not ordered. dict_ = dict if py4: logger.warning('Python 4 is not supported yet. Switching logic to Python 3.') # pragma: no cover py3 = True # pragma: no cover if py2: # pragma: no cover sys.exit('Python 2 is not supported anymore. The last version of DeepDiff that supported Py2 was 3.3.0') pypy3 = py3 and hasattr(sys, "pypy_translation_info") if np and get_semvar_as_integer(np.__version__) < 1019000: sys.exit('The minimum required Numpy version is 1.19.0. Please upgrade your Numpy package.') strings: Tuple[Type[str], Type[bytes], Type[memoryview]] = (str, bytes, memoryview) # which are both basestring unicode_type = str bytes_type = bytes only_complex_number: Tuple[Type[Any], ...] = (complex,) + numpy_complex_numbers only_numbers: Tuple[Type[Any], ...] = (int, float, complex, Decimal, Fraction) + numpy_numbers datetimes: Tuple[Type[Any], ...] = (datetime.datetime, datetime.date, datetime.timedelta, datetime.time, np_datetime64) ipranges: Tuple[Type[Any], ...] = (ipaddress.IPv4Interface, ipaddress.IPv6Interface, ipaddress.IPv4Network, ipaddress.IPv6Network, ipaddress.IPv4Address, ipaddress.IPv6Address) uuids: Tuple[Type[uuid.UUID]] = (uuid.UUID, ) times: Tuple[Type[Any], ...] = (datetime.datetime, datetime.time, np_datetime64) numbers: Tuple[Type[Any], ...] = only_numbers + datetimes # Type alias for use in type annotations NumberType = Union[int, float, complex, Decimal, Fraction, datetime.datetime, datetime.date, datetime.timedelta, datetime.time, Any] booleans: Tuple[Type[bool], Type[Any]] = (bool, np_bool_) basic_types: Tuple[Type[Any], ...] = strings + numbers + uuids + booleans + (type(None), ) class IndexedHash(NamedTuple): indexes: List[Any] item: Any current_dir = os.path.dirname(os.path.abspath(__file__)) ID_PREFIX = '!>*id' KEY_TO_VAL_STR = "{}:{}" TREE_VIEW = 'tree' TEXT_VIEW = 'text' DELTA_VIEW = '_delta' COLORED_VIEW = 'colored' COLORED_COMPACT_VIEW = 'colored_compact' ENUM_INCLUDE_KEYS: List[str] = ['__objclass__', 'name', 'value'] def short_repr(item: Any, max_length: int = 15) -> str: """Short representation of item if it is too long""" item = repr(item) if len(item) > max_length: item = '{}...{}'.format(item[:max_length - 3], item[-1]) return item class ListItemRemovedOrAdded: # pragma: no cover """Class of conditions to be checked""" pass class OtherTypes: def __repr__(self) -> str: return "Error: {}".format(self.__class__.__name__) # pragma: no cover __str__ = __repr__ class Skipped(OtherTypes): pass class Unprocessed(OtherTypes): pass class NotHashed(OtherTypes): pass class NotPresent: # pragma: no cover """ In a change tree, this indicated that a previously existing object has been removed -- or will only be added in the future. We previously used None for this but this caused problem when users actually added and removed None. Srsly guys? :D """ def __repr__(self) -> str: return 'not present' # pragma: no cover __str__ = __repr__ class CannotCompare(Exception): """ Exception when two items cannot be compared in the compare function. """ pass unprocessed = Unprocessed() skipped = Skipped() not_hashed = NotHashed() notpresent = NotPresent() # Disabling remapping from old to new keys since the mapping is deprecated. RemapDict = dict_ # class RemapDict(dict_): # """ # DISABLED # Remap Dictionary. # For keys that have a new, longer name, remap the old key to the new key. # Other keys that don't have a new name are handled as before. # """ # def __getitem__(self, old_key): # new_key = EXPANDED_KEY_MAP.get(old_key, old_key) # if new_key != old_key: # logger.warning( # "DeepDiff Deprecation: %s is renamed to %s. Please start using " # "the new unified naming convention.", old_key, new_key) # if new_key in self: # return self.get(new_key) # else: # pragma: no cover # raise KeyError(new_key) class indexed_set(set): """ A set class that lets you get an item by index >>> a = indexed_set() >>> a.add(10) >>> a.add(20) >>> a[0] 10 """ def add_to_frozen_set(parents_ids: FrozenSet[int], item_id: int) -> FrozenSet[int]: return parents_ids | {item_id} def convert_item_or_items_into_set_else_none(items: Union[str, Iterable[str], None]) -> Optional[Set[str]]: if items: if isinstance(items, str): return {items} else: return set(items) else: return None def add_root_to_paths(paths: Optional[Iterable[str]]) -> Optional[SetOrdered]: """ Sometimes the users want to just pass [key] instead of root[key] for example. Here we automatically add all sorts of variations that might match the path they were supposed to pass. """ if paths is None: return result = SetOrdered() for path in paths: if path.startswith('root'): result.add(path) else: if path.isdigit(): result.add(f"root['{path}']") result.add(f"root[{path}]") elif path[0].isdigit(): result.add(f"root['{path}']") else: result.add(f"root.{path}") result.add(f"root['{path}']") return result RE_COMPILED_TYPE = type(re.compile('')) def convert_item_or_items_into_compiled_regexes_else_none(items: Union[str, Pattern[str], Iterable[Union[str, Pattern[str]]], None]) -> Optional[List[Pattern[str]]]: if items: if isinstance(items, (str, RE_COMPILED_TYPE)): items_list = [items] # type: ignore else: items_list = list(items) # type: ignore return [i if isinstance(i, RE_COMPILED_TYPE) else re.compile(i) for i in items_list] else: return None def get_id(obj: Any) -> str: """ Adding some characters to id so they are not just integers to reduce the risk of collision. """ return "{}{}".format(ID_PREFIX, id(obj)) def get_type(obj: Any) -> Type[Any]: """ Get the type of object or if it is a class, return the class itself. """ if isinstance(obj, np_ndarray): return obj.dtype.type # type: ignore return obj if type(obj) is type else type(obj) def numpy_dtype_string_to_type(dtype_str: str) -> Type[Any]: return numpy_dtype_str_to_type[dtype_str] def type_in_type_group(item: Any, type_group: Tuple[Type[Any], ...]) -> bool: return get_type(item) in type_group def type_is_subclass_of_type_group(item: Any, type_group: Tuple[Type[Any], ...]) -> bool: return isinstance(item, type_group) \ or (isinstance(item, type) and issubclass(item, type_group)) \ or type_in_type_group(item, type_group) def get_doc(doc_filename: str) -> str: try: with open(os.path.join(current_dir, 'docstrings', doc_filename), 'r') as doc_file: doc = doc_file.read() doc = doc.replace(':orphan:\n\n', '', 1) except Exception: # pragma: no cover doc = 'Failed to load the docstrings. Please visit: https://zepworks.com/deepdiff/current/' # pragma: no cover return doc number_formatting: Dict[str, str] = { "f": r'{:.%sf}', "e": r'{:.%se}', } def number_to_string(number: Any, significant_digits: int, number_format_notation: Literal['f', 'e'] = 'f') -> Any: """ Convert numbers to string considering significant digits. """ try: using = number_formatting[number_format_notation] except KeyError: raise ValueError("number_format_notation got invalid value of {}. The valid values are 'f' and 'e'".format(number_format_notation)) from None if not isinstance(number, numbers): # type: ignore return number elif isinstance(number, Decimal): with localcontext() as ctx: # Precision = number of integer digits + significant_digits # Using number//1 to get the integer part of the number ctx.prec = len(str(abs(number // 1))) + significant_digits try: number = number.quantize(Decimal('0.' + '0' * significant_digits)) except InvalidDecimalOperation: # Sometimes rounding up causes a higher precision to be needed for the quantize operation # For example '999.99999999' will become '1000.000000' after quantize ctx.prec += 1 number = number.quantize(Decimal('0.' + '0' * significant_digits)) elif isinstance(number, Fraction): # Convert Fraction to float so that string formatting works on Python < 3.12 number = round(float(number), significant_digits) if significant_digits == 0: number = int(number) elif isinstance(number, only_complex_number): # type: ignore # Case for complex numbers. number = number.__class__( "{real}+{imag}j".format( # type: ignore real=number_to_string( number=number.real, # type: ignore significant_digits=significant_digits, number_format_notation=number_format_notation ), imag=number_to_string( number=number.imag, # type: ignore significant_digits=significant_digits, number_format_notation=number_format_notation ) ) # type: ignore ) else: number = round(number=number, ndigits=significant_digits) # type: ignore if significant_digits == 0: number = int(number) # type: ignore if number == 0.0: # Special case for 0: "-0.xx" should compare equal to "0.xx" number = abs(number) # type: ignore # Cast number to string result = (using % significant_digits).format(number) # https://bugs.python.org/issue36622 if number_format_notation == 'e': # Removing leading 0 for exponential part. result = re.sub( pattern=r'(?<=e(\+|\-))0(?=\d)+', repl=r'', string=result ) return result class DeepDiffDeprecationWarning(DeprecationWarning): """ Use this warning instead of DeprecationWarning """ pass def cartesian_product(a: Iterable[Tuple[Any, ...]], b: Iterable[Any]) -> Iterator[Tuple[Any, ...]]: """ Get the Cartesian product of two iterables **parameters** a: list of lists b: iterable to do the Cartesian product """ for i in a: for j in b: yield i + (j,) def cartesian_product_of_shape(dimentions: Iterable[int], result: Optional[Tuple[Tuple[Any, ...], ...]] = None) -> Iterator[Tuple[Any, ...]]: """ Cartesian product of a dimensions iterable. This is mainly used to traverse Numpy ndarrays. Each array has dimensions that are defined in ndarray.shape """ if result is None: result = ((),) # a tuple with an empty tuple for dimension in dimentions: result = tuple(cartesian_product(result, range(dimension))) return iter(result) def get_numpy_ndarray_rows(obj: Any, shape: Optional[Tuple[int, ...]] = None) -> Generator[Tuple[Tuple[int, ...], Any], None, None]: """ Convert a multi dimensional numpy array to list of rows """ if shape is None: shape = obj.shape # type: ignore dimentions = shape[:-1] if shape else () for path_tuple in cartesian_product_of_shape(dimentions): result = obj for index in path_tuple: result = result[index] yield path_tuple, result class _NotFound: def __eq__(self, other: Any) -> bool: return False __req__ = __eq__ def __repr__(self) -> str: return 'not found' __str__ = __repr__ not_found = _NotFound() warnings.simplefilter('once', DeepDiffDeprecationWarning) class RepeatedTimer: """ Threaded Repeated Timer by MestreLion https://stackoverflow.com/a/38317060/1497443 """ def __init__(self, interval: float, function: Callable[..., Any], *args: Any, **kwargs: Any) -> None: self._timer = None self.interval = interval self.function = function self.args = args self.start_time = time.time() self.kwargs = kwargs self.is_running = False self.start() def _get_duration_sec(self) -> int: return int(time.time() - self.start_time) def _run(self) -> None: self.is_running = False self.start() self.function(*self.args, **self.kwargs) def start(self) -> None: self.kwargs.update(duration=self._get_duration_sec()) if not self.is_running: self._timer = Timer(self.interval, self._run) self._timer.start() self.is_running = True def stop(self) -> int: duration = self._get_duration_sec() if self._timer is not None: self._timer.cancel() self.is_running = False return duration def _eval_decimal(params: str) -> Decimal: return Decimal(params) def _eval_datetime(params: str) -> datetime.datetime: params_with_parens = f'({params})' params_tuple = literal_eval(params_with_parens) return datetime.datetime(*params_tuple) # type: ignore def _eval_date(params: str) -> datetime.date: params_with_parens = f'({params})' params_tuple = literal_eval(params_with_parens) return datetime.date(*params_tuple) # type: ignore LITERAL_EVAL_PRE_PROCESS: List[Tuple[str, str, Callable[[str], Any]]] = [ ('Decimal(', ')', _eval_decimal), ('datetime.datetime(', ')', _eval_datetime), ('datetime.date(', ')', _eval_date), ] def literal_eval_extended(item: str) -> Any: """ An extended version of literal_eval """ try: return literal_eval(item) except (SyntaxError, ValueError): for begin, end, func in LITERAL_EVAL_PRE_PROCESS: if item.startswith(begin) and item.endswith(end): # Extracting and removing extra quotes so for example "Decimal('10.1')" becomes "'10.1'" and then '10.1' params = item[len(begin): -len(end)].strip('\'\"') return func(params) raise def time_to_seconds(t: datetime.time) -> int: return (t.hour * 60 + t.minute) * 60 + t.second def datetime_normalize( truncate_datetime:Union[str, None], obj:Union[datetime.datetime, datetime.time], default_timezone: Union[ datetime.timezone, "BaseTzInfo" ] = datetime.timezone.utc, ) -> Any: if truncate_datetime: if truncate_datetime == 'second': obj = obj.replace(microsecond=0) elif truncate_datetime == 'minute': obj = obj.replace(second=0, microsecond=0) elif truncate_datetime == 'hour': obj = obj.replace(minute=0, second=0, microsecond=0) elif truncate_datetime == 'day': obj = obj.replace(hour=0, minute=0, second=0, microsecond=0) if isinstance(obj, datetime.datetime): if has_timezone(obj): obj = obj.astimezone(default_timezone) else: obj = obj.replace(tzinfo=default_timezone) elif isinstance(obj, datetime.time): return time_to_seconds(obj) return obj def has_timezone(dt: datetime.datetime) -> bool: """ Function to check if a datetime object has a timezone Checking dt.tzinfo.utcoffset(dt) ensures that the datetime object is truly timezone-aware because some datetime objects may have a tzinfo attribute that is not None but still doesn't provide a valid offset. Certain tzinfo objects, such as pytz.timezone(None), can exist but do not provide meaningful UTC offset information. If tzinfo is present but calling .utcoffset(dt) returns None, the datetime is not truly timezone-aware. """ return dt.tzinfo is not None and dt.tzinfo.utcoffset(dt) is not None def get_truncate_datetime(truncate_datetime: Union[str, None]) -> Union[str, None]: """ Validates truncate_datetime value """ if truncate_datetime not in {None, 'second', 'minute', 'hour', 'day'}: raise ValueError("truncate_datetime must be second, minute, hour or day") return truncate_datetime def cartesian_product_numpy(*arrays: Any) -> Any: """ Cartesian product of Numpy arrays by Paul Panzer https://stackoverflow.com/a/49445693/1497443 """ la = len(arrays) dtype = np.result_type(*arrays) # type: ignore arr = np.empty((la, *map(len, arrays)), dtype=dtype) # type: ignore idx = slice(None), *repeat(None, la) for i, a in enumerate(arrays): arr[i, ...] = a[idx[:la - i]] return arr.reshape(la, -1).T def diff_numpy_array(A: Any, B: Any) -> Any: """ Numpy Array A - B return items in A that are not in B By Divakar https://stackoverflow.com/a/52417967/1497443 """ return A[~np.isin(A, B)] # type: ignore PYTHON_TYPE_TO_NUMPY_TYPE: Dict[Type[Any], Type[Any]] = { int: np_int64, float: np_float64, Decimal: np_float64 } def get_homogeneous_numpy_compatible_type_of_seq(seq: Sequence[Any]) -> Union[Type[Any], Literal[False]]: """ Return with the numpy dtype if the array can be converted to a non-object numpy array. Originally written by mgilson https://stackoverflow.com/a/13252348/1497443 This is the modified version. """ iseq = iter(seq) first_type = type(next(iseq)) if first_type in {int, float, Decimal}: type_match = first_type if all((type(x) is first_type) for x in iseq) else False if type_match: return PYTHON_TYPE_TO_NUMPY_TYPE.get(type_match, False) else: return False else: return False def detailed__dict__(obj: Any, ignore_private_variables: bool = True, ignore_keys: FrozenSet[str] = frozenset(), include_keys: Optional[List[str]] = None) -> Dict[str, Any]: """ Get the detailed dictionary of an object. This is used so we retrieve object properties too. """ if include_keys: result = {} for key in include_keys: try: value = getattr(obj, key) except Exception: pass else: if not callable(value) or key == '__objclass__': # We don't want to compare functions, however for backward compatibility, __objclass__ needs to be reported. result[key] = value else: result = obj.__dict__.copy() # A shallow copy private_var_prefix = f"_{obj.__class__.__name__}__" # The semi private variables in Python get this prefix for key in obj.__dict__: if key in ignore_keys or ( ignore_private_variables and key.startswith('__') and not key.startswith(private_var_prefix) ): del result[key] if isinstance(obj, PydanticBaseModel): getter = lambda x, y: getattr(type(x), y) else: getter = getattr for key in dir(obj): if key not in result and key not in ignore_keys and ( not ignore_private_variables or ( ignore_private_variables and not key.startswith('__') and not key.startswith(private_var_prefix) ) ): value = getter(obj, key) if not callable(value): result[key] = value return result def named_tuple_repr(self: NamedTuple) -> str: fields = [] for field, value in self._asdict().items(): # Only include fields that do not have their default value if field in self._field_defaults: if value != self._field_defaults[field]: fields.append(f"{field}={value!r}") else: fields.append(f"{field}={value!r}") return f"{self.__class__.__name__}({', '.join(fields)})" class OpcodeTag(EnumBase): insert = 'insert' delete = 'delete' equal = 'equal' replace = 'replace' # type: ignore # swapped = 'swapped' # in the future we should support reporting of items swapped with each other class Opcode(NamedTuple): tag: str t1_from_index: int t1_to_index: int t2_from_index: int t2_to_index: int old_values: Optional[List[Any]] = None new_values: Optional[List[Any]] = None __repr__ = __str__ = named_tuple_repr class FlatDataAction(EnumBase): values_changed = 'values_changed' type_changes = 'type_changes' set_item_added = 'set_item_added' set_item_removed = 'set_item_removed' dictionary_item_added = 'dictionary_item_added' dictionary_item_removed = 'dictionary_item_removed' iterable_item_added = 'iterable_item_added' iterable_item_removed = 'iterable_item_removed' iterable_item_moved = 'iterable_item_moved' iterable_items_inserted = 'iterable_items_inserted' # opcode iterable_items_deleted = 'iterable_items_deleted' # opcode iterable_items_replaced = 'iterable_items_replaced' # opcode iterable_items_equal = 'iterable_items_equal' # opcode attribute_removed = 'attribute_removed' attribute_added = 'attribute_added' unordered_iterable_item_added = 'unordered_iterable_item_added' unordered_iterable_item_removed = 'unordered_iterable_item_removed' initiated = "initiated" OPCODE_TAG_TO_FLAT_DATA_ACTION = { OpcodeTag.insert: FlatDataAction.iterable_items_inserted, OpcodeTag.delete: FlatDataAction.iterable_items_deleted, OpcodeTag.replace: FlatDataAction.iterable_items_replaced, OpcodeTag.equal: FlatDataAction.iterable_items_equal, } FLAT_DATA_ACTION_TO_OPCODE_TAG = {v: i for i, v in OPCODE_TAG_TO_FLAT_DATA_ACTION.items()} UnkownValueCode: str = 'unknown___' class FlatDeltaRow(NamedTuple): path: List action: FlatDataAction value: Optional[Any] = UnkownValueCode old_value: Optional[Any] = UnkownValueCode type: Optional[Any] = UnkownValueCode old_type: Optional[Any] = UnkownValueCode new_path: Optional[List] = None t1_from_index: Optional[int] = None t1_to_index: Optional[int] = None t2_from_index: Optional[int] = None t2_to_index: Optional[int] = None __repr__ = __str__ = named_tuple_repr class _FlatDeltaDictRequired(TypedDict): path: List action: FlatDataAction class FlatDeltaDict(_FlatDeltaDictRequired, total=False): value: Optional[Any] old_value: Optional[Any] type: Optional[Any] old_type: Optional[Any] new_path: Optional[List] t1_from_index: Optional[int] t1_to_index: Optional[int] t2_from_index: Optional[int] t2_to_index: Optional[int] JSON = Union[Dict[str, str], List[str], List[int], Dict[str, "JSON"], List["JSON"], str, int, float, bool, None] class SummaryNodeType(EnumBase): dict = 'dict' list = 'list' leaf = 'leaf' qlustered-deepdiff-41c7265/deepdiff/lfucache.py000066400000000000000000000157111516241264500214500ustar00rootroot00000000000000""" LFU cache Written by Shane Wang https://medium.com/@epicshane/a-python-implementation-of-lfu-least-frequently-used-cache-with-o-1-time-complexity-e16b34a3c49b https://github.com/luxigner/lfu_cache Modified by Sep Dehpour """ from collections import defaultdict from threading import Lock from statistics import mean from deepdiff.helper import not_found, dict_, SetOrdered class CacheNode: def __init__(self, key, report_type, value, freq_node, pre, nxt): self.key = key if report_type: self.content = defaultdict(SetOrdered) self.content[report_type].add(value) else: self.content = value self.freq_node = freq_node self.pre = pre # previous CacheNode self.nxt = nxt # next CacheNode def free_myself(self): if self.freq_node.cache_head == self.freq_node.cache_tail: # type: ignore self.freq_node.cache_head = self.freq_node.cache_tail = None # type: ignore elif self.freq_node.cache_head == self: # type: ignore self.nxt.pre = None # type: ignore self.freq_node.cache_head = self.nxt # type: ignore elif self.freq_node.cache_tail == self: # type: ignore self.pre.nxt = None # type: ignore self.freq_node.cache_tail = self.pre # type: ignore else: self.pre.nxt = self.nxt # type: ignore self.nxt.pre = self.pre # type: ignore self.pre = None self.nxt = None self.freq_node = None class FreqNode: def __init__(self, freq, pre, nxt): self.freq = freq self.pre = pre # previous FreqNode self.nxt = nxt # next FreqNode self.cache_head = None # CacheNode head under this FreqNode self.cache_tail = None # CacheNode tail under this FreqNode def count_caches(self): if self.cache_head is None and self.cache_tail is None: return 0 elif self.cache_head == self.cache_tail: return 1 else: return '2+' def remove(self): if self.pre is not None: self.pre.nxt = self.nxt if self.nxt is not None: self.nxt.pre = self.pre pre = self.pre nxt = self.nxt self.pre = self.nxt = self.cache_head = self.cache_tail = None return (pre, nxt) def pop_head_cache(self): if self.cache_head is None and self.cache_tail is None: return None elif self.cache_head == self.cache_tail: cache_head = self.cache_head self.cache_head = self.cache_tail = None return cache_head else: cache_head = self.cache_head self.cache_head.nxt.pre = None # type: ignore self.cache_head = self.cache_head.nxt # type: ignore return cache_head def append_cache_to_tail(self, cache_node): cache_node.freq_node = self if self.cache_head is None and self.cache_tail is None: self.cache_head = self.cache_tail = cache_node else: cache_node.pre = self.cache_tail cache_node.nxt = None self.cache_tail.nxt = cache_node # type: ignore self.cache_tail = cache_node def insert_after_me(self, freq_node): freq_node.pre = self freq_node.nxt = self.nxt if self.nxt is not None: self.nxt.pre = freq_node self.nxt = freq_node def insert_before_me(self, freq_node): if self.pre is not None: self.pre.nxt = freq_node freq_node.pre = self.pre freq_node.nxt = self self.pre = freq_node class LFUCache: def __init__(self, capacity): self.cache = dict_() # {key: cache_node} if capacity <= 0: raise ValueError('Capacity of LFUCache needs to be positive.') # pragma: no cover. self.capacity = capacity self.freq_link_head = None self.lock = Lock() def get(self, key): with self.lock: if key in self.cache: cache_node = self.cache[key] freq_node = cache_node.freq_node content = cache_node.content self.move_forward(cache_node, freq_node) return content else: return not_found def set(self, key, report_type=None, value=None): with self.lock: if key in self.cache: cache_node = self.cache[key] if report_type: cache_node.content[report_type].add(value) else: cache_node.content = value else: if len(self.cache) >= self.capacity: self.dump_cache() self.create_cache_node(key, report_type, value) def __contains__(self, key): return key in self.cache def move_forward(self, cache_node, freq_node): if freq_node.nxt is None or freq_node.nxt.freq != freq_node.freq + 1: target_freq_node = FreqNode(freq_node.freq + 1, None, None) target_empty = True else: target_freq_node = freq_node.nxt target_empty = False cache_node.free_myself() target_freq_node.append_cache_to_tail(cache_node) if target_empty: freq_node.insert_after_me(target_freq_node) if freq_node.count_caches() == 0: if self.freq_link_head == freq_node: self.freq_link_head = target_freq_node freq_node.remove() def dump_cache(self): head_freq_node = self.freq_link_head self.cache.pop(head_freq_node.cache_head.key) # type: ignore head_freq_node.pop_head_cache() # type: ignore if head_freq_node.count_caches() == 0: # type: ignore self.freq_link_head = head_freq_node.nxt # type: ignore head_freq_node.remove() # type: ignore def create_cache_node(self, key, report_type, value): cache_node = CacheNode( key=key, report_type=report_type, value=value, freq_node=None, pre=None, nxt=None) self.cache[key] = cache_node if self.freq_link_head is None or self.freq_link_head.freq != 0: new_freq_node = FreqNode(0, None, None) new_freq_node.append_cache_to_tail(cache_node) if self.freq_link_head is not None: self.freq_link_head.insert_before_me(new_freq_node) self.freq_link_head = new_freq_node else: self.freq_link_head.append_cache_to_tail(cache_node) def get_sorted_cache_keys(self): result = [(i, freq.freq_node.freq) for i, freq in self.cache.items()] result.sort(key=lambda x: -x[1]) return result def get_average_frequency(self): return mean(freq.freq_node.freq for freq in self.cache.values()) class DummyLFU: def __init__(self, *args, **kwargs): pass set = __init__ get = __init__ def __contains__(self, key): return False qlustered-deepdiff-41c7265/deepdiff/model.py000066400000000000000000001262051516241264500207770ustar00rootroot00000000000000import logging from collections.abc import Mapping from copy import copy from typing import Any, Dict, List, Optional, Set, Union, Literal, Type, TYPE_CHECKING from deepdiff.helper import ( RemapDict, strings, notpresent, get_type, numpy_numbers, np, literal_eval_extended, dict_, SetOrdered) from deepdiff.path import stringify_element if TYPE_CHECKING: from deepdiff.diff import DeepDiff logger = logging.getLogger(__name__) FORCE_DEFAULT: Literal['fake'] = 'fake' UP_DOWN: Dict[str, str] = {'up': 'down', 'down': 'up'} REPORT_KEYS: Set[str] = { "type_changes", "dictionary_item_added", "dictionary_item_removed", "values_changed", "unprocessed", "iterable_item_added", "iterable_item_removed", "iterable_item_moved", "attribute_added", "attribute_removed", "set_item_removed", "set_item_added", "repetition_change", } CUSTOM_FIELD: str = "__internal:custom:extra_info" class DoesNotExist(Exception): pass class ResultDict(RemapDict): def remove_empty_keys(self) -> None: """ Remove empty keys from this object. Should always be called after the result is final. :return: """ empty_keys = [k for k, v in self.items() if not isinstance(v, (int)) and not v] for k in empty_keys: del self[k] class TreeResult(ResultDict): def __init__(self) -> None: for key in REPORT_KEYS: self[key] = SetOrdered() def mutual_add_removes_to_become_value_changes(self) -> None: """ There might be the same paths reported in the results as removed and added. In such cases they should be reported as value_changes. Note that this function mutates the tree in ways that causes issues when report_repetition=True and should be avoided in that case. This function should only be run on the Tree Result. """ iterable_item_added = self.get('iterable_item_added') iterable_item_removed = self.get('iterable_item_removed') if iterable_item_added is not None and iterable_item_removed is not None: added_paths = {i.path(): i for i in iterable_item_added} removed_paths = {i.path(): i for i in iterable_item_removed} mutual_paths = set(added_paths) & set(removed_paths) if mutual_paths and 'values_changed' not in self or self['values_changed'] is None: self['values_changed'] = SetOrdered() for path in mutual_paths: level_before = removed_paths[path] iterable_item_removed.remove(level_before) level_after = added_paths[path] iterable_item_added.remove(level_after) level_before.t2 = level_after.t2 self['values_changed'].add(level_before) # type: ignore level_before.report_type = 'values_changed' if 'iterable_item_removed' in self and not iterable_item_removed: del self['iterable_item_removed'] if 'iterable_item_added' in self and not iterable_item_added: del self['iterable_item_added'] def __getitem__(self, item: str) -> SetOrdered: if item not in self: self[item] = SetOrdered() result = self.get(item) if result is None: result = SetOrdered() self[item] = result return result def __len__(self) -> int: length = 0 for value in self.values(): if isinstance(value, SetOrdered): length += len(value) elif isinstance(value, int): length += 1 return length class TextResult(ResultDict): ADD_QUOTES_TO_STRINGS: bool = True def __init__(self, tree_results: Optional['TreeResult'] = None, verbose_level: int = 1) -> None: self.verbose_level = verbose_level # TODO: centralize keys self.update({ "type_changes": dict_(), "dictionary_item_added": self.__set_or_dict(), "dictionary_item_removed": self.__set_or_dict(), "values_changed": dict_(), "unprocessed": [], "iterable_item_added": dict_(), "iterable_item_removed": dict_(), "iterable_item_moved": dict_(), "attribute_added": self.__set_or_dict(), "attribute_removed": self.__set_or_dict(), "set_item_removed": SetOrdered(), "set_item_added": SetOrdered(), "repetition_change": dict_() }) if tree_results: self._from_tree_results(tree_results) def __set_or_dict(self) -> Union[Dict[str, Any], SetOrdered]: return {} if self.verbose_level >= 2 else SetOrdered() def _from_tree_results(self, tree: 'TreeResult') -> None: """ Populate this object by parsing an existing reference-style result dictionary. :param tree: A TreeResult :return: """ self._from_tree_type_changes(tree) self._from_tree_default(tree, 'dictionary_item_added') self._from_tree_default(tree, 'dictionary_item_removed') self._from_tree_value_changed(tree) self._from_tree_unprocessed(tree) self._from_tree_default(tree, 'iterable_item_added') self._from_tree_default(tree, 'iterable_item_removed') self._from_tree_iterable_item_moved(tree) self._from_tree_default(tree, 'attribute_added') self._from_tree_default(tree, 'attribute_removed') self._from_tree_set_item_removed(tree) self._from_tree_set_item_added(tree) self._from_tree_repetition_change(tree) self._from_tree_deep_distance(tree) self._from_tree_custom_results(tree) def _from_tree_default(self, tree: 'TreeResult', report_type: str, ignore_if_in_iterable_opcodes: bool = False) -> None: if report_type in tree: for change in tree[report_type]: # report each change # When we convert from diff to delta result, we care more about opcodes than iterable_item_added or removed if ( ignore_if_in_iterable_opcodes and report_type in {"iterable_item_added", "iterable_item_removed"} and change.up.path(force=FORCE_DEFAULT) in self["_iterable_opcodes"] ): continue # determine change direction (added or removed) # Report t2 (the new one) whenever possible. # In cases where t2 doesn't exist (i.e. stuff removed), report t1. if change.t2 is not notpresent: item = change.t2 else: item = change.t1 # do the reporting report = self[report_type] if isinstance(report, SetOrdered): report.add(change.path(force=FORCE_DEFAULT)) elif isinstance(report, dict): report[change.path(force=FORCE_DEFAULT)] = item elif isinstance(report, list): # pragma: no cover # we don't actually have any of those right now, but just in case report.append(change.path(force=FORCE_DEFAULT)) else: # pragma: no cover # should never happen raise TypeError("Cannot handle {} report container type.". format(report)) def _from_tree_type_changes(self, tree): if 'type_changes' in tree: for change in tree['type_changes']: path = change.path(force=FORCE_DEFAULT) if type(change.t1) is type: include_values = False old_type = change.t1 new_type = change.t2 else: include_values = True old_type = get_type(change.t1) new_type = get_type(change.t2) remap_dict = RemapDict({ 'old_type': old_type, 'new_type': new_type, }) if self.verbose_level > 1: new_path = change.path(use_t2=True, force=FORCE_DEFAULT) if path != new_path: remap_dict['new_path'] = new_path self['type_changes'][path] = remap_dict if self.verbose_level and include_values: remap_dict.update(old_value=change.t1, new_value=change.t2) def _from_tree_value_changed(self, tree): if 'values_changed' in tree and self.verbose_level > 0: for change in tree['values_changed']: path = change.path(force=FORCE_DEFAULT) the_changed = {'new_value': change.t2, 'old_value': change.t1} if self.verbose_level > 1: new_path = change.path(use_t2=True, force=FORCE_DEFAULT) if path != new_path: the_changed['new_path'] = new_path self['values_changed'][path] = the_changed if 'diff' in change.additional: the_changed.update({'diff': change.additional['diff']}) def _from_tree_iterable_item_moved(self, tree): if 'iterable_item_moved' in tree and self.verbose_level > 1: for change in tree['iterable_item_moved']: the_changed = {'new_path': change.path(use_t2=True, reporting_move=True), 'value': change.t2} self['iterable_item_moved'][change.path( force=FORCE_DEFAULT, use_t2=False, reporting_move=True)] = the_changed def _from_tree_unprocessed(self, tree): if 'unprocessed' in tree: for change in tree['unprocessed']: self['unprocessed'].append("{}: {} and {}".format(change.path( force=FORCE_DEFAULT), change.t1, change.t2)) def _from_tree_set_item_added_or_removed(self, tree, key): if key in tree: set_item_info = self[key] is_dict = isinstance(set_item_info, Mapping) for change in tree[key]: path = change.up.path( ) # we want't the set's path, the added item is not directly accessible item = change.t2 if key == 'set_item_added' else change.t1 if self.ADD_QUOTES_TO_STRINGS and isinstance(item, strings): item = "'%s'" % item if is_dict: if path not in set_item_info: set_item_info[path] = set() # type: ignore set_item_info[path].add(item) else: set_item_info.add("{}[{}]".format(path, str(item))) # this syntax is rather peculiar, but it's DeepDiff 2.x compatible) def _from_tree_set_item_added(self, tree): self._from_tree_set_item_added_or_removed(tree, key='set_item_added') def _from_tree_set_item_removed(self, tree): self._from_tree_set_item_added_or_removed(tree, key='set_item_removed') def _from_tree_repetition_change(self, tree): if 'repetition_change' in tree: for change in tree['repetition_change']: path = change.path(force=FORCE_DEFAULT) self['repetition_change'][path] = RemapDict( change.additional['repetition'] ) self['repetition_change'][path]['value'] = change.t1 def _from_tree_deep_distance(self, tree): if 'deep_distance' in tree: self['deep_distance'] = tree['deep_distance'] def _from_tree_custom_results(self, tree): for k, _level_list in tree.items(): if k not in REPORT_KEYS: if not isinstance(_level_list, SetOrdered): continue # if len(_level_list) == 0: # continue # # if not isinstance(_level_list[0], DiffLevel): # continue # _level_list is a list of DiffLevel _custom_dict = {} for _level in _level_list: _custom_dict[_level.path( force=FORCE_DEFAULT)] = _level.additional.get(CUSTOM_FIELD, {}) self[k] = _custom_dict class DeltaResult(TextResult): ADD_QUOTES_TO_STRINGS: bool = False def __init__(self, tree_results: Optional['TreeResult'] = None, ignore_order: Optional[bool] = None, always_include_values: bool = False, _iterable_opcodes: Optional[Dict[str, Any]] = None) -> None: self.ignore_order = ignore_order self.always_include_values = always_include_values self.update({ "type_changes": dict_(), "dictionary_item_added": dict_(), "dictionary_item_removed": dict_(), "values_changed": dict_(), "iterable_item_added": dict_(), "iterable_item_removed": dict_(), "iterable_item_moved": dict_(), "attribute_added": dict_(), "attribute_removed": dict_(), "set_item_removed": dict_(), "set_item_added": dict_(), "iterable_items_added_at_indexes": dict_(), "iterable_items_removed_at_indexes": dict_(), "_iterable_opcodes": _iterable_opcodes or {}, }) if tree_results: self._from_tree_results(tree_results) def _from_tree_results(self, tree): """ Populate this object by parsing an existing reference-style result dictionary. :param tree: A TreeResult :return: """ self._from_tree_type_changes(tree) self._from_tree_default(tree, 'dictionary_item_added') self._from_tree_default(tree, 'dictionary_item_removed') self._from_tree_value_changed(tree) if self.ignore_order: self._from_tree_iterable_item_added_or_removed( tree, 'iterable_item_added', delta_report_key='iterable_items_added_at_indexes') self._from_tree_iterable_item_added_or_removed( tree, 'iterable_item_removed', delta_report_key='iterable_items_removed_at_indexes') else: self._from_tree_default(tree, 'iterable_item_added', ignore_if_in_iterable_opcodes=True) self._from_tree_default(tree, 'iterable_item_removed', ignore_if_in_iterable_opcodes=True) self._from_tree_iterable_item_moved(tree) self._from_tree_default(tree, 'attribute_added') self._from_tree_default(tree, 'attribute_removed') self._from_tree_set_item_removed(tree) self._from_tree_set_item_added(tree) self._from_tree_repetition_change(tree) def _from_tree_iterable_item_added_or_removed(self, tree, report_type, delta_report_key): if report_type in tree: for change in tree[report_type]: # report each change # determine change direction (added or removed) # Report t2 (the new one) whenever possible. # In cases where t2 doesn't exist (i.e. stuff removed), report t1. if change.t2 is not notpresent: item = change.t2 else: item = change.t1 # do the reporting path, param, _ = change.path(force=FORCE_DEFAULT, get_parent_too=True) try: iterable_items_added_at_indexes = self[delta_report_key][path] except KeyError: iterable_items_added_at_indexes = self[delta_report_key][path] = dict_() iterable_items_added_at_indexes[param] = item def _from_tree_type_changes(self, tree): if 'type_changes' in tree: for change in tree['type_changes']: include_values = None if type(change.t1) is type: include_values = False old_type = change.t1 new_type = change.t2 else: old_type = get_type(change.t1) new_type = get_type(change.t2) include_values = True try: if new_type in numpy_numbers: new_t1 = change.t1.astype(new_type) include_values = not np.array_equal(new_t1, change.t2) else: new_t1 = new_type(change.t1) # If simply applying the type from one value converts it to the other value, # there is no need to include the actual values in the delta. include_values = new_t1 != change.t2 except Exception: pass path = change.path(force=FORCE_DEFAULT) new_path = change.path(use_t2=True, force=FORCE_DEFAULT) remap_dict = RemapDict({ 'old_type': old_type, 'new_type': new_type, }) if path != new_path: remap_dict['new_path'] = new_path self['type_changes'][path] = remap_dict if include_values or self.always_include_values: remap_dict.update(old_value=change.t1, new_value=change.t2) def _from_tree_value_changed(self, tree): if 'values_changed' in tree: for change in tree['values_changed']: path = change.path(force=FORCE_DEFAULT) new_path = change.path(use_t2=True, force=FORCE_DEFAULT) the_changed = {'new_value': change.t2, 'old_value': change.t1} if path != new_path: the_changed['new_path'] = new_path self['values_changed'][path] = the_changed # If we ever want to store the difflib results instead of the new_value # these lines need to be uncommented and the Delta object needs to be able # to use them. # if 'diff' in change.additional: # the_changed.update({'diff': change.additional['diff']}) def _from_tree_repetition_change(self, tree): if 'repetition_change' in tree: for change in tree['repetition_change']: path, _, _ = change.path(get_parent_too=True) repetition = RemapDict(change.additional['repetition']) value = change.t1 try: iterable_items_added_at_indexes = self['iterable_items_added_at_indexes'][path] except KeyError: iterable_items_added_at_indexes = self['iterable_items_added_at_indexes'][path] = dict_() for index in repetition['new_indexes']: iterable_items_added_at_indexes[index] = value def _from_tree_iterable_item_moved(self, tree): if 'iterable_item_moved' in tree: for change in tree['iterable_item_moved']: if ( change.up.path(force=FORCE_DEFAULT, reporting_move=True) not in self["_iterable_opcodes"] ): the_changed = {'new_path': change.path(use_t2=True, reporting_move=True), 'value': change.t2} self['iterable_item_moved'][change.path( force=FORCE_DEFAULT, reporting_move=True)] = the_changed class DiffLevel: """ An object of this class represents a single object-tree-level in a reported change. A double-linked list of these object describes a single change on all of its levels. Looking at the tree of all changes, a list of those objects represents a single path through the tree (which is just fancy for "a change"). This is the result object class for object reference style reports. Example: >>> t1 = {2: 2, 4: 44} >>> t2 = {2: "b", 5: 55} >>> ddiff = DeepDiff(t1, t2, view='tree') >>> ddiff {'dictionary_item_added': {}, 'dictionary_item_removed': {}, 'type_changes': {}} Graph: ↑up ↑up | | | ChildRelationship | ChildRelationship | | ↓down ↓down .path() = 'root[5]' .path() = 'root[4]' Note that the 2 top level DiffLevel objects are 2 different objects even though they are essentially talking about the same diff operation. A ChildRelationship object describing the relationship between t1 and it's child object, where t1's child object equals down.t1. Think about it like a graph: +---------------------------------------------------------------+ | | | parent difflevel parent | | + ^ + | +------|--------------------------|---------------------|-------+ | | | up | | Child | | | ChildRelationship | Relationship | | | | down | | | +------|----------------------|-------------------------|-------+ | v v v | | child difflevel child | | | +---------------------------------------------------------------+ The child_rel example: # dictionary_item_removed is a set so in order to get an item from it: >>> (difflevel,) = ddiff['dictionary_item_removed']) >>> difflevel.up.t1_child_rel >>> (difflevel,) = ddiff['dictionary_item_added']) >>> difflevel >>> difflevel.up >>> >>> difflevel.up # t1 didn't exist >>> difflevel.up.t1_child_rel # t2 is added >>> difflevel.up.t2_child_rel """ def __init__(self, t1: Any, t2: Any, down: Optional['DiffLevel'] = None, up: Optional['DiffLevel'] = None, report_type: Optional[str] = None, child_rel1: Optional['ChildRelationship'] = None, child_rel2: Optional['ChildRelationship'] = None, additional: Optional[Dict[str, Any]] = None, verbose_level: int = 1) -> None: """ :param child_rel1: Either: - An existing ChildRelationship object describing the "down" relationship for t1; or - A ChildRelationship subclass. In this case, we will create the ChildRelationship objects for both t1 and t2. Alternatives for child_rel1 and child_rel2 must be used consistently. :param child_rel2: Either: - An existing ChildRelationship object describing the "down" relationship for t2; or - The param argument for a ChildRelationship class we shall create. Alternatives for child_rel1 and child_rel2 must be used consistently. """ # The current-level object in the left hand tree self.t1 = t1 # The current-level object in the right hand tree self.t2 = t2 # Another DiffLevel object describing this change one level deeper down the object tree self.down = down # Another DiffLevel object describing this change one level further up the object tree self.up = up self.report_type = report_type # If this object is this change's deepest level, this contains a string describing the type of change. # Examples: "set_item_added", "values_changed" # Note: don't use {} as additional's default value - this would turn out to be always the same dict object self.additional = dict_() if additional is None else additional # For some types of changes we store some additional information. # This is a dict containing this information. # Currently, this is used for: # - values_changed: In case the changes data is a multi-line string, # we include a textual diff as additional['diff']. # - repetition_change: additional['repetition']: # e.g. {'old_repeat': 2, 'new_repeat': 1, 'old_indexes': [0, 2], 'new_indexes': [2]} # the user supplied ChildRelationship objects for t1 and t2 # A ChildRelationship object describing the relationship between t1 and it's child object, # where t1's child object equals down.t1. # If this relationship is representable as a string, str(self.t1_child_rel) returns a formatted param parsable python string, # e.g. "[2]", ".my_attribute" self.t1_child_rel = child_rel1 # Another ChildRelationship object describing the relationship between t2 and it's child object. self.t2_child_rel = child_rel2 # Will cache result of .path() per 'force' as key for performance self._path = dict_() self.verbose_level = verbose_level def __repr__(self) -> str: if self.verbose_level: from deepdiff.summarize import summarize if self.additional: additional_repr = summarize(self.additional, max_length=35) result = "<{} {}>".format(self.path(), additional_repr) else: t1_repr = summarize(self.t1, max_length=35) t2_repr = summarize(self.t2, max_length=35) result = "<{} t1:{}, t2:{}>".format(self.path(), t1_repr, t2_repr) else: result = "<{}>".format(self.path()) return result def __setattr__(self, key: str, value: Any) -> None: # Setting up or down, will set the opposite link in this linked list. if key in UP_DOWN and value is not None: self.__dict__[key] = value opposite_key = UP_DOWN[key] value.__dict__[opposite_key] = self else: self.__dict__[key] = value def __iter__(self) -> Any: yield self.t1 yield self.t2 @property def repetition(self) -> Dict[str, Any]: return self.additional['repetition'] def auto_generate_child_rel(self, klass: Type['ChildRelationship'], param: Any, param2: Optional[Any] = None) -> None: """ Auto-populate self.child_rel1 and self.child_rel2. This requires self.down to be another valid DiffLevel object. :param klass: A ChildRelationship subclass describing the kind of parent-child relationship, e.g. DictRelationship. :param param: A ChildRelationship subclass-dependent parameter describing how to get from parent to child, e.g. the key in a dict """ if self.down.t1 is not notpresent: # type: ignore self.t1_child_rel = ChildRelationship.create( klass=klass, parent=self.t1, child=self.down.t1, param=param) # type: ignore if self.down.t2 is not notpresent: # type: ignore self.t2_child_rel = ChildRelationship.create( klass=klass, parent=self.t2, child=self.down.t2, param=param if param2 is None else param2) # type: ignore @property def all_up(self) -> 'DiffLevel': """ Get the root object of this comparison. (This is a convenient wrapper for following the up attribute as often as you can.) :rtype: DiffLevel """ level = self while level.up: level = level.up return level @property def all_down(self) -> 'DiffLevel': """ Get the leaf object of this comparison. (This is a convenient wrapper for following the down attribute as often as you can.) :rtype: DiffLevel """ level = self while level.down: level = level.down return level @staticmethod def _format_result(root: str, result: Optional[str]) -> Optional[str]: return None if result is None else "{}{}".format(root, result) def get_root_key(self, use_t2: bool = False) -> Any: """ Get the path's root key value for this change For example if the path to the element that is reported to have a change in value is root['X'][0] then get_root_key should return 'X' """ root_level = self.all_up if(use_t2): next_rel = root_level.t2_child_rel else: next_rel = root_level.t1_child_rel or root_level.t2_child_rel # next relationship object to get a formatted param from if next_rel: return next_rel.param return notpresent def path(self, root: str = "root", force: Optional[str] = None, get_parent_too: bool = False, use_t2: bool = False, output_format: Literal['str', 'list'] = 'str', reporting_move: bool = False) -> Any: """ A python syntax string describing how to descend to this level, assuming the top level object is called root. Returns None if the path is not representable as a string. This might be the case for example if there are sets involved (because then there's not path at all) or because custom objects used as dictionary keys (then there is a path but it's not representable). Example: root['ingredients'][0] Note: We will follow the left side of the comparison branch, i.e. using the t1's to build the path. Using t1 or t2 should make no difference at all, except for the last step of a child-added/removed relationship. If it does in any other case, your comparison path is corrupt. **Parameters** :param root: The result string shall start with this var name :param force: Bends the meaning of "no string representation". If None: Will strictly return Python-parsable expressions. The result those yield will compare equal to the objects in question. If 'yes': Will return a path including '(unrepresentable)' in place of non string-representable parts. If 'fake': Will try to produce an output optimized for readability. This will pretend all iterables are subscriptable, for example. :param output_format: The format of the output. The options are 'str' which is the default and produces a string representation of the path or 'list' to produce a list of keys and attributes that produce the path. :param reporting_move: This should be set to true if and only if we are reporting on iterable_item_moved. All other cases should leave this set to False. """ # TODO: We could optimize this by building on top of self.up's path if it is cached there cache_key = "{}{}{}{}".format(force, get_parent_too, use_t2, output_format) if cache_key in self._path: cached = self._path[cache_key] if get_parent_too: parent, param, result = cached return (self._format_result(root, parent), param, self._format_result(root, result)) else: return self._format_result(root, cached) if output_format == 'str': result = parent = param = "" else: result = [] level = self.all_up # start at the root # traverse all levels of this relationship while level and level is not self: # get this level's relationship object if level.additional.get("moved") and not reporting_move: # To ensure we can properly replay items such as values_changed in items that may have moved, we # need to make sure that all paths are reported relative to t2 if a level has reported a move. # If we are reporting a move, the path is already correct and does not need to be swapped. # Additional context of "moved" is only ever set if using iterable_compare_func and a move has taken place. level_use_t2 = not use_t2 else: level_use_t2 = use_t2 if level_use_t2: next_rel = level.t2_child_rel or level.t1_child_rel else: next_rel = level.t1_child_rel or level.t2_child_rel # next relationship object to get a formatted param from # t1 and t2 both are empty if next_rel is None: break # Build path for this level if output_format == 'str': item = next_rel.get_param_repr(force) if item: parent = result param = next_rel.param result += item else: # it seems this path is not representable as a string result = None break elif output_format == 'list': result.append(next_rel.param) # type: ignore # Prepare processing next level level = level.down if output_format == 'str': if get_parent_too: self._path[cache_key] = (parent, param, result) # type: ignore output = (self._format_result(root, parent), param, self._format_result(root, result)) # type: ignore else: self._path[cache_key] = result output = self._format_result(root, result) if isinstance(result, (str, type(None))) else None else: output = result return output def create_deeper(self, new_t1: Any, new_t2: Any, child_relationship_class: Type['ChildRelationship'], child_relationship_param: Optional[Any] = None, child_relationship_param2: Optional[Any] = None, report_type: Optional[str] = None) -> 'DiffLevel': """ Start a new comparison level and correctly link it to this one. :rtype: DiffLevel :return: New level """ level = self.all_down result = DiffLevel( new_t1, new_t2, down=None, up=level, report_type=report_type, verbose_level=self.verbose_level) level.down = result level.auto_generate_child_rel( klass=child_relationship_class, param=child_relationship_param, param2=child_relationship_param2) return result def branch_deeper(self, new_t1: Any, new_t2: Any, child_relationship_class: Type['ChildRelationship'], child_relationship_param: Optional[Any] = None, child_relationship_param2: Optional[Any] = None, report_type: Optional[str] = None) -> 'DiffLevel': """ Branch this comparison: Do not touch this comparison line, but create a new one with exactly the same content, just one level deeper. :rtype: DiffLevel :return: New level in new comparison line """ branch = self.copy() return branch.create_deeper(new_t1, new_t2, child_relationship_class, child_relationship_param, child_relationship_param2, report_type) def copy(self) -> 'DiffLevel': """ Get a deep copy of this comparision line. :return: The leaf ("downmost") object of the copy. """ orig = self.all_up result = copy(orig) # copy top level while orig is not None: result.additional = copy(orig.additional) if orig.down is not None: # copy and create references to the following level # copy following level result.down = copy(orig.down) if orig.t1_child_rel is not None: result.t1_child_rel = ChildRelationship.create( klass=orig.t1_child_rel.__class__, parent=result.t1, child=result.down.t1, param=orig.t1_child_rel.param) if orig.t2_child_rel is not None: result.t2_child_rel = ChildRelationship.create( klass=orig.t2_child_rel.__class__, parent=result.t2, child=result.down.t2, param=orig.t2_child_rel.param) # descend to next level orig = orig.down if result.down is not None: result = result.down return result class ChildRelationship: """ Describes the relationship between a container object (the "parent") and the contained "child" object. """ # Format to a be used for representing param. # E.g. for a dict, this turns a formatted param param "42" into "[42]". param_repr_format: Optional[str] = None # This is a hook allowing subclasses to manipulate param strings. # :param string: Input string # :return: Manipulated string, as appropriate in this context. quote_str: Optional[str] = None @staticmethod def create(klass: Type['ChildRelationship'], parent: Any, child: Any, param: Optional[Any] = None) -> 'ChildRelationship': if not issubclass(klass, ChildRelationship): raise TypeError return klass(parent, child, param) def __init__(self, parent: Any, child: Any, param: Optional[Any] = None) -> None: # The parent object of this relationship, e.g. a dict self.parent = parent # The child object of this relationship, e.g. a value in a dict self.child = child # A subclass-dependent parameter describing how to get from parent to child, e.g. the key in a dict self.param = param def __repr__(self) -> str: from deepdiff.summarize import summarize name = "<{} parent:{}, child:{}, param:{}>" parent = summarize(self.parent, max_length=35) child = summarize(self.child, max_length=35) param = summarize(self.param, max_length=15) return name.format(self.__class__.__name__, parent, child, param) def get_param_repr(self, force: Optional[str] = None) -> Optional[str]: """ Returns a formatted param python parsable string describing this relationship, or None if the relationship is not representable as a string. This string can be appended to the parent Name. Subclasses representing a relationship that cannot be expressed as a string override this method to return None. Examples: "[2]", ".attribute", "['mykey']" :param force: Bends the meaning of "no string representation". If None: Will strictly return partials of Python-parsable expressions. The result those yield will compare equal to the objects in question. If 'yes': Will return a formatted param including '(unrepresentable)' instead of the non string-representable part. """ return self.stringify_param(force) def stringify_param(self, force: Optional[str] = None) -> Optional[str]: """ Convert param to a string. Return None if there is no string representation. This is called by get_param_repr() :param force: Bends the meaning of "no string representation". If None: Will strictly return Python-parsable expressions. The result those yield will compare equal to the objects in question. If 'yes': Will return '(unrepresentable)' instead of None if there is no string representation TODO: stringify_param has issues with params that when converted to string via repr, it is not straightforward to turn them back into the original object. Although repr is meant to be able to reconstruct the original object but for complex objects, repr often does not recreate the original object. Perhaps we should log that the repr reconstruction failed so the user is aware. """ param = self.param if isinstance(param, strings): result = stringify_element(param, quote_str=self.quote_str) elif isinstance(param, tuple): # Currently only for numpy ndarrays result = ']['.join(map(repr, param)) elif hasattr(param, '__dataclass_fields__'): attrs_to_values = [f"{key}={value}" for key, value in [(i, getattr(param, i)) for i in param.__dataclass_fields__]] # type: ignore result = f"{param.__class__.__name__}({','.join(attrs_to_values)})" else: candidate = repr(param) try: resurrected = literal_eval_extended(candidate) # Note: This will miss string-representable custom objects. # However, the only alternative I can currently think of is using eval() which is inherently dangerous. except (SyntaxError, ValueError) as err: logger.error( f'stringify_param was not able to get a proper repr for "{param}". ' "This object will be reported as None. Add instructions for this object to DeepDiff's " f"helper.literal_eval_extended to make it work properly: {err}") result = None else: result = candidate if resurrected == param else None if result: result = ':' if self.param_repr_format is None else self.param_repr_format.format(result) return result class DictRelationship(ChildRelationship): param_repr_format: Optional[str] = "[{}]" quote_str: Optional[str] = "'{}'" class NumpyArrayRelationship(ChildRelationship): param_repr_format: Optional[str] = "[{}]" quote_str: Optional[str] = None class SubscriptableIterableRelationship(DictRelationship): pass class InaccessibleRelationship(ChildRelationship): pass # there is no random access to set elements class SetRelationship(InaccessibleRelationship): pass class NonSubscriptableIterableRelationship(InaccessibleRelationship): param_repr_format: Optional[str] = "[{}]" def get_param_repr(self, force: Optional[str] = None) -> Optional[str]: if force == 'yes': result = "(unrepresentable)" elif force == 'fake' and self.param: result = self.stringify_param() else: result = None return result class AttributeRelationship(ChildRelationship): param_repr_format: Optional[str] = ".{}" qlustered-deepdiff-41c7265/deepdiff/operator.py000066400000000000000000000046701516241264500215330ustar00rootroot00000000000000import re from typing import Any, Optional, List, TYPE_CHECKING from abc import ABCMeta, abstractmethod from deepdiff.helper import convert_item_or_items_into_compiled_regexes_else_none if TYPE_CHECKING: from deepdiff import DeepDiff class BaseOperatorPlus(metaclass=ABCMeta): @abstractmethod def match(self, level) -> bool: """ Given a level which includes t1 and t2 in the tree view, is this operator a good match to compare t1 and t2? If yes, we will run the give_up_diffing to compare t1 and t2 for this level. """ pass @abstractmethod def give_up_diffing(self, level, diff_instance: "DeepDiff") -> bool: """ Given a level which includes t1 and t2 in the tree view, and the "distance" between l1 and l2. do we consider t1 and t2 to be equal or not. The distance is a number between zero to one and is calculated by DeepDiff to measure how similar objects are. """ @abstractmethod def normalize_value_for_hashing(self, parent: Any, obj: Any) -> Any: """ You can use this function to normalize values for ignore_order=True For example, you may want to turn all the words to be lowercase. Then you return obj.lower() """ pass class BaseOperator: def __init__(self, regex_paths:Optional[List[str]]=None, types:Optional[List[type]]=None): if regex_paths: self.regex_paths = convert_item_or_items_into_compiled_regexes_else_none(regex_paths) else: self.regex_paths = None self.types = types def match(self, level) -> bool: if self.regex_paths: for pattern in self.regex_paths: matched = re.search(pattern, level.path()) is not None if matched: return True if self.types: for type_ in self.types: if isinstance(level.t1, type_) and isinstance(level.t2, type_): return True return False def give_up_diffing(self, level, diff_instance) -> bool: raise NotImplementedError('Please implement the diff function.') class PrefixOrSuffixOperator: def match(self, level) -> bool: return level.t1 and level.t2 and isinstance(level.t1, str) and isinstance(level.t2, str) def give_up_diffing(self, level, diff_instance) -> bool: t1 = level.t1 t2 = level.t2 return t1.startswith(t2) or t2.startswith(t1) qlustered-deepdiff-41c7265/deepdiff/path.py000066400000000000000000000245331516241264500206340ustar00rootroot00000000000000import logging from ast import literal_eval from functools import lru_cache logger = logging.getLogger(__name__) GETATTR = 'GETATTR' GET = 'GET' class PathExtractionError(ValueError): pass class RootCanNotBeModified(ValueError): pass def _add_to_elements(elements, elem, inside): # Ignore private items if not elem: return if not elem.startswith('__'): remove_quotes = False if '𝆺𝅥𝅯' in elem or '\\' in elem: remove_quotes = True else: try: elem = literal_eval(elem) remove_quotes = False except (ValueError, SyntaxError): remove_quotes = True if remove_quotes and elem[0] == elem[-1] and elem[0] in {'"', "'"}: elem = elem[1: -1] action = GETATTR if inside == '.' else GET elements.append((elem, action)) DEFAULT_FIRST_ELEMENT = ('root', GETATTR) @lru_cache(maxsize=1024 * 128) def _path_to_elements(path, root_element=DEFAULT_FIRST_ELEMENT): """ Given a path, it extracts the elements that form the path and their relevant most likely retrieval action. >>> from deepdiff import _path_to_elements >>> path = "root[4.3].b['a3']" >>> _path_to_elements(path, root_element=None) [(4.3, 'GET'), ('b', 'GETATTR'), ('a3', 'GET')] """ if isinstance(path, (tuple, list)): return path elements = [] if root_element: elements.append(root_element) elem = '' inside = False prev_char = None path = path[4:] # removing "root from the beginning" brackets = [] inside_quotes = False quote_used = '' for char in path: if prev_char == '𝆺𝅥𝅯': elem += char elif char in {'"', "'"}: elem += char # If we are inside and the quote is not what we expected, the quote is not closing if not(inside_quotes and quote_used != char): inside_quotes = not inside_quotes if inside_quotes: quote_used = char else: _add_to_elements(elements, elem, inside) elem = '' quote_used = '' elif inside_quotes: elem += char elif char == '[': if inside == '.': _add_to_elements(elements, elem, inside) inside = '[' elem = '' # we are already inside. The bracket is a part of the word. elif inside == '[': elem += char else: inside = '[' brackets.append('[') elem = '' elif char == '.': if inside == '[': elem += char elif inside == '.': _add_to_elements(elements, elem, inside) elem = '' else: inside = '.' elem = '' elif char == ']': if brackets and brackets[-1] == '[': brackets.pop() if brackets: elem += char else: _add_to_elements(elements, elem, inside) elem = '' inside = False else: elem += char prev_char = char if elem: _add_to_elements(elements, elem, inside) return tuple(elements) def _get_nested_obj(obj, elements, next_element=None): for (elem, action) in elements: check_elem(elem) if action == GET: obj = obj[elem] elif action == GETATTR: obj = getattr(obj, elem) return obj def _guess_type(elements, elem, index, next_element): # If we are not at the last elements if index < len(elements) - 1: # We assume it is a nested dictionary not a nested list return {} if isinstance(next_element, int): return [] return {} def check_elem(elem): if isinstance(elem, str) and elem.startswith("__") and elem.endswith("__"): raise ValueError("traversing dunder attributes is not allowed") def _get_nested_obj_and_force(obj, elements, next_element=None): prev_elem = None prev_action = None prev_obj = obj for index, (elem, action) in enumerate(elements): check_elem(elem) _prev_obj = obj if action == GET: try: obj = obj[elem] prev_obj = _prev_obj except KeyError: obj[elem] = _guess_type(elements, elem, index, next_element) obj = obj[elem] prev_obj = _prev_obj except IndexError: if isinstance(obj, list) and isinstance(elem, int) and elem >= len(obj): obj.extend([None] * (elem - len(obj))) obj.append(_guess_type(elements, elem, index), next_element) obj = obj[-1] prev_obj = _prev_obj elif isinstance(obj, list) and len(obj) == 0 and prev_elem: # We ran into an empty list that should have been a dictionary # We need to change it from an empty list to a dictionary obj = {elem: _guess_type(elements, elem, index, next_element)} if prev_action == GET: prev_obj[prev_elem] = obj else: setattr(prev_obj, prev_elem, obj) obj = obj[elem] elif action == GETATTR: obj = getattr(obj, elem) prev_obj = _prev_obj prev_elem = elem prev_action = action return obj def extract(obj, path): """ Get the item from obj based on path. Example: >>> from deepdiff import extract >>> obj = {1: [{'2': 'b'}, 3], 2: [4, 5]} >>> path = "root[1][0]['2']" >>> extract(obj, path) 'b' Note that you can use extract in conjunction with DeepDiff results or even with the search and :ref:`deepsearch_label` modules. For example: >>> from deepdiff import grep >>> obj = {1: [{'2': 'b'}, 3], 2: [4, 5]} >>> result = obj | grep(5) >>> result {'matched_values': ['root[2][1]']} >>> result['matched_values'][0] 'root[2][1]' >>> path = result['matched_values'][0] >>> extract(obj, path) 5 .. note:: Note that even if DeepDiff tried gives you a path to an item in a set, there is no such thing in Python and hence you will get an error trying to extract that item from a set. If you want to be able to get items from sets, use the SetOrdered module to generate the sets. In fact Deepdiff uses SetOrdered as a dependency. >>> from deepdiff import grep, extract >>> obj = {"a", "b"} >>> obj | grep("b") Set item detected in the path.'set' objects do NOT support indexing. But DeepSearch will still report a path. {'matched_values': SetOrdered(['root[0]'])} >>> extract(obj, 'root[0]') Traceback (most recent call last): File "", line 1, in File "deepdiff/deepdiff/path.py", line 126, in extract return _get_nested_obj(obj, elements) File "deepdiff/deepdiff/path.py", line 84, in _get_nested_obj obj = obj[elem] TypeError: 'set' object is not subscriptable >>> from orderly_set import SetOrdered >>> obj = SetOrdered(["a", "b"]) >>> extract(obj, 'root[0]') 'a' """ elements = _path_to_elements(path, root_element=None) return _get_nested_obj(obj, elements) def parse_path(path, root_element=DEFAULT_FIRST_ELEMENT, include_actions=False): """ Parse a path to a format that is machine readable **Parameters** path : A string The path string such as "root[1][2]['age']" root_element: string, default='root' What the root is called in the path. include_actions: boolean, default=False If True, we return the action required to retrieve the item at each element of the path. **Examples** >>> from deepdiff import parse_path >>> parse_path("root[1][2]['age']") [1, 2, 'age'] >>> parse_path("root[1][2]['age']", include_actions=True) [{'element': 1, 'action': 'GET'}, {'element': 2, 'action': 'GET'}, {'element': 'age', 'action': 'GET'}] >>> >>> parse_path("root['joe'].age") ['joe', 'age'] >>> parse_path("root['joe'].age", include_actions=True) [{'element': 'joe', 'action': 'GET'}, {'element': 'age', 'action': 'GETATTR'}] """ result = _path_to_elements(path, root_element=root_element) result = iter(result) if root_element: next(result) # We don't want the root item if include_actions is False: return [i[0] for i in result] return [{'element': i[0], 'action': i[1]} for i in result] def stringify_element(param, quote_str=None): has_quote = "'" in param has_double_quote = '"' in param if has_quote and has_double_quote and not quote_str: new_param = [] for char in param: if char in {'"', "'"}: new_param.append('𝆺𝅥𝅯') new_param.append(char) result = '"' + ''.join(new_param) + '"' elif has_quote: result = f'"{param}"' elif has_double_quote: result = f"'{param}'" else: result = param if quote_str is None else quote_str.format(param) return result def stringify_path(path, root_element=DEFAULT_FIRST_ELEMENT, quote_str="'{}'"): """ Gets the path as an string. For example [1, 2, 'age'] should become root[1][2]['age'] """ if not path: return root_element[0] result = [root_element[0]] has_actions = False try: if path[0][1] in {GET, GETATTR}: has_actions = True except (KeyError, IndexError, TypeError): pass if not has_actions: path = [(i, GET) for i in path] path[0] = (path[0][0], root_element[1]) # The action for the first element might be a GET or GETATTR. We update the action based on the root_element. for element, action in path: if isinstance(element, str) and action == GET: element = stringify_element(element, quote_str) if action == GET: result.append(f"[{element}]") else: result.append(f".{element}") return ''.join(result) qlustered-deepdiff-41c7265/deepdiff/py.typed000066400000000000000000000000001516241264500210040ustar00rootroot00000000000000qlustered-deepdiff-41c7265/deepdiff/search.py000066400000000000000000000335701516241264500211460ustar00rootroot00000000000000#!/usr/bin/env python import re from collections.abc import MutableMapping, Iterable from typing import Any, Dict, FrozenSet, List, Pattern, Set, Union, Tuple from deepdiff.helper import SetOrdered import logging from deepdiff.helper import ( strings, numbers, add_to_frozen_set, get_doc, dict_, RE_COMPILED_TYPE, ipranges ) logger = logging.getLogger(__name__) doc = get_doc('search_doc.rst') class DeepSearch(Dict[str, Union[Dict[str, Any], SetOrdered, List[str]]]): r""" **DeepSearch** Deep Search inside objects to find the item matching your criteria. **Parameters** obj : The object to search within item : The item to search for verbose_level : int >= 0, default = 1. Verbose level one shows the paths of found items. Verbose level 2 shows the path and value of the found items. exclude_paths: list, default = None. List of paths to exclude from the report. exclude_types: list, default = None. List of object types to exclude from the report. case_sensitive: Boolean, default = False match_string: Boolean, default = False If True, the value of the object or its children have to exactly match the item. If False, the value of the item can be a part of the value of the object or its children use_regexp: Boolean, default = False strict_checking: Boolean, default = True If True, it will check the type of the object to match, so when searching for '1234', it will NOT match the int 1234. Currently this only affects the numeric values searching. **Returns** A DeepSearch object that has the matched paths and matched values. **Supported data types** int, string, unicode, dictionary, list, tuple, set, frozenset, OrderedDict, NamedTuple and custom objects! **Examples** Importing >>> from deepdiff import DeepSearch >>> from pprint import pprint Search in list for string >>> obj = ["long somewhere", "string", 0, "somewhere great!"] >>> item = "somewhere" >>> ds = DeepSearch(obj, item, verbose_level=2) >>> print(ds) {'matched_values': {'root[0]': 'long somewhere', 'root[3]': 'somewhere great!'}} Search in nested data for string >>> obj = ["something somewhere", {"long": "somewhere", "string": 2, 0: 0, "somewhere": "around"}] >>> item = "somewhere" >>> ds = DeepSearch(obj, item, verbose_level=2) >>> pprint(ds, indent=2) { 'matched_paths': {"root[1]['somewhere']": 'around'}, 'matched_values': { 'root[0]': 'something somewhere', "root[1]['long']": 'somewhere'}} """ warning_num: int = 0 def __init__(self, obj: Any, item: Any, exclude_paths: Union[SetOrdered, Set[str], List[str]] = SetOrdered(), exclude_regex_paths: Union[SetOrdered, Set[Union[str, Pattern[str]]], List[Union[str, Pattern[str]]]] = SetOrdered(), exclude_types: Union[SetOrdered, Set[type], List[type]] = SetOrdered(), verbose_level: int = 1, case_sensitive: bool = False, match_string: bool = False, use_regexp: bool = False, strict_checking: bool = True, **kwargs: Any) -> None: if kwargs: raise ValueError(( "The following parameter(s) are not valid: %s\n" "The valid parameters are obj, item, exclude_paths, exclude_types,\n" "case_sensitive, match_string and verbose_level." ) % ', '.join(kwargs.keys())) self.obj: Any = obj self.case_sensitive: bool = case_sensitive if isinstance(item, strings) else True item = item if self.case_sensitive else (item.lower() if isinstance(item, str) else item) self.exclude_paths: SetOrdered = SetOrdered(exclude_paths) self.exclude_regex_paths: List[Pattern[str]] = [re.compile(exclude_regex_path) for exclude_regex_path in exclude_regex_paths] self.exclude_types: SetOrdered = SetOrdered(exclude_types) self.exclude_types_tuple: tuple[type, ...] = tuple( exclude_types) # we need tuple for checking isinstance self.verbose_level: int = verbose_level self.update( matched_paths=self.__set_or_dict(), matched_values=self.__set_or_dict(), unprocessed=[]) # Type narrowing for mypy/pyright self.matched_paths: Union[Dict[str, Any], SetOrdered] self.matched_values: Union[Dict[str, Any], SetOrdered] self.unprocessed: List[str] self.use_regexp: bool = use_regexp if not strict_checking and (isinstance(item, numbers) or isinstance(item, ipranges)): item = str(item) if self.use_regexp: try: item = re.compile(item) except TypeError as e: raise TypeError(f"The passed item of {item} is not usable for regex: {e}") from None self.strict_checking: bool = strict_checking # Cases where user wants to match exact string item self.match_string: bool = match_string self.__search(obj, item, parents_ids=frozenset({id(obj)})) empty_keys = [k for k, v in self.items() if not v] for k in empty_keys: del self[k] def __set_or_dict(self) -> Union[Dict[str, Any], SetOrdered]: return dict_() if self.verbose_level >= 2 else SetOrdered() def __report(self, report_key: str, key: str, value: Any) -> None: if self.verbose_level >= 2: report_dict = self[report_key] if isinstance(report_dict, dict): report_dict[key] = value else: report_set = self[report_key] if isinstance(report_set, SetOrdered): report_set.add(key) def __search_obj(self, obj: Any, item: Any, parent: str, parents_ids: FrozenSet[int] = frozenset(), is_namedtuple: bool = False) -> None: """Search objects""" found = False if obj == item: found = True # We report the match but also continue inside the match to see if there are # further matches inside the `looped` object. self.__report(report_key='matched_values', key=parent, value=obj) try: if is_namedtuple: obj = obj._asdict() else: # Skip magic methods. Slightly hacky, but unless people are defining # new magic methods they want to search, it should work fine. obj = {i: getattr(obj, i) for i in dir(obj) if not (i.startswith('__') and i.endswith('__'))} except AttributeError: try: obj = {i: getattr(obj, i) for i in obj.__slots__} except AttributeError: if not found: unprocessed = self.get('unprocessed', []) if isinstance(unprocessed, list): unprocessed.append("%s" % parent) return self.__search_dict( obj, item, parent, parents_ids, print_as_attribute=True) def __skip_this(self, item: Any, parent: str) -> bool: skip = False if parent in self.exclude_paths: skip = True elif self.exclude_regex_paths and any( [exclude_regex_path.search(parent) for exclude_regex_path in self.exclude_regex_paths]): skip = True else: if isinstance(item, self.exclude_types_tuple): skip = True return skip def __search_dict(self, obj: Union[Dict[Any, Any], MutableMapping[Any, Any]], item: Any, parent: str, parents_ids: FrozenSet[int] = frozenset(), print_as_attribute: bool = False) -> None: """Search dictionaries""" if print_as_attribute: parent_text = "%s.%s" else: parent_text = "%s[%s]" obj_keys = SetOrdered(obj.keys()) for item_key in obj_keys: if not print_as_attribute and isinstance(item_key, strings): item_key_str = "'%s'" % item_key else: item_key_str = item_key obj_child = obj[item_key] item_id = id(obj_child) if parents_ids and item_id in parents_ids: continue parents_ids_added = add_to_frozen_set(parents_ids, item_id) new_parent = parent_text % (parent, item_key_str) new_parent_cased = new_parent if self.case_sensitive else new_parent.lower() str_item = str(item) if (self.match_string and str_item == new_parent_cased) or\ (not self.match_string and str_item in new_parent_cased) or\ (self.use_regexp and item.search(new_parent_cased)): self.__report( report_key='matched_paths', key=new_parent, value=obj_child) self.__search( obj_child, item, parent=new_parent, parents_ids=parents_ids_added) def __search_iterable(self, obj: Iterable[Any], item: Any, parent: str = "root", parents_ids: FrozenSet[int] = frozenset()) -> None: """Search iterables except dictionaries, sets and strings.""" for i, thing in enumerate(obj): new_parent = "{}[{}]".format(parent, i) if self.__skip_this(thing, parent=new_parent): continue if self.case_sensitive or not isinstance(thing, strings): thing_cased = thing else: thing_cased = thing.lower() if isinstance(thing, str) else thing if not self.use_regexp and thing_cased == item: self.__report( report_key='matched_values', key=new_parent, value=thing) else: item_id = id(thing) if parents_ids and item_id in parents_ids: continue parents_ids_added = add_to_frozen_set(parents_ids, item_id) self.__search(thing, item, "%s[%s]" % (parent, i), parents_ids_added) def __search_str(self, obj: Union[str, bytes, memoryview], item: Union[str, bytes, memoryview, Pattern[str]], parent: str) -> None: """Compare strings""" obj_text = obj if self.case_sensitive else (obj.lower() if isinstance(obj, str) else obj) is_matched = False if self.use_regexp and isinstance(item, type(re.compile(''))): is_matched = bool(item.search(str(obj_text))) elif (self.match_string and str(item) == str(obj_text)) or (not self.match_string and str(item) in str(obj_text)): is_matched = True if is_matched: self.__report(report_key='matched_values', key=parent, value=obj) def __search_numbers(self, obj: Any, item: Any, parent: str) -> None: if ( item == obj or ( not self.strict_checking and ( item == str(obj) or ( self.use_regexp and item.search(str(obj)) ) ) ) ): self.__report(report_key='matched_values', key=parent, value=obj) def __search_tuple(self, obj: Tuple[Any, ...], item: Any, parent: str, parents_ids: FrozenSet[int]) -> None: # Checking to see if it has _fields. Which probably means it is a named # tuple. try: getattr(obj, '_asdict') # It must be a normal tuple except AttributeError: self.__search_iterable(obj, item, parent, parents_ids) # We assume it is a namedtuple then else: self.__search_obj( obj, item, parent, parents_ids, is_namedtuple=True) def __search(self, obj: Any, item: Any, parent: str = "root", parents_ids: FrozenSet[int] = frozenset()) -> None: """The main search method""" if self.__skip_this(item, parent): return elif isinstance(obj, strings) and isinstance(item, (strings, RE_COMPILED_TYPE)): self.__search_str(obj, item, parent) elif isinstance(obj, strings) and isinstance(item, numbers): return elif isinstance(obj, ipranges): self.__search_str(str(obj), item, parent) elif isinstance(obj, numbers): self.__search_numbers(obj, item, parent) elif isinstance(obj, MutableMapping): self.__search_dict(obj, item, parent, parents_ids) elif isinstance(obj, tuple): self.__search_tuple(obj, item, parent, parents_ids) elif isinstance(obj, (set, frozenset)): if self.warning_num < 10: logger.warning( "Set item detected in the path." "'set' objects do NOT support indexing. But DeepSearch will still report a path." ) self.warning_num += 1 self.__search_iterable(obj, item, parent, parents_ids) elif isinstance(obj, Iterable) and not isinstance(obj, strings): self.__search_iterable(obj, item, parent, parents_ids) else: self.__search_obj(obj, item, parent, parents_ids) class grep: __doc__ = doc def __init__(self, item: Any, **kwargs: Any) -> None: self.item: Any = item self.kwargs: Dict[str, Any] = kwargs def __ror__(self, other: Any) -> "DeepSearch": return DeepSearch(obj=other, item=self.item, **self.kwargs) if __name__ == "__main__": # pragma: no cover import doctest doctest.testmod() qlustered-deepdiff-41c7265/deepdiff/serialization.py000066400000000000000000000731021516241264500225510ustar00rootroot00000000000000import pickle import sys import io import os import json import uuid import logging import re # NOQA import builtins # NOQA import datetime # NOQA import decimal # NOQA import orderly_set # NOQA import collections # NOQA import fractions import ipaddress import base64 from copy import deepcopy, copy from functools import partial from collections.abc import Mapping, KeysView from typing import ( Callable, Optional, Union, overload, Literal, Any, ) from deepdiff.helper import ( strings, get_type, TEXT_VIEW, TREE_VIEW, np_float32, np_float64, np_int32, np_int64, np_ndarray, Opcode, SetOrdered, pydantic_base_model_type, PydanticBaseModel, NotPresent, ipranges, ) from deepdiff.model import DeltaResult try: import orjson except ImportError: # pragma: no cover. orjson = None logger = logging.getLogger(__name__) class UnsupportedFormatErr(TypeError): pass NONE_TYPE = type(None) CSV_HEADER_MAX_CHUNK_SIZE = 2048 # The chunk needs to be big enough that covers a couple of rows of data. MODULE_NOT_FOUND_MSG = 'DeepDiff Delta did not find {} in your modules. Please make sure it is already imported.' FORBIDDEN_MODULE_MSG = "Module '{}' is forbidden. You need to explicitly pass it by passing a safe_to_import parameter" DELTA_IGNORE_ORDER_NEEDS_REPETITION_REPORT = 'report_repetition must be set to True when ignore_order is True to create the delta object.' DELTA_ERROR_WHEN_GROUP_BY = 'Delta can not be made when group_by is used since the structure of data is modified from the original form.' SAFE_TO_IMPORT = frozenset({ 'builtins.range', 'builtins.complex', 'builtins.set', 'builtins.frozenset', 'builtins.slice', 'builtins.str', 'builtins.bytes', 'builtins.list', 'builtins.tuple', 'builtins.int', 'builtins.float', 'builtins.dict', 'builtins.bool', 'builtins.bin', 'builtins.None', 'datetime.datetime', 'datetime.time', 'datetime.timedelta', 'decimal.Decimal', 'fractions.Fraction', 'uuid.UUID', 'orderly_set.sets.OrderedSet', 'orderly_set.sets.OrderlySet', 'orderly_set.sets.StableSetEq', 'deepdiff.helper.SetOrdered', 'collections.namedtuple', 'collections.OrderedDict', 're.Pattern', 'deepdiff.helper.Opcode', 'ipaddress.IPv4Interface', 'ipaddress.IPv6Interface', 'ipaddress.IPv4Network', 'ipaddress.IPv6Network', 'ipaddress.IPv4Address', 'ipaddress.IPv6Address', 'collections.abc.KeysView', }) TYPE_STR_TO_TYPE = { 'range': range, 'complex': complex, 'set': set, 'frozenset': frozenset, 'slice': slice, 'str': str, 'bytes': bytes, 'list': list, 'tuple': tuple, 'int': int, 'float': float, 'dict': dict, 'bool': bool, 'bin': bin, 'None': None, 'NoneType': None, 'datetime': datetime.datetime, 'time': datetime.time, 'timedelta': datetime.timedelta, 'Decimal': decimal.Decimal, 'SetOrdered': SetOrdered, 'namedtuple': collections.namedtuple, 'OrderedDict': collections.OrderedDict, 'Pattern': re.Pattern, 'iprange': str, 'IPv4Address': ipaddress.IPv4Address, 'IPv6Address': ipaddress.IPv6Address, 'KeysView': list, } class ModuleNotFoundError(ImportError): """ Raised when the module is not found in sys.modules """ pass class ForbiddenModule(ImportError): """ Raised when a module is not explicitly allowed to be imported """ pass class SerializationMixin: def to_json_pickle(self): """ :ref:`to_json_pickle_label` Get the json pickle of the diff object. Unless you need all the attributes and functionality of DeepDiff, running to_json() is the safer option that json pickle. """ try: import jsonpickle copied = self.copy() # type: ignore return jsonpickle.encode(copied) except ImportError: # pragma: no cover. Json pickle is getting deprecated. logger.error('jsonpickle library needs to be installed in order to run to_json_pickle') # pragma: no cover. Json pickle is getting deprecated. @classmethod def from_json_pickle(cls, value): """ :ref:`from_json_pickle_label` Load DeepDiff object with all the bells and whistles from the json pickle dump. Note that json pickle dump comes from to_json_pickle """ try: import jsonpickle return jsonpickle.decode(value) except ImportError: # pragma: no cover. Json pickle is getting deprecated. logger.error('jsonpickle library needs to be installed in order to run from_json_pickle') # pragma: no cover. Json pickle is getting deprecated. def to_json(self, default_mapping: Optional[dict]=None, force_use_builtin_json=False, verbose_level: Optional[int]=None, **kwargs): """ Dump json of the text view. **Parameters** default_mapping : dictionary(optional), a dictionary of mapping of different types to json types. by default DeepDiff converts certain data types. For example Decimals into floats so they can be exported into json. If you have a certain object type that the json serializer can not serialize it, please pass the appropriate type conversion through this dictionary. force_use_builtin_json: Boolean, default = False When True, we use Python's builtin Json library for serialization, even if Orjson is installed. verbose_level: int, default=None Override the verbose_level for the serialized output. See to_dict() for details. kwargs: Any other kwargs you pass will be passed on to Python's json.dumps() **Example** Serialize custom objects >>> class A: ... pass ... >>> class B: ... pass ... >>> t1 = A() >>> t2 = B() >>> ddiff = DeepDiff(t1, t2) >>> ddiff.to_json() TypeError: We do not know how to convert <__main__.A object at 0x10648> of type for json serialization. Please pass the default_mapping parameter with proper mapping of the object to a basic python type. >>> default_mapping = {A: lambda x: 'obj A', B: lambda x: 'obj B'} >>> ddiff.to_json(default_mapping=default_mapping) '{"type_changes": {"root": {"old_type": "A", "new_type": "B", "old_value": "obj A", "new_value": "obj B"}}}' """ dic = self.to_dict(verbose_level=verbose_level) return json_dumps( dic, default_mapping=default_mapping, force_use_builtin_json=force_use_builtin_json, **kwargs, ) def to_dict(self, verbose_level: Optional[int]=None) -> dict: """ Convert the result to a python dictionary. **Parameters** verbose_level: int, default=None Override the verbose_level for the serialized output. When None, the behavior depends on the original view: - If the original view is 'text', the verbose_level from DeepDiff initialization is used. - If the original view is 'tree', verbose_level=2 is used to provide the most detailed output. Valid values are 0, 1, or 2. """ if verbose_level is not None and verbose_level not in {0, 1, 2}: raise ValueError('verbose_level should be 0, 1, or 2.') if verbose_level is None: if self.view == TREE_VIEW: # type: ignore verbose_level = 2 else: verbose_level = self.verbose_level # type: ignore return dict(self._get_view_results(TEXT_VIEW, verbose_level=verbose_level)) # type: ignore def _to_delta_dict( self, directed: bool = True, report_repetition_required: bool = True, always_include_values: bool = False, ) -> dict: """ Dump to a dictionary suitable for delta usage. Unlike to_dict, this is not dependent on the original view that the user chose to create the diff. **Parameters** directed : Boolean, default=True, whether to create a directional delta dictionary or a symmetrical Note that in the current implementation the symmetrical delta (non-directional) is ONLY used for verifying that the delta is being applied to the exact same values as what was used to generate the delta and has no other usages. If this option is set as True, then the dictionary will not have the "old_value" in the output. Otherwise it will have the "old_value". "old_value" is the value of the item in t1. If delta = Delta(DeepDiff(t1, t2)) then t1 + delta == t2 Note that it the items in t1 + delta might have slightly different order of items than t2 if ignore_order was set to be True in the diff object. """ if self.group_by is not None: # type: ignore raise ValueError(DELTA_ERROR_WHEN_GROUP_BY) if directed and not always_include_values: _iterable_opcodes = {} # type: ignore for path, op_codes in self._iterable_opcodes.items(): # type: ignore _iterable_opcodes[path] = [] for op_code in op_codes: new_op_code = Opcode( tag=op_code.tag, t1_from_index=op_code.t1_from_index, t1_to_index=op_code.t1_to_index, t2_from_index=op_code.t2_from_index, t2_to_index=op_code.t2_to_index, new_values=op_code.new_values, ) _iterable_opcodes[path].append(new_op_code) else: _iterable_opcodes = self._iterable_opcodes # type: ignore result = DeltaResult( tree_results=self.tree, # type: ignore ignore_order=self.ignore_order, # type: ignore always_include_values=always_include_values, _iterable_opcodes=_iterable_opcodes, ) result.remove_empty_keys() if report_repetition_required and self.ignore_order and not self.report_repetition: # type: ignore raise ValueError(DELTA_IGNORE_ORDER_NEEDS_REPETITION_REPORT) if directed: for report_key, report_value in result.items(): if isinstance(report_value, Mapping): for path, value in report_value.items(): if isinstance(value, Mapping) and 'old_value' in value: del value['old_value'] # type: ignore if self._numpy_paths: # type: ignore # Note that keys that start with '_' are considered internal to DeepDiff # and will be omitted when counting distance. (Look inside the distance module.) result['_numpy_paths'] = self._numpy_paths # type: ignore if self.iterable_compare_func: # type: ignore result['_iterable_compare_func_was_used'] = True return deepcopy(dict(result)) def pretty(self, prefix: Optional[Union[str, Callable]]=None): """ The pretty human readable string output for the diff object regardless of what view was used to generate the diff. prefix can be a callable or a string or None. Example: >>> t1={1,2,4} >>> t2={2,3} >>> print(DeepDiff(t1, t2).pretty()) Item root[3] added to set. Item root[4] removed from set. Item root[1] removed from set. """ result = [] if prefix is None: prefix = '' keys = sorted(self.tree.keys()) # type: ignore # sorting keys to guarantee constant order across python versions. for key in keys: for item_key in self.tree[key]: # type: ignore result += [pretty_print_diff(item_key)] if callable(prefix): return "\n".join(f"{prefix(diff=self)}{r}" for r in result) return "\n".join(f"{prefix}{r}" for r in result) # Maximum size allowed for integer arguments to constructors that allocate # memory proportional to the argument (e.g. bytes(n), bytearray(n)). # This prevents denial-of-service via crafted pickle payloads. (CVE-2026-33155) _MAX_ALLOC_SIZE = 128 * 1024 * 1024 # 128 MB # Callables where an integer argument directly controls memory allocation size. _SIZE_SENSITIVE_CALLABLES = frozenset({bytes, bytearray}) class _SafeConstructor: """Wraps a type constructor to prevent excessive memory allocation via the REDUCE opcode.""" __slots__ = ('_wrapped',) def __init__(self, wrapped): self._wrapped = wrapped def __call__(self, *args, **kwargs): for arg in args: if isinstance(arg, int) and arg > _MAX_ALLOC_SIZE: raise pickle.UnpicklingError( "Refusing to create {}() with size {}: " "exceeds the maximum allowed size of {} bytes. " "This could be a denial-of-service attack payload.".format( self._wrapped.__name__, arg, _MAX_ALLOC_SIZE ) ) return self._wrapped(*args, **kwargs) class _RestrictedUnpickler(pickle.Unpickler): def __init__(self, *args, **kwargs): self.safe_to_import = kwargs.pop('safe_to_import', None) if self.safe_to_import: if isinstance(self.safe_to_import, strings): self.safe_to_import = set([self.safe_to_import]) elif isinstance(self.safe_to_import, (set, frozenset)): pass else: self.safe_to_import = set(self.safe_to_import) self.safe_to_import = self.safe_to_import | SAFE_TO_IMPORT else: self.safe_to_import = SAFE_TO_IMPORT super().__init__(*args, **kwargs) def find_class(self, module, name): # Only allow safe classes from self.safe_to_import. module_dot_class = '{}.{}'.format(module, name) if module_dot_class in self.safe_to_import: try: module_obj = sys.modules[module] except KeyError: raise ModuleNotFoundError(MODULE_NOT_FOUND_MSG.format(module_dot_class)) from None cls = getattr(module_obj, name) # Wrap size-sensitive callables to prevent DoS via large allocations if cls in _SIZE_SENSITIVE_CALLABLES: return _SafeConstructor(cls) return cls # Forbid everything else. raise ForbiddenModule(FORBIDDEN_MODULE_MSG.format(module_dot_class)) from None def persistent_load(self, pid): if pid == "<>": return type(None) class _RestrictedPickler(pickle.Pickler): def persistent_id(self, obj): if obj is NONE_TYPE: # NOQA return "<>" return None def pickle_dump(obj, file_obj=None, protocol=4): """ **pickle_dump** Dumps the obj into pickled content. **Parameters** obj : Any python object file_obj : (Optional) A file object to dump the contents into **Returns** If file_obj is passed the return value will be None. It will write the object's pickle contents into the file. However if no file_obj is passed, then it will return the pickle serialization of the obj in the form of bytes. """ file_obj_passed = bool(file_obj) file_obj = file_obj or io.BytesIO() _RestrictedPickler(file_obj, protocol=protocol, fix_imports=False).dump(obj) if not file_obj_passed: return file_obj.getvalue() def pickle_load(content=None, file_obj=None, safe_to_import=None): """ **pickle_load** Load the pickled content. content should be a bytes object. **Parameters** content : Bytes of pickled object. file_obj : A file object to load the content from safe_to_import : A set of modules that needs to be explicitly allowed to be loaded. Example: {'mymodule.MyClass', 'decimal.Decimal'} Note that this set will be added to the basic set of modules that are already allowed. The set of what is already allowed can be found in deepdiff.serialization.SAFE_TO_IMPORT **Returns** A delta object that can be added to t1 to recreate t2. **Examples** Importing >>> from deepdiff import DeepDiff, Delta >>> from pprint import pprint """ if not content and not file_obj: raise ValueError('Please either pass the content or the file_obj to pickle_load.') if isinstance(content, str): content = content.encode('utf-8') if content: file_obj = io.BytesIO(content) return _RestrictedUnpickler(file_obj, safe_to_import=safe_to_import).load() def _get_pretty_form_text(verbose_level): pretty_form_texts = { "type_changes": "Type of {diff_path} changed from {type_t1} to {type_t2} and value changed from {val_t1} to {val_t2}.", "values_changed": "Value of {diff_path} changed from {val_t1} to {val_t2}.", "dictionary_item_added": "Item {diff_path} added to dictionary.", "dictionary_item_removed": "Item {diff_path} removed from dictionary.", "iterable_item_added": "Item {diff_path} added to iterable.", "iterable_item_removed": "Item {diff_path} removed from iterable.", "attribute_added": "Attribute {diff_path} added.", "attribute_removed": "Attribute {diff_path} removed.", "set_item_added": "Item root[{val_t2}] added to set.", "set_item_removed": "Item root[{val_t1}] removed from set.", "repetition_change": "Repetition change for item {diff_path}.", } if verbose_level == 2: pretty_form_texts.update( { "dictionary_item_added": "Item {diff_path} ({val_t2}) added to dictionary.", "dictionary_item_removed": "Item {diff_path} ({val_t1}) removed from dictionary.", "iterable_item_added": "Item {diff_path} ({val_t2}) added to iterable.", "iterable_item_removed": "Item {diff_path} ({val_t1}) removed from iterable.", "attribute_added": "Attribute {diff_path} ({val_t2}) added.", "attribute_removed": "Attribute {diff_path} ({val_t1}) removed.", } ) return pretty_form_texts def pretty_print_diff(diff): type_t1 = get_type(diff.t1).__name__ type_t2 = get_type(diff.t2).__name__ val_t1 = '"{}"'.format(str(diff.t1)) if type_t1 == "str" else str(diff.t1) val_t2 = '"{}"'.format(str(diff.t2)) if type_t2 == "str" else str(diff.t2) diff_path = diff.path(root='root') return _get_pretty_form_text(diff.verbose_level).get(diff.report_type, "").format( diff_path=diff_path, type_t1=type_t1, type_t2=type_t2, val_t1=val_t1, val_t2=val_t2) def load_path_content(path, file_type=None): """ Loads and deserializes the content of the path. """ if file_type is None: file_type = path.split('.')[-1] if file_type == 'json': with open(path, 'r') as the_file: content = json_loads(the_file.read()) elif file_type in {'yaml', 'yml'}: try: import yaml except ImportError: # pragma: no cover. raise ImportError('Pyyaml needs to be installed.') from None # pragma: no cover. with open(path, 'r') as the_file: content = yaml.safe_load(the_file) elif file_type == 'toml': try: if sys.version_info >= (3, 11): import tomllib as tomli else: import tomli except ImportError: # pragma: no cover. raise ImportError('On python<=3.10 tomli needs to be installed.') from None # pragma: no cover. with open(path, 'rb') as the_file: content = tomli.load(the_file) elif file_type == 'pickle': with open(path, 'rb') as the_file: content = the_file.read() content = pickle_load(content) elif file_type in {'csv', 'tsv'}: try: import clevercsv # type: ignore content = clevercsv.read_dicts(path) except ImportError: # pragma: no cover. import csv with open(path, 'r') as the_file: content = list(csv.DictReader(the_file)) logger.info(f"NOTE: CSV content was empty in {path}") # Everything in csv is string but we try to automatically convert any numbers we find for row in content: for key, value in row.items(): value = value.strip() for type_ in [int, float, complex]: try: value = type_(value) except Exception: pass else: row[key] = value break else: raise UnsupportedFormatErr(f'Only json, yaml, toml, csv, tsv and pickle are supported.\n' f' The {file_type} extension is not known.') return content def save_content_to_path(content, path, file_type=None, keep_backup=True): """ Saves and serializes the content of the path. """ backup_path = f"{path}.bak" os.rename(path, backup_path) try: _save_content( content=content, path=path, file_type=file_type, keep_backup=keep_backup) except Exception: os.rename(backup_path, path) raise else: if not keep_backup: os.remove(backup_path) def _save_content(content, path, file_type, keep_backup=True): if file_type == 'json': with open(path, 'w') as the_file: content = json_dumps(content) the_file.write(content) # type: ignore elif file_type in {'yaml', 'yml'}: try: import yaml except ImportError: # pragma: no cover. raise ImportError('Pyyaml needs to be installed.') from None # pragma: no cover. with open(path, 'w') as the_file: content = yaml.safe_dump(content, stream=the_file) elif file_type == 'toml': try: import tomli_w except ImportError: # pragma: no cover. raise ImportError('Tomli-w needs to be installed.') from None # pragma: no cover. with open(path, 'wb') as the_file: content = tomli_w.dump(content, the_file) elif file_type == 'pickle': with open(path, 'wb') as the_file: content = pickle_dump(content, file_obj=the_file) elif file_type in {'csv', 'tsv'}: try: import clevercsv # type: ignore dict_writer = clevercsv.DictWriter except ImportError: # pragma: no cover. import csv dict_writer = csv.DictWriter with open(path, 'w', newline='') as csvfile: fieldnames = list(content[0].keys()) writer = dict_writer(csvfile, fieldnames=fieldnames) writer.writeheader() writer.writerows(content) else: raise UnsupportedFormatErr('Only json, yaml, toml, csv, tsv and pickle are supported.\n' f' The {file_type} extension is not known.') return content def _serialize_decimal(value): if value.as_tuple().exponent == 0: return int(value) else: return float(value) def _serialize_fraction(value): if value.denominator == 1: return value.numerator else: return float(value) def _serialize_tuple(value): if hasattr(value, '_asdict'): # namedtuple return value._asdict() return value def _serialize_bytes(value): """ Serialize bytes to JSON-compatible format. First tries UTF-8 decoding for backward compatibility. Falls back to base64 encoding for binary data. """ try: return value.decode('utf-8') except UnicodeDecodeError: return base64.b64encode(value).decode('ascii') JSON_CONVERTOR = { decimal.Decimal: _serialize_decimal, fractions.Fraction: _serialize_fraction, SetOrdered: list, orderly_set.StableSetEq: list, set: list, type: lambda x: x.__name__, bytes: _serialize_bytes, datetime.datetime: lambda x: x.isoformat(), uuid.UUID: lambda x: str(x), np_float32: float, np_float64: float, np_int32: int, np_int64: int, np_ndarray: lambda x: x.tolist(), tuple: _serialize_tuple, Mapping: dict, NotPresent: str, ipranges: str, memoryview: lambda x: x.tobytes(), KeysView: list, } if PydanticBaseModel is not pydantic_base_model_type: JSON_CONVERTOR[PydanticBaseModel] = lambda x: x.model_dump() def json_convertor_default(default_mapping=None): if default_mapping: _convertor_mapping = JSON_CONVERTOR.copy() _convertor_mapping.update(default_mapping) else: _convertor_mapping = JSON_CONVERTOR def _convertor(obj): for original_type, convert_to in _convertor_mapping.items(): if isinstance(obj, original_type): return convert_to(obj) # This is to handle reverse() which creates a generator of type list_reverseiterator if obj.__class__.__name__ == 'list_reverseiterator': return list(copy(obj)) # 3) gather @property values by scanning __class__.__dict__ and bases props = {} for cls in obj.__class__.__mro__: for name, descriptor in cls.__dict__.items(): if isinstance(descriptor, property) and not name.startswith('_'): try: props[name] = getattr(obj, name) except Exception: # skip properties that error out pass if props: return props # 4) fallback: public __dict__ entries if hasattr(obj, '__dict__'): return { k: v for k, v in vars(obj).items() if not k.startswith('_') } # 5) give up raise TypeError( f"Don't know how to JSON-serialize {obj!r} " f"(type {type(obj).__name__}); " "consider adding it to default_mapping." ) return _convertor class JSONDecoder(json.JSONDecoder): def __init__(self, *args, **kwargs): json.JSONDecoder.__init__(self, object_hook=self.object_hook, *args, **kwargs) def object_hook(self, obj): # type: ignore if 'old_type' in obj and 'new_type' in obj: for type_key in ('old_type', 'new_type'): type_str = obj[type_key] obj[type_key] = TYPE_STR_TO_TYPE.get(type_str, type_str) return obj @overload def json_dumps( item: Any, **kwargs, ) -> str: ... @overload def json_dumps( item: Any, default_mapping:Optional[dict], force_use_builtin_json: bool, return_bytes:Literal[True], **kwargs, ) -> bytes: ... @overload def json_dumps( item: Any, default_mapping:Optional[dict], force_use_builtin_json: bool, return_bytes:Literal[False], **kwargs, ) -> str: ... _INT64_MAX = 9223372036854775807 _INT64_MIN = -9223372036854775808 def _convert_oversized_ints(obj): """Recursively convert integers exceeding 64-bit range to strings. orjson cannot serialize integers outside the signed 64-bit range.""" if isinstance(obj, bool): return obj if isinstance(obj, int) and (obj > _INT64_MAX or obj < _INT64_MIN): return str(obj) if isinstance(obj, dict): return {k: _convert_oversized_ints(v) for k, v in obj.items()} if isinstance(obj, (list, tuple)): converted = [_convert_oversized_ints(v) for v in obj] if hasattr(obj, '_fields'): # NamedTuple: reconstruct using keyword arguments return type(obj)(**dict(zip(obj._fields, converted))) return type(obj)(converted) return obj def json_dumps( item: Any, default_mapping:Optional[dict]=None, force_use_builtin_json: bool = False, return_bytes: bool = False, **kwargs, ) -> Union[str, bytes]: """ Dump json with extra details that are not normally json serializable parameters ---------- force_use_builtin_json: Boolean, default = False When True, we use Python's builtin Json library for serialization, even if Orjson is installed. """ if orjson and not force_use_builtin_json: indent = kwargs.pop('indent', None) kwargs['option'] = orjson.OPT_NON_STR_KEYS | orjson.OPT_SERIALIZE_NUMPY if indent: kwargs['option'] |= orjson.OPT_INDENT_2 if 'sort_keys' in kwargs: raise TypeError( "orjson does not accept the sort_keys parameter. " "If you need to pass sort_keys, set force_use_builtin_json=True " "to use Python's built-in json library instead of orjson.") try: result = orjson.dumps( item, default=json_convertor_default(default_mapping=default_mapping), **kwargs) except TypeError as e: if 'Integer exceeds 64-bit range' in str(e): item = _convert_oversized_ints(item) result = orjson.dumps( item, default=json_convertor_default(default_mapping=default_mapping), **kwargs) else: raise if return_bytes: return result return result.decode(encoding='utf-8') else: result = json.dumps( item, default=json_convertor_default(default_mapping=default_mapping), **kwargs) if return_bytes: return result.encode(encoding='utf-8') return result json_loads = partial(json.loads, cls=JSONDecoder) qlustered-deepdiff-41c7265/deepdiff/summarize.py000066400000000000000000000127731516241264500217170ustar00rootroot00000000000000from typing import Tuple from deepdiff.helper import JSON, SummaryNodeType from deepdiff.serialization import json_dumps def _truncate(s: str, max_len: int) -> str: """ Truncate string s to max_len characters. If possible, keep the first (max_len-5) characters, then '...' then the last 2 characters. """ if len(s) <= max_len: return s if max_len <= 5: return s[:max_len] return s[:max_len - 5] + "..." + s[-2:] # Re-defining the functions due to environment reset # Function to calculate node weights recursively def calculate_weights(node): if isinstance(node, dict): weight = 0 children_weights = {} for k, v in node.items(): try: edge_weight = len(k) except TypeError: edge_weight = 1 child_weight, child_structure = calculate_weights(v) total_weight = edge_weight + child_weight weight += total_weight children_weights[k] = (edge_weight, child_weight, child_structure) return weight, (SummaryNodeType.dict, children_weights) elif isinstance(node, list): weight = 0 children_weights = [] for v in node: edge_weight = 0 # Index weights are zero child_weight, child_structure = calculate_weights(v) total_weight = edge_weight + child_weight weight += total_weight children_weights.append((edge_weight, child_weight, child_structure)) return weight, (SummaryNodeType.list, children_weights) else: if isinstance(node, str): node_weight = len(node) elif isinstance(node, int): node_weight = len(str(node)) elif isinstance(node, float): node_weight = len(str(round(node, 2))) elif node is None: node_weight = 1 else: node_weight = 0 return node_weight, (SummaryNodeType.leaf, node) # Include previously defined functions for shrinking with threshold # (Implementing directly the balanced summarization algorithm as above) # Balanced algorithm (simplified version): def shrink_tree_balanced(node_structure, max_weight: int, balance_threshold: float) -> Tuple[JSON, float]: node_type, node_info = node_structure if node_type is SummaryNodeType.leaf: leaf_value = node_info leaf_weight, _ = calculate_weights(leaf_value) if leaf_weight <= max_weight: return leaf_value, leaf_weight else: if isinstance(leaf_value, str): truncated_value = _truncate(leaf_value, max_weight) return truncated_value, len(truncated_value) elif isinstance(leaf_value, (int, float)): leaf_str = str(leaf_value) truncated_str = leaf_str[:max_weight] try: return int(truncated_str), len(truncated_str) except Exception: try: return float(truncated_str), len(truncated_str) except Exception: return truncated_str, len(truncated_str) elif leaf_value is None: return None, 1 if max_weight >= 1 else 0 elif node_type is SummaryNodeType.dict: shrunk_dict = {} total_weight = 0 sorted_children = sorted(node_info.items(), key=lambda x: x[1][0] + x[1][1], reverse=True) for k, (edge_w, _, child_struct) in sorted_children: allowed_branch_weight = min(max_weight * balance_threshold, max_weight - total_weight) if allowed_branch_weight <= edge_w: continue remaining_weight = int(allowed_branch_weight - edge_w) shrunk_child, shrunk_weight = shrink_tree_balanced(child_struct, remaining_weight, balance_threshold) if shrunk_child is not None: shrunk_dict[k[:edge_w]] = shrunk_child total_weight += edge_w + shrunk_weight if total_weight >= max_weight: break if not shrunk_dict: return None, 0 return shrunk_dict, total_weight elif node_type is SummaryNodeType.list: shrunk_list = [] total_weight = 0 sorted_children = sorted(node_info, key=lambda x: x[0] + x[1], reverse=True) for edge_w, _, child_struct in sorted_children: allowed_branch_weight = int(min(max_weight * balance_threshold, max_weight - total_weight)) shrunk_child, shrunk_weight = shrink_tree_balanced(child_struct, allowed_branch_weight, balance_threshold) if shrunk_child is not None: shrunk_list.append(shrunk_child) total_weight += shrunk_weight if total_weight >= max_weight - 1: shrunk_list.append("...") break if not shrunk_list: return None, 0 return shrunk_list, total_weight return None, 0 def greedy_tree_summarization_balanced(json_data: JSON, max_weight: int, balance_threshold=0.6) -> JSON: total_weight, tree_structure = calculate_weights(json_data) if total_weight <= max_weight: return json_data shrunk_tree, _ = shrink_tree_balanced(tree_structure, max_weight, balance_threshold) return shrunk_tree def summarize(data: JSON, max_length:int=200, balance_threshold:float=0.6) -> str: try: return json_dumps( greedy_tree_summarization_balanced(data, max_length, balance_threshold) ) except Exception: return str(data) qlustered-deepdiff-41c7265/docs/000077500000000000000000000000001516241264500165015ustar00rootroot00000000000000qlustered-deepdiff-41c7265/docs/AGENTS.md000077700000000000000000000000001516241264500212562CLAUDE.mdustar00rootroot00000000000000qlustered-deepdiff-41c7265/docs/CLAUDE.md000066400000000000000000000040101516241264500177530ustar00rootroot00000000000000# Docs CLAUDE.md ## Building Documentation ```bash cd docs && source ~/.venvs/deep/bin/activate && python buildme.py ``` The build output goes to `~/Workspace/sites/blog/public/deepdiff//` (configured via `docs/.env`). To verify changes, check the generated HTML, e.g. `~/Workspace/sites/blog/public/deepdiff/8.7.0/faq.html`. The Sphinx doctree cache is stored in `/tmp/sphinx_doctree`. The build script clears it each run. If you get permission errors on that directory, ask the user to `rm -rf /tmp/sphinx_doctree` first. ## Theme Uses the **Furo** Sphinx theme. Key customizations: - **Font**: Open Sans, loaded via `_static/custom.css` and set in `conf.py` via `light_css_variables` / `dark_css_variables` - **Footer**: Custom `_templates/page.html` overrides the default footer to remove Sphinx/Furo credit while keeping the copyright notice - **GA4**: Google Analytics tag (`G-KVVHD37BKD`) is injected via `_templates/page.html` in the `extrahead` block - **Pygments**: Uses Furo's default syntax highlighting (no explicit `pygments_style` set) ## Symlinked Docstrings Some RST files in `docs/` (e.g., `diff_doc.rst`, `deephash_doc.rst`, `search_doc.rst`) are symlinks to `deepdiff/docstrings/`. The files need to exist in both places: - **`deepdiff/docstrings/`** — So they're included in the generated wheel. `flit_core` (our build system) only packages files under the `deepdiff/` directory, and these are loaded at runtime by `get_doc()` in `helper.py` to serve as Python docstrings. - **`docs/`** — So Sphinx can find and build them as documentation pages. These files have a `:orphan:` directive on line 1 (needed by Sphinx to suppress toctree warnings). `get_doc()` strips it at runtime so it doesn't appear in the Python docstrings. ## File Structure - `conf.py` — Sphinx configuration - `buildme.py` — Build script (reads `.env` for `BUILD_PATH` and `DOC_VERSION`) - `_templates/page.html` — Extends `furo/page.html` for GA4 and custom footer - `_static/custom.css` — Loads Open Sans font from Google Fonts qlustered-deepdiff-41c7265/docs/Makefile000066400000000000000000000166441516241264500201540ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext help: @echo "Please use \`make ' where is one of" @echo " buildme echos what to run to do live builds." @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " applehelp to make an Apple Help Book" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" buildme: @echo "Please make sure the .env is pointing to the right path for the build. Then run ./buildme.py" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/DeepDiff.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/DeepDiff.qhc" applehelp: $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp @echo @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." @echo "N.B. You won't be able to view it unless you put it in" \ "~/Library/Documentation/Help or install it in your application" \ "bundle." devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/DeepDiff" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/DeepDiff" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." qlustered-deepdiff-41c7265/docs/_static/000077500000000000000000000000001516241264500201275ustar00rootroot00000000000000qlustered-deepdiff-41c7265/docs/_static/Qluster_square_grey_optimized.svg000066400000000000000000000037111516241264500270030ustar00rootroot00000000000000 image/svg+xml 1603fa08fed7b5f4cdb19d5fea57a83cf5160f84.paxheader00006660000000000000000000000261151624126450020746xustar00rootroot00000000000000177 path=qlustered-deepdiff-41c7265/docs/_static/benchmark_array_no_numpy__3.8__ignore_order=True__cache_size=0__cache_tuning_sample_size=0__cutoff_intersection_for_pairs=1.png 1603fa08fed7b5f4cdb19d5fea57a83cf5160f84.data000066400000000000000000001006131516241264500176060ustar00rootroot00000000000000PNG  IHDR5sBIT|d pHYsaa?i8tEXtSoftwarematplotlib version3.2.1, http://matplotlib.org/: IDATxy|Ld YDHk کҪB]Z\[[JUUTinUi-M*"$UuE A$]213,$1[z1sΜy̙g9Q!n8XDDDDdY DDDDv0@"""";HDDDdg  a$"""3 DDDDv0@"""";HDDDdg  a$"""3 DDDDv0@"""";HDDDdg  a$"""3 DDDDv0@"""";HDDDdg  a$"""3 DDDDv0@"""";HDDDdg  a$"""3 DDDDv0@"""";HDDDdg  a$"""3 DDDDv0@"""";HDDDdg  qv2Zcǎ!((DDDuVEff&:t''BMرcx """#Gv3PHHIVgnTUq]UUq] Wk]]r<=wtݾ!!! 3U*ѰaC8;;tw㺪>㺪3ݷ)@"""";HDDDdg  a$"""˗/cȑ;.ywww gΜ1ZFNNbcc ___;~)n֭?"%%o6,]+W{gDxxxo߾())'O">>;w1~xk$HDDDt+o&5j 6_+V`Μ92d`ӦM Ž;0b:u vѣGѩS'U0`,[ }M·~N:'D СC|驩@^sHHH$$$WW^ppp@bb^M DDDd V:?u֡Yfؽ{7&L)S`ƍ pj 4hhX ٥H//t>Vo:tcܸqx,b>DDDdRRRаaCmWWJ Add}Z¶mLDdee-CV#''GxKb쒗`npio4n< $88{OGbb"Err~{BբsΦ~i -L6 ]voÇȑ# N_͚5CDDΝP :OuR0i$1G DDDDtc={6.\XyfΜ"?޽;v777<7oƤIгgO888 &&+WKb$"""{ =X .\ V9?>ss4Ƹ a$"""3 DDDDv~ 4P(رc~J¬YbԨQHOO7ZFNNbcc ___;~)DDDDW'`QQڵk5kTV\\sO拍ɓ';wb?~^a`W:F^<SNa׮]8z(:uXj e˖Ye@F""""K˃B/ !!zL""""())YO dddTRo'sVT&mny^݈j̵jIWPRaB`ݺuwŋW_p={x˯LlUq]UUq] Wz]eggtyu]u… ػw2_V#''U.s٘>}˗={aÆ&o||MFnn.ѱcG޽{jѹs* WWW|\bggg~ATUq]UUq] Wz]99ݕF(,,ٳgSSS!!!x'cΝh4VZ_~7n{=T*L4 #FDDDt׫0)) =zuao۷7zܾ}#6oތI&gϞppp@LL V\i@DDDdEu2>#BT9Vt矛YDDDDu]HDDDD DDDDv0@"""";HDDDdg  a$"""3 DDDDv0@"""";HDDDdg  a$"""3 DDDDv0@"""";HDDDdg  a$"""3 DDDDv ""KNZ OJ жa Ш7 Ǝ\[ 9@PR5rR 7\ &| ȤzӁۑH?> 0ܾyٴI>Ժ DDDT\)o={޼P^f**_XWW2ȼ{?a yyVkXy 1@7?l^hUU|]dL )U\\gУx}+[,*庁u] Yt);U `FpЯ_;_8U,XBatiٲ~zII &Nxzz"&&FHKKQ^=4h3f̀ZKc$""cftU-sVYY,KZ0.zUWcl/@֭qiӦMw}[_Ezz: h0p@(J:t7n'|yY`$""K:qB lGO? |>زUjز/sgu+pk K`` //ׯ裏cǎذa:Ç~'>CѿkXf VJ DDd9eM,]kd=q8xPI#U(PXbmx{All,PTի~ޖ-["<< 7NHH@TT8ye_M DDd9T3P y/Y^| ח=f*_h %>]Pj(K4WKiΝ;O>]n:DAA222r. B<##(YOGDDcwuQW+?tPWC x]`TG}^^f,XoZ(p/5@ddc߿m۶EΝѸqc|pwx>㺀,; `ِ_i= ,9|KNܿݍg,_ussBWIxA5 oơ ___4ogϞE޽T*kTDpp0 88G1Z(a<.`"{&p[A y*[13ő?GnCrr2ƌhtЧODFFgDZ{n̙3'Nv45@"{& `m]{)ˎW`.rt5y4HRiOJJ" VFq1\pr;p%|;xzwttΝ;h9F…  ]}; dݶO;t`_7N2́ ׯˑZ[׿WP"޵k@.ٳs]pepu֢*H5~ձe˖[NwssÚ5kf͚*iܸ1~S7X$*E`krt_ǎYd22 b[frsfMUUV{mw] 4h8;Yֵk@@Wa>TlL7B-+EW Dem̺FtT]~~̹qC[~<˲e.ϳׯd$qі@ Wy1ؿx+LB`Ŋ3g mbӦMHOOW O:]vᣏ>BΝѽ{wZ [lAQ\,,/+ X:mW+^Vo{N_x Ϝp|ݼ#bxeRT >,_GKR՛7.ܚ t*ߪ)ir#Lƪ>^e,S,_.G*kÍt4~}iNWk}#5+Jwo$ٳwV}-[ cLvT3+>o۫^ykt)]0?0իen ))6mB07Wu9y\PxMhFu2Jjj*222NΝ;:uӫW/888 11mJ$8Z%o6m6~[,]+7P&;_ZQQrU9PiDܑ޽e[U*_//4^z ؽ750. _aÀC>V(9r\ΞV봒q%;[ծ9N+H`!'Spv >++_c2|\V|ŋ 'mw >T\m r`qd#N(ۊr]̘!1l%%I@&#,Nwņ1ڶ5To5?lnP Cs ܮ]E6UYXxQQ@^rnLxƍh^߲EcZ5|/{}Gr= <|?t/p)6p͌ '}/m\$ߋ2=:޸|SΓgxMBeg]A++K5*J~Q3fG m)u<]pV|ņxp`a^Gf@K;[._}7 y\U(ut .pܭD@l߾]HOO7'ÇBh"Ѽy ˪_XvmURR"@ RiKQQ8߳:: o кmfB۲ /<t$4 І B3j*BȏPvhFDMǎB3zP/[& mӦrP^-smT $~~y;vu넶^= %˖GF #6_~YѷCjP:: Ml \FVB=yк P?P 86aPWE Mh*!A^z$BӧֺnD(Wz3F>Lh==<;9 sBB&{v^\۶muk^F9RhzEzqZ2D^_{M(z8]-7sEOu߾B+_ByzJhy{ m͛ mDPϘa= m&B.Ǚ3'hR*u>ܦkyM=Z^h۷ӷO,/ {BӯP o%$yGΝVhٞqc]h}Tgԯ0^zȰ:9ϡжi#5m*_SHжj%i{*4kz_?nrӧ6k&֯< mU^7i" MBӥPO. ̀  mP<\Ov ..1nn;[7mmBs3uqAhzt+qC(KKUyG(JOqqr hRhM}[/ $mZ%NmOM պuBs}#3feb|.77^FhyDhڸQjFQh=dnjM^Ih۶5fD11bB[#T6dž պut&JPm. /^o.mIhyF/-)BSBYR"֔ P\)oi~ 0Ah۶d;#"j!q  N en~*]֯/(п=B=uжh!o'_z!"F}'T?$ ׬EEE&*/$E/^W_}{A``ɟ=FBC "zB{/QW/hgQS'&$yd<4Jw4~)Z-N]rwŋ9nׯ , LOBkn.|~ ^7́ m֯GIqvƥ]ᚖ.]ݦ :ZP BgAǑChy37mqgt(:/^PX Mj]M&'}dž 0t(|ϜA-[83hݸm cG\ =݈XB7D~&xhL8 IDATWqr(zGVzM#QsoG fzu'ѩe .>(ڵC?1cPÇ?P7j_|#br"}\ho71gq 8?u*Tx ō86iDGIn.n" ލr׮hPZ\8~=_|ȋCqP<\󨗕ԗ_F^hiJݦ efzi7nDp[^}ދ==?ժU 7f r5ý|&;bV +WxZhٳ_RX?ig͂sa!JJ[j77p gz:NH=[( %KPа!\Gӧ_?@ݬiNAk[Td\һtA3s& qx qP|:?~ :NPٳ/`Wb"nѣ߸1۶E]Q닟{ .>V7hr82:uB9pq8v<۴A8D2JR 5wwRpH.m|ǹ/_ A{=Vnn؛CЩSSq</aj*\]RDx}64uB sq1z!1e!ׯFU*ptt_ue =rZaP eRʽ⭗uㆼԴ R>? ٳ=uJ(]z<ïMR˗ӧ B!T{eNPO"_UJK {YiVرٳBuȪHxPWX64TbvrJ*m(-D2Mwf`_]33\qoįK_e:VT?(U֌%GgBvmU!//Un/ iBBq3)xPEBy(*()O?-8_QѶXK^  ww?yA!K ̓mӦb zۺ +BuPՊ`l2}yyyU|BnIIIyv- |rŋfۀJУжk'СB:Uq !j?EiB\T8 T b@tK Q2l{oӋX^7n&Jr L^6}W*bB)D5O!ƍzZSb=P!OT*]ѭUFB;wN>ife5ڶn99BAAwB dǏe>lB#Đ!B (K/yfϲzBh=l|qM2 G B'mBiӄ>yQ;̆Ttߏ![<`/ C@]<_~x{˯8vQk %}za`<= ˽tI4 x{J)um=zoO>O>3gDQQƏ\tv킛17oƤIгgO888 &&+k;̬[7y;%ݮeK 9pu5|*Z~MwiA #Mk흮(aO=%.֍;/'Gt(wI Ѐq;@^놁0WBpIT'#<;  .… Ww5kBVڵ_^mX%!Z 6F.q !jFכ.jΗ9`:G7l@??00{-!Dd8q8az}aa<*J˝N%lrXvr6HdN᲋pue9ivwzx 3׫gEՓYa,*yŝnfT0-ce5h`ExˁBCrnSx]9vYӳ]"+yA4ϒ]cT(;;lPBW}|[Ӆ c{eeMW9*,dPéLQԙ4 yT=]ϗ hb߀F]yLO7ߤe$([47wwY)t*]|aeuhsceݠqcYi\]` P( UJX`~p_C0?YM݊+2Rvje[TW*8XV:? |K@P2bZ- }MwʒC6"^&Or!S c??gBa|_d 7Oz,pJeI c$""KMpKZ9/67@[ƃ@n\;( IKKMCѿkXf `^'IHe|啚@[HDDV/,[+_Jo1ĉ1p@dT*[lp$$$ <}E~~>N ,^ t,yMњvbxYZZ:J[~ba,)9Hnm-@///i>;v,Ox{{cɈF.]}Add$y,]3g&NXisY59GM02v<(1{Ö`u;ppp@LL JKKѷo_]V?;wĄ  a…Vk]5 ,X>  ѣ1g(nVՄ?>>C[nXn5kfK%<kgZl=/FܰfY4n?CkӦ7jT͖{7|֭ƍѺuk$%%a̘1)SK.ʕ+qFDDD`ܹ۷/RRRrOB޽ 4z NNc2 ޡC0d 8ФI|pXb̙!C6mڄ ر#FZZv\ DDXO_-O{iJ8r$쳦;+]v>77oǏX|9 55F#wsHHH4^pkZ JeT*8}ѭ{Sw#㺪>uPX ??5T*JM|J]ɓ2CMcmF6G˖-FE!66aFjTŋW_p={DŽi8{b{o&\WuU}\W5ckUw}pC-N-aBYY\^W&]u,/+V[6s*m$]Kl޼9Zn?SNEhh(jٳgcz._Hh$qSPTPhڬ `emT*ѻwo8;;[96flu}%'} :X-~ꈼe z53f_wFEE… Xx1#ogff"$$DLo߾eדprr2ˇXG''8-svv/S[uU}\WuU3rruÆΰvt<֏וS݋?/Sm}׭jb8;ڛEDD 88{OGbb~nkS ""7{}ͰGS(`/5~xXhٲ˭{ E!<<[Ʊcǰ|r<BS_GfbСVn}, + ]w1!O6m$ Rp7G6Z s /,üy̜9EEE?~yjIY\>QN2.?``J 9jͰY\ mJ% @f&p',5kd_a-> ,Zd,@""4y.`6y2";V7 Z,[Wg/^4>\ejY "b &"" p|}RRUaW uLKjȃ>JKQ?l;' V/Xn.]ᇫLGӧΝ+|<"-Ҕ:@"" l`,ضʹ˴L7v-p0zF^ll+X$"" pA=jK+:رCc =\׮@Rx<r@Lccc 5k,\(Oֱ<)Sj<@Zv:k? ?|<($("M3LDDfv<icchG IDATza\q, $@""2#Gq X%oMCL- য়ՠiJ.`""2#Gv+*bܦM@TDh-2]W=LOXENcH=[R`E˗sv&;p@N]-{' ɓVF<hXV +Y  AI 0s&8:Ql `EWUN)yNA5foRHDDf0e- xp7iڴ=_9F`M@L  tnn? ~-7N`0_~(G`{Ykj?} *P޿߰=;pX`H`VS?˓ 2WتٛSgHDD&kr 11w)]7etu!pHy;/Oy pLHDD&4jdV`:v>4˲칀u||䅪 XV2 Uʒmۚ-:nL,+Q8ԩF*FS1*v X=,м<'(C @""2|yFVmp0m DDdYYn8~t˳LϯRk{ L֭[mވƏ?^RR'" Aff20p@ԫW 43V݆>>XmƗLo,GG "x%`84`0ի°d4k BlܸC cкukL6 =n L4 Æ Dpp0:+W`ԨQpvvoQ6$$ȁN==d߾͕̘89cX9u!""ʒRݒ۳8h ۋ-ºupaa裏6l؀VZҥ ~'FPPڷo^{ f‚ r6L,S;Wr,~iCMV1 PVP~ݨ+Z,K`֭(**Btt4RЫW/<-[Dxx8ХK$$$ ** Ae[߾}1a,XP-Z<|WïjѼ90{6pULSly*[@nmbL + v+R0%% 6vuur^49_ǎqQxꩧT*kTD#G-Owpp5ߔ><=_ [ ÆU~^ploY)u ee|booZ=Vբ;v3كӧh@tt4-Z,4Yw*dURSk*YǧK6y_nh2._#G" BRR~CHHѫW/9sHDD&.ڛ={6ߏĉ={6~cǎӱo>$''c̘1F.]}Add$y?~wƜ9s0q[V+TO5BRn`)֭zǙ3g租gҥXr%6n܈̝;}EJJ ,f# DDd" X[YYY5j\mwF޽Rk׮?;wĄ  o㪪{C/ YYfyL+MK4f QT^_8}jLk@[VZYYx24LM@T(y; 9|B &95  ki+> KyΪUh޼9<Ν;)nODDqqqdff6"^PP@II UZiq1@Ii(#gTU<ڪbhݻB,ت[Y@0ǏqΪnlK…pU/vUOLN6[MnKNj1cIII4 /ڿ^uVn [dfHu\\q=t͛9s& z &~?K^M# u߽;:Ukbӧ!^IQ[yNm9U^;wRjĭ呑ȸxرr{?8eqTO֭ﯲ:*M@DDe`MI Np5if'?23㊪آEGÜ9'Wf?~l칔Vu'XcǚŠ^^\}5̘=zTK95zED oM 6mв%$$@.UM3nOVGPDDC]X51֯Y̬֭{`8igJ/8o}BVVRc] , ~)pft^HJŋ `08']-$8,q-'hXDD@05?ATj]wAFllYs94on=/c4 DDD*Ͳ`FsYСUSЧ|-Lؽ, ]MMg[oAvHf_Z-"""8v yl掫Az|'Q;8vS"#ͭӧ!sQ<>\@RZ W!7߄ > Bݺ)zS)< 23M[rrSCPaޡTI۶5GYȬcvv:3LJ="">`=;ۯiSo7{wiJx+Wn3ynԮ}mR):JK ޴4±cp f;!,e~@ Vt;&T˂5kwh.36kٞ-- zүSeg^h0oJ wm&"UzJ=""^&͞ ;v@֐df^t?} 'DG-ͲBkBΰsgzJJ""^q0 }lvws@p RwKs/fRƍ[0C\X-$"R%\.XڄEAMK3<ֵB59^yD{$& }-=@,("r~v0!/- v6m/1Mliwur6 6|TS\OD L/_j*| С0j^ZkS} ycм9̙c]暊ʲ[JD#.|ً2ykՂs,wWI{M͎4yyfʊRt*o*sض Z>}`x dƇPUJe(S0sy3rx4\.zEDN#/l6{ >",qqz+ۺz8xD:TSt*!ٳ.Ǐ [pav}5k*fSi|cBߜ9g”)0b]x`y {@f&<J@DOKM!2 ]꽱P,/1ѫٯG 3+z N!`#%%͐fn2Ŀ(+< >|׫GTLQ_y }fkRHN6CQQvW'vRiv,{t0zwqPP_WЩ~("5eJM5> m۞zY5[&vW*Nh,Zdo-]f'Rt* H cMK]d? #Gvȹ(Щ5~G@rADaMΖ-!w]9FcHK{;e_Dav.;DHٳKɁVOΏ`rr2ݺu#<#ƣ>ʤI=TLJN唷="wJK|f)".4Μ qq搜XPP@~~ȣ 22,ݻ:ТE 233K/%G~~>_uUHSt_v.}=-[B~ɰ{7,[f&wԪew/#HNN>׸\.ƍGԩ999ҠArFEE999_eU7 `ܸqcܬi+-#"a3tо=̘a[l1][l!//1a„s~Mbb"7of-|>nذ_~.sݬi_j~ cƘ!1cF޽K'Kupׯ ;cǎeҥ|\p?~C;?77h9h*{\vNuxaFo6l>^v> 7@׮]III>cڵ6V|m'"Ul^?CFߊoNs߰ap$˲;v,/fŊnݺ]v%$$ [M||<|W۷}Nzz:'66z~,DnFzSO=>~5Ӿ^QQQD ())j/- Aem:7|HK  [oxE]g=Wڪ<DIIUVZ{7oo6{"""]63$"##_>> Lѷo_bcc:u*999<$&&|6.X? 6'7kNrr2'O>xFF7>O4  ==J_ח<\Ml+˂ᅬ`Ŋ^}!kw{5zJ8z~yme'`n3۶j:ƌٳFs=G`` C ~K/ bҥSn]2ey,pO"==ZU81a܏Gbccի͛7S~=}!$$j_s5=;͛>w]Lll8Gժme'gh,KժUӧ3}3ӲeK-[V->طooJKKYz5׿7k x͚V6??*G\@HH~AxHm9VǏ%ZRS}Sb7 Zjqz[9Nl}2TO@^ꫯ=z4:tG /t߬9dԛ5mI "r_|aBkе+Lf&r4jdwu"I@|Ope֭KFu O?ZRS"M}BKNj)zO@OfMi!h!ހ&CGDU 7pʕ{r߲,s=̓wB/: H 7QɁsMofѣjy ޥTEBQ,]jBCp;u*c#@ү,@ީOoYliifACI;W-@Rt*aX} ,\ox;3kV"] NuEjRX9b7lC/CϞ9@ү HW0g̱gk=AOK[` ]Rt*1rriiiىc09uSgHen(oRt*8ZQ.}Ko&M 4 Ej6 {Sm#mjx]oP.@N "q |))&FE1fN7)zSi'[ƍMIK w?ɛo)SHuP.@,LO_ZZ0{өԩ0b4ibwu"CлJT|xu> 4;tѦ'<`wBC0 IDAT'RKLJ="^rʕff <,{]\|qއDлTl=RSvlmژG /] N{EN!sO_J l]wm:w:* ޥT,v(,_ o0`&t]T5@Rt*] p |)>lz&O67R.@$3.^mvxAt4xbb`@ظyĬ%}7Y "h ޥ@RxȲ盏ݻ $;ΝOL(.VO)zCh&͛~ EZ눋Hͦ]T4xFԩЭ,[{K/5(Hg3:BV^7LLL ,YeOҬY3j׮M޽ٶm[s<Ȉ#_> 4`̘1>|:rg©4,,s_ߨQЬаٞ-7;`]H)hYPXXHΝ>}i:u*/3gdݺuԭ[~q19#F믿&==KzjN!`Rۻ꫰mY !Ar+ Ns8` p,bڴi;|rSSSiڴ)YYYуV׊ 77f͚r嗻ٷo)5-dŬ\֭[{k׮!Cغu+QryR#> oeBߊP  /7A{""5ٳ'YV`ʔ)L2DFF2o7oo6"""]63$"##_>> #\r }opu0k y;Ps|6Θ10d)))5 { [TTD~x饗3,:wߙ7gdge[}Z:ߢ]>U[VZL>{R kBD jcWF' " 5zmrfH7 曡vm+}.@R`*-5._Kж-*UXhv́t7Wਈ8KЩ,/R^ߜ9fÇͰpаșл lbBܹf{6mࡇ஻""|C.AA@$ 9t(+s!+ 48q_ytuJXݥ$@,, ձc;0{v˗#((!, EDDIЩti\fEf+n3&uYKq |-YS;i*EDDj:{ks}x7wG4yqqu4 Nr̅"X i~`n9 NG;X3N;(+- N=fм9wYKNDDw):x萙k@ݺpm0s& AAvW(""ʇar;qަo EDD(:U ,#GZ履Kᩧ`pBT50;f苎6 ;]sՠxMVZ[Po2Gџfrx`I |ob/oϞ0xYYDDDIС: 5̓xF-NDDD<DlMKI?ƍD]SEDD,l`UQYe\xm(-o4}BhmyRt"`qfR?eO]wiK6_D.V5|+2;rE}x%Qt@}={L{Uض Z{o.]t_/St"/?fCXهWGMq@'{l1o%[nK0lDDTɷDЉ`<3c,X,r0zٓWDDD2\HOٳ-ǏCpZEDDD @'*/4:5s\r Lbv爉f"""R):QYY9`&h1rf񊈈):&;cBdvL1C!!6*"""54 IJ}} W]/CBd͵HD.hɬz`o /cL.PDDDj2@e ]EuE6W:-,"""UCЁ.-a!w8!!J~"""Ru,(wa%v"""">HЉ`'3QtJ"""" @'* M NT(a8/CPDDDAЉΰHUPt"M/8}tZjEZcvI """PI~.\HRR'N?s׏}[&8c3C%uxg{=z4̜9:uꫯ[X${_863T|q0aX`` {&33_STTDQQq^^w捻 m۽``_~>v"$$^~VS[yNmU1j/y w #,,+o)--%**(~Mrr2'O>W_?͇TN:{לk{^/??͛랖S$͚5vݛm۶TVF:Z߿M+99nݺNӦM4h[n-wαcHLLQFԫW!Cx?zyʵ?3c .2ׯOw?k;f…$%%1qD>s:wL~طoݥ9%\޽{k֬$(,,sL>O:^x3gn:֭K~8vX5Wjs@]kϯ cժU$&&vZ)..o߾?~<.-bժUٳX=>cǎQ^=/^Lll,6m5% R# 0e]F\\-[_g̘16V&fذa/R.2ڴiʕ+ի+11͛7[>^z)͚5W^ر6mTwj߾=6m"//7xVZewY>MCҸqcNݔKttMU 4]vl߾RZuV9]t7kmر,]? .}<::ǏsСru:8BCCڵ+tܙ^ה):Hhh(]v%##}rA||9Çٱc͚5kݺ5害|֭[8p_^ke1vX/^̊+hݺuvJHHHkk֭dggݵu:M6k."]S^!`IJJ"!!++iӦQXXѣ.Qz!nfZlɞ={8q"AAAyv.׋sN6mDdd$-Z`ܸq}PV-/믿pׯO׮]ٸqo7cǎm ]HE<|wtԉ)SФIvuʹG^`0x`nV4he2dݻwgС;-[`bbbXx1端m۶O>a厍1.]0c شi!!![hATT| mڴ(RNznqq13fpn9s搛Kz?fСdggBvv6111E hҥ ۷kB_e˖Sk׎v1~xNRRRرcر.]TY"",`qZjźuصkﯲ۵kLj#9r$o;wd$''{ǚ5k܏=رcYr%?~)6lcǎs֮]KXXURHE(HCDll,M4!;;^;%%#G?3h 6l@-5#F믿f֭qFIva*IDAT;0`'Ov1buԩED<`Yew""5?L~~>/9ݿ?۷gƍnݺ)O=""U+-[h8z׮]K "b@?("""gEDDDQ3 """"~FPDDD(ІIENDB`64ba4ed9ef211ec4f854d91b9b32c52aea3c3c6d.paxheader00006660000000000000000000000265151624126450021017xustar00rootroot00000000000000181 path=qlustered-deepdiff-41c7265/docs/_static/benchmark_array_no_numpy__3.8__ignore_order=True__cache_size=10000__cache_tuning_sample_size=0__cutoff_intersection_for_pairs=1.png 64ba4ed9ef211ec4f854d91b9b32c52aea3c3c6d.data000066400000000000000000001006641516241264500176610ustar00rootroot00000000000000PNG  IHDR5sBIT|d pHYsaa?i8tEXtSoftwarematplotlib version3.2.1, http://matplotlib.org/: IDATxy|LWgLEِ(RRXTRUoURPKJ[ޖZR"hlEUY-ddf2̽s;w2Oι\B @DDDDHDDD`  a$"""r0 DDDD09@""""HDDD`  a$"""r0 DDDD09@""""HDDD`  a$"""r0 DDDD09@""""HDDD`  a$"""r0 DDDD09@""""HDDD`  a$"""r0 DDDD09@""""HDDD`  a$"""r0 DDDD09@""""HDDD`  a$"""r0 DDDD09{:t8v,MDDT ]vpqq(䘵"ǎC] """ob]  *ݶNΝ;ѽ{wuw̺]18vUTt=Hbnߐԯ_JjzAVW鶕uw̺]18v]wG>}qkNDDD  a$"""r0 DDDD09@""""HDDDTd :@VoZ.iϟFFF ooobĈ͵uU0ƍܹ3j5n݊|GSiya…Xl:OOO ??ߴΐ!Cpibػw/Fi*RpDDDDeyѠA^XDDw!,X)Sk"((6mql۶ ;ڷoXhz?6[! ;;t+(((q۷ǠAvaŊ/^DZZzaz;vD\\ ..=z􀓓:dRdd$|||L9s攸KI&ؾ};Fqa͚54@PP󂂂Lh~~~ul]DDDP^=}773 h߾={=@vp),[ ÇIY[!yyyt+- 22͛#11 HOOX'==ݴ,88W\X鐑aZǖйsg={s!<<;wgggСCDGG#33G1k. t.`"""2kx{ᩧÇ|r,_R0~xhҤ """0uT' [ {쉗^z ˖-Vرc1x`t~ɓ1sLDDD`2di7xyyy9r$233ѥKl۶ uk;ݻw ڣJ DDDDy㏗\Ra̙9sfaݺu(^@""""HDDD`eܻw/PT*lڴɴLbҤIhժ<==aÆ!%%bJ 3-U6m`ɒ%ŖݼyGԩSqQ|8{,g.LDDDdKrH^ЫW 66ŋCHLLDXX.LDDDdKղR @yd&"""jX4iyx{{ 2t?''VrWۭXwǬ;gc^ut6}=%P⩧K.͙3Nw܉~Inv$rˑo_vͦD56?]vZ; ɓ1adDFF{W^?66>(jun[XwǬ;gc^uONNk)U wy޽ˋ^9** @.777lZZv?FcpYwo뺻S)r⯿2ݿx"????'ѣGyfzy}~~~puuUlZxtt-;|p̘1?#m۶۽{7v @Yd&"""jv !DZf 2ْCHDDDDf DDDD09@""""HDDD`  a$"""r0 DDDD09@""""HDDD`  a$"""r0 DDDD09@""""HDDD`  a$"""r0..M W_C. HI֮t:y_y ߋ#G,>/#a$";ڻ4DwW6m%ٳ+??y_AyG&{;$^S^G.`"r nɟYY-ڽطޥظ7HKT uH⟽;p2=m| D ϜnTgp ӷ-˳Y7O@"r TSTaШоm nJ դz 9@) Mٱc@׮e ~Ş dǏ[|F#CTTd~Sr[8HDA)-ׯnI䠏`y-@Fu 1(%|p}Q}!MpZ~^RԸC' *HDA)8͵k-GM6p 0cKaz EnZKS$.`Nip3n1cPJ4~]nrQ`6{\ KaEL.eHN?j, ᭘svg̘Jeqk֬iy~~>ƌԮ]Dzz6ѧOԪU 8q"tv<[?xM{ܼ)k^groww@,3PV-Z 55tWd2^{ ?[ٳ)))Pd^G>}hpY_|MfQ(%Yn b9J@ ֫W0@A ....xVVVZuG^͛7ѩS'ر _mb֬Y4if̘WWW[W-D _B.-ٲ+:P^%7r`RR)ݿyHy-V-''٦[A?hԨ D#GjѣGӺ͚5CXXqqqhժL ;;ORHD8 [+&+K㑙iT^/CFc*5+KPLpdd$|||L9s攸^ǎ_`۶mXt).^|999HKK+|}}-4@ZZE3.7.vcPJpui4W:u[7Z h՘Z+tXBcׯP+LVzz2޺uktذa<<0p@ &&~ؼy3Fhxzzb9sHVhЭвKB$Зfoݒ!JR4V@c 0 :] wPJnBC '9*ot\ < |=a ==H\|.c_` ?˄ s=WUׯ/s;,Y%KNxx8~.ݻ}Ehh(T*6md\iӦ!$$ѣΟ?oNFF ooobĈUHSs&K5 `JRs瀭[ /gy|fR-K~~կ7{jv N KoNF9^x !A^9p`Kϰ&d2\aC 7o@Q\pMT-`^^ڴiSjҞ7o.\e˖СCDLL L0d>}ؼy3݋#Gڪ 5[b 5 x%˗e +CM[U;8܅dK@jƍwUynq p5 u@ޥOrכP=4,5iR}[G^ظ/^7]kfgZw-;ΙwZƥ@!d WM ׹qh0G9/y$|he2 .[& 0e[ڵkbj)c `at16&[U֟СjL/\ cǎYCq !XjXj+ڗ/_˗%h4bӦMB$B Bl.DBɕ\@syS,ĸqB\,_ i܎ѳ,]Zo{r__!ΝgO!4HDV+*_hb\@# !6o5!22عS`ݺ C@WB%DD\[BtB oru"B\"SO qwn* !rn݄cYYB=*DP\{%A Bv !((:/7/۶mV MDŽ ĭ[] 1vFs}٠O ѯ^ 1t|۵K"=] :U!T*Y5kϚehFu9rDG8~\-~3!7ejUKOaaxٻWJ_xAly]( /\$DdbBy B<)`/Ԧ/$䓲|#DNu7h^oo!\\>ϗm< m~GhZSXB]"/cKݻxٻWI̟`odžᇅHKGǼM!CW_Ŵi;x1j\w4Y^{ckC~U 1}+W=z{cǘN'?.|oR?OV<&׬ Qsu:sg&MC3z Bt(O i^W!~[2D>d| Cř둕%]fs.ğs]^9hddȺo{{۷hBL({QIǏςQAE?gFM Ѱ|#+elfy̝+μ! :w&dJJBO@}Dҥ3.3FD%-+wuQbޠASO=%b٢iӦŶUn]駟Z"++tKHHŋF[^^Ч(3G{''!9RGjaڟ~7j$ NN R }Lw$ u 鏁~};W>[7˅AѣMhcc!4Դ- Ch0Ԯ- -Zyu C˖BB+={ 089 СB?h|ѣ{d<.M5=kWeKy|m"tjz=dl_>P i?04l(QQ' F# ^^B7o0dWx@d{m[?^jזAh~]%"BMa@aM1ԯ/_U+ahDnS':j CBc$'>Pj%Kg6MuC˖B߽M,?(4Bwнн$% 0Z-?l>>חYm~B"kB&L~ ]Zj2\h0xy CP-X B]k^kl>ƍKh1cprRVτ!2RKWW%4V|RnqdWO;9 }Xн0{y{y:h! ~~Bcȸ S\]nXݴIDžK荍Ǒn yyxCdGE / Mb0 ݤIE{2~~8;v,tհ!|D3 ӦAݓ'I#x%8M{D֪˽{#lN5 ~ hgMIzx`G!Z 9Vݺ<~ COG=hh;wFVDBk:vdf"9) nnjaՊhu+h7P_հ!|NBfFأ#⥗74gEXpܟqqxMh=<ܳ'"wNIA8Լ9:#sd4k}q~ K9x᚛ep#< =Nヒ[@ 3ϠÜ93gf͂5H ǎ=) >tԷnayp.(@ŋWǎE߆cAV8;x0GC77_Aí[BU-Y9sPlשlZ?;w#aPڅII8h(ƎCz9۷kzx{C!t|$wS#F@"z,8_AVV35ޗ/ϣGD®naa5 Bq_z: [Գ'h =FaxQ\]q|hܷp!NnQvMF?g8۹[@VDZ/_ڝ: ENP{@鉜Q9<BB;F81>n֭W#Z89Jpx,Z P;) hw/Z/^XԪS B=#{6oF3ԥ 24A+WoAA~>U*ܚ7nW㟘:.~|p3499b ug  jo|tp௿s¥BB/*NKO.+m nu `^ݺBױ͚ez\r~K[ Ov2MYM_gg Cֲr'%Zt*tg }>KO_ ׮랑!Ǐ_%S눡uknjlPƍR]B7Ty3FhJΆ a(IJ*whO>͛BVOM6\]N>DhClh/X6mnѢa|nׯ˲_/t ?ZLyoL YFO$49"4gF#t&GO׮ CvB|\73tǽЌ%4!t*?d>۴+dtDФmnj_?٪*tSݬYB+nTapwn4ݷ}9SRB99Bsܮl+| Ve)/Ot} ݤIr6jTr>Bc''`ܸn.@Z@U24yҸɩC(G{xހSl}n` 0bkSG~ ,ѕQ#9w||*իEWl=??39YkFj@.oyGBU?]+2㬲eU][kE5n,'_NW)U8~}۷R(#ڵI>0ܺ' (`!yŜjZxtt„ Ç/o<9ҥ mwwwsk;ݻw7]e…6KArJ]RwwQڕ[oрQQcw|UTLb{.ڃ1p$%1)ڪhe(+{g[N%t} szzin9Tj5t+W⼋ ŒT0:5=Tjv !DU*fΜY5n:kTzBB|D> df@;]t2t@kegSfjXת6|x铤-Q,^/tw-İa((:ȣ0:*ej:@"1Okh]wǀN0xxySZ߻a|?]h[C \ɴ]ݢ ب@__S, QkU@%c${ER[d ~4'':_WLǓ'pVV[j.=. ?1=+2`JW } q9/[ u7hV;C:*嵀 `$[E3xbfvQ*ܸU*sRP42 e+OK K`UvХαP*2 h `YX[uiΌݎ5uzyV{Ƿ1^k?^=͒W6jT %c0SZJ&l,/?rr9Pf P8 (% '\[GMo4 c6lvEO/y(`3ʘ퍪1=@PWkj-O?]<`v.`WhmV0Us g@@ 'J`ld e19)߿%] rbi:[0vv ||.`+cT2& {d uqR Y3e @c0QQJ `&L[[mtP{ڴ){F@+cT2@^o2d5}8wKvKVᅲ9'PC|WEr0* Q06Q;ռ5EF<`X[SSK[sww8~L5h= 2l$""b `Tƣ-R'""j+Wh= J.`""Rt5e.`XR1BYsh-T DDPb}Rq""R9t@uA ]?͛@ݺ@v@t4^1*GB!!k` _|+ʫfd.7d0i^2*H9 @vWWe4\^P~ Tm3* )Ν Ĕ Uf.]yR1BY;rw; *PHh_Bwn<ԩSmP8 ~6- @f&0|8ЪK@v6@@߾@T~LJ@""RTQ̝;* Ǐ7=1cknDjB`` &NNW1x0p$г'+_v+!""24 k(g}֭[[ƁaÆAV+5n֭~Xnص Q.}_ׅ-J@""RgggQ#뽆[sss1dXutUV?#<(^;v@BB+mz¬Ydh42_7=hT^t0aaիR1;DDV@rrrm1cЧO#G@Z<ެY3!..VZ!((ȴNLL q2h0ml 4& XHΝ׺ai`"##-O>3f(qQŖ!--ʹNg\n\V+ڵ:1* )ٳ[5lz[xW ;] V0:TF|Nj\ȑ#r >cz{ŋ}vh4dffZ#88Ç[l8JظNi*su`P*&""o94{8y$?noC 1VsNsΞ=DDGGqI\rŴNll,uC []DD0ɟ5"вeK<==oz|Ĉ0aW^yԩCdd${9̛7iii2e ƌSbsQkVÆU^56z̘1_}SLB`Xb233ѹsg,]M4sHDDp 0h[ViIeV䄁111OM˝yf=1srrKCb4ytRY-Z@||<^x`ܸqya…Xf """0uT !!.'zZ`$"";3d9sxX6jRWXd ,YRs?W7s "pwӧaÆoL'a !`L2]AAAشilPd$"";ٴI^矁^l:8}Z^ρ7F Rx,_ΝCӦMq ۷1ŋHKK;vD\\\bȜwt:Zm_B @g0@Tθ/zV\wϺ;fe?Pz+z=V} 5xcGy[[`jy'KTcol4k ={6 22h}LJe<HDDvp&/ؚlOW|˗+>R (p={:zҥRp.+K^g]Uc=@""#cG ={-&O<=H`ww9EOZ |\VQ JHDDvdlM Oޮ]uKvN 0* ّyߪsȎ22d_-tTLDDvtά=!h} JUo`$"";@c$"";Ȱ`؂m!,7ou+6-F^\ݻnݪn{ |>>h9lR$Sq0Qz:д}^@_x`pA۞~hرɑV$%V)S;k"U DDdG@h}^-%'c7v`@Y?\(c寨FdݶM@N{"z""1!@p&;v#j.9 LQu6)JHDDv`IXڵ@Vruk/l[^20AQ6e5=lޗ3b ?NY>oNqZG&>PݵkHDDv@eZX6X~@k^R*W1b T9xj[={AXE}#@K|+,w }#]5JH?,'Z`$"";P0o;r {ygC+GVˁ,L"#9O`V/NHDDvzg-JU-ϗdl:|X^\l3MC,-9Zm"T;LDDvJ~۞moA^x%40DDdr-]Kl''~޺;bt{7kW۷i'NVIQ  ˖?M˫Ku7Hǘ1cڵkcHOOFbb"Zj!00'NNp^yEN*K[e`.}Y@ÆE /ؤ8 @|<#.EqJj_>Ν&M@5k֠8vZh^{ [l~ ; z}App08T 6 j^p:krpUPFeR(  YYN@.IqJj۷/z&MiӦ={6j׮"++ VGyQQQXz58v؁|Wh۶-zYfaɒ%h4*ÀC(cpV r;@""'MTKR-`NN Tuss[oh9rZ=z0ӬY3!..:uB\\Zj"w111=z4N>voӦ}@VŻǍ` ?j;ڻ$HDDV$6lhb]O3fɓ'|Ԯ]?"##qqb ,ŸqqYE\ Ԯ #oETJ xVp,o٤(G]*G׮mgԫWtֿ{ǏGVV6n܈ÇcI̊.^&ֽ۠0p]HDDVv̯[zyyۻB뺺q,QQQ'駟FAffE+`zz:8|TF#=.wlӼzuɷO>^~j/99C?<<<ЪU+Ǜ !0m4=zRJc$""+z[ޥ(`0QQQPعsiٳgh@tt4N<+W։wnܼ Ԫ%lLL 0wnoŃߖsmfq:w Z["!!}ԩcZg޼yXp!-[C111ϷzHDDVi`&O{ҥK8y$&O_C F &`8r^xDGGSN{ xp l߾SL1ctb.므=䵜+6]7 =<䰕?_^J}4hL0a] `ʔ)߿?`ڵ ¦M0x`RJ^(Jj{\e׀15A,4dj!Z*:|=PYQnai,m}Wmw+PP֦fe)ia& t731{sqg>}]׮] >;wIx۷/=!!!$''S\\L6mZCCCYhwqԭ[T&O|1IuL .nzNbz:,6e >>}x #׏pJJJXl}%iSX0j{pk 5޽5W]o{}Ҥ~W]u_wɓOq#q7g?/D3+- 3\Ug1f 3棷K#<1UOз/|-w_Wם\xU0aIUa{ <(dFPRPDDΝf݌ شa $hN16ݾ|b*?o=•Oð`y׍ mژ~ S=P %""RoR!&Y;hiS0iGV-*ܯ@$QQf;㏤.o(Rm]/""`Yf ez:̝ {@n&Q6 4;3.`cs{1{z33͒%[/1*r|U^} ĘRS@A G_ skWlwHP++Z ;wjXq1l~j&<{*W\RpA*@aYfѬ xM3G:p& 3g/h N4U}K SiHؽ{fI?F2#=u3:*Щ!==~ԪfG^]_4 onM-"`,\7j#%LߒK5Lh_[̩,KDDDn.<Gx}n{X3^!P طTZ NDZ+*2k ̘aF֮ ]/W^y;~Ia:СU;@5""Վef̀7ހ4j;C@ݱ:@j#'f4~8^>_N@‹(t`׋8[fξ+^=zݣ.>[@rTqtfdf*^LoP[9ޛoK rsaǾjRcIЪ'𷿙홙pJMxz9/ˠQ#sU?*NRp""bʺx #{w}G5u{6 /&~xd`UT +WJKtIBCa,˃_6ϳi\rڵ[aܪO@һ^DoGΘa߲Vh肓*MysիcG~=A S XDħt~xF@O{\b*cǚA!UmrJJ]""^W>Qs(F:*#?43O S]/"5?hwg0&/\D?|xJx [L)t*,""g>jzYn]S@40?aV۬gUЩ,"r~W>AͰl)y&KN6I.`Ooe%y}'aɒO TK 2siS-{) K沅ylތl?x_:*"v^lp%MZex۵;:T.<$~`*> O?m. %N 3_Yz:,^l>䯽b<`j;DqMcĉf^pw衇 4Ԭ \UJJz _m>^{ ~.{n /cɒ]'E@GIIf믇#g5CC~\9ۭ.`~ χٳ!;7[n1^:ٯľŹ4Ę?L?6ߝRϮP S XD|:믛uK(އ6EN?;ooF{)@R("į‚&#S׬1h4h`wRhy~qQ;kZ+t*+*6ݻ˖JM^fҡC5u=U ˂#L!_n],qCxK@q/7Fzxy* 78iȔ)Sڵ+4i҄yf}:DZZ5^z$''Onn.W_}5uԡI&w}SSIod9rVU:"7¬YfO?ux|*'MrJڵ+<׏7RRرcYx1'22ѣG3tP>Sʸ꫉>cΝ >0|vzoڤЩ,"~v÷ߚ.={OZ|cVM{N.]~FFM4!;;={o>^y̙C޽HOO]v^nݺqF>Cԩ=<'N$#TnŠY|EII3ЩoN6 I@ )((@Æ Φ}ڶmKll,YYYdeeqUO)((oVN[P$O=.1cTl;z[9-")'{ ڶ5 _z`:ݦwР#?Yq2e)v3f ݻw}NcGEEmyyy_[w]t8zQNWo; \DtΜi[wo8?*7nYfk746lU|t~RRR9Sgwtܙt>3V^mcGQPD6soŌ5 j6|1S' `DDׯ*=z4-⣏>ΫÇٻwDGGWslOc})4j>y8zi-[`hLʼnYStvx}HI9~1'q ˲=z4 .d3s΄Ym撘@bb"_5vgٲeԯ_x4(o޼y|]N2PZZJtVmVۃO0m/,r1cF~B7&5ˬNynv`Y!|3qƜ9sxw"##]6vm7 R~}.֭#>>[o~<~aN2ܾ};w}7˖-VZ^;)S4iq333iܸ^ҟ6ARۃW0mwaÆ,_KVVSc_7.Ԭ_|ynemV_Ȓ%޽JO>+c{zz:#F#$$d߿?ӦM744EqwHݺuIMMegՖ3岬fȐ!Vl+++rORR{6oޜ1c0vJ{lpǎǓq7RRسiu׬!,,̫v-[F߾} j۷nYBxrs]je&%Q&U |!?\O>/xcؾ}ǵ|$ +}믿6rHڶm<_W W_5kzi Qi.\rj{p'k{a!&ddCDyHLtr>:^pgRC3\ݺuiԨQSNkMpyl<Ա;B40, qziHP1S̘۶wǏ7Kc`4 +<תUS2uT{:} :2sY`J{ 0r$\~:$8i@4 `EmVxPxcš f!O˥Z/)t*u,25ϝkxZt1tLԊVs T-%NA "c^3clش (,zLoF$${Ѽy+q -%N HV\ 5w/RҠV-hz<@@RPڱ,Άt3T.z״T@һ^S{Uذ$z HA Щ,h%%f}tX|X ;gV13bUoOS/H7o,χNg曡Q#  wCRT{8޽0oI>$z))fNN$0@@,?"wn7,_n a0Lr5Pd IDAT:"3/ߌ mĉf ޘ GW:"x-S[¬{㍦[7*A@RT,SVJo9z3aP+b7U}K S/[$o,ɁaX1NDʩ[JJ]"gaXLm]kx\ףǑqST9c_}e{5ؽTZ-Mo S(Ed[JJ@*ٳ5˱egC&[wH;:*u@RP 3T.R3G#fw"rT-%N mdFa*|O<QQvG'"ޠ o)t*}#_rl3f M7Aj*tK"F@RTPRgw5W]e&mj-&T-%Nk%HY|%̙cn:3Ϙ_&vG("i`|K S(AfN93izFTY75:t;:7u@RPfe>t)pU5P"AK]Jx PeeG?At4|3鐛[}c@KɟHSз'֩nuK@(*.|ذLҢ}BoI[JJ]RYYWa~3eK:6 uw91U}K S+TCf Ycf=qqvG'"Չ*Щ4 XÇa"Y~-9N^ M[JeYX[$}gU:. AN]ЩTڽ$|&#a"@r饐 vG:!JJt p0y2 `|ED^=o(t*[V|c^3ӷthcf8\Щ,6س5]kBFby;u;:%N.`|7+X,VZ [o5@xQ)t*M->g̛g|~dE-%DD$p)t*UŋJKa2Α*ߜ9pf)(QTl3fYf.'0'":f3tloÆffJ}%N.`R#]ee7@5PDDD S XNƍ&5 }{xIu)f*MI1]\±@, ; qBX (oușQTjeer%|NЮcZbbbp\[#cF&'`)SеkW"""hҤ f:t45jDzHNN&??ߦ.vÇ{Ш̝[ʫϻILT'"gȐ!=fY?<?0 C̜9Ri&.]/LBB=z_d޼yn ʕ+IKKc,[GQQQ>cǎwe\CQ,n E aDؾ/d0uCaa!*#''<*EFF@VVYYY4hЀ.]T쓔DHHk֬9tR4i҄lzɾ}xW3g{ ==vڱzjufGGXF={`|x5o#M|""裏2q*#//cTx,//&Mx<^F 6lXlx}аaC)))۶mKll,YYY& (--ī>ǭ\۾?,Z7BЧEz!C,1z>/~6j{pv?7nYfkќZAnƌCi߾=`p4hL¤I۞IƍsCbٲe^=nuR^VbFlЈ_4nܲ=vаbũW ~=xsݻwAXѿ/OӣhϧSNڵyoHذaV:?qU߱c7 r۷/aaa^=ӕl2Ƿ!Co.~EYLhqu%hvjեWۃ;vڱ∎&33"+((`͚5q$&&w^ܹ3˗/vX"ѣGW >*GGGsaQ?a6^fMpAA`-t9fƂf"hg2:wv᪸/_ljm`nmn^Fҟe˖999_ ˘1cxǹ c„ 0x`ڵkǀy饗())axxm+`@˲뮻Xp!+V ..Ν;Fff&l޼\ٓt":,[C.C&k{z=֭[G^*OQQFb޽уKRV̞=ѣGӧOBBBHNN^{[lƜ9sxw/22ڵkmƸqhذ!箻"11fn7|Y{7͠=Mkw""/W^y%Igs\L<ɓ'p 2g_wF6>}:`N1b=\E^\\L6m#=Mm͘۶n)"T[,S/WV-Nz¥]l [oAz[  ""&`jVЇaS{ ۻ7̜ CBݺvG("";JJKyݾ}&Ȁիu~qqwpfU S0_RTJش,ɖe"f2Ki>STEw/,]ڂ'eZ 5xx׼ToJ***,\XÇ;пoBZvG(""8:UsMߌk*}&iCn7aaYDD۔:UW̔-bő͒%SDD$`)t*;@2*W/9`RTsIafc-qqvG'"":˪c `R-] aa S_.@'F}LfHH0I߰aРH9%N{]eefꖌ X64?,\s haw"""R%NTp͜ ۷[&N4@'rͿJ Mnz:Zpf0GBBSPD]6v22:BR̙C6("""gD &ɛ5 mV:oKH"""EJȆp7fφ/4]]gxwWH QD~޳,6{ 7w~ jq"""b %NT^ɥfr x]swox$[d_RDDDF > ؛]gu}p0e t4mSD^0/^{${n1btDDD@':-2ݼpp SDDD'%NtKYnp72xE3a9*Xn:uW򋹮/#OT]"""R})t IFcAI ; 5tVEDD$*8)*Ø1GÄ Шbj}c|kE +ST'"""UЉ\PæMp!|3.`'=,\dd0$<kTkJȲӌ;67 O=ow`""":З_҃Oi+KSIDDDG]Ҿ@]ለH 'rd@3É'9  LJi`DDDDI )RD]JEDD:*"""CJkx@'(`ONJ-U |v.`rdp:|7n>(_|;vڵ8cs3 >ȑ#祗^N:.`Gqlpv-Ç+DVVV)..}ؾ}; nvj ضmaaa;v5PRRݻ k;wl;wjΝ;9^~5k֤f͚&9m{nʈŷ~[sL¤I~$FyDDDڷoqGeĉw&9mx&Əϸq*i&?|BBۛ^XXH||<7n$""«v:=8~=8~v%>>5BUU&7&44|DGGWJݻwI|4ḳ< l;wl;wl{lli{&9 p:wLfff6Mff&6F&"""N9CVƍGjj*]t.矧#G8H +1Hoߞ O̞=6mʕW^q}BPۃWۃ.mwzPU.˲4ݜH kEDDD@ PDDD$( 2JhԩhтZjnwH~1qD\.ǭm۶v1^{-111\.~m-GiӦԮ]${S}Ĉǽ `S5evJDDM4al޼cCFFWM>[]N_"ӧӡCׯOILLޫx<:믿θqxG/ر#g׮]v]t;wZ|;2uJyx饗Xf u֥:tϑzߩ0`ܹs\4V^Ͳe())_~U3vX}]ϟʕ+:tQ{o8O?M{ySO=Evv6֭w 4o~C`sdzQ.2+--~YYcM2ƨG:vhw~X .vhgضw^f͚ܹsgmeYVjj5h "]vYrJ˲y ϟ_ϦM,ʲ+L9eYW\quws9/tݲݲ;*raIJJBRRYYY6F?=111\pkwH~C^^ 22yX&MЦM~WC}аaC)))8m۶%666 /7{l7nL?~<#<)++c޼yTض sDΞv;޽2"44v3fwNsiРǾx+k?7L͉᫯`,Xh믿&11CQ^=.\H||<ׯ~C`s'S(1pwЁ7oomfcdO7xc/b:t@˖-Yb}12JKKcÆ {멜F_LӦMӧ[ne˖ӫڴiٷoo&\Dmsdvƍzȯ|m> 4ulٲP\}`\p4n8GfѢE|Gwyۣ9|0{?_8jՊΝ;3e:v?8'j{e;@ sdffVlsdffz\+,֭[iڴݡU\\֬Y~_5 e1zh.\ˉxs΄y͛7TvS\\2mL sG{x7oUfM+##ڸq5j(AV^^ݡ=cXɱ>S+))jܸk.CB/K }Y/~G˲,멧4h`;W_}e 4ȊCK/.BСCv~+22ZbsΊہ*_bZ˗/֭[g%&&Z6F=j-[ɓ'[֭rrrwyǺ ={f\ɱ+or>>'k{ sS@/k[]vzjCaÆYM6íf͚YÆ lbwX>GYqT˲T0&L5kZ}6olo^r8pׯuZaaaVͭo=`UnJOO֝wis9V:u!CX;w/h/:Usss={Z 6j֬ijʺ}7on[{էOϲ|ΝeYzM%""""AF HQ("""d%""""AF HQ("""dH@Xb.{k׎StR:uCd""S("ΕW^ɘ1c<]~ܹH[by = ,,ٳg!2)Ntt4.ﯽj*nJrri?gĈ >JDĔH2bV\?\..m۶A XhmڴN:\wu8p3fТE 9׿ztsҬY3֭KBB+V8iLͣo߾ԪUb_zEDDקsά[kuֱuVDDNC |woߞɓ'pm۶=p/ͣC2d4h%KHNN{ 6 ѣGqF͛GLL .d|\xᅕ'p7{lKIIK.a鄆~z*%**O>-[z#"rzHIxx8u!::0}c֬YS^=ի}Æ #77trss{eҥOV:?crssh۶-@cLL ?c""gK :uxTעhѢضk.khݺqiԨ _ݿƍOf"))믿J_ڵ9pODL)ut+t[t,'44F4qٳcĉYx1>(cȐ!o{g6PDӚo.صk7uִnݚcrM7^:t[r%x-~ӥQ"Rhт5kְm6v [nMJJ Çg3e/^|ߟUVU?x GfŊ|駬]vUzjj֬IbbWb %"R{dϹKnn׎ù{hӦnIDAT fڵƞ9)))|7l޼P~WN֭8p &MxܹsIIIN:^]Dt,˲BD((()ݽ{7mڴaݺu!:OxC=DO;z۶mL6MɟF@ HQ("""d%""""AF HQ("""d%""""AF HEyQIENDB`1b2e293901e4a51f1b29044fc91033da4af86cc0.paxheader00006660000000000000000000000320151624126450020406xustar00rootroot00000000000000208 path=qlustered-deepdiff-41c7265/docs/_static/benchmark_big_jsons__3.8__ignore_order=True__cache_size=0__cache_tuning_sample_size=0__max_diffs=300000__max_passes=40000__cutoff_intersection_for_pairs=1.png 1b2e293901e4a51f1b29044fc91033da4af86cc0.data000066400000000000000000000663641516241264500172700ustar00rootroot00000000000000PNG  IHDR5sBIT|d pHYsaa?i8tEXtSoftwarematplotlib version3.2.1, http://matplotlib.org/: IDATxwxUoK   ̊2d }QeQ"* "AূPT łE@!eXhٝY$ P s]$8I}sFDDDDD5|\]""""r.DDDD^ aHDDDeyDDDD^ aHDDDeyDDDD^ aHDDDeyDDDD^ aHDDDeyDDDD^ aHDDDeyDDDD^ aHDDDeyDDDD^ aHDDDeyDDDD^ aHDDDeyDDDD^ aHDDDeyDDDD^ aHDDDeyDDDD^ aHDDDeyDDDD^ aHDDDeyDDDD^ aHDDDeyDDDD^ aHDDDe\]Lc׮]ci""h4"337wByvk.4o ""uV4kp  ** zuzh߾:Xű.,X օV]>}͛77{#~W"SoLL Ufmt:DFFjժփ‚ua`]X.,U|9bHDDDeyDDDD^ aHDDDeyDDDD^ aHDDDeyDDDD^ ) н{wB`ҥt:F  $$ׯN:e .o߾ CDD\g ӕ0//sO~sعs'ƌ;wѣGcڵ_a>>.]Ž;ФIu`4ѢE@lQ0X օ‚uaQ.<2!erssq4޽+VDLL `Ν_`0UXW:wg}N_~{f`"""xe2ܾ};|A}Sl;`FY~zm0|h߾=|||S:\Lm۶3X",X`b ^1 Y0$"""2 @""""/0$"""2 @""""/0$"""2 @""""/0$"""2 @""""/0$"""2 @""""/0$"""2 @""""/0$"""2 @""""/0$"""2 @""""/0$?.$ 598ե|~.]L3Q)-%eq??g][6O ygSlWXx9 6h 7h\9 7ե|g07мkBDDVTзd j j~xeKW-[jQi.@`He_a:[Tf0y2Ыf[6"r+ǎY;>}V:ڀ'~ | a30}_Vj<@ֵ)kNPo~#G=zاh۶X#¥KqC,Wν3&MF͏`Ȑ!Tʗ/dffʾs m{.`vmyuu'O/]Oe.^.z xkY3)wZ`0v6R|L֭[ohm6|hذ#F+xb$%%ԩSx\TaHe)x=_] VeO"@j{w`n|HKSWo`2ӈG*WVM!@JJ͙$$aaSo@M}⫯B ̏geea٘o0%gkw3srrm!CЭ[7t;v@Y=^n]ԨQɷҲ` ;""z_ofTTׯRSj*-ǏUsXyNP@k>~\UK eO˖  8zv6@r=9mMo"#C5>PЩsg58ѣZb+*5XҹǖtJ~ch,l_>ͷ'\nѢEعs322ǣq?:@*ΟW?Xk }|?6K>)w|/KJ5Ç5Df\3cA}=/|:Kڥ¹Y";E5jI\#bw=1c\] ڳG}oh,k 6"3S[_oӧ]%l() رCmӁUՎ/]}m~h_F(g RSSe=eN4R٣]kVv3U;AuWf/f&LΝswwYY*{ǖK<)ھ:0_<|\^J/-1y}7tAlzmFV"ۚJ~t#gSشok#7/^}ա",,| j;v̙3{???$%%aԩCTTZ-.] .33+*{Al1uT̝;qqq3f :uTs޷o_>}k׮Nsa2jH5B3F\#)˗t+|Exme,P]ھxm!լܫ9HHku3 ;A񧫟/~+rO;Ȟ CkyyaD >9z@/ 2n>FExhO D`R`v5P),~Gnz|$z @]?Sӵ_%0m;pp'r!moj"oc .A"<\nyycj[(czRM F.c0S36:ԯ́Ox"#*_Aw׬Z=@:n/^~&ڴFT~^{M ~YwRٹ:~5RŤM:uu&?~͝ELM{hK7}o{zl[.FիHHHإK$00P.\(""@mf^fբh$==>y'OHiZYthZo4FEvٹSd qDwyVDZi֨P`4# S~]ԭ+r"uԮ-RH\H͚"&RHj"Q:͐hMT ɑȔJRHŊ"*T`B 7JxHX["}$7_BBDr%8P/"AA""> @_O|}G4kooĈԫ'R~[_>_-KX襮ߒP!Q&'IJR;=[>t_Sj J^T;~ZysOPۋV_žUOSEOoM62l0^xAjԨ!֭۷K||rLf%-- VqѢE $''wHNNFDDR:tGuEѝ`Pk= |J_׀O>~,2RekT-~>>_>>ߟ÷hڴz^Q79>{!YSB3t$4A* o4߇Q^]$~TM'4< ̓mBhFvf^yhFT`-h|4?"#yf0}з:cu54?Mժ 2 ) }:+^aCe4sO/ȁߧ1I|)z=>/&) (Tq#0b84Kժ  ٸUFor5c^.~9o௽@Ԩ~OjA3 ՑU@l^ M*WQYلLϓI{u 2 t:6n܈;s#X|z'U^Pe](ޫO so`6TU Z-`䜹 }Ϊ/VY@ճO%&VYPD ݏ?DhΟSWۚ!GfˤIҥi;=22m۪ Tubޮwoߍ&Ku&yyI6xz }!D3?3h^ _z + $-[ zYfAZt<<40q"Wtه]:Dl[Reu PٸwUή޾hHu6 bbTҼj}),o~U߫wߩ,/s>jnO?PXXN:aƌ.ֵ:UupӦM@N:e\^ &H:uVʕeƌ%@7S&1--MZ]oyyytR˳ 0h}}%JDV̞@dʦ9e}rx),b[/zu1 (ZV/F%'_;wGc\9?hn֭u=;'YD{y=cڢ1raC1o[@I1֨!ʕE[=vLtWgO1nfv*WرjUիW|P ;.1Q׷^vbO ͚~Z M~1FFP=~bHH0'c VտiZffm[1ԱXв~6bc "}|T]>,t-[&iD[}iREƩbhXеhs1n:%$1 ,Ơ 1V$Lŋb矫p@ ;pDY}QP PD?d~Et_~)>ŋjQСVWUu?/O11bYSO|^tkֈ6;[tk׊WӢ|YsEcbh^t/֭-ӯ?Ht?Dzf ?^ݦOk]bгu|ɉ\!F__1/V?HAAmN:=,_?_E߾bC׮۵S玪UE?~Smn&ѿZ2יխS'1 (ݢE=rDossE{EbMjm۪}<99_|-;rr#G1<\'N{Xl*$ѿ˗_1< Rk":uХ:%KDo3GCGЭh챼WCn[J~~b7dl(Юbb6ocrbS򲳭ΝЯɓb Cn&~vfR,:uDKjfϔ)bU˜44kޫc8rDwrަMM#!>^߳kRǮ;xg4--224qD?}]֫~=PP".GFR%\T qkaÐ٤ MCNj\ISo1AuԆ`װahܼ[I$uVNY!hp5WF}\ y-4kD VY'pY5 ѠyfؽgpO?!0'v‰]23qY.NݺuQ{B~T&3g"fMQ"x^5>Egר xiٳؕUP5<M@Ɉݼ!7Q |tO8i99h޼9^}l؀-_GU&~=v 55ܹw/V\6wV!qhu6GMsm07hqV6.ۮN3dƸq}m u onh°h5q'O"U+d4kDBSbO#ys3g%[Z#3BԦM'M‰Wv@n^|=zҾ}`E۽woD>FEZTP[?ixA3;}:ー)S書cRT o0^޽nP{yharUîF } Tg 4F#.W W8ީSReG㏣'1éQ)TO_~W\ (#_~m~u˗GвeH1ٷ݆,Af|<;޽Qg" teKl{ @˅ N$X{w;m͞DmߎUxx`|hϝηƙM…8~Ve¯8۰! W\_K:zg"B7}P ZpOW{Lm(HQDmۆF#_ w݅vH?>z=9GtAΝ2_[S] U+T8|1))7␺ IDAT(Xny8ٮv^?EĴi4^ÂJPP"ʝ9xlUTJME {-k⋸w4hQeÌF@c4Bf͂|yhډX/h|9rb#bDd<$3Xv-33Pj*=0Nc^|w} BZBV͚:990  |t;t{:wU΁>M )T۰QC`cbPG"{*tx(4a/CףoHϞ8j۴Aeː:bu邃[t͟C{(}z47suzpEk.Zn-CٳgKDD:N|}}.q_n<|XUVBя~ʼn1$䪋L}doD˲7%6rDrƍݲEt6{wȑMOWٓΝдuN/_*1"볠@ժmݢݲE[HiUڎyFѢ5bhD7seX14Ԝɻ2{ (,C>bl6%E-{옹+Z,FV{V@cbs{_o2NZ-3?g݌MMUWzO[C^cPz-;[ex*we͘!F f~P&V2cNJ10P%|^ {,@A?1YjW.o ' k]|4*%Ɛ-WT4/O.?(,-((P*UD{wZf϶֙3=oʢ~c bSGw)ڵU&.Nemd;X+ E{OWoIr*3iZ.+KϓXRX(yJ|ʹΉvKEvmUEOj//vN-~^jEb~-ڂUXyټ<,GU;f7_))WemcNJ1<\ ;[Wv~^d*#F??ѿq}ǎUwQ ղ'|1}Z~Js6#/<#y31׫/nR1(ҫ9~^q ՛u}ɷߪe32,gg[8Qd,u<ӧ%2xHݺۺ|YY~2nzg^"ݧ6# V-SO@Goɓ""3jԍ-UgW_F>b,_^/^l}01Uo{|v^ՅZHΪ?~5u{U"ψ, rQaEFVKN^'(ҴHoZ]~ ^xAS'vJ_ar?T7z׻vqh<ժ\YL cEI l$ȟcԑeE srrd׮]k. 'O]vEDdҤI!˖-Kz)qqqre6:w,7ٸqqOP9ݫ>/P@yYY"::B@PQPHժ"}gy6Md$PA Z ۭO%F "T}e`k:+r)%jo3FiS]۷UANI UR%ثUKR͚u^u.tr>c| _jD^#mӜ9KyP_SP Ұ8Q}wz`~_\tmZT¯Q}̅?_mW% )/zԨ#~ ԭ+Rկ U۷6e*aV3935ՅEڵԩn]J\]!C㣶'=?*|ҭӻ%oz쥄<S_-}me2\~ֿh1cHTTJs˓O>)˗08pP9B}`Uƶl2jŲ%R Ԑ&keu,AA˲l&h{EMlAi4nNL{iseM7T%)jFdu"6\&9S@+{UkYT2`P+@3VL%KTv믓oŭ6MeFT~ DG◔̜Ѿu䓖-͚ zckOCZ'G}a+*kz]6n<K\hαc\}WOہѨW8)M6- @d.Ax_d,YCJ/T8{V P\[DEe| *UT^{YQ^*#SNêny_O~OTSss]s^ETti衚~YYuMjT$$tx$?_e||/=<^}.oƍY E>X.\.Sf~M?4uYE{LG}j'D)( ZA-EF勢]7-;wb޶;p@.\(qS{`h[ Λh 5Yk 5-cJOͭwv,Ñ#j2Hq 0dqKԩjn˸m%hU+W^{D}??5/uZ__mXÆj,^ak-[MTPjʷG՘\+~]mxiij`H{ VS?uoS"#jƃ{jtfr]K@寿m |jZl܍}hVWıWѺu_jլǯ(5`dKuIILgKݺ@b!AA4W49qwFQAoܩS*^#5ί˴nn ˗/ݔFq}j{ RUSGwj=@ij`o@ my-%37~[ ؗ,Qs(>| >7T7g45'*8ڦ0`(kF攅bE\y n[GhRo ^?,|0 ݝ23gsH@e[h5=3*YѢ"2:YPWvxֳ}״eոls&u7m[7?_'X%qc]]سN7~~hB;/_LS~y);d T@x}rk soCeo~hӁ1cx,\>j%GWN'YQR?lv%jl2i\elv,fݝ2a_yNerj `lʪӠA}Y]~ɓ+n ]oV۶:BOѭ)6Sԭlv<&3w6/UaM][M&xD6>$LUrsu lv<&2GOaj TMV-[\9]]eKfTDGB$W]ϼo-_J&`cn0өL]IéjV+V@ Ȟ}TuһElv<3c;wC0$"rb CB,üc u&X_*UD <  I*\&wXy6;@wW `F]g` ȣyKPv2hZ?qB3DDMݕ28>е5 9FH@wg4"XO<uiB9300vdGDDށMͭ8o;puiMkܜ40z%!""r6;@7gO. S ر-[b?oX 8p v} U|QQ^HDD^'$0puiqcu\?Tn|a! ,Z4m ̘e3tC/j02HDD'$Dp$S ڶU cnNc00$""SZ<͵+Unl&""cz[O``ze2$""/Ԭ8ΥK@@:ݻ}3pw nwuIU=z:wVyd %Eo} 1HDDV,ڴQW֭ZPУc1HDD23:uUqSpc`1c`ԪU "eDcǎELL ѡC>|؅A@"""tezkGM|fΜs⮻1p@cС?SNܹs1cƠSNHMME^QMDDfʗW7~N͛ѳgOt PfM,\[nSLo={͛(,]{vY٭p h"""TW^#xldq}!11ٳ7nD.]iii@넇EHNNvImb#;]|7u&L@߾}(]9EY^Ng'"0ye]e‚ua`]X.,lՅ^wUq܆?ϟ ஻ݻ1|pƢ7͉'bW=HSi/_F?ЪU~YvZWm.,X օ¢x];w΅%7tg}{lk70_נA?~'ND DLLyL4j6G#G裏~h߾=Vjt:]S.jwjzر#=y4R`]X.,X օHOOwqT??5- +:Oh4Ds6@l>`|`?.,X օ¢x]GS 𩧀A oҽ{wL0+WıcǰdL<>(@`x|rݻCll,y&""H+W/[M3g9=B`6mƌ^z gΜAll,y;ּ믿<>;vNYnݺQz1R3L~rC]@7a!ׯouܸqxwl.w^ǣ˗ǒ%KP~}޽ZZ3Ԩ[e4ާhBf/55j"keN޽YYYѿ$%%o @71 ݅",,Tv&M`۶mO@ҥKVYLcӘ2 ܊hDaa!4i$&&;x N8_:yشVJ{5FMDDD.7zhlذǎ޽{1zh۷/1x`9ׯǎ;0p@_̙j*?,`*O{{Llvcޙ3gЯ_?>}hذ!~Wt駟 (,,DN0cƌkn3) X6 = Q= 20طO=gO ݘ@f\f|>((ӧOt[c\Cvq#p82hXbƘ$""|j_gbdx 9@7a`Y1 3DDD 3DDDRXn.@ʪK\SQf /3DDDkzO6}Xt@x8 \o{x 4)E* $""|;ժի;~,zxY7 'G g* x0@b"PvËTV@"""ϗ2*'֩V}:> HKSs<w*]Fv1HDD"##ݻՕru/C{,/ݲ;T;)E)Sx G~uNpX0cy30`?L *yG.UC8s>`v`D5j@y;}WM֤QСߧs@^٫ IDAT58sjv^Fu rJQf<٪1tǎGhr8xG`௿֩^{ xE5DDD/-tNdlq$5ʕlQ @""""b4őYDEf5^N S\*N)JYbMDDDm>E*+x ]qt7$$@@Ӧ@l,\f@޸XJ G/RYk3ŋѪU+CL:sE\\ƌN:!55AAAv-y:uѣ 2tP>h®Pzu̙3X\\)SogϞy!** K.E޽Z Ǜ5 (_HJR4>dz]w|rt zBRRV^z 6NKKCFF:t`^'<<-Z@rr[ KKs>Լ[:k&Gb̙9r$|Ml۶ CE@@ @W\FEERaa! srrz:ήt&`{o,17ׁ ‚ua`]X.,lՅ^wUqIU`ZjpG҈\9g@ӦMyfcCŶmې͛7UV8ubbb`EMCօ‚ua`]X ?GޠKZ8q>`D5[Vp"{;t#F-[ȑ#X`K 2h0|p{X|9݋~!66/`{ߗrx޹sҺu҅GϚq G?>Lz@n ݷ 2C&f##qT2~y`cRp:>8 uw:?Pp ƒA$aƎVƏt: :-ÐΕFs'><?} V ^ k.iZQ/)͙#]u矷|)SbL#(`׍7=~\̔6mfϖ}n %ӹ4asd EEI?/Nԩߛ }_Qs7OLCa@B·ߺ;y#4\{OV'=+eeI-\#T ^z pJ{I;xy iFi >2}^}HW^iqftvx@ZզGj;~JGB~[׿3 wT'y=+=xϱn sJ W]%tߟ#\RDDLi/Y3.,@egg릛nRttz3fgLmmճgOu]iiiЌ[G0apVPPt)//O 2eN_pEokÆ *((бc4sκerf˖->׭[޽{XǏWUU֮]^{M'N$jС***7iz$I=z$A)))1C QBB 20<555܎Tdddq\ZpƍaÆI8v]M-oxc/84IJJ򹝕Kt߿_;v츢玍_1<P߾}=[eddwկ_?rázUVVt+**p8\WnN .}0MttbbbVF?K.t |"qt<'שogliq%G.%'KGtWk뿤T{JuuUUO@ J Nzpq̹ D6 AD?XҜ$]Xwp_ +`}I=$)lұcҫJK<`sr o~k$<'l6ǥGq|ꔔ$u.=>:.0"""oh>+%&\@ @𪫓,nQ;Vڼٽ<7Vp_ |la6:֬RR.i|.blN'@Ԇ /Kw!/ .;'}{@3. AoQܿ>w"ßD4 x9?}? 0yܝ?Iuqc?7l. :s}o{<,YzH=$V?֯_:jժUA+77pݻwk͚5>|Eֆ TPPcǎi̙e:&tq)͙3G/**]V>&NQF)77W|򉊊8 _:uLOOmݦjhhY>d%$$l hgAg޽}劈P\\rݮfYWW:횚IҹsN3wkU$ahuw$툨^‹Zx5Usj:oC=z4i$۷]_CCe$i؈~Ayyyϟ!C5\p+--MTRR2%''7HE?[jIRΝ σ?9߼bQ /jE-;ei@tt 泬[nٳg =z(&&F>u7bʍ99  bŊԩ|Nm L2p۶m>L5tIh/Y@`҅U@;#]X _vF*:$ LB*:$ LB*:$ LB*:$ LB@ hg q-``҅U 0 ªhU| LB_hUtIHVEhUtIHVEhUtIHVʼnIHVʼnIVEtaUtIVEtaUtIVEtaUtIVʼnIHVEo߮o]lڼya詧R>}ԥK諯 l/ ¢lN M=Bӧ5b4y{\R/vܩnݺ)55U~ . ܴi4mڴ&3 C=x M>]/nk͚={?zHVu,TJII,՘1cTXXUT555܎TdddQ^^.I>v>+"aX ())I@OoZ@Lus?Ir8 dzB?|&!aX>(::Z111 r8,Ν;ܞmWtѩ֩StAR}gѣpBw'|R1cFg2U\|駺[<333%Isպu裏JG?Җ-[)hU0a h~ͦ˗k~Օ!aX 0 ê\. LA*:$$ D$$ +`` ٺ馛޽{kƌ*))S[[tSݻwWZZ***4& ڄQPPt)//O 2eN>h"ڰa t1͜93퉠ls{ݺuݻ5~xUUUiڵz4qDIRnn"|́@` $CT\\x 2D *,,l2թsFt9544dl:HB‹ZxQ /jT-Ν;XFH@˥ jܸq6l$\kU^^zlٲFիWv2]թuu^‹ZxQ /jua-N<XCHt߿_;v츢,YshI:z4i$JöyjZ'OVxxx#ihhP^^^A^‹ZxQ jq*>fddwկ_?rázUVVt+**p8\Wdd"##=%I;wn7KѩC+:\ZxQ /jE-ׅ9O(0M6?TbbFRxx=JJJTVVdO1S&^{Mo=ƪK.Ղ =z(&&F>Q4AW^-I0a\͛7Ob uIiiiSjjVZ6J A huLTTrrr@`Uq%`U&!aX 0 ê$$ "0b@` Uq)8`U\2= VEhU.A $ LB*NLB°*LA°*:$$ 9hUtIHV>$$ LB°*&!Za aX0}IHV>$@LB°*S@`U&!aX% HVi`IHVr 0Ъ0}IHVi`IVi`IHV>$$ +2 LA°"/ 0 ÊNI(`` &"aX?;aEtHV>D@+LD°"&"aX@`" 0Q 0@QQQ3fv),.SH'7xCҞ={4bĉ`) )Ƴ>OWRR^xuU/R`'FKlfL=@Wqq,YY֩S'թsJ7ܹs7#GYREu>[wР'O|$jq!jE-jǏxLLgldd"##r2ՅlzjB-3-mjo1l0?Qꫯj[XX&LB(^‹ZxQ /jE-ڣV me3 $?! @" @!`hҘ1ck׮oذAC QTTz{~uM7)::Z{֌3TRRc֭['gҥ^א!CZ|L0n hTͦ&}v~튏f͛}7 CO=.](%%E_}UmT=cխ[7{ձcZ\Ǭb޼y^ԩS[]omlC Uŕ"oLeeeiϞ=1bRSSuĉ&'{`ݻW3fЌ3~?ϼ(==]EEESCCLӧO?~s?\?|^׎;ݻ}j'I뮻}&] .IDATLlOֈ#rJ ڹsuT6ζ~XEK8s٣'|R{ƍURR;=fm4uT믷`.$z饗dٔz;vq ѣtmiMng٘1cy‰' IFAAAcrssX??#F\P.z!3\.W شi2?҈4^f+Mٵk!8rHcj1w\cmZOlӧO7&N`..@?WqqRRR<:uꤔ6B񒔚JԣGǝ:uJ5\ӧ/LW_)>>^^{̙fdžvQ__/)`.TZZryllƌr>o:*l6ŵ8-ﱎd۶mݻx@}]cCeлᆱ :6Xɓr:>v˛|LyyywT.K .Ըq4lذf #C b@?ШQYrŸPrrxIkv|Gb222i&}JLLl:Nۧ>}09u: Brssջwovmmz\nr8>jܹٿ|tW_}>@={l:Z{uT~f_W0o]VF҈#`. Q(hFddn:Ưk#..(//7 0~_oiv#22Ҙ4iRYUTf?;nYŵh=fU-̙3Ɣ)S7ow}\(lYҥQYY:eR60 S[1@C1@C1@C1@Cm&ͦʀ<~~*-[?\.ft8&L… };VǏWlll@裏'PXXXcNp~4F"""p8d;vСCvɏ7oV\iy@ʼyTPP^6M6MnuwyGV׮]ӟTgΜ_ 0@W]u_kۺ:-^X}Un4fm۶9_^'OVTTg[nEъѨQ駟z駟СC[ =h^aÆi咤Zn4̙3Zr֯_͜9Swy{믿VZZƍYfI222t_^ڴiN}iM飏>~3esȑ#zj>Sxxv}Gک:pi:XEDDk׮r8-mhhի=?^yUTT{JJJ-ܢ[j֬Y*++Snn/IZxl٢\=M>ϑ#G<+++#>^Gis J]tv ݻ,;q$i߾}r:4hzԳgfٳ>_JRff~_W^QJJF.]̙3r r$c9uT\\h Cz>˖.]gzw??uwz|/ p""".|{m5rH9N8qB?4Z>h 4H-=ܣ\OաC4rv?\* 0@;wÇuv;A4g{ڸqJKKk.eggwmqڱcٳgm۶ȑ#㏵{n :3HJNNn@[t8/VXXtW֝{W?3fhJHHh1s_|IRXX;{4hnM6M˖-<_ל9sԵkv;\*aF'#tpB 8BCC|*K%F={o O>jƽ[vm00 N-e{nUc7cxSq3]JYYYxW0rHh^G@@ ֬Y0mڴ|˷lق:u87f91|eff {e""""0` 0elѢEٳgѰaCbƍطo:w Xp!wyey1HDDDD)))h4ٳV )#3DDDҐj OORm3++ F ___@BB@BBB^Q[jժYfj{<RXxJY6$"""tԫWz4?Kwlݺ՚`$&&ڭo4`_4$"""___8q7oFڵ֭q벭[l6k׮G1HDDDTt`^2uUr0jlXѥ9Sr2@߾MW= h!m] 隆 L9{/%f 9zT>0vٳ_￁^ުIPd6l*|x}60:\z={5F.`*)ٳ]wI]]U/ܹزE> %x$0$r&K(`j{+<4s!@h( Hjbb/%ܵ rSmUucK/I5p K$-V Wjx ey-|dph(ЪDUP)ҨУG/\_~aX9%K2NWg oD9˷h{+ ~~`pD&? ,v[mU&@rؾJɡLe`ig0H(`7K_ -ZwMÆIS2tH\ z<⤂8GF4iiR|L[l<|Xn,WL1$rG$͝ FE*׵k]06VN۶%VY3 @A% kזSs:=*/^j/KVn]h j?ߏ nތ! @G:~0i4l;1p@xxAo^"ݻ=!,@ˀ]T`j$&?fwZS&>߿ڵKT1$rcrۘ1]XNpJ~1kD.`q;'?AXQ%;@DDcc%ӥ ;4AۡCrZ~Ht Ir\;Hz$K,`-:,dd#6;[Q [G0e"f B͚,z[$ɚeO~u%;weJZ2!!.If(l>'i2ș_e( nȔhiwG1٭ffsNgQzU͵c;_?LM߭J_rhH lߢ3@߮YhΟs1%,.۞Zm_Kf |}Io7zuޟgB䈦"0܉R5㏒nӦu/^ ʢK3wpJFb"TJޣGB;ͧJ-FVCk?.W'2l_1*^LrSr[#T߾[>@p`8q_F:m8ñcϢ[7Q9ٳA\ǎ7&A-Sҫeܬ5%#iq! EZڃ8ڣ-FNSFsm7K9 ?/<&c2%+v'ñmہw.Ayp3Jgkגʒ7?+V~|}ؾ}*gx\ Y˝wʜoe9\OT}5߻,kn:%EڤRpɩv؁A!44W{\))S $$^^^ӧN8aNRRF___CzzzyZ%UPOWtI2~ݭ.{AR饊]֖ɵm[)8oG{xD%ŭ>w>ج/P)য়$x`,Z[!lP8%EFټYj{L l)ѵjI a`ģCvL__hF?sHJ$V+ϫQCZRsWjF4mLfV9)oƍW''KrL%2\͚7W.˱j2wYϞ2P Ғ%r%nt}HAEAV.ònٗ@D(ۦ$=zMY71Q.^{M$X|9$4[5ݜKf322Ю];<6lXΝ `ɒ% ɓѯ_?9ro\5 񈊊`#~č [CÜM=\r+p]ֳ|`R 7O"ؗ_,pql.ڶ7q -_^/o6Хeѽd'mMx] e[ݶ|vlm"}VzϜ)) ChӦ9VU\j*}٬om]<==˕RJ9rDPahԅ ΝSԹs眰'rrrիUNNR*Cc_q&y'oTTuwzlVw;V+WJ)cVVf^zIػ*UR&SdžUQ#y/l}an_DR11eSnZL^'1Qgg+O?]߲헏TV7_~s)բEϙ;W)//NXa)@)ڵ3ўߪ=שO?Uꫯ;~IG2oRƽ{uUJ(աR&ǵ0JO+vRii7yJJ2K#~YNR5k*5{ӊ>r|ׯcټyɎO^J {h:r$fRժ)`O?UJuRR:)դRJ5hzjNNV7ߴ_?t~YRᇕjFOH‹/گsR y'9t׬R7N~3KYQ9,߮%3ECBBc]燮]bϞ=1btٺN>}j{ FeSiiiI,3 deA@}5/`מJ7w.4øht<H$'C7x04{B  CqG2M/Cw/4{ T~Pkkݸq~%7f LAM(e۰(hfS'4Qz$TrS ܄{m#"`41ݑ#4k˥iӠ>dOSOA K/;B=0LO>)Q"=CC{Yݳƍaj=Gv"ﺫTa04ibWe O`|5NCB߮tojڵ@d$LІG?~(Nز-s?`D ժٽ1wn('٬)^stZ--ZwB0Oԩ7`6CCl@x1TPZIf% * fͤ>^< ͥKPwݕ༔A}}"# ߙWkz=4AoΜqjɺY(}T/ Ιر@` ;YY(-Z@uռ9TӦPMȨEs_In~hz~""`hҍU0F>ԡ:)m~ :,\spo_0HOG9sP;6$kv};>}P#!5Jxdd :ZQQh4 ¤#38!! ᑖjiijLUCFp02Ďq%8bǏusnl\fXc.2!Sbsp0륗 ^`696m­~j_|R7Ƶf͠F'{:Rpysz뱪84n/ȬWϮ|՛4Aߌ 4'Dyz:+Ry[NFUVaСݻw;ŋbFw}z K,17bڴix | .UVCz_QQQ۷/m !0͘ø{7TЍ x^}yh-qfnj^q&DEAh4;wBcJiSVJ.<\ra˖jwoY'FAb"L/ @2~ /@$iH"k׶;&Mzk0mZDqɖܸ!#C+kgZj׆ &=hG9sa1C2-׮Aq[H&}0 saC\}˖0 g{w*rzupev^h~]2Jִ)4/Cr%NΣ}0Vn⩧ܼ9sC1G'`;=Rϵc˞HM*P ^x ډAtZKK#KQ}о<}g>+>E@mxvl͹/ΆWҽV-y}|ȐvN׮A70 ܹس'p-lwr23]aZhŋ`z=}|V9qBnqqPuHFY3dnX|!ԠA?t?aotݻC{L&<{,G0TAϞnt PCCm+ߏס_N5j,kxd0BJɱѓWm ȑ 4 0X/Xo%Lŋh+n#@y3p}s{h?z8hRR$v$4OŋnA0-XA׳'40gߓFvTs <{66pkQp mہ_}@ffn/ qcP6oՠ MR|Ư_K-oB0<vBh_}Dii9nO˾͙SxfN)Uٯ8h^ ~9?eSYGO&*;zÅ sΡ~9YTtti!OGP1yRu]WJ)+ tjʕ~rkvpAS+t:W( ի' sR+^Qml}񅴭)^fqe$iհ8v_&<78 ٬ԼypS'Ejq7S H;=LYzUՕzek{[=';[m=[ Oj)ջR^vz /RufP塇A8zTk.9Rs[''A_Tx 3m{d2Is\ThRҖIUJ^¤1)ۮ]XFL` d=m(?ӧWBXary5k|YrmߦŞ=]ԿUvRj7/V\f|8{| t]J f82*'obgHLߋ3rsӼfΜ)i#]["m"9SC%<X `Ѫ\h;X dMM6UN +WʏՕ+q j7JuԩSN/RJ~;B(%'ՕZ&4v2wٿ_l?СiefRRkɪoj ,=.q;uJ_VsoÇKi{;Y)??fͲJ=RJKS0$_G =lM"?rD>;Epo*ۊ]v,뢛~ޒ%h^P0bj6^~Yd6+սR]ʅRw)YYvRH{qr7tJn-eΖҦ㟝{Qef,%%vy(S0N҂ˮSO)$NcgL.׿GJo@ 0,Kiii*&&F(j޼y*&&Fqe2{l֬YO5dۜ:t蠢ծ]TfȢN@(G5!E罪-OM*JɉI/bl2?J)ϗKyOl (=,˒% gC)@[vRWK !-e|Rn:yR>{>%٬+Sp7,L~]>s={*$DW5JGJխ+YO w&X۩SerSӧ%x߲EʿnaחᣏgSː1 -^&{|Jٌ`gdy?m/xӝԩז?LÆ6m.}zTA bi`|z(xԻ7'SڰAqUR6ҙd6 IFu~`[V) 85W+Ҡ 9w5j$q0`]ޕtt9#~6֣A7 .c[JtIvv+W/_.=1t(Q,LjEo('s%<(ġCewB,+?/%܎T/S9SKc$Jp${ -pABr:\(8^hA2QCWEd mV&^|Q=m5b֒"I wh*7. `eaA6/w 2Mϱ*IWX:5Inl*jyxHB2W C0փCGToq(qWfvD^wF xxHg"Bs`؛g7nSmɭvm(:h1oDygTr3=.qiZR njq|{gO N6`􄖱+"H@h&;dڷ͛e_ Sd̝:>^_YED^deAs2;}.%i/K< Y5krotc ]A `e$'[s 2DU׫\5p 䈣Ged(G^Me`ΞMC-V+v"?% M$9s$X0887c%74'^~ANT.^P,ر2kތN^R-'i7'54Jo#""f2j:b*We֯/Yo dh*W\w+w"fgT\SD.[&Q7n q`R֦? +WJ;[W|-Z$'sǿBFo iw쒺>z vGŰa=>w\,XK,AXX&O~ȑ#ihGxDEE`0Gرc+lV22g075Jb6_|?`b)'-e=V+9(,L:%'Nwo`;~K $9R?>^u 1W_} ^#F@ll,6n܈}s bxwZU⨂*s^xAnUPzwK/ݷߖ&wW Z1*$UVZZRSS===i/␐>6еkWٳ#F={o O>jƽ[)!t=XE)%3u,m!Z(K5peUVg͚5HHH- ><'^:Q+*(-M~˗Ky082wԳĿ?W0XSF&xQ@XD sdRN tBl...]7FbbF#/ov ?իˤ :udE. U!aaaƖ-[RSSnݺud8p֭[a6ѵkr/3 k`%%?Z<,;%""'OZСC@Æ 1~x̘1͚5CZl'G}qaĈ`,;&M%}%""߿l:0a`̘1//###cǎErr2z聍7ZKbܸqVñ`r `\&CXe;v 8z9DΛ( }\`>}zTؠa X\16Ak Ff-::w*WhHf0hV[.g ""0XM?CBv:L {M]50X ?^o/F֯ߌȁ DDDT"*\3ZHwξrUv0ϥKԩSOmVti\ @W `>&WDDDT\TVѥ4?`Bv. a00mO>Y%"""rM ]A SRK[e#ߪUlIDD(uyqaC` \])㭷]Hٻ};O@&_n""DUYI7d i \.I;8fM_?W9VBJi`0P矁l[nm,п?0x0;jDU̟ÆQZ}ƿ^C.|=Х {ˍVB0b1\>5aB||EtӁ7Tfa's [ CJN DGWtJgz + ハ. {c h;믚:K~:u*$DDD%.ع8s:tus<@1жNp |_&м |@p:4 2Ez$ǔ)oQ]$eWOaCIԔ@W`$\pz w`Pi'жm:)[o|40iз/YkfeI8c\<`vr̀N{_?Ot.FiSK寿ŋs%X8 xf̐lߋ/oMѣ?Ա\)򥗀%|%wg|ey8-ͱmVN & 'OFXXФIP6R2e BBB>}ĉXBt@7ddɑeo mJ:vw8AGKWq[[R ܺt!C_vzv;DDDTlؽ{w|'8~87o?v¼yqqqHHH@t~~~ڵ+S,ɾZ) e<6P ԛo9DDDTr_}U[gQeI&!55z+t:L&fΜQFAyY+;;i7Z^F ߲=` l 1q׮XaV TÁشI3A5+C1vaΜ(rݺuC 2)wE"$7cxSqrJ1-[X=$xCF)G&4hI&g.1c=zNB&M[ٳ'ڷo?6f/\VZ!..sj зS'hテ^:Ԭ ;vh0nN7Уδ4:zͺ5cxJ1==cm+÷ŗ_ݺm [UI9ǭx昂^ZO׮r?_^ wP mBb43g?ӧOcժU7oEh4?~#DGGf͚ׯ~ہ9s(df̀6mڵe~ .UTvlHDDTvލ!C{4j˗/޽{Hox1dW_} ^#Fpە+2י3@:@r:cX1HDDT&Ґj &޽;>?~͛7]va޼y8$$$O>k׮سgO@:ud_gaTQ1HDDT&Zjew7ԩS7i$[oNd̙31j(@BB ((yAAA*]SQ9rճ/(+VҥKl2n1cƔWqQEeǩʄ|}}oK/I&Yr۴i3g`֬Y3f .]BHHy.]B˦^*Tff&yzZt:o `lٲxjj*ѭ[r-kqo0;.,@ݺ}%,\R B 43gDÆ Ѻuk`޼yxGǏnj3ЬY3a P'ض )P^o`0~~$AaرSO>>R$ QZp!&Ogy œޝGUO!Ȗ4-"hF0Z" Ѫ_$E"DQ+(k+ą(-$3㚑0wM|<򐹙|r(s=zRrrjؽ[=ZߍYZұcg~:i6_s(t}Ͱ)@kVΖ6m2KN~{)!A;Vs3?ms~zMI 2Ɔ 8EI&U_7u͛-5<~Je4}gzI 5k{z{ÇKRlt R>RB%onjƦ%5*, Mllr~TX ŝwJ#FHoars"vnz9@^zv4sq$dĦuռRdG0^KGA8?eM5>k|\u_pwZ;:biH/p"o, A;d3hwl @ۺUzMc;FMM @ܠAo**޼9p n,Q!C/0UoVJIIKwRrrW?Aw _?pX2~ۨQ)Qq:SS@3NyDcș LW:wy RvŸdE _.}U ; U_أT^ 'C,M Z%Yyرލg}Z~>ݻ5biFٳ6lc=:(::Z^@_-"g35Z6ҿe,_c~k+]vuYgySOiz饗'*--M%%% 6 m,Nv{st)#ﷀOs9GkIII?n͚5K>~_I)..NK,ѭz|Fǎaά?x\ ~[iii>|ԱcG]?7m6ٳGU~z`UU<+++%I555kjj 7Esᅨy =7̛oN5o555.>llK믾=׸ֱCލ{kHcDAk1mݺUsUNN~a_^cǎUDDF={HNXO܉rss5yz նm[8ڥoMQAA%4J̛o71ga|м۷ςJ̄ Qp+VԍP5-HƲ3Ӎ@LrԧOM6MԫW/m޼Yȑ#}s„ ޽{RRR4h u/uתVAAZ4k睧_ү7Es6x`szic|üy9 Sݻ-wKH&]rI?_[ L5z3g͕M35vA)))uu]o$)>>^TVV:xSVV H8 3F! i $}ys7 [X#\\H׿~PçlSZj 'UVJ{-:׾+uI $>>^WTThڵ߿iuybAO~|]ɗ|Բ可#"ml8Avv iӦoֺu4|͟?_p8SK.m`4tPSj AMV*)1OSco@o.Y`XFzEnc} _|/^ &hʔ)JJJҬYyn H)@,$=g8 K/>@%%%ڵkռysѣgCCO[I˖IGAD20@;W첀ӨpxY[I9RDΚexb3G93 4:RoÇ}ի?a:^L[gҮ]FW??oW_-FE RW{l.]{رcrǸIRNxtҭJgr:```YqɄ[\>:"%%ErtEiڴiիnu]7o^jF ͛4F|/0j?JdžrScǎz<3ſml7tL𗿔&NZ s4ite ~-[z>N<^1׌t6n4Vel k(0G-]׷lۜ<RHXylRL xΜO=t&Y{4aY2NHK3B NBrrV/?HSm)ӻqZIz}.]6QD]:؂U._<,:KQ.ܜq9 3m 0X@1O[6F֒ x v]ȻI6Fk@@7ol1: l@]zuiNر۸ѻX@ ili().N3o_MiVkhgt9sg"" cr#XG} 0-UVoW_~<HR|NL֬1mv{?HUWIomy();[w%,t@?Fv6jcm$f3ʸ^^.Mx@s 4u4oRxhkt$J]Vzltm }Us~<H]wIIkJw+H/{qo@zp avpdnj~<9\.CBp8.XyD?޸|𠔒"h!9bm,d0_߾j3$!ژ/AJ0AG0@Zĸo3sE{\.:{NJM5>\5JZ [tac!N'@oH4dytRMs9owo=z+,I@[s 9Zaa3E{nۍΟ$=*I͛}ߢEލK1:#1?mm`n>r8\;z233զMhB*++34~z=s \;C7xCEEE4l0l@`&< =:묳< /hƌꪫԻwoO?՚5k,.34뮻Nunݺ)11QWt'Eɶ.\7j>gEDDUVuiϞ='JUUUוUWWrCuu.\ry즪M{̙o7ߜjjjj]4k.7N۸u:r8 ч~T߿NSNRvvvܽ{RRRm6uѯ?Cuu3d>tH+:vSU]]  uw^]tEkNS+WԳ>?PǎӁtop6FĨe˖|Y{ [tacZ{ %ynݺZl1coom 7sL(==]UUUJKKӜ9s.H63Oc[D1%7hc,f X@`f hc!lL@16f 3m@16f @`f @` 0BN?#ڕe 3] ®Nt3vU$?#QIDATUm[vEhW,&!@IvEhWtIvEhWtIvEhW?@wEH6`#h`]q0 ЮE $@b`]&!@`]&i077W_|bbbԾ}{ :TusQeffM6jѢUVVfQ'Ld`QQ233fZW_}:yOvvy***w}aÆYXq_\ti/ڷob]ve*// / 誫${Zf.+A4%I[$ZtMZz%5A$(K.KYYY8pz!Iڳg"""ԪU:Ӟ={JUUUוUWWc&l۫ȳ]vZ`n&IҖ-[Խ{w^u h@ۭ1chZbE'I{Vxx .I*--Ν;տnjTdduEE$),,(n!! 30oa޼ǜyMCJrv]0233`[<h;TNNZn-[j̘1߿=NBfRI㏟kjM6Ν;WtWԹo]4sL(==PS#0MIIIGNuVktmR^^PNBC-[Gw}W+W`||;)>>ޯ5 -& s=z/^e˖r]AZW`&lN 64hW55rs k @+:B_W]9t)v"`]\t)vEaW,&!a@`M9X LB+:$$ @`]9rq hW55@`]&!@IHv60$@b00 î$$ 00Ю0LB°+&!amAaWtIvEaWN\t vtJt Hv>$$ 00Ю0LB° @`]40 î8 hWtIHvEhG.n7@` 9D aQm00Ўjj$ a@@`@`v A0ԹsgEEE_~Zn% 6d࣠Nrrr4i$mܸQ^xҴw^k c0b࣠3f]wݥQF)%%ESf׿`+ > ;vLŚ0aZHHRSSzJUUU咤]vv?l߮0Ieھ}7vV]]}1g^b|üy9 S/=޲eKHEFF˗`wA'ө:e˖&77W'Ow}Ԩ?~ףG:'M| v&LPNNuMMKs9 JD111~b|üys77˥;w*%%Eaa?FMUm*44Teeeu)>>i5۷f͚CiԨQVlez1HCZO.vޭ#FM6VϞ=a˲5ө'*))I:?AAkʕ á%Kc=:(::Z->N5ozճgO5o\ wYX=m^SNN&M7 /TZZkuiUTTLYFW_CY]Z~z=s .߯*<<\|JJJ?Yguե5w\=/5}t=Szg.V: /Pyyy ~ٳ5o<]V͛7WZZ=JTvamܸQ'Nƍh"jȐ!Tj3nJ߾}ݙNӝ͵e޽nI"KJw.]/=n8K|}饗Z]Fsu׹:׆ Ȱ"^xrǻ?z8p~W(іN[-ɽcǎUeOtmرc*..VjjZHHRSSzj +k\%I[u]Ws8~[}վ}{K?eހTXXJkժUk-ضmSjll/pUVVb)}t*..8mٲŢ˥, 8P=z[[p6nܨ[]JuV͝;W999zᇵ~z;V9rC= uMr:z'auiƞ={$O;z|A׿V˖-.R@4)ڼyVZeu)k.7NFrO>6m$W^ڼy͛G<_],X?_6mRVV7Luunfn͝;r,-`i۶BCCUVVVzYY-=z}]-_\g}Zqqݫ.Haaa SQQfϞ09NK:(%%εݻkΝU8?^=nVSo\Kk4j7oǎ*((D[k.K߿ٛѣxb-[LIIIVd{ _|M6y>飌 mڴIVhKW_}N:YTQpau*eQEORR~ڵkj_>Hmڴ$[hȑӧYfСC5jեVff,XzK111abccmquS͛M6<;y 0`MoY֭5|KnAO<u>ӌ3twX]П|5\p+ #h"""/j*}JOO?5{l#hTnv駟~ժU+ڵ5knIK/Ν;묳رcܶ߯;yׯVXqʚ.\+**s?וW^lR{ֆ <a} 8 aVxW_G2e$]vھ}{>|Xg… UYYaÆoTVk֭JOOu-HF-\P Zx}ҥK5}o~SZFFzꥹs*44T6mRxx󉉉;OQUDD5kSZsnI/ԢE +u-hΝΝ; ItRkڴi ~;vx_kΝ?~u&I DŽر93Ed5k֬Nw-..N;wV-\ۻw$/TrrrqԦM~#GԹ+I999~_~Y>|xN_tt>"h*Ik۱ս{w{֬YH/7_JIIQvsNnMwvCjJLLhddeRf9?. 4M~'O{on99I{c tDDDDj)@0("""fEDDDŒHQ 3 """"aFPDDD$(@0("""fEDDDŒHQ 3 """"aFPDDD$(@0("""fEDDDŒHQ 3 """"aFPDDD$(@0("""fEDDDŒHQ 3 """"aFPDDD$(@0("""fEDDDŒHQ 3 """"aFPDDD$(@0("""fEDDDŒHQ 3 """"aFPDDD$(@0("""fEDDDŒHQ 3 """"a&2#̙O?ŋٺu+SN{\. Y~=OΝ;;Op:уcWzn.]Jjj*NH0zl۶-ZQ.))YftM\s5۳gK,a4k֌]vqwӵkW-ZۮW^lݺ\\.L2Xt)[yH8]pcL+q2G,\֭[qF4hO?Dvv6 .UV̘1.]e˖r=Dzi&6lw}Gzz-̙3СCwb'ԞR{KiPlϭ[(4^(((pPV-͛GZ| ''ɂ +}өWuv\$''ITTGjO{=^ܞ<|+}2d_= 瓒RnH?JKK)---G.z؟ WjO{=^؞n;U./c0aIoԨQ13g$99_\7\=^jO{R{n߾=U ƍꫯ|iiiowܹsС <طGvv6:t 33ұcǐrԞR{KiPlϼ@W!B2k֬a֬YԩSm۶e,^-[W_ziӦQMtto"***dԞR{KiPjP9de vZ XlIII,YO?חD583ܹ3r 'Nr1p@zY3EDDDUPE?ٷ|l>}xGh޼y͚5[o1p@:tzܸqUDDDD((`9􅕙0))&} ;HR 3 """"aFPDDD$(@0("""fEDDDŒHQ 3 """"aFPDDD$(@0("""fEDDDŒHQ 3 """"aFPDDDFO||<)))tޝUVۦ}8rn-@5>>@c={6 `r()))-֭[}ѣG Tg3f(lʞ={QJQ" }DGG1^AѮ];>ln 2dV?2EDD$,egg[~GxGb Ν[n}}s9ӡC֭[Gƍm]EDD$,\Lzȧ~ʜ9sW1mӦ k׮U.IHH8vNN_MVVql2O("""r  `ʔ)|GǓ@bb"[)SХKԩ˹{9s\)Ä kCM4}RF 2fJJJ_>=z`ذam(1ׯ_ٳgWQmyEDDDŒHQ 3 """"aFPDDD$(@0("""fEDDDŒHQ 3 """"aFPDDD$(@0("""fEDDDŒHQ 3 """"a&2#̙O?ŋٺu+SN{1<#+޽v1a4ifΝy|'8Nzرc S@x`.HNtMx<й36@Asb3nO߽^.^/EE-Je^5jEGCD8_rSj9sjطvޛA-7e,))YftM\s5G?zhƍ믿NVVÇSN\z֭[rѯ_?ϔ)SHSK/76VRݻCV0|8rJk|ʬw);+^?dl4*4"0 2'~B}g;vPN \JHOU^j\$&_c=G@c {w}ɓٳ'?,\VZ0c t–-[ȨT[l~l޼z\.ӧOK.DEEٺc3a@hܸʭc/h۹ݰwu۳v3o<ڶmKDD$n-l(ܧ k@1^”0^CqBE89ks,s(c?Cn.|c(**$>>1S촒].7[ʯ qcn]s^\~%ŸS2X-2q: g -hOXivjdu'Ud(,4!4Zs蠟%-8GLhzB֮~~o;Nbb %jHJz+x~Dհ6^hԨGFZxy">FDf:/!"%.C4 j:SIBbV/'Dž33(6JwQpuypY2&O38~jۄᆊ3n'/\usΑ?oi-w>+.9sp(: } џ"({eÆ 瓓[H6m7o={d޼yԪUrrrp:,X:U?1VJMx1_qև۶\.[sGB 7?.};څ]l^b,*lʢͩ,^oz`[o8z3W&9x!PQaกSb qŻ{o'!~ "" 4{)F|4$"*=(%{nuؾ ^VO26QGKcpu6ddt,_1Р;zx2^30Eト]pՕyP~>|Up}V߷>Jw@+pOvOx )=E޾/86lqs/f-!- A׮VhN;® ÌKn._nχ-pVͮ]P-y-aк5λ}0opл ͳ>c Àz9m:0~ܹ0~S#*. O߷xs c஻`w5O?ڜq^(,^ut_F͚G5nk[ӦuѱUlr0??'%%$%%Hii)墢"]r\q|%Ep,\c2eeT[-/Z8֯뭟vao߾P΁'FĀ8xϐ!x|8y[o QǺunku`N;R+)p"6fCbs/7Kxg76q:quo{ {weoa%&hx=SFKf߹n%z_8<IJSC,{ZnMDFF %qp8h7D%%NG!ow411˅߭uf͠ڵ8LӦM;<>LMO%p,Yq kc*XkdeYavmL8^Ê8nÁcH5c?z~ 23289u K/<^ۍs8DFb /ÆO<}]>" 5SSpDtiy6?x/L^b2"o<2 p8Dv&x͛q|3p|c '{ˆQ]oɹsq#G8>NLNv M޴vÆܰĉnݻqqZ~ۻO\)S0;DY{CV# 6)ѳ'K<|GO" z p|s0";}:_SO=~ Lx#2'rrp%$&n"^~>ر|E+ԭ ;wF[f&{cϞr{n QVpw98!wo]v믤_w'_gժUˆ#+,GeĈGWI xᆪ8#]MI%%QkzjYC5k:耲hm .2j{3= ,; )hcx./q7~&"x(c`wa ;đN+0 j N0u TE:DGP#CXHNKJ^+nfv[O̮]cbp+.a G5khcIMeSVu yeN=43" FE+Up1DQ"j14n̮N4)"%aF7,2Ӵ|R,aر;UTDƍGԭ$5r_HQEEZkcUbn^/ݤ cWnkYmo-[RPɚ> q;& ][7Nٺŋ9qݐ!?Ę=}#3`]׮:4<Xzi>~< fbѽkv$[YCWЫso(W-[0N';O?mڰK)U\Lر#?{|ݻ[Y[={u+5n%_IgT"wCi-)̙DX5q/I]!ޢÜ ~ T{/Ňt)b󥗖+ҥxJJwo_~9\r}xfh<{=))MN&~6upsP3?ߺmJKJ$-tԭK…ԟ=}kG'Of_:,nvi~W)IqzՂƍڳzϯZEիܻW4ծM̮]m)oB¦MnԈU]G~֤/X@'de^ڃ0&|@[oᎉaz".|abwd,[FY,-%U+6t R%%\rクcc'FGS{j4d?˺n*l9o6mbGv6uK˭3N>,(+}E^t떒B39^?jo C!ׯ_OƍYt)͛7mw%мysƎ˿/^v2vC呝͆ ȬPIp\2[[o_M6.NVض=lɦbؔIYYH(SF)#z:;4XYs֑A,!- 644l RSԷ*,ѹ35{Dviό]g7c>е|V/͜9+86yuxӪ]dP1_ǜ9Dtc99V֡'x3fTy{ګ{x?GVVVX@LԩS}^פu&::ڼcV\ih"6_|q8&//eo޼f6<̴iLYY⋍c̮] fLFDF^7IkŘyyc>̘+).6_m_un̗&oV1Ԭiѹ\ `\{1^oksDkf=c9zsVƜ9w\p1gioXvھݘ8c 3&%Řoû<;Ә5iϷĘ;+Vz˙3ǘMNvV6fŊ'j*c,~Yiޘ .{";wZܥ׬צ}{3nٲʗuecY~x?;Xe,**2K.5K.5ygҥKƍ1<䓦VZ棏>2˗/7ݺu3YYYf!_;w6-Z0 ,0s5M41_ գJ^ͨQԮmLl}3~b1W#d_Rc&Ot*iΘtMʱ ׌iʘkXU:bwc&Mx㏍9O 1͛N3y[O^ks΁t05k3f)*n.'* Ê;-ԃ>W3EDC=Ր^j:0}5DZ ڴ t-DDN`x("""P nB0XD0XPDDDl,4PDDDl,("""6Q ("""6Q :,"""6Q :,"""6Q  """b`` x("""P S="""b`v[?EDD @PDDDl @()))))@`0@`0@5j>񤤤н{wVZUn}1`ԩC\\=z`۶m) ٳg3`ϟOnn...6s| >g_k `M"(̌3-O</^_LAASLK/`ҤIy̟? . >&6 HJJ`Ÿ\.rrr|ۜq4hЀyǣ.`1"""+**зMtt1z4hڵ ??5jPVrۦom`C"""&11w5jq3`VX;S5%`(""b+W[>^O3gOKKݻwܶmiiiW (""bx|@c dԩ|Wdeee˖DEE1sLߺUVi&ڶmGK)h H 0)SGחHll,| <$;i۶m<̄ h߾}&Mo߾,bcciܸ1?01Nll,999Y&> M#"""6 nz &sYgh"Gbb"wuGfܸqdee1|p:uʕ+ 38 Y67ƍgԭ -Z@۶8o[n\qzꩼ|w7f Fnx7HMMeڴi3`u?ނca"HM ;a:+C@Æ /_f4mڔsa=&116m0o޼ `ii)墢"n7.؟YV3"eCSN^jO{=v]Z5o_C_0oj/^k_!|A 93ӫW/HMM-T}5j#F8b̙3INNXrssi~=̙>/e@W!=^Ԟ۷otxIGGC_-?d{[oŔ)S8묳Xl "##>}}:ΦCdffUuO+77;3mҥK['gTTTԞR{KiPlϼ@WpuX7;ly}r96nȨQӧiiil۶tmF+gtt4ѾB"##G녨(!HQQQ!V=^Ԟx\10k ^kfϞ=8^DD^,Ҙ9sB,X@۶mǥ@DDDBЧsr Ÿ99pUp晰|xUWg/0uT}Yj ?~o$##ݻѥDDDB}Y'z ?;[_V|!J?<Ç;~###[o~ط<@II g\tE̘1z AS%Xgׯ_}mX?tꟲC6U3fƌsm#GdȑUX?@PDD$lMZgfZ:L);d] NDD$x#"8|vSR0x DDD$*Y0y2Zx&B0Hi^yrZGnJ@PDD$}u1@cEDDFV zEDDBoTnol`C"""!o_Hpq8×H9Lk.޽ᦛsl (""r~> /V` 4PDD$$i/[]w{Az:+W0HH[;=+O0(P!]S/Țzp6 f΄M!7U * iyy0p |'֩L+ kOVߡ5V׌JPDD$%'=eHM; # 2n*mv6tPEDDByY8_->uXøqxqQ@3gW]u8MV}p8:w|Be\!.]aC xض ֮]{})0pxolԗK/HU= 5k㏺MΝٺuoWzw AӦMaR77Y:W5b򧴼Տl-ϛg,CUR(""P_~9_~1&--Jxyxd.UcMc\uGiuVk (Gz(""Rm}פpscǎJ=^pX@\bSep!ZmX2\h_C6ڵvʕ+yg]oѣG3n8&NȂ Y&:ub߾}aEDDBM7YW[X޺6)3dSO=E4io]VVwc cƌaذat 7xTMFϞ=R iÇZTM1:ukedffrwp-a=&116m0o޼ `ii)墢"n7.؟ gDnnݯSR{Ki/B=X&:tXXX5v0a; .䮻FӇ|RSS=.55wFň#X?sLUxbt? !Ei/Ԟ ܾ}{PΫB>b5 Rv̪ jYs."`Ϝ]^/Z'EX'ҧO?ϡC2x`r^^tЁL[}"77N=Ț5ҥ7ڳcǎDiꡓ^jO{b{ ̛gMGpNUgͪx֩/os϶".3? -- m۶f۶m4o޼}FGG[.߳? ׋Ï7QQQjK=^ԞllwZS%iK.9}ݺ۵kǪUʭ[z5 6BҘ9s/`nvq4HH۱Ti`.֮us'`ڵL2_~p84h=1?7x#tֺGgk9Rɢ+L:C2rH3f zmPRRBٽ{7]t3f &&ֺM#""Қ6Ca\8#O. |p駡Mw}WrW~ȑ#9reFEDDBګB\̞mps暊?Zo*AG="""!mÆ/j֯=z@^@PVfƍ_5˱ R i{XSz54jd̪̄; x|x!kο3ؠ1"""!mPRp_ 2k 6:gᩧ+ȱPDD$Mf .(ᴳ΂uSfe]ꭠvhP i))G/)JvjwPBQ[EDDBZVgW_mSf$={ !rA#1"""!'aJgXo=r^@T]ҁYpadHN>3;] .e'+|%w̛g-C WʯKKಿvn7ԬZ5n|dT R%ń$GcEDDVJ՝ۭ1"""!_eWM+8К ГA a*NPY"""!h-IcXs)VDPDD$$}uoVtk쪙sexqT%hԪutV<NzgOoUm.w44rzEDDBZL 3gŠu2:w;WfL吞^%U : """!oJ;?WރM}K_z1zEDDB޸qЯҥк5ԩLj1"""!F xͅXH۴ .=6WxmX$З;wZ7h[oLL="""!K㏭{S`5<"""7g*222p8L6~aӉ%''5kTz/ =d>`_p0r$L`39H@+))Yf?G͸q8q" ,f͚tԉ}UjNg=3:),1@˹(c3f Æ [n2m4zY2^z ֭̄ࢋl{*>4HaKLLM6̛7RC:xR(-O ՚񟢢" }Zn}jjy18^yo?WjMOvv6ۨQRU⋏\wL%jLcEDDgʕdffOxiiil۶tm۶ѼyJ֮SO-~\h脫T)FDDoIHHH"--3g`ڶm[}r }7,X+w~ WRԵTPDD$YvoyÆ ,[$4hAxhҤ YYY > w^? xСcw9)YTc """h"? @>}`РAucԩS8zmX#i Hz}x o_~ y0… y饗8s˭{Ox={6+\sMjY1 ][@˖gm{=I!^z+Pvm^{5}Y.RZlɤIo?~k\.:Hߪ]K + ''{̷~Ÿ\rn>3hР .pXTTrZw .cݯSR{Ki/B=nw1зovԬY~C6;,Y q_~~>5jԠVZͣFbĈG9s&'_4xUfIDAT_&BHQ{Ki/B=o*ЧO޽ 7o&77ғ0VСC}?呝M]K.Uغ\ye a.\:vHTTTԞR{KiPlϼ@W{ /~x<̙3^x/2v]p۶m:W$::u DDu9)QQQ!V=^Ԟ:24`ʭׯgqC ~DEE1sLzUشiS/\ۅd.f͚ԩSǷof$%%wI۶mzH 8^@]ئ{I=(--SN⋁V9G'&~cbb?~<ǏL*A="""!=tsj `u@j̩1""" ՘Pt`5EDD1׫@`5i`DDD3("""6S("""X,`jLg?(Vc """ ՘?(Vc: DDDDAS`51""" ՘zEDD1?(Vc """ ՘Cg(VcN?PF=""GUl; lCQҐjDiA奶%D@- BZDC  vԊ4B$ @,JXM @Hv-x7{rvsq5}{(6Fffhc,3Pm3Pm@` @(6 0Ƹ hc,3Pm%`` xQ0@`0 ]~Qbd hW~Qbd hWP=$@b hW,` +W{[+]Kĉ׿<,?;}0c Sbb1 ]S:uJݾGooeoQjkkIG+f0UFF|ѦM}vUVVѣ;tԩk8 r)ZZZ4vXϛ5koVhܸqzc A;X^^)S(66V 3gZ[[9w4j( 6Ljoo(?ŋ@X>|qF4}&'4O:A555=ܣ9/o?'|=ӧu3(۷o{i&%$$Iwy/k˖-7)I҄ Р}kV,YٳgkܸqOTVV&өsZmB]tuuIF)IjjjR__}礧+99Yw]!tqg U~I2C?Bi,Xq3_oRBDx<*))Q^^233%I'OTDDFwɓ'|rZ㵵=z4Iv#G }PVSScux4i`Ϋ:ڤ$ XTTC+UZZ{~ eddhƌ~WJ>]a))w(SMMn['1bydwUmmmͽq/E $A.e늍닋SttcT#G+77+%nLR4}tUUUZ`$W j̙Z~5Nzm~y UTT\DWO 0 ЮY$@ᐆGE)-/@?`] LAð+[ (v@` ]QI(v//{ hvvS)hvE @r 0 î&? hW\ LBð)&3$4 gP%`` ] LBð+nLB+fIhv 0 Ю&a&&K$4 0슫I(6` 0 ЮX&a&3$4 "` ]1LBð+fI(vm`I(vU@b 0 în 0 îLB+Ihvm`I(vR)(v/I L@ð/ {hvvKK@;8H&( `EERRRݻHl)ƫRi4ifΜk} `0H!]{9?… 6(&&Fo b0HaVԤ˗ 2Dڽ{UooyWW$?VG*LRS:vÍ{קN Oc1 O%]>|xdd"##/90Bvvvvrw\z|MyyVZuiӦQŅ0\ff2\.d `,_\zt 7h˵NRFFZZZk{"XOc1 x} i,XOc1 WzV{B PB  PmB)))RNNkuT^^)S(66V 3gZ[[4~p(ĉwQF)::Zrguvb *::Z7t~k;Cٳg+))ICӟzDcƌQttX_f^}ULפI4sLuttX-թH Q__X-566꥗^ҭju+//Oz뭷Ң_묎yUVV_{ァgyF>^xM4I~gպuaٳGC̙3uܹkF606)S_tN7pq-[tTWW;8Ӻ۵~z=Ӻ۴vZce˖wծ]or_+((Ptt~Y,8mݺUs̑ta/))IO>,Y"IҦM#X ?^MMM2d{n .Iȑ#-N؊o})*!!AYYYmu5m4Ò^fͲ8Y;zN<>..N999|>н uvvvrw\z-J<JJJLj߿_VG }*++UZZGjllO<͟?xgٲeVzzNnV^B'OJҀOBDH(**ҡCT__oukѢEQTTqQvv֬Y#IҡCa kڼyl٢'Y%%%JJJb<=ZNS~ەhQW\\m۶iǎ뭎ѡo]aaa S]]֭[0n#1c(##؄ fQtR-[L|Xƍ(Q`;s挆 Xs:x<% JLL|֞={| P,LiiϟlM:Uk׮UOO.\huSTT-[_WlloJ\\-Nxbcc/?9tP5}xbM6Mk֬C={jƍڸqٳzj%''kĉ{NN>?ѣjnnȑ#=Ӻ曕+V())w0 /xީSz$ :Z7]h1oFFFzӽ7n:R.Zț썊xާzkucǎ9|z<+.1c4bb(!b(!b(!b(! (ܹSC_|%߿V&Ln߾]v?e(URRwlڴiOgI,?Ͻ{͛7_dp) D9ku\k,Xu֙ . ,X@uuuzp8p8tرK7mڤ#Fh۶m?~bbb̙3o]wuz'm{{{d;VCUNNv_3UWWVTT؁t]w)66VÇɓo>gϞ}ȑ#\0xuaeff?$)>>^ǎ3ghݺu֩Stӈ#E} ~XT\\UWW+))I[nսޫo0Ӯ]裏+,,TVV*++t:ܬpדrk.tM\ (11קJ_z+]Æ SFF.رC?TUU6%%%I,Y۷Jk֬?~wEmmmZt%i򘔔Ǐ_WEbbbf\.RRR4l0cv+--}z{{5jԨ~g-JRiiW^Q~~|Kfu̙A`(ֿ/J۱>}ZNSMMM\?=Z߱+WG՛ozKeee};_{ DDD\VVVn:::tw\ZZZ.94-^XsUUU;wNGQVVaJq0={رc4iii*,,Լy?QG޽{U^^7|󲯛9s}Ϟ=bܹSǏ׻ᆱFM0wNCC"##kHv@gɒ%r:P|| {着*͛7OO>Ə9s樱Qɗ}MaaUt:gi޼yJKKC=YfiժUWaabbb WzV@tRuww륗^vvvjڷoRSSA: ੧Ҹqh9رcZ~=e1 @ @ @ @Vn-IENDB`eec6d110849db6dfdbbfbec6ae2510ccaff67940.paxheader00006660000000000000000000000265151624126450021240xustar00rootroot00000000000000181 path=qlustered-deepdiff-41c7265/docs/_static/benchmark_deeply_nested_a__3.8__ignore_order=True__cache_size=5000__cache_tuning_sample_size=0__cutoff_intersection_for_pairs=1.png eec6d110849db6dfdbbfbec6ae2510ccaff67940.data000066400000000000000000000520641516241264500201020ustar00rootroot00000000000000PNG  IHDR5sBIT|d pHYsaa?i8tEXtSoftwarematplotlib version3.2.1, http://matplotlib.org/: IDATxy|T$!Ũ D@(""VQpmi~**\Z]D,b] +jXEcPdq!XDAd~|ɐ ٙ}^{n5b]eQeQeQeQeQeQeQeQeQeQeQeQeQeQeQeQeQeQeQeQeQeQeQeQeQeQeQe. \.m۶M!KTVVCj(ߵl۶M^xak.#(!##C]. 5f̘I8zWxzp^ԅ^=Bʄ۾YYYٳOt:=WxzWx ͏oEw2_^W]ueXzjgNSwu deggG?8uÇkҥvnIUUUV.,`uu?|-[ώ=?Pׇ~_|QEEE4i~ӧOZ~n@} AN0A&LhTxmӟ /P_|z]vi͚5tHyM8Q X2Wyy,v*Iڸqv 4vXhͺkfzEE$z򸸸 Sq +pK(_P:²pkߞ={o{^9RG֭[=֮]:1"TXFપ*ݻ׳^\\,ͦ4eeeW_Umm繾4k?~nf=cr:5kMF`2nٲE_~g3f?OIҐ!Cѣ%I>f͚1c(&&FSNҥK QXѣGv7yKKKKӪU|Y@Xg< @!D @!D @!D @!D @!D @!D @!D @!D @!D @!D @!D @ /^Ç+%%E=zɓUTTѣeX'Au@[N3gԦMTPP ө+R^|:xgyTq. Yk}ʕѣnݪK/ԳSN tyB J,M_W^^.IJKK*==]ԼytQ+~}ok4{l5Jl>}(;;[۷o]wݥ"(@Tڹsrrr< ~̙3cmذk-y?h eeei̘1ڷo>l#@RRRԥK6?k,Z~z#F$ݻnnnvKzl6IRVV @ fΜUV_VJJJKK%IJJJҾ}j*M8Qݻw5g]z=^Rrd4ս,)6{9u@}>\}^][G)?_N+nݤ?vv2wKo!:d#zX~vKGJRUU˯ØyzJfIHhΝ,>^ZMm V>uuN֛ʱJLkrՃ H#Қ5۵Kcb#Ə5:,LС~y%4HRzzO0¹\&%տ6U_/˪#udgvm[gQptJ]WziHGh ~zw֭[uAKgƷ--X`Z,N^{Cݦ%2?Z7r +k\gH΋N|vN\_L.{\k#gI.YzYruϐfI.g\'j-uѪ*|Xk[-ƯmNg^{Mx⢠  &LЄ vkɒ%{tWKyeddh՚6mvڥ5k>\ IzG4qDWvvvKӦOl-86[ۧ8p%^'WjcGٶKO>6V5]{qêO>c䰝<Ʊc:x-Zy?y7H:rɭ15-kLZ9~y\jdq Ycܲ&ZeMS\rdM$kJcd[V UVɥN:u:xH=pѣfX}@ ؒbjرm1b6nܨiӦiƍڵ'Iرc͛7kiC5*++%I.K}2 fŤ˵kyJ*zuTY)ˉTRbi𙪪di巵;)dhLNa8٬''{q׿?$+>Ji׫j1vѣ&(?.XBfhڼųor"׉e1J$%(pmMHp+!y,!A֭NuuG$YsZ s+60urq)|/")|,EE-KQT o;.N:l=WsΕsOg? mi%lmn@UWQYnb<3SSsz%,6aZnyn_7_wKZߠ 7{w`;ZUTYVf2rrPqT=##YiizjUZZg,^X ,8e{aaO&4@qzwO06VJM5K{݊=~\c{올ǎZSklcZZzg55bcJJ2KbD9;uұtUge:+KUYY:֣>lap,r8bpXpĪ&ֳ~}jjכqs,mZ,n*.NuZN7?;Ԫs:uvr?|m\\'Zԯ(x2PϋTwTΌnWCT9bmrmԀ8i 8y7(e~u޿_)%%gT;Yұ4U䨪gOUi^srTӽ{ӿdnY\.YkáXC֚[ڧ~,mj+!A ML4eMHWuur%&6=]=>|mB$R[:Yjk=K.WYOop\|f耉4սZ掉[,]T99_X]Vi޼y;wgDyyy3fr|t:UPPq).ºQYUWkb㞕!NRyҴ8ԷBXTlUU'Ꙛ*KEE3NJVulUgꬳU}ƙN٪i>#Ӱaj0 :zTr:$繬$)9٭NLG~r[ben{QҋBYNi_8UW҆>wLFNu<9{w>'m5JNz_NVI EjGpngÜnY_^2wNNM|]%m:`Ĵǰ4xٵך|ħ G333%Ieeel/++Ӑ!C<|W^_rta7%!!A 'W)IL롿Q**O?UС vۍ 5eǩ{a_vJѣSoJ*9`Q:騒uXXk~XIꕓKߛ6-)IiCcM])[?f۠A4~,Fr/Ԋ_ݺI#F!K3inYv6=iC6K-Sq+BNLLЉeәVkşv?\eff***yfvm#Gȑ#ںu &IZv4?7K}@k9prWqCGfH73XJJ5dՀPCeٻ w}'fZ3ϔ5Nr1gi>G_}e[[ʩҸq㏛{ vj5??#}@UUUڻwgX6MiiiݻfϞEo߾a`=c0@Ǐ7߬{LNSfҴiCnҁFITRJ,OWUu֞\ѣR8DCG?LEԹtfA%Iv7ߔӓ5 _ X].ӲW?D֭f%z墋7@ ml٢/ܳ^\ތ3rJy睪-ܢ#GK.њ5kYfi̘1.]%\9]ˁރZ,RFILVbzk)5YbcMB80ᰤm݂L/gfFprЛbh…Zpa1s[Z u%%'Bl&Y6z%3S5N˓V 1c?k_ m4km9DŽƎǥw=G sM+߰a! , :ƻծ&[2N]NrָZi?Z %%ޭͅn+7?tkkךҙ&}i=<=V믽.'n5`û{`[?z6KpXW3kJOkn~d;,3}}k]Hv  WbLzݺIFi}~]زh CPZti^{m&NRù]`W@ImơCaEZ: ȉ@6쳃] QۥA@D`<@ v?Bst@ {:$ n7SR] @B v1@ ¿JJÇi _@  nvzv% f3K+'_ ?Ҟ=@B c@H!v)6V:`W l6)11ؕ::iv !(.xb >\)))ѣ&O"}jjj4sLu];wԩSUVV[GluȐiZnfΜM6@NSW^y=̙3G^x[NД)SXuˬ.n22@[fʕ+գGmݺU^zSOiժU+$I+VЀi&]tE(E?P咤4I֭[t:5vX>W޽qƠ q,կٳ5j( 8PTZZxukߌ E}<iyyyJMM,/nkfΜ;v @3+-sNxZ֬YzW~zӳ=33SǏב#GZʔ}@.%$HJhVJJtY n[fK/k*77aÆ)..NmEEE/4rH~E |n7Z3gjժUz嗕y/55UIIIJMMM7ݤs*--M]t߮#Gd`x1/_.I=z+V믗$=CԩSp8G}4rI,͘J >Zl-[N·$ 0 |n7, |fu v%@3- !)+3 Fw!MY:`WZ@ +hbw?8C:T9RJL9 ]`W@XxYᇥ-[ );[JJ3ot鮻kM5?rss>[?5ֽޫ,%%%iرڳgOc55<@ *-]*]i;xPںUڰAڹS^~Y.@zߞ?b[x-_\O?;$vmذA&L$Tcǎ|MjjF7fK}JvKkJ7;OĶTEEX׿O.I*--$eddx}]FFgRrt:}Z|}\٤AT&Z]h /\rIґ#~&}tE M(G7׳>UVΓfٳ3ft蘋/ւ N^XX-I~9Oݚe\sZ ^zWx uС` Iϥ3W^Ə7-7SnY F^z闿fΜٶh"vޭO?Tg}mۦ! :.\ve2d~SٸDyy4 IDATy*..VNNOw:*((иqc+o_V{`Wau 3\׫D/ճgϠՑ#Z%]vTR"enm>}i$%b[=FƪNLz`EE6oެnc&$$(!!^QQ!IZ~! ٿ@;wJÆI^+pK(_/54OYԯyc{K~pUW׿z;O۶mGx㍒$ŢٳgkѢE۷glM<9Շ]JK3?Mؓ뱱f@z ZGyDOS}W֭ު{׳ϝwީjr-:r.Y1f3'R%\)w+8/"6hɒ%ZdIX,-\P . `en7)6[z≓뙙_z>RYi&,d 8׳{G#J lcKV4`@+ Q 4b?KJw!=%M.5}8ͼ8-IIY /{δ gKgVIH}JӦIÇݺtA{yi i:)?_|{~`!vu ۴if9s 33Ȳew?7Ґ!ԻoժX%vy0\?vԷo+DDyRsm/w ۿtf*v:rפPַ̣DD?.I:h:sCKTՠARϞRf~޽;I|{~:vNZ8 &!iͼСfSSWx@HO&O9٤\^ gaq0GD$5L#v g%rH))) dgJǛnA1C-I7ސ*+pV={{ ~̈]Y,PR"͚%M,?_zy3?~;Rq6- uk֘tӥ9sM͛M{O*egKwq+_ںV).mIctѣ'o}K7ϴVUI4liCcߝ'xm;vH˖IKJ[ ÎR]ccN :lNG)SIO7mfc00nYtp8L74c"r:݋.j>I*]vRQ4bC]Mz途#cqq?&v"=dtғOJ#1w쐆H)>3tD몫Rvv,V^_/ⵌ?ǟ@ov3ҠAf$i`P9mg'iti\3+Avܩz{:n䬳Rzzݫ1cƴ(͟o5l۰t8t<9k ƙbpdKRIIIQ.]|~믿VVVVG?:m$3sL_xOa\\+ પw^zqql6Ҕ hԩԾ}twsQ~~~)/6?thMeҲe~/ (e :TC$͝;WCսޫXm߾]&MR~tM7iذazw|Ks~:o3c[V:UJMJ);[JL̀akfP%\.3S=z-t}O ׯ? J6xMuI/`㏛^-' {9h={ZS͛:9%܀!0&$xuf\:v sef3@f07p \RS͂gKzIiiWf>xAjVi i׮SgXL(* ZfK7*X7('=(!yԧ5Jqqq;wuҥKciJNNV~~jjjXy!6f_[MoONbcrx@zҊ+qbjرIMMՈ#qFdARL6t7S? N o#GLח_BZn)?S-_\sկ~+};3fTus8r8JIY>^\p{=϶;C|6nܨ{OFҁ{,r_ ,8eO>t bN}g4}tl`:tH?_gϞ.G))fޅ@>q\v4{O3ȨQTTTO>QsssBO૨͛um5ȳ%$$x+NtnZ~! _;%E֡Cv!\ /|Ј?͓6l07cߟ3Nߙo|hΜ9F?.IX,={-Z}*77WWvv&OZR}[tufib 8eJ˥?6;ÇK/yi…Ւ%K4}t>wyu-ȑ#Kf%&&d3P\s&6w{W/iTit~wE .… }~vyG_Ǐ0xfp Ll0370p 'HgeH~7mیfMN͛g~)c&F/%%-fҫW+ ^mEyOvyҾ}9g`Z/6S[IM@N}GSWWWq``Qi틍5IW_h'ۿHȑ9g`n=*urr=>䮪j:"x{ o~#M``p6{q}%pA׏rXVΡ돑>}d!\boL o!}[ƍf{x{[f׿\Ph >he?g}jT> ivԿw?tQ?0B @׮f2A7 "a.XZ\ҀfI۷K\Jxnԧ4c4th`dq*=03 Jx/=+x4}ԭqvi?Jiր0sܹK/I={JӦI=w``Y9Veٌ@$&J]'J;vy2Ə9srwԜۥlrǍv~񋦟󥚚S?;vL> H)hA7$\߾҇n_~iF`:sR~7wK˖wRЌÇO-DVbz?'GK```FۤyLgoB`FF@JA3o7@o?cn6vJf^3޽70}:MJH0z+xL ݺIÇh. e܁G)}1%L_t QDn 0yԵk+@O_fʔ⋾??0ڹmFjjpOvC bEp3n7@Ԡ0f&3 v%D]zy/l }G `/1( KJ7 edH۶I^(u.}4aohgAK<"Kw)Hw!hv옙/.ؼOJ*+o?$F?h 23Ç޽Mb3Z?n80ؕB?n̑ƍ}ksNzG3MW)ؕܐ3Mޓ&Mn?$F3 bb6,~=vK۷;uI#GJ%%f_*m矛Tozo&9f{yhU?_^W]ueXzjn^eee)))Icǎ՞={|ExB;}("f2v%jZlY?Zt{1m޼YWMMM_T$]zSS#GN $Z?%ؕ&L vds=%I<222zjMkCoLi^3o uV4Z=QRQ];TcǎlKMMՈ#q6曥Lڼٴ8 =KڄhTQaf&X^^}ݧ￿](--$eddxm|֚_Ҍ8ft`6#F+vܩzBBBPX~ s+J˓:w3}[n7݌ v%MJJtY:333%Ieee^"c(6=zn)ʕ+%IE .Tiijjjo_~w~i0~@wΜ >Пg }x -Zȳ\O=VZ+Bb 0@6mE]nrr`W@Ts7-TS#'Rr~/sGt ̙3|ktnIںuNwm;llf̐z0sJ]'eg\_!b[{9}NTڵFv8^TVVJ\.N*7Zv]w||hzWx r]$3KDd/Liŋkl/,,TnXp>Ӂ^qq/^^%סC]BYn;Eիu5(9X,믿cojӧfϞ9͌ظDyyy*..JN 4n8䘖ke?^Ώ>='DŽzWx URR\}ٳg l3f>#m7pﯻKzR\\ 5uTIRQQGNHH&BdZCc!%%)n,|zw\ /|ֈ?)))8p׶duݳnܹs.]oȑ##A?\Dx衇Sp(??_>h/] $joe˖iٲe)(Ўvn5ؕ q]5l6hJ<}n>[JI v% n/Dn7xAI@ vJ"F]JM v% AK+! l6n`!`۱ 3dH+!v)6V:`WB0lҹJ`c0I۷BdRU- 01h0lRRff+!v F2z&#Ց#) HeKfn] 1HE `$r;IHTT$9@$`$0h0RRn `$h"F"z#MYY v% D# @@+f:w:+ؕE4v4håM#%D:V#IM{7@@dN@"`$$< `$ۥsΑ] aHbh0RmB_|!9B"F /~Y,.X]|nҤ=] ӛoYZ;Bw8?%ؕqV233]p 8RlO٣luY>}`tZRڷ@;TVV³8&1bV\5kh*..ַmUVVb!F>2@)55ճ,^&LkVV~~^{59rD?|+ /Õ=zh***ڧF3gTչsgM:UeeeA4* J;wTyyg7o^k׮ׯ 'bu4sLmڴIr:+U]]gΜ9zW /hݺu:pLĪ;f8zIDAT3/!!ؕ6RRRԥKϒߣUUUڷo\Dl/5kx\R=z֭[u饗\O=VZ+Bb 0@6mE];@ꪫԧO8p@wbcc إuX%Iiii[tjر}޽{kƍM@hß.KNӧG]B+pK8\/ծ߯ugK.ѦMtgBXWWٳgkԨQ8p$Tڵ׾*--m8/ւ N^XXt.ϓKJ4Qmv8^K h֮B +pK(_Ck{OOT3gjǎڰaig޼y;wgDyyy3frrrNL/NS7n ? wm^ \ /pJJJ]BE|5k^}U_^=LǏȑ#^eee͎hEE$3:~Z>KYY>^/WxzP^>/Dl/`ۭYf饗^ڵkaB϶"}9rd8:vJO_K%bnuD-[L˖- @E~`W h}0*RbԷo+avi 30@=@f @ h'`ڽ[r: ᪾`ۥ\)55ؕ0C W6@ÑM`apttt0w2$uD G6ԥt!`8/K+aN0;&} #;:Z@Í&Hn] Spc8-pRWG`pϤJ 8-pR?`8ۥt)++ؕ0F '@`8ٸ w1MRhEС([An$Ei[,6KQcu񲹉K4Y 802&\ER/… ]v{:1f1-uttt+Wyyy!##ǏyL{{;-oܸr L&ݾ sDtqצ~ى9/yy˳x¼t?>tP˺ZZߑd[[ZZ`6h5 .\`7b͚5SRR%# """鐗k#ɶ:bʕɱ6L8&LDlٲo… FDDDndueg;Dll,~zl޼PXX(uS*4i}q^,gr`/BH\G$"""+@""""a$"""@""""a___$%%ɓW_!::CqqR{f{naذa6l222s\P(>}z?'{;ׯ#;;!!!Pjwхז-[A!,, ˗/ݻw]lrz^T*gq9xb(l?vP*bӦMJ^ZJ'/{g)o.~wqy` ^dz֊QF4K/(-;v1a1uTQVV&jkkEii0 .N.OΫPjQXX(jkkw}'BBB](Dmm6Ehhظqg/ZRRx5'ޙd2 o߾Hpd^&I>L̟?ЅΝ;رcEGG"=WvvxgrrrDjjj椇C.сrdddXּǏb?ڵk1rHkIpd^| FXlذfUeˑy0qMM 1uTd&Kt{fh4uF .<~Irdf{wګȓ922|0 Hpd^555駟b\|K.Egg't:+b˖#DKK &N!L&x L3D,??zq>;w.vލ PtuuaȑO9s`ժUصkȆRlذ;v3gpAaݺuRG5t (J455Y755!881v'rdf=6oތ|?~|Ƥ;jFL6Ͳŋٿȇ߯@TZƍFttt@Rkf9sd^|ΝEpm,YVEI?uSTHHH@IIe %%%HNNyLrr~ˑMn:9r&LpET󊎎Fee% _Dzz: \_vJMM˗-E.]~ȼz.述pR_"Gz^jw^QUU%,Y"EccBs{ϲرc[l޼Y?^t: ;|R_-,RYw^Ue녿Xlx8|9r bt:555bR||"<<\T*(N8a3<#ϟo/ZVT*O"'&{f6zhׇNs}pw^,g~W$j;vX~0L.N-_̫SH+ҥKŵk$HN=B+9DDDD2HDDD$3,DDDD2HDDD$3,DDDD2HDDD$3,DDDD2HDDD$3,D4 BP|7f?9rO?{ yI&᭷޲ZKIIACC$;`Ֆ7)S.HFD  * P(.eee̙3|̂ m۶~LED`,DQ,X_~[nBBh޽{Ç#** ~~~5kڰo>DDD`ذax7mooGnn.F#)) ͤsײvYCEBBN>mipiTWW;DDR "֭[q%bڵ#Fh4ֆm۶Aף/2f̘@3gDjj*̙XlCaʔ)O>>χCѣtOoXȣ@Rى;wZ ֬Y~455aȐ!Azz:~g̙3(((@}}=BCC8r aߧβG}}=VXhYCCCQWWgπQрguvM ""C ZknnTVVl6CZ}vwܱzrrrh"߿xWz4hDDb$އ\@P\y9[nATռڵkVkyyyDQQ[t:z̘1òƈ#oDD>fHKK븪^ZZ˗/ǫK{.DD}ūDDD~hDKK^PY"++ Q[['Obƍ(**zq'OFYY;wl2ǎéS0n8˞'N@V#99)ىHD'77J1111b 0o<ۈq)?𘬬,;w/^(J_7oZ-fϞk֬sdeeiىJ!R "t+V͛7'ޖDEE3f Y@""'XjFݧF#vGD@""""@""""a$"""@""""a$"""@""""a$"""@""""a$"""@""""?Be|<IENDB`0aaf42844b31e99679932ff6f0fd310aa5dc4e97.paxheader00006660000000000000000000000264151624126450020536xustar00rootroot00000000000000180 path=qlustered-deepdiff-41c7265/docs/_static/benchmark_deeply_nested_a__3.8__ignore_order=True__cache_size=500__cache_tuning_sample_size=0__cutoff_intersection_for_pairs=1.png 0aaf42844b31e99679932ff6f0fd310aa5dc4e97.data000066400000000000000000000504201516241264500173730ustar00rootroot00000000000000PNG  IHDR5sBIT|d pHYsaa?i8tEXtSoftwarematplotlib version3.2.1, http://matplotlib.org/: IDATxy|TL&d#llJdYD XkKWPzA VT5۫EKʵZ)5RE(RP0,BL2?d ɜY_]IIIJIIѬYT^^oB2VTThZ|y*++hѢE/k߾}2eqӧO'|B_^[lٳ-v]X,zW4u&ٹsoСCѣ>SjΝ2d$iƍ4ikegg~g tnb(%%Em6xŸ$jjǎyU]]Yw\Oս{wY!ٙ @ĩUII $-"y=ܣJJJ$+==8ͦT7y-^x4t@W# ::N}ߗ֊+} jJӻᆱv!ө-[hъA5k.\о|F 6օC魷IRff;uɓ'9cccYONN$u]ݺuyݫ.@&}E5k`hߺ˾|VX~uoΝ;{1bJKKk.϶zK6l W!X^^x<ݻw+55UYYY}Z~jjj<*&&F}Մ tmiʕr:;wM`B2=u͘1C<^{5I_uoƌ#Iz4w\;VVUZl 8f57}ak6LMMڵk}Y-i@C0@C0@C0@C0@C0@C0@C0@C0@C0@C0@C0@C0@C0@C0@ChFAADkԩڷo1cƌbZ~-#4c͚3go߮B9N]{:n7|Yyոe@W mܸk}JOO׮]4zh:(33kz.n$zm甖~i… DZ@p8<뱱mkjkk5o<5Jlя~={*;;[۷O/io H^xffΜ9ڳgn}ٞWVVƎ/B_|+@ݫ]z[;w֯_-[[n;l0Iҁ"11QIII-vuwW^ѦM޽[zЌ9shڵzWbIRrr_hڵ4i:w?Xѣ5`׾q@fXB1sCV̙3Zt***Խ{w{ @m[ ݻk~o0 @!D @!D @!D @!D @ eM}>jڲefϞo `BY'NĉvtR{コ%Ik֬QFF֭[iӦO?ƍsN 2DkҤIol}/9Tqq<ے5l0m۶MӦMӶm۔ 'ժ;v{^ PvKRITU%%&JIIƫjj%1QMX[D[{++xxPcǠ҉ҿ%<(o>_֯rZWujoi.Z>~\sgi.K222gddx+==kfSjjTWWڳ^VV&Ir\r:>gO*:%:%˩SɓɓKKe9y8IYN;)I8P/7tJ˲w,{`q9KBj=tM)ұcԽ;V~4䎋 F,sշV*+/rYj9痮됞.!^|S?]9#7ѣW< }**di&--3ݡtmg/ĺS'SSNTSRTcС!n7ݗ_7{oտeѡCRy-,b{/cߟ]|,Xϖo[-ųz+UWY,-Ӧ:\.ߞ0]4SAA/^|"􋵰_ok+B1ee./Wt#aA\11r&$ș3PMJӟrm"+Kzޫ993m$YjjԱX+a%>įRѣ[,gBlOKUU\jbc劏,x So~2Ŗ*nWL#NN֙dU'':5UսzSRt&9Y5ўDi*+ߟ8!_WX䊋גr߯N+G=WEFJ/DzVi*K57kN{>wR?T}X=Z>^ 㥨&RSX]q'O6:u^;lIgʯIK{w4;&6.[QZTUV\\B助h{tϯ餣N*.MQ{;#UGi:RY3EQ_+5%X=b{Q9Ԛ㊮:[sniQMLOƻס_5R[+KM55z.}Sǹ-T%>{MnU _-vQCǏBQLIRIIYΜ1չܹrO(wn\r*K5jrKMM]>UV&ݮ*#!A^NOt=XI/4$gmm}ϊj|t Y/eٹSqv) TVmJ^*С=K11*v>Dwߕn9tHbwKNV9p8]㊉QTr\KVQaD%efʝ%u*СRV\RV=;[Y/jΚT%:2?\GuFǏ NthWtЉw&xm'VyuF=uH9C2#]rVNQ9DQuHN%:Zٌ[;ή+1YJz^/;1QW nx;!ARV\l[5UU^ggp.s=6*T%\K:ꑝ-km,gkM}Ƙ1-->t/]Qffg:ڳazLV|0cB!:pgڽ{RSSգG͛7O>z-ZHٚ:u$o߾0an6\RNSsմikn/Y%ˍ7o/.>~yM# *͜ z ޘH`|i,cGIgC6n/vf0z2>jjK'NX;tUƭfn`h>{tBJ+*T{ZnuQ?䎿Y/wlj[~Er|kxs|{noEKtv9[7I W-:—di%)%IIH_W_}g3zj}ݪٳUZZ+R7nT\\k{9͝;WcǎjU~~-[T M̔&N44:**jKmTSqW [dQHSwʑݖ*{Y$ Vt[IIFgëC 5-.n[2}T֒od).qʸ>1Adg-KΝezfKpۚ[o}c.DMM=߿|cj=_cWz%*`p̘1r7bђ%Kdɒ&IMMڵkͨ^tF:sl/(T8nmpH&]`@ Vۃ4}upji[mtESYtW?]Z@mk,11Ɣi;/:gf֯7Զ]Ӌślpf.//6.Ϙ!u.11p C߾}|iX;`8s8nAܶsWU5̾MV@YFjj32GffՅakSm֣t/~!jTP w'rtu͟ڢ"#}![Wƌ1Fa|Z|1P5|6gpUB{@U IymzZr9׿(PdJW]e,?.#RR4`pYӥÍm[%3gv5z3Da>wSa1L<,c$]Ft& lOMbS`0s8 ^`(z|j}V<#ȑF/7ݿiiWK7lK.FNkBEY.Cխ7\[>}Tzs0]#oH}=|Cuvc^6l0P-M1Cqsf|"ע?ޘLT!!ץW_6m2F^~tҔ)Wx*7Ji3]]d!vGƥګ{L -uWz,ki@`ۍ]dn7Fp` +@n'F4 pEM + h0\9LE Gn7=Iptr@n7^ pD # h0@30^4z pdK RTTk01 4h0@3A Gv;@@)((СCtM:U:JsQΝ|-##.37o֜9s}vtkUEE_׋/͛7ѣX]lܸk}JOO׮]4zhv=ZvIҪUԷo_m߾]ÇDE`8"Т29R]]ݪ}Bjj$i׮]r:s饗Gڶm+pDEJNN,-~Mmm͛QF_~b(%%، Rp6 ڻwvYmk̙={h֭fVtpshD%]˹sjڲeuٞ3gΨԫD>p 88+=ܹs+護RNN+::ZEEEmÇ5bWU 7u=@|bΜ9Zv^}U%&&zKNNV||5k,-X@JJJwܡ#F`~Ԋ+$Icƌھj*͜9Scj*??_?~|I?״O˵|r?Ԩ0@ n@)`ۙ4n.f -XSSE)''G_k$}ݧ,+//O`}Z֊+OO??GyD?GyD˖-ʕ+cuQǏWUUUkN@Ђw_뮻NtE{I2z.]{W_$i͚5кu4mڴս]v pȑzꩧ矫O>裏uV=裒XyyyINNְaôm۶F`uu=eee$%םBk+-Umbj}\pEо}E+r<(tTY)u" $!řSfr8K׿% ;WAA/^|";0^:T\}6Rps C5k@VvC='wKRv1ɓ_ot{=}[v^xA=֮].Lwּy󔝝3f .Ԃ d=z.Ӈ~G}Tz$bh޼yzջw8IW IDATohѢEԩS\6ۍW !~[:}Z9Rɜr6>ZhKǎSvv~swBgViiJmܸQqf 1QZ*HÇK?ҤIһӥ7ߔ }atR-]c,,Y%Kf&"2~3cnjK&=۶IVt/i ֮ ܽ[ҰaL1 dH}v5k8LҿmNpbKM|V_,}Kn7zy)!xrIWKu;YSI]AG33?c@ '@Bq!0@+N DYӺneÉ.]|qkZaLcfLp ѷ1M7Ij#ߚ=@7ӧѣ!C+e&`8!R ~{o;^xAʒO+.jk# x^ŋo[zyҼ᢬̸Q#GzM5.wd^  u7 @Hxi*ifixG:.v@H6xRFd?;}_60\)=z]1 !Rx0ڏZ.vj:v tM@ ~|`ۍ)`,@` Qp<"}viG?B:q·s`p8 @ؼYz5ǥ xR\tT\,I3gJ{|.x !ec9~\ںU:tx.pZ4hXMVK @BRZ4upAD @JpAD 1 @ ;ZڱRY)ub qk5=ߖڿe'~'tA%>^:yzIgK?*@0AMi {w4)~ ]tRY1_K+I}HW)TVJ55@BБ#ܹ_b/tyeN:xxwc5ԫw7n4BY33x%rҤݻ;뿤l;>}槭Xnݱر' W\a<n\\>O>]9n۳GZ\ZLڵ+` @Bi\4ISߤ'JJm7/7z6o6\s_*a0^sRVqѴOmۤag+?{HOiim`_/ ;\)6cҌ>3_O`^1 mwl 6FƜh:Ӹ;|xOl6骫|WΝҾ}Ұa{"藪%QQ K/\$'K3gy.'NBKU@`-[hΖbѺuϜ9Sk0aB?ut)MG)TP`<Gy>@LSQQ[o 71&LЪU<]=GҒ%ƃ6~4tmv].履JJtDZᗪ% 8q&N1l}ָ#n׮gƳX C ,;f, }Y ~{^fwZt^]5k(##C֭Ӵi^6!vf̐>X,R("\^{MC э7ިt 4HO?gU\\<϶d 6L۶m Dۦ.2tR>һdzö/Ԋ+`Ν;uw*&&F3fPqq$)##222C8kmmh_sѾ} rVvcRz%c: X[[!C衇$ 4H{ʕ+5cƌ6@/>o{QQUߦ6Ϯ]ʉ6l0pR}h_sѾ}=~xnرGc\;JQQ\m}K/$I̔$x=D_~y\p,XY?rrss5vXg Taaƍ&n$k.4iOwm_ k.\}9r3={HI6˔)/?0%Ÿ\QQRNI"G}ymճgOIƀLyЎ;t7zXYgf3Cʤ~HQv}E5W f ۶?3kZ^Zj }.fngEΟ?_#GC=zSO驧$Y,͛7O>z&;;[SNY=LsiwaLht'^uU^"}W^yE .Ԓ%KKjcnUTTh*--ՕW^7w4Ÿ,@J7w~MX,Zd,Ynzt-@pqWfp@z`@BX>…֭RNߗtJҰaIh"<ҞyFJH6o6,P7v]{T%iLJR~4}:!-N 9c/6&G1U~)&MM dUVSKzۺv~sߗi)}_smbÚnY ۴Ij8]^?Sz?㍡->*=n~peI_|aN,(0fKNI>(=_kuKohIoe cR|آ7dug#)?++{bbG^Cĉ޽%ww=^@_Duqիmft0 BҕWJwK$7ߔBڶX7`O{o̔ugL @X󣒙/q ,`U76%55/?𭷤sɠϝnFVFKu†0([S1C4ȿe'.]jXXIN~c@B{I>kɑnU>]3GI&4k]RB d b\iWnݤiӤBsO,)1͘@ȋnI**11atye'vj|GMc)+/U +@_O7N3鮻̽?p$i"}OK/}~JX!ΜgN޽>0M|1)đr/K}-cgI˗KT%lS&DcO?83z32~p1Y21~32RQ^n2 !)cկ˿rJD76wջ:#xہ+OI:ņ !Wڈn]b0X]b? S@Y7c_~C_PE dZ0T@vz / /HKgxߗG`lt-RFҷ-u,}4q9eCU40 =SK11wKҝw?@ȑxx)S&0F )(\)=]}(s"C.Y,Rbbk@ث|F?#ZlV\;vcǎ?~Zu}ѣߞ,McH(ۍg%`'jbdsZt^]5k(##C֭ӴV̔.{֭R^}Hneeer8 pA+//ϳ-99YÆ ӶmZun~SiѣsI?t\V0h\_<X=##ó%?1رReq986wqAi5`(r8 @;ݻW]v_Jwe\ ./rsictq 8@%&&*))ɳ%fffJJJJxVLmc4J99\V!" A!''G***ls8ڱcFVWK JCH#GJuˬZe3 lodX4o<϶*͙3G;wVBBKAߔkڽ{$cݻuaOxknfeggkԩ͞+ѿtF{Qc===;w{ 0ko_TrrΝnA?T @o}]}՞ Hf̘իWVEEfϞR]yڸq=/JkHSH{HH.G}f X^^ӧ駟փ>njڵk$IVR߾}}v >.34پ \h_sC\PM1f ?>ܹ}ŊQJJ)((ŋ^TTW^3g4/6Rf$9}[h_sѾ d?~<`e7vK3g=TU%'RǎǽW_? [~!.\/IGQnnƎ5/8NjܸqndU&Hd'h_sѾ} {ȑ{3oeeܵk;+³F[lO>|9{n7^(,`bb絭cǎܹgY`*))IwqF#% h cj*??_?~|@We@N7m˗k偩P[x\Xq8ɂڀjx h'`!v" h'`!v" h'`!v"9@C =ډJNi h`({00Ca@;CI] v .  `(ۥXch#`(ahPB>@ %0LڍJ |J|J|*jjrFv#2@NPa@NPA>B @#PA>B @#PpHQQR||kB0TMm& CO>B @#PA>B @#PA>B Z0@ !  `(pK@PP^.OCnx@kK]vhx%]v{mЎP]HQ40fSfff3\\ ߯lKӧOÇ]v XPPC*11Q:uuLUU̙Ν;+!!A*)) PAp8<nng ۼݻW]vƶRRRԧO8p.beTIҮ]t:9K/U=m۶ԱI<KLLTRRgim,//_|,khlVӨQԯ_?IRqqbbbulFF=Ouuu].NO\w>)kiIIrHְ}{h_sѾ u\t~3M-#5l@W'о}E+ȑ#t?oRM'lwܡW^yE6mRNN+::ZEEEϗ$۷Oֈ#=gllOl}e-+dEGGDh_sѾ dS<|!/QVIDATl[`Μ9Zv^}U%&&zKNNV||5k,-X@JJJwܡ#F`#l+$Icƌھj*͜9Scj*??_?~|I?״ |(ln񘸸8-_\˗/Cځ|(b Ynp]UtO`;z0Y5HB v#; 1`++3^ |,vԱ0A v<0c g00A{=AtRUE>E bѕ !` A40AK  f0=  ]Y)w\0B b ziF# A,KAVY%`s F 00EWTM>F b\f 1.3UMlUUL||vKLB R0XLB Vu@Ap6*J tU@!+]$%5av:tt-@"+CΎ] ARw 000Xq ,v\C`E 0I˗뢋.R\\ {/U20 A%h3CDt |j:vX`+V[k A(࣏>nMrrssrJuA[rYn.$63LJ#/` f+H`aX΅i25XՆԊK# p\_ P?F G~zp׹~Ë?Hr 455a߾}1777$''~94߿v~͛7.?"ܼ ߞs`jj a}u,ױX_ǚqooo󸇇<<<3wma4h,5 ~px||CrġCވhEDDX/((3wmo>߼y^MExx8:::5M:X& }}}ǢE ʹPɶR f3pBBC,_b{Xc:|oHHձ l?R*dBUU$̌擅3vrssG}1118tK# gPݎ2___;8ptg/RD'a}u,ױX_rV !: """"rپH @""""aHDDD$3l%R\\Pj… w/zjj<8}2u]Ը Vi&APɓwc0#> JJJaڹP(000ऌ]KQQ{1xyyaٲeHMMEWW]q =+ 6ϑ477cڵHIIЌ=oߎ^x---HMMEjj*.]]5ooooNuaڵ(..*ʕ+ظq#ڊ޽gΜqpeq.[AdeegϞx :k/W.&&FdeeF$f߶mظqXllx饗+z^8+(//c̞={Ě5k,DJJ#S[ouu ^꤬!@5~ԗ4d7n@SScnnnHNNF}}s- %%expuXxt<@_uuuR2]Xt1>nl?{Wl%RDLLhhh0?n:i_V+T*XfprƮǖc5xꩧDssYvۭzfffuM)T*x^wzޮ{bժUBVKDq9iw395~ԗ4BF""""HDDD$3ld ̰$"""6DDDD2Hf @""""aHD `B-WUU!,, F񮱕drBfDDӱ$"xGك7|J6l;?̈cHD JB@@ _زesvڅ#G80+"ٱ$"k.P(P(v 8ut:<==uVO>Ahh(^+m'''˗{All, s*++Vc?#oooDGGM؈-I->x뭷;>>#G xgyfӸ|2lقс26l@[[zs:C6D$ wd ̰$"""6DDDD2Hf @""""aHDDD$3ld`%iIENDB`8083737c9226b39c0b16c4ddab0959660309545f.paxheader00006660000000000000000000000266151624126450020155xustar00rootroot00000000000000182 path=qlustered-deepdiff-41c7265/docs/_static/benchmark_deeply_nested_a__3.8__ignore_order=True__cache_size=500__cache_tuning_sample_size=500__cutoff_intersection_for_pairs=1.png 8083737c9226b39c0b16c4ddab0959660309545f.data000066400000000000000000000462631516241264500170220ustar00rootroot00000000000000PNG  IHDR5sBIT|d pHYsaa?i8tEXtSoftwarematplotlib version3.2.1, http://matplotlib.org/: IDATx{|޲ AJ+ VQ@.rlh9ڀ~-VT͑*U#[VcRDRP&4 B6{C, f~sۙgI—gfa!؆3@bl`3@! f6C l`3@! f6C l`3@! f6C l`3@! f6C l`3@! f6C l`3@! f6C l`3@! f,׬Y &XC+Vl uggϞ*..ַm}QسgLiڴijjjJGH8Kf{m߾}ڴiϟM6^жmt5D7e/5khI0 Hv!Ћ/'vzK^xjTZZzKC $\RǏ.; wQAANKfil'Fnwr8˓$[Nyyy'IFԆ o|~ƍuWƷ .|M :4H---u 7(''GT]]}Fv\ ,~ѢEϏm@\ٳGsUAAA4io0 -Y丏7o<͙3'k.W~UVVjx<1=6:z3I:۵kΝk۷6z#TXXݻwG g^W^7 l=I_t uf=ԙPg֓JufKo߮^{M{>|piƍuᰆ $%#pSSvY޹s6oެ|ߴi& B󕑑jر[+hƌ@Y21BG꾰+]k,%=8< l`3@! f6C l`3@! f6C l`3@! f6C \CUvv'j۶mQ1B#jV^2_^ ꪫ-ܢ/22-Z(I%>:w V\l2W7ne]YߣG&xB F544D&ߥK?ӧ yi߾}1/sl4jw}a͚5K\r Y:U\\{O~mۦ^x!E?n@`K[nU~"^)++Ӗ-[vڨӧO̟}***ȑ#GN]cl);;[999]ƌz嗵ftIGwذa;v0 ͜9S/VZg͒x[GPVV˗륗^Rvv%IG}˗kݻ{=͞=[]v9$s@#Xd$.]nIz״xb5774i$kG`_WNPib~l`3@! f6C l`3@! f,׬Y &XC+VnN)++KFۣٳgLiڴijjjJH Kf{tE׆ ԳgO3F---}L_z嗵fM>=Q i.@w7NƍtaZx]{zJZb&O>@+W[o!CHy?^?T\\HmGOd;ҶCׅR($|  oUIf)ԴO2!=dʈ^dHGLPȩۿr3fH{ǴHEܹS5jTd]nn uiZn"OF%ө 6FFIR0T h;^+Ð7%Kr%K-9k{u:c#&a_mۂA)3SFRϞRkϞu ì={{娫38.2` ԙq:~p )+wУ_ܧwQtUjh0z9;Wcr|c3Y(h)oտmSf ڼ9`С}m--Ҿ}p௿ßɓ9o`0ZPjIRAAAȶj7jV~~~dΔkWUUO>[NUVVx9|+f͊1%)tpdtz` "CPH@@@@@@`0j[_ۭ׫׫Pf9i.{ o{=G/+햧Y&yqyaRS @^ZO:IaGpXPHpXC_A9moڶrrrxuGm8 edDCPFt C2PX|d26^ ˩TX.S, ^cBr*lD p?VT({PI y3PO.FXPX2 9##a9 m0nȐ # Z%swq`ہeÐ6KXց0ӱow+p)>GWwdiz>Z  #i2]zpqq*RwX<'6|0r{ =^ :&+3<#++#+0GrCrrrn+s_Tz4C0R(B9=P 朠@n99j͕?;[jV8##8g~{>sjimN޳3x!G=R0YY `s>A>0!6dz 4o<͙3'k.jȑׯ_LTYYѣGs PGxUIRp*}D];pBGp6Gq:/4E:d[1[.Uddh72*a}jvK--r'57M ^r57˵o_dܾZ[+Gs}SF ǁMLW/W/zIL<)?\6'yz=y=u92BN[Cj zb^Jj"]k7 prfC_;[}:MN!Ѐtnk;DZ+hպ˕݂Wu>}"^y]8~67?NRFGm  )7+:ix9Wg~T l?yN=n ;1KoDmذAz$i᪫ƍ5x`I믿p8aÆ% zwhx'~~%] (̯Щopkwn@@f$y-,v*tdԤ;vDwܩ͛7+??_ ЬYpB~*))U\\'Jc[n?@ 3fh)p0y QW 4}[[ `,t6 G~yǜ|DW].ޫ?_  uY2ۺ+"mM:U˖-ܹsܬӧN^zV\v|5c 9RNS&M?ҙ?&LpkϞrQ!;*娹T@e>z#~pH6+ZeX!^,G!7E9s={O~~/_liȰvRgOWr0.&ϐ/*i$%`;D?WjSİIGMl.ϑ#jk nhv=6GsLZ9|+[# h/@ ֚@lh'>9@znnKh'm}:v$`'t D ^@@{%DO'٥IFPHmc^0h  @Z0??IG Oɑ22]d@hp.]@%I@@  8ha>}]v$ :tշo_M8Q۶mڧEeeeݻN8M4I555I*qիWLׯWee*577G={?ի_Kb̝ tʕ+-[}jƍT__'|R˗/וW^)IZt뢋.JF@;5_ zIR~~$iƍ 5jTd3J={&$RF򣾧L[l3<-vÑ2nݪ~E7c ZfN:B.F/x ht @ɉL ahƌz믫$jxTUUYm6}>|x\?Cwh@L˗/K/}}RnnM9s(??_9999sOK@{%MK,$1"jҥKuM7Iz!9NM4I~_cƌc=v|>j5auLUTT"%:~h\Iv)@ V@ALwALw@p`5_ `|>ڍM-m`(URR,vi{:s4 CwyQFiI,u|R^˕В%K裏>qk?OUݷGsuģά:Tڶ>r 馛^?z/['=4dcciȺ ?iwyGw^d/\w~_t8-vRiivܩ~Ŵ@@=z<O.*Rx,o=CgbUgHz3I:۵kJJJg餓NJJ^}U3k|'Hio>9ѷ8\.aIRII UUU ڰanNzz# $jc޽r+Wq:zRǟ?ɼ?ַ' &ӀtYgwу>|;$áYfi…:URR竸X'NLrc <%]CGyD}޽[w;3ܹsܬӧN^zV\$<UW'ҦMEI?4~}E:؟;m`vv/^ŋvá{GsOK@m7H9?ԩ(k[g:wӟb-m D )/.|ץa< ]sM|ΝAC@RXMt|~f0@׿sn`:N= RC8,\].8~>ָ_m5 J˖ImcKp,.@0@/.JM}jk O>I޹0:A `:Jv)@'zk}۱?70|$)妛@n8@ 0)l@/o v8~ )?Iڿ_2ii!&+F &K_~)vsRQ4eَ/tU[kHyYY~ H^(=y1/^@,a.O&O5/sH"Ҟ{NZTZZ3Fϥ8^<);;%<ml~sU[0GK:1` X ){@)`3]Ϥ='+ )ks(E>踽^z]{ +O'٥XZG͓z4̔oIڲKtE )k̩VZV?q?ߜqVKLG94ק4qbb=h:ALG+t0$!_ڰ|e>G\JJZ_+H9wG \)+|XN=U>])|R^!oRرI'I7,M*s5Ht)_"56Vۥ;3ΐ*+^GXƮ]] IDATҌ~g#=s?^}s9׾fwf] Njk XD>rg!}RqtmһO[tmRih.n I6?vI^sۖ-REƍI+FZ"`)y xx䓥W_}Tv0]}ΗXQa^m>r}%(i3ۓ@ʛ9S**2/q;ҺuԳ~&}aΙD?,G51Bʺu"?iBh2nyD:|>}6XIL Qf70a.m>O' a5kh„ *..Њ+tMr8Qرct@{Eٳϧ[ҶmҰa#.OH?O4n8[N.Ibƍ{s8M3iR^ÇK?t )NZ#%Qwuc>ɓ#g}9viZjFva;-1̮^4=l3Jf80A5 >ԣG!@[_~uSOU>}cǎ.:Lq8bRJS_bj[/OHQ}%ɉL |>2Dzs&&TbZ}T2E3rZ[%CMMMڱcGdyΝڼy󕟟 hҤI*,,G}s+_ƌӥ?49%֮5 Gs@Lۺ+"s̑$M:UK,{ァ_תSqq*{]nQ|i>kΒP2/fq#Fh9q/͞ux=cJ}F#.KW:;g>!-_/M$O"K޽kJbv _"5Z ׬9x.UUﱉiӤoތOSRgH ԙPg֓JuV[["D9Rzt Srrp8!C$ڲeqM:[ǜ7o^d|ȤT#GT~bR6@@=z<]|Xƹy{kWǴ,:zRvڕ"Dկ{l 5 yyH\RIJ.JQQJKK 8P%IEy1^oԸ~ zN_0::CJά:T3;.[gv?wf@ՙG-|sNy%h۶mQOd>RXXHkhhІ t뭷Ƭ E'X̙fW$Bb~۵J"=HLٳu7Mz'OHf͚ OtS\\'Ƭ Er|>ią?)YA˥;bzȡC_o[ 4H{/^)SD;wfΜӧkСjjjʕ+DK%5.כ׿u}_?vá{GsOϝ>@|q4ovt؟30뿤aÒ]կNV6+|}}R ;]_4I2%.me%aQ)1x3C:Ts]~ҏs&)륟oʄ6XҼyPpVI;"5Jz31-oIYY->(=9b-0b.(zളΒ>(>LL `y9[}9\…ҁ1z#m0??_tc!1p62ݻrz[iRl|CpdCH_IÇ眉I I9932̋MMG^Z_7Nں0xc梶^ҲeţCh,K͛Os$Ei:s9 ~uo~sp9^]]:Q)?$!5 G~_>]`!.WצxHL K3fAD2biR%`,0ON??NL\']|C0X̛oJO>i>[R"};Ҕ)R^?wb.4v_uqcB̧ XƐ!/̑^|Q:$id2NL1;8XVftRUe9NرҞ=;gb`~':ޓRD>)wh;b%1pxi|%-@2Z[g5;ti&>3;讉y;^8|_5TQ!BO%Z");| _4p`K'p؜/+3uxCߍ9 VG tF?9y9=<@\YF&LPqqVX0 y***RVVF۷97å]uvm>E4B޽@⨹Y{***:ݾh"=zǵaScƌQKKKKcƘOէF{ǃ͸qpB}'2 ŋuwk9眣zJ_|EYPzq闿H@kc`jΝ֨Q"rss5l0[KضM첎ssX4t[cc"1$D/((l;Biǎ׮N=%@+k }$XPiirss#SyyyRq-~ m 9_HO?-ҭtcem0??nݪ~E^1PTSSwy]:Əl8roy95̙\.jklshpLKJJTXXȺmذAÇ1?5۲ _Wڿ%Z}wMMM&;wj׀4k,-\P~JJJ4|kĉt Ԝ-x+1!Z{uWD̙#I:u-[s窹YӧOW]].R\RGSzKs9'jٳ?Q?V^/B]w]JM>O`Q?/=I9W0(9suljjҔ)S_RzדO>|PW^yK7XcD A擿g[ZXVVZF… #7nܨ@ kghݺu袋:=$Q bZnO*pitZk:EB*FD* &U@7 -@tЪjk [V*55@Q@q Z@@"Zq ZQm9p^^K,hE>ԫr%$VD'8Њ8 m@+@&%n#ZM] V0p8-0&jjkW &|RϞכ"Z @D 8N@!D.`q!Z -8 J @pVoq!Z  ZI[)`pVB ֚@pVIn #ZI[0GK,h%bh%@@+7bh%@pVB wwpDMgyfu\.($Yg^{-v[;BYvgJ n ]U001بU\\SO=USLѧ~^r :T۷&Nm۶EҢ2['p&M$( ͍L7l0-[L+WԒ%KsN}k_ScccK;i{ x*++СC DW]unݪ={JfϞ?Oz畛3f$+-֭[կ_ȲtqE9 6L'|{9M6-匇 +WZ^l7.S}}|I-_\W^y$iҥ8p֯_.(>^[Dvvrrr}yyy:3cǎ8*1%I7*hԨQ}RQQQmHpXf%\AI} T]]q~ m@LvWr`σ9ΐ3ά', ?5a|/t]wrnS L[lڵk8Z`AUUU3*++%IrNy啸Vgz3I:m>sp |:u饗j:TK8c ZfN:B.̛7Os̉,ڵK9rdMTYYѣGW|x=, R4pɒ%#FD_tn&IC=$өI&k̘1z\.D@0Off***TQQ0&M?V_/B@@+hbhm0N} {!ZA[ Z@ 瓲 8>@ bh>O!Z- VP[K1CZ@ b7' `ccT09h1FLu@cسGrd `|HvI@ ={ bt b STW[K )`sp 1`@c䨫#"0OS9C1DLa O"s@Ci$"HOnA@Z!F99Ǔ4BLa\1GLaF\1FLatb2x0e46@cH 9`2 qALUrB< b|>󕧀@S@0Ur 1`"8!(Ǟ= y$ SU%`#(ĉ`EEN9effjذaz7]$Ӟ= H)g՜9st]wiӦM:s5f޽;E0)$3C7:>[t7T?zIn~9 NvU7nԼy"NFuu/Y$}g +\uܒ>7 e<O쎍 'ԙePgCYO*ٗ_~)w<] IDAT]^W^ ζVPHQ vr-X/8.e#AE-u];ם̐lc޼y3gNd9 >PtjzccJKKuVes)3ά:Tp8O?TrFZҕm`>}rTSSFK.$.khh$/y:z3I:0`@NfHu}$##CVUUUd]8VUUĒT-4gM:UC х^ŋY7|sRHeݝh R^^>g?$=W䒙\.FuRuf=ԙPg֓uX9 0]$m+ l`3$)L 6Loug*33Sg}^yK-[L#jL`im͚50ap8bŊgժU z|E˖-AXmժU~Tb{+//СC}jĉڶmQǿiGLg}Vs]wݥM6s՘1c{N7t 7hڴizw4qDM8Q[lIpXMrrr_F ,577sUEEE߹sj]qڼyf͚Ы璢c6۶m]۷oJV^2_^ ꪫ|oZ0p^xQVVYBFqqQ^^7:jݰa{˥LZm~PC%TP xE$e!EIFY_aʂ,L#Xf*,KCS!gi?~x_۷>ɜjڣ1c߿_%$$ؿaRbhh  HDNdd$222ggg~>~sҐ"" M+iiix!N:LLa [nA@TbÆ ŋヹsb׮]fmc֬YXb***I!&&ϟ#** gƜ9sk֬AMM @DD0 YԩSx5pa;ZZZ#??:}}}X~=֭[ܾ}͈Gxx8hjjN^KӣGh6 @.ǽR#,Yd^"a$i Jc~aNNNX|9P^^Aբ ^^^LBѣ#>Okk6deeaٲe0bxBkkůџb$R4R'''.^hZ6;7oި300`v݋[ظq+}M ~ 2lır{7ϡWnnn1;tQRR;w '':֭3|:6"?HDӎBY*((F]]]XrE󚚚jjٳ[lV5_`0 ((h'"/ <}---TVHJJBJJ _wٳg8vJJJFJHOOGEEZ[[QUUjj_~2dn޼iBDDDdw.oݺGy/~۷oØ4i>'N@nݲmׯ_?;v شivލaÆ)9 P;wFΝs|L""""}-BӦMUضm<&M.\.]?Dpp͟dTrr24 QQQ0?h׮<<<~}tٳ8rʔ)+駟Çi&dff)S>>>S:uꄡCbٲet5jDDDMݞ5kjԨ֭[#99+VuЦM@xx8ԩh4o?3bcccAx1~xL2>>>vN.Y ̄aÆhذ!`ܸqhذ!&Oxlܸ/^D PbEe߾}}]kF۶mѥKj ˗/wS""""5k`h48tt:ڵkgڦvڨZ*#$ܱcGرcv>PJx^)S֭fȅɆ E|||LSZ ?%k +44Zt9sfb t廌d Qaf-ڿcǎM!##IIIjM3hzXHDDDnTR7] PvjqFdd'N ..aaad+tGhhyXHDDDp 0 }ZC qPLc C:t@hh(^x̙3 8q"Fo@"""|رqqqsd;kJ+SG$̟/MJ|ɸy :_Wì+Ѩ_NZ!_*m/+ "orH9)o D(:Zzq < QQk'>?sFh!cF<=sML 22j_,hpsT;z`:HM1w [KO?ut(~`"Ovt:P. #w@ߠ/V /?}]Oe˗߽`.Vr%/[bR_wHh_V9YCx8$*͟o4jL(uccn}͞=^6 HK]]Xb2ʕ2oΞK/wD1) D&:Z&gmPndfk8\}'.^x 㨁w-Aiu.Y҂dfBw/խ{o/g &kh,F]Hc1hy.^\nݻK30z 9N6nAkRlʌSL DEnc&9WCC3d;z@ҋiMRwOO n옂R܍qHVC|]a(۷!yM.nUν*U`εzڻW*_|4)AyڽI5k޻odF(>ص.x72HOڵ-ȝ\&ժq0%e==%rx QY/+v8gOy9/^\)[vj3?dΞ-YMzvMG9U,F@X Nȝk-lOȸzU:OLqtojx{!o_2ʭ[2D [uyتՃM(M֞`i6Νw±cyy%hbcQqHNe $#9gW֭()9݄Ԋ>46&.z O4/jwB=؃U*]$>к}""dDh۞,fz6ڶzaT߾nE1c*W{l_x8Ko 4n,ǖQYܯn]G8YCM*#O׮B۽􄺿fyV8] `Ge.'eK\_{ѦM>yx޻nsBZ[||<y-[ŋGz{mRsݻw'Dpp04 6lؐqbհDv-M>6^^Rv->IFƽI||8a8PF)oˀm;J-Ưn]H\Y?\Vf͚!** 4iĴMv?֭[xGxwjX"{iKJ{!C)+ٴI>s)M/JSsm[J?.vܙ ͚5CVpB|Wt钽}4tNzh׎Ar.5rR0st3=]j؞{0={) ;}OڕS11*U*xQs{җ+WcȤ6E$Vv-ZH]__3\[~L[سGܷoRqo Æ >oCCCjM3g渝`@F0c 4lÆ СCIlAx9֖_5l>})X,Vb.Oz:(+WB{yu/wUիKZy1m?t rw ֭+2Lu )iѢ2_7nȈшx͚N~~YF"w$͓rXٹ(\MBxƍ\qbPI)hJ.\g. ^Z5畒<}nΕܹ#ǻsGkֿWFFzεZd.Y"̤R5nl]\9/>`k\fFh̘fE+By*ScJxxyA߫yprx5n `$X_|C0,j0 [5O4`hS'u $Yfek+Lx\ óe7߄sAw/T.n\(Qy-y}ex:)P:yݒ%1v,CS>͚wJ/II'4G%* shz(fMzaҤBΈmDx} 3gt>;AA^XԀ 3%,_b߭M-Us2-[}'ODBBBHSKII1nsXXp!4nsN 4kj\E.̙31uDrlr̈/6RX2J>>0@yxCGF}pOr2* !TX>3fd/FHo2%/~(S&,a6+D6Gд)}70~o<8͛:pPܨY{O7;كGHOGzԭBBۣ~c̨-q2m݊##G"nVO##={OBՏ>ﯿ6駨 r?Xm^xb;{BDDEpz}$ڤj7oW-`3vXh3f3<`X~6W`̘16mj֬L4 ѣ1ԩXaԨQӧQU&ʙF?`:gΜA5cJкuk4h^{ 7z+V ׯϵ xٳ٪A!""۷wx|߹uvϡ?{Eϧӑiɇû~} jԐZաz3*WKMs߹i48s;w ukFrEkk'B=0 ÇKMX2 !Aj/_~Mt4#0DsZ~fe3%K'n\֊Z^손73SV /i"#չ32̓>\|9hbc)x kB<~f^ʓObIyX׾RR)e|6jT-̟$^= 1ݴ{Tz-[P7n(իi(ܱ޾ |3R?p~?L!9Y>}Æe_z\G*Ut@ɒ=*+lH.GCst)ȑƎ{;s'vG^^2^` 20#8 2ҲI]ɔ)2 /ȔDIMto~hEfj,t4N{ݻeEk̻Yq#[P&{RRMC4YF 頴Z%+_u'TvU z?psGP`]@O2qx߾5[Jbq% 'NロWĿl/  qM~}%TykO?^t2uϦM21&hd"e~frr} 䵱n# K:~ܢcP0t@w-5X/MLL5eӈ7>AV<\I{wayŋR'欛[z,ѳgޫ5ZDEIзfdUGUt_%Je~}Y]fT࣏d%}r~6YdܗȃV`zz\CUgɒoٷ\];;W7)ͿY  du$в%p,Z7Si4RkWεwK+ |~+ÇyU+JrV8Qj/ &`@V{*T0popk)7u~ es4S t,JR,3A`jH0*@ˋ/Z{B~]-WNZ hh HV/\(Ty9TJ1~zU)S%"{x=ɒeZLt+$2SJfسرCw{(t FgC (-M},/j_v7Z}] ƏyGf'"R&`)@WHDN*3G}{׻7{.9Bɦ] 9k׀9s5dTYpZ"@"r_~)>%}%#GcsctErHD6gLڜ[v/* RE2(_>e$&`XHD6om??M>``7gYWHD6 4^Rm ij[@JAr. n݀ƍp%"Wur23b+b$"+2lcc?@@WHDV< +QQaH`Ҥhv:d]Y04˫6Q>}XkE G6;7tEiiKAD.,<x}`,g]*||X芌5DDc0l\|ѥA[ٳooˤ aa  QW/];`b$!zv-0>@`  /.G@~@jM !A{^]q)Sh]j׮mz<-- #GDٲeQdI ]DPx <۰!`0p] :$5$?(4i_os⿿+b$" ݹ̀KD(5׭[;v0 jرؼy3֯_VQFgϞػw/ 33]vEPPۇ˗/ƌ3r=Y@ǎx L;gatE DdA?>D+;D.R?99+VuЦM@xx8ԩh4o?3bcccAx1~xL2>>>93web.6"@"ԩ8{pjj*RRRL\=uC_~:t:ڵ3m[vmTZQQQ(ԫWm:v숔;vr+%swn ܸa`tM Dd/8c 'go V5]fΜv͚5ʕ+m6,]gϞc=T$$$-x@BBBg|X^zCc@'ԑ}K 1୷]rGJ*n2nΝM?ׯ_͚5Cj7ߠx6-QQ Ijw1K04&###IIIjM}pl0Ω_aV[ʊ=[h*UY3y|l[7]k())υqZ9$uM?X"7n oooDFF?q _+W?BCC D6g }II/^}ڵk(_gb*Nj+ŋcŹnSZ5lٲcW |AA2XĤIŋFx2mɓQbE/^ڵéSXj3e 0r 3LsplK8{l,]-1{l̙3 .4m3g,X˖-燎;"--́%7 e?л 7ѥ!'k09"o>t]vT^_~iR ĉѽ{wի 6O>+{']e/:" +,իۮ[d*Zh˗ɓUٳs={ ٖnjh֬]$}GKd1oo ^^2tN4@z )))]6<==ӧ_~-,-˒mԻCoz=tVȸ;3z//6zn*x~mx^^աsg/df?GREo5{k6Z/\fp]թ#s><0x0Pu[d7|kbݺu[.91c 88 (>gΜS>pdd$c[YDDD۞wb 0ܒuڎNnY/̜rtvã ""p{UGlǎI??xa`_- Q* EVJ[e8ܴiӰf8s jԨ4hM֭ѠA̟?}_P={6Z֠W;8]$zn*x~m+-MNn`lݚ-[Ck??`ɒ49 Prew@ O}6<<r` ((cĈ97ی)))///;3SYt{ܒUZR[oy ::_'(o׮us EGK/.}W}+0'|ӧOGժUQn]`ܹر2ӹs@N+νe-b$*2;8zޢ>>@͚2Ð!@@h(Р,OJZXU<徍FX1 {:'J] TtH,s޹s/b\  Kx0Aի3оKEd;ZLR~玣KBYy 9@"t&0y2PO>bbj+pػ]  K1d_Z91SaG rKJא9sd@-[5]3lv5XYȮnܐi}V^ѥ"{5l,\(]Gd$pb+ (7IK?3eӧeG˖.c puի^r,WhP.het5iil%r& ޲69,\9YZ095k?۶utidIib sat5 DN߁_ |bVg@gjTBS#˖.q2hr 5DN)= ~DYq98cA 2m|ybb4@"0j4;DDE _zOܻ?'YF_PC2+K/J٥H.,] |@. apuT $}T)lܭ̀Z*SU_(N*葑2U~D͋ʯ*A]"&‹ozo22 ?]gezHm 2;a$r{?.ӾQ\xcGHoZ_.'e}>6oPNJ DP@ko/NO %ѥ(:5Gnޔ'K=۱cqmG 9.5Dwnu:˖utV $%9O&.]jdEDYvZ5i0cRӷk ki>!Eq9 DK͛%"r~Z .=XQVkbb(xOZ|!>.dhQ+';إ8.a yݺUjZpt\C@ii+QP…@Ϟ+WNavdvٲ2#=]F!sRB)ׁիҹKD:Zu1N'ͻ͛= hڼ}'<84k&W/>XF_ Ze<@"5K޶.uti\˽κ_Xط8P)!Hdw˗o L ۬,= d*kooBJ9S;W $1շ"EG&94DMW&{Z2#W^l z|>~x*r KQ\ DD='McKT [BjRzptf$*CdQ́M&MG#ɂDkKW Ō(g&YfA`̘10rH-[%KD^еkW(Q*To^_2(uobO ~QCj6B`:ť5 $uDNGV3R_.9QѠպ~'~;v,6oތCbԨQٳ' D׮]}߿?1c z5SrV-i@}˟}E@~q274 IDATE Q\+ґѥ"r/Zۮ|M~)Mf?99+VuЦM@xx8ԩh4o?3bcccAx1~xL2>f!͝+FZWZ>:>MBu N bԯ< DFJO?"s&T.ƮV99r$vveСCt]6V(@TTի,^;v숔;v̬.\,] ̞-C*ȗY,Y ukhGa1MYW/C1 844Zt9sf}W8|p'$$i6vbm&?/\֢y\kd0[ĉ2w&$MD* J:tW\AFLeffbXhoߎ $%%eLLLDPP ((ȶ_(a6:UP޽^{kKAV><<~ˁdyL9:vyԱyq\ Q._p;wr3&`Ҷm[_4hj׮ǣJ*Fdd$z8q qTPU^eM :ud҆ -^KJ|y ܹ#m2`| D9ڼYF۶ut~>J*?CٲeM2ƍC2eѣG#,, ͛7tx0g$$$`ĉ9r$|}}.K5y^Zua| D٤Kp5#rVZBZt:]^z!==;vĒ%KL{zzbӦM1b޳XW`~}Sb 2¨Q\ș}RR\_5bŊaX@jժa˖->CЕDERUJ ]*"ʏ1&'f{,Am+VkJҤW'7$7oR9Z2%˜9|Lc $-ͿQ7nH|G,q}5 m[޵`JJɪd@rC:5s& PKEDL3x(<8Jn>0 jOO $xu`Pť19yRf:|XB[o^l rI %?oc;_r?)I񆼫d,@rJɷ1cJ}M]*"* ooW&=ZO$@ >u]s.d s%]*"%HN.bɵkر I[Nvt) q۶d{? +G2~~:6PϞ7sIN `"*16 XH*Vtt$ j&L/=8W,۟N|ЬK~Ym(ofˊ꒨h*Q5g"k\h5cR~a1~xlݺo?p4i»ヒO?IIIhٲ%.]5kZ,VHE@\}\{ytiGlI:Y>0*U^~| luV/_NB,0s j*`ҤIر#bccQVZLCbt:য়m2h߾ǣr _"wõk.kȐ0XFŲO a={6T, 1¼y0qDtzjbÆ ӧ˜/ Q U;R5k&;}(U%$"{+QB3g nߖ`V'O=$U$Z~} #C3ɦ۸q#:v޽{c׮]T^~e ;ٳgvڙ~GբYf1#==t;55Z~V>ܒu(<ǪUxUO?)<Pmy _ṵ-N??=zC_&Rp=BkLo+{\`lYFΜ9KbܸqxqA+oB@c9s&N(WO/%$-[lr,wRŕoU޽pRI*U-ZNj/#4<=Bׯڎ_ ϱW:a4oLݺ?X?3eFŋe33l 4i3f6lGbٲe0`@9a7t;>>h۶-*Udrt:DDD};7PUtMN f. Ǝѣh =z(9s8{ee7kF~0yD{+ut]f˸ b#,̆̊._/[i-- 1up#?TK1[? 18r"#{jLf()Z۷/;vq믿m۶1!$..,w;|06l`ĉ^iXΝ-[{o;iEEk;Сƺyy^ǯ X+VEرsu]4Ne]? 7ƍyxlL2G}:iժ#FX(&rWv3RҬ1Bk]~ g~!\|1_o<+xxŚW{^eOW^,]iӦ1sLڷoܹs=z{wR:D~Xr5@1͡C0q",Yc³j.CL.Oq|wkk963g2sLVUOUU:njAu]:~XϊKd8 X̥7Ej0&z\qic?'"gI l6`QWiccPvv `pe0>ra|T7&z|9g合@P1|D]rA۶.u}#}\c{Ü9 (>{FF33" $aBco?s]7]~2rr|RR/;r[ssD"#KN} - .5tͦ5 pҨĵx֭Ы11୷Kr 76&eem^W]z [՞x/86o6Fkg[!ڈR `}<(  _}e,uxHsG4˵ q? #GBj6nvO8)/7v2:t0va;1=LyAxј ܩq =*+|RRr_êUOIX Drgvsc=6;薖<- vĉ0m~d#>)o)J=0Z+W_~9̘a' %/G1rqn^mŠWsQqi^[ RK_omUYi|o ͚]4T u'޹v=cĹԍŽ0f1k׾ٕ`u˕WxZ_T@,'QUe{>gA>fW%"r(UUPRbmsh/`pS}&O6V ?(㣺ͥ/uJ .,^ ^۷={NDڪ6Pc3xWbvU""ge@s?_(ʯ>&L0x +=u[ZEY`W^nl~У'"ϟOn݈$22D>T7oNxx8)))ָF~~>Æ iӦr=PQQq5R P `o-S'jԘO-mڴat/̵^_|\ԩSyy뭷bҤI5> J F\\9c̘1㏟}ò7( krcmHX.DDfŠ^c=ΦM6,\ŋ3p@222ҥ Ӈ?<>#v;=zGc鄆}325 _(68Nm[(H`.cUVVdJKKILL$''IRRΝ;X~=^x!cMNNl߾_8Zcƍ?uHF `qq1v? #knJbb"p.]JBB]|NAA5_կoÛoÂkeK/cǞ=v-uŸt'$$~̚5vԉ\6lĉ;v,yyy>g`xۍ/7テC~= ͮBo`ߍb|stѺukS={i&~inF9tPVBcƍ5W=K37^xn-2wCp`.QZZNy9deԩбx9={OODU#""KDFF62zIHHYYYvA~~>$&&uV>'33Hj~peƿ4b߷ ^/BAŻ|9dfZ5c Vn"\[ӦMcСS\\ŋYz5V"** &FLL L׻7w f|k4$V3fGTTݺucժU <9sDJJ eee$''3o<|r&NHbb"͚5cر̜95 7\t1pTcRZ:)ރc!"qвU*]pӾ޸qcIOO?9m۶eŊwj1L ~=@wa4x)4#bb̮JD|QQPRƮ rfAAƣM7z_KP+wCF\LODZ^m>HL{cjUW Bo,[f(,4)N?c] Ej_dc_@YqN)) @+/)S`H1<(<<"voߺ\.#+Z֮]mZIj9]koch;Z-Wrh ~٘5yŸ4kM .`[gRW 0>*ZBUXaر#l=ͮND?ƪ.ngذhdGwĉu%:d,咞~kE_].h41Œ8yrݯMmG﫯 _zEDG-uc=]%%p1;.MqѬ~;glkЧŸو 5ߥz ڷu4 nhvNDD>3S<-f̀>}۴e XqDDw4<;rɒ%lٲM6ZAADGG8n)((85(++s?/.. s:9BPX~@&k_ 1B{otK{todys?ѣ>]袢wofQßg233iYf1cƌgeeѢE ϱ233hN=ʺ+ {|.̞Ykdd3_[:qYDG<>{/\[ cٲe9c+++lj*_jmۖ)S0uԓ^{]hᩥNLL᧟\ʣ᯾`֭qU̜YU ې@zzƍ6kNU{}ٳ6m-$ [ ֭[k?~<;w#$$,RRRرc$&&aaaÇhԨ~XT^M #<ڴ1`~BBBCދtK{to۪q_B-o3FDDеkǚ5kF'L@ZZ111DFF2ydӧ%M]i*+<xJKᡇ஻X3}/ `m̙3 RRR(++#99y]9cvXL\V5K 9f͌f^ WqƤnNAuٽXwoXXEDD4G@0P,)ΝW ""IW/ֿp+obP ?(+ v-Cc1WOD$  @[w믇+0m[,mͮLDD|E]P;|q~зٕUw޶֦1 @Z++!#XϯM35kfve""bX\I DD]Má@s"nlv<7}ŸHCŠ͡huN4 By132h8k,zEDD1;v8pJ '%%73l06mJll,sR0>Yt}:逸 'GDD@+/fRSS&33ɐ!I IDATC(--u3uT{=z-֬YÏ?ȨQܯWVV2l0yYh=_ZmیVO?n EDDAAn\re-"66OQQ .d 8 tBvv6}?$//>N=xG>}:>hu~GDDԂeKk@NNN$9;w&>>~z.B}@rr2f>ޠ@r{Nj#P+׈Hqq1v? # 2e }k׮Jtttsv;s կWkj:d3wQ2k7܏YfsRSSٶmK,Aޣ@X 2nVx xIMM=cˣ7(Z`y9<<h 6i%H矡Y0̟?8q3gAAAPVVFrr2s˙8q"4k֌c2sL_}5X6K &]/+RŠ =ZNrƍNzz)i۶-+Vdihu&߆nazc` $++3ZCB|eŸxϱP|C-gs8? cM/yOX?.8Ш,^ _l '*5mj/W (Z#Xѣkafc?__R0F]VWVx0;w…0~ZDDv@_R:/^3Ɂݽ6"""en /۸)[ g\c ryZSo)Z…W_ˋZup-6P:/Gc4,ưb+iK@.C+""`  V 3[:/テ?_eEDDMZ-_z "#@ @\ڵR"""^QO?[GChaA?2Ûoh$""bE w,S77k} .CEx f͢W^DDDˈ#رcGs4oޜpRRR(P{ `XX> ^ynBC=Xh7fRSS&33ɐ!C(--u3uT{=z-֬YÏ?ȨQLm|]g'""C-cW\YE%''STT… Yx1]/##.]M>}(N4˗vZڴi>Gyy9 XXXH\\I5m4KBB utqqau""Ot! @F"ƍtcos y/._ѽ3V34jxb}]"""hҤ QQQL04bbbd$&&Zb0`E8ywjX㣖񮀝<|0`{oϙ3g\s )))ߟ8y!%Xs4jdvZNVl,[.zs=&M7|S㜃2zh"##f„ ˨!`:cܸqs7nLzz:wy\^^]@/;􋈈 c S+-SZZJIOO?O<< ?<6lYf$''p8=۷鞛pwK8AvΙ}e@СCObܹ<\{+v-[M7ė_~ʕ+ٴi\r >,W_}5O>$>Pr\ z53IYDD/Y-ή](((DTT{vo,~z )) 6lA-V9sm[9Ex)..v'QՓLv{n,QPP@l_5jԈSn>mj޷^ݘ""|@TT1k,←E(.XXYI v2_9ԵpO-,,su/,,GsUTTpA&jx(̟#DE0/_ܻAddQؾ}{r;|06lpo,ȡCqSUUE޽ ZXm*<w飢DDDzO?wZv~k.rss!>>)S裏ҡCڷo_WZjň#ҥ W]u~;?,JDDZ4>ZH-vI ۷Çcm6&""ECd7N<{6wp:]H-v3~3="""~˟GjSo&P(zUUTTYy20K/ԩдIxw)ZUYUɟzh1&@R*xL ƶo&i7 l-Y~O΂U$> TU)&$""#tBQٕ&@:.;~[ SDD$PYm7@hU ĚDDD|/];+ LZЪ~ ဿƌ1 tУU.ZEgjI""" XѨ1pС5H@PU|ݰl刈HP U9Ta\s tnvA"""(hUr-_}݈]ZQ+Whv5"""HhQYifz EDDē,,j a m("""hAxi<jDDD$(ZPv5RDDD$)ZB.EDDDUQ("""hQ6PhUZEDD+JPDDD@Ъ*,"""^hUeejP*M/Q*M/Q(-#"""ޢhUj/Q2W(ZZEDDKJcEDDKJ-"""% VTYT xEk׎ƍӻwo6nhvIPV@ dfx xٲe ݻw'99[@lfz)nvƏOBB?liӦ~^QQ_~yGPg{ӋIHH //^ӽ._["??5- /P5آE ),,q~ɚ> @֭k4Oӽ._[ V`'ҳgOǪ"11DDDJ134@4Ǝ%\¥^ܹs)--ef&"""h!xF;6]v%::{'|^{N:\!88 {]ޥ=ꙡl.ev""""; v HC("""(40 """" Ӯ];7nL޽ٸq%k2|pZjfcٲef0f͚E^ 66#Fc ϧ[nDFFIbb"|eٳgc٘2e٥ӧcj<:wlvY żewNrr274WZZJIOO7fRSS&33ɐ!C(--5ЦMfϞMNN7of\{l߾ʦMX`ݺu3so>cݺufiݻ7z;<&OoruftRFav)駟~"665kп H111oc„ fJJJ⋙7o>(=z`ܹfOβe59Z-ǂHJJb&V&R7EEERij*++Yd ~2lذ?sZj~;FM~~%5xw;pv;_}IUMUUSLo߾tr֭[ILLpҥKIHH0dl¦M.% ݛEѩS'nj3ٶmf`)Gm6N:KQQo6cǎe͚5 giϞ=&33ƍ]N@:tݺuw޴mۖ7|SCLh!-Z 88 3*ڛ4i˗/gڵirJhh(ٳ'6m駟f&Wrrrؿ?_|Xee%k׮瞣`+ >b/^իYj٥ƪ6k֌͛k } >m?7lvi xOLDD "w )Sj[{$88^uUk2)H@ %..^n~-)))q3x*SS2n8֬YO?ff{-ZDtt4˗/SN4mڔ뮻#G/Ӯ];9ݶeee}ݴnݚf͚ѻwoV^}ښ,YiܸJ"""gϞl޼ټy3~goH-42xڵ+3ge˖޽s93<Ò%K(..fԨQ9hVXw}GJJ }o`ҤIdZjҥKꪫغu+:t8iM~)rKcG梋.bKHHxv;~)H(_"44MwsN'w뮻W_}BIHH+O>o$?? iժw}7+W$##߻ϯ=CΝN[j_{ "rE$`5mڴFn]v8~nJee%;vq27o~9zh_4n6^}UOhkҤ G'"R_ "rl'=VKII 0x-Z/86}tn}>~a,Yȑ#cӦMt}Nvv6aaa$&&zvPswLBB-[$??c`̘1u]tԉ#Fi&O9Gfر`~gƌCǎ:t(3fp믿ѣiڴj-r]{8|0 ,8SNl޼:("<m۶Uwݻ7oŸF-"""" ZEDDD@FPDDDQi`EDDD@FPDDDQi`EDDD fIENDB`qlustered-deepdiff-41c7265/docs/_static/custom.css000066400000000000000000000030451516241264500221550ustar00rootroot00000000000000@import url('https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&display=swap'); h1, h2, h3, h4, h5, h6 { font-weight: normal; } /* Sidebar brand: logo links externally, text links to docs root */ .sidebar-brand { display: flex; flex-direction: column; align-items: center; padding: var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal); } .sidebar-brand-text { overflow-wrap: anywhere; text-decoration: none; color: var(--color-sidebar-brand-text, var(--color-foreground-primary)); font-size: 1.5rem; } .sidebar-brand-text:visited { color: #2A6496; } .sidebar-brand-text:hover { color: var(--color-sidebar-brand-text, var(--color-foreground-primary)); } /* Sidebar active/current item */ .sidebar-tree .current-page > .reference { color: #195190; } /* Right-side TOC active link */ .toc-tree a.active { color: #195190; } /* Dark mode overrides */ @media (prefers-color-scheme: dark) { body[data-theme="auto"] .sidebar-tree .current-page > .reference, body[data-theme="dark"] .sidebar-tree .current-page > .reference { color: #7EB8DA; } body[data-theme="auto"] .toc-tree a.active, body[data-theme="dark"] .toc-tree a.active { color: #7EB8DA; } body[data-theme="auto"] .sidebar-brand img { filter: brightness(0) invert(1); } } body[data-theme="dark"] .sidebar-tree .current-page > .reference { color: #7EB8DA; } body[data-theme="dark"] .toc-tree a.active { color: #7EB8DA; } body[data-theme="dark"] .sidebar-brand img { filter: brightness(0) invert(1); } qlustered-deepdiff-41c7265/docs/_static/favicon.ico000066400000000000000000000353561516241264500222640ustar00rootroot00000000000000 h6 00 %F(  @ 7;;;;;;;;;;;;<'< )) 7q% $]L]kaka_iiic3t++o54 }UB+Af[eoO! TAT''m\m6,6Y43 e m0/[L[}=  wG1F7K6KWNWa# "SƉ$ #M:L+!zjz=2=k32  w[:"9i]iuC C  I  I  I  O  SK;K-,*)! .-! 5acccccccccc/( @   !OWYYYYYYYYYYYYYYYYYYYYYYYW;  .-M ] ].-& %6554-,IOk65wΫ%# %;#:cacAZGZ ?222+########  O10  kI=%< }W slZkIo^o% $g1C65GeSdY' U('Q],+ mMaOa5p# "c-Kϻ('WR=QSoy$ #G_& %iC޹L7K777'sbr[%C& %  w}s}K=&<M)dQc444?{_1еB+ARRR/\H[[  Wu21  sA VAU,,,Cvfu]'=,+  cG:#9  }O yQ-,  k=165URU?I3HS i*)Qg32  uU aNa Cm\m# "c/E--?P;OO qt!  E#"gA !!))uD.C3_L_a'!a  s  s  s  s  s  s  u    }o$#oC  [VBU)(@)?>'>0/G{# !##################### (0` %/QUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU/3IUY[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[YYU/)aS:#9aQY|l|10C,BC,B@)@?'>} C=%J4IM@(?mvہrãůӯӯӯӯӯӯӯӯӯM '-!SWCgggggggggggggg9%%jXiQYEX[?/%y43c;o0/ }US IS  mU/%VBV  wQ'@)@YU%)(U('  sWS7vSyhxcU/ OK5JcMI?'>YS/SĽĹ  oU/%kZkQS>R[U/%43a+43  }YS ISiS/%VBV  mQ'G0FYS!)(U$ #  qUS%jXiS_L^aU/%y@(?c;o8!8YS ISiS/%dQcQ#N8M[U%)(U)( }US IS ycS/5K5J  mQ3:#9[S!S  oU/%jXiQiWhcU%y43a3y0/US IScS/%VBV  wQ'@(?]S%)(U('  uUQ7vSyhxiU/ O@(?cII65WS/SĽĹ kU/%kZkQVBV_S%)(U+)(  wWS- }SkU//QUUUUUUUUUUUUUUUSO; 8!8  mS-,[S3[}  qW/caS 21G_Q8 7R=QR=QO:OF/Eu?=#+'- ]mgggggggggggggggggggggggggggggggg9 %qlustered-deepdiff-41c7265/docs/_static/logo.svg000066400000000000000000000014371516241264500216150ustar00rootroot00000000000000 qlustered-deepdiff-41c7265/docs/_static/logo_long_B1_black.svg000066400000000000000000000074521516241264500243150ustar00rootroot00000000000000 qlustered-deepdiff-41c7265/docs/_static/qluster_grey_on_white_bg_optimized.svg000066400000000000000000000066311516241264500300330ustar00rootroot00000000000000 image/svg+xml qlustered-deepdiff-41c7265/docs/_templates/000077500000000000000000000000001516241264500206365ustar00rootroot00000000000000qlustered-deepdiff-41c7265/docs/_templates/page.html000066400000000000000000000052441516241264500224450ustar00rootroot00000000000000{% extends "furo/page.html" %} {% block extrahead %} {% endblock %} {% block footer %}
{%- if show_copyright %} {%- endif %}
{% if theme_footer_icons -%}
{% for icon_dict in theme_footer_icons -%} {{- icon_dict.html -}} {% endfor %}
{%- endif %}
{% endblock %} qlustered-deepdiff-41c7265/docs/_templates/sidebar/000077500000000000000000000000001516241264500222475ustar00rootroot00000000000000qlustered-deepdiff-41c7265/docs/_templates/sidebar/brand.html000066400000000000000000000015331516241264500242250ustar00rootroot00000000000000{# Override Furo's sidebar brand so the logo links to getqluster.com #} qlustered-deepdiff-41c7265/docs/authors.rst000077700000000000000000000000001516241264500272722../deepdiff/docstrings/authors.rstustar00rootroot00000000000000qlustered-deepdiff-41c7265/docs/basics.rst000077700000000000000000000000001516241264500266302../deepdiff/docstrings/basics.rstustar00rootroot00000000000000qlustered-deepdiff-41c7265/docs/buildme.py000077500000000000000000000022251516241264500205000ustar00rootroot00000000000000#!/usr/bin/env python """ Create a .env file in this folder that has BUILD_PATH defined, otherwise the _build will be used. Then run ./buildme.py It will remove the contents of the BUILD_PATH folder and recreate it. """ import os import datetime import shutil from dotenv import load_dotenv from sphinx.cmd.build import main as sphinx_main CACHE_PATH = '/tmp/sphinx_doctree' def ensure_dir(file_path): directory = os.path.dirname(file_path) if not os.path.exists(directory): os.makedirs(directory) def delete_dir_contents(directory): if os.path.exists(directory): shutil.rmtree(directory) if __name__ == "__main__": load_dotenv(override=True) build_path = os.environ.get('BUILD_PATH', '_build') doc_version = os.environ.get('DOC_VERSION', '') if not build_path.endswith('/'): build_path = build_path + '/' build_path += doc_version argv = ['-b', 'html', '-d', CACHE_PATH, '.', build_path] ensure_dir(build_path) delete_dir_contents(build_path) delete_dir_contents('/tmp/sphinx_doctree') # Disable this for faster build time but it might not properly invalidate the cache sphinx_main(argv) qlustered-deepdiff-41c7265/docs/changelog.rst000077700000000000000000000000001516241264500277762../deepdiff/docstrings/changelog.rstustar00rootroot00000000000000qlustered-deepdiff-41c7265/docs/colored_view.rst000077700000000000000000000000001516241264500312622../deepdiff/docstrings/colored_view.rstustar00rootroot00000000000000qlustered-deepdiff-41c7265/docs/commandline.rst000077700000000000000000000000001516241264500306742../deepdiff/docstrings/commandline.rstustar00rootroot00000000000000qlustered-deepdiff-41c7265/docs/conf.py000066400000000000000000000322441516241264500200050ustar00rootroot00000000000000#!/usr/bin/env python3 # # DeepDiff documentation build configuration file, created by # sphinx-quickstart on Mon Jul 20 06:06:44 2015. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys import os import datetime from dotenv import load_dotenv # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # import pdb; pdb.set_trace() sys.path.insert(0, os.path.abspath('..')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx_sitemap', 'sphinxemoji.sphinxemoji', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. year = datetime.datetime.now().year project = 'DeepDiff' copyright = '2015-{}, Sep Dehpour'.format(year) author = 'Sep Dehpour' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '9.0.0' # The full version, including alpha/beta/rc tags. release = '9.0.0' load_dotenv(override=True) DOC_VERSION = os.environ.get('DOC_VERSION', version) # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = 'en' # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all # documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. # Let Furo use its default pygments styles (light + dark mode aware) pygments_style = 'xcode' pygments_dark_style = 'github-dark' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'furo' html_theme_options = { 'footer_icons': [ { 'name': 'GitHub', 'url': 'https://github.com/seperman/deepdiff', 'html': '', 'class': '', }, ], 'source_repository': 'https://github.com/seperman/deepdiff', 'source_branch': 'master', 'source_directory': 'docs/', 'light_css_variables': { # Fonts 'font-stack': "'Open Sans', sans-serif", 'font-stack--headings': "'Open Sans', sans-serif", 'font-size--normal': '17px', # Body colors (from Alabaster) 'color-foreground-primary': '#3E4349', 'color-foreground-secondary': '#555', 'color-foreground-muted': '#888', 'color-background-primary': '#fff', 'color-background-secondary': '#f8f8f8', 'color-background-border': '#EEE', # Links 'color-link': '#004B6B', 'color-link--hover': '#6D4100', # Sidebar 'color-sidebar-background': '#fff', 'color-sidebar-text': '#555', 'color-sidebar-link': '#444', 'color-sidebar-link-text': '#444', 'color-sidebar-link-text--top-level': '#444', 'color-sidebar-item-background--hover': '#EEE', 'color-sidebar-item-expander-color': '#444', 'color-sidebar-caption-text': '#444', 'color-sidebar-item-background--current': 'transparent', 'color-sidebar-search-border': '#CCC', # Code blocks 'color-code-background': '#ecf0f3', 'color-code-foreground': '#222', 'color-inline-code-background': '#ecf0f3', # Admonitions 'color-admonition-background': '#EEE', 'color-admonition-title': '#195190', 'color-admonition-title-background': 'rgba(25, 81, 144, 0.1)', # Table of contents (right sidebar) 'color-toc-item-text': '#444', 'color-toc-item-text--hover': '#195190', 'color-toc-item-text--active': '#195190', # Headerlink 'color-header-text': '#3E4349', }, 'dark_css_variables': { 'font-stack': "'Open Sans', sans-serif", 'font-stack--headings': "'Open Sans', sans-serif", 'font-size--normal': '17px', # Links 'color-link': '#7EB8DA', 'color-link--hover': '#D4A76A', # Sidebar 'color-sidebar-background': '#1a1c1e', 'color-sidebar-background-border': '#2d2d2d', 'color-sidebar-link-text': '#ccc', 'color-sidebar-link-text--top-level': '#ccc', 'color-sidebar-caption-text': '#ccc', 'color-sidebar-item-background--hover': '#2d2d2d', # Code blocks 'color-code-background': '#2b2b2b', 'color-inline-code-background': '#2b2b2b', # Admonitions 'color-admonition-title': '#7EB8DA', 'color-admonition-title-background': 'rgba(126, 184, 218, 0.15)', # TOC 'color-toc-item-text--hover': '#7EB8DA', 'color-toc-item-text--active': '#7EB8DA', }, } # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. html_logo = '_static/qluster_grey_on_white_bg_optimized.svg' # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. html_favicon = "./_static/favicon.ico" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] html_css_files = ['custom.css'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. #html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr' html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # Now only 'ja' uses this config value #html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. #html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = 'DeepDiffdoc' # Sitemap settings: html_baseurl = f'https://zepworks.com/deepdiff/{DOC_VERSION}' sitemap_url_scheme = "{link}" # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', # Latex figure (float) alignment #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'DeepDiff.tex', 'DeepDiff Documentation', 'Sep Dehpour', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'deepdiff', 'DeepDiff Documentation', [author], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'DeepDiff', 'DeepDiff Documentation', author, 'DeepDiff', 'Deep difference of Python objects.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False qlustered-deepdiff-41c7265/docs/custom.rst000077700000000000000000000000001516241264500267442../deepdiff/docstrings/custom.rstustar00rootroot00000000000000qlustered-deepdiff-41c7265/docs/deep_distance.rst000077700000000000000000000000001516241264500314762../deepdiff/docstrings/deep_distance.rstustar00rootroot00000000000000qlustered-deepdiff-41c7265/docs/deephash.rst000077700000000000000000000000001516241264500274622../deepdiff/docstrings/deephash.rstustar00rootroot00000000000000qlustered-deepdiff-41c7265/docs/deephash_doc.rst000077700000000000000000000000001516241264500311342../deepdiff/docstrings/deephash_doc.rstustar00rootroot00000000000000qlustered-deepdiff-41c7265/docs/delta.rst000077700000000000000000000000001516241264500263022../deepdiff/docstrings/delta.rstustar00rootroot00000000000000qlustered-deepdiff-41c7265/docs/diff.rst000077700000000000000000000000001516241264500257402../deepdiff/docstrings/diff.rstustar00rootroot00000000000000qlustered-deepdiff-41c7265/docs/diff_doc.rst000077700000000000000000000000001516241264500274122../deepdiff/docstrings/diff_doc.rstustar00rootroot00000000000000qlustered-deepdiff-41c7265/docs/dsearch.rst000077700000000000000000000000001516241264500271422../deepdiff/docstrings/dsearch.rstustar00rootroot00000000000000qlustered-deepdiff-41c7265/docs/exclude_paths.rst000077700000000000000000000000001516241264500316002../deepdiff/docstrings/exclude_paths.rstustar00rootroot00000000000000qlustered-deepdiff-41c7265/docs/extract.rst000077700000000000000000000000001516241264500272442../deepdiff/docstrings/extract.rstustar00rootroot00000000000000qlustered-deepdiff-41c7265/docs/faq.rst000077700000000000000000000000001516241264500254362../deepdiff/docstrings/faq.rstustar00rootroot00000000000000qlustered-deepdiff-41c7265/docs/ignore_order.rst000077700000000000000000000000001516241264500312542../deepdiff/docstrings/ignore_order.rstustar00rootroot00000000000000qlustered-deepdiff-41c7265/docs/ignore_types_or_values.rst000077700000000000000000000000001516241264500354742../deepdiff/docstrings/ignore_types_or_values.rstustar00rootroot00000000000000qlustered-deepdiff-41c7265/docs/index.rst000077700000000000000000000000001516241264500263362../deepdiff/docstrings/index.rstustar00rootroot00000000000000qlustered-deepdiff-41c7265/docs/make.bat000066400000000000000000000161201516241264500201060ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. xml to make Docutils-native XML files echo. pseudoxml to make pseudoxml-XML files for display purposes echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled echo. coverage to run coverage check of the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) REM Check if sphinx-build is available and fallback to Python version if any %SPHINXBUILD% 2> nul if errorlevel 9009 goto sphinx_python goto sphinx_ok :sphinx_python set SPHINXBUILD=python -m sphinx.__init__ %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) :sphinx_ok if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\DeepDiff.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\DeepDiff.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %~dp0 echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %~dp0 echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "coverage" ( %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage if errorlevel 1 exit /b 1 echo. echo.Testing of coverage in the sources finished, look at the ^ results in %BUILDDIR%/coverage/python.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) :end qlustered-deepdiff-41c7265/docs/numbers.rst000077700000000000000000000000001516241264500272462../deepdiff/docstrings/numbers.rstustar00rootroot00000000000000qlustered-deepdiff-41c7265/docs/optimizations.rst000077700000000000000000000000001516241264500317422../deepdiff/docstrings/optimizations.rstustar00rootroot00000000000000qlustered-deepdiff-41c7265/docs/other.rst000077700000000000000000000000001516241264500263622../deepdiff/docstrings/other.rstustar00rootroot00000000000000qlustered-deepdiff-41c7265/docs/search_doc.rst000077700000000000000000000000001516241264500303042../deepdiff/docstrings/search_doc.rstustar00rootroot00000000000000qlustered-deepdiff-41c7265/docs/serialization.rst000077700000000000000000000000001516241264500316522../deepdiff/docstrings/serialization.rstustar00rootroot00000000000000qlustered-deepdiff-41c7265/docs/stats.rst000077700000000000000000000000001516241264500264142../deepdiff/docstrings/stats.rstustar00rootroot00000000000000qlustered-deepdiff-41c7265/docs/support.rst000077700000000000000000000000001516241264500273502../deepdiff/docstrings/support.rstustar00rootroot00000000000000qlustered-deepdiff-41c7265/docs/troubleshoot.rst000077700000000000000000000000001516241264500314022../deepdiff/docstrings/troubleshoot.rstustar00rootroot00000000000000qlustered-deepdiff-41c7265/docs/view.rst000077700000000000000000000000001516241264500260442../deepdiff/docstrings/view.rstustar00rootroot00000000000000qlustered-deepdiff-41c7265/mypy.ini000066400000000000000000000000431516241264500172450ustar00rootroot00000000000000[mypy] warn_unused_ignores = False qlustered-deepdiff-41c7265/noxfile.py000066400000000000000000000020351516241264500175670ustar00rootroot00000000000000"""nox configuration file.""" # ruff: noqa: ANN001, D401 import nox @nox.session def flake8(session) -> None: """Run flake8.""" posargs = session.posargs if session.posargs else ["deepdiff"] session.install(".[cli,dev,static]") session.run( "python", "-m", "flake8", *posargs, ) @nox.session def mypy(session) -> None: """Run mypy.""" posargs = session.posargs if session.posargs else ["deepdiff"] session.install(".[cli,dev,static]") session.run( "python", "-m", "mypy", "--install-types", "--non-interactive", *posargs, ) @nox.session(python=["3.10", "3.11", "3.12", "3.13", "3.14"]) def pytest(session) -> None: """Test with pytest.""" posargs = session.posargs if session.posargs else ["-vv", "tests"] session.install(".[cli,dev,static,test]") session.run( "python", "-m", "pytest", "--cov=deepdiff", "--cov-report", "term-missing", *posargs, ) qlustered-deepdiff-41c7265/pyproject.toml000066400000000000000000000047271516241264500204770ustar00rootroot00000000000000[build-system] requires = ["flit_core >=3.11,<4"] build-backend = "flit_core.buildapi" [project] name = "deepdiff" version = "9.0.0" dependencies = [ "orderly-set>=5.5.0,<6", ] requires-python = ">=3.10" authors = [ { name = "Seperman", email = "sep@zepworks.com" } ] maintainers = [ { name = "Seperman", email = "sep@zepworks.com" } ] description = "Deep Difference and Search of any Python object/data. Recreate objects by adding adding deltas to each other." readme = "README.md" license = {file = "LICENSE"} keywords = [] classifiers = [ "Intended Audience :: Developers", "Operating System :: OS Independent", "Topic :: Software Development", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: Implementation :: PyPy", "Development Status :: 5 - Production/Stable", "License :: OSI Approved :: MIT License" ] # `dependency-groups` would make this a lot cleaner, in theory. [project.optional-dependencies] coverage = [ "coverage~=7.13.5", ] cli = [ "click~=8.3.1", "pyyaml~=6.0.3" ] dev = [ "bump2version~=1.0.1", "jsonpickle~=4.1.1", "ipdb~=0.13.13", "numpy~=2.4.3; python_version >= '3.14'", "numpy~=2.2.0; python_version < '3.14'", "python-dateutil~=2.9.0.post0", "orjson~=3.11.7", "tomli~=2.4.0", "tomli-w~=1.2.0", "pandas~=3.0.1; python_version >= '3.11'", "pandas~=2.2.0; python_version < '3.11'", "polars~=1.39.3", "nox==2026.2.9", "uuid6==2025.0.1", "pytz", "flit-core==3.12.0", ] docs = [ "Sphinx~=8.1.3", "sphinx-sitemap~=2.9.0", "sphinxemoji~=0.3.2", "furo>=2024.8.6", ] static = [ "flake8~=7.3.0", "flake8-pyproject~=1.2.4", "pydantic~=2.12.5", ] test = [ "pytest~=9.0.2", "pytest-benchmark~=5.2.3", "pytest-cov~=7.1.0", "python-dotenv~=1.2.2", ] optimize = [ "orjson", ] [project.scripts] deep = "deepdiff.commands:cli" [project.urls] Homepage = "https://zepworks.com/deepdiff/" Documentation = "https://zepworks.com/deepdiff/" Repository = "https://github.com/qlustered/deepdiff" Issues = "https://github.com/qlustered/deepdiff/issues" [tool.coverage.run] branch = true source = ["."] [tool.flake8] max-line-length = 120 builtins = "json" statistics = true ignore = "E202" exclude = "./data,./src,.svn,CVS,.bzr,.hg,.git,__pycache__" [tool.pytest.ini_options] addopts = "--pdbcls=IPython.terminal.debugger:Pdb" qlustered-deepdiff-41c7265/pyrightconfig.json.example000066400000000000000000000001121516241264500227440ustar00rootroot00000000000000{ "venvPath": "/home/[your user name]/.venvs/", "venv": "deep", } qlustered-deepdiff-41c7265/tests/000077500000000000000000000000001516241264500167135ustar00rootroot00000000000000qlustered-deepdiff-41c7265/tests/__init__.py000066400000000000000000000043571516241264500210350ustar00rootroot00000000000000def parameterize_cases(argnames, cases): """ This is used for parametrizing pytest test cases. argnames: a comma separated string of arguments that the test expects. cases: a dictionary of test cases. argnames_list = [i.strip() for i in argnames.split(',')] ids = list(cases.keys()) argvalues = [tuple(test_name if k == 'test_name' else i[k - 1] for k in argnames_list) for test_name, i in cases.items()] return {'argnames': argnames, 'argvalues': argvalues, 'ids': ids} """ argnames_list = [i.strip() for i in argnames.split(',')] if 'test_name' not in argnames_list: argnames_list.append('test_name') argvalues = [tuple(test_name if (k == 'test_name') else test_dict[k] for k in argnames_list) for test_name, test_dict in cases.items()] ids = list(cases.keys()) return {'argnames': argnames, 'argvalues': argvalues, 'ids': ids} class CustomClass: def __init__(self, a, b=None): self.a = a self.b = b def __str__(self): return "Custom({}, {})".format(self.a, self.b) def __repr__(self): return self.__str__() class CustomClassMisleadingRepr(CustomClass): def __str__(self): return "({}, {})".format(self.a, self.b) class CustomClass2: def __init__(self, prop1=None, prop2=None): self.prop1 = prop1 or [] self.prop2 = prop2 or [] def __eq__(self, other): return self.__dict__ == other.__dict__ def __repr__(self): return "".format( id(self), self.prop1, self.prop2) __str__ = __repr__ class PicklableClass: def __init__(self, item): if item != 'delete': self.item = item def __reduce__(self): if hasattr(self, 'item'): item = self.item else: item = 'delete' return (self.__class__, (item, )) def __eq__(self, other): if hasattr(self, 'item') and hasattr(other, 'item'): return self.item == other.item if not hasattr(self, 'item') and not hasattr(other, 'item'): return True return False def __str__(self): return f"" __repr__ = __str__ qlustered-deepdiff-41c7265/tests/fixtures/000077500000000000000000000000001516241264500205645ustar00rootroot00000000000000qlustered-deepdiff-41c7265/tests/fixtures/another.yaml000066400000000000000000000002661516241264500231140ustar00rootroot00000000000000--- - first_name: Joe last_name: Nobody zip: 90011 - first_name: Jack last_name: Doit zip: 22222 - first_name: Sara last_name: Stanley zip: 11111 qlustered-deepdiff-41c7265/tests/fixtures/c_t1.csv000066400000000000000000000000711516241264500221250ustar00rootroot00000000000000id,name,food AA,Joe,Chips BB,James,Kebab CC,Jack,Veggies qlustered-deepdiff-41c7265/tests/fixtures/c_t2.csv000066400000000000000000000000731516241264500221300ustar00rootroot00000000000000id,name,food BB,James,Chicken CC,Jack,Veggies AA,Joe,Chips qlustered-deepdiff-41c7265/tests/fixtures/compare_func_result1.json000066400000000000000000000023241516241264500256000ustar00rootroot00000000000000{ "dictionary_item_added": [ "root['Cars'][3]['dealers']" ], "dictionary_item_removed": [ "root['Cars'][3]['production']" ], "values_changed": { "root['Cars'][2]['dealers'][0]['quantity']": { "new_value": 50, "old_value": 20 }, "root['Cars'][1]['model_numbers'][2]": { "new_value": 3, "old_value": 4 }, "root['Cars'][3]['model']": { "new_value": "Supra", "old_value": "supra" } }, "iterable_item_added": { "root['Cars'][2]['dealers'][1]": { "id": 200, "address": "200 Fake St", "quantity": 10 }, "root['Cars'][1]['model_numbers'][3]": 4, "root['Cars'][0]": { "id": "7", "make": "Toyota", "model": "8Runner" } }, "iterable_item_removed": { "root['Cars'][2]['dealers'][0]": { "id": 103, "address": "103 Fake St", "quantity": 50 }, "root['Cars'][1]": { "id": "2", "make": "Toyota", "model": "Highlander", "dealers": [ { "id": 123, "address": "123 Fake St", "quantity": 50 }, { "id": 125, "address": "125 Fake St", "quantity": 20 } ] } } } qlustered-deepdiff-41c7265/tests/fixtures/compare_func_t1.json000066400000000000000000000015351516241264500245300ustar00rootroot00000000000000{ "Cars": [ { "id": "1", "make": "Toyota", "model": "Camry", "dealers": [ { "id": 103, "address": "103 Fake St", "quantity": 50 }, { "id": 105, "address": "105 Fake St", "quantity": 20 } ] }, { "id": "2", "make": "Toyota", "model": "Highlander", "dealers": [ { "id": 123, "address": "123 Fake St", "quantity": 50 }, { "id": 125, "address": "125 Fake St", "quantity": 20 } ] }, { "id": "3", "make": "Toyota", "model": "4Runner", "model_numbers": [1, 2, 4] }, { "id": "4", "make": "Toyota", "model": "supra", "production": false } ] } qlustered-deepdiff-41c7265/tests/fixtures/compare_func_t2.json000066400000000000000000000015021516241264500245230ustar00rootroot00000000000000{ "Cars": [ { "id": "7", "make": "Toyota", "model": "8Runner" }, { "id": "3", "make": "Toyota", "model": "4Runner", "model_numbers": [1, 2, 3, 4] }, { "id": "1", "make": "Toyota", "model": "Camry", "dealers": [ { "id": 105, "address": "105 Fake St", "quantity": 50 }, { "id": 200, "address": "200 Fake St", "quantity": 10 } ] }, { "id": "4", "make": "Toyota", "model": "Supra", "dealers": [ { "id": 123, "address": "123 Fake St", "quantity": 50 }, { "id": 125, "address": "125 Fake St", "quantity": 20 } ] } ] } qlustered-deepdiff-41c7265/tests/fixtures/compounds.json000066400000000000000000032415761516241264500235100ustar00rootroot00000000000000{ "RecordType": "CID", "RecordNumber": 2719, "RecordTitle": "Chloroquine", "Section": [ { "TOCHeading": "Structures", "Description": "Structure depictions and information for 2D, 3D, and crystal related", "Section": [ { "TOCHeading": "2D Structure", "Description": "A two-dimensional representation of the compound", "DisplayControls": { "MoveToTop": true }, "Information": [ { "ReferenceNumber": 69, "Value": { "Boolean": [ true ] } } ] }, { "TOCHeading": "3D Conformer", "Description": "A three-dimensional representation of the compound. The 3D structure is not experimentally determined, but computed by PubChem. More detailed information on this conformer model is described in the PubChem3D thematic series published in the Journal of Cheminformatics.", "DisplayControls": { "MoveToTop": true }, "Information": [ { "ReferenceNumber": 69, "Description": "Chloroquine", "Value": { "Number": [ 2719 ] } } ] } ] }, { "TOCHeading": "Chemical Safety", "Description": "Launch the Laboratory Chemical Safety Summary datasheet, and link to the safety and hazard section", "DisplayControls": { "HideThisSection": true, "MoveToTop": true }, "Information": [ { "ReferenceNumber": 69, "Name": "Chemical Safety", "Value": { "StringWithMarkup": [ { "String": " ", "Markup": [ { "Start": 0, "Length": 1, "URL": "https://pubchem.ncbi.nlm.nih.gov/images/ghs/GHS07.svg", "Type": "Icon", "Extra": "Irritant" } ] } ] } } ] }, { "TOCHeading": "Names and Identifiers", "Description": "Record identifiers, synonyms, chemical names, descriptors, etc.", "Section": [ { "TOCHeading": "Record Description", "Description": "Summary Information", "DisplayControls": { "HideThisSection": true, "MoveToTop": true }, "Information": [ { "ReferenceNumber": 3, "Name": "Record Description", "Description": "Ontology Summary", "Value": { "StringWithMarkup": [ { "String": "Chloroquine is an aminoquinoline that is quinoline which is substituted at position 4 by a [5-(diethylamino)pentan-2-yl]amino group at at position 7 by chlorine. It is used for the treatment of malaria, hepatic amoebiasis, lupus erythematosus, light-sensitive skin eruptions, and rheumatoid arthritis. It has a role as an antimalarial, an antirheumatic drug, a dermatologic drug, an autophagy inhibitor and an anticoronaviral agent. It is an aminoquinoline, a secondary amino compound, a tertiary amino compound and an organochlorine compound. It is a conjugate base of a chloroquine(2+).", "Markup": [ { "Start": 0, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 18, "Length": 14, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/aminoquinoline", "Type": "PubChem Internal Link", "Extra": "CID-11379" }, { "Start": 41, "Length": 9, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/quinoline", "Type": "PubChem Internal Link", "Extra": "CID-7047" }, { "Start": 152, "Length": 8, "URL": "https://pubchem.ncbi.nlm.nih.gov/element/Chlorine", "Type": "PubChem Internal Link", "Extra": "Element-Chlorine" }, { "Start": 442, "Length": 14, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/aminoquinoline", "Type": "PubChem Internal Link", "Extra": "CID-11379" }, { "Start": 470, "Length": 5, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/amino", "Type": "PubChem Internal Link", "Extra": "CID-136037442" }, { "Start": 497, "Length": 5, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/amino", "Type": "PubChem Internal Link", "Extra": "CID-136037442" }, { "Start": 572, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } }, { "ReferenceNumber": 10, "Value": { "StringWithMarkup": [ { "String": "Chloroquine is an aminoquinolone derivative first developed in the 1940s for the treatment of malaria. It was the drug of choice to treat malaria until the development of newer antimalarials such as [pyrimethamine], [artemisinin], and [mefloquine]. Chloroquine and its derivative [hydroxychloroquine] have since been repurposed for the treatment of a number of other conditions including HIV, systemic lupus erythematosus, and rheumatoid arthritis. **The FDA emergency use authorization for [hydroxychloroquine] and chloroquine in the treatment of COVID-19 was revoked on 15 June 2020.** Chloroquine was granted FDA Approval on 31 October 1949.", "Markup": [ { "Start": 0, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 18, "Length": 14, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/aminoquinolone", "Type": "PubChem Internal Link", "Extra": "CID-170348" }, { "Start": 200, "Length": 13, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/pyrimethamine", "Type": "PubChem Internal Link", "Extra": "CID-4993" }, { "Start": 217, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/artemisinin", "Type": "PubChem Internal Link", "Extra": "CID-2240" }, { "Start": 236, "Length": 10, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/mefloquine", "Type": "PubChem Internal Link", "Extra": "CID-4046" }, { "Start": 249, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 281, "Length": 18, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/hydroxychloroquine", "Type": "PubChem Internal Link", "Extra": "CID-3652" }, { "Start": 493, "Length": 18, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/hydroxychloroquine", "Type": "PubChem Internal Link", "Extra": "CID-3652" }, { "Start": 517, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 590, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } }, { "ReferenceNumber": 22, "Description": "LiverTox Summary", "Value": { "StringWithMarkup": [ { "String": "Chloroquine is an aminoquinoline used for the prevention and therapy of malaria. It is also effective in extraintestinal amebiasis and as an antiinflammatory agent for therapy of rheumatoid arthritis and lupus erythematosus. Chloroquine is not associated with serum enzyme elevations and is an extremely rare cause of clinically apparent acute liver injury.", "Markup": [ { "Start": 0, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 18, "Length": 14, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/aminoquinoline", "Type": "PubChem Internal Link", "Extra": "CID-11379" }, { "Start": 225, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } }, { "ReferenceNumber": 23, "Value": { "StringWithMarkup": [ { "String": "Chloroquine is a natural product found in Cinchona calisaya with data available.", "Markup": [ { "Start": 42, "Length": 17, "URL": "https://pubchem.ncbi.nlm.nih.gov/taxonomy/153742#section=Natural-Products" }, { "Start": 0, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } }, { "ReferenceNumber": 68, "Value": { "StringWithMarkup": [ { "String": "The prototypical antimalarial agent with a mechanism that is not well understood. It has also been used to treat rheumatoid arthritis, systemic lupus erythematosus, and in the systemic therapy of amebic liver abscesses." } ] } } ] }, { "TOCHeading": "Computed Descriptors", "Description": "Descriptors generated from chemical structure input", "Section": [ { "TOCHeading": "IUPAC Name", "Description": "Chemical name computed from chemical structure that uses International Union of Pure and Applied Chemistry (IUPAC) nomenclature standards.", "URL": "http://old.iupac.org/publications/books/seriestitles/nomenclature.html", "Information": [ { "ReferenceNumber": 69, "Reference": [ "Computed by Lexichem TK 2.7.0 (PubChem release 2021.05.07)" ], "Value": { "StringWithMarkup": [ { "String": "4-N-(7-chloroquinolin-4-yl)-1-N,1-N-diethylpentane-1,4-diamine", "Markup": [ { "Start": 2, "Length": 1, "Type": "Italics" }, { "Start": 30, "Length": 1, "Type": "Italics" }, { "Start": 34, "Length": 1, "Type": "Italics" } ] } ] } } ] }, { "TOCHeading": "InChI", "Description": "International Chemical Identifier (InChI) computed from chemical structure using the International Union of Pure and Applied Chemistry (IUPAC) standard.", "URL": "http://www.iupac.org/home/publications/e-resources/inchi.html", "Information": [ { "ReferenceNumber": 69, "Reference": [ "Computed by InChI 1.0.6 (PubChem release 2021.05.07)" ], "Value": { "StringWithMarkup": [ { "String": "InChI=1S/C18H26ClN3/c1-4-22(5-2)12-6-7-14(3)21-17-10-11-20-18-13-15(19)8-9-16(17)18/h8-11,13-14H,4-7,12H2,1-3H3,(H,20,21)" } ] } } ] }, { "TOCHeading": "InChI Key", "Description": "International Chemical Identifier hash (InChIKey) computed from chemical structure using the International Union of Pure and Applied Chemistry (IUPAC) standard.", "URL": "http://www.iupac.org/home/publications/e-resources/inchi.html", "Information": [ { "ReferenceNumber": 69, "Reference": [ "Computed by InChI 1.0.6 (PubChem release 2021.05.07)" ], "Value": { "StringWithMarkup": [ { "String": "WHTVZRBIWZFKQO-UHFFFAOYSA-N" } ] } } ] }, { "TOCHeading": "Canonical SMILES", "Description": "Simplified Molecular-Input Line-Entry System (SMILES) computed from chemical structure devoid of isotopic and stereochemical information.", "URL": "http://www.daylight.com/dayhtml/doc/theory/theory.smiles.html", "Information": [ { "ReferenceNumber": 69, "Reference": [ "Computed by OEChem 2.3.0 (PubChem release 2021.05.07)" ], "Value": { "StringWithMarkup": [ { "String": "CCN(CC)CCCC(C)NC1=C2C=CC(=CC2=NC=C1)Cl" } ] } } ] } ] }, { "TOCHeading": "Molecular Formula", "Description": "A chemical formula is a way of expressing information about the proportions of atoms that constitute a particular chemical compound, using a single line of chemical element symbols and numbers. PubChem uses the Hill system whereby the number of carbon atoms in a molecule is indicated first, the number of hydrogen atoms second, and then the number of all other chemical elements in alphabetical order. When the formula contains no carbon, all the elements, including hydrogen, are listed alphabetically. Sources other than PubChem may include a variant of the formula that is more structural or natural to chemists, for example \"H2SO4\" for sulfuric acid, rather than the Hill version \"H2O4S.\"", "DisplayControls": { "MoveToTop": true }, "Information": [ { "ReferenceNumber": 69, "Reference": [ "Computed by PubChem 2.1 (PubChem release 2021.05.07)" ], "Value": { "StringWithMarkup": [ { "String": "C18H26ClN3" } ] } } ] }, { "TOCHeading": "Other Identifiers", "Description": "Important identifiers assigned to this chemical substance by authoritative organizations", "Section": [ { "TOCHeading": "CAS", "Description": "A proprietary registry number assigned by the Chemical Abstracts Service (CAS) division of the American Chemical Society (ACS) often used to help describe chemical ingredients.", "URL": "http://en.wikipedia.org/wiki/CAS_Registry_Number", "Information": [ { "ReferenceNumber": 1, "URL": "https://commonchemistry.cas.org/detail?cas_rn=54-05-7", "Value": { "StringWithMarkup": [ { "String": "54-05-7" } ] } }, { "ReferenceNumber": 4, "Value": { "StringWithMarkup": [ { "String": "54-05-7" } ] } }, { "ReferenceNumber": 10, "Value": { "StringWithMarkup": [ { "String": "54-05-7" } ] } }, { "ReferenceNumber": 11, "Name": "CAS", "Value": { "StringWithMarkup": [ { "String": "54-05-7" } ] } }, { "ReferenceNumber": 12, "Value": { "StringWithMarkup": [ { "String": "54-05-7" } ] } }, { "ReferenceNumber": 15, "Value": { "StringWithMarkup": [ { "String": "54-05-7" } ] } }, { "ReferenceNumber": 18, "Value": { "StringWithMarkup": [ { "String": "54-05-7" } ] } }, { "ReferenceNumber": 19, "Value": { "StringWithMarkup": [ { "String": "54-05-7" } ] } } ] }, { "TOCHeading": "Deprecated CAS", "Description": "The CAS registry number(s) in this section refer(s) to old, deprecated, previously assigned, deleted, etc. CAS number(s) which are no longer used, but users can still see in references, sometimes.", "Information": [ { "ReferenceNumber": 4, "Value": { "StringWithMarkup": [ { "String": "56598-66-4" } ] } } ] }, { "TOCHeading": "European Community (EC) Number", "Description": "A seven-digit regulatory identifier currently assigned by the European Chemicals Agency (ECHA) known as a European Community (EC) number. It is sometimes referred to as an EINECS, ELINCS, or NLP number, which are subsets of an EC number.", "URL": "http://en.wikipedia.org/wiki/European_Community_number", "Information": [ { "ReferenceNumber": 15, "URL": "https://echa.europa.eu/substance-information/-/substanceinfo/100.000.175", "Value": { "StringWithMarkup": [ { "String": "200-191-2" } ] } } ] }, { "TOCHeading": "NSC Number", "Description": "The NSC number is a numeric identifier for substances submitted to the National Cancer Institute (NCI) for testing and evaluation. It is a registration number for the Developmental Therapeutics Program (DTP) repository. NSC stands for National Service Center.", "Information": [ { "ReferenceNumber": 11, "Name": "NSC Number", "URL": "https://dtp.cancer.gov/dtpstandard/servlet/dwindex?searchtype=NSC&outputformat=html&searchlist=187208", "Value": { "StringWithMarkup": [ { "String": "187208" } ] } } ] }, { "TOCHeading": "DSSTox Substance ID", "Description": "Substance identifier at the Distributed Structure-Searchable Toxicity (DSSTox) Database.", "URL": "https://www.epa.gov/chemical-research/distributed-structure-searchable-toxicity-dsstox-database/", "Information": [ { "ReferenceNumber": 12, "URL": "https://comptox.epa.gov/dashboard/DTXSID2040446", "Value": { "StringWithMarkup": [ { "String": "DTXSID2040446" } ] } } ] }, { "TOCHeading": "Wikipedia", "Description": "Links to Wikipedia for this record.", "Information": [ { "ReferenceNumber": 66, "URL": "https://en.wikipedia.org/wiki/Chloroquine", "Value": { "StringWithMarkup": [ { "String": "Chloroquine" } ] } } ] }, { "TOCHeading": "Wikidata", "Description": "Wikidata entity identifier for the given compound.", "URL": "https://www.wikidata.org/w/index.php?title=Special:WhatLinksHere/Property:P662", "Information": [ { "ReferenceNumber": 65, "URL": "https://www.wikidata.org/wiki/Q422438", "Value": { "StringWithMarkup": [ { "String": "Q422438" } ] } } ] } ] }, { "TOCHeading": "Synonyms", "Description": "Alternative names for this PubChem Compound record. A compound can have many different names. For example, acetone (CH3C(=O)CH3) is also known as propanone, propan-2-one, or dimethyl ketone. The brand name of a product is commonly used to indicate the primary chemical ingredient(s) in the product (e.g., Tylenol, a common pain killer, is often used for acetaminophen, its active ingredient). Another example of common synonyms is record identifiers used in different data collections, such as Chemical Abstract Service (CAS) registry numbers, FDA UNII (Unique Ingredient Identifiers), and many others. All these various names and identifiers that designate this compound are organized under the Synonyms section.", "Section": [ { "TOCHeading": "MeSH Entry Terms", "Description": "Medical Subject Heading (MeSH) names or identifiers matching this PubChem Compound record. The matching between the MeSH and compound records is performed by name matching (i.e., identical common names).", "DisplayControls": { "ListType": "Columns" }, "Information": [ { "ReferenceNumber": 68, "Value": { "StringWithMarkup": [ { "String": "Aralen" }, { "String": "Arechine" }, { "String": "Arequin" }, { "String": "Chingamin" }, { "String": "Chlorochin" }, { "String": "Chloroquine" }, { "String": "Chloroquine Sulfate" }, { "String": "Chloroquine Sulphate" }, { "String": "Khingamin" }, { "String": "Nivaquine" }, { "String": "Sulfate, Chloroquine" }, { "String": "Sulphate, Chloroquine" } ] } } ] }, { "TOCHeading": "Depositor-Supplied Synonyms", "Description": "Chemical names provided by individual data contributors. Synonyms of Substances corresponding to a PubChem Compound record are combined. Some contributed names may be considered erroneous and filtered out. The link on each synonym shows which depositors provided that particular synonym for this structure.", "DisplayControls": { "ListType": "Columns", "MoveToTop": true }, "Information": [ { "ReferenceNumber": 69, "Value": { "StringWithMarkup": [ { "String": "chloroquine" }, { "String": "54-05-7" }, { "String": "Aralen" }, { "String": "Chlorochin" }, { "String": "Chloraquine" }, { "String": "Artrichin" }, { "String": "Chloroquinium" }, { "String": "Chloroquina" }, { "String": "Reumachlor" }, { "String": "Capquin" }, { "String": "Chemochin" }, { "String": "Chlorquin" }, { "String": "Clorochina" }, { "String": "Malaquin" }, { "String": "Arthrochin" }, { "String": "Bemasulph" }, { "String": "Benaquin" }, { "String": "Bipiquin" }, { "String": "Chingamin" }, { "String": "Cidanchin" }, { "String": "Cocartrit" }, { "String": "Dichinalex" }, { "String": "Gontochin" }, { "String": "Heliopar" }, { "String": "Iroquine" }, { "String": "Klorokin" }, { "String": "Lapaquin" }, { "String": "Mesylith" }, { "String": "Pfizerquine" }, { "String": "Quinachlor" }, { "String": "Quinercyl" }, { "String": "Quinilon" }, { "String": "Quinoscan" }, { "String": "Sanoquin" }, { "String": "Silbesan" }, { "String": "Solprina" }, { "String": "Sopaquin" }, { "String": "Tresochin" }, { "String": "Amokin" }, { "String": "Bemaco" }, { "String": "Elestol" }, { "String": "Imagon" }, { "String": "Malaren" }, { "String": "Malarex" }, { "String": "Neochin" }, { "String": "Roquine" }, { "String": "Siragan" }, { "String": "Trochin" }, { "String": "Nivaquine B" }, { "String": "Bemaphate" }, { "String": "Resoquine" }, { "String": "Nivaquine" }, { "String": "Chlorochine" }, { "String": "Chloroquinum" }, { "String": "Cloroquina" }, { "String": "Quingamine" }, { "String": "Avloclor" }, { "String": "Ronaquine" }, { "String": "Khingamin" }, { "String": "N4-(7-chloroquinolin-4-yl)-N1,N1-diethylpentane-1,4-diamine" }, { "String": "Avlochlor" }, { "String": "Nivachine" }, { "String": "Quinagamin" }, { "String": "Quinagamine" }, { "String": "Resochen" }, { "String": "Resoquina" }, { "String": "Reumaquin" }, { "String": "Resochin" }, { "String": "Delagil" }, { "String": "Tanakan" }, { "String": "WIN 244" }, { "String": "RP 3377" }, { "String": "1,4-Pentanediamine, N4-(7-chloro-4-quinolinyl)-N1,N1-diethyl-" }, { "String": "W 7618" }, { "String": "Chloroin" }, { "String": "Miniquine" }, { "String": "Rivoquine" }, { "String": "Tanakene" }, { "String": "Arolen" }, { "String": "7-Chloro-4-((4-(diethylamino)-1-methylbutyl)amino)quinoline" }, { "String": "CHEBI:3638" }, { "String": "N4-(7-Chloro-4-quinolinyl)-N1,N1-diethyl-1,4-pentanediamine" }, { "String": "{4-[(7-chloroquinolin-4-yl)amino]pentyl}diethylamine" }, { "String": "Gontochin phosphate" }, { "String": "CHEMBL76" }, { "String": "SN 6718" }, { "String": "Ipsen 225" }, { "String": "Chlorochinum" }, { "String": "4-N-(7-chloroquinolin-4-yl)-1-N,1-N-diethylpentane-1,4-diamine" }, { "String": "N(sup 4)-(7-Chloro-4-quinolinyl)-N(sup 1),N(sup 1)-diethyl-1,4-pentanediamine" }, { "String": "MFCD00024009" }, { "String": "NSC187208" }, { "String": "NSC-187208" }, { "String": "SN 7618" }, { "String": "Chloroquine (VAN)" }, { "String": "Clorochina [DCIT]" }, { "String": "7-Chloro-4-[[4-(diethylamino)-1-methylbutyl]amino]quinoline" }, { "String": "Quinoline, 7-chloro-4-((4-(diethylamino)-1-methylbutyl)amino)-" }, { "String": "3377 RP" }, { "String": "CQ" }, { "String": "SN-7618" }, { "String": "1,4-Pentanediamine, N(sup 4)-(7-chloro-4-quinolinyl)-N(sup 1),N(sup 1)-diethyl-" }, { "String": "ST 21 (pharmaceutical)" }, { "String": "Chloroquinum [INN-Latin]" }, { "String": "Cloroquina [INN-Spanish]" }, { "String": "3377 RP opalate" }, { "String": "Chloroquin" }, { "String": "Quinoline, 7-chloro-4-[[4-(diethylamino)-1-methylbutyl]amino]-" }, { "String": "N(4)-(7-chloro-4-quinolinyl)-N(1),N(1)-diethyl-1,4-pentanediamine" }, { "String": "ST 21" }, { "String": "(+-)-Chloroquine" }, { "String": "NSC14050" }, { "String": "CCRIS 3439" }, { "String": "HSDB 3029" }, { "String": "Chloroquine (USP/INN)" }, { "String": "EINECS 200-191-2" }, { "String": "Malaquin (*Diphosphate*)" }, { "String": "NSC 187208" }, { "String": "BRN 0482809" }, { "String": "Cloroquine" }, { "String": "Chloroquine [USP:INN:BAN]" }, { "String": "Chloroquine, 17" }, { "String": "Chloroquine-[d4]" }, { "String": "4,7-Dichloroquine" }, { "String": "Arechin (Salt/Mix)" }, { "String": "Delagil (Salt/Mix)" }, { "String": "Tanakan (Salt/Mix)" }, { "String": "1246815-14-4" }, { "String": "RP-3377" }, { "String": "Bemaphate (Salt/Mix)" }, { "String": "Resoquine (Salt/Mix)" }, { "String": "Spectrum_000132" }, { "String": "Chloroquine + Proveblue" }, { "String": "Prestwick0_000548" }, { "String": "Prestwick1_000548" }, { "String": "Prestwick2_000548" }, { "String": "Prestwick3_000548" }, { "String": "Spectrum2_000127" }, { "String": "Spectrum3_000341" }, { "String": "Spectrum4_000279" }, { "String": "Spectrum5_000707" }, { "String": "(.+/-.)-Chloroquine" }, { "String": "1,4-Pentanediamine, N(4)-(7-chloro-4-quinolinyl)-N(1),N(1)-diethyl-" }, { "String": "Epitope ID:131785" }, { "String": "MolMap_000009" }, { "String": "SCHEMBL8933" }, { "String": "Lopac0_000296" }, { "String": "BSPBio_000595" }, { "String": "BSPBio_002001" }, { "String": "KBioGR_000778" }, { "String": "KBioSS_000592" }, { "String": "DivK1c_000404" }, { "String": "CU-01000012392-2" }, { "String": "SPBio_000174" }, { "String": "SPBio_002516" }, { "String": "GNF-Pf-4216" }, { "String": "BPBio1_000655" }, { "String": "GTPL5535" }, { "String": "DTXSID2040446" }, { "String": "BDBM22985" }, { "String": "KBio1_000404" }, { "String": "KBio2_000592" }, { "String": "KBio2_003160" }, { "String": "KBio2_005728" }, { "String": "KBio3_001221" }, { "String": "NINDS_000404" }, { "String": "HMS2090O03" }, { "String": "ALBB-025694" }, { "String": "HY-17589A" }, { "String": "s6999" }, { "String": "AKOS015935106" }, { "String": "CCG-204391" }, { "String": "CS-W004760" }, { "String": "DB00608" }, { "String": "KH-0005" }, { "String": "MCULE-3610827164" }, { "String": "SB73098" }, { "String": "SDCCGSBI-0050284.P005" }, { "String": "IDI1_000404" }, { "String": "SMP2_000034" }, { "String": "NCGC00015256-02" }, { "String": "NCGC00015256-03" }, { "String": "NCGC00015256-04" }, { "String": "NCGC00015256-05" }, { "String": "NCGC00015256-06" }, { "String": "NCGC00015256-07" }, { "String": "NCGC00015256-08" }, { "String": "NCGC00015256-09" }, { "String": "NCGC00015256-10" }, { "String": "NCGC00015256-13" }, { "String": "NCGC00015256-17" }, { "String": "NCGC00015256-28" }, { "String": "NCGC00162120-01" }, { "String": "NCI60_000894" }, { "String": "SY086904" }, { "String": "WLN: T66 BNJ EMY1&3N2&2 IG" }, { "String": "SBI-0050284.P004" }, { "String": "AB00053436" }, { "String": "CS-0021871" }, { "String": "FT-0623612" }, { "String": "C07625" }, { "String": "D02366" }, { "String": "MLS-0466768.0001" }, { "String": "AB00053436-05" }, { "String": "AB00053436_06" }, { "String": "AB00053436_07" }, { "String": "1, N4-(7-chloro-4-quinolinyl)-N1,N1-diethyl-" }, { "String": "Q422438" }, { "String": "BRD-A91699651-065-01-1" }, { "String": "BRD-A91699651-316-06-7" }, { "String": "n(sup4)-(7-chloro-4-quinolinyl)-n(sup1),4-pentanediamine" }, { "String": "N'-(7-chloroquinolin-4-yl)-N,N-diethylpentane-1,4-diamine" }, { "String": "N4-(7-chloro-4-quinolyl)-N1,N1-diethyl-pentane-1,4-diamine" }, { "String": "Quinoline, 7-chloro-4-(4-diethylamino-1-methyl-butylamino)-" }, { "String": "N(4)-(7-chloroquinolin-4-yl)-N(1),N(1)-diethylpentane-1,4-diamine" }, { "String": "1,4-pentanediamine, N~4~-(7-chloro-4-quinolinyl)-N~1~,N~1~-diethyl-, phosphate (1:2)" }, { "String": "N(sup4)-(7-chloro-4-quinolinyl)-N(sup1),N(sup1)-diethyl-1,4-pentanediamine" }, { "String": "117399-83-4" }, { "String": "Chloroquine; Chloroquine Sulphate; 4-N-(7-chloroquinolin-4-yl)-1-N,1-N-diethylpentane-1,4-diamine" } ] } } ] }, { "TOCHeading": "Removed Synonyms", "Description": "Potentially erroneous chemical names and identifiers provided by PubChem Substance records for the same chemical structure that were removed by name/structure consistency filtering.", "DisplayControls": { "HideThisSection": true }, "Information": [ { "ReferenceNumber": 69, "Value": { "StringWithMarkup": [ { "String": "Arechin" }, { "String": "Arechine" }, { "String": "Arequin" }, { "String": "Chloroquine phosphate" }, { "String": "Chloroquine sulfate" }, { "String": "Plaquenil" }, { "String": "Aralen HCl" }, { "String": "Chloroquine sulphate" }, { "String": "chloroquin-" }, { "String": "Chloroquine diphosphate" }, { "String": "Chloroquine HCl" }, { "String": "(+)-Chloroquine" }, { "String": "(-)-Chloroquine" }, { "String": "Chloroquine, D-" }, { "String": "( -)-Chloroquine" }, { "String": "( )-Chloroquine" }, { "String": "Dawaquin (TN)" }, { "String": "Resochin (TN)" }, { "String": "Sulfate, Chloroquine" }, { "String": "Chloroquine hydrochloride" }, { "String": "Sulphate, Chloroquine" }, { "String": "(R)-(-)-Chloroquine" }, { "String": "Chloroquine FNA (TN)" }, { "String": "Chloroquine [USAN:INN:BAN]" }, { "String": "UNII-886U3H6UFF" }, { "String": "C18H26ClN3" }, { "String": "D09EGZ" }, { "String": "AC1L1EB8" }, { "String": "AC1Q2ZA7" }, { "String": "AC1Q2ZA8" }, { "String": "Chloroquine Bis-Phosphoric Acid" }, { "String": "Chloroquine [USAN:BAN:INN]" }, { "String": "WHTVZRBIWZFKQO-UHFFFAOYSA-N" }, { "String": "886U3H6UFF" }, { "String": "HYDROXYCHLOROQUINE SULFATE" }, { "String": "CTK1H1520" }, { "String": "C18-H26-Cl-N3" }, { "String": "CID2719" }, { "String": "Ro 01-6014/N2" }, { "String": "SBB072644" }, { "String": "ACN-029973" }, { "String": "KS-00000F97" }, { "String": "AK116457" }, { "String": "BC208405" }, { "String": "SC-48578" }, { "String": "N(C(C)CCCN(CC)CC)c1ccnc2cc(Cl)ccc12" }, { "String": "LS-141726" }, { "String": "ST2401962" }, { "String": "4CH-019706" }, { "String": "NS00001540" }, { "String": "ST45028748" }, { "String": "D002738" }, { "String": "{4-[(7-chloro(4-quinolyl))amino]pentyl}diethylamine" }, { "String": "7-chloro-4-(4-diethylamino-1-methylbutylamino)quinoline" }, { "String": "1,4-Pentanediamine, N4-(7-chloro-4-quinolinyl)-N1,N1-diethyl-, (+)-" }, { "String": "58175-86-3" }, { "String": "(+)-N4-(7-Chloro-4-quinolinyl)-N1,N1-diethyl-1,4-pentanediamine" }, { "String": "(+-)-N4-(7-Chloro-4-quinolinyl)-N1,N1-diethyl-1,4-pentanediamine" }, { "String": "(4R)-4-N-(7-chloroquinolin-4-yl)-1-N,1-N-diethylpentane-1,4-diamine" }, { "String": "1,4-Pentanediamine, N(4)-(7-chloro-4-quinolinyl)-N(1)-,N(1)-diethyl-" }, { "String": "1,4-Pentanediamine, N4-(7-chloro-4-quinolinyl)-N1,N1-diethyl-, (+-)-" }, { "String": "58175-87-4" }, { "String": "N~4~-(7-Chloro-4-quinolinyl)-N~1~,N~1~-diethyl-1,4-pentanediamine" }, { "String": "N~4~-(7-chloroquinolin-4-yl)-N~1~,N~1~-diethylpentane-1,4-diamine" }, { "String": "N4-(7-CHLORO-QUINOLIN-4-YL)-N1,N1-DIETHYL-PENTANE-1,4-DIAMINE" }, { "String": "50-63-5" }, { "String": "56598-66-4" } ] } } ] } ] }, { "TOCHeading": "Create Date", "Description": "Date the compound record was initially added to PubChem", "DisplayControls": { "HideThisSection": true, "MoveToTop": true }, "Information": [ { "ReferenceNumber": 69, "Value": { "DateISO8601": [ "2005-03-25" ] } } ] }, { "TOCHeading": "Modify Date", "Description": "Date this record was last updated in PubChem", "DisplayControls": { "HideThisSection": true, "MoveToTop": true }, "Information": [ { "ReferenceNumber": 69, "Value": { "DateISO8601": [ "2022-05-14" ] } } ] } ] }, { "TOCHeading": "Chemical and Physical Properties", "Description": "Chemical and physical properties such as melting point, molecular weight, etc.", "Section": [ { "TOCHeading": "Computed Properties", "Description": "Properties computed automatically from the given chemical structure", "DisplayControls": { "CreateTable": { "FromInformationIn": "Subsections", "NumberOfColumns": 3, "ColumnHeadings": [ "Property Name", "Property Value", "Reference" ], "ColumnContents": [ "Name", "Value", "Reference" ] } }, "Section": [ { "TOCHeading": "Molecular Weight", "Description": "Molecular weight or molecular mass refers to the mass of a molecule. It is calculated as the sum of the mass of each constituent atom multiplied by the number of atoms of that element in the molecular formula.", "DisplayControls": { "MoveToTop": true }, "Information": [ { "ReferenceNumber": 69, "Reference": [ "Computed by PubChem 2.1 (PubChem release 2021.05.07)" ], "Value": { "StringWithMarkup": [ { "String": "319.9" } ], "Unit": "g/mol" } } ] }, { "TOCHeading": "XLogP3", "Description": "Computed Octanol/Water Partition Coefficient", "Information": [ { "ReferenceNumber": 69, "Reference": [ "Computed by XLogP3 3.0 (PubChem release 2021.05.07)" ], "Value": { "Number": [ 4.6 ] } } ] }, { "TOCHeading": "Hydrogen Bond Donor Count", "Description": "The number of hydrogen bond donors in the structure.", "Information": [ { "ReferenceNumber": 69, "Reference": [ "Computed by Cactvs 3.4.8.18 (PubChem release 2021.05.07)" ], "Value": { "Number": [ 1 ] } } ] }, { "TOCHeading": "Hydrogen Bond Acceptor Count", "Description": "The number of hydrogen bond acceptors in the structure.", "Information": [ { "ReferenceNumber": 69, "Reference": [ "Computed by Cactvs 3.4.8.18 (PubChem release 2021.05.07)" ], "Value": { "Number": [ 3 ] } } ] }, { "TOCHeading": "Rotatable Bond Count", "Description": "A rotatable bond is defined as any single-order non-ring bond, where atoms on either side of the bond are in turn bound to nonterminal heavy (i.e., non-hydrogen) atoms. That is, where rotation around the bond axis changes the overall shape of the molecule, and generates conformers which can be distinguished by standard fast spectroscopic methods.", "Information": [ { "ReferenceNumber": 69, "Reference": [ "Computed by Cactvs 3.4.8.18 (PubChem release 2021.05.07)" ], "Value": { "Number": [ 8 ] } } ] }, { "TOCHeading": "Exact Mass", "Description": "The exact mass of an isotopic species is obtained by summing the masses of the individual isotopes of the molecule.", "Information": [ { "ReferenceNumber": 69, "Reference": [ "Computed by PubChem 2.1 (PubChem release 2021.05.07)" ], "Value": { "StringWithMarkup": [ { "String": "319.1815255" } ], "Unit": "g/mol" } } ] }, { "TOCHeading": "Monoisotopic Mass", "Description": "The monoisotopic mass is the sum of the masses of the atoms in a molecule using the unbound, ground-state, rest mass of the principal (most abundant) isotope for each element instead of the isotopic average mass.", "Information": [ { "ReferenceNumber": 69, "Reference": [ "Computed by PubChem 2.1 (PubChem release 2021.05.07)" ], "Value": { "StringWithMarkup": [ { "String": "319.1815255" } ], "Unit": "g/mol" } } ] }, { "TOCHeading": "Topological Polar Surface Area", "Description": "The topological polar surface area (TPSA) of a molecule is defined as the surface sum over all polar atoms in a molecule.", "Information": [ { "ReferenceNumber": 69, "Reference": [ "Computed by Cactvs 3.4.8.18 (PubChem release 2021.05.07)" ], "Value": { "Number": [ 28.2 ], "Unit": "Ų" } } ] }, { "TOCHeading": "Heavy Atom Count", "Description": "A heavy atom is defined as any atom except hydrogen in a chemical structure.", "Information": [ { "ReferenceNumber": 69, "Reference": [ "Computed by PubChem" ], "Value": { "Number": [ 22 ] } } ] }, { "TOCHeading": "Formal Charge", "Description": "Formal charge is the difference between the number of valence electrons of each atom and the number of electrons the atom is associated with. Formal charge assumes any shared electrons are equally shared between the two bonded atoms.", "Information": [ { "ReferenceNumber": 69, "Reference": [ "Computed by PubChem" ], "Value": { "Number": [ 0 ] } } ] }, { "TOCHeading": "Complexity", "Description": "The complexity rating of a compound is a rough estimate of how complicated a structure is, seen from both the point of view of the elements contained and the displayed structural features including symmetry. This complexity rating is computed using the Bertz/Hendrickson/Ihlenfeldt formula.", "Information": [ { "ReferenceNumber": 69, "Reference": [ "Computed by Cactvs 3.4.8.18 (PubChem release 2021.05.07)" ], "Value": { "Number": [ 309 ] } } ] }, { "TOCHeading": "Isotope Atom Count", "Description": "Isotope Atom Count is the number of isotopes that are not most abundant for the corresponding chemical elements. Isotopes are variants of a chemical element which differ in neutron number. For example, among three isotopes of carbon (i.e., C-12, C-13, and C-14), the isotope atom count considers the C-13 and C-14 atoms, because C-12 is the most abundant isotope of carbon.", "Information": [ { "ReferenceNumber": 69, "Reference": [ "Computed by PubChem" ], "Value": { "Number": [ 0 ] } } ] }, { "TOCHeading": "Defined Atom Stereocenter Count", "Description": "An atom stereocenter, also known as a chiral center, is an atom that is attached to four different types of atoms (or groups of atoms) in the tetrahedral arrangement. It can have either (R)- or (S)- configurations. Some compounds, such as racemic mixtures, have an undefined atom stereocenter, whose (R/S)-configuration is not specifically defined.", "Information": [ { "ReferenceNumber": 69, "Reference": [ "Computed by PubChem" ], "Value": { "Number": [ 0 ] } } ] }, { "TOCHeading": "Undefined Atom Stereocenter Count", "Description": "An atom stereocenter, also known as a chiral center, is an atom that is attached to four different types of atoms (or groups of atoms) in the tetrahedral arrangement. It can have either (R)- or (S)- configurations. Some compounds, such as racemic mixtures, have an undefined atom stereocenter, whose (R/S)-configuration is not specifically defined.", "Information": [ { "ReferenceNumber": 69, "Reference": [ "Computed by PubChem" ], "Value": { "Number": [ 1 ] } } ] }, { "TOCHeading": "Defined Bond Stereocenter Count", "Description": "A bond stereocenter is a non-rotatable bond around which two atoms can have different arrangement (as in cis- and trans-forms of butene around its double bond). Some compounds have an undefined bond stereocenter, whose stereochemistry is not specifically defined.", "Information": [ { "ReferenceNumber": 69, "Reference": [ "Computed by PubChem" ], "Value": { "Number": [ 0 ] } } ] }, { "TOCHeading": "Undefined Bond Stereocenter Count", "Description": "A bond stereocenter is a non-rotatable bond around which two atoms can have different arrangement (as in cis- and trans-forms of butene around its double bond). Some compounds have an undefined bond stereocenter, whose stereochemistry is not specifically defined.", "Information": [ { "ReferenceNumber": 69, "Reference": [ "Computed by PubChem" ], "Value": { "Number": [ 0 ] } } ] }, { "TOCHeading": "Covalently-Bonded Unit Count", "Description": "The number of separate chemical structures not connected by covalent bonds.", "Information": [ { "ReferenceNumber": 69, "Reference": [ "Computed by PubChem" ], "Value": { "Number": [ 1 ] } } ] }, { "TOCHeading": "Compound Is Canonicalized", "Description": "Whether the compound has successfully passed PubChem's valence bond canonicalization procedure. Some large, complex, or highly symmetric structures may fail this process.", "Information": [ { "ReferenceNumber": 69, "Reference": [ "Computed by PubChem (release 2021.05.07)" ], "Value": { "StringWithMarkup": [ { "String": "Yes" } ] } } ] } ] }, { "TOCHeading": "Experimental Properties", "Description": "Properties determined experimentally (See also Safety and Hazard Properties section for more information if available)", "Section": [ { "TOCHeading": "Physical Description", "Description": "Physical description refers to the appearance or features of a given chemical compound including color, odor, state, taste and more in general", "Information": [ { "ReferenceNumber": 19, "Value": { "StringWithMarkup": [ { "String": "Solid" } ] } } ] }, { "TOCHeading": "Color/Form", "Description": "Physical description - color", "Information": [ { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "Osol, A. and J.E. Hoover, et al. (eds.). Remington's Pharmaceutical Sciences. 15th ed. Easton, Pennsylvania: Mack Publishing Co., 1975., p. 1155" ], "Value": { "StringWithMarkup": [ { "String": "WHITE TO SLIGHTLY YELLOW, CRYSTALLINE POWDER" } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "Lewis, R.J. Sr.; Hawley's Condensed Chemical Dictionary 14th Edition. John Wiley & Sons, Inc. New York, NY 2001., p. 259" ], "Value": { "StringWithMarkup": [ { "String": "Colorless crystals" } ] } } ] }, { "TOCHeading": "Odor", "Description": "Physical description - odor", "Information": [ { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "Osol, A. and J.E. Hoover, et al. (eds.). Remington's Pharmaceutical Sciences. 15th ed. Easton, Pennsylvania: Mack Publishing Co., 1975., p. 1155" ], "Value": { "StringWithMarkup": [ { "String": "ODORLESS" } ] } } ] }, { "TOCHeading": "Taste", "Description": "Physical description - taste", "Information": [ { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "Lewis, R.J. Sr.; Hawley's Condensed Chemical Dictionary 14th Edition. John Wiley & Sons, Inc. New York, NY 2001., p. 259" ], "Value": { "StringWithMarkup": [ { "String": "Bitter taste" } ] } } ] }, { "TOCHeading": "Melting Point", "Description": "This section provides the melting point and/or freezing point. The melting point is the temperature at which a substance changes state from solid to liquid at atmospheric pressure. When considered as the temperature of the reverse change, from liquid to solid, it is referred to as the freezing point.", "Information": [ { "ReferenceNumber": 10, "Reference": [ "ChemSpider" ], "Value": { "StringWithMarkup": [ { "String": "87-89.5" } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "O'Neil, M.J. (ed.). The Merck Index - An Encyclopedia of Chemicals, Drugs, and Biologicals. 13th Edition, Whitehouse Station, NJ: Merck and Co., Inc., 2001., p. 373" ], "Value": { "StringWithMarkup": [ { "String": "87 °C" } ] } }, { "ReferenceNumber": 19, "Value": { "StringWithMarkup": [ { "String": "289°C" } ] } } ] }, { "TOCHeading": "Solubility", "Description": "The solubility of a substance is the amount of that substance that will dissolve in a given amount of solvent. The default solvent is water, if not indicated.", "Information": [ { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "O'Neil, M.J. (ed.). The Merck Index - An Encyclopedia of Chemicals, Drugs, and Biologicals. 13th Edition, Whitehouse Station, NJ: Merck and Co., Inc., 2001., p. 373" ], "Value": { "StringWithMarkup": [ { "String": "Bitter colorless crystals, dimorphic. Freely soluble in water, less sol in neutral or alkaline pH. Stable to heat in soln pH4 to 6.5. Practically in soluble in alcohol, benzene and chloroform /Diphosphate/", "Markup": [ { "Start": 56, "Length": 5, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/water", "Type": "PubChem Internal Link", "Extra": "CID-962" }, { "Start": 169, "Length": 7, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/benzene", "Type": "PubChem Internal Link", "Extra": "CID-241" }, { "Start": 181, "Length": 10, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroform", "Type": "PubChem Internal Link", "Extra": "CID-6212" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "Osol, A. and J.E. Hoover, et al. (eds.). Remington's Pharmaceutical Sciences. 15th ed. Easton, Pennsylvania: Mack Publishing Co., 1975., p. 1155" ], "Value": { "StringWithMarkup": [ { "String": "WHITE CRYSTALLINE POWDER; ODORLESS; BITTER TASTE; FREELY SOL IN WATER;PRACTICALLY INSOL IN ALCOHOL, CHLOROFORM, ETHER; AQ SOLN HAS PH OF ABOUT 4.5; PKA1= 7; PKA2= 9.2 /PHOSPHATE/", "Markup": [ { "Start": 64, "Length": 5, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/WATER", "Type": "PubChem Internal Link", "Extra": "CID-962" }, { "Start": 100, "Length": 10, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/CHLOROFORM", "Type": "PubChem Internal Link", "Extra": "CID-6212" }, { "Start": 168, "Length": 9, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/PHOSPHATE", "Type": "PubChem Internal Link", "Extra": "CID-1061" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "Osol, A. and J.E. Hoover, et al. (eds.). Remington's Pharmaceutical Sciences. 15th ed. Easton, Pennsylvania: Mack Publishing Co., 1975., p. 1155" ], "Value": { "StringWithMarkup": [ { "String": "VERY SLIGHTLY SOL IN WATER; SOL IN DIL ACIDS, CHLOROFORM, ETHER", "Markup": [ { "Start": 21, "Length": 5, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/WATER", "Type": "PubChem Internal Link", "Extra": "CID-962" }, { "Start": 46, "Length": 10, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/CHLOROFORM", "Type": "PubChem Internal Link", "Extra": "CID-6212" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "Lewis, R.J. Sr.; Hawley's Condensed Chemical Dictionary 14th Edition. John Wiley & Sons, Inc. New York, NY 2001., p. 259" ], "Value": { "StringWithMarkup": [ { "String": "Insoluble in alcohol, benzene, chloroform, ether.", "Markup": [ { "Start": 22, "Length": 7, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/benzene", "Type": "PubChem Internal Link", "Extra": "CID-241" }, { "Start": 31, "Length": 10, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroform", "Type": "PubChem Internal Link", "Extra": "CID-6212" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "US EPA; Estimation Program Interface (EPI) Suite. Ver.3.12. Nov 30, 2004. Available from, as of Dec 23, 2005: https://www.epa.gov/oppt/exposure/pubs/episuitedl.htm" ], "Value": { "StringWithMarkup": [ { "String": "In water, 0.14 mg/L at 25 °C (est)", "Markup": [ { "Start": 3, "Length": 5, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/water", "Type": "PubChem Internal Link", "Extra": "CID-962" } ] } ] } }, { "ReferenceNumber": 19, "Value": { "StringWithMarkup": [ { "String": "1.75e-02 g/L" } ] } } ] }, { "TOCHeading": "Vapor Pressure", "Description": "Vapor pressure is the pressure of a vapor in thermodynamic equilibrium with its condensed phases in a closed system.", "Information": [ { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "US EPA; Estimation Program Interface (EPI) Suite. Ver.3.12. Nov 30, 2004. Available from, as of Dec 23, 2005: https://www.epa.gov/oppt/exposure/pubs/episuitedl.htm" ], "Value": { "StringWithMarkup": [ { "String": "5.0X10-9 mm Hg at 25 °C (est)" } ] } } ] }, { "TOCHeading": "LogP", "Description": "Log P is the partition coefficient expressed in logarithmic form. The partition coefficient is the ratio of concentrations of a compound in a mixture of two immiscible solvents at equilibrium. This ratio is therefore used to compare the solubilities of the solute in these two solvents. Because octanol and water are the most commonly used pair of solvents for measuring partition coefficients, the Log P values listed in this section refer to \"octanol/water partition coefficients\", unless indicated otherwise.", "Information": [ { "ReferenceNumber": 10, "Reference": [ "HANSCH,C ET AL. (1995)" ], "Value": { "Number": [ 4.63 ] } }, { "ReferenceNumber": 12, "Reference": [ "HANSCH,C ET AL. (1995)" ], "Value": { "StringWithMarkup": [ { "String": "4.63 (LogP)" } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "Hansch, C., Leo, A., D. Hoekman. Exploring QSAR - Hydrophobic, Electronic, and Steric Constants. Washington, DC: American Chemical Society., 1995., p. 159" ], "Value": { "StringWithMarkup": [ { "String": "log Kow = 4.63" } ] } }, { "ReferenceNumber": 19, "Value": { "StringWithMarkup": [ { "String": "4.3" } ] } } ] }, { "TOCHeading": "Henrys Law Constant", "Description": "At a constant temperature, the amount of a given gas that dissolves in a given type and volume of liquid is directly proportional to the partial pressure of that gas in equilibrium with that liquid", "Information": [ { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "US EPA; Estimation Program Interface (EPI) Suite. Ver.3.12. Nov 30, 2004. Available from, as of Dec 23, 2005: https://www.epa.gov/oppt/exposure/pubs/episuitedl.htm" ], "Value": { "StringWithMarkup": [ { "String": "Henry's Law constant = 1.1X10-12 atm cu-m/mole at 25 °C (est)" } ] } } ] }, { "TOCHeading": "Stability/Shelf Life", "Description": "Tendency of a material to resist change or decomposition due to internal reaction, or due to the action of air, heat, light, pressure, etc. (See also Stability and Reactivity section under Safety and Hazards)", "Information": [ { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "O'Neil, M.J. (ed.). The Merck Index - An Encyclopedia of Chemicals, Drugs, and Biologicals. 13th Edition, Whitehouse Station, NJ: Merck and Co., Inc., 2001., p. 373" ], "Value": { "StringWithMarkup": [ { "String": "Stable to heat in solutions of pH 4.0 to 6.5 /Chloroquine Diphosphate/", "Markup": [ { "Start": 46, "Length": 23, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine%20Diphosphate", "Type": "PubChem Internal Link", "Extra": "CID-64927" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "Sunshine, I. (ed.). CRC Handbook of Analytical Toxicology. Cleveland: The Chemical Rubber Co., 1969., p. 28" ], "Value": { "StringWithMarkup": [ { "String": "SENSITIVE TO LIGHT. /PHOSPHATE/", "Markup": [ { "Start": 21, "Length": 9, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/PHOSPHATE", "Type": "PubChem Internal Link", "Extra": "CID-1061" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "Sunshine, I. (ed.). CRC Handbook of Analytical Toxicology. Cleveland: The Chemical Rubber Co., 1969., p. 28" ], "Value": { "StringWithMarkup": [ { "String": "SENSITIVE TO LIGHT. /SULFATE/", "Markup": [ { "Start": 21, "Length": 7, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/SULFATE", "Type": "PubChem Internal Link", "Extra": "CID-1117" } ] } ] } } ] }, { "TOCHeading": "Dissociation Constants", "Description": "A specific type of equilibrium constant that measures the propensity of a larger object to separate (dissociate) reversibly into smaller components, as when a complex falls apart into its component molecules, or when a salt splits up into its component ions. This includes pKa (the negative logarithm of the acid dissociation constant) and pKb (the negative logarithm of the base dissociation constant).", "Information": [ { "ReferenceNumber": 10, "Name": "pKa", "Reference": [ "SANGSTER (1994)" ], "Value": { "StringWithMarkup": [ { "String": "10.1" } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "Sangster J; LOGKOW Database. A databank of evaluated octanol-water partition coefficients (Log P). Available from, as of May 2, 2006: https://logkow.cisti.nrc.ca/logkow/search.html" ], "Value": { "StringWithMarkup": [ { "String": "pKa = 10.1" } ] } } ] }, { "TOCHeading": "Collision Cross Section", "Description": "Molecular collision cross section (CCS) values measured following ion mobility separation (IMS).", "URL": "https://doi.org/10.1002/mas.21585", "Information": [ { "ReferenceNumber": 2, "Reference": [ "https://www.sciencedirect.com/science/article/pii/S0021967318301894" ], "Value": { "StringWithMarkup": [ { "String": "176.8 Ų [M+H]+ [CCS Type: TW, Method: Major Mix IMS/Tof Calibration Kit (Waters)]", "Markup": [ { "Start": 14, "Length": 1, "Type": "Superscript" } ] } ] } } ] }, { "TOCHeading": "Kovats Retention Index", "Description": "Kovats (gas phase) retention index.", "URL": "http://en.wikipedia.org/wiki/Kovats_retention_index", "DisplayControls": { "CreateTable": { "FromInformationIn": "ThisSection", "NumberOfColumns": 2, "ColumnContents": [ "Name", "Value" ] }, "ListType": "CommaSeparated" }, "Information": [ { "ReferenceNumber": 53, "Name": "Standard non-polar", "Value": { "Number": [ 2600, 2610, 2630, 2637, 2660, 2578.2, 2590, 2660, 2642.7 ] } }, { "ReferenceNumber": 53, "Name": "Semi-standard non-polar", "Value": { "Number": [ 2626.3, 2604, 2624.8 ] } } ] }, { "TOCHeading": "Other Experimental Properties", "Description": "Additional property information.", "Information": [ { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "Osol, A. and J.E. Hoover, et al. (eds.). Remington's Pharmaceutical Sciences. 15th ed. Easton, Pennsylvania: Mack Publishing Co., 1975., p. 1155" ], "Value": { "StringWithMarkup": [ { "String": "USUALLY IS IN A PARTLY HYDRATED FORM" } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "Osol, A. and J.E. Hoover, et al. (eds.). Remington's Pharmaceutical Sciences. 15th ed. Easton, Pennsylvania: Mack Publishing Co., 1975., p. 1155" ], "Value": { "StringWithMarkup": [ { "String": "COLORLESS LIQUID; PH BETWEEN 5.5 & 6.5 /CHLOROQUINE HYDROCHLORIDE INJECTION/", "Markup": [ { "Start": 40, "Length": 25, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/CHLOROQUINE%20HYDROCHLORIDE", "Type": "PubChem Internal Link", "Extra": "CID-83820" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "Goodman, L.S., and A. Gilman. (eds.) The Pharmacological Basis of Therapeutics. 5th ed. New York: Macmillan Publishing Co., Inc., 1975., p. 1050" ], "Value": { "StringWithMarkup": [ { "String": "WHITE POWDER /CHLOROQUINE DIPHOSPHATE/", "Markup": [ { "Start": 14, "Length": 23, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/CHLOROQUINE%20DIPHOSPHATE", "Type": "PubChem Internal Link", "Extra": "CID-64927" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "Lewis, R.J. Sax's Dangerous Properties of Industrial Materials. 10th ed. Volumes 1-3 New York, NY: John Wiley & Sons Inc., 1999., p. 899" ], "Value": { "StringWithMarkup": [ { "String": "Upon decomosition emits NOx" } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "US EPA; Estimation Program Interface (EPI) Suite. Ver.3.12. Nov 30, 2004. Available from, as of Dec 23, 2005: https://www.epa.gov/oppt/exposure/pubs/episuitedl.htm" ], "Value": { "StringWithMarkup": [ { "String": "Hydroxyl radical reaction rate constant = 1.5X10-10 cu cm/molec-sec at 25 °C (est)" } ] } } ] } ] } ] }, { "TOCHeading": "Spectral Information", "Description": "Spectral data for chemical compounds", "Section": [ { "TOCHeading": "1D NMR Spectra", "Description": "1D NMR Spectra data or Linking.", "DisplayControls": { "CreateTable": { "FromInformationIn": "ThisSection", "NumberOfColumns": 2, "ColumnContents": [ "Name", "Value" ] }, "ShowAtMost": 2 }, "Section": [ { "TOCHeading": "13C NMR Spectra", "Description": "Carbon-13 NMR (13C NMR or CMR) is the application of nuclear magnetic resonance (NMR) spectroscopy to carbon isotope 13.", "DisplayControls": { "CreateTable": { "FromInformationIn": "ThisSection", "NumberOfColumns": 2, "ColumnContents": [ "Name", "Value" ] }, "ShowAtMost": 2 }, "Information": [ { "ReferenceNumber": 58, "Name": "Copyright", "Value": { "StringWithMarkup": [ { "String": "Copyright © 2016-2021 W. Robien, Inst. of Org. Chem., Univ. of Vienna. All Rights Reserved." } ] } }, { "ReferenceNumber": 58, "Name": "Thumbnail", "URL": "https://spectrabase.com/spectrum/10sStszu4T5", "Value": { "ExternalDataURL": [ "https://pubchem.ncbi.nlm.nih.gov/rest/pug_view/data/key/5068776_1" ], "MimeType": "image/png" } }, { "ReferenceNumber": 59, "Name": "Instrument Name", "Value": { "StringWithMarkup": [ { "String": "Bruker AM-400" } ] } }, { "ReferenceNumber": 59, "Name": "Copyright", "Value": { "StringWithMarkup": [ { "String": "Copyright © 2002-2021 Wiley-VCH Verlag GmbH & Co. KGaA. All Rights Reserved." } ] } }, { "ReferenceNumber": 59, "Name": "Thumbnail", "URL": "https://spectrabase.com/spectrum/E4IWgm7hoj9", "Value": { "ExternalDataURL": [ "https://pubchem.ncbi.nlm.nih.gov/rest/pug_view/data/key/10722562_1" ], "MimeType": "image/png" } } ] } ] }, { "TOCHeading": "Mass Spectrometry", "Description": "Mass spectrometry (MS or mass spec) is a technique to determine molecular structure through ionization and fragmentation of the parent compound into smaller components.", "DisplayControls": { "CreateTable": { "FromInformationIn": "ThisSection", "NumberOfColumns": 2, "ColumnContents": [ "Name", "Value" ] }, "ShowAtMost": 2 }, "Section": [ { "TOCHeading": "GC-MS", "Description": "Data from GC-MS experiments.", "DisplayControls": { "CreateTable": { "FromInformationIn": "ThisSection", "NumberOfColumns": 2, "ColumnContents": [ "Name", "Value" ] }, "ListType": "Columns", "ShowAtMost": 2 }, "Information": [ { "ReferenceNumber": 20, "Name": "Spectra ID", "URL": "https://hmdb.ca/spectra/c_ms/27431", "Value": { "StringWithMarkup": [ { "String": "27431" } ] } }, { "ReferenceNumber": 20, "Name": "Instrument Type", "Value": { "StringWithMarkup": [ { "String": "CI-B" } ] } }, { "ReferenceNumber": 20, "Name": "Ionization Mode", "Value": { "StringWithMarkup": [ { "String": "positive" } ] } }, { "ReferenceNumber": 20, "Name": "SPLASH", "URL": "https://mona.fiehnlab.ucdavis.edu/spectra/browse?query=splash.splash%3D%3D%22splash10-00di-0009000000-d54119d64cfc341cee7d%22", "Value": { "StringWithMarkup": [ { "String": "splash10-00di-0009000000-d54119d64cfc341cee7d" } ] } }, { "ReferenceNumber": 20, "Name": "Top 5 Peaks", "Value": { "StringWithMarkup": [ { "String": "320.0 99.99" }, { "String": "322.0 34" }, { "String": "321.0 21" }, { "String": "323.0 7" }, { "String": "319.0 5" } ] } }, { "ReferenceNumber": 20, "Name": "Thumbnail", "URL": "https://pubchem.ncbi.nlm.nih.gov/image/ms.cgi?peaks=320.0:99.99,322.0:34,321.0:21,323.0:7,319.0:5", "Value": { "ExternalDataURL": [ "https://pubchem.ncbi.nlm.nih.gov/image/ms.cgi?peaks=320.0:99.99,322.0:34,321.0:21,323.0:7,319.0:5" ], "MimeType": "image/svg" } }, { "ReferenceNumber": 20, "Name": "Notes", "Value": { "StringWithMarkup": [ { "String": "instrument=Unknown" } ] } }, { "ReferenceNumber": 31, "Name": "MoNA ID", "URL": "https://mona.fiehnlab.ucdavis.edu/spectra/display/JP003161", "Value": { "StringWithMarkup": [ { "String": "JP003161" } ] } }, { "ReferenceNumber": 31, "Name": "MS Category", "Value": { "StringWithMarkup": [ { "String": "Experimental" } ] } }, { "ReferenceNumber": 31, "Name": "MS Type", "Value": { "StringWithMarkup": [ { "String": "GC-MS" } ] } }, { "ReferenceNumber": 31, "Name": "MS Level", "Value": { "StringWithMarkup": [ { "String": "MS1" } ] } }, { "ReferenceNumber": 31, "Name": "Instrument", "Value": { "StringWithMarkup": [ { "String": "Unknown" } ] } }, { "ReferenceNumber": 31, "Name": "Instrument Type", "Value": { "StringWithMarkup": [ { "String": "CI-B" } ] } }, { "ReferenceNumber": 31, "Name": "Ionization Mode", "Value": { "StringWithMarkup": [ { "String": "positive" } ] } }, { "ReferenceNumber": 31, "Name": "Top 5 Peaks", "Value": { "StringWithMarkup": [ { "String": "320 99.99" }, { "String": "322 34" }, { "String": "321 21" }, { "String": "323 7" }, { "String": "319 5" } ] } }, { "ReferenceNumber": 31, "Name": "SPLASH", "URL": "https://mona.fiehnlab.ucdavis.edu/spectra/browse?query=splash.splash%3D%3D%22splash10-00di-0009000000-d54119d64cfc341cee7d%22", "Value": { "StringWithMarkup": [ { "String": "splash10-00di-0009000000-d54119d64cfc341cee7d" } ] } }, { "ReferenceNumber": 31, "Name": "Thumbnail", "URL": "https://pubchem.ncbi.nlm.nih.gov/image/ms.cgi?peaks=320:99.99,322:34,321:21,323:7,319:5", "Value": { "ExternalDataURL": [ "https://pubchem.ncbi.nlm.nih.gov/image/ms.cgi?peaks=320:99.99,322:34,321:21,323:7,319:5" ], "MimeType": "image/svg" } }, { "ReferenceNumber": 31, "Name": "Submitter", "Value": { "StringWithMarkup": [ { "String": "University of Tokyo Team, Faculty of Engineering, University of Tokyo" } ] } }, { "ReferenceNumber": 36, "Name": "MoNA ID", "URL": "https://mona.fiehnlab.ucdavis.edu/spectra/display/HMDB0014746_c_ms_100159", "Value": { "StringWithMarkup": [ { "String": "HMDB0014746_c_ms_100159" } ] } }, { "ReferenceNumber": 36, "Name": "MS Category", "Value": { "StringWithMarkup": [ { "String": "Experimental" } ] } }, { "ReferenceNumber": 36, "Name": "MS Type", "Value": { "StringWithMarkup": [ { "String": "GC-MS" } ] } }, { "ReferenceNumber": 36, "Name": "Instrument", "Value": { "StringWithMarkup": [ { "String": "Unknown" } ] } }, { "ReferenceNumber": 36, "Name": "Instrument Type", "Value": { "StringWithMarkup": [ { "String": "CI-B" } ] } }, { "ReferenceNumber": 36, "Name": "Ionization Mode", "Value": { "StringWithMarkup": [ { "String": "positive" } ] } }, { "ReferenceNumber": 36, "Name": "Top 5 Peaks", "Value": { "StringWithMarkup": [ { "String": "320.0 99.99" }, { "String": "322.0 34" }, { "String": "321.0 21" }, { "String": "323.0 7" }, { "String": "319.0 5" } ] } }, { "ReferenceNumber": 36, "Name": "SPLASH", "URL": "https://mona.fiehnlab.ucdavis.edu/spectra/browse?query=splash.splash%3D%3D%22splash10-00di-0009000000-d54119d64cfc341cee7d%22", "Value": { "StringWithMarkup": [ { "String": "splash10-00di-0009000000-d54119d64cfc341cee7d" } ] } }, { "ReferenceNumber": 36, "Name": "Thumbnail", "URL": "https://pubchem.ncbi.nlm.nih.gov/image/ms.cgi?peaks=320.0:99.99,322.0:34,321.0:21,323.0:7,319.0:5", "Value": { "ExternalDataURL": [ "https://pubchem.ncbi.nlm.nih.gov/image/ms.cgi?peaks=320.0:99.99,322.0:34,321.0:21,323.0:7,319.0:5" ], "MimeType": "image/svg" } }, { "ReferenceNumber": 36, "Name": "Submitter", "Value": { "StringWithMarkup": [ { "String": "David Wishart, University of Alberta" } ] } }, { "ReferenceNumber": 43, "Name": "NIST Number", "Value": { "Number": [ 42361 ] } }, { "ReferenceNumber": 43, "Name": "Library", "Value": { "StringWithMarkup": [ { "String": "Main library" } ] } }, { "ReferenceNumber": 43, "Name": "Total Peaks", "Value": { "Number": [ 145 ] } }, { "ReferenceNumber": 43, "Name": "m/z Top Peak", "Value": { "Number": [ 86 ] } }, { "ReferenceNumber": 43, "Name": "m/z 2nd Highest", "Value": { "Number": [ 30 ] } }, { "ReferenceNumber": 43, "Name": "m/z 3rd Highest", "Value": { "Number": [ 58 ] } }, { "ReferenceNumber": 43, "Name": "Thumbnail", "Value": { "ExternalDataURL": [ "https://pubchem.ncbi.nlm.nih.gov/rest/pug_view/data/key/61394_1" ], "MimeType": "image/png" } }, { "ReferenceNumber": 44, "Name": "NIST Number", "Value": { "Number": [ 250714 ] } }, { "ReferenceNumber": 44, "Name": "Library", "Value": { "StringWithMarkup": [ { "String": "Replicate library" } ] } }, { "ReferenceNumber": 44, "Name": "Total Peaks", "Value": { "Number": [ 183 ] } }, { "ReferenceNumber": 44, "Name": "m/z Top Peak", "Value": { "Number": [ 86 ] } }, { "ReferenceNumber": 44, "Name": "m/z 2nd Highest", "Value": { "Number": [ 58 ] } }, { "ReferenceNumber": 44, "Name": "m/z 3rd Highest", "Value": { "Number": [ 30 ] } }, { "ReferenceNumber": 44, "Name": "Thumbnail", "Value": { "ExternalDataURL": [ "https://pubchem.ncbi.nlm.nih.gov/rest/pug_view/data/key/260140_1" ], "MimeType": "image/png" } }, { "ReferenceNumber": 45, "Name": "NIST Number", "Value": { "Number": [ 378097 ] } }, { "ReferenceNumber": 45, "Name": "Library", "Value": { "StringWithMarkup": [ { "String": "Replicate library" } ] } }, { "ReferenceNumber": 45, "Name": "Total Peaks", "Value": { "Number": [ 157 ] } }, { "ReferenceNumber": 45, "Name": "m/z Top Peak", "Value": { "Number": [ 86 ] } }, { "ReferenceNumber": 45, "Name": "m/z 2nd Highest", "Value": { "Number": [ 58 ] } }, { "ReferenceNumber": 45, "Name": "m/z 3rd Highest", "Value": { "Number": [ 42 ] } }, { "ReferenceNumber": 45, "Name": "Thumbnail", "Value": { "ExternalDataURL": [ "https://pubchem.ncbi.nlm.nih.gov/rest/pug_view/data/key/260147_1" ], "MimeType": "image/png" } }, { "ReferenceNumber": 46, "Name": "NIST Number", "Value": { "Number": [ 15077 ] } }, { "ReferenceNumber": 46, "Name": "Library", "Value": { "StringWithMarkup": [ { "String": "Replicate library" } ] } }, { "ReferenceNumber": 46, "Name": "Total Peaks", "Value": { "Number": [ 59 ] } }, { "ReferenceNumber": 46, "Name": "m/z Top Peak", "Value": { "Number": [ 86 ] } }, { "ReferenceNumber": 46, "Name": "m/z 2nd Highest", "Value": { "Number": [ 58 ] } }, { "ReferenceNumber": 46, "Name": "m/z 3rd Highest", "Value": { "Number": [ 73 ] } }, { "ReferenceNumber": 46, "Name": "Thumbnail", "Value": { "ExternalDataURL": [ "https://pubchem.ncbi.nlm.nih.gov/rest/pug_view/data/key/260153_1" ], "MimeType": "image/png" } }, { "ReferenceNumber": 47, "Name": "NIST Number", "Value": { "Number": [ 312956 ] } }, { "ReferenceNumber": 47, "Name": "Library", "Value": { "StringWithMarkup": [ { "String": "Replicate library" } ] } }, { "ReferenceNumber": 47, "Name": "Total Peaks", "Value": { "Number": [ 133 ] } }, { "ReferenceNumber": 47, "Name": "m/z Top Peak", "Value": { "Number": [ 86 ] } }, { "ReferenceNumber": 47, "Name": "m/z 2nd Highest", "Value": { "Number": [ 58 ] } }, { "ReferenceNumber": 47, "Name": "m/z 3rd Highest", "Value": { "Number": [ 87 ] } }, { "ReferenceNumber": 47, "Name": "Thumbnail", "Value": { "ExternalDataURL": [ "https://pubchem.ncbi.nlm.nih.gov/rest/pug_view/data/key/260155_1" ], "MimeType": "image/png" } }, { "ReferenceNumber": 48, "Name": "NIST Number", "Value": { "Number": [ 379514 ] } }, { "ReferenceNumber": 48, "Name": "Library", "Value": { "StringWithMarkup": [ { "String": "Replicate library" } ] } }, { "ReferenceNumber": 48, "Name": "Total Peaks", "Value": { "Number": [ 142 ] } }, { "ReferenceNumber": 48, "Name": "m/z Top Peak", "Value": { "Number": [ 86 ] } }, { "ReferenceNumber": 48, "Name": "m/z 2nd Highest", "Value": { "Number": [ 58 ] } }, { "ReferenceNumber": 48, "Name": "m/z 3rd Highest", "Value": { "Number": [ 87 ] } }, { "ReferenceNumber": 48, "Name": "Thumbnail", "Value": { "ExternalDataURL": [ "https://pubchem.ncbi.nlm.nih.gov/rest/pug_view/data/key/260156_1" ], "MimeType": "image/png" } }, { "ReferenceNumber": 49, "Name": "NIST Number", "Value": { "Number": [ 246903 ] } }, { "ReferenceNumber": 49, "Name": "Library", "Value": { "StringWithMarkup": [ { "String": "Replicate library" } ] } }, { "ReferenceNumber": 49, "Name": "Total Peaks", "Value": { "Number": [ 184 ] } }, { "ReferenceNumber": 49, "Name": "m/z Top Peak", "Value": { "Number": [ 86 ] } }, { "ReferenceNumber": 49, "Name": "m/z 2nd Highest", "Value": { "Number": [ 319 ] } }, { "ReferenceNumber": 49, "Name": "m/z 3rd Highest", "Value": { "Number": [ 58 ] } }, { "ReferenceNumber": 49, "Name": "Thumbnail", "Value": { "ExternalDataURL": [ "https://pubchem.ncbi.nlm.nih.gov/rest/pug_view/data/key/260300_1" ], "MimeType": "image/png" } }, { "ReferenceNumber": 56, "Name": "Source of Spectrum", "Value": { "StringWithMarkup": [ { "String": "Mass Spectrometry Committee of the Toxicology Section of the American Academy of Forensic Sciences" } ] } }, { "ReferenceNumber": 56, "Name": "Copyright", "Value": { "StringWithMarkup": [ { "String": "Copyright © 2012-2021 John Wiley & Sons, Inc. Portions provided by AAFS, Toxicology Section. All Rights Reserved." } ] } }, { "ReferenceNumber": 56, "Name": "Thumbnail", "URL": "https://spectrabase.com/spectrum/30UDEp4qVU", "Value": { "ExternalDataURL": [ "https://pubchem.ncbi.nlm.nih.gov/rest/pug_view/data/key/5068772_1" ], "MimeType": "image/png" } }, { "ReferenceNumber": 57, "Name": "Source of Spectrum", "Value": { "StringWithMarkup": [ { "String": "Mass Spectrometry Committee of the Toxicology Section of the American Academy of Forensic Sciences" } ] } }, { "ReferenceNumber": 57, "Name": "Copyright", "Value": { "StringWithMarkup": [ { "String": "Copyright © 2012-2021 John Wiley & Sons, Inc. Portions provided by AAFS, Toxicology Section. All Rights Reserved." } ] } }, { "ReferenceNumber": 57, "Name": "Thumbnail", "URL": "https://spectrabase.com/spectrum/BrpTswYWahi", "Value": { "ExternalDataURL": [ "https://pubchem.ncbi.nlm.nih.gov/rest/pug_view/data/key/5068773_1" ], "MimeType": "image/png" } } ] }, { "TOCHeading": "MS-MS", "Description": "Data from MS-MS experiments.", "DisplayControls": { "CreateTable": { "FromInformationIn": "ThisSection", "NumberOfColumns": 2, "ColumnContents": [ "Name", "Value" ] }, "ListType": "Columns", "ShowAtMost": 2 }, "Information": [ { "ReferenceNumber": 50, "Name": "NIST Number", "Value": { "Number": [ 1181214 ] } }, { "ReferenceNumber": 50, "Name": "Instrument Type", "Value": { "StringWithMarkup": [ { "String": "IT/ion trap" } ] } }, { "ReferenceNumber": 50, "Name": "Collision Energy", "Value": { "Number": [ 0 ] } }, { "ReferenceNumber": 50, "Name": "Spectrum Type", "Value": { "StringWithMarkup": [ { "String": "MS2" } ] } }, { "ReferenceNumber": 50, "Name": "Precursor Type", "Value": { "StringWithMarkup": [ { "String": "[M+H]+" } ] } }, { "ReferenceNumber": 50, "Name": "Precursor m/z", "Value": { "Number": [ 320.1888 ] } }, { "ReferenceNumber": 50, "Name": "Total Peaks", "Value": { "Number": [ 6 ] } }, { "ReferenceNumber": 50, "Name": "m/z Top Peak", "Value": { "Number": [ 247.1 ] } }, { "ReferenceNumber": 50, "Name": "m/z 2nd Highest", "Value": { "Number": [ 142.2 ] } }, { "ReferenceNumber": 50, "Name": "m/z 3rd Highest", "Value": { "Number": [ 164 ] } }, { "ReferenceNumber": 50, "Name": "Thumbnail", "Value": { "ExternalDataURL": [ "https://pubchem.ncbi.nlm.nih.gov/rest/pug_view/data/key/282935_1" ], "MimeType": "image/png" } }, { "ReferenceNumber": 51, "Name": "NIST Number", "Value": { "Number": [ 1181230 ] } }, { "ReferenceNumber": 51, "Name": "Instrument Type", "Value": { "StringWithMarkup": [ { "String": "IT/ion trap" } ] } }, { "ReferenceNumber": 51, "Name": "Collision Energy", "Value": { "Number": [ 0 ] } }, { "ReferenceNumber": 51, "Name": "Spectrum Type", "Value": { "StringWithMarkup": [ { "String": "MS2" } ] } }, { "ReferenceNumber": 51, "Name": "Precursor Type", "Value": { "StringWithMarkup": [ { "String": "[M+2H]2+" } ] } }, { "ReferenceNumber": 51, "Name": "Precursor m/z", "Value": { "Number": [ 160.598 ] } }, { "ReferenceNumber": 51, "Name": "Total Peaks", "Value": { "Number": [ 34 ] } }, { "ReferenceNumber": 51, "Name": "m/z Top Peak", "Value": { "Number": [ 146.5 ] } }, { "ReferenceNumber": 51, "Name": "m/z 2nd Highest", "Value": { "Number": [ 147 ] } }, { "ReferenceNumber": 51, "Name": "m/z 3rd Highest", "Value": { "Number": [ 132.5 ] } }, { "ReferenceNumber": 51, "Name": "Thumbnail", "Value": { "ExternalDataURL": [ "https://pubchem.ncbi.nlm.nih.gov/rest/pug_view/data/key/282936_1" ], "MimeType": "image/png" } }, { "ReferenceNumber": 52, "Name": "NIST Number", "Value": { "Number": [ 1006454 ] } }, { "ReferenceNumber": 52, "Name": "Instrument Type", "Value": { "StringWithMarkup": [ { "String": "IT/ion trap" } ] } }, { "ReferenceNumber": 52, "Name": "Collision Energy", "Value": { "Number": [ 0 ] } }, { "ReferenceNumber": 52, "Name": "Spectrum Type", "Value": { "StringWithMarkup": [ { "String": "MS2" } ] } }, { "ReferenceNumber": 52, "Name": "Precursor Type", "Value": { "StringWithMarkup": [ { "String": "[M+H]+" } ] } }, { "ReferenceNumber": 52, "Name": "Precursor m/z", "Value": { "Number": [ 320.1888 ] } }, { "ReferenceNumber": 52, "Name": "Total Peaks", "Value": { "Number": [ 5 ] } }, { "ReferenceNumber": 52, "Name": "m/z Top Peak", "Value": { "Number": [ 247 ] } }, { "ReferenceNumber": 52, "Name": "m/z 2nd Highest", "Value": { "Number": [ 142 ] } }, { "ReferenceNumber": 52, "Name": "m/z 3rd Highest", "Value": { "Number": [ 164 ] } }, { "ReferenceNumber": 52, "Name": "Thumbnail", "Value": { "ExternalDataURL": [ "https://pubchem.ncbi.nlm.nih.gov/rest/pug_view/data/key/285619_1" ], "MimeType": "image/png" } } ] }, { "TOCHeading": "LC-MS", "Description": "Linking to LC-MS spectrum.", "DisplayControls": { "CreateTable": { "FromInformationIn": "ThisSection", "NumberOfColumns": 2, "ColumnContents": [ "Name", "Value" ] }, "ShowAtMost": 2 }, "Information": [ { "ReferenceNumber": 25, "Name": "Accession ID", "URL": "https://massbank.eu/MassBank/RecordDisplay?id=WA000965", "Value": { "StringWithMarkup": [ { "String": "WA000965" } ] } }, { "ReferenceNumber": 25, "Name": "Authors", "Value": { "StringWithMarkup": [ { "String": "Nihon Waters K.K." } ] } }, { "ReferenceNumber": 25, "Name": "Instrument", "Value": { "StringWithMarkup": [ { "String": "ZQ, Waters" } ] } }, { "ReferenceNumber": 25, "Name": "Instrument Type", "Value": { "StringWithMarkup": [ { "String": "LC-ESI-Q" } ] } }, { "ReferenceNumber": 25, "Name": "MS Level", "Value": { "StringWithMarkup": [ { "String": "MS" } ] } }, { "ReferenceNumber": 25, "Name": "Ionization Mode", "Value": { "StringWithMarkup": [ { "String": "POSITIVE" } ] } }, { "ReferenceNumber": 25, "Name": "Ionization", "Value": { "StringWithMarkup": [ { "String": "ESI" } ] } }, { "ReferenceNumber": 25, "Name": "Column Name", "Value": { "StringWithMarkup": [ { "String": "2.1 mm id - 3. 5{mu}m XTerra C18MS" } ] } }, { "ReferenceNumber": 25, "Name": "Retention Time", "Value": { "StringWithMarkup": [ { "String": "9.800 min" } ] } }, { "ReferenceNumber": 25, "Name": "Top 5 Peaks", "Value": { "StringWithMarkup": [ { "String": "179 999" }, { "String": "191 494" }, { "String": "181 341" }, { "String": "247 306" }, { "String": "205 215" } ] } }, { "ReferenceNumber": 25, "Name": "SPLASH", "URL": "https://massbank.eu/MassBank/Result.jsp?splash=splash10-002f-0920000000-90f3db87cbeed5fd67c6", "Value": { "StringWithMarkup": [ { "String": "splash10-002f-0920000000-90f3db87cbeed5fd67c6" } ] } }, { "ReferenceNumber": 25, "Name": "Thumbnail", "URL": "https://pubchem.ncbi.nlm.nih.gov/image/ms.cgi?peaks=179:999,191:494,181:341,247:306,205:215", "Value": { "ExternalDataURL": [ "https://pubchem.ncbi.nlm.nih.gov/image/ms.cgi?peaks=179:999,191:494,181:341,247:306,205:215" ], "MimeType": "image/svg" } }, { "ReferenceNumber": 25, "Name": "License", "Value": { "StringWithMarkup": [ { "String": "CC BY-NC" } ] } }, { "ReferenceNumber": 26, "Name": "Accession ID", "URL": "https://massbank.eu/MassBank/RecordDisplay?id=WA000966", "Value": { "StringWithMarkup": [ { "String": "WA000966" } ] } }, { "ReferenceNumber": 26, "Name": "Authors", "Value": { "StringWithMarkup": [ { "String": "Nihon Waters K.K." } ] } }, { "ReferenceNumber": 26, "Name": "Instrument", "Value": { "StringWithMarkup": [ { "String": "ZQ, Waters" } ] } }, { "ReferenceNumber": 26, "Name": "Instrument Type", "Value": { "StringWithMarkup": [ { "String": "LC-ESI-Q" } ] } }, { "ReferenceNumber": 26, "Name": "MS Level", "Value": { "StringWithMarkup": [ { "String": "MS" } ] } }, { "ReferenceNumber": 26, "Name": "Ionization Mode", "Value": { "StringWithMarkup": [ { "String": "POSITIVE" } ] } }, { "ReferenceNumber": 26, "Name": "Ionization", "Value": { "StringWithMarkup": [ { "String": "ESI" } ] } }, { "ReferenceNumber": 26, "Name": "Column Name", "Value": { "StringWithMarkup": [ { "String": "2.1 mm id - 3. 5{mu}m XTerra C18MS" } ] } }, { "ReferenceNumber": 26, "Name": "Retention Time", "Value": { "StringWithMarkup": [ { "String": "9.800 min" } ] } }, { "ReferenceNumber": 26, "Name": "Top 5 Peaks", "Value": { "StringWithMarkup": [ { "String": "247 999" }, { "String": "179 686" }, { "String": "142 443" }, { "String": "191 380" }, { "String": "249 345" } ] } }, { "ReferenceNumber": 26, "Name": "SPLASH", "URL": "https://massbank.eu/MassBank/Result.jsp?splash=splash10-002e-0950000000-849a8e9960219d54f689", "Value": { "StringWithMarkup": [ { "String": "splash10-002e-0950000000-849a8e9960219d54f689" } ] } }, { "ReferenceNumber": 26, "Name": "Thumbnail", "URL": "https://pubchem.ncbi.nlm.nih.gov/image/ms.cgi?peaks=247:999,179:686,142:443,191:380,249:345", "Value": { "ExternalDataURL": [ "https://pubchem.ncbi.nlm.nih.gov/image/ms.cgi?peaks=247:999,179:686,142:443,191:380,249:345" ], "MimeType": "image/svg" } }, { "ReferenceNumber": 26, "Name": "License", "Value": { "StringWithMarkup": [ { "String": "CC BY-NC" } ] } }, { "ReferenceNumber": 27, "Name": "Accession ID", "URL": "https://massbank.eu/MassBank/RecordDisplay?id=WA000967", "Value": { "StringWithMarkup": [ { "String": "WA000967" } ] } }, { "ReferenceNumber": 27, "Name": "Authors", "Value": { "StringWithMarkup": [ { "String": "Nihon Waters K.K." } ] } }, { "ReferenceNumber": 27, "Name": "Instrument", "Value": { "StringWithMarkup": [ { "String": "ZQ, Waters" } ] } }, { "ReferenceNumber": 27, "Name": "Instrument Type", "Value": { "StringWithMarkup": [ { "String": "LC-ESI-Q" } ] } }, { "ReferenceNumber": 27, "Name": "MS Level", "Value": { "StringWithMarkup": [ { "String": "MS" } ] } }, { "ReferenceNumber": 27, "Name": "Ionization Mode", "Value": { "StringWithMarkup": [ { "String": "POSITIVE" } ] } }, { "ReferenceNumber": 27, "Name": "Ionization", "Value": { "StringWithMarkup": [ { "String": "ESI" } ] } }, { "ReferenceNumber": 27, "Name": "Column Name", "Value": { "StringWithMarkup": [ { "String": "2.1 mm id - 3. 5{mu}m XTerra C18MS" } ] } }, { "ReferenceNumber": 27, "Name": "Retention Time", "Value": { "StringWithMarkup": [ { "String": "9.800 min" } ] } }, { "ReferenceNumber": 27, "Name": "Top 5 Peaks", "Value": { "StringWithMarkup": [ { "String": "247 999" }, { "String": "142 470" }, { "String": "249 364" }, { "String": "179 172" }, { "String": "191 78" } ] } }, { "ReferenceNumber": 27, "Name": "SPLASH", "URL": "https://massbank.eu/MassBank/Result.jsp?splash=splash10-0002-0690000000-f317eb87cceee189094a", "Value": { "StringWithMarkup": [ { "String": "splash10-0002-0690000000-f317eb87cceee189094a" } ] } }, { "ReferenceNumber": 27, "Name": "Thumbnail", "URL": "https://pubchem.ncbi.nlm.nih.gov/image/ms.cgi?peaks=247:999,142:470,249:364,179:172,191:78", "Value": { "ExternalDataURL": [ "https://pubchem.ncbi.nlm.nih.gov/image/ms.cgi?peaks=247:999,142:470,249:364,179:172,191:78" ], "MimeType": "image/svg" } }, { "ReferenceNumber": 27, "Name": "License", "Value": { "StringWithMarkup": [ { "String": "CC BY-NC" } ] } }, { "ReferenceNumber": 28, "Name": "Accession ID", "URL": "https://massbank.eu/MassBank/RecordDisplay?id=WA000968", "Value": { "StringWithMarkup": [ { "String": "WA000968" } ] } }, { "ReferenceNumber": 28, "Name": "Authors", "Value": { "StringWithMarkup": [ { "String": "Nihon Waters K.K." } ] } }, { "ReferenceNumber": 28, "Name": "Instrument", "Value": { "StringWithMarkup": [ { "String": "ZQ, Waters" } ] } }, { "ReferenceNumber": 28, "Name": "Instrument Type", "Value": { "StringWithMarkup": [ { "String": "LC-ESI-Q" } ] } }, { "ReferenceNumber": 28, "Name": "MS Level", "Value": { "StringWithMarkup": [ { "String": "MS" } ] } }, { "ReferenceNumber": 28, "Name": "Ionization Mode", "Value": { "StringWithMarkup": [ { "String": "POSITIVE" } ] } }, { "ReferenceNumber": 28, "Name": "Ionization", "Value": { "StringWithMarkup": [ { "String": "ESI" } ] } }, { "ReferenceNumber": 28, "Name": "Column Name", "Value": { "StringWithMarkup": [ { "String": "2.1 mm id - 3. 5{mu}m XTerra C18MS" } ] } }, { "ReferenceNumber": 28, "Name": "Retention Time", "Value": { "StringWithMarkup": [ { "String": "9.800 min" } ] } }, { "ReferenceNumber": 28, "Name": "Top 5 Peaks", "Value": { "StringWithMarkup": [ { "String": "247 999" }, { "String": "320 529" }, { "String": "142 357" }, { "String": "249 349" }, { "String": "322 192" } ] } }, { "ReferenceNumber": 28, "Name": "SPLASH", "URL": "https://massbank.eu/MassBank/Result.jsp?splash=splash10-0002-0394000000-811a6863cd54caddde50", "Value": { "StringWithMarkup": [ { "String": "splash10-0002-0394000000-811a6863cd54caddde50" } ] } }, { "ReferenceNumber": 28, "Name": "Thumbnail", "URL": "https://pubchem.ncbi.nlm.nih.gov/image/ms.cgi?peaks=247:999,320:529,142:357,249:349,322:192", "Value": { "ExternalDataURL": [ "https://pubchem.ncbi.nlm.nih.gov/image/ms.cgi?peaks=247:999,320:529,142:357,249:349,322:192" ], "MimeType": "image/svg" } }, { "ReferenceNumber": 28, "Name": "License", "Value": { "StringWithMarkup": [ { "String": "CC BY-NC" } ] } }, { "ReferenceNumber": 29, "Name": "Accession ID", "URL": "https://massbank.eu/MassBank/RecordDisplay?id=WA000969", "Value": { "StringWithMarkup": [ { "String": "WA000969" } ] } }, { "ReferenceNumber": 29, "Name": "Authors", "Value": { "StringWithMarkup": [ { "String": "Nihon Waters K.K." } ] } }, { "ReferenceNumber": 29, "Name": "Instrument", "Value": { "StringWithMarkup": [ { "String": "ZQ, Waters" } ] } }, { "ReferenceNumber": 29, "Name": "Instrument Type", "Value": { "StringWithMarkup": [ { "String": "LC-ESI-Q" } ] } }, { "ReferenceNumber": 29, "Name": "MS Level", "Value": { "StringWithMarkup": [ { "String": "MS" } ] } }, { "ReferenceNumber": 29, "Name": "Ionization Mode", "Value": { "StringWithMarkup": [ { "String": "POSITIVE" } ] } }, { "ReferenceNumber": 29, "Name": "Ionization", "Value": { "StringWithMarkup": [ { "String": "ESI" } ] } }, { "ReferenceNumber": 29, "Name": "Column Name", "Value": { "StringWithMarkup": [ { "String": "2.1 mm id - 3. 5{mu}m XTerra C18MS" } ] } }, { "ReferenceNumber": 29, "Name": "Retention Time", "Value": { "StringWithMarkup": [ { "String": "9.800 min" } ] } }, { "ReferenceNumber": 29, "Name": "Top 5 Peaks", "Value": { "StringWithMarkup": [ { "String": "320 999" }, { "String": "322 360" }, { "String": "161 231" }, { "String": "321 153" }, { "String": "247 102" } ] } }, { "ReferenceNumber": 29, "Name": "SPLASH", "URL": "https://massbank.eu/MassBank/Result.jsp?splash=splash10-00di-0209000000-52baee7b914fe967f2ac", "Value": { "StringWithMarkup": [ { "String": "splash10-00di-0209000000-52baee7b914fe967f2ac" } ] } }, { "ReferenceNumber": 29, "Name": "Thumbnail", "URL": "https://pubchem.ncbi.nlm.nih.gov/image/ms.cgi?peaks=320:999,322:360,161:231,321:153,247:102", "Value": { "ExternalDataURL": [ "https://pubchem.ncbi.nlm.nih.gov/image/ms.cgi?peaks=320:999,322:360,161:231,321:153,247:102" ], "MimeType": "image/svg" } }, { "ReferenceNumber": 29, "Name": "License", "Value": { "StringWithMarkup": [ { "String": "CC BY-NC" } ] } }, { "ReferenceNumber": 30, "Name": "Accession ID", "URL": "https://massbank.eu/MassBank/RecordDisplay?id=WA000970", "Value": { "StringWithMarkup": [ { "String": "WA000970" } ] } }, { "ReferenceNumber": 30, "Name": "Authors", "Value": { "StringWithMarkup": [ { "String": "Nihon Waters K.K." } ] } }, { "ReferenceNumber": 30, "Name": "Instrument", "Value": { "StringWithMarkup": [ { "String": "ZQ, Waters" } ] } }, { "ReferenceNumber": 30, "Name": "Instrument Type", "Value": { "StringWithMarkup": [ { "String": "LC-ESI-Q" } ] } }, { "ReferenceNumber": 30, "Name": "MS Level", "Value": { "StringWithMarkup": [ { "String": "MS" } ] } }, { "ReferenceNumber": 30, "Name": "Ionization Mode", "Value": { "StringWithMarkup": [ { "String": "POSITIVE" } ] } }, { "ReferenceNumber": 30, "Name": "Ionization", "Value": { "StringWithMarkup": [ { "String": "ESI" } ] } }, { "ReferenceNumber": 30, "Name": "Column Name", "Value": { "StringWithMarkup": [ { "String": "2.1 mm id - 3. 5{mu}m XTerra C18MS" } ] } }, { "ReferenceNumber": 30, "Name": "Retention Time", "Value": { "StringWithMarkup": [ { "String": "9.800 min" } ] } }, { "ReferenceNumber": 30, "Name": "Top 5 Peaks", "Value": { "StringWithMarkup": [ { "String": "320 999" }, { "String": "322 364" }, { "String": "161 341" }, { "String": "321 137" }, { "String": "181 102" } ] } }, { "ReferenceNumber": 30, "Name": "SPLASH", "URL": "https://massbank.eu/MassBank/Result.jsp?splash=splash10-00di-0309000000-1a057c0ea492b42f9148", "Value": { "StringWithMarkup": [ { "String": "splash10-00di-0309000000-1a057c0ea492b42f9148" } ] } }, { "ReferenceNumber": 30, "Name": "Thumbnail", "URL": "https://pubchem.ncbi.nlm.nih.gov/image/ms.cgi?peaks=320:999,322:364,161:341,321:137,181:102", "Value": { "ExternalDataURL": [ "https://pubchem.ncbi.nlm.nih.gov/image/ms.cgi?peaks=320:999,322:364,161:341,321:137,181:102" ], "MimeType": "image/svg" } }, { "ReferenceNumber": 30, "Name": "License", "Value": { "StringWithMarkup": [ { "String": "CC BY-NC" } ] } }, { "ReferenceNumber": 32, "Name": "MoNA ID", "URL": "https://mona.fiehnlab.ucdavis.edu/spectra/display/WA000965", "Value": { "StringWithMarkup": [ { "String": "WA000965" } ] } }, { "ReferenceNumber": 32, "Name": "MS Category", "Value": { "StringWithMarkup": [ { "String": "Experimental" } ] } }, { "ReferenceNumber": 32, "Name": "MS Type", "Value": { "StringWithMarkup": [ { "String": "LC-MS" } ] } }, { "ReferenceNumber": 32, "Name": "MS Level", "Value": { "StringWithMarkup": [ { "String": "MS1" } ] } }, { "ReferenceNumber": 32, "Name": "Instrument", "Value": { "StringWithMarkup": [ { "String": "ZQ, Waters" } ] } }, { "ReferenceNumber": 32, "Name": "Instrument Type", "Value": { "StringWithMarkup": [ { "String": "LC-ESI-Q" } ] } }, { "ReferenceNumber": 32, "Name": "Ionization", "Value": { "StringWithMarkup": [ { "String": "ESI" } ] } }, { "ReferenceNumber": 32, "Name": "Ionization Mode", "Value": { "StringWithMarkup": [ { "String": "positive" } ] } }, { "ReferenceNumber": 32, "Name": "Retention Time", "Value": { "StringWithMarkup": [ { "String": "9.800 min" } ] } }, { "ReferenceNumber": 32, "Name": "Top 5 Peaks", "Value": { "StringWithMarkup": [ { "String": "179 100" }, { "String": "191 49.45" }, { "String": "181 34.13" }, { "String": "247 30.63" }, { "String": "205 21.52" } ] } }, { "ReferenceNumber": 32, "Name": "SPLASH", "URL": "https://mona.fiehnlab.ucdavis.edu/spectra/browse?query=splash.splash%3D%3D%22splash10-002f-0920000000-90f3db87cbeed5fd67c6%22", "Value": { "StringWithMarkup": [ { "String": "splash10-002f-0920000000-90f3db87cbeed5fd67c6" } ] } }, { "ReferenceNumber": 32, "Name": "Thumbnail", "URL": "https://pubchem.ncbi.nlm.nih.gov/image/ms.cgi?peaks=179:100,191:49.45,181:34.13,247:30.63,205:21.52", "Value": { "ExternalDataURL": [ "https://pubchem.ncbi.nlm.nih.gov/image/ms.cgi?peaks=179:100,191:49.45,181:34.13,247:30.63,205:21.52" ], "MimeType": "image/svg" } }, { "ReferenceNumber": 32, "Name": "Submitter", "Value": { "StringWithMarkup": [ { "String": "Nihon Waters, Nihon Waters K.K." } ] } }, { "ReferenceNumber": 33, "Name": "MoNA ID", "URL": "https://mona.fiehnlab.ucdavis.edu/spectra/display/WA000966", "Value": { "StringWithMarkup": [ { "String": "WA000966" } ] } }, { "ReferenceNumber": 33, "Name": "MS Category", "Value": { "StringWithMarkup": [ { "String": "Experimental" } ] } }, { "ReferenceNumber": 33, "Name": "MS Type", "Value": { "StringWithMarkup": [ { "String": "LC-MS" } ] } }, { "ReferenceNumber": 33, "Name": "MS Level", "Value": { "StringWithMarkup": [ { "String": "MS1" } ] } }, { "ReferenceNumber": 33, "Name": "Instrument", "Value": { "StringWithMarkup": [ { "String": "ZQ, Waters" } ] } }, { "ReferenceNumber": 33, "Name": "Instrument Type", "Value": { "StringWithMarkup": [ { "String": "LC-ESI-Q" } ] } }, { "ReferenceNumber": 33, "Name": "Ionization", "Value": { "StringWithMarkup": [ { "String": "ESI" } ] } }, { "ReferenceNumber": 33, "Name": "Ionization Mode", "Value": { "StringWithMarkup": [ { "String": "positive" } ] } }, { "ReferenceNumber": 33, "Name": "Retention Time", "Value": { "StringWithMarkup": [ { "String": "9.800 min" } ] } }, { "ReferenceNumber": 33, "Name": "Top 5 Peaks", "Value": { "StringWithMarkup": [ { "String": "247 100" }, { "String": "179 68.67" }, { "String": "142 44.34" }, { "String": "191 38.04" }, { "String": "249 34.53" } ] } }, { "ReferenceNumber": 33, "Name": "SPLASH", "URL": "https://mona.fiehnlab.ucdavis.edu/spectra/browse?query=splash.splash%3D%3D%22splash10-002e-0950000000-849a8e9960219d54f689%22", "Value": { "StringWithMarkup": [ { "String": "splash10-002e-0950000000-849a8e9960219d54f689" } ] } }, { "ReferenceNumber": 33, "Name": "Thumbnail", "URL": "https://pubchem.ncbi.nlm.nih.gov/image/ms.cgi?peaks=247:100,179:68.67,142:44.34,191:38.04,249:34.53", "Value": { "ExternalDataURL": [ "https://pubchem.ncbi.nlm.nih.gov/image/ms.cgi?peaks=247:100,179:68.67,142:44.34,191:38.04,249:34.53" ], "MimeType": "image/svg" } }, { "ReferenceNumber": 33, "Name": "Submitter", "Value": { "StringWithMarkup": [ { "String": "Nihon Waters, Nihon Waters K.K." } ] } }, { "ReferenceNumber": 34, "Name": "MoNA ID", "URL": "https://mona.fiehnlab.ucdavis.edu/spectra/display/WA000967", "Value": { "StringWithMarkup": [ { "String": "WA000967" } ] } }, { "ReferenceNumber": 34, "Name": "MS Category", "Value": { "StringWithMarkup": [ { "String": "Experimental" } ] } }, { "ReferenceNumber": 34, "Name": "MS Type", "Value": { "StringWithMarkup": [ { "String": "LC-MS" } ] } }, { "ReferenceNumber": 34, "Name": "MS Level", "Value": { "StringWithMarkup": [ { "String": "MS1" } ] } }, { "ReferenceNumber": 34, "Name": "Instrument", "Value": { "StringWithMarkup": [ { "String": "ZQ, Waters" } ] } }, { "ReferenceNumber": 34, "Name": "Instrument Type", "Value": { "StringWithMarkup": [ { "String": "LC-ESI-Q" } ] } }, { "ReferenceNumber": 34, "Name": "Ionization", "Value": { "StringWithMarkup": [ { "String": "ESI" } ] } }, { "ReferenceNumber": 34, "Name": "Ionization Mode", "Value": { "StringWithMarkup": [ { "String": "positive" } ] } }, { "ReferenceNumber": 34, "Name": "Retention Time", "Value": { "StringWithMarkup": [ { "String": "9.800 min" } ] } }, { "ReferenceNumber": 34, "Name": "Top 5 Peaks", "Value": { "StringWithMarkup": [ { "String": "247 100" }, { "String": "142 47.05" }, { "String": "249 36.44" }, { "String": "179 17.22" }, { "String": "248 7.81" } ] } }, { "ReferenceNumber": 34, "Name": "SPLASH", "URL": "https://mona.fiehnlab.ucdavis.edu/spectra/browse?query=splash.splash%3D%3D%22splash10-0002-0690000000-f317eb87cceee189094a%22", "Value": { "StringWithMarkup": [ { "String": "splash10-0002-0690000000-f317eb87cceee189094a" } ] } }, { "ReferenceNumber": 34, "Name": "Thumbnail", "URL": "https://pubchem.ncbi.nlm.nih.gov/image/ms.cgi?peaks=247:100,142:47.05,249:36.44,179:17.22,248:7.81", "Value": { "ExternalDataURL": [ "https://pubchem.ncbi.nlm.nih.gov/image/ms.cgi?peaks=247:100,142:47.05,249:36.44,179:17.22,248:7.81" ], "MimeType": "image/svg" } }, { "ReferenceNumber": 34, "Name": "Submitter", "Value": { "StringWithMarkup": [ { "String": "Nihon Waters, Nihon Waters K.K." } ] } }, { "ReferenceNumber": 35, "Name": "MoNA ID", "URL": "https://mona.fiehnlab.ucdavis.edu/spectra/display/WA000968", "Value": { "StringWithMarkup": [ { "String": "WA000968" } ] } }, { "ReferenceNumber": 35, "Name": "MS Category", "Value": { "StringWithMarkup": [ { "String": "Experimental" } ] } }, { "ReferenceNumber": 35, "Name": "MS Type", "Value": { "StringWithMarkup": [ { "String": "LC-MS" } ] } }, { "ReferenceNumber": 35, "Name": "MS Level", "Value": { "StringWithMarkup": [ { "String": "MS1" } ] } }, { "ReferenceNumber": 35, "Name": "Instrument", "Value": { "StringWithMarkup": [ { "String": "ZQ, Waters" } ] } }, { "ReferenceNumber": 35, "Name": "Instrument Type", "Value": { "StringWithMarkup": [ { "String": "LC-ESI-Q" } ] } }, { "ReferenceNumber": 35, "Name": "Ionization", "Value": { "StringWithMarkup": [ { "String": "ESI" } ] } }, { "ReferenceNumber": 35, "Name": "Ionization Mode", "Value": { "StringWithMarkup": [ { "String": "positive" } ] } }, { "ReferenceNumber": 35, "Name": "Retention Time", "Value": { "StringWithMarkup": [ { "String": "9.800 min" } ] } }, { "ReferenceNumber": 35, "Name": "Top 5 Peaks", "Value": { "StringWithMarkup": [ { "String": "247 100" }, { "String": "320 52.95" }, { "String": "142 35.74" }, { "String": "249 34.93" }, { "String": "322 19.22" } ] } }, { "ReferenceNumber": 35, "Name": "SPLASH", "URL": "https://mona.fiehnlab.ucdavis.edu/spectra/browse?query=splash.splash%3D%3D%22splash10-0002-0394000000-811a6863cd54caddde50%22", "Value": { "StringWithMarkup": [ { "String": "splash10-0002-0394000000-811a6863cd54caddde50" } ] } }, { "ReferenceNumber": 35, "Name": "Thumbnail", "URL": "https://pubchem.ncbi.nlm.nih.gov/image/ms.cgi?peaks=247:100,320:52.95,142:35.74,249:34.93,322:19.22", "Value": { "ExternalDataURL": [ "https://pubchem.ncbi.nlm.nih.gov/image/ms.cgi?peaks=247:100,320:52.95,142:35.74,249:34.93,322:19.22" ], "MimeType": "image/svg" } }, { "ReferenceNumber": 35, "Name": "Submitter", "Value": { "StringWithMarkup": [ { "String": "Nihon Waters, Nihon Waters K.K." } ] } }, { "ReferenceNumber": 37, "Name": "MoNA ID", "URL": "https://mona.fiehnlab.ucdavis.edu/spectra/display/CCMSLIB00005723985", "Value": { "StringWithMarkup": [ { "String": "CCMSLIB00005723985" } ] } }, { "ReferenceNumber": 37, "Name": "MS Category", "Value": { "StringWithMarkup": [ { "String": "Experimental" } ] } }, { "ReferenceNumber": 37, "Name": "MS Type", "Value": { "StringWithMarkup": [ { "String": "LC-MS" } ] } }, { "ReferenceNumber": 37, "Name": "MS Level", "Value": { "StringWithMarkup": [ { "String": "MS2" } ] } }, { "ReferenceNumber": 37, "Name": "Precursor Type", "Value": { "StringWithMarkup": [ { "String": "[M+H]+" } ] } }, { "ReferenceNumber": 37, "Name": "Precursor m/z", "Value": { "StringWithMarkup": [ { "String": "320.189" } ] } }, { "ReferenceNumber": 37, "Name": "Instrument", "Value": { "StringWithMarkup": [ { "String": "qTof" } ] } }, { "ReferenceNumber": 37, "Name": "Ionization Mode", "Value": { "StringWithMarkup": [ { "String": "positive" } ] } }, { "ReferenceNumber": 37, "Name": "Top 5 Peaks", "Value": { "StringWithMarkup": [ { "String": "320.187012 100" }, { "String": "322.187134 29.96" }, { "String": "321.189575 20.29" }, { "String": "98.091202 4.35" }, { "String": "247.107574 4.21" } ] } }, { "ReferenceNumber": 37, "Name": "SPLASH", "URL": "https://mona.fiehnlab.ucdavis.edu/spectra/browse?query=splash.splash%3D%3D%22splash10-00di-0009000000-76adc55bafbe5f5ba846%22", "Value": { "StringWithMarkup": [ { "String": "splash10-00di-0009000000-76adc55bafbe5f5ba846" } ] } }, { "ReferenceNumber": 37, "Name": "Thumbnail", "URL": "https://pubchem.ncbi.nlm.nih.gov/image/ms.cgi?peaks=320.187012:100,322.187134:29.96,321.189575:20.29,98.091202:4.35,247.107574:4.21", "Value": { "ExternalDataURL": [ "https://pubchem.ncbi.nlm.nih.gov/image/ms.cgi?peaks=320.187012:100,322.187134:29.96,321.189575:20.29,98.091202:4.35,247.107574:4.21" ], "MimeType": "image/svg" } }, { "ReferenceNumber": 37, "Name": "Submitter", "Value": { "StringWithMarkup": [ { "String": "GNPS Team, University of California, San Diego" } ] } } ] }, { "TOCHeading": "Other MS", "Description": "This section provides additional MS linking information.", "DisplayControls": { "CreateTable": { "FromInformationIn": "ThisSection", "NumberOfColumns": 2, "ColumnContents": [ "Name", "Value" ] }, "ShowAtMost": 2 }, "Information": [ { "ReferenceNumber": 24, "Name": "Accession ID", "URL": "https://massbank.eu/MassBank/RecordDisplay?id=JP003161", "Value": { "StringWithMarkup": [ { "String": "JP003161" } ] } }, { "ReferenceNumber": 24, "Name": "Authors", "Value": { "StringWithMarkup": [ { "String": "YOSHIZUMI H, FAC. OF PHARMACY, MEIJO UNIV." } ] } }, { "ReferenceNumber": 24, "Name": "Instrument", "Value": { "StringWithMarkup": [ { "String": "Unknown" } ] } }, { "ReferenceNumber": 24, "Name": "Instrument Type", "Value": { "StringWithMarkup": [ { "String": "CI-B" } ] } }, { "ReferenceNumber": 24, "Name": "MS Level", "Value": { "StringWithMarkup": [ { "String": "MS" } ] } }, { "ReferenceNumber": 24, "Name": "Ionization Mode", "Value": { "StringWithMarkup": [ { "String": "POSITIVE" } ] } }, { "ReferenceNumber": 24, "Name": "Top 5 Peaks", "Value": { "StringWithMarkup": [ { "String": "320 999" }, { "String": "322 340" }, { "String": "321 210" }, { "String": "323 70" }, { "String": "319 50" } ] } }, { "ReferenceNumber": 24, "Name": "SPLASH", "URL": "https://massbank.eu/MassBank/Result.jsp?splash=splash10-00di-0009000000-d54119d64cfc341cee7d", "Value": { "StringWithMarkup": [ { "String": "splash10-00di-0009000000-d54119d64cfc341cee7d" } ] } }, { "ReferenceNumber": 24, "Name": "Thumbnail", "URL": "https://pubchem.ncbi.nlm.nih.gov/image/ms.cgi?peaks=320:999,322:340,321:210,323:70,319:50", "Value": { "ExternalDataURL": [ "https://pubchem.ncbi.nlm.nih.gov/image/ms.cgi?peaks=320:999,322:340,321:210,323:70,319:50" ], "MimeType": "image/svg" } }, { "ReferenceNumber": 24, "Name": "License", "Value": { "StringWithMarkup": [ { "String": "CC BY-NC-SA" } ] } } ] } ] }, { "TOCHeading": "Other Spectra", "Description": "Other spectra include fluorescence, emission, etc.", "Information": [ { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "Pfleger, K., H. Maurer and A. Weber. Mass Spectral and GC Data of Drugs, Poisons and their Metabolites. Parts I and II. Mass Spectra Indexes. Weinheim, Federal Republic of Germany. 1985., p. 561" ], "Value": { "StringWithMarkup": [ { "String": "Intense mass spectral peaks: 58 m/z, 86 m/z, 245 m/z, 290 m/z, 319 m/z" } ] } } ] } ] }, { "TOCHeading": "Related Records", "Description": "Related compounds/substances information based on the similar structure, annotations, etc.", "Section": [ { "TOCHeading": "Related Compounds with Annotation", "Description": "The subset of compounds that are related to the one currently displayed AND that have biomedical annotations.", "Information": [ { "ReferenceNumber": 69, "Value": { "Boolean": [ true ] } } ] }, { "TOCHeading": "Related Compounds", "Description": "Compound records closely associated to this record.", "DisplayControls": { "CreateTable": { "FromInformationIn": "ThisSection", "NumberOfColumns": 2, "ColumnContents": [ "Name", "Value" ] }, "ShowAtMost": 1 }, "Information": [ { "ReferenceNumber": 69, "Name": "Same Connectivity Count", "URL": "https://www.ncbi.nlm.nih.gov/pccompound?cmd=Link&LinkName=pccompound_pccompound_sameconnectivity_pulldown&from_uid=2719", "Value": { "Number": [ 10 ] } }, { "ReferenceNumber": 69, "Name": "Same Stereo Count", "URL": "https://www.ncbi.nlm.nih.gov/pccompound?cmd=Link&LinkName=pccompound_pccompound_samestereochem_pulldown&from_uid=2719", "Value": { "Number": [ 8 ] } }, { "ReferenceNumber": 69, "Name": "Same Isotope Count", "URL": "https://www.ncbi.nlm.nih.gov/pccompound?cmd=Link&LinkName=pccompound_pccompound_sameisotopic_pulldown&from_uid=2719", "Value": { "Number": [ 3 ] } }, { "ReferenceNumber": 69, "Name": "Same Parent, Connectivity Count", "URL": "https://www.ncbi.nlm.nih.gov/pccompound?cmd=Link&LinkName=pccompound_pccompound_parent_connectivity_pulldown&from_uid=2719", "Value": { "Number": [ 72 ] } }, { "ReferenceNumber": 69, "Name": "Same Parent, Stereo Count", "URL": "https://www.ncbi.nlm.nih.gov/pccompound?cmd=Link&LinkName=pccompound_pccompound_parent_stereo_pulldown&from_uid=2719", "Value": { "Number": [ 56 ] } }, { "ReferenceNumber": 69, "Name": "Same Parent, Isotope Count", "URL": "https://www.ncbi.nlm.nih.gov/pccompound?cmd=Link&LinkName=pccompound_pccompound_parent_isotopes_pulldown&from_uid=2719", "Value": { "Number": [ 60 ] } }, { "ReferenceNumber": 69, "Name": "Same Parent, Exact Count", "URL": "https://www.ncbi.nlm.nih.gov/pccompound?cmd=Link&LinkName=pccompound_pccompound_parent_pulldown&from_uid=2719", "Value": { "Number": [ 44 ] } }, { "ReferenceNumber": 69, "Name": "Mixtures, Components, and Neutralized Forms Count", "URL": "https://www.ncbi.nlm.nih.gov/pccompound?cmd=Link&LinkName=pccompound_pccompound_mixture&from_uid=2719", "Value": { "Number": [ 168 ] } }, { "ReferenceNumber": 69, "Name": "Similar Compounds Count", "URL": "https://www.ncbi.nlm.nih.gov/pccompound?cmd=Link&LinkName=pccompound_pccompound&from_uid=2719", "Value": { "Number": [ 2251 ] } }, { "ReferenceNumber": 69, "Name": "Similar Conformers Count", "URL": "https://www.ncbi.nlm.nih.gov/pccompound?cmd=Link&LinkName=pccompound_pccompound_3d&from_uid=2719", "Value": { "Number": [ 218 ] } } ] }, { "TOCHeading": "Substances", "Description": "Substance records linked to this compound.", "Section": [ { "TOCHeading": "Related Substances", "Description": "Substances identical or nearly identical to this record.", "URL": "https://pubchemdocs.ncbi.nlm.nih.gov/substances", "DisplayControls": { "CreateTable": { "FromInformationIn": "ThisSection", "NumberOfColumns": 2, "ColumnContents": [ "Name", "Value" ] }, "ShowAtMost": 1 }, "Information": [ { "ReferenceNumber": 69, "Name": "All Count", "URL": "https://www.ncbi.nlm.nih.gov/pcsubstance/?term=2719[CompoundID]", "Value": { "Number": [ 836 ] } }, { "ReferenceNumber": 69, "Name": "Same Count", "URL": "https://www.ncbi.nlm.nih.gov/pcsubstance/?term=2719[StandardizedCID]", "Value": { "Number": [ 200 ] } }, { "ReferenceNumber": 69, "Name": "Mixture Count", "URL": "https://www.ncbi.nlm.nih.gov/pcsubstance/?term=2719[ComponentCID]", "Value": { "Number": [ 636 ] } } ] }, { "TOCHeading": "Substances by Category", "Description": "Substance category according to the depositors. Substance Categorization Classification - The subheaders in this section of a PubChem Compound record reflect the various categories of depositors that have submitted corresponding PubChem Substance records. This allows you to quickly find the corresponding PubChem Substance records that are likely to contain a given type of information, such as Chemical Reactions.", "URL": "https://pubchemdocs.ncbi.nlm.nih.gov/substances", "Information": [ { "ReferenceNumber": 69, "Value": { "StringWithMarkup": [ { "String": "Chemical Vendors" }, { "String": "Curation Efforts" }, { "String": "Governmental Organizations" }, { "String": "Journal Publishers" }, { "String": "Legacy Depositors" }, { "String": "NIH Initiatives" }, { "String": "Research and Development" }, { "String": "Subscription Services" } ] } } ] } ] }, { "TOCHeading": "Entrez Crosslinks", "Description": "Cross-references to associated records in other Entrez databases such as PubMed, Gene, Protein, etc.", "DisplayControls": { "CreateTable": { "FromInformationIn": "ThisSection", "NumberOfColumns": 2, "ColumnContents": [ "Name", "Value" ] }, "ShowAtMost": 1 }, "Information": [ { "ReferenceNumber": 69, "Name": "PubMed Count", "URL": "https://www.ncbi.nlm.nih.gov/sites/entrez?LinkName=pccompound_pubmed&db=pccompound&cmd=Link&from_uid=2719", "Value": { "Number": [ 580 ] } }, { "ReferenceNumber": 69, "Name": "Taxonomy Count", "URL": "https://www.ncbi.nlm.nih.gov/sites/entrez?LinkName=pccompound_taxonomy&db=pccompound&cmd=Link&from_uid=2719", "Value": { "Number": [ 10 ] } }, { "ReferenceNumber": 69, "Name": "OMIM Count", "URL": "https://www.ncbi.nlm.nih.gov/sites/entrez?LinkName=pccompound_omim&db=pccompound&cmd=Link&from_uid=2719", "Value": { "Number": [ 51 ] } }, { "ReferenceNumber": 69, "Name": "Gene Count", "URL": "https://www.ncbi.nlm.nih.gov/sites/entrez?LinkName=pccompound_gene&db=pccompound&cmd=Link&from_uid=2719", "Value": { "Number": [ 336 ] } } ] }, { "TOCHeading": "Associated Chemicals", "Description": "Associated Chemicals", "Information": [ { "ReferenceNumber": 18, "Value": { "StringWithMarkup": [ { "String": "Chloroquine phosphate; 50-63-5", "Markup": [ { "Start": 0, "Length": 21, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine%20phosphate", "Type": "PubChem Internal Link", "Extra": "CID-64927" }, { "Start": 23, "Length": 7, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/50-63-5", "Type": "PubChem Internal Link", "Extra": "CID-64927" } ] } ] } }, { "ReferenceNumber": 18, "Value": { "StringWithMarkup": [ { "String": "Hydroxychloroquine sulfate; 747-36-4", "Markup": [ { "Start": 0, "Length": 26, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Hydroxychloroquine%20sulfate", "Type": "PubChem Internal Link", "Extra": "CID-12947" }, { "Start": 28, "Length": 8, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/747-36-4", "Type": "PubChem Internal Link", "Extra": "CID-12947" } ] } ] } }, { "ReferenceNumber": 18, "Value": { "StringWithMarkup": [ { "String": "Chloroquine hydrochloride; 3545-67-3", "Markup": [ { "Start": 0, "Length": 25, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine%20hydrochloride", "Type": "PubChem Internal Link", "Extra": "CID-83820" }, { "Start": 27, "Length": 9, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/3545-67-3", "Type": "PubChem Internal Link", "Extra": "CID-83820" } ] } ] } } ] }, { "TOCHeading": "NCBI LinkOut", "Description": "LinkOut is a service that allows one to link directly from NCBI databases to a wide range of information and services beyond NCBI systems.", "URL": "https://www.ncbi.nlm.nih.gov/projects/linkout", "Information": [ { "ReferenceNumber": 92, "Value": { "Boolean": [ true ] } } ] } ] }, { "TOCHeading": "Chemical Vendors", "Description": "A list of chemical vendors that sell this compound. Each vendor may have multiple products containing the same chemical, but different in various aspects, such as amount and purity. For each product, the external identifier used to locate the product on the vendor's website is provided under the Purcharsable Chemical column, and clicking this identifier directs you to the vendor's website. The information on the product provided by the vendor to PubChem can be accessed at the Summary page of the corresponding PubChem Substance ID (SID). Note that the order of chemical vendors on the list is randomized, and that PubChem do not endorse any of the vendors.", "Information": [ { "ReferenceNumber": 69, "Value": { "Boolean": [ true ] } } ] }, { "TOCHeading": "Drug and Medication Information", "Description": "Drug and medication information from multiple sources.", "Section": [ { "TOCHeading": "Drug Indication", "Description": "Drug Indication information from various sources.", "DisplayControls": { "ShowAtMost": 3 }, "Information": [ { "ReferenceNumber": 10, "Value": { "StringWithMarkup": [ { "String": "Chloroquine is indicated to treat infections of _P. vivax_, _P. malariae_, _P. ovale_, and susceptible strains of _P. falciparum_. It is also used to treat extraintestinal amebiasis. Chloroquine is also used off label for the treatment of rheumatic diseases, as well as treatment and prophylaxis of Zika virus. Chloroquine is currently undergoing clinical trials for the treatment of COVID-19.", "Markup": [ { "Start": 0, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 184, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 312, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } }, { "ReferenceNumber": 10, "URL": "http://s3-us-west-2.amazonaws.com/drugbank/fda_labels/DB00608.pdf?1265922797", "Value": { "StringWithMarkup": [ { "String": "FDA Label" } ] } } ] }, { "TOCHeading": "LiverTox Summary", "Description": "This section provides an overview of drug induced liver injury, diagnostic criteria, assessment of causality and severity, descriptions of different clinical patterns (phenotypes), information on management and treatment, and standardized nomenclature. The role of liver biopsy and major histological patterns of drug induced liver disease are also given.", "URL": "https://livertox.nlm.nih.gov/aboutus.html", "Information": [ { "ReferenceNumber": 22, "Value": { "StringWithMarkup": [ { "String": "Chloroquine is an aminoquinoline used for the prevention and therapy of malaria. It is also effective in extraintestinal amebiasis and as an antiinflammatory agent for therapy of rheumatoid arthritis and lupus erythematosus. Chloroquine is not associated with serum enzyme elevations and is an extremely rare cause of clinically apparent acute liver injury.", "Markup": [ { "Start": 0, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 18, "Length": 14, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/aminoquinoline", "Type": "PubChem Internal Link", "Extra": "CID-11379" }, { "Start": 225, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } } ] }, { "TOCHeading": "Drug Classes", "Description": "Drug classes information from various sources.", "Information": [ { "ReferenceNumber": 22, "Value": { "StringWithMarkup": [ { "String": "Antimalarial Agents" } ] } } ] }, { "TOCHeading": "WHO Essential Medicines", "Description": "The WHO Essential Medicines present a list of minimum medicine needs for a basic health-care system, listing the most efficacious, safe and cost–effective medicines for priority conditions.", "URL": "https://www.who.int/groups/expert-committee-on-selection-and-use-of-essential-medicines/essential-medicines-lists", "DisplayControls": { "CreateTable": { "FromInformationIn": "ThisSection", "NumberOfColumns": 4, "ColumnsFromNamedLists": { "Name": [ "Drug", "Drug Classes", "Formulation", "Indication" ], "UseNamesAsColumnHeadings": true } }, "ShowAtMost": 3 }, "Information": [ { "ReferenceNumber": 64, "Name": "Drug", "Value": { "StringWithMarkup": [ { "String": "Chloroquine", "Markup": [ { "Start": 0, "Length": 11, "URL": "https://list.essentialmeds.org/medicines/275" } ] }, { "String": "Chloroquine", "Markup": [ { "Start": 0, "Length": 11, "URL": "https://list.essentialmeds.org/medicines/275" } ] }, { "String": "Chloroquine", "Markup": [ { "Start": 0, "Length": 11, "URL": "https://list.essentialmeds.org/medicines/275" } ] } ] } }, { "ReferenceNumber": 64, "Name": "Drug Classes", "Value": { "StringWithMarkup": [ { "String": "Antimalarial medicines -> For chemoprevention" }, { "String": "Antimalarial medicines -> For curative treatment" }, { "String": "Disease-modifying anti-rheumatic drugs (DMARDs)" } ] } }, { "ReferenceNumber": 64, "Name": "Formulation", "Value": { "StringWithMarkup": [ { "String": "(1) Oral - Liquid: 50 mg per 5 mL syrup (as phosphate or sulfate); (2) Oral - Solid: 150 mg tablet (as phosphate or sulfate)", "Markup": [ { "Start": 44, "Length": 9, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/phosphate", "Type": "PubChem Internal Link", "Extra": "CID-1061" }, { "Start": 57, "Length": 7, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/sulfate", "Type": "PubChem Internal Link", "Extra": "CID-1117" }, { "Start": 103, "Length": 9, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/phosphate", "Type": "PubChem Internal Link", "Extra": "CID-1061" }, { "Start": 116, "Length": 7, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/sulfate", "Type": "PubChem Internal Link", "Extra": "CID-1117" } ] }, { "String": "(1) Parenteral - General injections - IV: 40 mg per mL in 5 mL ampoule (as hydrochloride, phosphate or sulfate); (2) Oral - Liquid: 50 mg per 5 mL syrup (as phosphate or sulfate); (3) Oral - Solid: 150 mg tablet (as phosphate or sulfate); 100 mg tablet (as phosphate or sulfate)", "Markup": [ { "Start": 91, "Length": 9, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/phosphate", "Type": "PubChem Internal Link", "Extra": "CID-1061" }, { "Start": 104, "Length": 7, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/sulfate", "Type": "PubChem Internal Link", "Extra": "CID-1117" }, { "Start": 158, "Length": 9, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/phosphate", "Type": "PubChem Internal Link", "Extra": "CID-1061" }, { "Start": 171, "Length": 7, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/sulfate", "Type": "PubChem Internal Link", "Extra": "CID-1117" }, { "Start": 217, "Length": 9, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/phosphate", "Type": "PubChem Internal Link", "Extra": "CID-1061" }, { "Start": 230, "Length": 7, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/sulfate", "Type": "PubChem Internal Link", "Extra": "CID-1117" }, { "Start": 258, "Length": 9, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/phosphate", "Type": "PubChem Internal Link", "Extra": "CID-1061" }, { "Start": 271, "Length": 7, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/sulfate", "Type": "PubChem Internal Link", "Extra": "CID-1117" } ] }, { "String": "Oral - Solid: 100 mg tablet (as phosphate or sulfate); 150 mg tablet (as phosphate or sulfate)", "Markup": [ { "Start": 32, "Length": 9, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/phosphate", "Type": "PubChem Internal Link", "Extra": "CID-1061" }, { "Start": 45, "Length": 7, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/sulfate", "Type": "PubChem Internal Link", "Extra": "CID-1117" }, { "Start": 73, "Length": 9, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/phosphate", "Type": "PubChem Internal Link", "Extra": "CID-1061" }, { "Start": 86, "Length": 7, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/sulfate", "Type": "PubChem Internal Link", "Extra": "CID-1117" } ] } ] } }, { "ReferenceNumber": 64, "Name": "Indication", "Value": { "StringWithMarkup": [ { "String": "(1) Malaria due to Plasmodium falciparum [co-prescribed with P01BA01]; (2) Malaria due to Plasmodium ovale [co-prescribed with P01BA01]; (3) Malaria due to Plasmodium vivax [co-prescribed with P01BA01]; (4) Malaria due to Plasmodium malariae [co-prescribed with P01BA01]" }, { "String": "(1) Malaria due to Plasmodium falciparum [co-prescribed with P01BA01]; (2) Malaria due to Plasmodium vivax [co-prescribed with P01BA01]" }, { "String": "Rheumatoid arthritis [co-prescribed with P01BA01]" } ] } } ] }, { "TOCHeading": "FDA Orange Book", "Description": "The Orange Book identifies drug products approved on the basis of safety and effectiveness by the Food and Drug Administration (FDA) under the Federal Food, Drug, and Cosmetic Act.", "URL": "https://www.fda.gov/drugs/drug-approvals-and-databases/approved-drug-products-therapeutic-equivalence-evaluations-orange-book", "Information": [ { "ReferenceNumber": 17, "Value": { "ExternalTableName": "fdaorangebook", "ExternalTableNumRows": 1 } } ] }, { "TOCHeading": "FDA National Drug Code Directory", "Description": "The National Drug Code (NDC) is a unique product identifier in three-segment number used in the United States for human drugs (the Drug Listing Act of 1972).", "URL": "https://www.fda.gov/drugs/drug-approvals-and-databases/national-drug-code-directory", "Information": [ { "ReferenceNumber": 38, "Value": { "StringWithMarkup": [ { "String": "CHLOROQUINE is an active ingredient in the product 'VISUAL DETOX'.", "Markup": [ { "Start": 0, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/CHLOROQUINE", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } } ] }, { "TOCHeading": "Clinical Trials", "Description": "Clinical trials are research studies performed in people that are aimed at evaluating a medical, surgical, or behavioral intervention. They are the primary way that researchers find out if a new treatment, like a new drug or diet or medical device (for example, a pacemaker) is safe and effective in people.", "Section": [ { "TOCHeading": "ClinicalTrials.gov", "Description": "The brief clinical trials summary from the ClinicalTrials.gov at the U.S. National Library of Medicine.", "URL": "https://clinicaltrials.gov/", "Information": [ { "ReferenceNumber": 6, "Name": "ClinicalTrials.gov", "Value": { "ExternalTableName": "clinicaltrials", "ExternalTableNumRows": 94 } } ] }, { "TOCHeading": "EU Clinical Trials Register", "Description": "The clinical trials summary from the EU Clinical Trials Register.", "URL": "https://www.clinicaltrialsregister.eu/", "Information": [ { "ReferenceNumber": 13, "Name": "EU Clinical Trials Register", "Value": { "ExternalTableName": "clinicaltrials_eu", "ExternalTableNumRows": 7 } } ] } ] }, { "TOCHeading": "EMA Drug Information", "Description": "Drug and medicines information from the European Medicines Agency (EMA)", "URL": "https://www.ema.europa.eu/en/medicines", "DisplayControls": { "CreateTable": { "FromInformationIn": "ThisSection", "NumberOfColumns": 2, "ColumnContents": [ "Name", "Value" ] }, "ShowAtMost": 2 }, "Information": [ { "ReferenceNumber": 16, "Name": "Disease/Condition", "Value": { "StringWithMarkup": [ { "String": "Treatment of glioma" } ] } }, { "ReferenceNumber": 16, "Name": "Active Substance", "Value": { "StringWithMarkup": [ { "String": "Chloroquine" } ] } }, { "ReferenceNumber": 16, "Name": "Status of Orphan Designation", "Value": { "StringWithMarkup": [ { "String": "Positive" } ] } }, { "ReferenceNumber": 16, "Name": "Decision Date", "Value": { "StringWithMarkup": [ { "String": "2014-11-19" } ] } } ] }, { "TOCHeading": "Therapeutic Uses", "Description": "Therapeutic Uses information from HSDB", "Information": [ { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "National Library of Medicine, SIS; ChemIDplus Record for Chloroquine. (54-05-7). Available from, as of April 17, 2006: https://chem.sis.nlm.nih.gov/chemidplus/chemidlite.jsp" ], "Value": { "StringWithMarkup": [ { "String": "Mesh Heading: Amebicides, antimalarials, antirheumatic Agents" } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "O'Neil, M.J. (ed.). The Merck Index - An Encyclopedia of Chemicals, Drugs, and Biologicals. 13th Edition, Whitehouse Station, NJ: Merck and Co., Inc., 2001., p. 373" ], "Value": { "StringWithMarkup": [ { "String": "Antimalarial; antiamebic; antirheumatic. Lupus erythematosus suppressant." } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "Thomson.Micromedex. Drug Information for the Health Care Professional. 25th ed. Volume 1. Plus Updates. Content Reviewed by the United States Pharmacopeial Convention, Inc. Greenwood Village, CO. 2005., p. 837" ], "Value": { "StringWithMarkup": [ { "String": "Chloroquine is indicated in the suppressive treatment and the treatment of acute attacks of malaria caused by plasmodium vivax, Plasmodium malariae, Plasmodium ovale, chlrorquine-susceptible strains of P. falciparum. /Included in the US product label/", "Markup": [ { "Start": 0, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "Thomson.Micromedex. Drug Information for the Health Care Professional. 25th ed. Volume 1. Plus Updates. Content Reviewed by the United States Pharmacopeial Convention, Inc. Greenwood Village, CO. 2005., p. 837" ], "Value": { "StringWithMarkup": [ { "String": "Chloroquine is indicated for the treatment of amebic liver abscess, usually in combination with and effective intestinal amebicide. However, it is not considered a primary drug. /Included in the US product label/", "Markup": [ { "Start": 0, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Value": { "StringWithMarkup": [ { "String": "For more Therapeutic Uses (Complete) data for CHLOROQUINE (13 total), please visit the HSDB record page.", "Markup": [ { "Start": 87, "Length": 16, "URL": "https://pubchem.ncbi.nlm.nih.gov/source/hsdb/3029#section=Therapeutic-Uses-(Complete)" }, { "Start": 46, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/CHLOROQUINE", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } } ] }, { "TOCHeading": "Drug Warnings", "Description": "Drug Warning information from HSDB", "Information": [ { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "McEvoy, G.K. (ed.). American Hospital Formulary Service. AHFS Drug Information. American Society of Health-System Pharmacists, Bethesda, MD. 2006., p. 858" ], "Value": { "StringWithMarkup": [ { "String": "Chloroquine is contraindicated in patients who are hypersensitive to 4-aminoquinoline derivatives.", "Markup": [ { "Start": 0, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 69, "Length": 16, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/4-aminoquinoline", "Type": "PubChem Internal Link", "Extra": "CID-68476" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "McEvoy, G.K. (ed.). American Hospital Formulary Service. AHFS Drug Information. American Society of Health-System Pharmacists, Bethesda, MD. 2006., p. 858" ], "Value": { "StringWithMarkup": [ { "String": "Ophthalmologic examinations, including slit lamp, funduscopic, and visual field tests, should be performed prior to initiation of chloroquine therapy and periodically during therapy whenever long term use of the drug is contemplated. Chloroquine should be discontinued immediately and the patient observed for possible progression if there is any indication of abnormalities in visual acuity or visual field, abnormalities in the retinal macular area such as pigmentary changes or loss of foveal reflex, or if any other visual symptoms such as light flashes and streaks occur which are not fully explainable by difficulties of accommodation or corneal opacities.", "Markup": [ { "Start": 130, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 234, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 430, "Length": 7, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/retinal", "Type": "PubChem Internal Link", "Extra": "CID-638015" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "McEvoy, G.K. (ed.). American Hospital Formulary Service. AHFS Drug Information. American Society of Health-System Pharmacists, Bethesda, MD. 2006., p. 858" ], "Value": { "StringWithMarkup": [ { "String": "Because chloroquine may concentrate in the liver, the drug should be used with caution in patients with hepatic disease or alcoholism and in patients receiving other hepatotoxic drugs.", "Markup": [ { "Start": 8, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "McEvoy, G.K. (ed.). American Hospital Formulary Service. AHFS Drug Information. American Society of Health-System Pharmacists, Bethesda, MD. 2006., p. 858" ], "Value": { "StringWithMarkup": [ { "String": "Complete blood cell counts should be performed periodically in patients receiving prolonged therapy with chloroquine. Chloroquine should be discontinued if there is evidence of adverse hematologic effects that are severe and not attributable to the disease being treated. The manufacturer states that chloroquine should be administered with caution to patients with glucose-6-phosphate dehydrogenase deficiency.", "Markup": [ { "Start": 105, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 118, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 301, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 366, "Length": 19, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/glucose-6-phosphate", "Type": "PubChem Internal Link", "Extra": "CID-5958" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Value": { "StringWithMarkup": [ { "String": "For more Drug Warnings (Complete) data for CHLOROQUINE (21 total), please visit the HSDB record page.", "Markup": [ { "Start": 84, "Length": 16, "URL": "https://pubchem.ncbi.nlm.nih.gov/source/hsdb/3029#section=Drug-Warnings-(Complete)" }, { "Start": 43, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/CHLOROQUINE", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } } ] }, { "TOCHeading": "Reported Fatal Dose", "Description": "Minimum/Potential Fatal Human Dose information from HSDB", "Information": [ { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "Olson, K.R. (Ed.); Poisoning & Drug Overdose. 4th ed. Lange Medical Books/McGraw-Hill. New York, N.Y. 2004., p. 166" ], "Value": { "StringWithMarkup": [ { "String": "... The lethal dose of chloroquine for an adult is estimated at 30 to 50 mg/kg.", "Markup": [ { "Start": 23, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "Hardman, J.G., L.E. Limbird, P.B., A.G. Gilman. Goodman and Gilman's The Pharmacological Basis of Therapeutics. 10th ed. New York, NY: McGraw-Hill, 2001., p. 1079" ], "Value": { "StringWithMarkup": [ { "String": "Chloroquine doses of more than 5 g given parenterally usually are fatal.", "Markup": [ { "Start": 0, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "WHO; Poisons Information Monographs (PIMs) 030: Amodiaquine. Available from, as of July 24, 2006: https://www.inchem.org/pages/pims.html" ], "Value": { "StringWithMarkup": [ { "String": "... Fatal dose ... of chloroquine phosphate (2 to 3 g, adult) ... /Chloroquine phosphate/", "Markup": [ { "Start": 22, "Length": 21, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine%20phosphate", "Type": "PubChem Internal Link", "Extra": "CID-64927" }, { "Start": 67, "Length": 21, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine%20phosphate", "Type": "PubChem Internal Link", "Extra": "CID-64927" } ] } ] } } ] }, { "TOCHeading": "Drug Tolerance", "Description": "Drug Tolerance information from HSDB", "Information": [ { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "WHO; WHO Guidelines for the Treatment of Malaria (2006). Available from, as of July 31, 2006: https://www.who.int/malaria/docs/TreatmentGuidelines2006.pdf" ], "Value": { "StringWithMarkup": [ { "String": "Although there are a few areas where chloroquine is still effective, parenteral chloroquine is no longer recommended for the treatment of severe malaria because of widespread resistance.", "Markup": [ { "Start": 37, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 80, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "WHO; WHO Guidelines for the Treatment of Malaria (2006). Available from, as of July 31, 2006: https://www.who.int/malaria/docs/TreatmentGuidelines2006.pdf" ], "Value": { "StringWithMarkup": [ { "String": "Resistance to antimalarials has been documented for P. falciparum, P. vivax and, recently, P. malariae. In P. falciparum, resistance has been observed to almost all currently used antimalarials (amodiaquine, chloroquine, mefloquine, quinine and sulfadoxine - pyrimethamine) except for artemisinin and its derivatives. The geographical distributions and rates of spread have varied considerably. P. vivax has developed resistance rapidly to sulfadoxine -pyrimethamine in many areas. Chloroquine resistance is confined largely to Indonesia, East Timor, Papua New Guinea and other parts of Oceania. There are also documented reports from Peru. P. vivax remains sensitive to chloroquine in South-East Asia, the Indian subcontinent, the Korean peninsula, the Middle East, north-east Africa, and most of South and Central America.", "Markup": [ { "Start": 195, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/amodiaquine", "Type": "PubChem Internal Link", "Extra": "CID-2165" }, { "Start": 208, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 221, "Length": 10, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/mefloquine", "Type": "PubChem Internal Link", "Extra": "CID-4046" }, { "Start": 233, "Length": 7, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/quinine", "Type": "PubChem Internal Link", "Extra": "CID-3034034" }, { "Start": 245, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/sulfadoxine", "Type": "PubChem Internal Link", "Extra": "CID-17134" }, { "Start": 259, "Length": 13, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/pyrimethamine", "Type": "PubChem Internal Link", "Extra": "CID-4993" }, { "Start": 285, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/artemisinin", "Type": "PubChem Internal Link", "Extra": "CID-2240" }, { "Start": 440, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/sulfadoxine", "Type": "PubChem Internal Link", "Extra": "CID-17134" }, { "Start": 453, "Length": 13, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/pyrimethamine", "Type": "PubChem Internal Link", "Extra": "CID-4993" }, { "Start": 482, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 671, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } } ] } ] }, { "TOCHeading": "Pharmacology and Biochemistry", "Description": "Pharmacology and biochemistry information related to this record", "Section": [ { "TOCHeading": "Pharmacology", "Description": "Pharmacology information related to this record", "Information": [ { "ReferenceNumber": 10, "Value": { "StringWithMarkup": [ { "String": "Chloroquine inhibits the action of heme polymerase, which causes the buildup of toxic heme in _Plasmodium_ species. It has a long duration of action as the half life is 20-60 days. Patients should be counselled regarding the risk of retinopathy with long term usage or high dosage, muscle weakness, and toxicity in children.", "Markup": [ { "Start": 0, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } } ] }, { "TOCHeading": "MeSH Pharmacological Classification", "Description": "Pharmacological action classes that provided by MeSH", "Information": [ { "ReferenceNumber": 88, "Name": "Amebicides", "Value": { "StringWithMarkup": [ { "String": "Agents which are destructive to amebae, especially the parasitic species causing AMEBIASIS in man and animal. (See all compounds classified as Amebicides.)", "Markup": [ { "Start": 115, "Length": 38, "URL": "https://www.ncbi.nlm.nih.gov/sites/entrez?Db=pccompound&DbFrom=mesh&Cmd=Link&LinkName=mesh_pccompound&IdsFromResult=68000563" } ] } ] } }, { "ReferenceNumber": 89, "Name": "Antirheumatic Agents", "Value": { "StringWithMarkup": [ { "String": "Drugs that are used to treat RHEUMATOID ARTHRITIS. (See all compounds classified as Antirheumatic Agents.)", "Markup": [ { "Start": 56, "Length": 48, "URL": "https://www.ncbi.nlm.nih.gov/sites/entrez?Db=pccompound&DbFrom=mesh&Cmd=Link&LinkName=mesh_pccompound&IdsFromResult=68018501" } ] } ] } }, { "ReferenceNumber": 90, "Name": "Antimalarials", "Value": { "StringWithMarkup": [ { "String": "Agents used in the treatment of malaria. They are usually classified on the basis of their action against plasmodia at different stages in their life cycle in the human. (From AMA, Drug Evaluations Annual, 1992, p1585) (See all compounds classified as Antimalarials.)", "Markup": [ { "Start": 224, "Length": 41, "URL": "https://www.ncbi.nlm.nih.gov/sites/entrez?Db=pccompound&DbFrom=mesh&Cmd=Link&LinkName=mesh_pccompound&IdsFromResult=68000962" } ] } ] } } ] }, { "TOCHeading": "ATC Code", "Description": "The Anatomical Therapeutic Chemical (ATC) Classification System is used for the classification of drugs. This pharmaceutical coding system divides drugs into different groups according to the organ or system on which they act and/or their therapeutic and chemical characteristics. Each bottom-level ATC code stands for a pharmaceutically used substance, or a combination of substances, in a single indication (or use). This means that one drug can have more than one code: acetylsalicylic acid (aspirin), for example, has A01AD05 as a drug for local oral treatment, B01AC06 as a platelet inhibitor, and N02BA01 as an analgesic and antipyretic. On the other hand, several different brands share the same code if they have the same active substance and indications.", "URL": "http://www.whocc.no/atc/", "Information": [ { "ReferenceNumber": 63, "Name": "ATC Code", "Value": { "StringWithMarkup": [ { "String": "P - Antiparasitic products, insecticides and repellents", "Markup": [ { "Start": 0, "Length": 1, "URL": "https://www.whocc.no/atc_ddd_index/?code=P" } ] }, { "String": "P01 - Antiprotozoals", "Markup": [ { "Start": 0, "Length": 3, "URL": "https://www.whocc.no/atc_ddd_index/?code=P01" } ] }, { "String": "P01B - Antimalarials", "Markup": [ { "Start": 0, "Length": 4, "URL": "https://www.whocc.no/atc_ddd_index/?code=P01B" } ] }, { "String": "P01BA - Aminoquinolines", "Markup": [ { "Start": 0, "Length": 5, "URL": "https://www.whocc.no/atc_ddd_index/?code=P01BA" } ] }, { "String": "P01BA01 - Chloroquine", "Markup": [ { "Start": 0, "Length": 7, "URL": "https://www.whocc.no/atc_ddd_index/?code=P01BA01" } ] } ] } } ] }, { "TOCHeading": "Absorption, Distribution and Excretion", "Information": [ { "ReferenceNumber": 10, "Name": "Absorption", "Value": { "StringWithMarkup": [ { "String": "Chloroquine oral solution has a bioavailability of 52-102% and oral tablets have a bioavailability of 67-114%. Intravenous chloroquine reaches a Cmax of 650-1300µg/L and oral chloroquine reaches a Cmax of 65-128µg/L with a Tmax of 0.5h.", "Markup": [ { "Start": 0, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 123, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 186, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } }, { "ReferenceNumber": 10, "Name": "Route of Elimination", "Value": { "StringWithMarkup": [ { "String": "Chloroquine is predominantly eliminated in the urine. 50% of a dose is recovered in the urine as unchanged chloroquine, with 10% of the dose recovered in the urine as desethylchloroquine.", "Markup": [ { "Start": 0, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 107, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 167, "Length": 19, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/desethylchloroquine", "Type": "PubChem Internal Link", "Extra": "CID-95478" } ] } ] } }, { "ReferenceNumber": 10, "Name": "Volume of Distribution", "Value": { "StringWithMarkup": [ { "String": "The volume of distribution of chloroquine is 200-800L/kg.", "Markup": [ { "Start": 30, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } }, { "ReferenceNumber": 10, "Name": "Clearance", "Value": { "StringWithMarkup": [ { "String": "Chloroquine has a total plasma clearance of 0.35-1L/h/kg.", "Markup": [ { "Start": 0, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "McEvoy, G.K. (ed.). American Hospital Formulary Service. AHFS Drug Information. American Society of Health-System Pharmacists, Bethesda, MD. 2006., p. 859" ], "Value": { "StringWithMarkup": [ { "String": "Chloroquine is rapidly and almost completely absorbed from the GI tract following oral administration, and peak plasma concn of the drug are generally attained within 1-2 hr. Considerable interindividual variations in serum concn of chloroquine have been reported. Oral administration of 310 mg of chloroquine daily reportedly results in peak plasma concn of about 0.125 ug/mL. If 500 mg of chloroquine is administered once weekly, peak plasma concn of the drug reportedly range from 0.15-0.25 ug/mL and trough plasma concn reportedly range from 0.02-0.04 ug/mL. Results of one study indicate that chloroquine may exhibit nonlinear dose dependent pharmacokinetics. In this study, administration of a single 500 mg oral dose of chloroquine resulted in a peak serum concentration of 0.12 ug/mL, and administration of a single 1 g oral dose of the drug resulted in a peak serum concentration of 0.34 ug/mL.", "Markup": [ { "Start": 0, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 233, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 298, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 391, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 598, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 727, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "McEvoy, G.K. (ed.). American Hospital Formulary Service. AHFS Drug Information. American Society of Health-System Pharmacists, Bethesda, MD. 2006., p. 859" ], "Value": { "StringWithMarkup": [ { "String": "Results of one cross-over study in healthy adults indicate that the bioavailability of chloroquine is greater when the drug is administered with food than when the drug is administered in the fasting state. In this study, the rate of absorption of chloroquine was unaffected by the presence of food in the GI tract however, peak plasma concn of chloroquine and areas under the plasma concentration-time curves were higher when 600 mg of the drug was administered with food than when the same dose was administered without food.", "Markup": [ { "Start": 87, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 248, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 345, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "McEvoy, G.K. (ed.). American Hospital Formulary Service. AHFS Drug Information. American Society of Health-System Pharmacists, Bethesda, MD. 2006., p. 859" ], "Value": { "StringWithMarkup": [ { "String": "Chloroquine is widely distributed into body tissues. The drug has an apparent volume of distribution of 116-285 L/kg in healthy adults. Animal studies indicate that concn of chloroquine in liver, spleen, kidney, and lung are at least 200-700 times higher than those in plasma, and concentration of the drug in brain and spinal cord are at least 10-30 times higher than those in plasma. Chloroquine binds to melanin containing cells in the eyes and skin; skin concn of the drug are considerably higher than plasma concentration. Animal studies indicate that the drug is concentrated in the iris and choroid and, to a lesser extent, in the cornea, retina, and sclera and is found in these tissues in higher concentration than in other tissues.", "Markup": [ { "Start": 0, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 174, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 386, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 407, "Length": 7, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/melanin", "Type": "PubChem Internal Link", "Extra": "CID-6325610" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "McEvoy, G.K. (ed.). American Hospital Formulary Service. AHFS Drug Information. American Society of Health-System Pharmacists, Bethesda, MD. 2006., p. 859" ], "Value": { "StringWithMarkup": [ { "String": "Chloroquine is also concentrated in erythrocytes and binds to platelets and granulocytes. Serum concentrations of chloroquine are higher than those in plasma, presumably because the drug is released from platelets during coagulation, and plasma concentrations are 10 to 15% lower than whole blood concentration of the drug.", "Markup": [ { "Start": 0, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 114, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Value": { "StringWithMarkup": [ { "String": "For more Absorption, Distribution and Excretion (Complete) data for CHLOROQUINE (16 total), please visit the HSDB record page.", "Markup": [ { "Start": 109, "Length": 16, "URL": "https://pubchem.ncbi.nlm.nih.gov/source/hsdb/3029#section=Absorption-Distribution-and-Excretion-(Complete)" }, { "Start": 68, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/CHLOROQUINE", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } } ] }, { "TOCHeading": "Metabolism/Metabolites", "Description": "Metabolism/Metabolites information related to the record", "Information": [ { "ReferenceNumber": 10, "Value": { "StringWithMarkup": [ { "String": "Chloroquine is N-dealkylated primarily by CYP2C8 and CYP3A4 to N-desethylchloroquine. It is N-dealkylated to a lesser extent by CYP3A5, CYP2D6, and to an ever lesser extent by CYP1A1. N-desethylchloroquine can be further N-dealkylated to N-bidesethylchloroquine, which is further N-dealkylated to 7-chloro-4-aminoquinoline.", "Markup": [ { "Start": 0, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 65, "Length": 19, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/desethylchloroquine", "Type": "PubChem Internal Link", "Extra": "CID-95478" }, { "Start": 186, "Length": 19, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/desethylchloroquine", "Type": "PubChem Internal Link", "Extra": "CID-95478" }, { "Start": 297, "Length": 25, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/7-chloro-4-aminoquinoline", "Type": "PubChem Internal Link", "Extra": "CID-94711" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "McEvoy, G.K. (ed.). American Hospital Formulary Service. AHFS Drug Information. American Society of Health-System Pharmacists, Bethesda, MD. 2006., p. 860" ], "Value": { "StringWithMarkup": [ { "String": "Chloroquine is partially metabolized; the major metabolite is desethylchloroquine. Desethylchloroquine also has antiplasmodial activity, but is slightly less active than chloroquine. Bisdesethylchloroquine, which is a carboxylic acid derivative, and several other unidentified metabolites are also formed in small amounts.", "Markup": [ { "Start": 0, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 62, "Length": 19, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/desethylchloroquine", "Type": "PubChem Internal Link", "Extra": "CID-95478" }, { "Start": 83, "Length": 19, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Desethylchloroquine", "Type": "PubChem Internal Link", "Extra": "CID-95478" }, { "Start": 170, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 183, "Length": 22, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Bisdesethylchloroquine", "Type": "PubChem Internal Link", "Extra": "CID-122672" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "Thomson.Micromedex. Drug Information for the Health Care Professional. 25th ed. Volume 1. Plus Updates. Content Reviewed by the United States Pharmacopeial Convention, Inc. Greenwood Village, CO. 2005., p. 837" ], "Value": { "StringWithMarkup": [ { "String": "Hepatic (partially), to active de-ethylated metabolites. Principal metabolite is desethylchloroquine", "Markup": [ { "Start": 81, "Length": 19, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/desethylchloroquine", "Type": "PubChem Internal Link", "Extra": "CID-95478" } ] } ] } } ] }, { "TOCHeading": "Biological Half-Life", "Description": "Biological Half-Life information related to the record", "Information": [ { "ReferenceNumber": 10, "Value": { "StringWithMarkup": [ { "String": "The half life of chloroquine is 20-60 days.", "Markup": [ { "Start": 17, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "McEvoy, G.K. (ed.). American Hospital Formulary Service. AHFS Drug Information. American Society of Health-System Pharmacists, Bethesda, MD. 2006., p. 860" ], "Value": { "StringWithMarkup": [ { "String": "The plasma half-life of chloroquine in healthy individuals is generally reported to be 72-120 hr. In one study, serum concentrations of chloroquine appeared to decline in a biphasic manner and the serum half-life of the terminal phase increased with higher dosage of the drug. In this study, the terminal half-life of chloroquine was 3.1 hr after a single 250 mg oral dose, 42.9 hr after a single 500 mg oral dose, and 312 hr after a single 1 g oral dose of the drug.", "Markup": [ { "Start": 24, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 136, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 318, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "Thomson.Micromedex. Drug Information for the Health Care Professional. 25th ed. Volume 1. Plus Updates. Content Reviewed by the United States Pharmacopeial Convention, Inc. Greenwood Village, CO. 2005., p. 837" ], "Value": { "StringWithMarkup": [ { "String": "Terminal elimination half-life is 1 to 2 months." } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "Haddad, L.M. (Ed). Clinical Management of Poisoning and Drug Overdose 3rd Edition. Saunders, Philadelphia, PA. 1998., p. 711" ], "Value": { "StringWithMarkup": [ { "String": "... extremely slow elimination, with a terminal elimination half-life of 200 to 300 hours)" } ] } } ] }, { "TOCHeading": "Mechanism of Action", "Description": "Mechanism of Action information related to the record", "Information": [ { "ReferenceNumber": 10, "Value": { "StringWithMarkup": [ { "String": "Chloroquine inhibits the action of heme polymerase in malarial trophozoites, preventing the conversion of heme to hemazoin. _Plasmodium_ species continue to accumulate toxic heme, killing the parasite. Chloroquine passively diffuses through cell membranes and into endosomes, lysosomes, and Golgi vesicles; where it becomes protonated, trapping the chloroquine in the organelle and raising the surrounding pH. The raised pH in endosomes, prevent virus particles from utilizing their activity for fusion and entry into the cell. Chloroquine does not affect the level of ACE2 expression on cell surfaces, but inhibits terminal glycosylation of ACE2, the receptor that SARS-CoV and SARS-CoV-2 target for cell entry. ACE2 that is not in the glycosylated state may less efficiently interact with the SARS-CoV-2 spike protein, further inhibiting viral entry.", "Markup": [ { "Start": 0, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 203, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 350, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 530, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "McEvoy, G.K. (ed.). American Hospital Formulary Service. AHFS Drug Information. American Society of Health-System Pharmacists, Bethesda, MD. 2006., p. 859" ], "Value": { "StringWithMarkup": [ { "String": "The exact mechanism of antimalarial activity of chloroquine has not been determined. The 4-aminoquinoline derivatives appear to bind to nucleoproteins and interfere with protein synthesis in susceptible organisms; the drugs intercalate readily into double-stranded DNA and inhibit both DNA and RNA polymerase. In addition, studies using chloroquine indicate that the drug apparently concentrates in parasite digestive vacuoles, increases the pH of the vacuoles, and interferes with the parasite's ability to metabolize and utilize erythrocyte hemoglobin. Plasmodial forms that do not have digestive vacuoles and do not utilize hemoglobin, such as exoerythrocytic forms, are not affected by chloroquine.", "Markup": [ { "Start": 48, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 89, "Length": 16, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/4-aminoquinoline", "Type": "PubChem Internal Link", "Extra": "CID-68476" }, { "Start": 337, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 690, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "McEvoy, G.K. (ed.). American Hospital Formulary Service. AHFS Drug Information. American Society of Health-System Pharmacists, Bethesda, MD. 2006., p. 859" ], "Value": { "StringWithMarkup": [ { "String": "The 4-aminoquinoline derivatives, including chloroquine, also have anti-inflammatory activity; however, the mechanism(s) of action of the drugs in the treatment of rheumatoid arthritis and lupus erythematosus has not been determined. Chloroquine reportedly antagonizes histamine in vitro, has antiserotonin effects, and inhibits prostaglandin effects in mammalian cells presumably by inhibiting conversion of arachidonic acid to prostaglandin F2. In vitro studies indicate that chloroquine also inhibits chemotaxis of polymorphonuclear leukocytes, macrophages, and eosinophils.", "Markup": [ { "Start": 4, "Length": 16, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/4-aminoquinoline", "Type": "PubChem Internal Link", "Extra": "CID-68476" }, { "Start": 44, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 234, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 269, "Length": 9, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/histamine", "Type": "PubChem Internal Link", "Extra": "CID-774" }, { "Start": 409, "Length": 16, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/arachidonic%20acid", "Type": "PubChem Internal Link", "Extra": "CID-444899" }, { "Start": 429, "Length": 16, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/prostaglandin%20F2", "Type": "PubChem Internal Link", "Extra": "CID-71312086" }, { "Start": 478, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "Thomson.Micromedex. Drug Information for the Health Care Professional. 25th ed. Volume 1. Plus Updates. Content Reviewed by the United States Pharmacopeial Convention, Inc. Greenwood Village, CO. 2005., p. 837" ], "Value": { "StringWithMarkup": [ { "String": "Antiprotozoal-Malaria: /Mechanism of action/ may be based on ability of chloroquine to bind and alter the properties of DNA. Chloroquine also is taken up into the acidic food vacuoles of the parasite in the erythrocyte. It increases the pH of the acid vesicles, interfering with vesicle functions and possibly inhibiting phospholipid metabolism. In suppressive treatment, chloroquine inhibits the erythrocytic stage of development of plasmodia. In acute attacks of malaria, chloroquine interrupts erythrocytic schizogony of the parasite. its ability to concentrate in parasitized erythrocytes may account for its selective toxicity against the erythrocytic stages of plasmodial infection.", "Markup": [ { "Start": 72, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 125, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 372, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 474, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "Thomson.Micromedex. Drug Information for the Health Care Professional. 25th ed. Volume 1. Plus Updates. Content Reviewed by the United States Pharmacopeial Convention, Inc. Greenwood Village, CO. 2005., p. 837" ], "Value": { "StringWithMarkup": [ { "String": "Antirheumatic-Chloroquine is though to act as a mild immunosuppressant, inhibiting the production of rheumatoid factor and acute phase reactants. It also accumulates in white blood cells, stabilizing lysosomal membranes and inhibiting the activity of many enzymes, including collagenase and the proteases that cause cartilage breakdown.", "Markup": [ { "Start": 14, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } } ] }, { "TOCHeading": "Human Metabolite Information", "Description": "Chemical metabolite information from the Human Metabolome Database (HMDB).", "URL": "http://www.hmdb.ca/", "Section": [ { "TOCHeading": "Cellular Locations", "Description": "The metabolome in Cellular Locations", "DisplayControls": { "ListType": "Columns" }, "Information": [ { "ReferenceNumber": 19, "Name": "Cellular Locations", "Value": { "StringWithMarkup": [ { "String": "Cytoplasm" }, { "String": "Extracellular" }, { "String": "Membrane" } ] } } ] } ] } ] }, { "TOCHeading": "Use and Manufacturing", "Description": "The use and manufacture of the chemical and related information", "Section": [ { "TOCHeading": "Uses", "Description": "This section presents the major uses of the chemical in the United States today. In addition, past uses of the chemical are summarized.", "Information": [ { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Value": { "StringWithMarkup": [ { "String": "MEDICATION" } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "O'Neil, M.J. (ed.). The Merck Index - An Encyclopedia of Chemicals, Drugs, and Biologicals. 13th Edition, Whitehouse Station, NJ: Merck and Co., Inc., 2001., p. 373" ], "Value": { "StringWithMarkup": [ { "String": "Antimalarial, antiamebic, antitheuratic, Lupus erthematus supressant" } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "National Library of Medicine, SIS; ChemIDplus Record for Chloroquine. (54-05-7). Available from, as of April 17, 2006: https://chem.sis.nlm.nih.gov/chemidplus/chemidlite.jsp" ], "Value": { "StringWithMarkup": [ { "String": "Mesh Heading: Amebicides, antimalarials, antirheumatic Agents" } ] } } ], "Section": [ { "TOCHeading": "Use Classification", "Description": "This section contains use classification/category information from various sources", "Information": [ { "ReferenceNumber": 16, "Name": "Use Classification", "Value": { "StringWithMarkup": [ { "String": "Human drugs -> Rare disease (orphan)" } ] } }, { "ReferenceNumber": 17, "Value": { "StringWithMarkup": [ { "String": "Human Drugs -> FDA Approved Drug Products with Therapeutic Equivalence Evaluations (Orange Book) -> Active Ingredients" } ] } }, { "ReferenceNumber": 54, "Reference": [ "S72 | NTUPHTW | Pharmaceutically Active Substances from National Taiwan University | DOI:10.5281/zenodo.3955664" ], "Value": { "StringWithMarkup": [ { "String": "Pharmaceuticals" } ] } } ] } ] }, { "TOCHeading": "Methods of Manufacturing", "Description": "Methods of Manufacturing from HSDB and other sources.", "Information": [ { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "O'Neil, M.J. (ed.). The Merck Index - An Encyclopedia of Chemicals, Drugs, and Biologicals. 13th Edition, Whitehouse Station, NJ: Merck and Co., Inc., 2001., p. 373" ], "Value": { "StringWithMarkup": [ { "String": "H. Andersag et al., US 2233970 (1941 to Winthrop)" } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "O'Neil, M.J. (ed.). The Merck Index - An Encyclopedia of Chemicals, Drugs, and Biologicals. 13th Edition, Whitehouse Station, NJ: Merck and Co., Inc., 2001., p. 373" ], "Value": { "StringWithMarkup": [ { "String": "Condensation of 4,7-dichloroquinoline with 1-diethylamino-4-aminopentane: German patent 683692 (1939);", "Markup": [ { "Start": 16, "Length": 21, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/4%2C7-dichloroquinoline", "Type": "PubChem Internal Link", "Extra": "CID-6866" }, { "Start": 43, "Length": 29, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/1-diethylamino-4-aminopentane", "Type": "PubChem Internal Link", "Extra": "CID-78953" } ] } ] } } ] }, { "TOCHeading": "Formulations/Preparations", "Description": "Formulations/Preparations from HSDB and other sources", "Information": [ { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "Gilman, A.G., T.W. Rall, A.S. Nies and P. Taylor (eds.). Goodman and Gilman's The Pharmacological Basis of Therapeutics. 8th ed. New York, NY. Pergamon Press, 1990., p. 982" ], "Value": { "StringWithMarkup": [ { "String": "Chloroquine phosphate, USP ... is available as tablets containing either 250 or 500 mg of diphosphate. Approximately 60% of diphosphate represents base. /Chloroquine phosphate/", "Markup": [ { "Start": 0, "Length": 21, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine%20phosphate", "Type": "PubChem Internal Link", "Extra": "CID-64927" }, { "Start": 154, "Length": 21, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine%20phosphate", "Type": "PubChem Internal Link", "Extra": "CID-64927" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "O'Neil, M.J. (ed.). The Merck Index - An Encyclopedia of Chemicals, Drugs, and Biologicals. 13th Edition, Whitehouse Station, NJ: Merck and Co., Inc., 2001., p. 373" ], "Value": { "StringWithMarkup": [ { "String": "Arechin; Avloclor; Imagon; Malaquin; Resochin; Tresochin. /Chloroquine diphosphate/", "Markup": [ { "Start": 0, "Length": 7, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Arechin", "Type": "PubChem Internal Link", "Extra": "CID-64927" }, { "Start": 9, "Length": 8, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Avloclor", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 19, "Length": 6, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Imagon", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 37, "Length": 8, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Resochin", "Type": "PubChem Internal Link", "Extra": "CID-83818" }, { "Start": 47, "Length": 9, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Tresochin", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 59, "Length": 23, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine%20diphosphate", "Type": "PubChem Internal Link", "Extra": "CID-64927" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "O'Neil, M.J. (ed.). The Merck Index - An Encyclopedia of Chemicals, Drugs, and Biologicals. 13th Edition, Whitehouse Station, NJ: Merck and Co., Inc., 2001., p. 373" ], "Value": { "StringWithMarkup": [ { "String": "Nivaquine. /Chloroquine Sulfate/", "Markup": [ { "Start": 12, "Length": 19, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine%20Sulfate", "Type": "PubChem Internal Link", "Extra": "CID-91441" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "Hussar, D.A. (ed.). Modell's Drugs in Current Use and New Drugs. 38th ed. New York, NY: Springer Publishing Co., 1992., p. 37" ], "Value": { "StringWithMarkup": [ { "String": "Tablets (as the phosphate), 500 mg. Vials (as the dihydrochloride), 50 mg/ml.", "Markup": [ { "Start": 16, "Length": 9, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/phosphate", "Type": "PubChem Internal Link", "Extra": "CID-1061" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "McEvoy, G.K. (ed.). American Hospital Formulary Service. AHFS Drug Information. American Society of Health-System Pharmacists, Bethesda, MD. 2006., p. 860" ], "Value": { "StringWithMarkup": [ { "String": "Chloroquine Phosphate: Oral tablets 300 mg or 150 mg (of chloroquine).", "Markup": [ { "Start": 0, "Length": 21, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine%20Phosphate", "Type": "PubChem Internal Link", "Extra": "CID-64927" }, { "Start": 57, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } } ] }, { "TOCHeading": "U.S. Production", "Description": "U.S. Production", "Information": [ { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "SRI" ], "Value": { "StringWithMarkup": [ { "String": "(1977) PROBABLY MORE THAN 4.5X10+5 G /PHOSPHATE/", "Markup": [ { "Start": 38, "Length": 9, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/PHOSPHATE", "Type": "PubChem Internal Link", "Extra": "CID-1061" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "SRI" ], "Value": { "StringWithMarkup": [ { "String": "(1979) PROBABLY MORE THAN 4.5X10+5 G /PHOSPHATE/", "Markup": [ { "Start": 38, "Length": 9, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/PHOSPHATE", "Type": "PubChem Internal Link", "Extra": "CID-1061" } ] } ] } } ] }, { "TOCHeading": "U.S. Imports", "Description": "Information regarding U.S. Imports", "Information": [ { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "SRI" ], "Value": { "StringWithMarkup": [ { "String": "(1977) 6X10+5 G-PRINCPL CUSTMS DISTS /PHOSPHATE/", "Markup": [ { "Start": 38, "Length": 9, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/PHOSPHATE", "Type": "PubChem Internal Link", "Extra": "CID-1061" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "SRI" ], "Value": { "StringWithMarkup": [ { "String": "(1979) 3X10+5 G-PRINCPL CUSTMS DISTS /PHOSPHATE/", "Markup": [ { "Start": 38, "Length": 9, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/PHOSPHATE", "Type": "PubChem Internal Link", "Extra": "CID-1061" } ] } ] } } ] }, { "TOCHeading": "General Manufacturing Information", "Description": "General Manufacturing Information", "DisplayControls": { "ListType": "Columns" }, "Information": [ { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "Lewis, R.J. Sr.; Hawley's Condensed Chemical Dictionary 14th Edition. John Wiley & Sons, Inc. New York, NY 2001., p. 259" ], "Value": { "StringWithMarkup": [ { "String": "Usually dispensed as the phosphate.", "Markup": [ { "Start": 25, "Length": 9, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/phosphate", "Type": "PubChem Internal Link", "Extra": "CID-1061" } ] } ] } } ] } ] }, { "TOCHeading": "Identification", "Description": "This section contains laboratory methods how to identify the chemical and more.", "Section": [ { "TOCHeading": "Analytic Laboratory Methods", "Description": "Analytic Laboratory Methods for the sample analysis", "Information": [ { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "Sunshine, I. (ed.). CRC Handbook of Analytical Toxicology. Cleveland: The Chemical Rubber Co., 1969., p. 28" ], "Value": { "StringWithMarkup": [ { "String": "GENERAL SAMPLE, FLUOROMETRY (EXCITATION= 350, EMISSION= 405)." } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "U.S. Pharmacopeia. The United States Pharmacopeia, USP 29/The National Formulary, NF 24; Rockville, MD: U.S. Pharmacopeial Convention, Inc., p480 (2006)" ], "Value": { "StringWithMarkup": [ { "String": "Analyte: chloroquine; matrix: chemical identification; procedure: infrared absorption spectrophotometry with comparison to standards", "Markup": [ { "Start": 9, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "U.S. Pharmacopeia. The United States Pharmacopeia, USP 29/The National Formulary, NF 24; Rockville, MD: U.S. Pharmacopeial Convention, Inc., p480 (2006)" ], "Value": { "StringWithMarkup": [ { "String": "Analyte: chloroquine; matrix: chemical identification; procedure: ultraviolet absorption spectrophotometry with comparison to standards", "Markup": [ { "Start": 9, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "U.S. Pharmacopeia. The United States Pharmacopeia, USP 29/The National Formulary, NF 24; Rockville, MD: U.S. Pharmacopeial Convention, Inc., p480 (2006)" ], "Value": { "StringWithMarkup": [ { "String": "Analyte: chloroquine; matrix: chemical purity; procedure: dissolution in glacial acetic acid; addition of crystal violet indicator; titration with perchloric acid", "Markup": [ { "Start": 9, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 81, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/acetic%20acid", "Type": "PubChem Internal Link", "Extra": "CID-176" }, { "Start": 106, "Length": 14, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/crystal%20violet", "Type": "PubChem Internal Link", "Extra": "CID-11057" }, { "Start": 147, "Length": 15, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/perchloric%20acid", "Type": "PubChem Internal Link", "Extra": "CID-24247" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Value": { "StringWithMarkup": [ { "String": "For more Analytic Laboratory Methods (Complete) data for CHLOROQUINE (20 total), please visit the HSDB record page.", "Markup": [ { "Start": 98, "Length": 16, "URL": "https://pubchem.ncbi.nlm.nih.gov/source/hsdb/3029#section=Analytic-Laboratory-Methods-(Complete)" }, { "Start": 57, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/CHLOROQUINE", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } } ] }, { "TOCHeading": "Clinical Laboratory Methods", "Description": "Clinical Laboratory Methods for the sample analysis", "Information": [ { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "Sunshine, Irving (ed.) Methodology for Analytical Toxicology. Cleveland: CRC Press, Inc., 1975., p. 83" ], "Value": { "StringWithMarkup": [ { "String": "Determination of chloroquine in blood, plasma, red cells, or urine specimen using spectrophotometer with UV absorption spectrum at 0.0 to 0.1 absorbance range. Recovery is about 90 + or - 2%.", "Markup": [ { "Start": 17, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "Chaulet JF et al; J Chromatogr Biomed Appl 613 (2): 303-10 (1993)" ], "Value": { "StringWithMarkup": [ { "String": "A high-performance liquid chromatography method with fluorescence detection is described for the simultaneous measurement of quinine, chloroquine and mono- and bidesethylchloroquine in human plasma, erythrocytes and urine ... The limit of detection was ca 5 ng/mL of chloroquine and ca 23 ng/mL for quinine ...", "Markup": [ { "Start": 125, "Length": 7, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/quinine", "Type": "PubChem Internal Link", "Extra": "CID-3034034" }, { "Start": 134, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 267, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 299, "Length": 7, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/quinine", "Type": "PubChem Internal Link", "Extra": "CID-3034034" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "PMID:2313570", "Escande C et al; J Pharm Sci 79 (1): 23-7 (1990)" ], "Value": { "StringWithMarkup": [ { "String": "Two new methods for the simultaneous detn of chloroquine and its two main metabolites (monodesethylchloroquine and bisdesethylchloroquine) in biol samples, RIA and ELISA, are described ... Sensitivity limits are, respectively, 0.70 nM (3 pg of chloroquine sulfate measured in 10 uLof plasma sample) for RIA, and 10 nM (22 pg of chloroquine sulfate measured in 5 uL of plasma sample) for ELISA. The interassay coefficients of variation are, respectively, <10 and <16% for RIA and ELISA in the range 14 to 410 nM (6 to 180 ng/mL) ...", "Markup": [ { "Start": 45, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 87, "Length": 23, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/monodesethylchloroquine", "Type": "PubChem Internal Link", "Extra": "CID-38989112" }, { "Start": 115, "Length": 22, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/bisdesethylchloroquine", "Type": "PubChem Internal Link", "Extra": "CID-122672" }, { "Start": 244, "Length": 19, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine%20sulfate", "Type": "PubChem Internal Link", "Extra": "CID-91441" }, { "Start": 328, "Length": 19, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine%20sulfate", "Type": "PubChem Internal Link", "Extra": "CID-91441" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "Tracqui A et al; J Forensic Sci 40: 254-262 (1995). As cited in: Lunn G, Schmuff N; HPLC Methods for Pharmaceutical Analysis. New York, NY: John Wiley & Sons, 1997., p.459" ], "Value": { "StringWithMarkup": [ { "String": "Analyte: chloroquine; matrix: blood (whole, plasma); procedure: high-performance liquid chromatography with ultraviolet detection at 229 nm; limit of detection: <120 ng/mL", "Markup": [ { "Start": 9, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Value": { "StringWithMarkup": [ { "String": "For more Clinical Laboratory Methods (Complete) data for CHLOROQUINE (17 total), please visit the HSDB record page.", "Markup": [ { "Start": 98, "Length": 16, "URL": "https://pubchem.ncbi.nlm.nih.gov/source/hsdb/3029#section=Clinical-Laboratory-Methods-(Complete)" }, { "Start": 57, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/CHLOROQUINE", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } } ] } ] }, { "TOCHeading": "Safety and Hazards", "Description": "Safety and hazards information, properties, management techniques, reactivities and incompatibilities, first aid treatments, and more. For toxicity and related information, please visit Toxicity section.", "Section": [ { "TOCHeading": "Hazards Identification", "Description": "Hazards Identification includes all hazards regarding the chemical; required label elements", "Section": [ { "TOCHeading": "GHS Classification", "Description": "GHS (Globally Harmonized System of Classification and Labelling of Chemicals) is a United Nations system to identify hazardous chemicals and to inform users about these hazards. GHS has been adopted by many countries around the world and is now also used as the basis for international and national transport regulations for dangerous goods. The GHS hazard statements, class categories, pictograms, signal words, and the precautionary statements can be found on the PubChem GHS page.", "URL": "https://pubchem.ncbi.nlm.nih.gov/ghs/", "DisplayControls": { "CreateTable": { "FromInformationIn": "ThisSection", "NumberOfColumns": 2, "ColumnContents": [ "Name", "Value" ] }, "ShowAtMost": 1 }, "Information": [ { "ReferenceNumber": 14, "Name": "Pictogram(s)", "Value": { "StringWithMarkup": [ { "String": " ", "Markup": [ { "Start": 0, "Length": 1, "URL": "https://pubchem.ncbi.nlm.nih.gov/images/ghs/GHS07.svg", "Type": "Icon", "Extra": "Irritant" } ] } ] } }, { "ReferenceNumber": 14, "Name": "Signal", "Value": { "StringWithMarkup": [ { "String": "Warning", "Markup": [ { "Start": 0, "Length": 7, "Type": "Color", "Extra": "GHSWarning" } ] } ] } }, { "ReferenceNumber": 14, "Name": "GHS Hazard Statements", "Value": { "StringWithMarkup": [ { "String": "H302 (100%): Harmful if swallowed [Warning Acute toxicity, oral]", "Markup": [ { "Start": 35, "Length": 7, "Type": "Color", "Extra": "GHSWarning" } ] } ] } }, { "ReferenceNumber": 14, "Name": "Precautionary Statement Codes", "Value": { "StringWithMarkup": [ { "String": "P264, P270, P301+P317, P330, and P501" }, { "String": "(The corresponding statement to each P-code can be found at the GHS Classification page.)", "Markup": [ { "Start": 64, "Length": 18, "URL": "https://pubchem.ncbi.nlm.nih.gov/ghs/#_prec" } ] } ] } }, { "ReferenceNumber": 14, "Name": "ECHA C&L Notifications Summary", "Value": { "StringWithMarkup": [ { "String": "Aggregated GHS information provided by 40 companies from 2 notifications to the ECHA C&L Inventory.", "Markup": [ { "Start": 0, "Length": 103, "Type": "Italics" } ] }, { "String": "Information may vary between notifications depending on impurities, additives, and other factors. The percentage value in parenthesis indicates the notified classification ratio from companies that provide hazard codes. Only hazard codes with percentage values above 10% are shown.", "Markup": [ { "Start": 0, "Length": 281, "Type": "Italics" } ] } ] } } ] }, { "TOCHeading": "Hazard Classes and Categories", "Description": "The Hazard Classes and Categories are aligned with GHS (Globally Harmonized System of Classification and Labelling of Chemicals) hazard statement codes. More info can be found at the PubChem GHS summary page. The percentage data in the parenthesis from ECHA indicates that the hazard classes and categories information are consolidated from multiple companies, see the detailed explanation from the above GHS classification section.", "URL": "https://pubchem.ncbi.nlm.nih.gov/ghs/", "DisplayControls": { "ShowAtMost": 2 }, "Information": [ { "ReferenceNumber": 14, "Value": { "StringWithMarkup": [ { "String": "Acute Tox. 4 (100%)" } ] } } ] }, { "TOCHeading": "Skin, Eye, and Respiratory Irritations", "Description": "Symptoms of Skin, Eye, and Respiratory Irritations cause by chemical hazards", "Information": [ { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "PMID:2253570", "Obikili AG; East Afr Med J 67 (9): 614-21 (1990)" ], "Value": { "StringWithMarkup": [ { "String": "Eleven cases of macular degeneration occurring between the ages of 22 yr and 40 yr are presented. All the patients gave positive history of chloroquine intake and outdoor activity. In 4 of the 11 cases, pterygium was an associated ocular finding. The female to male ratio was 3 to 1. The macular lesions were bilateral and symmetrical in all the cases. It is postulated that the effect of chronic chloroquine ingestion exacerbated by chronic light toxicity might be responsible for this type of macular degeneration presenting in adults.", "Markup": [ { "Start": 140, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 397, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } } ] } ] }, { "TOCHeading": "Accidental Release Measures", "Description": "Accidental release measures lists emergency procedures; protective equipment; proper methods of containment and cleanup.", "Section": [ { "TOCHeading": "Disposal Methods", "Description": "Disposal Methods for this chemical", "Information": [ { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Value": { "StringWithMarkup": [ { "String": "SRP: At the time of review, criteria for land treatment or burial (sanitary landfill) disposal practices are subject to significant revision. Prior to implementing land disposal of waste residue (including waste sludge), consult with environmental regulatory agencies for guidance on acceptable disposal practices." } ] } } ] } ] }, { "TOCHeading": "Regulatory Information", "Description": "Related Regulatory Information", "Section": [ { "TOCHeading": "FDA Requirements", "Description": "FDA Requirements for the chemical's safety and hard information", "Information": [ { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "DHHS/FDA; Electronic Orange Book-Approved Drug Products with Therapeutic Equivalence Evaluations. Available from, as of July 26, 2006: https://www.fda.gov/cder/ob/" ], "Value": { "StringWithMarkup": [ { "String": "The Approved Drug Products with Therapeutic Equivalence Evaluations List identifies currently marketed prescription drug products, incl chloroquine phosphate, approved on the basis of safety and effectiveness by FDA under sections 505 of the Federal Food, Drug, and Cosmetic Act. /Chloroquine phosphate/", "Markup": [ { "Start": 137, "Length": 21, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine%20phosphate", "Type": "PubChem Internal Link", "Extra": "CID-64927" }, { "Start": 283, "Length": 21, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine%20phosphate", "Type": "PubChem Internal Link", "Extra": "CID-64927" } ] } ] } } ] } ] }, { "TOCHeading": "Other Safety Information", "Description": "Other Safety Information includes the date of preparation or last revision", "Section": [ { "TOCHeading": "Special Reports", "Description": "Special Reports for the given chemical", "Information": [ { "ReferenceNumber": 18, "Value": { "StringWithMarkup": [ { "String": "Fitch CD; Ferriprotoporphyrin IX: role in chloroquine susceptibility and resistance in malaria.; Prog Clin Biol Res 313: 45-52 (1989). A review of all available evidence supports the hypothesis that ferriprotoporphyrin is the receptor for chloroquine and mediator of its antimalarial activity.", "Markup": [ { "Start": 10, "Length": 22, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Ferriprotoporphyrin%20IX", "Type": "PubChem Internal Link", "Extra": "multiple-CIDs" }, { "Start": 42, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 199, "Length": 19, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/ferriprotoporphyrin", "Type": "PubChem Internal Link", "Extra": "CID-455658" }, { "Start": 239, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } }, { "ReferenceNumber": 18, "Value": { "StringWithMarkup": [ { "String": "Ochsendorf FR, Runne U; Chloroquine and hydroxychloroquine: side effect profile of important therapeutic drugs; Hautarzt 42 (3): 140-6 (1991). Precise knowledge of the undesirable effects of chloroquine and hydroxychloroquine allows better exploitation of their therapeutic effects.", "Markup": [ { "Start": 24, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 40, "Length": 18, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/hydroxychloroquine", "Type": "PubChem Internal Link", "Extra": "CID-3652" }, { "Start": 191, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 207, "Length": 18, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/hydroxychloroquine", "Type": "PubChem Internal Link", "Extra": "CID-3652" } ] } ] } } ] } ] } ] }, { "TOCHeading": "Toxicity", "Description": "Toxicity information related to this record, includes routes of exposure; related symptoms, acute and chronic effects; numerical measures of toxicity.", "Section": [ { "TOCHeading": "Toxicological Information", "Description": "Toxicological Information", "Section": [ { "TOCHeading": "Toxicity Summary", "Description": "Toxicity Summary", "Information": [ { "ReferenceNumber": 10, "Value": { "StringWithMarkup": [ { "String": "Patients experiencing an overdose may present with headache, drowsiness, visual disturbances, nausea, vomiting, cardiovascular collapse, shock, convulsions, respiratory arrest, cardiac arrest, and hypokalemia. Overdose should be managed with symptomatic and supportive treatment which may include prompt emesis, gastric lavage, and activated charcoal.", "Markup": [ { "Start": 342, "Length": 8, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/charcoal", "Type": "PubChem Internal Link", "Extra": "CID-5462310" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "International Programme on Chemical Safety; Poisons Information Monograph: Chloroquine (PIM 123) (1994) Available from, as of October 24, 2005: https://www.inchem.org/pages/pims.html" ], "Value": { "StringWithMarkup": [ { "String": "IDENTIFICATION: Chloroquine is a white or slightly yellow, odorless crystalline powder with a bitter taste. Very slightly soluble in water, soluble in chloroform, ether and dilute acids. Chloroquine diphosphate is a white, bitter, crystalline powder. Chloroquine sulfate is a white, odorless, bitter, crystalline powder. Hydroxychloride chloroquine is a colorless liquid. Uses: Indications: Malaria: Chloroquine is the drug of choice for the prophylaxis and treatment of malaria caused by Plasmodium vivax. P. ovale, P. malariae and sensitive P. falciparum. Amebiasis: Chloroquine is used for the treatment of extraintestinal amebiasis (usually in combination with amebicides). Treatment of discoid lupus erythematosis and rheumatoid arthritis (acute and chronic). Chloroquine may be used for the treatment of these conditions. Other less common indications are: amebic liver abscess, porphyria cutanea tarda, solar urticaria, chronic cutaneous vasculitis. HUMAN EXPOSURE: Main risks and target organs: The main toxic effects of chloroquine are related to its quinidine-like (membrane stabilizing) actions on the heart. Other acute effects are respiratory depression and severe gastro-intestinal irritation. Summary of clinical effects: Toxic manifestations appear rapidly within one to three hours after ingestion and include: Cardiac disturbances: circulatory arrest, shock, conduction disturbances, ventricular arrhythmias. Neurological symptoms: drowsiness, coma and sometimes convulsions. Visual disturbances not uncommon. Respiratory symptoms: apnea. Gastrointestinal symptoms: severe gastrointestinal irritation; nausea, vomiting, cramps, diarrhea. Children are specially sensitive to toxic effects. Dizziness, nausea, vomiting, diarrhea, headache, drowsiness, blurred vision, diplopia, blindness, convulsions, coma, hypotension, cardiogenic shock, cardiac arrest and impaired respiration are the characteristic features of chloroquine poisoning. Electrocardiography (ECG) may show decrease of T wave, widening of QRS, ventricular tachycardia and fibrillation. Hypokalemia is associated with severe poisoning. Contraindications: Hepatic and renal function impairment, blood disorders, gastrointestinal illnesses, glucose-6-phosphate dehydrogenase (G-6-PD) deficiency, severe neurological disorders, retinal or visual field changes. Chloroquine should not be used in association with gold salts or phenylbutazone. Routes of entry: Oral: Oral absorption is the most frequent cause of intoxication. Parenteral: Intoxication after parenteral administration is rare. A fatal outcome reported was after 250 mg IV chloroquine in a 42-year-old man. Absorption by route of exposure: Readily and almost completely absorbed from the gastrointestinal tract. Bioavailability is 89% for tablets. Peak plasma concentration is reached 1.5 to 3 hours after ingestion. Distribution by route of exposure: Protein binding: 5O to 65%. Chloroquine accumulates in high concentrations in kidney, liver, lung and spleen, and is strongly bound in melanin-containing cells (eye and skin). Red cell concentration is five to ten times the plasma concentration. Very low concentrations are found in the intestinal wall. Crosses the placenta. Biological half-life by route of exposure: Plasma terminal half-life is mean 278 hours or 70 to 120 hours. Shorter plasma elimination half-lives have been reported in children: 75 to 136 hours. Metabolism: Chloroquine undergoes metabolism by hepatic mechanisms. The main active metabolite is desethylchloroquine. Plasma half-life of desethylchloroquine is similar to chloroquine. Elimination by route of exposure: Chloroquine is eliminated very slowly. About 55% is excreted in urine and 19% in feces within 77 days following therapy with 310 mg for 14 days. Kidney: in urine about 70% is unchanged chloroquine and 23% is desethylchloroquine. It is excreted in breast milk. Toxicodynamics: The cardiotoxicity of chloroquine is related to it quinidine-like (membrane/stabilizing) effects. Chloroquine has a negative inotropic action, inhibits spontaneous diastolic depolarization, slows conduction, lengthens the effective refractory period and raises the electrical threshold. This results in depression of contractility, impairment of conductivity, decrease of excitability, but with possible abnormal stimulus re-entry mechanism. Hypokalemia: Acute hypokalemia may occur in acute poisoning. It is probably related to intracellular transport of potassium by a direct effect on cellular membrane permeability. Neurological symptoms: Neurological symptoms in acute overdose may be related to a direct toxic effect on CNS or to cerebral ischemia due to circulatory failure or respiratory insufficiency. The mechanism of the anti-inflammatory effect is not known. Toxicity: Human data: Chloroquine has a low margin of safety; the therapeutic, toxic and lethal doses are very close. Fatalities have been reported in children after chloroquine overdoses. Interactions: Chloroquine toxicity may be increased by all drugs with quinidine-like effects. Combination with hepatotoxic or dermatitis-causing medication should be avoided, as well as with heparin (risk of hemorrhage) and penicillamine. Eye: Keratopathy and retinopathy may occur when large doses of chloroquine are used for long periods. Changes occurring in the cornea are usually completely reversible on discontinuing treatment; changes in the retina, pigmentary degeneration of the retina, loss of vision, scotomas, optic nerve atrophy, field defects and blindness are irreversible. Retinopathy is considered to occur when the total cumulative dose ingested exceeds 100 g. Blurring of vision, diplopia may occur with short-term chloroquine therapy and are reversible. ANIMAL/PLANT STUDIES: The following progression of ECG changes was observed in dogs with experimental overdosage: severe tachycardia preceded by loss of voltage and widening of QRS, followed by sinus bradycardia, ventricular tachycardia, ventricular fibrillation and finally asystole.", "Markup": [ { "Start": 16, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 134, "Length": 5, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/water", "Type": "PubChem Internal Link", "Extra": "CID-962" }, { "Start": 152, "Length": 10, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroform", "Type": "PubChem Internal Link", "Extra": "CID-6212" }, { "Start": 188, "Length": 23, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine%20diphosphate", "Type": "PubChem Internal Link", "Extra": "CID-64927" }, { "Start": 252, "Length": 19, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine%20sulfate", "Type": "PubChem Internal Link", "Extra": "CID-91441" }, { "Start": 323, "Length": 15, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Hydroxychloride", "Type": "PubChem Internal Link", "Extra": "CID-24341" }, { "Start": 339, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 402, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 571, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 768, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 1032, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 1063, "Length": 9, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/quinidine", "Type": "PubChem Internal Link", "Extra": "CID-441074" }, { "Start": 1937, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 2226, "Length": 19, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/glucose-6-phosphate", "Type": "PubChem Internal Link", "Extra": "CID-5958" }, { "Start": 2312, "Length": 7, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/retinal", "Type": "PubChem Internal Link", "Extra": "CID-638015" }, { "Start": 2345, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 2396, "Length": 4, "URL": "https://pubchem.ncbi.nlm.nih.gov/element/Gold", "Type": "PubChem Internal Link", "Extra": "Element-Gold" }, { "Start": 2410, "Length": 14, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/phenylbutazone", "Type": "PubChem Internal Link", "Extra": "CID-4781" }, { "Start": 2622, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 2931, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 3038, "Length": 7, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/melanin", "Type": "PubChem Internal Link", "Extra": "CID-6325610" }, { "Start": 3440, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 3527, "Length": 19, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/desethylchloroquine", "Type": "PubChem Internal Link", "Extra": "CID-95478" }, { "Start": 3569, "Length": 19, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/desethylchloroquine", "Type": "PubChem Internal Link", "Extra": "CID-95478" }, { "Start": 3603, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 3650, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 3837, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 3860, "Length": 19, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/desethylchloroquine", "Type": "PubChem Internal Link", "Extra": "CID-95478" }, { "Start": 3950, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 3979, "Length": 9, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/quinidine", "Type": "PubChem Internal Link", "Extra": "CID-441074" }, { "Start": 4027, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 4487, "Length": 9, "URL": "https://pubchem.ncbi.nlm.nih.gov/element/Potassium", "Type": "PubChem Internal Link", "Extra": "Element-Potassium" }, { "Start": 4825, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 4969, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 5006, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 5063, "Length": 9, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/quinidine", "Type": "PubChem Internal Link", "Extra": "CID-441074" }, { "Start": 5185, "Length": 7, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/heparin", "Type": "PubChem Internal Link", "Extra": "CID-772" }, { "Start": 5218, "Length": 13, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/penicillamine", "Type": "PubChem Internal Link", "Extra": "CID-5852" }, { "Start": 5296, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 5732, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } } ] }, { "TOCHeading": "Hepatotoxicity", "Description": "This section provides a short description about the hepatotoxicity that associated with the agent, the rate of serum enzyme elevations during use, and the frequency and character of the clinically apparent liver injury associated with the medication.", "URL": "https://www.ncbi.nlm.nih.gov/books/NBK547852/", "Information": [ { "ReferenceNumber": 22, "Value": { "StringWithMarkup": [ { "String": "Despite use for more than 50 years, chloroquine has rarely been linked to serum aminotransferase elevations or to clinically apparent acute liver injury. In patients with acute porphyria and porphyria cutanea tarda, chloroquine can trigger an acute attack with fever and serum aminotransferase elevations, sometimes resulting in jaundice. Hydroxychloroquine does not cause this reaction and appears to have partial beneficial effects in porphyria. In clinical trials of chloroquine for COVID-19 prevention and treatment, there were no reports of hepatotoxicity, and rates of serum enzyme elevations during chloroquine treatment were low and similar to those in patients receiving placebo or standard of care.", "Markup": [ { "Start": 36, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 216, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 339, "Length": 18, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Hydroxychloroquine", "Type": "PubChem Internal Link", "Extra": "CID-3652" }, { "Start": 470, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 606, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] }, { "String": "Likelihood score: D (possible rare cause of clinically apparent liver injury)." } ] } } ] }, { "TOCHeading": "Drug Induced Liver Injury", "Description": "Severity grade was defined by the description of drug-induced liver injury severity in the drug labeling, ranging from 1 to 8 with 1 (steatosis) as lowest and 8 (fatal hepatotoxicity) as highest grade. More detail could be found in Chen et al. Drug Discovery Today 2016 (PMID:21624500 DOI:10.1016/j.drudis.2011.05.007).", "URL": "https://www.fda.gov/science-research/liver-toxicity-knowledge-base-ltkb/drug-induced-liver-injury-rank-dilirank-dataset", "DisplayControls": { "CreateTable": { "FromInformationIn": "ThisSection", "NumberOfColumns": 2, "ColumnContents": [ "Name", "Value" ] } }, "Information": [ { "ReferenceNumber": 9, "Name": "Compound", "Value": { "StringWithMarkup": [ { "String": "chloroquine" } ] } }, { "ReferenceNumber": 9, "Name": "DILI Annotation", "Value": { "StringWithMarkup": [ { "String": "Less-DILI-Concern" } ] } }, { "ReferenceNumber": 9, "Name": "Severity Grade", "Value": { "StringWithMarkup": [ { "String": "3" } ] } }, { "ReferenceNumber": 9, "Name": "Label Section", "Value": { "StringWithMarkup": [ { "String": "Adverse reactions" } ] } }, { "ReferenceNumber": 9, "Name": "References", "Value": { "StringWithMarkup": [ { "String": "M Chen, V Vijay, Q Shi, Z Liu, H Fang, W Tong. FDA-Approved Drug Labeling for the Study of Drug-Induced Liver Injury, Drug Discovery Today, 16(15-16):697-703, 2011. PMID:21624500 DOI:10.1016/j.drudis.2011.05.007", "Markup": [ { "Start": 165, "Length": 13, "URL": "https://pubmed.ncbi.nlm.nih.gov/21624500/" }, { "Start": 179, "Length": 32, "URL": "https://doi.org/10.1016/j.drudis.2011.05.007" } ] }, { "String": "M Chen, A Suzuki, S Thakkar, K Yu, C Hu, W Tong. DILIrank: the largest reference drug list ranked by the risk for developing drug-induced liver injury in humans. Drug Discov Today 2016, 21(4): 648-653. PMID:26948801 DOI:10.1016/j.drudis.2016.02.015", "Markup": [ { "Start": 202, "Length": 13, "URL": "https://pubmed.ncbi.nlm.nih.gov/26948801/" }, { "Start": 216, "Length": 32, "URL": "https://doi.org/10.1016/j.drudis.2016.02.015" } ] } ] } } ] }, { "TOCHeading": "Evidence for Carcinogenicity", "Description": "Evidence for substance or agent that can cause cancer", "Information": [ { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "IARC. Monographs on the Evaluation of the Carcinogenic Risk of Chemicals to Humans. Geneva: World Health Organization, International Agency for Research on Cancer, 1972-PRESENT. (Multivolume work). Available at: https://monographs.iarc.fr/ENG/Classification/index.php, p. S7 60 (1987)" ], "Value": { "StringWithMarkup": [ { "String": "No data are available in humans. Inadequate evidence of carcinogenicity in animals. OVERALL EVALUATION: Group 3: The agent is not classifiable as to its carcinogenicity to humans." } ] } } ] }, { "TOCHeading": "Carcinogen Classification", "Description": "This section provide the International Agency for Research on Cancer (IARC) Carcinogenic Classification and related monograph links.", "URL": "https://monographs.iarc.who.int/agents-classified-by-the-iarc/", "DisplayControls": { "CreateTable": { "FromInformationIn": "ThisSection", "NumberOfColumns": 2, "ColumnContents": [ "Name", "Value" ] } }, "Information": [ { "ReferenceNumber": 21, "Name": "IARC Carcinogenic Agent", "Value": { "StringWithMarkup": [ { "String": "Chloroquine" } ] } }, { "ReferenceNumber": 21, "Name": "IARC Carcinogenic Classes", "Reference": [ "https://monographs.iarc.who.int/agents-classified-by-the-iarc/" ], "Value": { "StringWithMarkup": [ { "String": "Group 3: Not classifiable as to its carcinogenicity to humans" } ] } }, { "ReferenceNumber": 21, "Name": "IARC Monographs", "Value": { "StringWithMarkup": [ { "String": "Volume 13: (1977) Some Miscellaneous Pharmaceutical Substances", "Markup": [ { "Start": 0, "Length": 9, "URL": "http://publications.iarc.fr/31" } ] }, { "String": "Volume Sup 7: Overall Evaluations of Carcinogenicity: An Updating of IARC Monographs Volumes 1 to 42, 1987; 440 pages; ISBN 92-832-1411-0 (out of print)", "Markup": [ { "Start": 0, "Length": 12, "URL": "http://publications.iarc.fr/139" } ] } ] } } ] }, { "TOCHeading": "Acute Effects", "Description": "The results from acute animal tests and/or acute human studies are presented in this section. Acute animal studies consist of LD50 and LC50 tests, which present the median lethal dose (or concentration) to the animals. Acute human studies usually consist of case reports from accidental poisonings or industrial accidents. These case reports often help to define the levels at which acute toxic effects are seen in humans.", "Information": [ { "ReferenceNumber": 5, "Value": { "ExternalTableName": "chemidplus", "ExternalTableNumRows": 19 } } ] }, { "TOCHeading": "Interactions", "Description": "Interactions", "Information": [ { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "Thomson.Micromedex. Drug Information for the Health Care Professional. 25th ed. Volume 1. Plus Updates. Content Reviewed by the United States Pharmacopeial Convention, Inc. Greenwood Village, CO. 2005., p. 838" ], "Value": { "StringWithMarkup": [ { "String": "Concurrent use of penicillamine /with chloroquine/ may increase penicillamine plasma concentrations, increasing the potential for serious hematologic and/or renal adverse reactions as well as the possibility of severe skin reactions.", "Markup": [ { "Start": 18, "Length": 13, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/penicillamine", "Type": "PubChem Internal Link", "Extra": "CID-5852" }, { "Start": 38, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 64, "Length": 13, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/penicillamine", "Type": "PubChem Internal Link", "Extra": "CID-5852" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "Thomson.Micromedex. Drug Information for the Health Care Professional. 25th ed. Volume 1. Plus Updates. Content Reviewed by the United States Pharmacopeial Convention, Inc. Greenwood Village, CO. 2005., p. 838" ], "Value": { "StringWithMarkup": [ { "String": "Concurrent use /of mefloquine and chloroquine may increase the risk of seizures.", "Markup": [ { "Start": 19, "Length": 10, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/mefloquine", "Type": "PubChem Internal Link", "Extra": "CID-4046" }, { "Start": 34, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "Thomson.Micromedex. Drug Information for the Health Care Professional. 25th ed. Volume 1. Plus Updates. Content Reviewed by the United States Pharmacopeial Convention, Inc. Greenwood Village, CO. 2005., p. 838" ], "Value": { "StringWithMarkup": [ { "String": "Concurrent use of other hepatotoxic medications with chloroquine may increase the potential for hepatotoxicity and should be avoided.", "Markup": [ { "Start": 53, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "Thomson.Micromedex. Drug Information for the Health Care Professional. 25th ed. Volume 1. Plus Updates. Content Reviewed by the United States Pharmacopeial Convention, Inc. Greenwood Village, CO. 2005., p. 838" ], "Value": { "StringWithMarkup": [ { "String": "Concurrent use may cause a sudden increase in cyclosporine plasma concentrations; close monitoring of serum cyclosporine level is recommended following concurrent use of chloroquine; chloroquine should be discontinued if necessary.", "Markup": [ { "Start": 46, "Length": 12, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/cyclosporine", "Type": "PubChem Internal Link", "Extra": "CID-5280754" }, { "Start": 108, "Length": 12, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/cyclosporine", "Type": "PubChem Internal Link", "Extra": "CID-5280754" }, { "Start": 170, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 183, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Value": { "StringWithMarkup": [ { "String": "For more Interactions (Complete) data for CHLOROQUINE (16 total), please visit the HSDB record page.", "Markup": [ { "Start": 83, "Length": 16, "URL": "https://pubchem.ncbi.nlm.nih.gov/source/hsdb/3029#section=Interactions-(Complete)" }, { "Start": 42, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/CHLOROQUINE", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } } ] }, { "TOCHeading": "Antidote and Emergency Treatment", "Description": "Antidote and Emergency Treatment", "Information": [ { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "McEvoy, G.K. (ed.). American Hospital Formulary Service. AHFS Drug Information. American Society of Health-System Pharmacists, Bethesda, MD. 2006., p. 859" ], "Value": { "StringWithMarkup": [ { "String": "Treatment of overdosage of 4-aminoquinoline derivatives must be prompt, since acute toxicity with the drugs can progress rapidly, possibly leading to cardiovascular collapse and respiratory and cardiac arrest. ECG should be monitored. Because of the importance of supporting respiration, early endotracheal intubation and mechanical ventilation may be necessary. Early gastric lavage may provide some benefit in reducing absorption of the drugs, but generally should be preceded by measures to correct severe cardiovascular disturbances, if present, and by respiratory support that includes endotracheal intubation with cuff inflated and in place to prevent aspiration (since seizures may occur). IV diazepam may control seizures and other manifestations of cerebral stimulation and, possibly, may prevent or minimize other toxic effects (eg, cardiotoxicity, including ECG abnormalities and conduction disturbances) of 4-aminoquinoline derivatives. However, additional study and experience are necessary to further establish the effects of diazepam on noncerebral manifestations of toxicity with these drugs. If seizures are caused by anoxia, anoxia should be corrected with oxygen and respiratory support. Equipment and facilities for cardioversion and for insertion of a transvenous pacemaker should be readily available. Administration of IV fluids and placement of the patient in Trendelenburg's position may be useful in managing hypotension, but more aggressive therapy, including administration of vasopressors (eg, epinephrine, isoproterenol, dopamine), may be necessary, particularly if shock appears to be impending. Administration of activated charcoal by stomach tube, after lavage and within 30 min after ingestion of 4-aminoquinoline derivatives, may inhibit further intestinal absorption of the drugs; the dose of activated charcoal should be at least 5 times the estimated dose of chloroquine... ingested. Peritoneal dialysis, hemodialysis, and hemoperfusion do not appear to be useful in the management of overdosage with 4-aminoquinoline derivatives. Patients who survive the acute phase of overdosage and are asymptomatic should be closely observed for at least 48-96 hr after ingestion", "Markup": [ { "Start": 27, "Length": 16, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/4-aminoquinoline", "Type": "PubChem Internal Link", "Extra": "CID-68476" }, { "Start": 700, "Length": 8, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/diazepam", "Type": "PubChem Internal Link", "Extra": "CID-3016" }, { "Start": 919, "Length": 16, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/4-aminoquinoline", "Type": "PubChem Internal Link", "Extra": "CID-68476" }, { "Start": 1040, "Length": 8, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/diazepam", "Type": "PubChem Internal Link", "Extra": "CID-3016" }, { "Start": 1175, "Length": 6, "URL": "https://pubchem.ncbi.nlm.nih.gov/element/Oxygen", "Type": "PubChem Internal Link", "Extra": "Element-Oxygen" }, { "Start": 1523, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/epinephrine", "Type": "PubChem Internal Link", "Extra": "CID-5816" }, { "Start": 1536, "Length": 13, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/isoproterenol", "Type": "PubChem Internal Link", "Extra": "CID-3779" }, { "Start": 1551, "Length": 8, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/dopamine", "Type": "PubChem Internal Link", "Extra": "CID-681" }, { "Start": 1655, "Length": 8, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/charcoal", "Type": "PubChem Internal Link", "Extra": "CID-5462310" }, { "Start": 1731, "Length": 16, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/4-aminoquinoline", "Type": "PubChem Internal Link", "Extra": "CID-68476" }, { "Start": 1839, "Length": 8, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/charcoal", "Type": "PubChem Internal Link", "Extra": "CID-5462310" }, { "Start": 1897, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 2039, "Length": 16, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/4-aminoquinoline", "Type": "PubChem Internal Link", "Extra": "CID-68476" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "PMID:1503290", "Demaziere J et al; Ann Fr Anesth Reanim 11 (2): 164-7 (1992)" ], "Value": { "StringWithMarkup": [ { "String": "A retrospective study was carried out, over a twelve year period, of all cases of acute chloroquine poisoning where more than 2 g of chloroquine had been taken. It included 386 patients; of these, 60 who had taken drugs other than chloroquine, and 17 who had ingested less than 1 g of the drug, were excluded. The remaining 309 patients were allocated to two groups: a control group, consisting of the patients admitted between January 1973 and April 1980 (n = 146), and a diazepam group, made up of those admitted from May 1980 to December 1989 (n = 163). The patients in the latter group had had the same symptomatic treatment as those in the control group, and had been routinely given a 0.5 mg/kg bolus of diazepam on admission followed by 0.1 mg/kg/day for every 100 mg of chloroquine supposed to have been ingested. Both groups were divided into three subgroups, those patients with cardiorespiratory arrest, and those with, and those without, symptoms on admission. No statistically significant difference was found between either the control and diazepam groups or between subgroups, concerning the distribution of age, sex, amount of chloroquine supposed to have been ingested, delay in hospital admission and death rate. However, there was a higher death rate in the asymptomatic subgroup not treated with diazepam than in the diazepam group. Therefore, the routine use of diazepam for the treatment of acute chloroquine poisoning does not seem to be justified in symptomatic cases and in those with inaugural cardiac arrest.", "Markup": [ { "Start": 88, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 133, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 231, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 473, "Length": 8, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/diazepam", "Type": "PubChem Internal Link", "Extra": "CID-3016" }, { "Start": 710, "Length": 8, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/diazepam", "Type": "PubChem Internal Link", "Extra": "CID-3016" }, { "Start": 778, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 1054, "Length": 8, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/diazepam", "Type": "PubChem Internal Link", "Extra": "CID-3016" }, { "Start": 1143, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 1316, "Length": 8, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/diazepam", "Type": "PubChem Internal Link", "Extra": "CID-3016" }, { "Start": 1337, "Length": 8, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/diazepam", "Type": "PubChem Internal Link", "Extra": "CID-3016" }, { "Start": 1383, "Length": 8, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/diazepam", "Type": "PubChem Internal Link", "Extra": "CID-3016" }, { "Start": 1419, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "PMID:1503289", "Kempf J, Saissy JM; Ann Fr Anesth Reanim 11 (2): 160-3 (1992)" ], "Value": { "StringWithMarkup": [ { "String": "The effects of diazepam and the incidence of hypoxemia on the course of acute chloroquine poisoning were studied prospectively in 21 patients. Patients excluded were those who had ingested more than one drug or who had major symptoms on admission (systolic blood pressure less than 80 mmHg; QRS greater than 0.12 s; cardiac dysrhythmias, respiratory disturbances). Arterial blood gases were measured on admission (T0) and 15 min after 0.5 mg/kg of diazepam had been given (T1). Gastric lavage was carried out as soon as the results of the blood gases had been obtained, and after treatment of hypoxemia (PaO2 < 90 mmHg). An infusion of diazepam (1 mg/kg/day) was then given. Arterial blood gases were measured after 1 (T2), 6 (T3), 12 (T4) and 24 hr (T5). Hypoxemia was present on admission in four patients who had a PaO2 = 75 + or - 10 mmHg (Pa(sys) = 130 + or - 19 mmHg; blood chloroquine concn = 8.2 + or - 5.2 umol/L; kaliemia /serum potassium/ = 3.1 + or - 0.3 mmol/L; PaCO2 = 35 + or - 1 mmHg). In two patients, hypoxemia decreased after the initial dose of diazepam (T1); however, oxygen was still required by the other two at that time. Oxygen was no longer needed by any patient at T2, as all the blood gas values had returned to normal.", "Markup": [ { "Start": 15, "Length": 8, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/diazepam", "Type": "PubChem Internal Link", "Extra": "CID-3016" }, { "Start": 78, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 448, "Length": 8, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/diazepam", "Type": "PubChem Internal Link", "Extra": "CID-3016" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "Olson, K.R. (Ed.); Poisoning & Drug Overdose. 4th ed. Lange Medical Books/McGraw-Hill. New York, N.Y. 2004., p. 166" ], "Value": { "StringWithMarkup": [ { "String": "Emergency and supportive measures: Maintain an open airway and assist ventilation if necessary. Treat seizures, coma, hypotension, and methemoglobinemia if they occur. Treat massive hemolysis with blood transfusions if needed, and prevent hemoglobin deposition in the kidney tubules by alkaline diuresis ... continuously monitor the ECG for at least 6 to 8 hr." } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Value": { "StringWithMarkup": [ { "String": "For more Antidote and Emergency Treatment (Complete) data for CHLOROQUINE (8 total), please visit the HSDB record page.", "Markup": [ { "Start": 102, "Length": 16, "URL": "https://pubchem.ncbi.nlm.nih.gov/source/hsdb/3029#section=Antidote-and-Emergency-Treatment-(Complete)" }, { "Start": 62, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/CHLOROQUINE", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } } ] }, { "TOCHeading": "Human Toxicity Excerpts", "Description": "Human Toxicity Excerpts", "Information": [ { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "PMID:2051527", "elZaki K et al; J Trop Med Hyg 94 (3): 206-9 (1991)" ], "Value": { "StringWithMarkup": [ { "String": "/HUMAN EXPOSURE STUDIES/ This prospective study contains clinical and experimental parts. In the clinical study, 125 patients given im chloroquine for malaria were followed for 2 months in order to detect local injection site complications. Adequate local antiseptic conditions were ensured before giving the injection. Twenty three patients (18.4%) had minimal local reaction in the form of redness, induration and/or a lump. No pyogenic abscess was noted in contrast to a previous report.", "Markup": [ { "Start": 135, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "Olson, K.R. (Ed.); Poisoning & Drug Overdose. 4th ed. Lange Medical Books/McGraw-Hill. New York, N.Y. 2004., p. 166" ], "Value": { "StringWithMarkup": [ { "String": "/HUMAN EXPOSURE STUDIES/ Cardiotoxicity may be seen with serum levels of 1 mg/L (1000 ng/mL); serum levels reported in fetal cases have ranged from 1 to 210 mg/L (average, 60 mg/L)." } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "PMID:3306266", "Jaeger A et al; Med Toxicol Adverse Drug Exp 2 (4): 242-73 (1987)" ], "Value": { "StringWithMarkup": [ { "String": "/SIGNS AND SYMPTOMS/ The toxicities of antimalarial drugs vary because of the differences in the chemical structures of these compounds. Quinine, the oldest antimalarial, has been used for 300 yr. Of the 200 to 300 compounds synthesized since the first synthetic antimalarial, primaquine in 1926, 15 to 20 are currently used for malaria treatment, most of which are quinoline derivatives. Quinoline derivatives, particularly quinine and chloroquine, are highly toxic in overdose. The toxic effects are related to their quinidine-like actions on the heart and include circulatory arrest, cardiogenic shock, conduction disturbances and ventricular arrhythmias. Additional clinical features are obnubilation, coma, convulsions, respiratory depression. Blindness is a frequent complication in quinine overdose. Hypokalaemia is consistently present, although apparently self-correcting, in severe chloroquine poisoning and is a good index of severity. Recent toxicokinetic studies of quinine and chloroquine showed good correlations between dose ingested, serum concn and clinical features, and confirmed the inefficacy of hemodialysis, hemoperfusion and peritoneal dialysis for enhancing drug removal. The other quinoline derivatives appear to be less toxic. Amodiaquine may induce side effects such as gastrointestinal symptoms, agranulocytosis and hepatitis. The main feature of primaquine overdose is methemoglobinemia. No cases of mefloquine and piperaquine overdose have been reported. Overdose with quinacrine, an acridine derivative, may result in nausea, vomiting, confusion, convulsion and acute psychosis. The dehydrofolate reductase inhibitors used in malaria treatment are sulfadoxine, dapsone, proguanil (chloroguanide), trimethoprim and pyrimethamine. Most of these drugs are given in combination. Proguanil is one of the safest antimalarials. Convulsion, coma and blindness have been reported in pyrimethamine overdose. Sulfadoxine can induce Lyell and Stevens-Johnson syndromes. The main feature of dapsone poisoning is severe methemoglobinemia which is related to dapsone and to its metabolites. Recent toxicokinetic studies confirmed the efficacy of oral activated charcoal, hemodialysis and hemoperfusion in enhancing removal of dapsone and its metabolites. No overdose has been reported with artemesinine, a new antimalarial tested in the People's Republic of China. The general management of antimalarial overdose include gastric lavage and symptomatic treatment.", "Markup": [ { "Start": 137, "Length": 7, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Quinine", "Type": "PubChem Internal Link", "Extra": "CID-3034034" }, { "Start": 277, "Length": 10, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/primaquine", "Type": "PubChem Internal Link", "Extra": "CID-4908" }, { "Start": 366, "Length": 9, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/quinoline", "Type": "PubChem Internal Link", "Extra": "CID-7047" }, { "Start": 389, "Length": 9, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Quinoline", "Type": "PubChem Internal Link", "Extra": "CID-7047" }, { "Start": 425, "Length": 7, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/quinine", "Type": "PubChem Internal Link", "Extra": "CID-3034034" }, { "Start": 437, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 519, "Length": 9, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/quinidine", "Type": "PubChem Internal Link", "Extra": "CID-441074" }, { "Start": 789, "Length": 7, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/quinine", "Type": "PubChem Internal Link", "Extra": "CID-3034034" }, { "Start": 892, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 979, "Length": 7, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/quinine", "Type": "PubChem Internal Link", "Extra": "CID-3034034" }, { "Start": 991, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 1208, "Length": 9, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/quinoline", "Type": "PubChem Internal Link", "Extra": "CID-7047" }, { "Start": 1255, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Amodiaquine", "Type": "PubChem Internal Link", "Extra": "CID-2165" }, { "Start": 1377, "Length": 10, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/primaquine", "Type": "PubChem Internal Link", "Extra": "CID-4908" }, { "Start": 1431, "Length": 10, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/mefloquine", "Type": "PubChem Internal Link", "Extra": "CID-4046" }, { "Start": 1446, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/piperaquine", "Type": "PubChem Internal Link", "Extra": "CID-122262" }, { "Start": 1501, "Length": 10, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/quinacrine", "Type": "PubChem Internal Link", "Extra": "CID-237" }, { "Start": 1516, "Length": 8, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/acridine", "Type": "PubChem Internal Link", "Extra": "CID-9215" }, { "Start": 1681, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/sulfadoxine", "Type": "PubChem Internal Link", "Extra": "CID-17134" }, { "Start": 1694, "Length": 7, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/dapsone", "Type": "PubChem Internal Link", "Extra": "CID-2955" }, { "Start": 1703, "Length": 9, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/proguanil", "Type": "PubChem Internal Link", "Extra": "CID-6178111" }, { "Start": 1714, "Length": 13, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroguanide", "Type": "PubChem Internal Link", "Extra": "CID-6178111" }, { "Start": 1730, "Length": 12, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/trimethoprim", "Type": "PubChem Internal Link", "Extra": "CID-5578" }, { "Start": 1747, "Length": 13, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/pyrimethamine", "Type": "PubChem Internal Link", "Extra": "CID-4993" }, { "Start": 1808, "Length": 9, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Proguanil", "Type": "PubChem Internal Link", "Extra": "CID-6178111" }, { "Start": 1907, "Length": 13, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/pyrimethamine", "Type": "PubChem Internal Link", "Extra": "CID-4993" }, { "Start": 1931, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Sulfadoxine", "Type": "PubChem Internal Link", "Extra": "CID-17134" }, { "Start": 2011, "Length": 7, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/dapsone", "Type": "PubChem Internal Link", "Extra": "CID-2955" }, { "Start": 2077, "Length": 7, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/dapsone", "Type": "PubChem Internal Link", "Extra": "CID-2955" }, { "Start": 2179, "Length": 8, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/charcoal", "Type": "PubChem Internal Link", "Extra": "CID-5462310" }, { "Start": 2244, "Length": 7, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/dapsone", "Type": "PubChem Internal Link", "Extra": "CID-2955" }, { "Start": 2308, "Length": 12, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/artemisinine", "Type": "PubChem Internal Link", "Extra": "CID-9838675" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "Haddad, L.M., Clinical Management of Poisoning and Drug Overdose. 2nd ed. Philadelphia, PA: W.B. Saunders Co., 1990., p. 381" ], "Value": { "StringWithMarkup": [ { "String": "/SIGNS AND SYMPTOMS/ In the treatment of collagen vascular diseases ... retinopathy has become recognized as a significant potential problem. ... The earliest ophthalmoscopic sign of ... retinopathy is loss of the foveal reflex. This is followed by pigmentary changes in the macula, typically progressing to a pigmented ring surrounding the fovea (\"bull's eye lesion\") and sometimes accompanied by pigment flecks in the midperiphery. ... The most common complaint is difficulty in reading, which with further questioning can be usually related to paracentral scotomas. Light flashes and streaks and other entopic phenomena may also be present." } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Value": { "StringWithMarkup": [ { "String": "For more Human Toxicity Excerpts (Complete) data for CHLOROQUINE (27 total), please visit the HSDB record page.", "Markup": [ { "Start": 94, "Length": 16, "URL": "https://pubchem.ncbi.nlm.nih.gov/source/hsdb/3029#section=Human-Toxicity-Excerpts-(Complete)" }, { "Start": 53, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/CHLOROQUINE", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } } ] }, { "TOCHeading": "Non-Human Toxicity Excerpts", "Description": "Non-Human Toxicity Excerpts", "Information": [ { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "PMID:8411306", "Musabayane CT et al; J Trop Med Hyg 96 (5): 305-10 (1993)" ], "Value": { "StringWithMarkup": [ { "String": "/LABORATORY ANIMALS: Acute Exposure/ The effect of a 2 hr iv chloroquine infusion (0.015, 0.030 and 1.25 ug/min) on renal fluid and electrolyte handling was investigated in the saline infused, Inactin anaesthetized rat. Blood pressure and glomerular filtration rate were not affected by chloroquine administration, remaining around 128 mmHg and 2.4 mL/min, respectively throughout the 5 hr post-equilibration period. Chloroquine produced an increase in Na+ and Cl- excretion without affecting the urine flow. By 1 hr after the start of treatment (0.03 ug chloroquine/min) the Na+ excretion rate had increased to 14.5 + or - 2.1 umol/min (n = 6), and was significantly (P < 0.01) greater than in control animals (8.6 + or - 1.0 umol/min) at the corresponding time. Parallel but lesser increases in Cl- excretion rates were also observed. The plasma aldosterone and corticosterone levels following either 10, 30 or 120 min infusion of chloroquine at 0.03 ug/min did not differ statistically from each other or from control values. It is concluded that acute chloroquine administration induces an increase in Na+ excretion. The mechanism of this natriuresis cannot be established from the present study, but is likely to involve altered tubular handling of Na+.", "Markup": [ { "Start": 61, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 193, "Length": 7, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Inactin", "Type": "PubChem Internal Link", "Extra": "CID-15086288" }, { "Start": 287, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 417, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 555, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "IARC. Monographs on the Evaluation of the Carcinogenic Risk of Chemicals to Humans. Geneva: World Health Organization, International Agency for Research on Cancer, 1972-PRESENT. (Multivolume work). Available at: https://monographs.iarc.fr/ENG/Classification/index.php, p. V13 51 (1976)" ], "Value": { "StringWithMarkup": [ { "String": "/LABORATORY ANIMALS: Chronic Exposure or Carcinogenicity/ Groups of 10 male and 10 female 21-day-old Osborne-Mendel rats were given 0 (control), 100, 200, 400, 800 or 1000 mg/kg of diet chloroquine for up to 2 years. Inhibition of growth was severe at the 800 and 1000 mg/kg levels but temporary at 400 mg/kg. The toxicity of chloroquine became progressively more severe with increasing dosage, and 100% mortality was observed at the two highest dose levels at 35 and 25 weeks, respectively. No tumours were reported in 86 treated rats or in 15 control rats examined microscopically", "Markup": [ { "Start": 186, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 326, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "IARC. Monographs on the Evaluation of the Carcinogenic Risk of Chemicals to Humans. Geneva: World Health Organization, International Agency for Research on Cancer, 1972-PRESENT. (Multivolume work). Available at: https://monographs.iarc.fr/ENG/Classification/index.php, p. V13 51 (1976)" ], "Value": { "StringWithMarkup": [ { "String": "/LABORATORY ANIMALS: Chronic Exposure or Carcinogenicity/ In two year ... study in rats fed diets containing from 100-1000 mg ... /kg of diet/ ... myocardial and voluntary muscle damage, centrilobular necrosis of liver and testicular atrophy were ... observed." } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "PMID:1437656", "el-Mofty MM et al; Nutr Cancer 18 (2): 191-8 (1992)" ], "Value": { "StringWithMarkup": [ { "String": "/LABORATORY ANIMALS: Chronic Exposure or Carcinogenicity/ Feeding Egyptian toads (Bufo regularis) with chloroquine and primaquine separately induced tumor formation in 14% and 19% of the animals, respectively. When chloroquine and primaquine were given in combination, the tumor incidence increased to 23.5%. Chloroquine feeding resulted in tumors located in the liver (lymphosarcomas) and primaquine in tumors in the kidney (histiocytic sarcomas). Toads fed chloroquine plus primaquine developed tumors in the liver, kidney, lung, and urinary bladder, and all the tumors were diagnosed as histiocytic sarcomas. It is speculated that one or more metabolites of chloroquine and primaquine (eg, quinone) may be responsible for tumor induction in the toads.", "Markup": [ { "Start": 103, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 119, "Length": 10, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/primaquine", "Type": "PubChem Internal Link", "Extra": "CID-4908" }, { "Start": 215, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 231, "Length": 10, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/primaquine", "Type": "PubChem Internal Link", "Extra": "CID-4908" }, { "Start": 309, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 390, "Length": 10, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/primaquine", "Type": "PubChem Internal Link", "Extra": "CID-4908" }, { "Start": 459, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 476, "Length": 10, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/primaquine", "Type": "PubChem Internal Link", "Extra": "CID-4908" }, { "Start": 661, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 677, "Length": 10, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/primaquine", "Type": "PubChem Internal Link", "Extra": "CID-4908" } ] } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Value": { "StringWithMarkup": [ { "String": "For more Non-Human Toxicity Excerpts (Complete) data for CHLOROQUINE (11 total), please visit the HSDB record page.", "Markup": [ { "Start": 98, "Length": 16, "URL": "https://pubchem.ncbi.nlm.nih.gov/source/hsdb/3029#section=Non-Human-Toxicity-Excerpts-(Complete)" }, { "Start": 57, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/CHLOROQUINE", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } } ] }, { "TOCHeading": "Human Toxicity Values", "Description": "Human Toxicity Values", "DisplayControls": { "ShowAtMost": 5 }, "Information": [ { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "McEvoy, G.K. (ed.). American Hospital Formulary Service. AHFS Drug Information. American Society of Health-System Pharmacists, Bethesda, MD. 2006., p. 859" ], "Value": { "StringWithMarkup": [ { "String": "... Reports of suicides have indicated that the margin of safety in adults is also small. Without prompt effective therapy, acute ingestion of 5 g or more of chloroquine in adults has usually been fatal, although death has occurred with smaller doses. Fatalities have been reported following the accidental ingestion of relatively small doses of chloroquine (e.g., 750 mg or 1 g of chloroquine phosphate in a 3-year-old child).", "Markup": [ { "Start": 158, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 346, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 382, "Length": 21, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine%20phosphate", "Type": "PubChem Internal Link", "Extra": "CID-64927" } ] } ] } } ] }, { "TOCHeading": "Non-Human Toxicity Values", "Description": "Non-Human Toxicity Values", "DisplayControls": { "ShowAtMost": 5 }, "Information": [ { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "Verschueren, K. Handbook of Environmental Data on Organic Chemicals. Volumes 1-2. 4th ed. John Wiley & Sons. New York, NY. 2001, p. 551" ], "Value": { "StringWithMarkup": [ { "String": "LD50 Rat oral 330 mg/kg" } ] } }, { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "Verschueren, K. Handbook of Environmental Data on Organic Chemicals. Volumes 1-2. 4th ed. John Wiley & Sons. New York, NY. 2001, p. 551" ], "Value": { "StringWithMarkup": [ { "String": "LD50 Mouse oral 311 mg/kg" } ] } } ] }, { "TOCHeading": "Protein Binding", "Description": "Protein Binding", "Information": [ { "ReferenceNumber": 10, "Value": { "StringWithMarkup": [ { "String": "Chloroquine is 46-74% bound to plasma proteins. (-)-chloroquine binds more strongly to alpha-1-acid glycoprotein and (+)-chloroquine binds more strongly to serum albumin.", "Markup": [ { "Start": 0, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 48, "Length": 15, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/%28-%29-chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-444810" }, { "Start": 117, "Length": 15, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/%28%2B%29-chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-639540" } ] } ] } } ] } ] }, { "TOCHeading": "Ecological Information", "Description": "This section provides eco-related toxicity information.", "Section": [ { "TOCHeading": "Environmental Water Concentrations", "Description": "Environmental Water Concentrations", "Information": [ { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "(1) Heberer T; Tox Lett 131: 5-17 (2002) (2) Koplin DW et al; Environ Sci Toxicol 36: 1202-211 (2002)" ], "Value": { "StringWithMarkup": [ { "String": "While data specific to chloroquine were not available(SRC, 2005), the literature suggests that some pharmaceutically active compounds originating from human and veterinary therapy are not eliminated completely in municipal sewage treatment plants and are therefore discharged into receiving waters(1). Wastewater treatment processes often were not designed to remove them from the effluent(2). Another concern is that selected organic waste compounds may be degrading to new and more persistent compounds that may be released instead of or in addition to the parent compound(2). Studies have indicated that several polar pharmaceutically active compounds can leach through subsoils(1).", "Markup": [ { "Start": 23, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } } ] }, { "TOCHeading": "Milk Concentrations", "Description": "Milk Concentrations", "Information": [ { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "McEvoy, G.K. (ed.). American Hospital Formulary Service. AHFS Drug Information. American Society of Health-System Pharmacists, Bethesda, MD. 2006., p. 860" ], "Value": { "StringWithMarkup": [ { "String": "EXPERIMENTAL: Small amounts of chloroquine and its major metabolite, desethylchloroquine, are distributed into milk. Following oral administration of a single 300 or 600 mg dose of chloroquine, peak concentration of the drug in milk range from 1.7-7.5 ug/mL and generally are greater than concurrent plasma concentrations.", "Markup": [ { "Start": 31, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 69, "Length": 19, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/desethylchloroquine", "Type": "PubChem Internal Link", "Extra": "CID-95478" }, { "Start": 181, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } } ] }, { "TOCHeading": "Probable Routes of Human Exposure", "Description": "Probable Routes of Human Exposure", "Information": [ { "ReferenceNumber": 18, "Description": "PEER REVIEWED", "Reference": [ "Grant, W.M. Toxicology of the Eye. 3rd ed. Springfield, IL: Charles C. Thomas Publisher, 1986., p. 216" ], "Value": { "StringWithMarkup": [ { "String": "CORNEAL DEPOSITS HAVE ... BEEN DESCRIBED AS INDUSTRIAL COMPLICATION IN WORKERS MFR CHLOROQUINE ... . APPARENTLY DEPOSITS ARE SAME AS THOSE PRODUCED BY ORAL ADMIN. ... INDUSTRIALLY MATERIAL MAY HAVE REACHED CORNEA DIRECTLY IN FORM OF DUST, BUT THIS HAS NOT BEEN ESTABLISHED.", "Markup": [ { "Start": 83, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/CHLOROQUINE", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } } ] } ] } ] }, { "TOCHeading": "Associated Disorders and Diseases", "Description": "Disease information available for this compound", "DisplayControls": { "CreateTable": { "FromInformationIn": "Subsections", "NumberOfColumns": 2, "ColumnHeadings": [ "Disease", "References" ], "ColumnContents": [ "Name", "Value" ] } }, "Information": [ { "ReferenceNumber": 7, "Value": { "ExternalTableName": "ctd_chemical_disease" } } ] }, { "TOCHeading": "Literature", "Description": "Literature citation references mainly refers to regular publications such as journal articles, etc.", "URL": "https://pubchemdocs.ncbi.nlm.nih.gov/literature", "Section": [ { "TOCHeading": "Coronavirus Studies", "Description": "Literature references aggregated from multiple sources including PubMed and ClinicalTrials.gov. For additional clinical studies, see clinical trials section.", "URL": "https://pubchemdocs.ncbi.nlm.nih.gov/covid-19", "Information": [ { "ReferenceNumber": 55, "Value": { "ExternalTableName": "literature_coronavirus", "ExternalTableNumRows": 1152 } } ] }, { "TOCHeading": "NLM Curated PubMed Citations", "Description": "The \"NLM Curated PubMed Citations\" section links to all PubMed records that are tagged with the same MeSH term that has been associated with a particular compound.", "Information": [ { "ReferenceNumber": 69, "URL": "https://www.ncbi.nlm.nih.gov/sites/entrez?LinkName=pccompound_pubmed_mesh&db=pccompound&cmd=Link&from_uid=2719", "Value": { "Boolean": [ true ] } } ] }, { "TOCHeading": "Springer Nature References", "Description": "Literature references related to scientific contents from Springer Nature journals and books. These references have been ranked automatically by an algorithm which calculates the relevance for each substance in a Springer Nature document. It is based on: 1. the TF-IDF, adapted to chemical structures, 2. location information in the text (e.g. title, abstract, keywords), and 3. the document size. Springer Nature aims to provide only high qualitative and relevant content but references of lower relevance aren't withheld as they might contain also very useful information", "URL": "https://group.springernature.com/gp/group/aboutus", "Information": [ { "ReferenceNumber": 60, "Name": "Springer Nature References", "Value": { "ExternalTableName": "springernature" } }, { "ReferenceNumber": 61, "Name": "Springer Nature References", "Value": { "ExternalTableName": "springernature" } } ] }, { "TOCHeading": "Thieme References", "Description": "Literature references related to scientific contents from Thieme Chemistry journals and books. The Thieme Chemistry content within this section is provided under a CC-BY-NC-ND 4.0 license (https://creativecommons.org/licenses/by-nc-nd/4.0/), unless otherwise stated.", "URL": "https://www.thieme.de/en/thieme-chemistry/home-51399.htm", "Information": [ { "ReferenceNumber": 62, "Name": "Thieme References", "Value": { "ExternalTableName": "ThiemeChemistry" } } ] }, { "TOCHeading": "Wiley References", "Description": "Literature references related to scientific contents from Wiley journals and books.", "URL": "https://onlinelibrary.wiley.com/", "Information": [ { "ReferenceNumber": 67, "Value": { "ExternalTableName": "wiley" } } ] }, { "TOCHeading": "Depositor Provided PubMed Citations", "Description": "This section displays a concatenated list of all PubMed records that have been cited by the depositors of all PubChem Substance records that contain the same chemical structure as the compound.", "Information": [ { "ReferenceNumber": 69, "Name": "Depositor Provided PubMed Citations", "URL": "https://www.ncbi.nlm.nih.gov/sites/entrez?LinkName=pccompound_pubmed&db=pccompound&cmd=Link&from_uid=2719", "Value": { "ExternalTableName": "collection=pubmed&pmidsrcs=xref", "ExternalTableNumRows": 580 } } ] }, { "TOCHeading": "Synthesis References", "Description": "References that are related to the preparation and synthesis reaction.", "Information": [ { "ReferenceNumber": 10, "Value": { "StringWithMarkup": [ { "String": "Andersag, H., Breitner, S.and Jung, H.; U S . Patent 2,233,970; March 4,1941; assigned to Winthrop Chemical Company, Inc." } ] } } ] }, { "TOCHeading": "General References", "Description": "General References", "DisplayControls": { "ListType": "Numbered" }, "Information": [ { "ReferenceNumber": 10, "Value": { "StringWithMarkup": [ { "String": "Li C, Zhu X, Ji X, Quanquin N, Deng YQ, Tian M, Aliyari R, Zuo X, Yuan L, Afridi SK, Li XF, Jung JU, Nielsen-Saines K, Qin FX, Qin CF, Xu Z, Cheng G: Chloroquine, a FDA-approved Drug, Prevents Zika Virus Infection and its Associated Congenital Microcephaly in Mice. EBioMedicine. 2017 Oct;24:189-194. doi: 10.1016/j.ebiom.2017.09.034. Epub 2017 Sep 28. [PMID:29033372]", "Markup": [ { "Start": 354, "Length": 13, "URL": "https://www.ncbi.nlm.nih.gov/pubmed/29033372" }, { "Start": 150, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] }, { "String": "Shiryaev SA, Mesci P, Pinto A, Fernandes I, Sheets N, Shresta S, Farhy C, Huang CT, Strongin AY, Muotri AR, Terskikh AV: Repurposing of the anti-malaria drug chloroquine for Zika Virus treatment and prophylaxis. Sci Rep. 2017 Nov 17;7(1):15771. doi: 10.1038/s41598-017-15467-6. [PMID:29150641]", "Markup": [ { "Start": 279, "Length": 13, "URL": "https://www.ncbi.nlm.nih.gov/pubmed/29150641" }, { "Start": 158, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] }, { "String": "Gao J, Tian Z, Yang X: Breakthrough: Chloroquine phosphate has shown apparent efficacy in treatment of COVID-19 associated pneumonia in clinical studies. Biosci Trends. 2020 Feb 19. doi: 10.5582/bst.2020.01047. [PMID:32074550]", "Markup": [ { "Start": 212, "Length": 13, "URL": "https://www.ncbi.nlm.nih.gov/pubmed/32074550" }, { "Start": 37, "Length": 21, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine%20phosphate", "Type": "PubChem Internal Link", "Extra": "CID-64927" } ] }, { "String": "Authors unspecified: Chloroquine . [PMID:31643549]", "Markup": [ { "Start": 36, "Length": 13, "URL": "https://www.ncbi.nlm.nih.gov/pubmed/31643549" }, { "Start": 21, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] }, { "String": "Kim KA, Park JY, Lee JS, Lim S: Cytochrome P450 2C8 and CYP3A4/5 are involved in chloroquine metabolism in human liver microsomes. Arch Pharm Res. 2003 Aug;26(8):631-7. [PMID:12967198]", "Markup": [ { "Start": 170, "Length": 13, "URL": "https://www.ncbi.nlm.nih.gov/pubmed/12967198" }, { "Start": 81, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] }, { "String": "Kaewkhao K, Chotivanich K, Winterberg M, Day NP, Tarning J, Blessborn D: High sensitivity methods to quantify chloroquine and its metabolite in human blood samples using LC-MS/MS. Bioanalysis. 2019 Mar;11(5):333-347. doi: 10.4155/bio-2018-0202. Epub 2019 Mar 15. [PMID:30873854]", "Markup": [ { "Start": 264, "Length": 13, "URL": "https://www.ncbi.nlm.nih.gov/pubmed/30873854" }, { "Start": 110, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] }, { "String": "Projean D, Baune B, Farinotti R, Flinois JP, Beaune P, Taburet AM, Ducharme J: In vitro metabolism of chloroquine: identification of CYP2C8, CYP3A4, and CYP2D6 as the main isoforms catalyzing N-desethylchloroquine formation. Drug Metab Dispos. 2003 Jun;31(6):748-54. [PMID:12756207]", "Markup": [ { "Start": 268, "Length": 13, "URL": "https://www.ncbi.nlm.nih.gov/pubmed/12756207" }, { "Start": 102, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 194, "Length": 19, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/desethylchloroquine", "Type": "PubChem Internal Link", "Extra": "CID-95478" } ] }, { "String": "Ofori-Adjei D, Ericsson O, Lindstrom B, Sjoqvist F: Protein binding of chloroquine enantiomers and desethylchloroquine. Br J Clin Pharmacol. 1986 Sep;22(3):356-8. doi: 10.1111/j.1365-2125.1986.tb02900.x. [PMID:3768249]", "Markup": [ { "Start": 205, "Length": 12, "URL": "https://www.ncbi.nlm.nih.gov/pubmed/3768249" }, { "Start": 71, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 99, "Length": 19, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/desethylchloroquine", "Type": "PubChem Internal Link", "Extra": "CID-95478" } ] }, { "String": "Walker O, Birkett DJ, Alvan G, Gustafsson LL, Sjoqvist F: Characterization of chloroquine plasma protein binding in man. Br J Clin Pharmacol. 1983 Mar;15(3):375-7. doi: 10.1111/j.1365-2125.1983.tb01513.x. [PMID:6849768]", "Markup": [ { "Start": 206, "Length": 12, "URL": "https://www.ncbi.nlm.nih.gov/pubmed/6849768" }, { "Start": 78, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] }, { "String": "Ducharme J, Farinotti R: Clinical pharmacokinetics and metabolism of chloroquine. Focus on recent advancements. Clin Pharmacokinet. 1996 Oct;31(4):257-74. doi: 10.2165/00003088-199631040-00003. [PMID:8896943]", "Markup": [ { "Start": 195, "Length": 12, "URL": "https://www.ncbi.nlm.nih.gov/pubmed/8896943" }, { "Start": 69, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] }, { "String": "Coronado LM, Nadovich CT, Spadafora C: Malarial hemozoin: from target to tool. Biochim Biophys Acta. 2014 Jun;1840(6):2032-41. doi: 10.1016/j.bbagen.2014.02.009. Epub 2014 Feb 17. [PMID:24556123]", "Markup": [ { "Start": 181, "Length": 13, "URL": "https://www.ncbi.nlm.nih.gov/pubmed/24556123" } ] }, { "String": "Colson P, Rolain JM, Raoult D: Chloroquine for the 2019 novel coronavirus SARS-CoV-2. Int J Antimicrob Agents. 2020 Feb 15:105923. doi: 10.1016/j.ijantimicag.2020.105923. [PMID:32070753]", "Markup": [ { "Start": 172, "Length": 13, "URL": "https://www.ncbi.nlm.nih.gov/pubmed/32070753" }, { "Start": 31, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] }, { "String": "Wang M, Cao R, Zhang L, Yang X, Liu J, Xu M, Shi Z, Hu Z, Zhong W, Xiao G: Remdesivir and chloroquine effectively inhibit the recently emerged novel coronavirus (2019-nCoV) in vitro. Cell Res. 2020 Mar;30(3):269-271. doi: 10.1038/s41422-020-0282-0. Epub 2020 Feb 4. [PMID:32020029]", "Markup": [ { "Start": 267, "Length": 13, "URL": "https://www.ncbi.nlm.nih.gov/pubmed/32020029" }, { "Start": 75, "Length": 10, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Remdesivir", "Type": "PubChem Internal Link", "Extra": "CID-121304016" }, { "Start": 90, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] }, { "String": "Vincent MJ, Bergeron E, Benjannet S, Erickson BR, Rollin PE, Ksiazek TG, Seidah NG, Nichol ST: Chloroquine is a potent inhibitor of SARS coronavirus infection and spread. Virol J. 2005 Aug 22;2:69. doi: 10.1186/1743-422X-2-69. [PMID:16115318]", "Markup": [ { "Start": 228, "Length": 13, "URL": "https://www.ncbi.nlm.nih.gov/pubmed/16115318" }, { "Start": 95, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] }, { "String": "Chou AC, Fitch CD: Heme polymerase: modulation by chloroquine treatment of a rodent malaria. Life Sci. 1992;51(26):2073-8. doi: 10.1016/0024-3205(92)90158-l. [PMID:1474861]", "Markup": [ { "Start": 159, "Length": 12, "URL": "https://www.ncbi.nlm.nih.gov/pubmed/1474861" }, { "Start": 50, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] }, { "String": "Slater AF, Cerami A: Inhibition by chloroquine of a novel haem polymerase enzyme activity in malaria trophozoites. Nature. 1992 Jan 9;355(6356):167-9. doi: 10.1038/355167a0. [PMID:1729651]", "Markup": [ { "Start": 175, "Length": 12, "URL": "https://www.ncbi.nlm.nih.gov/pubmed/1729651" }, { "Start": 35, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] }, { "String": "Vandekerckhove S, D'hooghe M: Quinoline-based antimalarial hybrid compounds. Bioorg Med Chem. 2015 Aug 15;23(16):5098-119. doi: 10.1016/j.bmc.2014.12.018. Epub 2014 Dec 19. [PMID:25593097]", "Markup": [ { "Start": 174, "Length": 13, "URL": "https://www.ncbi.nlm.nih.gov/pubmed/25593097" }, { "Start": 30, "Length": 9, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Quinoline", "Type": "PubChem Internal Link", "Extra": "CID-7047" } ] }, { "String": "Plantone D, Koudriavtseva T: Current and Future Use of Chloroquine and Hydroxychloroquine in Infectious, Immune, Neoplastic, and Neurological Diseases: A Mini-Review. Clin Drug Investig. 2018 Aug;38(8):653-671. doi: 10.1007/s40261-018-0656-y. [PMID:29737455]", "Markup": [ { "Start": 244, "Length": 13, "URL": "https://www.ncbi.nlm.nih.gov/pubmed/29737455" }, { "Start": 55, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 71, "Length": 18, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Hydroxychloroquine", "Type": "PubChem Internal Link", "Extra": "CID-3652" } ] }, { "String": "FDA Approved Drug Products: Chloroquine Phosphate Oral Tablets", "Markup": [ { "Start": 0, "Length": 62, "URL": "https://www.accessdata.fda.gov/drugsatfda_docs/label/2009/083082s050lbl.pdf" }, { "Start": 28, "Length": 21, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine%20Phosphate", "Type": "PubChem Internal Link", "Extra": "CID-64927" } ] }, { "String": "FDA Approved Drug Products: Aralen Chloroquine Oral Tablets (Discontinued)", "Markup": [ { "Start": 0, "Length": 74, "URL": "https://www.accessdata.fda.gov/scripts/cder/daf/index.cfm?event=overview.process&ApplNo=006002" }, { "Start": 28, "Length": 6, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Aralen", "Type": "PubChem Internal Link", "Extra": "CID-2719" }, { "Start": 35, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] }, { "String": "FDA: Emergency use Authorization for Hydroxychloroquine and Chloroquine Revoked", "Markup": [ { "Start": 0, "Length": 79, "URL": "https://www.fda.gov/news-events/press-announcements/coronavirus-covid-19-update-fda-revokes-emergency-use-authorization-chloroquine-and" }, { "Start": 37, "Length": 18, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Hydroxychloroquine", "Type": "PubChem Internal Link", "Extra": "CID-3652" }, { "Start": 60, "Length": 11, "URL": "https://pubchem.ncbi.nlm.nih.gov/compound/Chloroquine", "Type": "PubChem Internal Link", "Extra": "CID-2719" } ] } ] } }, { "ReferenceNumber": 39, "URL": "http://dx.doi.org/10.1038/nchembio.87", "Value": { "StringWithMarkup": [ { "String": "Kato et al. Gene expression signatures and small molecule compounds link a protein kinase to Plasmodium falciparum motility Nature Chemical Biology, doi: 10.1038/nchembio.87, published online 27 April 2008. http://www.nature.com/naturechemicalbiology" } ] } }, { "ReferenceNumber": 40, "URL": "http://dx.doi.org/10.1038/nchembio.215", "Value": { "StringWithMarkup": [ { "String": "Yuan et al. Genetic mapping targets of differential chemical phenotypes in Plasmodium falciparum. Nature Chemical Biology, doi: 10.1038/nchembio.215, published online 06 September 2009 http://www.nature.com/naturechemicalbiology" } ] } }, { "ReferenceNumber": 41, "URL": "http://dx.doi.org/10.1038/nchembio.368", "Value": { "StringWithMarkup": [ { "String": "Sek Tong Ong et al. Endoplasmic Reticulum Ca2+ Increases Enhance Mutant Glucocerebrosidase Proteostasis. Nature Chemical Biology, doi: 10.1038/nchembio.368, published online 9 May 2010 http://www.nature.com/naturechemicalbiology" } ] } }, { "ReferenceNumber": 42, "URL": "https://www.nature.com/articles/s41589-019-0336-0/compounds/16", "Value": { "StringWithMarkup": [ { "String": "Buter et al. Mycobacterium tuberculosis releases an antacid that remodels phagosomes. Nature Chemical Biology, doi: 10.1038/s41589-019-0336-0, published online 19 August 2019" } ] } } ] }, { "TOCHeading": "Chemical Co-Occurrences in Literature", "Description": "Chemical co-occurrences in literature highlight chemicals mentioned together in scientific articles. This may suggest an important relationship exists between the two. Please note that this content is not human curated. It is generated by text-mining algorithms that can be fooled such that a co-occurrence may be happenstance or a casual mention. The lists are ordered by relevancy as indicated by count of publications and other statistics, with the most relevant mentions appearing at the top.", "URL": "https://pubchemdocs.ncbi.nlm.nih.gov/knowledge-panels", "Information": [ { "ReferenceNumber": 69, "Name": "Co-Occurrence Panel", "Value": { "StringWithMarkup": [ { "String": "ChemicalNeighbor" }, { "String": "Chemical" }, { "String": "ChemicalName_1" }, { "String": "ChemicalName_2" }, { "String": "SUMMARY_URL.cid" }, { "String": "CID" }, { "String": "CID" } ] } } ] }, { "TOCHeading": "Chemical-Gene Co-Occurrences in Literature", "Description": "Chemical-gene co-occurrences in the literature highlight chemical-'gene' pairs mentioned together in scientific articles. Note that a co-occurring 'gene' entity is organism non-specific and could refer to a gene, protein, or enzyme. This may suggest an important relationship exists between the two. Please note that this content is not human curated. It is generated by text-mining algorithms that can be fooled such that a co-occurrence may be happenstance or a casual mention. The lists are ordered by relevancy as indicated by count of publications and other statistics, with the most relevant mentions appearing at the top.", "URL": "https://pubchemdocs.ncbi.nlm.nih.gov/knowledge-panels", "Information": [ { "ReferenceNumber": 69, "Name": "Co-Occurrence Panel", "Value": { "StringWithMarkup": [ { "String": "ChemicalGeneSymbolNeighbor" }, { "String": "Gene/Protein/Enzyme" }, { "String": "ChemicalName" }, { "String": "GeneSymbolName" }, { "String": "SUMMARY_URL.genesymbol" }, { "String": "CID" }, { "String": "GeneSymbol" } ] } } ] }, { "TOCHeading": "Chemical-Disease Co-Occurrences in Literature", "Description": "Chemical-disease co-occurrences in literature highlight chemical-disease pairs mentioned together in scientific articles. This may suggest an important relationship exists between the two. Please note that this content is not human curated. It is generated by text-mining algorithms that can be fooled such that a co-occurrence may be happenstance or a casual mention. The lists are ordered by relevancy as indicated by count of publications and other statistics, with the most relevant mentions appearing at the top.", "URL": "https://pubchemdocs.ncbi.nlm.nih.gov/knowledge-panels", "Information": [ { "ReferenceNumber": 69, "Name": "Co-Occurrence Panel", "Value": { "StringWithMarkup": [ { "String": "ChemicalDiseaseNeighbor" }, { "String": "Disease" }, { "String": "ChemicalName" }, { "String": "DiseaseName" }, { "String": "https://meshb.nlm.nih.gov/record/ui?ui=" }, { "String": "CID" }, { "String": "MeSH" } ] } } ] } ] }, { "TOCHeading": "Patents", "Description": "A PubChem summary page displays Patent information when available for the given molecule.", "URL": "https://pubchemdocs.ncbi.nlm.nih.gov/patents", "DisplayControls": { "ListType": "Columns" }, "Section": [ { "TOCHeading": "Depositor-Supplied Patent Identifiers", "Description": "Patent identifiers and more information provided by depositors in form of a widget.", "Information": [ { "ReferenceNumber": 69, "Value": { "ExternalTableName": "patent" } }, { "ReferenceNumber": 69, "Value": { "StringWithMarkup": [ { "String": "Link to all deposited patent identifiers", "Markup": [ { "Start": 0, "Length": 40, "URL": "https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/2719/xrefs/PatentID/TXT" } ] } ] } } ] }, { "TOCHeading": "WIPO PATENTSCOPE", "Description": "Use the provided link to show patents associated with this chemical structure in WIPO's PATENTSCOPE system.", "URL": "https://patentscope.wipo.int/", "Information": [ { "ReferenceNumber": 91, "Value": { "StringWithMarkup": [ { "String": "Patents are available for this chemical structure:" }, { "String": "https://patentscope.wipo.int/search/en/result.jsf?inchikey=WHTVZRBIWZFKQO-UHFFFAOYSA-N", "Markup": [ { "Start": 0, "Length": 86, "URL": "https://patentscope.wipo.int/search/en/result.jsf?inchikey=WHTVZRBIWZFKQO-UHFFFAOYSA-N" } ] } ] } } ] } ] }, { "TOCHeading": "Biomolecular Interactions and Pathways", "Description": "A PubChem summary page displays biomolecular interactions and pathways information when available for the given record.", "Section": [ { "TOCHeading": "Drug-Gene Interactions", "Description": "Drug-gene interactions provided by the Drug Gene Interaction Database (DGIdb)", "Information": [ { "ReferenceNumber": 8, "Value": { "ExternalTableName": "collection=dgidb&view=concise_cid" } } ] }, { "TOCHeading": "Chemical-Gene Interactions", "Description": "Interactions between chemical and this gene", "Section": [ { "TOCHeading": "CTD Chemical-Gene Interactions", "Description": "Chemical-gene interactions provided by the Comparative Toxicogenomics Database (CTD)", "Information": [ { "ReferenceNumber": 7, "Value": { "ExternalTableName": "ctdchemicalgene" } } ] } ] }, { "TOCHeading": "DrugBank Interactions", "Description": "Drug interactions with macromolecules such as targets, enzymes, transporters, and carriers", "Information": [ { "ReferenceNumber": 10, "Value": { "ExternalTableName": "collection=drugbank&view=concise_cid" } } ] }, { "TOCHeading": "Drug-Drug Interactions", "Description": "A drug-drug interaction is a change in the action or side effects of a drug caused by concomitant administration with another drug.", "Information": [ { "ReferenceNumber": 10, "Value": { "ExternalTableName": "drugbankddi" } } ] }, { "TOCHeading": "Drug-Food Interactions", "Description": "A drug-food interaction occurs when your food and medicine interfere with one another", "DisplayControls": { "ListType": "Bulleted" }, "Information": [ { "ReferenceNumber": 10, "Value": { "StringWithMarkup": [ { "String": "Take with food. Food reduces irritation and increases bioavailability." } ] } } ] }, { "TOCHeading": "Pathways", "Description": "Pathways that include the compound as a component.", "Information": [ { "ReferenceNumber": 69, "Value": { "ExternalTableName": "collection=pathway&core=1" } } ] } ] }, { "TOCHeading": "Biological Test Results", "Description": "A PubChem substance or compound summary page displays biological test results from the PubChem BioAssay database, if/as available, for the chemical structure currently displayed. (Note that you can embed biological test results displays within your own web pages, for a PubChem Compound or Substance of interest, by using the BioActivity Widget.)", "URL": "https://pubchemdocs.ncbi.nlm.nih.gov/bioassays", "Section": [ { "TOCHeading": "BioAssay Results", "Description": "BioActivity information showed in tabular widget.", "Information": [ { "ReferenceNumber": 69, "Value": { "ExternalTableName": "bioactivity" } } ] } ] }, { "TOCHeading": "Taxonomy", "Description": "The organism(s) where the compound originated or is associated", "Information": [ { "ReferenceNumber": 23, "Reference": [ "The LOTUS Initiative for Open Natural Products Research: frozen dataset union wikidata (with metadata) | DOI:10.5281/zenodo.5794106" ], "Value": { "ExternalTableName": "collection=lotus&view=concise_cid" } } ] }, { "TOCHeading": "Classification", "Description": "Classification systems from MeSH, ChEBI, Kegg, etc.", "URL": "https://pubchem.ncbi.nlm.nih.gov/classification", "Section": [ { "TOCHeading": "Ontologies", "Description": "Ontologies", "Section": [ { "TOCHeading": "MeSH Tree", "Description": "MeSH tree", "Information": [ { "ReferenceNumber": 70, "Name": "HID", "URL": "https://pubchem.ncbi.nlm.nih.gov/classification/#hid=1", "Value": { "Number": [ 1 ] } } ] }, { "TOCHeading": "NCI Thesaurus Tree", "Description": "NCI Thesaurus (NCIt) hierarchy", "URL": "https://ncithesaurus.nci.nih.gov", "Information": [ { "ReferenceNumber": 86, "Name": "HID", "URL": "https://pubchem.ncbi.nlm.nih.gov/classification/#hid=112", "Value": { "Number": [ 112 ] } } ] }, { "TOCHeading": "ChEBI Ontology", "Description": "ChEBI Ontology tree", "Information": [ { "ReferenceNumber": 71, "Name": "HID", "URL": "https://pubchem.ncbi.nlm.nih.gov/classification/#hid=2", "Value": { "Number": [ 2 ] } } ] }, { "TOCHeading": "KEGG: ATC", "Description": "KEGG : ATC tree", "Information": [ { "ReferenceNumber": 72, "Name": "HID", "URL": "https://pubchem.ncbi.nlm.nih.gov/classification/#hid=16", "Value": { "Number": [ 16 ] } } ] }, { "TOCHeading": "KEGG : Antiinfectives", "Description": "KEGG : Antiinfectives tree", "Information": [ { "ReferenceNumber": 73, "Name": "HID", "URL": "https://pubchem.ncbi.nlm.nih.gov/classification/#hid=20", "Value": { "Number": [ 20 ] } } ] }, { "TOCHeading": "WHO ATC Classification System", "Description": "The Anatomical Therapeutic Chemical (ATC) Classification System is used for the classification of drugs. This pharmaceutical coding system divides drugs into different groups according to the organ or system on which they act and/or their therapeutic and chemical characteristics. Each bottom-level ATC code stands for a pharmaceutically used substance, or a combination of substances, in a single indication (or use). This means that one drug can have more than one code: acetylsalicylic acid (aspirin), for example, has A01AD05 as a drug for local oral treatment, B01AC06 as a platelet inhibitor, and N02BA01 as an analgesic and antipyretic. On the other hand, several different brands share the same code if they have the same active substance and indications.", "URL": "http://www.whocc.no/atc/", "Information": [ { "ReferenceNumber": 75, "Name": "HID", "URL": "https://pubchem.ncbi.nlm.nih.gov/classification/#hid=79", "Value": { "Number": [ 79 ] } } ] }, { "TOCHeading": "ChemIDplus", "Description": "ChemIDplus tree", "Information": [ { "ReferenceNumber": 77, "Name": "HID", "URL": "https://pubchem.ncbi.nlm.nih.gov/classification/#hid=84", "Value": { "Number": [ 84 ] } } ] }, { "TOCHeading": "IUPHAR/BPS Guide to PHARMACOLOGY Target Classification", "Description": "Protein classification from IUPHAR/BPS Guide to PHARMACOLOGY", "URL": "http://guidetopharmacology.org/targets.jsp", "Information": [ { "ReferenceNumber": 79, "Name": "HID", "URL": "https://pubchem.ncbi.nlm.nih.gov/classification/#hid=92", "Value": { "Number": [ 92 ] } } ] }, { "TOCHeading": "ChEMBL Target Tree", "Description": "Protein target tree from ChEMBL", "URL": "https://www.ebi.ac.uk/chembl/target/browser", "Information": [ { "ReferenceNumber": 78, "Name": "HID", "URL": "https://pubchem.ncbi.nlm.nih.gov/classification/#hid=87", "Value": { "Number": [ 87 ] } } ] }, { "TOCHeading": "UN GHS Classification", "Description": "The United Nations' Globally Harmonized System of Classification and Labelling of Chemicals (GHS) provides a harmonized basis for globally uniform physical, environmental, and health and safety information on hazardous chemical substances and mixtures.", "Information": [ { "ReferenceNumber": 76, "Name": "HID", "URL": "https://pubchem.ncbi.nlm.nih.gov/classification/#hid=83", "Value": { "Number": [ 83 ] } } ] }, { "TOCHeading": "NORMAN Suspect List Exchange Classification", "Description": "NORMAN Suspect List Exchange Classification", "Information": [ { "ReferenceNumber": 80, "Name": "HID", "URL": "https://pubchem.ncbi.nlm.nih.gov/classification/#hid=101", "Value": { "Number": [ 101 ] } } ] }, { "TOCHeading": "CCSBase Classification", "Description": "CCSBase Classification", "Information": [ { "ReferenceNumber": 81, "Name": "HID", "URL": "https://pubchem.ncbi.nlm.nih.gov/classification/#hid=104", "Value": { "Number": [ 104 ] } } ] }, { "TOCHeading": "EPA DSSTox Classification", "Description": "EPA DSSTox Classification", "Information": [ { "ReferenceNumber": 82, "Name": "HID", "URL": "https://pubchem.ncbi.nlm.nih.gov/classification/#hid=105", "Value": { "Number": [ 105 ] } } ] }, { "TOCHeading": "International Agency for Research on Cancer (IARC) Classification", "Description": "International Agency for Research on Cancer (IARC) Classification", "Information": [ { "ReferenceNumber": 84, "Name": "HID", "URL": "https://pubchem.ncbi.nlm.nih.gov/classification/#hid=107", "Value": { "Number": [ 107 ] } } ] }, { "TOCHeading": "LOTUS Tree", "Description": "Biological and chemical tree provided by the the naturaL prOducTs occUrrence databaSe (LOTUS)", "URL": "https://lotus.naturalproducts.net/", "Information": [ { "ReferenceNumber": 87, "Name": "HID", "URL": "https://pubchem.ncbi.nlm.nih.gov/classification/#hid=115", "Value": { "Number": [ 115 ] } } ] } ] } ] } ], "Reference": [ { "ReferenceNumber": 1, "SourceName": "CAS Common Chemistry", "SourceID": "54-05-7", "Name": "Chloroquine", "Description": "CAS Common Chemistry is an open community resource for accessing chemical information. Nearly 500,000 chemical substances from CAS REGISTRY cover areas of community interest, including common and frequently regulated chemicals, and those relevant to high school and undergraduate chemistry classes. This chemical information, curated by our expert scientists, is provided in alignment with our mission as a division of the American Chemical Society.", "URL": "https://commonchemistry.cas.org/detail?cas_rn=54-05-7", "LicenseNote": "The data from CAS Common Chemistry is provided under a CC-BY-NC 4.0 license, unless otherwise stated.", "LicenseURL": "https://creativecommons.org/licenses/by-nc/4.0/", "ANID": 13015812 }, { "ReferenceNumber": 4, "SourceName": "ChemIDplus", "SourceID": "0000054057", "Name": "Chloroquine [USP:INN:BAN]", "Description": "ChemIDplus is a free, web search system that provides access to the structure and nomenclature authority files used for the identification of chemical substances cited in National Library of Medicine (NLM) databases, including the TOXNET system.", "URL": "https://chem.nlm.nih.gov/chemidplus/sid/0000054057", "LicenseURL": "https://www.nlm.nih.gov/copyright.html", "IsToxnet": true, "ANID": 762003 }, { "ReferenceNumber": 10, "SourceName": "DrugBank", "SourceID": "DB00608", "Name": "Chloroquine", "Description": "The DrugBank database is a unique bioinformatics and cheminformatics resource that combines detailed drug (i.e. chemical, pharmacological and pharmaceutical) data with comprehensive drug target (i.e. sequence, structure, and pathway) information.", "URL": "https://www.drugbank.ca/drugs/DB00608", "LicenseNote": "Creative Common's Attribution-NonCommercial 4.0 International License (http://creativecommons.org/licenses/by-nc/4.0/legalcode)", "LicenseURL": "https://www.drugbank.ca/legal/terms_of_use", "ANID": 3604382 }, { "ReferenceNumber": 11, "SourceName": "DTP/NCI", "SourceID": "NSC 187208", "Name": "chloroquine", "Description": "The NCI Development Therapeutics Program (DTP) provides services and resources to the academic and private-sector research communities worldwide to facilitate the discovery and development of new cancer therapeutic agents.", "URL": "https://dtp.cancer.gov/dtpstandard/servlet/dwindex?searchtype=NSC&outputformat=html&searchlist=187208", "LicenseNote": "Unless otherwise indicated, all text within NCI products is free of copyright and may be reused without our permission. Credit the National Cancer Institute as the source.", "LicenseURL": "https://www.cancer.gov/policies/copyright-reuse", "ANID": 6746909 }, { "ReferenceNumber": 12, "SourceName": "EPA DSSTox", "SourceID": "DTXSID2040446", "Name": "Chloroquine", "Description": "DSSTox provides a high quality public chemistry resource for supporting improved predictive toxicology.", "URL": "https://comptox.epa.gov/dashboard/DTXSID2040446", "LicenseURL": "https://www.epa.gov/privacy/privacy-act-laws-policies-and-resources", "ANID": 1157411 }, { "ReferenceNumber": 15, "SourceName": "European Chemicals Agency (ECHA)", "SourceID": "200-191-2", "Name": "Chloroquine", "Description": "The European Chemicals Agency (ECHA) is an agency of the European Union which is the driving force among regulatory authorities in implementing the EU's groundbreaking chemicals legislation for the benefit of human health and the environment as well as for innovation and competitiveness.", "URL": "https://echa.europa.eu/substance-information/-/substanceinfo/100.000.175", "LicenseNote": "Use of the information, documents and data from the ECHA website is subject to the terms and conditions of this Legal Notice, and subject to other binding limitations provided for under applicable law, the information, documents and data made available on the ECHA website may be reproduced, distributed and/or used, totally or in part, for non-commercial purposes provided that ECHA is acknowledged as the source: \"Source: European Chemicals Agency, http://echa.europa.eu/\". Such acknowledgement must be included in each copy of the material. ECHA permits and encourages organisations and individuals to create links to the ECHA website under the following cumulative conditions: Links can only be made to webpages that provide a link to the Legal Notice page.", "LicenseURL": "https://echa.europa.eu/web/guest/legal-notice", "ANID": 2018228 }, { "ReferenceNumber": 18, "SourceName": "Hazardous Substances Data Bank (HSDB)", "SourceID": "3029", "Name": "CHLOROQUINE", "Description": "The Hazardous Substances Data Bank (HSDB) is a toxicology database that focuses on the toxicology of potentially hazardous chemicals. It provides information on human exposure, industrial hygiene, emergency handling procedures, environmental fate, regulatory requirements, nanomaterials, and related areas. The information in HSDB has been assessed by a Scientific Review Panel.", "URL": "https://pubchem.ncbi.nlm.nih.gov/source/hsdb/3029", "IsToxnet": true, "ANID": 2211 }, { "ReferenceNumber": 19, "SourceName": "Human Metabolome Database (HMDB)", "SourceID": "HMDB0014746", "Name": "Chloroquine", "Description": "The Human Metabolome Database (HMDB) is a freely available electronic database containing detailed information about small molecule metabolites found in the human body.", "URL": "http://www.hmdb.ca/metabolites/HMDB0014746", "LicenseNote": "\tHMDB is offered to the public as a freely available resource. Use and re-distribution of the data, in whole or in part, for commercial purposes requires explicit permission of the authors and explicit acknowledgment of the source material (HMDB) and the original publication (see the HMDB citing page). We ask that users who download significant portions of the database cite the HMDB paper in any resulting publications.", "LicenseURL": "http://www.hmdb.ca/citing", "ANID": 2151222 }, { "ReferenceNumber": 2, "SourceName": "CCSbase", "SourceID": "CCSBASE_59309EAE4E", "Name": "Chloroquine", "Description": "CCSbase curates experimental collision cross section values measured on various ion mobility platforms as a resource for the research community. CCSbase also builds prediction models for comprehensive prediction of collision cross sections for a given molecule.", "ANID": 9265152 }, { "ReferenceNumber": 3, "SourceName": "ChEBI", "SourceID": "CHEBI:OBO:2719", "Name": "Chloroquine", "Description": "Chemical Entities of Biological Interest (ChEBI) is a database and ontology of molecular entities focused on 'small' chemical compounds, that is part of the Open Biomedical Ontologies effort. The term \"molecular entity\" refers to any constitutionally or isotopically distinct atom, molecule, ion, ion pair, radical, radical ion, complex, conformer, etc., identifiable as a separately distinguishable entity.", "URL": "http://www.ebi.ac.uk/chebi/searchId.do?chebiId=CHEBI:3638", "ANID": 2454331 }, { "ReferenceNumber": 22, "SourceName": "LiverTox", "SourceID": "Chloroquine", "Name": "Chloroquine", "Description": "LIVERTOX provides up-to-date, accurate, and easily accessed information on the diagnosis, cause, frequency, patterns, and management of liver injury attributable to prescription and nonprescription medications, herbals and dietary supplements.", "URL": "https://www.ncbi.nlm.nih.gov/books/n/livertox/Chloroquine/", "LicenseURL": "https://www.nlm.nih.gov/copyright.html", "IsToxnet": true, "ANID": 2261790 }, { "ReferenceNumber": 23, "SourceName": "LOTUS - the natural products occurrence database", "SourceID": "Compound::2719", "Description": "LOTUS is one of the biggest and best annotated resources for natural products occurrences available free of charge and without any restriction.", "LicenseNote": "The code for LOTUS is released under the GNU General Public License v3.0.", "LicenseURL": "https://lotus.nprod.net/", "ANID": 14925523 }, { "ReferenceNumber": 5, "SourceName": "ChemIDplus", "SourceID": "r_2719", "Description": "The toxicity data was from the legacy RTECS data set in ChemIDplus.", "URL": "https://chem.nlm.nih.gov/chemidplus/sid/0000054057", "LicenseURL": "https://www.nlm.nih.gov/copyright.html", "IsToxnet": true, "ANID": 3671823 }, { "ReferenceNumber": 6, "SourceName": "ClinicalTrials.gov", "SourceID": "cid2719", "Description": "ClinicalTrials.gov is an NIH registry and results database of publicly and privately supported clinical studies of human participants conducted around the world.", "URL": "https://clinicaltrials.gov/", "LicenseNote": "The ClinicalTrials.gov data carry an international copyright outside the United States and its Territories or Possessions. Some ClinicalTrials.gov data may be subject to the copyright of third parties; you should consult these entities for any additional terms of use.", "LicenseURL": "https://clinicaltrials.gov/ct2/about-site/terms-conditions#Use", "ANID": 5187499 }, { "ReferenceNumber": 7, "SourceName": "Comparative Toxicogenomics Database (CTD)", "SourceID": "D002738::Compound", "Description": "CTD is a robust, publicly available database that aims to advance understanding about how environmental exposures affect human health.", "URL": "http://ctdbase.org/detail.go?type=chem&acc=D002738", "LicenseNote": "It is to be used only for research and educational purposes. Any reproduction or use for commercial purpose is prohibited without the prior express written permission of NC State University.", "LicenseURL": "http://ctdbase.org/about/legal.jsp", "ANID": 9023530 }, { "ReferenceNumber": 8, "SourceName": "Drug Gene Interaction database (DGIdb)", "SourceID": "CHLOROQUINE", "Description": "The Drug Gene Interaction Database (DGIdb, www.dgidb.org) is a web resource that consolidates disparate data sources describing drug - gene interactions and gene druggability.", "URL": "https://www.dgidb.org/drugs/CHLOROQUINE", "LicenseNote": "The data used in DGIdb is all open access and where possible made available as raw data dumps in the downloads section.", "LicenseURL": "http://www.dgidb.org/downloads", "ANID": 8268029 }, { "ReferenceNumber": 9, "SourceName": "Drug Induced Liver Injury Rank (DILIrank) Dataset", "SourceID": "LT01207", "Name": "chloroquine", "Description": "Drug-Induced Liver Injury Rank (DILIrank) Dataset is a list of drugs ranked by their risk for developing DILI in humans.", "URL": "https://www.fda.gov/science-research/liver-toxicity-knowledge-base-ltkb/drug-induced-liver-injury-rank-dilirank-dataset", "LicenseNote": "Unless otherwise noted, the contents of the FDA website (www.fda.gov), both text and graphics, are not copyrighted. They are in the public domain and may be republished, reprinted and otherwise used freely by anyone without the need to obtain permission from FDA. Credit to the U.S. Food and Drug Administration as the source is appreciated but not required.", "LicenseURL": "https://www.fda.gov/about-fda/about-website/website-policies#linking", "ANID": 12652756 }, { "ReferenceNumber": 39, "SourceName": "Nature Chemical Biology", "SourceID": "nchembio.87-comp17", "Description": "Nature Chemical Biology is an international monthly journal that provides a high-visibility forum for the publication of top-tier original research and commentary for the chemical biology community. Chemical biology combines the scientific ideas and approaches of chemistry, biology and allied disciplines to understand and manipulate biological systems with molecular precision.", "URL": "https://pubchem.ncbi.nlm.nih.gov/substance/49681217", "ANID": 8533874 }, { "ReferenceNumber": 40, "SourceName": "Nature Chemical Biology", "SourceID": "nchembio.215-comp4", "Description": "Nature Chemical Biology is an international monthly journal that provides a high-visibility forum for the publication of top-tier original research and commentary for the chemical biology community. Chemical biology combines the scientific ideas and approaches of chemistry, biology and allied disciplines to understand and manipulate biological systems with molecular precision.", "URL": "https://pubchem.ncbi.nlm.nih.gov/substance/85154871", "ANID": 8535872 }, { "ReferenceNumber": 41, "SourceName": "Nature Chemical Biology", "SourceID": "nchembio.368-comp8", "Description": "Nature Chemical Biology is an international monthly journal that provides a high-visibility forum for the publication of top-tier original research and commentary for the chemical biology community. Chemical biology combines the scientific ideas and approaches of chemistry, biology and allied disciplines to understand and manipulate biological systems with molecular precision.", "URL": "https://pubchem.ncbi.nlm.nih.gov/substance/92310316", "ANID": 8536578 }, { "ReferenceNumber": 42, "SourceName": "Nature Chemical Biology", "SourceID": "nchembio.A181208768B-comp16", "Description": "Nature Chemical Biology is an international monthly journal that provides a high-visibility forum for the publication of top-tier original research and commentary for the chemical biology community. Chemical biology combines the scientific ideas and approaches of chemistry, biology and allied disciplines to understand and manipulate biological systems with molecular precision.", "URL": "https://pubchem.ncbi.nlm.nih.gov/substance/384405292", "ANID": 8543859 }, { "ReferenceNumber": 13, "SourceName": "EU Clinical Trials Register", "SourceID": "cid2719", "Description": "The EU Clinical Trials Register contains information on interventional clinical trials on medicines conducted in the European Union (EU), or the European Economic Area (EEA) which started after 1 May 2004.", "URL": "https://www.clinicaltrialsregister.eu/", "ANID": 6479070 }, { "ReferenceNumber": 14, "SourceName": "European Chemicals Agency (ECHA)", "SourceID": "37273", "Name": "Chloroquine", "Description": "The information provided here is aggregated from the \"Notified classification and labelling\" from ECHA's C&L Inventory. Read more: https://echa.europa.eu/information-on-chemicals/cl-inventory-database", "URL": "https://echa.europa.eu/information-on-chemicals/cl-inventory-database/-/discli/details/37273", "LicenseNote": "Use of the information, documents and data from the ECHA website is subject to the terms and conditions of this Legal Notice, and subject to other binding limitations provided for under applicable law, the information, documents and data made available on the ECHA website may be reproduced, distributed and/or used, totally or in part, for non-commercial purposes provided that ECHA is acknowledged as the source: \"Source: European Chemicals Agency, http://echa.europa.eu/\". Such acknowledgement must be included in each copy of the material. ECHA permits and encourages organisations and individuals to create links to the ECHA website under the following cumulative conditions: Links can only be made to webpages that provide a link to the Legal Notice page.", "LicenseURL": "https://echa.europa.eu/web/guest/legal-notice", "ANID": 1861959 }, { "ReferenceNumber": 16, "SourceName": "European Medicines Agency (EMA)", "SourceID": "EU/3/14/1377_1", "Name": "Chloroquine (EU/3/14/1377)", "Description": "The European Medicines Agency (EMA) presents information on regulatory topics of the medicinal product lifecycle in EU countries.", "URL": "https://www.ema.europa.eu/en/medicines/human/orphan-designations/eu3141377", "LicenseNote": "Information on the European Medicines Agency's (EMA) website is subject to a disclaimer and copyright and limited reproduction notices.", "LicenseURL": "https://www.ema.europa.eu/en/about-us/legal-notice", "ANID": 8856841 }, { "ReferenceNumber": 17, "SourceName": "FDA Orange Book", "SourceID": "org_2719", "Description": "The publication, Approved Drug Products with Therapeutic Equivalence Evaluations (the List, commonly known as the Orange Book), identifies drug products approved on the basis of safety and effectiveness by the Food and Drug Administration (FDA) under the Federal Food, Drug, and Cosmetic Act (the Act).", "URL": "https://www.fda.gov/drugs/drug-approvals-and-databases/approved-drug-products-therapeutic-equivalence-evaluations-orange-book", "LicenseNote": "Unless otherwise noted, the contents of the FDA website (www.fda.gov), both text and graphics, are not copyrighted. They are in the public domain and may be republished, reprinted and otherwise used freely by anyone without the need to obtain permission from FDA. Credit to the U.S. Food and Drug Administration as the source is appreciated but not required.", "LicenseURL": "https://www.fda.gov/about-fda/about-website/website-policies#linking", "ANID": 398493 }, { "ReferenceNumber": 54, "SourceName": "NORMAN Suspect List Exchange", "SourceID": "nrm_2719", "Name": "Chloroquine", "Description": "The NORMAN network enhances the exchange of information on emerging environmental substances, and encourages the validation and harmonisation of common measurement methods and monitoring tools so that the requirements of risk assessors and risk managers can be better met. It specifically seeks both to promote and to benefit from the synergies between research teams from different countries in the field of emerging substances.", "LicenseNote": "Data: CC-BY 4.0; Code (hosted by ECI, LCSB): Artistic-2.0", "LicenseURL": "https://creativecommons.org/licenses/by/4.0/", "ANID": 9150586 }, { "ReferenceNumber": 20, "SourceName": "Human Metabolome Database (HMDB)", "SourceID": "HMDB0014746_cms_27431", "Name": "HMDB0014746_cms_27431", "Description": "The Human Metabolome Database (HMDB) is a freely available electronic database containing detailed information about small molecule metabolites found in the human body.", "URL": "https://hmdb.ca/metabolites/HMDB0014746#spectra", "LicenseNote": "\tHMDB is offered to the public as a freely available resource. Use and re-distribution of the data, in whole or in part, for commercial purposes requires explicit permission of the authors and explicit acknowledgment of the source material (HMDB) and the original publication (see the HMDB citing page). We ask that users who download significant portions of the database cite the HMDB paper in any resulting publications.", "LicenseURL": "http://www.hmdb.ca/citing", "ANID": 15325547 }, { "ReferenceNumber": 31, "SourceName": "MassBank of North America (MoNA)", "SourceID": "JP003161", "Name": "CHLOROQUINE", "Description": "MassBank of North America (MoNA) is a metadata-centric, auto-curating repository designed for efficient storage and querying of mass spectral records. There are total 14 MS data records(14 experimental records) for this compound, click the link above to see all spectral information at MoNA website.", "URL": "https://mona.fiehnlab.ucdavis.edu/spectra/browse?query=compound.metaData%3Dq%3D%27name%3D%3D%22InChIKey%22%20and%20value%3D%3D%22WHTVZRBIWZFKQO-UHFFFAOYSA-N%22%27", "LicenseNote": "The content of the MoNA database is licensed under CC BY 4.0.", "LicenseURL": "https://mona.fiehnlab.ucdavis.edu/documentation/license", "ANID": 3419050 }, { "ReferenceNumber": 36, "SourceName": "MassBank of North America (MoNA)", "SourceID": "HMDB0014746_c_ms_100159", "Name": "Chloroquine", "Description": "MassBank of North America (MoNA) is a metadata-centric, auto-curating repository designed for efficient storage and querying of mass spectral records. There are total 14 MS data records(14 experimental records) for this compound, click the link above to see all spectral information at MoNA website.", "URL": "https://mona.fiehnlab.ucdavis.edu/spectra/browse?query=compound.metaData%3Dq%3D%27name%3D%3D%22InChIKey%22%20and%20value%3D%3D%22WHTVZRBIWZFKQO-UHFFFAOYSA-N%22%27", "LicenseNote": "The content of the MoNA database is licensed under CC BY 4.0.", "LicenseURL": "https://mona.fiehnlab.ucdavis.edu/documentation/license", "ANID": 8428111 }, { "ReferenceNumber": 43, "SourceName": "NIST Mass Spectrometry Data Center", "SourceID": "GC-MS #1 for WHTVZRBIWZFKQO-UHFFFAOYSA-N", "Name": "Chloroquine", "Description": "The NIST Mass Spectrometry Data Center, a Group in the Biomolecular Measurement Division (BMD), develops evaluated mass spectral libraries and provides related software tools. These products are intended to assist compound identification by providing reference mass spectra for GC/MS (by electron ionization) and LC-MS/MS (by tandem mass spectrometry) as well as gas phase retention indices for GC.", "URL": "http://www.nist.gov/srd/nist1a.cfm", "LicenseURL": "https://www.nist.gov/srd/public-law", "ANID": 61394 }, { "ReferenceNumber": 44, "SourceName": "NIST Mass Spectrometry Data Center", "SourceID": "GC-MS #2 for WHTVZRBIWZFKQO-UHFFFAOYSA-N", "Name": "Chloroquine", "Description": "The NIST Mass Spectrometry Data Center, a Group in the Biomolecular Measurement Division (BMD), develops evaluated mass spectral libraries and provides related software tools. These products are intended to assist compound identification by providing reference mass spectra for GC/MS (by electron ionization) and LC-MS/MS (by tandem mass spectrometry) as well as gas phase retention indices for GC.", "URL": "http://www.nist.gov/srd/nist1a.cfm", "LicenseURL": "https://www.nist.gov/srd/public-law", "ANID": 260140 }, { "ReferenceNumber": 45, "SourceName": "NIST Mass Spectrometry Data Center", "SourceID": "GC-MS #3 for WHTVZRBIWZFKQO-UHFFFAOYSA-N", "Name": "Chloroquine", "Description": "The NIST Mass Spectrometry Data Center, a Group in the Biomolecular Measurement Division (BMD), develops evaluated mass spectral libraries and provides related software tools. These products are intended to assist compound identification by providing reference mass spectra for GC/MS (by electron ionization) and LC-MS/MS (by tandem mass spectrometry) as well as gas phase retention indices for GC.", "URL": "http://www.nist.gov/srd/nist1a.cfm", "LicenseURL": "https://www.nist.gov/srd/public-law", "ANID": 260147 }, { "ReferenceNumber": 46, "SourceName": "NIST Mass Spectrometry Data Center", "SourceID": "GC-MS #4 for WHTVZRBIWZFKQO-UHFFFAOYSA-N", "Name": "Chloroquine", "Description": "The NIST Mass Spectrometry Data Center, a Group in the Biomolecular Measurement Division (BMD), develops evaluated mass spectral libraries and provides related software tools. These products are intended to assist compound identification by providing reference mass spectra for GC/MS (by electron ionization) and LC-MS/MS (by tandem mass spectrometry) as well as gas phase retention indices for GC.", "URL": "http://www.nist.gov/srd/nist1a.cfm", "LicenseURL": "https://www.nist.gov/srd/public-law", "ANID": 260153 }, { "ReferenceNumber": 47, "SourceName": "NIST Mass Spectrometry Data Center", "SourceID": "GC-MS #5 for WHTVZRBIWZFKQO-UHFFFAOYSA-N", "Name": "Chloroquine", "Description": "The NIST Mass Spectrometry Data Center, a Group in the Biomolecular Measurement Division (BMD), develops evaluated mass spectral libraries and provides related software tools. These products are intended to assist compound identification by providing reference mass spectra for GC/MS (by electron ionization) and LC-MS/MS (by tandem mass spectrometry) as well as gas phase retention indices for GC.", "URL": "http://www.nist.gov/srd/nist1a.cfm", "LicenseURL": "https://www.nist.gov/srd/public-law", "ANID": 260155 }, { "ReferenceNumber": 48, "SourceName": "NIST Mass Spectrometry Data Center", "SourceID": "GC-MS #6 for WHTVZRBIWZFKQO-UHFFFAOYSA-N", "Name": "Chloroquine", "Description": "The NIST Mass Spectrometry Data Center, a Group in the Biomolecular Measurement Division (BMD), develops evaluated mass spectral libraries and provides related software tools. These products are intended to assist compound identification by providing reference mass spectra for GC/MS (by electron ionization) and LC-MS/MS (by tandem mass spectrometry) as well as gas phase retention indices for GC.", "URL": "http://www.nist.gov/srd/nist1a.cfm", "LicenseURL": "https://www.nist.gov/srd/public-law", "ANID": 260156 }, { "ReferenceNumber": 49, "SourceName": "NIST Mass Spectrometry Data Center", "SourceID": "GC-MS #7 for WHTVZRBIWZFKQO-UHFFFAOYSA-N", "Name": "Chloroquine", "Description": "The NIST Mass Spectrometry Data Center, a Group in the Biomolecular Measurement Division (BMD), develops evaluated mass spectral libraries and provides related software tools. These products are intended to assist compound identification by providing reference mass spectra for GC/MS (by electron ionization) and LC-MS/MS (by tandem mass spectrometry) as well as gas phase retention indices for GC.", "URL": "http://www.nist.gov/srd/nist1a.cfm", "LicenseURL": "https://www.nist.gov/srd/public-law", "ANID": 260300 }, { "ReferenceNumber": 56, "SourceName": "SpectraBase", "SourceID": "30UDEp4qVU", "Name": "Chloroquine", "Description": "Wiley Science Solutions (https://sciencesolutions.wiley.com) is a leading publisher of spectral databases and KnowItAll spectroscopy software. SpectraBase provides fast text access to hundreds of thousands of NMR, IR, Raman, UV-Vis, and mass spectra.", "URL": "https://spectrabase.com/spectrum/30UDEp4qVU", "ANID": 5068772 }, { "ReferenceNumber": 57, "SourceName": "SpectraBase", "SourceID": "BrpTswYWahi", "Name": "Chloroquine", "Description": "Wiley Science Solutions (https://sciencesolutions.wiley.com) is a leading publisher of spectral databases and KnowItAll spectroscopy software. SpectraBase provides fast text access to hundreds of thousands of NMR, IR, Raman, UV-Vis, and mass spectra.", "URL": "https://spectrabase.com/spectrum/BrpTswYWahi", "ANID": 5068773 }, { "ReferenceNumber": 21, "SourceName": "International Agency for Research on Cancer (IARC)", "SourceID": "iarc_660", "Name": "Chloroquine", "Description": "The International Agency for Research on Cancer (IARC) is the specialized cancer agency of the World Health Organization. The objective of the IARC is to promote international collaboration in cancer research.", "URL": "https://monographs.iarc.who.int/list-of-classifications", "LicenseNote": "Materials made available by IARC/WHO enjoy copyright protection under the Berne Convention for the Protection of Literature and Artistic Works, under other international conventions, and under national laws on copyright and neighbouring rights. IARC exercises copyright over its Materials to make sure that they are used in accordance with the Agency's principles. All rights are reserved.", "LicenseURL": "https://publications.iarc.fr/Terms-Of-Use", "ANID": 13098049 }, { "ReferenceNumber": 24, "SourceName": "MassBank Europe", "SourceID": "WHTVZRBIWZFKQO-UHFFFAOYSA-N_1", "Name": "CHLOROQUINE", "Description": "MassBank Europe (MassBank.EU) was created in 2011 as an open access database of mass spectra of emerging substances to support identification of unknown substances within the NORMAN Network (https://www.norman-network.com/). MassBank.EU is the partner project of MassBank.JP, hosted at the Helmholtz Centre for Environmental Research (UFZ) Leipzig and jointly maintained by UFZ, LCSB (University of Luxembourg) and IPB Halle.", "URL": "https://massbank.eu/MassBank/Result.jsp?inchikey=WHTVZRBIWZFKQO-UHFFFAOYSA-N", "LicenseURL": "https://github.com/MassBank/MassBank-web/blob/master/LICENSE", "ANID": 13641668 }, { "ReferenceNumber": 25, "SourceName": "MassBank Europe", "SourceID": "WHTVZRBIWZFKQO-UHFFFAOYSA-N_2", "Name": "Chloroquine", "Description": "MassBank Europe (MassBank.EU) was created in 2011 as an open access database of mass spectra of emerging substances to support identification of unknown substances within the NORMAN Network (https://www.norman-network.com/). MassBank.EU is the partner project of MassBank.JP, hosted at the Helmholtz Centre for Environmental Research (UFZ) Leipzig and jointly maintained by UFZ, LCSB (University of Luxembourg) and IPB Halle.", "URL": "https://massbank.eu/MassBank/Result.jsp?inchikey=WHTVZRBIWZFKQO-UHFFFAOYSA-N", "LicenseURL": "https://github.com/MassBank/MassBank-web/blob/master/LICENSE", "ANID": 13698577 }, { "ReferenceNumber": 26, "SourceName": "MassBank Europe", "SourceID": "WHTVZRBIWZFKQO-UHFFFAOYSA-N_3", "Name": "Chloroquine", "Description": "MassBank Europe (MassBank.EU) was created in 2011 as an open access database of mass spectra of emerging substances to support identification of unknown substances within the NORMAN Network (https://www.norman-network.com/). MassBank.EU is the partner project of MassBank.JP, hosted at the Helmholtz Centre for Environmental Research (UFZ) Leipzig and jointly maintained by UFZ, LCSB (University of Luxembourg) and IPB Halle.", "URL": "https://massbank.eu/MassBank/Result.jsp?inchikey=WHTVZRBIWZFKQO-UHFFFAOYSA-N", "LicenseURL": "https://github.com/MassBank/MassBank-web/blob/master/LICENSE", "ANID": 13698578 }, { "ReferenceNumber": 27, "SourceName": "MassBank Europe", "SourceID": "WHTVZRBIWZFKQO-UHFFFAOYSA-N_4", "Name": "Chloroquine", "Description": "MassBank Europe (MassBank.EU) was created in 2011 as an open access database of mass spectra of emerging substances to support identification of unknown substances within the NORMAN Network (https://www.norman-network.com/). MassBank.EU is the partner project of MassBank.JP, hosted at the Helmholtz Centre for Environmental Research (UFZ) Leipzig and jointly maintained by UFZ, LCSB (University of Luxembourg) and IPB Halle.", "URL": "https://massbank.eu/MassBank/Result.jsp?inchikey=WHTVZRBIWZFKQO-UHFFFAOYSA-N", "LicenseURL": "https://github.com/MassBank/MassBank-web/blob/master/LICENSE", "ANID": 13698579 }, { "ReferenceNumber": 28, "SourceName": "MassBank Europe", "SourceID": "WHTVZRBIWZFKQO-UHFFFAOYSA-N_5", "Name": "Chloroquine", "Description": "MassBank Europe (MassBank.EU) was created in 2011 as an open access database of mass spectra of emerging substances to support identification of unknown substances within the NORMAN Network (https://www.norman-network.com/). MassBank.EU is the partner project of MassBank.JP, hosted at the Helmholtz Centre for Environmental Research (UFZ) Leipzig and jointly maintained by UFZ, LCSB (University of Luxembourg) and IPB Halle.", "URL": "https://massbank.eu/MassBank/Result.jsp?inchikey=WHTVZRBIWZFKQO-UHFFFAOYSA-N", "LicenseURL": "https://github.com/MassBank/MassBank-web/blob/master/LICENSE", "ANID": 13698580 }, { "ReferenceNumber": 29, "SourceName": "MassBank Europe", "SourceID": "WHTVZRBIWZFKQO-UHFFFAOYSA-N_6", "Name": "Chloroquine", "Description": "MassBank Europe (MassBank.EU) was created in 2011 as an open access database of mass spectra of emerging substances to support identification of unknown substances within the NORMAN Network (https://www.norman-network.com/). MassBank.EU is the partner project of MassBank.JP, hosted at the Helmholtz Centre for Environmental Research (UFZ) Leipzig and jointly maintained by UFZ, LCSB (University of Luxembourg) and IPB Halle.", "URL": "https://massbank.eu/MassBank/Result.jsp?inchikey=WHTVZRBIWZFKQO-UHFFFAOYSA-N", "LicenseURL": "https://github.com/MassBank/MassBank-web/blob/master/LICENSE", "ANID": 13698581 }, { "ReferenceNumber": 30, "SourceName": "MassBank Europe", "SourceID": "WHTVZRBIWZFKQO-UHFFFAOYSA-N_7", "Name": "Chloroquine", "Description": "MassBank Europe (MassBank.EU) was created in 2011 as an open access database of mass spectra of emerging substances to support identification of unknown substances within the NORMAN Network (https://www.norman-network.com/). MassBank.EU is the partner project of MassBank.JP, hosted at the Helmholtz Centre for Environmental Research (UFZ) Leipzig and jointly maintained by UFZ, LCSB (University of Luxembourg) and IPB Halle.", "URL": "https://massbank.eu/MassBank/Result.jsp?inchikey=WHTVZRBIWZFKQO-UHFFFAOYSA-N", "LicenseURL": "https://github.com/MassBank/MassBank-web/blob/master/LICENSE", "ANID": 13698582 }, { "ReferenceNumber": 32, "SourceName": "MassBank of North America (MoNA)", "SourceID": "WA000965", "Name": "Chloroquine", "Description": "MassBank of North America (MoNA) is a metadata-centric, auto-curating repository designed for efficient storage and querying of mass spectral records. There are total 14 MS data records(14 experimental records) for this compound, click the link above to see all spectral information at MoNA website.", "URL": "https://mona.fiehnlab.ucdavis.edu/spectra/browse?query=compound.metaData%3Dq%3D%27name%3D%3D%22InChIKey%22%20and%20value%3D%3D%22WHTVZRBIWZFKQO-UHFFFAOYSA-N%22%27", "LicenseNote": "The content of the MoNA database is licensed under CC BY 4.0.", "LicenseURL": "https://mona.fiehnlab.ucdavis.edu/documentation/license", "ANID": 7674579 }, { "ReferenceNumber": 33, "SourceName": "MassBank of North America (MoNA)", "SourceID": "WA000966", "Name": "Chloroquine", "Description": "MassBank of North America (MoNA) is a metadata-centric, auto-curating repository designed for efficient storage and querying of mass spectral records. There are total 14 MS data records(14 experimental records) for this compound, click the link above to see all spectral information at MoNA website.", "URL": "https://mona.fiehnlab.ucdavis.edu/spectra/browse?query=compound.metaData%3Dq%3D%27name%3D%3D%22InChIKey%22%20and%20value%3D%3D%22WHTVZRBIWZFKQO-UHFFFAOYSA-N%22%27", "LicenseNote": "The content of the MoNA database is licensed under CC BY 4.0.", "LicenseURL": "https://mona.fiehnlab.ucdavis.edu/documentation/license", "ANID": 7674580 }, { "ReferenceNumber": 34, "SourceName": "MassBank of North America (MoNA)", "SourceID": "WA000967", "Name": "Chloroquine", "Description": "MassBank of North America (MoNA) is a metadata-centric, auto-curating repository designed for efficient storage and querying of mass spectral records. There are total 14 MS data records(14 experimental records) for this compound, click the link above to see all spectral information at MoNA website.", "URL": "https://mona.fiehnlab.ucdavis.edu/spectra/browse?query=compound.metaData%3Dq%3D%27name%3D%3D%22InChIKey%22%20and%20value%3D%3D%22WHTVZRBIWZFKQO-UHFFFAOYSA-N%22%27", "LicenseNote": "The content of the MoNA database is licensed under CC BY 4.0.", "LicenseURL": "https://mona.fiehnlab.ucdavis.edu/documentation/license", "ANID": 7674581 }, { "ReferenceNumber": 35, "SourceName": "MassBank of North America (MoNA)", "SourceID": "WA000968", "Name": "Chloroquine", "Description": "MassBank of North America (MoNA) is a metadata-centric, auto-curating repository designed for efficient storage and querying of mass spectral records. There are total 14 MS data records(14 experimental records) for this compound, click the link above to see all spectral information at MoNA website.", "URL": "https://mona.fiehnlab.ucdavis.edu/spectra/browse?query=compound.metaData%3Dq%3D%27name%3D%3D%22InChIKey%22%20and%20value%3D%3D%22WHTVZRBIWZFKQO-UHFFFAOYSA-N%22%27", "LicenseNote": "The content of the MoNA database is licensed under CC BY 4.0.", "LicenseURL": "https://mona.fiehnlab.ucdavis.edu/documentation/license", "ANID": 8284615 }, { "ReferenceNumber": 37, "SourceName": "MassBank of North America (MoNA)", "SourceID": "CCMSLIB00005723985", "Name": "Chloroquine", "Description": "MassBank of North America (MoNA) is a metadata-centric, auto-curating repository designed for efficient storage and querying of mass spectral records. There are total 14 MS data records(14 experimental records) for this compound, click the link above to see all spectral information at MoNA website.", "URL": "https://mona.fiehnlab.ucdavis.edu/spectra/browse?query=compound.metaData%3Dq%3D%27name%3D%3D%22InChIKey%22%20and%20value%3D%3D%22WHTVZRBIWZFKQO-UHFFFAOYSA-N%22%27", "LicenseNote": "The content of the MoNA database is licensed under CC BY 4.0.", "LicenseURL": "https://mona.fiehnlab.ucdavis.edu/documentation/license", "ANID": 9288247 }, { "ReferenceNumber": 38, "SourceName": "National Drug Code (NDC) Directory", "SourceID": "s_CHLOROQUINE", "Name": "CHLOROQUINE", "Description": "The National Drug Code (NDC) is a unique, three-segment number that serves as FDA's identifier for drugs. The NDC Directory contains information on active and certified finished and unfinished drugs submitted to FDA.", "URL": "https://www.fda.gov/drugs/drug-approvals-and-databases/national-drug-code-directory", "LicenseNote": "Unless otherwise noted, the contents of the FDA website (www.fda.gov), both text and graphics, are not copyrighted. They are in the public domain and may be republished, reprinted and otherwise used freely by anyone without the need to obtain permission from FDA. Credit to the U.S. Food and Drug Administration as the source is appreciated but not required.", "LicenseURL": "https://www.fda.gov/about-fda/about-website/website-policies#linking", "ANID": 15993378 }, { "ReferenceNumber": 50, "SourceName": "NIST Mass Spectrometry Data Center", "SourceID": "MS-MS #1 for WHTVZRBIWZFKQO-UHFFFAOYSA-N", "Name": "Chloroquine", "Description": "The NIST Mass Spectrometry Data Center, a Group in the Biomolecular Measurement Division (BMD), develops evaluated mass spectral libraries and provides related software tools. These products are intended to assist compound identification by providing reference mass spectra for GC/MS (by electron ionization) and LC-MS/MS (by tandem mass spectrometry) as well as gas phase retention indices for GC.", "URL": "http://www.nist.gov/srd/nist1a.cfm", "LicenseURL": "https://www.nist.gov/srd/public-law", "ANID": 282935 }, { "ReferenceNumber": 51, "SourceName": "NIST Mass Spectrometry Data Center", "SourceID": "MS-MS #2 for WHTVZRBIWZFKQO-UHFFFAOYSA-N", "Name": "Chloroquine", "Description": "The NIST Mass Spectrometry Data Center, a Group in the Biomolecular Measurement Division (BMD), develops evaluated mass spectral libraries and provides related software tools. These products are intended to assist compound identification by providing reference mass spectra for GC/MS (by electron ionization) and LC-MS/MS (by tandem mass spectrometry) as well as gas phase retention indices for GC.", "URL": "http://www.nist.gov/srd/nist1a.cfm", "LicenseURL": "https://www.nist.gov/srd/public-law", "ANID": 282936 }, { "ReferenceNumber": 52, "SourceName": "NIST Mass Spectrometry Data Center", "SourceID": "MS-MS #3 for WHTVZRBIWZFKQO-UHFFFAOYSA-N", "Name": "Chloroquine", "Description": "The NIST Mass Spectrometry Data Center, a Group in the Biomolecular Measurement Division (BMD), develops evaluated mass spectral libraries and provides related software tools. These products are intended to assist compound identification by providing reference mass spectra for GC/MS (by electron ionization) and LC-MS/MS (by tandem mass spectrometry) as well as gas phase retention indices for GC.", "URL": "http://www.nist.gov/srd/nist1a.cfm", "LicenseURL": "https://www.nist.gov/srd/public-law", "ANID": 285619 }, { "ReferenceNumber": 53, "SourceName": "NIST Mass Spectrometry Data Center", "SourceID": "RI for WHTVZRBIWZFKQO-UHFFFAOYSA-N", "Name": "Chloroquine", "Description": "The NIST Mass Spectrometry Data Center, a Group in the Biomolecular Measurement Division (BMD), develops evaluated mass spectral libraries and provides related software tools. These products are intended to assist compound identification by providing reference mass spectra for GC/MS (by electron ionization) and LC-MS/MS (by tandem mass spectrometry) as well as gas phase retention indices for GC.", "URL": "http://www.nist.gov/srd/nist1a.cfm", "LicenseURL": "https://www.nist.gov/srd/public-law", "ANID": 302505 }, { "ReferenceNumber": 55, "SourceName": "PubChem", "SourceID": "covid19_l_3964", "URL": "https://pubchem.ncbi.nlm.nih.gov", "ANID": 9352331 }, { "ReferenceNumber": 58, "SourceName": "SpectraBase", "SourceID": "10sStszu4T5", "Name": "CHLOROQUINE", "Description": "Wiley Science Solutions (https://sciencesolutions.wiley.com) is a leading publisher of spectral databases and KnowItAll spectroscopy software. SpectraBase provides fast text access to hundreds of thousands of NMR, IR, Raman, UV-Vis, and mass spectra.", "URL": "https://spectrabase.com/spectrum/10sStszu4T5", "ANID": 5068776 }, { "ReferenceNumber": 59, "SourceName": "SpectraBase", "SourceID": "E4IWgm7hoj9", "Name": "", "Description": "Wiley Science Solutions (https://sciencesolutions.wiley.com) is a leading publisher of spectral databases and KnowItAll spectroscopy software. SpectraBase provides fast text access to hundreds of thousands of NMR, IR, Raman, UV-Vis, and mass spectra.", "URL": "https://spectrabase.com/spectrum/E4IWgm7hoj9", "ANID": 10722562 }, { "ReferenceNumber": 60, "SourceName": "Springer Nature", "SourceID": "22051007-172322592", "Description": "Literature references related to scientific contents from Springer Nature journals and books. https://link.springer.com/", "URL": "https://pubchem.ncbi.nlm.nih.gov/substance/341140522", "ANID": 3847585 }, { "ReferenceNumber": 61, "SourceName": "Springer Nature", "SourceID": "22051007-172323581", "Description": "Literature references related to scientific contents from Springer Nature journals and books. https://link.springer.com/", "URL": "https://pubchem.ncbi.nlm.nih.gov/substance/341825136", "ANID": 4446852 }, { "ReferenceNumber": 62, "SourceName": "Thieme Chemistry", "SourceID": "22051007-172322592", "Description": "Literature references related to scientific contents from Thieme journals and books. Read more: http://www.thieme-chemistry.com", "LicenseNote": "The Thieme Chemistry contribution within PubChem is provided under a CC-BY-NC-ND 4.0 license, unless otherwise stated.", "LicenseURL": "https://creativecommons.org/licenses/by-nc-nd/4.0/", "ANID": 5545898 }, { "ReferenceNumber": 63, "SourceName": "WHO Anatomical Therapeutic Chemical (ATC) Classification", "SourceID": "1462", "Description": "The WHO Anatomical Therapeutic Chemical (ATC) Classification System is a classification of active ingredients of drugs according to the organ or system on which they act and their therapeutic, pharmacological and chemical properties.", "URL": "https://www.whocc.no/atc/", "LicenseNote": "Use of all or parts of the material requires reference to the WHO Collaborating Centre for Drug Statistics Methodology. Copying and distribution for commercial purposes is not allowed. Changing or manipulating the material is not allowed.", "LicenseURL": "https://www.whocc.no/copyright_disclaimer/", "ANID": 753723 }, { "ReferenceNumber": 64, "SourceName": "WHO Model Lists of Essential Medicines", "SourceID": "275", "Name": "Chloroquine", "Description": "The WHO Model Lists of Essential Medicines contains the medications considered to be most effective and safe to meet the most important needs in a health system. It has been updated every two years since 1977.", "URL": "https://list.essentialmeds.org/medicines/275", "LicenseNote": "WHO supports open access to the published output of its activities as a fundamental part of its mission and a public benefit to be encouraged wherever possible. WHO's open access applies to WHO CC BY-NC-SA 3.0 IGO.", "LicenseURL": "https://www.who.int/about/who-we-are/publishing-policies/copyright", "ANID": 13127783 }, { "ReferenceNumber": 65, "SourceName": "Wikidata", "SourceID": "Q422438", "Name": "Chloroquine", "Description": "Link to the compound information in Wikidata.", "URL": "https://www.wikidata.org/wiki/Q422438", "LicenseNote": "CCZero", "LicenseURL": "https://creativecommons.org/publicdomain/zero/1.0/", "ANID": 16295616 }, { "ReferenceNumber": 66, "SourceName": "Wikipedia", "SourceID": "wpQ422438", "Name": "Chloroquine", "Description": "Link to the compound information in Wikipedia.", "URL": "https://en.wikipedia.org/wiki/Chloroquine", "ANID": 16295617 }, { "ReferenceNumber": 67, "SourceName": "Wiley", "SourceID": "142564", "Description": "Literature references related to scientific contents from Wiley. Read more: https://onlinelibrary.wiley.com/", "URL": "https://pubchem.ncbi.nlm.nih.gov/substance/?source=wiley&sourceid=142564", "ANID": 8318242 }, { "ReferenceNumber": 68, "SourceName": "Medical Subject Headings (MeSH)", "SourceID": "68002738", "Name": "Chloroquine", "Description": "MeSH (Medical Subject Headings) is the U.S. National Library of Medicine's controlled vocabulary thesaurus used for indexing articles for PubMed.", "URL": "https://www.ncbi.nlm.nih.gov/mesh/68002738", "LicenseNote": "Works produced by the U.S. government are not subject to copyright protection in the United States. Any such works found on National Library of Medicine (NLM) Web sites may be freely used or reproduced without permission in the U.S.", "LicenseURL": "https://www.nlm.nih.gov/copyright.html" }, { "ReferenceNumber": 69, "SourceName": "PubChem", "SourceID": "PubChem", "Description": "Data deposited in or computed by PubChem", "URL": "https://pubchem.ncbi.nlm.nih.gov" }, { "ReferenceNumber": 70, "SourceName": "Medical Subject Headings (MeSH)", "SourceID": "DescTree", "Name": "MeSH Tree", "Description": "MeSH (Medical Subject Headings) is the NLM controlled vocabulary thesaurus used for indexing articles for PubMed.", "URL": "http://www.nlm.nih.gov/mesh/meshhome.html", "LicenseNote": "Works produced by the U.S. government are not subject to copyright protection in the United States. Any such works found on National Library of Medicine (NLM) Web sites may be freely used or reproduced without permission in the U.S.", "LicenseURL": "https://www.nlm.nih.gov/copyright.html" }, { "ReferenceNumber": 71, "SourceName": "ChEBI", "SourceID": "OBO", "Name": "ChEBI Ontology", "Description": "The ChEBI Ontology is a structured classification of the entities contained within ChEBI.", "URL": "http://www.ebi.ac.uk/chebi/userManualForward.do#ChEBI%20Ontology" }, { "ReferenceNumber": 72, "SourceName": "KEGG", "SourceID": "br08303", "Name": "Anatomical Therapeutic Chemical (ATC) classification", "Description": "KEGG is an encyclopedia of genes and genomes. Assigning functional meanings to genes and genomes both at the molecular and higher levels is the primary objective of the KEGG database project.", "URL": "http://www.genome.jp/kegg-bin/get_htext?br08303.keg", "LicenseNote": "Academic users may freely use the KEGG website. Non-academic use of KEGG generally requires a commercial license", "LicenseURL": "https://www.kegg.jp/kegg/legal.html" }, { "ReferenceNumber": 73, "SourceName": "KEGG", "SourceID": "br08307", "Name": "Antiinfectives", "Description": "KEGG is an encyclopedia of genes and genomes. Assigning functional meanings to genes and genomes both at the molecular and higher levels is the primary objective of the KEGG database project.", "URL": "http://www.genome.jp/kegg-bin/get_htext?br08307.keg", "LicenseNote": "Academic users may freely use the KEGG website. Non-academic use of KEGG generally requires a commercial license", "LicenseURL": "https://www.kegg.jp/kegg/legal.html" }, { "ReferenceNumber": 75, "SourceName": "WHO Anatomical Therapeutic Chemical (ATC) Classification", "SourceID": "ATCTree", "Name": "ATC Code", "Description": "In the World Health Organization (WHO) Anatomical Therapeutic Chemical (ATC) classification system, the active substances are divided into different groups according to the organ or system on which they act and their therapeutic, pharmacological and chemical properties.", "URL": "https://www.whocc.no/atc_ddd_index/", "LicenseNote": "Use of all or parts of the material requires reference to the WHO Collaborating Centre for Drug Statistics Methodology. Copying and distribution for commercial purposes is not allowed. Changing or manipulating the material is not allowed.", "LicenseURL": "https://www.whocc.no/copyright_disclaimer/" }, { "ReferenceNumber": 76, "SourceName": "UN Globally Harmonized System of Classification and Labelling of Chemicals (GHS)", "SourceID": "UN_GHS_tree", "Name": "GHS Classification Tree", "Description": "The United Nations' Globally Harmonized System of Classification and Labeling of Chemicals (GHS) provides a harmonized basis for globally uniform physical, environmental, and health and safety information on hazardous chemical substances and mixtures. It sets up criteria for the classification of chemicals for physical-chemical, health, and environmental hazards of chemical substances and mixtures and sets up standardized hazard information to facilitate global trade of chemicals. Please note that obsolete codes are added in this classification for completeness purposes, as they are still in use. Any obsolete codes are annotated as such.", "URL": "http://www.unece.org/trans/danger/publi/ghs/ghs_welcome_e.html" }, { "ReferenceNumber": 77, "SourceName": "ChemIDplus", "SourceID": "ChemIDplus_tree", "Name": "ChemIDplus Chemical Information Classification", "Description": "ChemIDplus is a TOXNET (TOXicology Data NETwork) databases that contain chemicals and drugs related information. It is managed by the Toxicology and Environmental Health Information Program (TEHIP) in the Division of Specialized Information Services (SIS) of the National Library of Medicine (NLM).", "URL": "https://chem.nlm.nih.gov/chemidplus/", "LicenseURL": "https://www.nlm.nih.gov/copyright.html", "IsToxnet": true }, { "ReferenceNumber": 78, "SourceName": "ChEMBL", "SourceID": "Target Tree", "Name": "ChEMBL Protein Target Tree", "Description": "The ChEMBL Protein Target Tree is a structured classification of the protein target entities contained with the ChEMBL resource release version 30.", "URL": "https://www.ebi.ac.uk/chembl/g/#browse/targets", "LicenseNote": "Access to the web interface of ChEMBL is made under the EBI's Terms of Use (http://www.ebi.ac.uk/Information/termsofuse.html). The ChEMBL data is made available on a Creative Commons Attribution-Share Alike 3.0 Unported License (http://creativecommons.org/licenses/by-sa/3.0/).", "LicenseURL": "http://www.ebi.ac.uk/Information/termsofuse.html" }, { "ReferenceNumber": 79, "SourceName": "IUPHAR/BPS Guide to PHARMACOLOGY", "SourceID": "Target Classification", "Name": "Guide to Pharmacology Target Classification", "Description": "An expert-driven guide to pharmacological targets and the substances that act on them", "URL": "https://www.guidetopharmacology.org/targets.jsp", "LicenseNote": "The Guide to PHARMACOLOGY database is licensed under the Open Data Commons Open Database License (ODbL) https://opendatacommons.org/licenses/odbl/. Its contents are licensed under a Creative Commons Attribution-ShareAlike 4.0 International License (http://creativecommons.org/licenses/by-sa/4.0/)", "LicenseURL": "https://www.guidetopharmacology.org/about.jsp#license" }, { "ReferenceNumber": 80, "SourceName": "NORMAN Suspect List Exchange", "SourceID": "norman_sle_tree", "Name": "NORMAN Suspect List Exchange Classification", "Description": "The NORMAN Suspect List Exchange (NORMAN-SLE) is a central access point for NORMAN members (and others) to find suspect lists relevant for their environmental monitoring questions.
Update: 05/03/22 09:15:02", "URL": "https://www.norman-network.com/nds/SLE/", "LicenseNote": "Data: CC-BY 4.0; Code (hosted by ECI, LCSB): Artistic-2.0", "LicenseURL": "https://creativecommons.org/licenses/by/4.0/" }, { "ReferenceNumber": 81, "SourceName": "CCSbase", "SourceID": "ccsbase_tree", "Name": "CCSbase Classification", "Description": "CCSbase is an integrated platform consisting of a comprehensive database of Collision Cross Section (CCS) measurements taken from a variety of sources and a high-quality and high-throughput CCS prediction model trained with this database using machine learning.", "URL": "https://ccsbase.net/" }, { "ReferenceNumber": 82, "SourceName": "EPA DSSTox", "SourceID": "dsstoxlist_tree", "Name": "CompTox Chemicals Dashboard Chemical Lists", "Description": "This classification lists the chemical categories from the EPA CompTox Chemicals Dashboard.
Update: 04/28/22 12:53:01", "URL": "https://comptox.epa.gov/dashboard/chemical-lists/", "LicenseURL": "https://www.epa.gov/privacy/privacy-act-laws-policies-and-resources" }, { "ReferenceNumber": 84, "SourceName": "International Agency for Research on Cancer (IARC)", "SourceID": "iarc_tree", "Name": "IARC Classification", "Description": "The International Agency for Research on Cancer (IARC) is the specialized cancer agency of the World Health Organization. The objective of the IARC is to promote international collaboration in cancer research.", "URL": "https://www.iarc.fr/", "LicenseNote": "Materials made available by IARC/WHO enjoy copyright protection under the Berne Convention for the Protection of Literature and Artistic Works, under other international conventions, and under national laws on copyright and neighbouring rights. IARC exercises copyright over its Materials to make sure that they are used in accordance with the Agency's principles. All rights are reserved.", "LicenseURL": "https://publications.iarc.fr/Terms-Of-Use" }, { "ReferenceNumber": 86, "SourceName": "NCI Thesaurus (NCIt)", "SourceID": "NCIt", "Name": "NCI Thesaurus Tree", "Description": "The NCI Thesaurus (NCIt) provides reference terminology for many NCI and other systems. It covers vocabulary for clinical care, translational and basic research, and public information and administrative activities.", "URL": "https://ncit.nci.nih.gov", "LicenseNote": "Unless otherwise indicated, all text within NCI products is free of copyright and may be reused without our permission. Credit the National Cancer Institute as the source.", "LicenseURL": "https://www.cancer.gov/policies/copyright-reuse" }, { "ReferenceNumber": 87, "SourceName": "LOTUS - the natural products occurrence database", "SourceID": "biochem", "Name": "LOTUS Tree", "Description": "Biological and chemical tree provided by the LOTUS (naturaL products occurrence database)", "URL": "https://lotus.naturalproducts.net/", "LicenseNote": "The code for LOTUS is released under the GNU General Public License v3.0.", "LicenseURL": "https://lotus.nprod.net/" }, { "ReferenceNumber": 88, "SourceName": "Medical Subject Headings (MeSH)", "SourceID": "68000563", "Name": "Amebicides", "Description": "MeSH (Medical Subject Headings) is the U.S. National Library of Medicine's controlled vocabulary thesaurus used for indexing articles for PubMed.", "URL": "https://www.ncbi.nlm.nih.gov/mesh/68000563", "LicenseNote": "Works produced by the U.S. government are not subject to copyright protection in the United States. Any such works found on National Library of Medicine (NLM) Web sites may be freely used or reproduced without permission in the U.S.", "LicenseURL": "https://www.nlm.nih.gov/copyright.html" }, { "ReferenceNumber": 89, "SourceName": "Medical Subject Headings (MeSH)", "SourceID": "68018501", "Name": "Antirheumatic Agents", "Description": "MeSH (Medical Subject Headings) is the U.S. National Library of Medicine's controlled vocabulary thesaurus used for indexing articles for PubMed.", "URL": "https://www.ncbi.nlm.nih.gov/mesh/68018501", "LicenseNote": "Works produced by the U.S. government are not subject to copyright protection in the United States. Any such works found on National Library of Medicine (NLM) Web sites may be freely used or reproduced without permission in the U.S.", "LicenseURL": "https://www.nlm.nih.gov/copyright.html" }, { "ReferenceNumber": 90, "SourceName": "Medical Subject Headings (MeSH)", "SourceID": "68000962", "Name": "Antimalarials", "Description": "MeSH (Medical Subject Headings) is the U.S. National Library of Medicine's controlled vocabulary thesaurus used for indexing articles for PubMed.", "URL": "https://www.ncbi.nlm.nih.gov/mesh/68000962", "LicenseNote": "Works produced by the U.S. government are not subject to copyright protection in the United States. Any such works found on National Library of Medicine (NLM) Web sites may be freely used or reproduced without permission in the U.S.", "LicenseURL": "https://www.nlm.nih.gov/copyright.html" }, { "ReferenceNumber": 91, "SourceName": "PATENTSCOPE (WIPO)", "Name": "SID 403383553", "Description": "The PATENTSCOPE database from WIPO includes patent and chemical structure search (with a free account) that gives access to millions of patent documents. The World Intellectual Property Organisation (WIPO) is a specialized United Nations (UN) agency headquartered in Geneva (Switzerland). Our mission is to lead the development of a balanced and effective international Intellectual Property (IP) system that enables innovation and creativity for the benefit of all. We help governments, businesses and society realize the benefits of Intellectual Property and are notably a world reference source for IP information.", "URL": "https://pubchem.ncbi.nlm.nih.gov/substance/403383553" }, { "ReferenceNumber": 92, "SourceName": "NCBI", "SourceID": "LinkOut", "Description": "LinkOut is a service that allows one to link directly from NCBI databases to a wide range of information and services beyond NCBI systems.", "URL": "https://www.ncbi.nlm.nih.gov/projects/linkout" } ] } qlustered-deepdiff-41c7265/tests/fixtures/d_t1.yaml000066400000000000000000000001241516241264500222740ustar00rootroot00000000000000- [name , hr, avg ] - [Mark McGwire, 65, 0.278] - [Sammy Sosa , 63, 0.288] qlustered-deepdiff-41c7265/tests/fixtures/d_t2.yaml000066400000000000000000000001241516241264500222750ustar00rootroot00000000000000- [name , hr, avg ] - [Mark McGwire, 65, 0.278] - [Sammy Sosa , 63, 0.289] qlustered-deepdiff-41c7265/tests/fixtures/invalid_yaml.yaml000066400000000000000000000000221516241264500241120ustar00rootroot00000000000000["invalid csv!"]? qlustered-deepdiff-41c7265/tests/fixtures/nested_a_result.json000066400000000000000000000124171516241264500246440ustar00rootroot00000000000000{ "iterable_item_added": { "root[0][0][2][0][1]": 0, "root[0][1][1][1][5]": "f", "root[0][2][1]": [ [ "b", 2, "d" ], [ "a", "e", "c", "d" ], [ "f", 2, "d", 80 ] ], "root[1][0][2][2][3]": 80, "root[1][1][2][0][1]": 0, "root[1][2][0]": [ [ "a12", "b", "f", "d" ], [ "a", "c", "d" ], [ "e", "c", "d", 9, 2, 4, 5 ] ], "root[1][2][0][1][5]": "f" }, "iterable_item_removed": { "root[0][1][1][2][1]": 22, "root[0][1][1][2][2]": "d", "root[0][2][0][0][2]": "c", "root[0][2][0][0][3]": "d", "root[0][3][1][2][1]": 22, "root[0][3][1][2][2]": "d", "root[1][0][0][1][2]": "d", "root[1][0][0][2][1]": "c", "root[1][2][1][2][3]": 801, "root[1][3][1][2][1]": 22, "root[1][3][1][2][2]": "d" }, "values_changed": { "root[0][0][1][0][0]": { "new_value": 1, "old_value": 10 }, "root[0][1][0][2][3]": { "new_value": 9, "old_value": 1 }, "root[0][1][2][0]": { "new_value": [ 10, "dd3" ], "old_value": [ "10", "dd" ] }, "root[0][1][2][1][0]": { "new_value": "ab", "old_value": "a" }, "root[0][2][0][1][2]": { "new_value": "c1", "old_value": "c" }, "root[0][3][0][2][3]": { "new_value": 9, "old_value": 1 }, "root[0][3][1][0][2]": { "new_value": 2, "old_value": 22 }, "root[0][3][1][1]": { "new_value": [ "f", 9 ], "old_value": [ "a", "e", "c", "d", "d" ] }, "root[0][3][2][0][1]": { "new_value": "dd1", "old_value": "dd" }, "root[0][3][2][1][0]": { "new_value": "ab", "old_value": "a" }, "root[1][0][0][0][1]": { "new_value": "e", "old_value": "b" }, "root[1][0][0][0][2]": { "new_value": "c1", "old_value": "f" }, "root[1][0][0][1][1]": { "new_value": "b", "old_value": "c" }, "root[1][0][1][0]": { "new_value": [ "b", 22, "d" ], "old_value": [ 10, "dada" ] }, "root[1][0][1][2][1]": { "new_value": 2, "old_value": 22 }, "root[1][0][2][0][1]": { "new_value": "d", "old_value": "dd" }, "root[1][0][2][1][2]": { "new_value": 89, "old_value": 389 }, "root[1][1][1][0][0]": { "new_value": 1, "old_value": 10 }, "root[1][2][0][0][0]": { "new_value": 10, "old_value": "a" }, "root[1][2][0][0][2]": { "new_value": 2, "old_value": "c" }, "root[1][2][0][2]": { "new_value": [ "f", 80 ], "old_value": [ "e", "c", "d", 1, 2, 4, 5 ] }, "root[1][2][1][0]": { "new_value": [ 10, "dd3" ], "old_value": [ "10", "d" ] }, "root[1][2][1][1][0]": { "new_value": "ab", "old_value": "a" }, "root[1][3][0][1]": { "new_value": [], "old_value": [ "a", "c", "d" ] }, "root[1][3][0][2][3]": { "new_value": 9, "old_value": 1 }, "root[1][3][1][0][2]": { "new_value": 2, "old_value": 22 }, "root[1][3][1][1]": { "new_value": [ "f", 9 ], "old_value": [ "a", "e", "d" ] }, "root[1][3][2][0][1]": { "new_value": "dd1", "old_value": "dd" }, "root[1][3][2][1][0]": { "new_value": "ab", "old_value": "a" }, "root[1][3][2][2][2]": { "new_value": "4", "old_value": 11 } } } qlustered-deepdiff-41c7265/tests/fixtures/nested_a_t1.json000066400000000000000000000033541516241264500236520ustar00rootroot00000000000000[ [ [ [["a", "b", "c", "d"], ["a", "e", "c", "d"], ["e", "c", "d", 1, 2, 4, 5]], [[10, "b", 2, "d"], ["a", "e", "c", "d"], ["f", 2, "d", 80]], [["10", "d"], ["a", "d", 89], [10, 2, 2, 80]] ], [ [["a", "b", "f", "d"], ["a", "c", "d"], ["e", "c", "d", 1, 2, 4, 5]], [[10, "b", 2, "d"], ["a", "e", "c", "d", "d"], ["f", 22, "d", 80]], [["10", "dd"], ["a", "d", 89], [10, 2]] ], [ [["a", "b", "c", "d"], ["a", "e", "c", "d"], ["e", "c", "d", 1, 2, 4, 5]], [["10", "d"], ["a", "d", 89], [10, 2, 2, 80]] ], [ [["a", "b", "f", "d"], ["a", "c", "d"], ["e", "c", "d", 1, 2, 4, 5]], [[10, "b", 22, "d"], ["a", "e", "c", "d", "d"], ["f", 22, "d", 80]], [["10", "dd"], ["a", "d", 89], [10, 2]] ] ], [ [ [["a", "b", "f", "d"], ["a", "c", "d"], ["e", "c", "d", 1, 2, 4, 5]], [[10, "dada"], ["a", "e", "c", "d", "d"], ["f", 22, "d", 80]], [["10", "dd"], ["a", "d", 389], [10, 2]] ], [ [["a", "b", "c", "d"], ["a", "e", "c", "d"], ["e", "c", "d", 1, 2, 4, 5]], [[10, "b", 2, "d"], ["a", "e", "c", "d"], ["f", 2, "d", 80]], [["10", "d"], ["a", "d", 89], [10, 2, 2, 80]] ], [ [["a", "b", "c", "d"], ["a", "e", "c", "d"], ["e", "c", "d", 1, 2, 4, 5]], [["10", "d"], ["a", "d", 89], [10, 2, 2, 801]] ], [ [["a", "b", "f", "d"], ["a", "c", "d"], ["e", "c", "d", 1, 2, 4, 5]], [[10, "b", 22, "d"], ["a", "e", "d"], ["f", 22, "d", 80]], [["10", "dd"], ["a", "d", 89], [10, 2, 11]] ] ] ] qlustered-deepdiff-41c7265/tests/fixtures/nested_a_t2.json000066400000000000000000000034571516241264500236570ustar00rootroot00000000000000[ [ [ [["a", "b", "c", "d"], ["a", "e", "c", "d"], ["e", "c", "d", 1, 2, 4, 5]], [[1, "b", 2, "d"], ["a", "e", "c", "d"], ["f", 2, "d", 80]], [["10", 0, "d"], ["a", "d", 89], [10, 2, 2, 80]] ], [ [["a", "b", "f", "d"], ["a", "c", "d"], ["e", "c", "d", 9, 2, 4, 5]], [[10, "dd3"], ["ab", "d", 89], [10, 2]], [[10, "b", 2, "d"], ["a", "e", "c", "d", "d", "f"], ["f", 80]] ], [ [["a", "b"], ["a", "e", "c1", "d"], ["e", "c", "d", 1, 2, 4, 5]], [["b", 2, "d"], ["a", "e", "c", "d"], ["f", 2, "d", 80]], [["10", "d"], ["a", "d", 89], [10, 2, 2, 80]] ], [ [["a", "b", "f", "d"], ["a", "c", "d"], ["e", "c", "d", 9, 2, 4, 5]], [[10, "b", 2, "d"], ["f", 9], ["f", 80]], [["10", "dd1"], ["ab", "d", 89], [10, 2]] ] ], [ [ [["a", "b", "c", "d"], ["a", "e", "c", "d"], ["e", "c", "d", 1, 2, 4, 5]], [[1, "b", 2, "d"], ["a", "e", "c", "d"], ["f", 2, "d", 80]], [["10", 0, "d"], ["a", "d", 89], [10, 2, 2, 80]] ], [ [["a12", "b", "f", "d"], ["a", "c", "d"], ["e", "c", "d", 9, 2, 4, 5]], [[10, "dd3"], ["ab", "d", 89], [10, 2]], [[10, "b", 2, "d"], ["a", "e", "c", "d", "d", "f"], ["f", 80]] ], [ [["a", "b"], ["a", "e", "c1", "d"], ["e", "d", 1, 2, 4, 5]], [["b", 22, "d"], ["a", "e", "c", "d"], ["f", 2, "d", 80]], [["10", "d"], ["a", "d", 89], [10, 2, 2, 80]] ], [ [["a", "b", "f", "d"], [], ["e", "c", "d", 9, 2, 4, 5]], [[10, "b", 2, "d"], ["f", 9], ["f", 80]], [["10", "dd1"], ["ab", "d", 89], [10, 2, "4"]] ] ] ] qlustered-deepdiff-41c7265/tests/fixtures/nested_b_result.json000066400000000000000000000007471516241264500246500ustar00rootroot00000000000000{ "iterable_item_removed": { "root[0]['key3'][0][0][0][0][0][0][1][0][0][1]": 7 }, "values_changed": { "root[0]['key3'][0][0][0][0][0][0][0][0][0][1]": { "new_value": 3, "old_value": 2 }, "root[0]['key3'][0][0][0][0][0][0][1][0][0][2]": { "new_value": 1, "old_value": 3 }, "root[1]['key5']": { "new_value": "CHANGE", "old_value": "val5" } } } qlustered-deepdiff-41c7265/tests/fixtures/nested_b_t1.json000066400000000000000000000002451516241264500236470ustar00rootroot00000000000000[ { "key3": [[[[[[[[[[1, 2, 4, 5]]], [[[8, 7, 3, 5]]]]]]]]]], "key4": [7, 8] }, { "key5": "val5", "key6": "val6" } ] qlustered-deepdiff-41c7265/tests/fixtures/nested_b_t2.json000066400000000000000000000002471516241264500236520ustar00rootroot00000000000000[ { "key5": "CHANGE", "key6": "val6" }, { "key3": [[[[[[[[[[1, 3, 5, 4]]], [[[8, 8, 1, 5]]]]]]]]]], "key4": [7, 8] } ] qlustered-deepdiff-41c7265/tests/fixtures/patch1.pickle000066400000000000000000000001101516241264500231250ustar00rootroot00000000000000=}values_changed}root[2]['zip']} new_valueJ_sss.qlustered-deepdiff-41c7265/tests/fixtures/t1.csv000066400000000000000000000001201516241264500216160ustar00rootroot00000000000000first_name,last_name,zip Joe,Nobody,90011 Jack,Mickey,90007 James,Molotov,90001 qlustered-deepdiff-41c7265/tests/fixtures/t1.json000066400000000000000000000000611516241264500220000ustar00rootroot00000000000000[{ "key1": "value1", "key2": "value2" }] qlustered-deepdiff-41c7265/tests/fixtures/t1.pickle000066400000000000000000000000341516241264500222760ustar00rootroot00000000000000}(KKKKKKu.qlustered-deepdiff-41c7265/tests/fixtures/t1.toml000066400000000000000000000007761516241264500220170ustar00rootroot00000000000000# This is a TOML document. title = "TOML Example" [owner] name = "Tom Preston-Werner" dob = 1979-05-27 [database] server = "192.168.1.1" ports = [ 8001, 8001, 8002 ] connection_max = 5000 enabled = true [servers] # Indentation (tabs and/or spaces) is allowed but not required [servers.alpha] ip = "10.0.0.1" dc = "eqdc10" [servers.beta] ip = "10.0.0.2" dc = "eqdc10" [clients] data = [ ["gamma", "delta"], [1, 2] ] # Line breaks are OK when inside arrays hosts = [ "alpha", "omega" ] qlustered-deepdiff-41c7265/tests/fixtures/t1.yaml000066400000000000000000000001241516241264500217710ustar00rootroot00000000000000- [name , hr, avg ] - [Mark McGwire, 65, 0.278] - [Sammy Sosa , 63, 0.288] qlustered-deepdiff-41c7265/tests/fixtures/t1_corrupt.json000066400000000000000000000000621516241264500235570ustar00rootroot00000000000000[{ "key1": "value1", "key2": "value2", }] qlustered-deepdiff-41c7265/tests/fixtures/t2.csv000066400000000000000000000001201516241264500216170ustar00rootroot00000000000000first_name,last_name,zip Joe,Nobody,90011 James,Molotov,90002 Jack,Mickey,90007 qlustered-deepdiff-41c7265/tests/fixtures/t2.json000066400000000000000000000001071516241264500220020ustar00rootroot00000000000000[{ "key1": "value1", "key2": "valueB", "key3": "value3" }] qlustered-deepdiff-41c7265/tests/fixtures/t2.pickle000066400000000000000000000000341516241264500222770ustar00rootroot00000000000000}(KKKKKKu.qlustered-deepdiff-41c7265/tests/fixtures/t2.toml000066400000000000000000000007601516241264500220110ustar00rootroot00000000000000# This is a TOML document. title = "TOML Example" [owner] name = "Jake" dob = 1979-05-27 [database] server = "192.168.1.1" ports = [ 8001, 8004, 8002 ] connection_max = 5000 enabled = true [servers] # Indentation (tabs and/or spaces) is allowed but not required [servers.alpha] ip = "10.0.0.2" dc = "eqdc10" [servers.beta] ip = "10.0.0.2" dc = "eqdc10" [clients] data = [ ["gamma", "delta"], [1, 2] ] # Line breaks are OK when inside arrays hosts = [ "alpha", "omega" ] qlustered-deepdiff-41c7265/tests/fixtures/t2.yaml000066400000000000000000000001241516241264500217720ustar00rootroot00000000000000- [name , hr, avg ] - [Mark McGwire, 61, 0.278] - [Sammy Sosa , 63, 0.288] qlustered-deepdiff-41c7265/tests/fixtures/t2_json.csv000066400000000000000000000000301516241264500226500ustar00rootroot00000000000000key1,key2 value1,value3 qlustered-deepdiff-41c7265/tests/test_anyset.py000066400000000000000000000024211516241264500216260ustar00rootroot00000000000000from deepdiff.anyset import AnySet class TestAnySet: def test_anyset_init1(self): items = [1, 2, 4, 4] result = AnySet(items) expected = ({1, 2, 4}, {}) assert expected == result assert repr(result) == r'< AnySet SetOrdered([1, 2, 4]), {} >' def test_anyset_init2(self): items = [1, 2, {1}, 4, 4, {1}] result = AnySet(items) expected = ({1, 2, 4}, {'e298e5a6cfa50a5b9d2cd4392c6c34a867d325e8de2966a8183c4cdf9a93120d': {1}}) assert expected == result def test_anyset_init3_add(self): items = [1, 2, {1}, 4, 4, {1}] result = AnySet() for item in items: result.add(item) expected = ({1, 2, 4}, {'e298e5a6cfa50a5b9d2cd4392c6c34a867d325e8de2966a8183c4cdf9a93120d': {1}}) assert expected == result def test_anyset_pop1(self): items = [1, 2, {1}, 4, 4, {1}] result = AnySet(items) while result: result_len = len(result) item = result.pop() assert item in items assert len(result) == result_len - 1 def test_iter_anyset(self): items = [1, 2, {1}, 4, 4, {1}, {3: 3}] obj = AnySet(items) result = [i for i in obj] assert [1, 2, 4, {1}, {3: 3}] == result qlustered-deepdiff-41c7265/tests/test_cache.py000066400000000000000000000164261516241264500214000ustar00rootroot00000000000000import pytest from decimal import Decimal from deepdiff import DeepDiff from deepdiff.helper import py_current_version class TestCache: @pytest.mark.slow def test_cache_deeply_nested_a1(self, nested_a_t1, nested_a_t2, nested_a_result, nested_a_affected_paths, benchmark): benchmark(self._test_cache_deeply_nested_a1, nested_a_t1, nested_a_t2, nested_a_result, nested_a_affected_paths) def _test_cache_deeply_nested_a1(self, nested_a_t1, nested_a_t2, nested_a_result, nested_a_affected_paths): diff = DeepDiff(nested_a_t1, nested_a_t2, ignore_order=True, cache_size=5000, cache_tuning_sample_size=280, cutoff_intersection_for_pairs=1) stats = diff.get_stats() expected_stats = { "PASSES COUNT": 1671, "DIFF COUNT": 8556, "DISTANCE CACHE HIT COUNT": 3445, "MAX PASS LIMIT REACHED": False, "MAX DIFF LIMIT REACHED": False, } # assert expected_stats == stats assert nested_a_result == diff diff_of_diff = DeepDiff(nested_a_result, diff.to_dict(), ignore_order=False) assert not diff_of_diff assert nested_a_affected_paths == diff.affected_paths assert [0, 1] == diff.affected_root_keys @pytest.mark.slow def test_cache_deeply_nested_a2(self, nested_a_t1, nested_a_t2, nested_a_result): diff = DeepDiff(nested_a_t1, nested_a_t2, ignore_order=True, cache_size=500, cache_tuning_sample_size=500, cutoff_intersection_for_pairs=1) stats = diff.get_stats() # Somehow just in python 3.5 the cache stats are different. Weird. expected_stats = { 'PASSES COUNT': 3960, 'DIFF COUNT': 19469, 'DISTANCE CACHE HIT COUNT': 11847, 'MAX PASS LIMIT REACHED': False, 'MAX DIFF LIMIT REACHED': False } assert not DeepDiff(expected_stats, stats, use_log_scale=True) assert nested_a_result == diff diff_of_diff = DeepDiff(nested_a_result, diff.to_dict(), ignore_order=False) assert not diff_of_diff def test_cache_deeply_nested_b(self, nested_b_t1, nested_b_t2, nested_b_result): diff = DeepDiff(nested_b_t1, nested_b_t2, ignore_order=True, cache_size=5000, cache_tuning_sample_size=0, cutoff_intersection_for_pairs=1) stats = diff.get_stats() expected_stats = { 'PASSES COUNT': 110, 'DIFF COUNT': 306, 'DISTANCE CACHE HIT COUNT': 0, 'MAX PASS LIMIT REACHED': False, 'MAX DIFF LIMIT REACHED': False } stats_diff = DeepDiff(expected_stats, stats, use_log_scale=True, log_scale_similarity_threshold=0.15) assert not stats_diff assert nested_b_result == diff diff_of_diff = DeepDiff(nested_b_result, diff.to_dict(), ignore_order=False) assert not diff_of_diff def test_cache_1D_array_of_numbers_that_do_not_overlap(self): ignore_order = True cache_size = 5000 max_diffs = 30000 max_passes = 40000 t1 = list(range(1, 30)) t2 = list(range(100, 120)) diff = DeepDiff(t1, t2, ignore_order=ignore_order, max_passes=max_passes, max_diffs=max_diffs, cache_size=cache_size, cache_tuning_sample_size=100) stats = diff.get_stats() # Since there was no overlap between the 2 arrays, even though ignore_order=True, # the algorithm switched to comparing by order. expected_stats = { 'PASSES COUNT': 0, 'DIFF COUNT': 50, 'DISTANCE CACHE HIT COUNT': 0, 'MAX PASS LIMIT REACHED': False, 'MAX DIFF LIMIT REACHED': False } assert expected_stats == stats expected = {'values_changed': {'root[0]': {'new_value': 100, 'old_value': 1}, 'root[1]': {'new_value': 101, 'old_value': 2}, 'root[2]': {'new_value': 102, 'old_value': 3}, 'root[3]': {'new_value': 103, 'old_value': 4}, 'root[4]': {'new_value': 104, 'old_value': 5}, 'root[5]': {'new_value': 105, 'old_value': 6}, 'root[6]': {'new_value': 106, 'old_value': 7}, 'root[7]': {'new_value': 107, 'old_value': 8}, 'root[8]': {'new_value': 108, 'old_value': 9}, 'root[9]': {'new_value': 109, 'old_value': 10}, 'root[10]': {'new_value': 110, 'old_value': 11}, 'root[11]': {'new_value': 111, 'old_value': 12}, 'root[12]': {'new_value': 112, 'old_value': 13}, 'root[13]': {'new_value': 113, 'old_value': 14}, 'root[14]': {'new_value': 114, 'old_value': 15}, 'root[15]': {'new_value': 115, 'old_value': 16}, 'root[16]': {'new_value': 116, 'old_value': 17}, 'root[17]': {'new_value': 117, 'old_value': 18}, 'root[18]': {'new_value': 118, 'old_value': 19}, 'root[19]': {'new_value': 119, 'old_value': 20}}, 'iterable_item_removed': {'root[20]': 21, 'root[21]': 22, 'root[22]': 23, 'root[23]': 24, 'root[24]': 25, 'root[25]': 26, 'root[26]': 27, 'root[27]': 28, 'root[28]': 29}} assert expected == diff def test_cache_1D_array_of_numbers_that_overlap(self): ignore_order = True cache_size = 5000 max_diffs = 30000 max_passes = 40000 t1 = list(range(1, 30)) t2 = list(range(13, 33)) diff = DeepDiff(t1, t2, ignore_order=ignore_order, max_passes=max_passes, max_diffs=max_diffs, cache_size=cache_size, cache_tuning_sample_size=100) stats = diff.get_stats() # In this case an optimization happened to compare numbers. Behind the scene we converted the python lists # into Numpy arrays and cached the distances between numbers. # So the distance cache was heavily used and stayed ON but the level cache was turned off due to low cache hit expected_stats = { 'PASSES COUNT': 1, 'DIFF COUNT': 16, 'DISTANCE CACHE HIT COUNT': 0, 'MAX PASS LIMIT REACHED': False, 'MAX DIFF LIMIT REACHED': False } assert expected_stats == stats expected = { 'values_changed': { 'root[11]': { 'new_value': 30, 'old_value': 12 }, 'root[10]': { 'new_value': 31, 'old_value': 11 }, 'root[9]': { 'new_value': 32, 'old_value': 10 } }, 'iterable_item_removed': { 'root[0]': 1, 'root[1]': 2, 'root[2]': 3, 'root[3]': 4, 'root[4]': 5, 'root[5]': 6, 'root[6]': 7, 'root[7]': 8, 'root[8]': 9 } } assert expected == diff def test_cache_does_not_affect_final_results(self): t1 = [[['b'], 8, 1, 2, 3, 4]] t2 = [[['a'], 5, 1, 2, 3, 4]] diff1 = DeepDiff(t1, t2, ignore_order=True, cache_size=0, cache_tuning_sample_size=0, cutoff_intersection_for_pairs=1) diff2 = DeepDiff(t1, t2, ignore_order=True, cache_size=5000, cache_tuning_sample_size=0, cutoff_intersection_for_pairs=1) print(diff1.get_stats()) print(diff1) assert diff1 == diff2 qlustered-deepdiff-41c7265/tests/test_colored_view.py000066400000000000000000000257541516241264500230220ustar00rootroot00000000000000from deepdiff import DeepDiff from deepdiff.helper import COLORED_VIEW, COLORED_COMPACT_VIEW from deepdiff.colored_view import RED, GREEN, RESET, ColoredView def test_colored_view_basic(): t1 = { "name": "John", "age": 30, "gender": "male", "scores": [1, 2, 3], "address": { "city": "New York", "zip": "10001", }, } t2 = { "name": "John", "age": 31, # Changed "scores": [1, 2, 4], # Changed "address": { "city": "Boston", # Changed "zip": "10001", }, "team": "abc", # Added } diff = DeepDiff(t1, t2, view=COLORED_VIEW) result = str(diff) expected = f'''{{ "name": "John", "age": {RED}30{RESET} -> {GREEN}31{RESET}, "scores": [ 1, 2, {RED}3{RESET} -> {GREEN}4{RESET} ], "address": {{ "city": {RED}"New York"{RESET} -> {GREEN}"Boston"{RESET}, "zip": "10001" }}, {GREEN}"team": {GREEN}"abc"{RESET}{RESET}, {RED}"gender": {RED}"male"{RESET}{RESET} }}''' assert result == expected def test_colored_view_nested_changes(): t1 = { "level1": { "level2": { "level3": { "level4": True } } } } t2 = { "level1": { "level2": { "level3": { "level4": False } } } } diff = DeepDiff(t1, t2, view=COLORED_VIEW) result = str(diff) expected = f'''{{ "level1": {{ "level2": {{ "level3": {{ "level4": {RED}true{RESET} -> {GREEN}false{RESET} }} }} }} }}''' assert result == expected def test_colored_view_list_changes(): t1 = [1, 2, 3, 4] t2 = [1, 5, 3, 6] diff = DeepDiff(t1, t2, view=COLORED_VIEW) result = str(diff) expected = f'''[ 1, {RED}2{RESET} -> {GREEN}5{RESET}, 3, {RED}4{RESET} -> {GREEN}6{RESET} ]''' assert result == expected def test_colored_view_list_deletions(): t1 = [1, 2, 3, 4, 5, 6] t2 = [2, 4] diff = DeepDiff(t1, t2, view=COLORED_VIEW) result = str(diff) expected = f'''[ {RED}1{RESET}, 2, {RED}3{RESET}, 4, {RED}5{RESET}, {RED}6{RESET} ]''' assert result == expected def test_colored_view_list_additions(): t1 = [2, 4] t2 = [1, 2, 3, 4, 5] diff = DeepDiff(t1, t2, view=COLORED_VIEW) result = str(diff) expected = f'''[ {GREEN}1{RESET}, 2, {GREEN}3{RESET}, 4, {GREEN}5{RESET} ]''' assert result == expected def test_colored_view_list_all_items_removed(): """Test that all removed items are displayed when list becomes empty.""" t1 = [2, 4] t2 = [] diff = DeepDiff(t1, t2, view=COLORED_VIEW) result = str(diff) expected = f'''[ {RED}2{RESET}, {RED}4{RESET} ]''' assert result == expected def test_colored_compact_view_list_all_items_removed(): """Test that all removed items are displayed when list becomes empty with compact view.""" t1 = [2, 4] t2 = [] diff = DeepDiff(t1, t2, view=COLORED_COMPACT_VIEW) result = str(diff) expected = f'''[ {RED}2{RESET}, {RED}4{RESET} ]''' assert result == expected def test_colored_view_list_changes_deletions(): t1 = [1, 5, 7, 3, 6] t2 = [1, 2, 3, 4] diff = DeepDiff(t1, t2, view=COLORED_VIEW) result = str(diff) expected = f'''[ 1, {RED}5{RESET} -> {GREEN}2{RESET}, {RED}7{RESET}, 3, {RED}6{RESET} -> {GREEN}4{RESET} ]''' assert result == expected def test_colored_view_list_changes_additions(): t1 = [1, 2, 3, 4] t2 = [1, 5, 7, 3, 6] diff = DeepDiff(t1, t2, view=COLORED_VIEW) result = str(diff) expected = f'''[ 1, {RED}2{RESET} -> {GREEN}5{RESET}, {GREEN}7{RESET}, 3, {RED}4{RESET} -> {GREEN}6{RESET} ]''' assert result == expected def test_colored_view_list_no_changes_with_ignore_order(): t1 = [1, 2, 3] t2 = [3, 2, 1] diff = DeepDiff(t1, t2, view=COLORED_VIEW, ignore_order=True) result = str(diff) expected = '''[ 3, 2, 1 ]''' assert result == expected def test_colored_view_list_with_ignore_order(): t1 = { "hobbies": [ "reading", "hiking" ] } t2 = { "hobbies": [ "hiking", "painting", "coding" ] } diff = DeepDiff(t1, t2, view=COLORED_VIEW, ignore_order=True) result = str(diff) expected = f'''{{ "hobbies": [ {RED}"reading"{RESET}, "hiking", {GREEN}"painting"{RESET}, {GREEN}"coding"{RESET} ] }}''' assert result == expected def test_colored_view_no_changes(): t1 = {"a": 1, "b": 2} t2 = {"a": 1, "b": 2} diff = DeepDiff(t1, t2, view=COLORED_VIEW) result = str(diff) expected = '''{ "a": 1, "b": 2 }''' assert result == expected def test_compact_view_basic(): t1 = { "name": "John", "age": 30, "gender": "male", "scores": [1, 2, 3], "address": { "city": "New York", "zip": "10001", "details": { "type": "apartment", "floor": 5 } }, "hobbies": ["reading", {"sport": "tennis", "level": "advanced"}] } t2 = { "name": "John", "age": 31, # Changed "scores": [1, 2, 4], # Changed "address": { "city": "Boston", # Changed "zip": "10001", "details": { "type": "apartment", "floor": 5 } }, "team": "abc", # Added "hobbies": ["reading", {"sport": "tennis", "level": "advanced"}] } diff = DeepDiff(t1, t2, view=COLORED_COMPACT_VIEW) result = str(diff) expected = f'''{{ "name": "John", "age": {RED}30{RESET} -> {GREEN}31{RESET}, "scores": [ 1, 2, {RED}3{RESET} -> {GREEN}4{RESET} ], "address": {{ "city": {RED}"New York"{RESET} -> {GREEN}"Boston"{RESET}, "zip": "10001", "details": {{...}} }}, {GREEN}"team": {GREEN}"abc"{RESET}{RESET}, "hobbies": [...], {RED}"gender": {RED}"male"{RESET}{RESET} }}''' assert result == expected def test_compact_view_nested_changes(): t1 = { "level1": { "unchanged1": { "deep1": True, "deep2": [1, 2, 3] }, "level2": { "a": 1, "b": "test", "c": [1, 2, 3], "d": {"x": 1, "y": 2} }, "unchanged2": [1, 2, {"a": 1}] } } t2 = { "level1": { "unchanged1": { "deep1": True, "deep2": [1, 2, 3] }, "level2": { "a": 2, # Changed "b": "test", "c": [1, 2, 4], # Changed "d": {"x": 1, "y": 3} # Changed }, "unchanged2": [1, 2, {"a": 1}] } } diff = DeepDiff(t1, t2, view=COLORED_COMPACT_VIEW) result = str(diff) expected = f'''{{ "level1": {{ "unchanged1": {{...}}, "level2": {{ "a": {RED}1{RESET} -> {GREEN}2{RESET}, "b": "test", "c": [ 1, 2, {RED}3{RESET} -> {GREEN}4{RESET} ], "d": {{ "x": 1, "y": {RED}2{RESET} -> {GREEN}3{RESET} }} }}, "unchanged2": [...] }} }}''' assert result == expected def test_compact_view_no_changes(): # Test with dict t1 = {"a": 1, "b": [1, 2], "c": {"x": True}} t2 = {"a": 1, "b": [1, 2], "c": {"x": True}} diff = DeepDiff(t1, t2, view=COLORED_COMPACT_VIEW) assert str(diff) == "{...}" # Test with list t1 = [1, {"a": 1}, [1, 2]] t2 = [1, {"a": 1}, [1, 2]] diff = DeepDiff(t1, t2, view=COLORED_COMPACT_VIEW) assert str(diff) == "[...]" def test_compact_view_list_changes(): t1 = [1, {"a": 1, "b": {"x": 1, "y": 2}}, [1, 2, {"z": 3}]] t2 = [1, {"a": 2, "b": {"x": 1, "y": 2}}, [1, 2, {"z": 3}]] diff = DeepDiff(t1, t2, view=COLORED_COMPACT_VIEW) result = str(diff) expected = f'''[ 1, {{ "a": {RED}1{RESET} -> {GREEN}2{RESET}, "b": {{...}} }}, [...] ]''' assert result == expected def test_compact_view_primitive_siblings(): t1 = { "changed": 1, "str_sibling": "hello", "int_sibling": 42, "bool_sibling": True, "nested_sibling": {"a": 1, "b": 2} } t2 = { "changed": 2, "str_sibling": "hello", "int_sibling": 42, "bool_sibling": True, "nested_sibling": {"a": 1, "b": 2} } diff = DeepDiff(t1, t2, view=COLORED_COMPACT_VIEW) result = str(diff) expected = f'''{{ "changed": {RED}1{RESET} -> {GREEN}2{RESET}, "str_sibling": "hello", "int_sibling": 42, "bool_sibling": true, "nested_sibling": {{...}} }}''' assert result == expected def test_colored_view_bool_evaluation(): # Test COLORED_VIEW # Scenario 1: No differences t1_no_diff = {"a": 1, "b": 2} t2_no_diff = {"a": 1, "b": 2} diff_no_diff_colored = DeepDiff(t1_no_diff, t2_no_diff, view=COLORED_VIEW) assert not bool(diff_no_diff_colored), "bool(diff) should be False when no diffs (colored view)" # Scenario 2: With differences t1_with_diff = {"a": 1, "b": 2} t2_with_diff = {"a": 1, "b": 3} diff_with_diff_colored = DeepDiff(t1_with_diff, t2_with_diff, view=COLORED_VIEW) assert bool(diff_with_diff_colored), "bool(diff) should be True when diffs exist (colored view)" # Test COLORED_COMPACT_VIEW # Scenario 1: No differences diff_no_diff_compact = DeepDiff(t1_no_diff, t2_no_diff, view=COLORED_COMPACT_VIEW) assert not bool(diff_no_diff_compact), "bool(diff) should be False when no diffs (compact view)" # Scenario 2: With differences diff_with_diff_compact = DeepDiff(t1_with_diff, t2_with_diff, view=COLORED_COMPACT_VIEW) assert bool(diff_with_diff_compact), "bool(diff) should be True when diffs exist (compact view)" def test_colored_view_with_empty_list_shows_removals(): """ Tests ColoredView correctly shows about an empty list. This covers the bug where it would just show '[]'. """ t1 = [1, 2, 3] t2 = [] ddiff = DeepDiff(t1, t2) view = ColoredView(t2, ddiff.tree) result = str(view) # The output should contain the removed items, colored in red. assert f"{RED}1{RESET}" in result assert f"{RED}2{RESET}" in result assert f"{RED}3{RESET}" in result assert result.strip().startswith('[') assert result.strip().endswith(']') assert result != '[]' def test_colored_view_with_empty_dict_shows_removals(): """ Tests ColoredView correctly shows about an empty dict. This covers the bug where it would just show '{}'. """ t1 = {'a': 1, 'b': 2} t2 = {} ddiff = DeepDiff(t1, t2) view = ColoredView(t2, ddiff.tree) result = str(view) assert f'{RED}{{"a": 1' in result assert f'"b": 2}}{RESET}' in result assert result.strip().startswith(RED) assert result.strip().endswith(RESET) assert result != '{}' qlustered-deepdiff-41c7265/tests/test_command.py000066400000000000000000000121511516241264500217420ustar00rootroot00000000000000import os import pytest from shutil import copyfile from click.testing import CliRunner from deepdiff.commands import diff, patch, grep, extract from conftest import FIXTURES_DIR from deepdiff.helper import pypy3 @pytest.mark.skipif(pypy3, reason='clevercsv is not supported in pypy3') class TestCommands: @pytest.mark.parametrize('name1, name2, expected_in_stdout, expected_exit_code', [ ('t1.json', 't2.json', """dictionary_item_added": {\n "root[0]['key3']": "value3\"""", 0), ('t1_corrupt.json', 't2.json', "Error when loading t1:", 1), ('t1.json', 't2_json.csv', '"old_value": "value2"', 0), ('t2_json.csv', 't1.json', '"old_value": "value3"', 0), ('t1.csv', 't2.csv', '"new_value": "James"', 0), ('t1.toml', 't2.toml', "10.0.0.2", 0), ('t1.pickle', 't2.pickle', '"new_value": 5,\n "old_value": 1', 0), ('t1.yaml', 't2.yaml', '"new_value": 61,\n "old_value": 65', 0), ]) def test_diff_command(self, name1, name2, expected_in_stdout, expected_exit_code): t1 = os.path.join(FIXTURES_DIR, name1) t2 = os.path.join(FIXTURES_DIR, name2) runner = CliRunner() result = runner.invoke(diff, [t1, t2]) assert result.exit_code == expected_exit_code, f"test_diff_command failed for {name1}, {name2}" assert expected_in_stdout in result.output, f"test_diff_command failed for {name1}, {name2}" def test_cli_cant_find_file(self): runner = CliRunner() result = runner.invoke(diff, ['phantom_file1', 'phantom_file2']) assert result.exit_code == 2 assert "Path 'phantom_file1' does not exist" in result.output @pytest.mark.parametrize('t1, t2, args, expected_exit_code', [ ('t1.json', 't2.json', {}, 0), ('t1_corrupt.json', 't2.json', {}, 1), ('t1.json', 't2_json.csv', {}, 0), ('t2_json.csv', 't1.json', {}, 0), ('t1.csv', 't2.csv', ["--ignore-order", "--report-repetition"], 0), ('t1.toml', 't2.toml', {}, 0), ('t1.pickle', 't2.pickle', {}, 0), ('t1.yaml', 't2.yaml', {}, 0), ]) def test_deeppatch_command(self, t1, t2, args, expected_exit_code, tmp_path): t1_copy_path = os.path.join(tmp_path, t1) t1 = os.path.join(FIXTURES_DIR, t1) t2 = os.path.join(FIXTURES_DIR, t2) copyfile(t1, t1_copy_path) runner = CliRunner() delta_pickled = runner.invoke(diff, [t1, t2, '--create-patch', *args]) assert delta_pickled.exit_code == expected_exit_code if expected_exit_code == 0: delta_path = os.path.join(tmp_path, 'delta.pickle') with open(delta_path, 'wb') as the_file: the_file.write(delta_pickled.stdout_bytes) runner = CliRunner() deeppatched = runner.invoke(patch, [t1_copy_path, delta_path]) assert deeppatched.exit_code == expected_exit_code runner = CliRunner() final_diff = runner.invoke(diff, [t1_copy_path, t2, *args]) assert final_diff.exit_code == expected_exit_code assert final_diff.output == '{}\n' def test_command_group_by(self): t1 = os.path.join(FIXTURES_DIR, 'c_t1.csv') t2 = os.path.join(FIXTURES_DIR, 'c_t2.csv') runner = CliRunner() diffed = runner.invoke(diff, [t1, t2, '--group-by', 'id']) assert 0 == diffed.exit_code assert 'values_changed' in diffed.output assert '"new_value": "Chicken"' in diffed.output def test_command_math_epsilon(self): t1 = os.path.join(FIXTURES_DIR, 'd_t1.yaml') t2 = os.path.join(FIXTURES_DIR, 'd_t2.yaml') runner = CliRunner() diffed = runner.invoke(diff, [t1, t2, '--math-epsilon', '0.1']) assert 0 == diffed.exit_code assert '{}\n' == diffed.output diffed2 = runner.invoke(diff, [t1, t2, '--math-epsilon', '0.001']) assert 0 == diffed2.exit_code assert '{\n "values_changed": {\n "root[2][2]": {\n "new_value": 0.289,\n "old_value": 0.288\n }\n }\n}\n' == diffed2.output def test_command_grep(self): path = os.path.join(FIXTURES_DIR, 'd_t1.yaml') runner = CliRunner() diffed = runner.invoke(grep, ['Sammy', path]) assert 0 == diffed.exit_code assert "{'matched_values': ['root[2][0]']}\n" == diffed.output def test_command_err_grep1(self): path = os.path.join(FIXTURES_DIR, 'd_t1.yaml') runner = CliRunner() diffed = runner.invoke(grep, [path, 'Sammy']) assert "Path 'Sammy' does not exist" in diffed.output assert 2 == diffed.exit_code def test_command_err_grep2(self): path = os.path.join(FIXTURES_DIR, 'invalid_yaml.yaml') runner = CliRunner() diffed = runner.invoke(grep, ['invalid', path]) assert "mapping keys are not allowed here" in diffed.output assert 1 == diffed.exit_code def test_command_extract(self): path = os.path.join(FIXTURES_DIR, 'd_t1.yaml') runner = CliRunner() diffed = runner.invoke(extract, ['root[2][2]', path]) assert 0 == diffed.exit_code assert '0.288\n' == diffed.output qlustered-deepdiff-41c7265/tests/test_delta.py000066400000000000000000003372641516241264500214340ustar00rootroot00000000000000import copy import datetime from typing import NamedTuple import pytest import os import io import json import sys from decimal import Decimal from unittest import mock from deepdiff import Delta, DeepDiff from deepdiff.helper import np, number_to_string, TEXT_VIEW, DELTA_VIEW, CannotCompare, FlatDeltaRow, FlatDataAction, SetOrdered from deepdiff.path import GETATTR, GET from deepdiff.delta import ( ELEM_NOT_FOUND_TO_ADD_MSG, VERIFICATION_MSG, VERIFY_BIDIRECTIONAL_MSG, not_found, DeltaNumpyOperatorOverrideError, BINIARY_MODE_NEEDED_MSG, DELTA_AT_LEAST_ONE_ARG_NEEDED, DeltaError, INVALID_ACTION_WHEN_CALLING_GET_ELEM, INVALID_ACTION_WHEN_CALLING_SIMPLE_SET_ELEM, INVALID_ACTION_WHEN_CALLING_SIMPLE_DELETE_ELEM, INDEXES_NOT_FOUND_WHEN_IGNORE_ORDER, FAIL_TO_REMOVE_ITEM_IGNORE_ORDER_MSG, UNABLE_TO_GET_PATH_MSG, NOT_VALID_NUMPY_TYPE) from deepdiff.serialization import ( DELTA_IGNORE_ORDER_NEEDS_REPETITION_REPORT, DELTA_ERROR_WHEN_GROUP_BY, json_dumps, json_loads, ) from tests import PicklableClass, parameterize_cases, CustomClass, CustomClass2 class TestBasicsOfDelta: def test_from_null_delta_json(self): t1 = None t2 = [1, 2, 3, 5] diff = DeepDiff(t1, t2) delta = Delta(diff, serializer=json_dumps) dump = delta.dumps() delta2 = Delta(dump, deserializer=json_loads) assert delta2 + t1 == t2 assert t1 + delta2 == t2 with pytest.raises(ValueError) as exc_info: t2 - delta assert 'Please recreate the delta with bidirectional=True' == str(exc_info.value) delta = Delta(diff, serializer=json_dumps, bidirectional=True) assert t2 - delta == t1 def test_to_null_delta1_json(self): t1 = 1 t2 = None diff = DeepDiff(t1, t2) delta = Delta(diff, serializer=json_dumps) dump = delta.dumps() delta2 = Delta(dump, deserializer=json_loads) assert delta2 + t1 == t2 assert t1 + delta2 == t2 def test_to_null_delta2_json(self): t1 = [1, 2, 3, 5] t2 = None diff = DeepDiff(t1, t2) delta = Delta(diff) assert delta + t1 == t2 assert t1 + delta == t2 def test_list_difference_add_delta(self): t1 = [1, 2] t2 = [1, 2, 3, 5] diff = {'iterable_item_added': {'root[3]': 5, 'root[2]': 3}} delta = Delta(diff) assert delta + t1 == t2 assert t1 + delta == t2 flat_result1 = delta.to_flat_rows() flat_expected1 = [ FlatDeltaRow(path=[3], value=5, action='iterable_item_added', type=int), FlatDeltaRow(path=[2], value=3, action='iterable_item_added', type=int), ] assert flat_expected1 == flat_result1 delta2 = Delta(diff=diff, bidirectional=True) assert t1 == t2 - delta2 def test_list_difference_dump_delta(self): t1 = [1, 2] t2 = [1, 2, 3, 5] diff = DeepDiff(t1, t2) dump = Delta(diff).dumps() delta = Delta(dump) assert delta + t1 == t2 def test_multiple_delta(self): t1 = [1, 2] t2 = [1, 2, 3, 5] t3 = [{1}, 3, 5] dump1 = Delta(DeepDiff(t1, t2)).dumps() dump2 = Delta(DeepDiff(t2, t3)).dumps() delta1 = Delta(dump1) delta2 = Delta(dump2) assert t1 + delta1 + delta2 == t3 def test_delta_dump_and_read1(self, tmp_path): t1 = [1, 2] t2 = [1, 2, 3, 5] diff = DeepDiff(t1, t2) path = os.path.join(tmp_path, 'delta_test.delta') with open(path, 'wb') as the_file: Delta(diff).dump(the_file) delta = Delta(delta_path=path) os.remove(path) assert delta + t1 == t2 def test_delta_dump_and_read2(self, tmp_path): t1 = [1, 2] t2 = [1, 2, 3, 5] diff = DeepDiff(t1, t2) delta_content = Delta(diff).dumps() path = os.path.join(tmp_path, 'delta_test2.delta') with open(path, 'wb') as the_file: the_file.write(delta_content) delta = Delta(delta_path=path) os.remove(path) assert delta + t1 == t2 def test_delta_dump_and_read3(self, tmp_path): t1 = [1, 2] t2 = [1, 2, 3, 5] diff = DeepDiff(t1, t2) delta_content = Delta(diff).dumps() path = os.path.join(tmp_path, 'delta_test2.delta') with open(path, 'wb') as the_file: the_file.write(delta_content) with pytest.raises(ValueError) as excinfo: with open(path, 'r') as the_file: delta = Delta(delta_file=the_file) assert BINIARY_MODE_NEEDED_MSG[:20] == str(excinfo.value)[:20] with open(path, 'rb') as the_file: delta = Delta(delta_file=the_file) os.remove(path) assert delta + t1 == t2 def test_delta_when_no_arg_passed(self): with pytest.raises(ValueError) as excinfo: Delta() assert DELTA_AT_LEAST_ONE_ARG_NEEDED == str(excinfo.value) def test_delta_when_group_by(self): t1 = [ {'id': 'AA', 'name': 'Joe', 'last_name': 'Nobody'}, {'id': 'BB', 'name': 'James', 'last_name': 'Blue'}, ] t2 = [ {'id': 'AA', 'name': 'Joe', 'last_name': 'Nobody'}, {'id': 'BB', 'name': 'James', 'last_name': 'Brown'}, ] diff = DeepDiff(t1, t2, group_by='id') with pytest.raises(ValueError) as excinfo: Delta(diff) assert DELTA_ERROR_WHEN_GROUP_BY == str(excinfo.value) def test_delta_repr(self): t1 = [1, 2] t2 = [1, 2, 3, 5] diff = DeepDiff(t1, t2) delta = Delta(diff) options = { '', '', } assert repr(delta) in options def test_get_elem_and_compare_to_old_value(self): delta = Delta({}) with pytest.raises(DeltaError) as excinfo: delta._get_elem_and_compare_to_old_value( obj=None, path_for_err_reporting=None, expected_old_value=None, action='ketchup on steak') assert INVALID_ACTION_WHEN_CALLING_GET_ELEM.format('ketchup on steak') == str(excinfo.value) def test_simple_set_elem_value(self): delta = Delta({}, raise_errors=True) with pytest.raises(DeltaError) as excinfo: delta._simple_set_elem_value( obj=None, elem=None, value=None, action='mayo on salad', path_for_err_reporting=None) assert INVALID_ACTION_WHEN_CALLING_SIMPLE_SET_ELEM.format('mayo on salad') == str(excinfo.value) with pytest.raises(DeltaError) as excinfo: delta._simple_set_elem_value( obj={}, elem={1}, value=None, action=GET, path_for_err_reporting='mypath') assert str(excinfo.value) in {"Failed to set mypath due to unhashable type: 'set'", "Failed to set mypath due to 'set' objects are unhashable", "Failed to set mypath due to cannot use 'set' as a dict key (unhashable type: 'set')"} def test_simple_delete_elem(self): delta = Delta({}, raise_errors=True) with pytest.raises(DeltaError) as excinfo: delta._simple_delete_elem( obj=None, elem=None, action='burnt oil', path_for_err_reporting=None) assert INVALID_ACTION_WHEN_CALLING_SIMPLE_DELETE_ELEM.format('burnt oil') == str(excinfo.value) with pytest.raises(DeltaError) as excinfo: delta._simple_delete_elem( obj={}, elem=1, action=GET, path_for_err_reporting='mypath') assert "Failed to set mypath due to 1" == str(excinfo.value) def test_raise_error(self): t1 = [1, 2, [3, 5, 6]] t2 = [2, 3, [3, 6, 8]] diff = DeepDiff(t1, t2, ignore_order=True, report_repetition=True) delta = Delta(diff, raise_errors=False) t3 = [1, 2, 3, 5] t4 = t3 + delta assert [3, 2, 3, 5] == t4 delta2 = Delta(diff, raise_errors=True) with pytest.raises(DeltaError) as excinfo: t3 + delta2 assert "Unable to get the item at root[2][1]" == str(excinfo.value) def test_identical_delta(self): delta = Delta({}) t1 = [1, 3] assert t1 + delta == t1 flat_result1 = delta.to_flat_rows() flat_expected1 = [] assert flat_expected1 == flat_result1 def test_delta_mutate(self): t1 = [1, 2] t2 = [1, 2, 3, 5] diff = DeepDiff(t1, t2) delta = Delta(diff, mutate=True) t1 + delta assert t1 == t2 @mock.patch('deepdiff.delta.logger.error') def test_list_difference_add_delta_when_index_not_valid(self, mock_logger): t1 = [1, 2] diff = {'iterable_item_added': {'root[20]': 3, 'root[3]': 5}} delta = Delta(diff, log_errors=False) assert delta + t1 == t1 # since we sort the keys by the path elements, root[3] is gonna be processed before root[20] expected_msg = ELEM_NOT_FOUND_TO_ADD_MSG.format(3, 'root[3]') delta2 = Delta(diff, bidirectional=True, raise_errors=True, log_errors=False) with pytest.raises(ValueError) as excinfo: delta2 + t1 assert expected_msg == str(excinfo.value) assert not mock_logger.called delta3 = Delta(diff, bidirectional=True, raise_errors=True, log_errors=True) with pytest.raises(ValueError) as excinfo: delta3 + t1 assert expected_msg == str(excinfo.value) mock_logger.assert_called_once_with(expected_msg) def test_list_difference3_delta(self): t1 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": [1, 2, 5]}} t2 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": [1, 3, 2, 5]}} diff = { 'values_changed': { "root[4]['b'][2]": { 'new_value': 2, 'old_value': 5 }, "root[4]['b'][1]": { 'new_value': 3, 'old_value': 2 } }, 'iterable_item_added': { "root[4]['b'][3]": 5 } } delta = Delta(diff) assert delta + t1 == t2 assert t1 + delta == t2 flat_result1 = delta.to_flat_rows() flat_expected1 = [ FlatDeltaRow(path=[4, 'b', 2], action='values_changed', value=2, old_value=5, type=int, old_type=int), FlatDeltaRow(path=[4, 'b', 1], action='values_changed', value=3, old_value=2, type=int, old_type=int), FlatDeltaRow(path=[4, 'b', 3], value=5, action='iterable_item_added', type=int), ] assert flat_expected1 == flat_result1 delta2 = Delta(diff=diff, bidirectional=True) assert t1 == t2 - delta2 def test_list_difference_delta_raises_error_if_prev_value_does_not_match(self): t1 = [1, 2, 6] t2 = [1, 3, 2, 5] diff = { 'values_changed': { "root[2]": { 'new_value': 2, 'old_value': 5 }, "root[1]": { 'new_value': 3, 'old_value': 2 } }, 'iterable_item_added': { "root[3]": 5 } } expected_msg = VERIFICATION_MSG.format('root[2]', 5, 6, VERIFY_BIDIRECTIONAL_MSG) delta = Delta(diff, bidirectional=True, raise_errors=True) with pytest.raises(ValueError) as excinfo: delta + t1 assert expected_msg == str(excinfo.value) delta2 = Delta(diff, bidirectional=False) assert delta2 + t1 == t2 flat_result2 = delta2.to_flat_rows() flat_expected2 = [ FlatDeltaRow(path=[2], action='values_changed', value=2, old_value=5, type=int, old_type=int), FlatDeltaRow(path=[1], action='values_changed', value=3, old_value=2, type=int, old_type=int), FlatDeltaRow(path=[3], value=5, action='iterable_item_added', type=int), ] assert flat_expected2 == flat_result2 def test_list_difference_delta1(self): t1 = { 1: 1, 2: 2, 3: 3, 4: { "a": "hello", "b": [1, 2, 'to_be_removed', 'to_be_removed2'] } } t2 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": [1, 2]}} diff = { 'iterable_item_removed': { "root[4]['b'][2]": "to_be_removed", "root[4]['b'][3]": 'to_be_removed2' } } delta = Delta(diff) assert delta + t1 == t2 flat_result = delta.to_flat_rows() flat_expected = [ FlatDeltaRow(path=[4, 'b', 2], value='to_be_removed', action='iterable_item_removed', type=str), FlatDeltaRow(path=[4, 'b', 3], value='to_be_removed2', action='iterable_item_removed', type=str), ] assert flat_expected == flat_result delta2 = Delta(diff=diff, bidirectional=True) assert t1 == t2 - delta2 @mock.patch('deepdiff.delta.logger.error') def test_list_difference_delta_if_item_is_already_removed(self, mock_logger): t1 = [1, 2, 'to_be_removed'] t2 = [1, 2] diff = { 'iterable_item_removed': { "root[2]": "to_be_removed", "root[3]": 'to_be_removed2' } } delta = Delta(diff, bidirectional=True, raise_errors=True) assert delta + t1 == t2, ( "We used to throw errors when the item to be removed was not found. " "Instead, we try to look for the item to be removed even when the " "index of it in delta is different than the index of it in the object." ) delta2 = Delta(diff, bidirectional=False, raise_errors=False) assert t1 + delta2 == t2 expected_msg = UNABLE_TO_GET_PATH_MSG.format('root[3]') assert 0 == mock_logger.call_count def test_list_difference_delta_does_not_raise_error_if_prev_value_changed(self): t1 = { 1: 1, 2: 2, 3: 3, 4: { "a": "hello", "b": [1, 2, 'wrong', 'to_be_removed2'] } } t2 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": [1, 2]}} diff = { 'iterable_item_removed': { "root[4]['b'][2]": "to_be_removed", "root[4]['b'][3]": 'to_be_removed2' } } # The previous behavior was to throw an error here because the original value for "root[4]['b'][2]" was not 'wrong' anymore. # However, I decided to change that behavior to what makes more sense and is consistent with the bidirectional flag. # No more verify_symmetry flag. delta = Delta(diff, bidirectional=True, raise_errors=True) assert delta + t1 != t2 expected = {1: 1, 2: 2, 3: 3, 4: {'a': 'hello', 'b': [1, 2, 'wrong']}} assert expected == delta + t1 delta2 = Delta(diff, bidirectional=False, raise_errors=True) assert expected == t1 + delta2 def test_delta_dict_items_added_retain_order(self): t1 = { 6: 6 } t2 = { 6: 6, 7: 7, 3: 3, 5: 5, 2: 2, 4: 4 } expected_delta_dict = { 'dictionary_item_added': { 'root[7]': 7, 'root[3]': 3, 'root[5]': 5, 'root[2]': 2, 'root[4]': 4 } } diff = DeepDiff(t1, t2, threshold_to_diff_deeper=0) delta_dict = diff._to_delta_dict() assert expected_delta_dict == delta_dict delta = Delta(diff, bidirectional=False, raise_errors=True) result = t1 + delta assert result == t2 assert set(result.keys()) == {6, 7, 3, 5, 2, 4} assert set(result.keys()) == set(t2.keys()) delta2 = Delta(diff=diff, bidirectional=True) assert t1 == t2 - delta2 delta3 = Delta(diff, always_include_values=True, bidirectional=True, raise_errors=True) flat_rows_list = delta3.to_flat_rows() delta4 = Delta(flat_rows_list=flat_rows_list, always_include_values=True, bidirectional=True, raise_errors=True) assert t1 == t2 - delta4 assert t1 + delta4 == t2 def test_delta_constr_flat_dict_list_param_preserve(self): """ Issue: https://github.com/seperman/deepdiff/issues/457 Scenario: We found that when a flat_rows_list was provided as a constructor parameter for instantiating a new delta, the provided flat_rows_list is unexpectedly being mutated/changed, which can be troublesome for the caller if they were expecting the flat_rows_list to be used BY COPY rather than BY REFERENCE. Intent: Preserve the original value of the flat_rows_list variable within the calling module/function after instantiating the new delta. """ t1 = { "individualNames": [ { "firstName": "Johnathan", "lastName": "Doe", "prefix": "COLONEL", "middleName": "A", "primaryIndicator": True, "professionalDesignation": "PHD", "suffix": "SR", "nameIdentifier": "00001" }, { "firstName": "John", "lastName": "Doe", "prefix": "", "middleName": "", "primaryIndicator": False, "professionalDesignation": "", "suffix": "SR", "nameIdentifier": "00002" } ] } t2 = { "individualNames": [ { "firstName": "Johnathan", "lastName": "Doe", "prefix": "COLONEL", "middleName": "A", "primaryIndicator": True, "professionalDesignation": "PHD", "suffix": "SR", "nameIdentifier": "00001" }, { "firstName": "Johnny", "lastName": "Doe", "prefix": "", "middleName": "A", "primaryIndicator": False, "professionalDesignation": "", "suffix": "SR", "nameIdentifier": "00003" } ] } def compare_func(item1, item2, level=None): print("*** inside compare ***") it1_keys = item1.keys() try: # --- individualNames --- if 'nameIdentifier' in it1_keys and 'lastName' in it1_keys: match_result = item1['nameIdentifier'] == item2['nameIdentifier'] print("individualNames - matching result:", match_result) return match_result else: print("Unknown list item...", "matching result:", item1 == item2) return item1 == item2 except Exception: raise CannotCompare() from None # ---------------------------- End of nested function # This diff should show: # 1 - list item (with an index on the path) being added # 1 - list item (with an index on the path) being removed diff = DeepDiff(t1, t2, report_repetition=True, ignore_order=True, iterable_compare_func=compare_func, cutoff_intersection_for_pairs=1) # Now create a flat_rows_list from a delta instantiated from the diff... temp_delta = Delta(diff, always_include_values=True, bidirectional=True, raise_errors=True) flat_rows_list = temp_delta.to_flat_rows() # Note: the list index is provided on the path value... assert flat_rows_list == [FlatDeltaRow(path=['individualNames', 1], value={'firstName': 'Johnny', 'lastName': 'Doe', 'prefix': '', 'middleName': 'A', 'primaryIndicator': False, 'professionalDesignation': '', 'suffix': 'SR', 'nameIdentifier': '00003'}, action='unordered_iterable_item_added', type=dict), FlatDeltaRow(path=['individualNames', 1], value={'firstName': 'John', 'lastName': 'Doe', 'prefix': '', 'middleName': '', 'primaryIndicator': False, 'professionalDesignation': '', 'suffix': 'SR', 'nameIdentifier': '00002'}, action='unordered_iterable_item_removed', type=dict), ] preserved_flat_dict_list = copy.deepcopy(flat_rows_list) # Use this later for assert comparison # Now use the flat_rows_list to instantiate a new delta... delta = Delta(flat_rows_list=flat_rows_list, always_include_values=True, bidirectional=True, raise_errors=True) flat_rows_list_again = delta.to_flat_rows() # if the flat_rows_list is (unexpectedly) mutated, it will be missing the list index number on the path value. old_mutated_list_missing_indexes_on_path = [FlatDeltaRow(path=['individualNames'], value={'firstName': 'Johnny', 'lastName': 'Doe', 'prefix': '', 'middleName': 'A', 'primaryIndicator': False, 'professionalDesignation': '', 'suffix': 'SR', 'nameIdentifier': '00003'}, action='unordered_iterable_item_added'), FlatDeltaRow(path=['individualNames'], value={'firstName': 'John', 'lastName': 'Doe', 'prefix': '', 'middleName': '', 'primaryIndicator': False, 'professionalDesignation': '', 'suffix': 'SR', 'nameIdentifier': '00002'}, action='unordered_iterable_item_removed')] # Verify that our fix in the delta constructor worked... assert flat_rows_list != old_mutated_list_missing_indexes_on_path assert flat_rows_list == preserved_flat_dict_list assert flat_rows_list == flat_rows_list_again def test_namedtuple_add_delta(self): class Point(NamedTuple): x: int y: int p1 = Point(1, 1) p2 = Point(1, 2) diff = DeepDiff(p1, p2) delta = Delta(diff) assert p2 == p1 + delta def test_namedtuple_frozenset_add_delta(self): class Article(NamedTuple): tags: frozenset a1 = Article(frozenset(["a" ])) a2 = Article(frozenset(["a", "b"])) diff = DeepDiff(a1, a2) delta = Delta(diff) assert a2 == a1 + delta picklalbe_obj_without_item = PicklableClass(11) del picklalbe_obj_without_item.item DELTA_CASES = { 'delta_case0': { 't1': frozenset([1, 2, 'B']), 't2': frozenset([1, 2, 'B']), 'deepdiff_kwargs': {}, 'to_delta_kwargs': {}, 'expected_delta_dict': {}, }, 'delta_case1': { 't1': frozenset([1, 2, 'B']), 't2': frozenset([1, 2, 3, 5]), 'deepdiff_kwargs': {}, 'to_delta_kwargs': {}, 'expected_delta_dict': {'set_item_removed': {'root': {'B'}}, 'set_item_added': {'root': {3, 5}}}, }, 'delta_case2': { 't1': [1, 2, 'B'], 't2': [1, 2, 3, 5], 'deepdiff_kwargs': {}, 'to_delta_kwargs': {}, 'expected_delta_dict': { 'type_changes': { 'root[2]': { 'old_type': str, 'new_type': int, 'new_value': 3 } }, 'iterable_item_added': { 'root[3]': 5 } }, }, 'delta_case3': { 't1': [1, 2, '3'], 't2': [1, 2, 3], 'deepdiff_kwargs': {}, 'to_delta_kwargs': {}, 'expected_delta_dict': { 'type_changes': { 'root[2]': { 'old_type': str, 'new_type': int, } } }, }, 'delta_case4': { 't1': 3, 't2': '3', 'deepdiff_kwargs': {}, 'to_delta_kwargs': {}, 'expected_delta_dict': { 'type_changes': { 'root': { 'old_type': int, 'new_type': str, } } }, }, 'delta_case5': { 't1': 3.2, 't2': Decimal('3.2'), 'deepdiff_kwargs': {}, 'to_delta_kwargs': {}, 'expected_delta_dict': { 'type_changes': { 'root': { 'old_type': float, 'new_type': Decimal, 'new_value': Decimal('3.2') } } }, }, 'delta_case6': { 't1': (1, 2), 't2': (1, 3), 'deepdiff_kwargs': {}, 'to_delta_kwargs': {}, 'expected_delta_dict': { 'values_changed': { 'root[1]': { 'new_value': 3 } } }, }, 'delta_case7': { 't1': (1, 2, 5), 't2': (1, ), 'deepdiff_kwargs': {}, 'to_delta_kwargs': {}, 'expected_delta_dict': { 'iterable_item_removed': { 'root[1]': 2, 'root[2]': 5 } }, }, 'delta_case8': { 't1': (1, 2, 5), 't2': (1, 3), 'deepdiff_kwargs': {}, 'to_delta_kwargs': {}, 'expected_delta_dict': { 'iterable_item_removed': { 'root[2]': 5 }, 'values_changed': { 'root[1]': { 'new_value': 3 } } }, }, 'delta_case9': { 't1': (1, ), 't2': (1, 3), 'deepdiff_kwargs': {}, 'to_delta_kwargs': {}, 'expected_delta_dict': { 'iterable_item_added': { 'root[1]': 3 }, }, }, 'delta_case10': { 't1': { 2: 2 }, 't2': { 2: 2, 3: 3 }, 'deepdiff_kwargs': {}, 'to_delta_kwargs': {}, 'expected_delta_dict': { 'dictionary_item_added': { 'root[3]': 3 }, }, }, 'delta_case11': { 't1': { 1: 1, 2: 2 }, 't2': { 2: 2, 3: 3 }, 'deepdiff_kwargs': {}, 'to_delta_kwargs': {}, 'expected_delta_dict': { 'dictionary_item_added': { 'root[3]': 3 }, 'dictionary_item_removed': { 'root[1]': 1 }, }, }, 'delta_case12': { 't1': PicklableClass(10), 't2': PicklableClass(11), 'deepdiff_kwargs': {}, 'to_delta_kwargs': {}, 'expected_delta_dict': { 'values_changed': { 'root.item': { 'new_value': 11 } } } }, 'delta_case13': { 't1': PicklableClass(10), 't2': picklalbe_obj_without_item, 'deepdiff_kwargs': {}, 'to_delta_kwargs': {}, 'expected_delta_dict': { 'attribute_removed': { 'root.item': 10 } } }, 'delta_case14': { 't1': picklalbe_obj_without_item, 't2': PicklableClass(10), 'deepdiff_kwargs': {}, 'to_delta_kwargs': {}, 'expected_delta_dict': { 'attribute_added': { 'root.item': 10 } } }, 'delta_case14b_threshold_to_diff_deeper': { 't1': picklalbe_obj_without_item, 't2': PicklableClass(11), 'deepdiff_kwargs': {'threshold_to_diff_deeper': 0.5}, 'to_delta_kwargs': {}, 'expected_delta_dict': {'attribute_added': {'root.item': 11}} }, 'delta_case15_diffing_simple_numbers': { 't1': 1, 't2': 2, 'deepdiff_kwargs': {}, 'to_delta_kwargs': {}, 'expected_delta_dict': {'values_changed': {'root': {'new_value': 2}}} }, 'delta_case16_diffmultiline_text': { 't1': {1: 1, 2: 2, 3: 3, 4: {'a': 'hello', 'b': 'world\n1\n2\nEnd'}}, 't2': {1: 1, 2: 2, 3: 3, 4: {'a': 'hello', 'b': 'world!\nGoodbye!\n1\n2\nEnd'}}, 'deepdiff_kwargs': {}, 'to_delta_kwargs': {}, 'expected_delta_dict': {'values_changed': {"root[4]['b']": {'new_value': 'world!\nGoodbye!\n1\n2\nEnd'}}} }, 'delta_case17_numbers_and_letters': { 't1': [0, 1, 2, 3, 4, 5, 6, 7, 8], 't2': [0, 1, 2, 3, 4, 5, 6, 7, 8, 'a', 'b', 'c'], 'deepdiff_kwargs': {}, 'to_delta_kwargs': {}, 'expected_delta_dict': {'iterable_item_added': {'root[9]': 'a', 'root[10]': 'b', 'root[11]': 'c'}} }, 'delta_case18_numbers_and_letters': { 't1': [0, 1, 2, 3, 4, 5, 6, 7, 8, 'a', 'b', 'c'], 't2': [0, 1, 2, 3, 4, 5, 6, 7, 8], 'deepdiff_kwargs': {}, 'to_delta_kwargs': {}, 'expected_delta_dict': {'iterable_item_removed': {'root[9]': 'a', 'root[10]': 'b', 'root[11]': 'c'}} }, 'delta_case19_value_removed_from_the_middle_of_list': { 't1': [0, 1, 2, 3, 4, 5, 6, 7, 8, 'a', 'b', 'c'], 't2': [0, 1, 2, 3, 5, 6, 7, 8, 'a', 'b', 'c'], 'deepdiff_kwargs': {}, 'to_delta_kwargs': {'directed': True}, 'expected_delta_dict': {'iterable_item_removed': {'root[4]': 4}} }, 'delta_case20_quotes_in_path': { 't1': {"a']['b']['c": 1}, 't2': {"a']['b']['c": 2}, 'deepdiff_kwargs': {}, 'to_delta_kwargs': {'directed': True}, 'expected_delta_dict': {'values_changed': {'root["a\'][\'b\'][\'c"]': {'new_value': 2}}} }, 'delta_case21_empty_list_add': { 't1': {'car_model': [], 'car_model_version_id': 0}, 't2': {'car_model': ['Super Duty F-250'], 'car_model_version_id': 1}, 'deepdiff_kwargs': {}, 'to_delta_kwargs': {'directed': True}, 'expected_delta_dict': {'iterable_item_added': {"root['car_model'][0]": 'Super Duty F-250'}, 'values_changed': {"root['car_model_version_id']": {'new_value': 1}}}, }, } DELTA_CASES_PARAMS = parameterize_cases('test_name, t1, t2, deepdiff_kwargs, to_delta_kwargs, expected_delta_dict', DELTA_CASES) class TestDelta: @pytest.mark.parametrize(**DELTA_CASES_PARAMS) def test_delta_cases(self, test_name, t1, t2, deepdiff_kwargs, to_delta_kwargs, expected_delta_dict): diff = DeepDiff(t1, t2, **deepdiff_kwargs) delta_dict = diff._to_delta_dict(**to_delta_kwargs) assert expected_delta_dict == delta_dict, f"test_delta_cases {test_name} failed." delta = Delta(diff, bidirectional=False, raise_errors=True) assert t1 + delta == t2, f"test_delta_cases {test_name} failed." delta2 = Delta(diff, bidirectional=True, raise_errors=True) assert t2 - delta2 == t1, f"test_delta_cases {test_name} failed." DELTA_IGNORE_ORDER_CASES = { 'delta_ignore_order_case1': { 't1': [1, 2, 'B', 3], 't2': [1, 2, 3, 5], 'deepdiff_kwargs': { 'ignore_order': True, 'report_repetition': True }, 'to_delta_kwargs': {}, 'expected_delta_dict': { 'iterable_items_added_at_indexes': { 'root': { 3: 5 } }, 'iterable_items_removed_at_indexes': { 'root': { 2: 'B' } } }, 'expected_t1_plus_delta': 't2', }, 'delta_ignore_order_case2': { 't1': [1, 2, 'B', 3, 'B', 'B', 4], 't2': [1, 2, 3, 5], 'deepdiff_kwargs': { 'ignore_order': True, 'report_repetition': True }, 'to_delta_kwargs': {}, 'expected_delta_dict': { 'values_changed': { 'root[6]': { 'new_value': 5, 'new_path': 'root[3]', }, }, 'iterable_items_removed_at_indexes': { 'root': { 2: 'B', 4: 'B', 5: 'B' } } }, 'expected_t1_plus_delta': 't2', }, 'delta_ignore_order_case_reverse2': { 't1': [1, 2, 3, 5], 't2': [1, 2, 'B', 3, 'B', 'B', 4], 'deepdiff_kwargs': { 'ignore_order': True, 'report_repetition': True }, 'to_delta_kwargs': {}, 'expected_delta_dict': { 'values_changed': { 'root[3]': { 'new_value': 4, 'new_path': 'root[6]', }, }, 'iterable_items_added_at_indexes': { 'root': { 2: 'B', 4: 'B', 5: 'B' } } }, 'expected_t1_plus_delta': 't2', }, 'delta_ignore_order_case3': { 't1': [5, 1, 1, 1, 6], 't2': [7, 1, 1, 1, 8], 'deepdiff_kwargs': { 'ignore_order': True, 'report_repetition': True }, 'to_delta_kwargs': {}, 'expected_delta_dict': { 'values_changed': { 'root[4]': { 'new_value': 7, 'new_path': 'root[0]' }, 'root[0]': { 'new_value': 8, 'new_path': 'root[4]' } } }, 'expected_t1_plus_delta': [8, 1, 1, 1, 7], }, 'delta_ignore_order_case4': { 't1': [5, 1, 3, 1, 4, 4, 6], 't2': [7, 4, 4, 1, 3, 4, 8], 'deepdiff_kwargs': { 'ignore_order': True, 'report_repetition': True }, 'to_delta_kwargs': {}, 'expected_delta_dict': { 'values_changed': { 'root[6]': { 'new_value': 7, 'new_path': 'root[0]' }, 'root[0]': { 'new_value': 8, 'new_path': 'root[6]' } }, 'iterable_items_added_at_indexes': { 'root': { 1: 4, 2: 4, 5: 4, 3: 1, } } }, 'expected_t1_plus_delta': [8, 4, 4, 1, 3, 4, 7], }, 'delta_ignore_order_case5': { 't1': (5, 1, 3, 1, 4, 4, 6), 't2': (7, 4, 4, 1, 3, 4, 8, 1), 'deepdiff_kwargs': { 'ignore_order': True, 'report_repetition': True }, 'to_delta_kwargs': {}, 'expected_delta_dict': { 'iterable_items_added_at_indexes': { 'root': { 1: 4, 2: 4, 5: 4 } }, 'values_changed': { 'root[6]': { 'new_value': 7, 'new_path': 'root[0]', }, 'root[0]': { 'new_value': 8, 'new_path': 'root[6]', } } }, 'expected_t1_plus_delta': (8, 4, 4, 1, 3, 4, 1, 7), }, 'delta_ignore_order_case6': { 't1': [{1, 2, 3}, {4, 5}], 't2': [{4, 5, 6}, {1, 2, 3}], 'deepdiff_kwargs': { 'ignore_order': True, 'report_repetition': True }, 'to_delta_kwargs': {}, 'expected_delta_dict': {'set_item_added': {'root[1]': {6}}}, 'expected_t1_plus_delta': [{1, 2, 3}, {4, 5, 6}], }, 'delta_ignore_order_case7': { 't1': [{1, 2, 3}, {4, 5, 'hello', 'right!'}, {4, 5, (2, 4, 7)}], 't2': [{4, 5, 6, (2, )}, {1, 2, 3}, {5, 'hello', 'right!'}], 'deepdiff_kwargs': { 'ignore_order': True, 'report_repetition': True }, 'to_delta_kwargs': {}, 'expected_delta_dict': { 'set_item_removed': { 'root[1]': {4} }, 'iterable_items_added_at_indexes': { 'root': { 0: {(2, ), 4, 5, 6} } }, 'iterable_items_removed_at_indexes': { 'root': { 2: {4, 5, (2, 4, 7)} } } }, 'expected_t1_plus_delta': 't2', }, 'delta_ignore_order_case8_multi_dimensional_list': { 't1': [[1, 2, 3, 4], [4, 2, 2, 1]], 't2': [[4, 1, 1, 1], [1, 3, 2, 4]], 'deepdiff_kwargs': { 'ignore_order': True, 'report_repetition': True }, 'to_delta_kwargs': {}, 'expected_delta_dict': { 'iterable_items_added_at_indexes': { 'root[1]': { 1: 1, 2: 1, 3: 1 } }, 'iterable_items_removed_at_indexes': { 'root[1]': { 1: 2, 2: 2 } } }, 'expected_t1_plus_delta': [[1, 2, 3, 4], [4, 1, 1, 1]], }, 'delta_ignore_order_case9': { 't1': [{ "path": ["interface1", "ipv1"] }, { "path": ["interface2", "ipv2"] }, { "path": ["interface3", "ipv3"] }, { "path": [{ "test0": "interface4.0", "test0.0": "ipv4.0" }, { "test1": "interface4.1", "test1.1": "ipv4.1" }] }, { "path": ["interface5", "ipv5"] }], 't2': [{ "path": ["interface1", "ipv1"] }, { "path": ["interface3", "ipv3"] }, { "path": [{ "test0": "interface4.0", "test0.0": "ipv4.0" }, { "test2": "interface4.2", "test2.2": "ipv4.0" }, { "test1": "interface4.1", "test1.1": "ipv4.1" }] }, { "path": ["interface6", "ipv6"] }, { "path": ["interface5", "ipv5"] }], 'deepdiff_kwargs': { 'ignore_order': True, 'report_repetition': True }, 'to_delta_kwargs': {}, 'expected_delta_dict': { 'iterable_items_added_at_indexes': { "root[3]['path']": { 1: { 'test2': 'interface4.2', 'test2.2': 'ipv4.0' } }, 'root': { 3: { 'path': [ 'interface6', 'ipv6' ] } } }, 'iterable_items_removed_at_indexes': { 'root': { 1: { 'path': ['interface2', 'ipv2'] } } } }, 'expected_t1_plus_delta': 't2', }, } DELTA_IGNORE_ORDER_CASES_PARAMS = parameterize_cases( 'test_name, t1, t2, deepdiff_kwargs, to_delta_kwargs, expected_delta_dict, expected_t1_plus_delta', DELTA_IGNORE_ORDER_CASES) class TestIgnoreOrderDelta: @pytest.mark.parametrize(**DELTA_IGNORE_ORDER_CASES_PARAMS) def test_ignore_order_delta_cases( self, test_name, t1, t2, deepdiff_kwargs, to_delta_kwargs, expected_delta_dict, expected_t1_plus_delta, request): # test_name = request.node.callspec.id diff = DeepDiff(t1, t2, **deepdiff_kwargs) delta_dict = diff._to_delta_dict(**to_delta_kwargs) assert expected_delta_dict == delta_dict, f"test_ignore_order_delta_cases {test_name} failed" delta = Delta(diff, bidirectional=False, raise_errors=True) expected_t1_plus_delta = t2 if expected_t1_plus_delta == 't2' else expected_t1_plus_delta t1_plus_delta = t1 + delta assert t1 + delta == t1_plus_delta, f"test_ignore_order_delta_cases {test_name} 'asserting that delta is not mutated once it is applied' failed" # assert not DeepDiff(t1_plus_delta, expected_t1_plus_delta, ignore_order=True), f"test_ignore_order_delta_cases {test_name} failed: diff = {DeepDiff(t1_plus_delta, expected_t1_plus_delta, ignore_order=True)}" DELTA_NUMPY_TEST_CASES = { 'delta_case15_similar_to_delta_numpy': { 't1': [1, 2, 3], 't2': [1, 2, 5], 'deepdiff_kwargs': {}, 'to_delta_kwargs': {}, 'expected_delta_dict': {'values_changed': {'root[2]': {'new_value': 5}}}, 'expected_result': 't2' }, 'delta_numpy1_operator_override': { 't1': np.array([1, 2, 3], np.int8), 't2': np.array([1, 2, 5], np.int8), 'deepdiff_kwargs': {}, 'to_delta_kwargs': {}, 'expected_delta_dict': {'values_changed': {'root[2]': {'new_value': 5}}, '_numpy_paths': {'root': 'int8'}}, 'expected_result': DeltaNumpyOperatorOverrideError }, 'delta_numpy2': { 't1': np.array([1, 2, 3], np.int8), 't2': np.array([1, 2, 5], np.int8), 'deepdiff_kwargs': {}, 'to_delta_kwargs': {}, 'expected_delta_dict': {'values_changed': {'root[2]': {'new_value': 5}}, '_numpy_paths': {'root': 'int8'}}, 'expected_result': 't2' }, 'delta_numpy3_type_change_but_no_value_change': { 't1': np.array([1, 2, 3], np.int8), 't2': np.array([1, 2, 3], np.int16), 'deepdiff_kwargs': {}, 'to_delta_kwargs': {}, 'expected_delta_dict': {'type_changes': {'root': {'old_type': np.int8, 'new_type': np.int16}}}, 'expected_result': 't2' }, 'delta_numpy4_type_change_plus_value_change': { 't1': np.array([1, 2, 3], np.int8), 't2': np.array([1, 2, 5], np.int16), 'deepdiff_kwargs': {}, 'to_delta_kwargs': {}, 'expected_delta_dict': None, # Not easy to compare since it throws: # ValueError: The truth value of an array with more than one element is ambiguous. # And we don't want to use DeepDiff for testing the equality inside deepdiff tests themselves! 'expected_result': 't2' }, 'delta_numpy4_type_change_ignore_numeric_type_changes': { 't1': np.array([1, 2, 3], np.int8), 't2': np.array([1, 2, 5], np.int16), 'deepdiff_kwargs': { 'ignore_numeric_type_changes': True }, 'to_delta_kwargs': {}, 'expected_delta_dict': { 'values_changed': { 'root[2]': { 'new_value': 5 } }, '_numpy_paths': { 'root': 'int16' } }, 'expected_result': 't2' }, 'delta_numpy5_multi_dimensional': { 't1': np.array([[1, 2, 3], [4, 2, 2]], np.int8), 't2': np.array([[1, 2, 5], [4, 1, 2]], np.int8), 'deepdiff_kwargs': {}, 'to_delta_kwargs': {}, 'expected_delta_dict': { 'values_changed': { 'root[0][2]': { 'new_value': 5 }, 'root[1][1]': { 'new_value': 1 } }, '_numpy_paths': { 'root': 'int8' } }, 'expected_result': 't2' }, 'delta_numpy6_multi_dimensional_ignore_order': { 't1': np.array([[1, 2, 3, 4], [4, 2, 2, 1]], np.int8), 't2': np.array([[4, 1, 1, 1], [1, 3, 2, 4]], np.int8), 'deepdiff_kwargs': { 'ignore_order': True, 'report_repetition': True }, 'to_delta_kwargs': {}, 'expected_delta_dict': { 'iterable_items_added_at_indexes': { 'root[1]': { 1: 1, 2: 1, 3: 1 } }, 'iterable_items_removed_at_indexes': { 'root[1]': { 1: 2, 2: 2 } }, '_numpy_paths': { 'root': 'int8' } }, 'expected_result': 't2_via_deepdiff' }, 'delta_numpy7_arrays_of_different_sizes': { 't1': np.array([1, 2, 3, 4]), 't2': np.array([5, 6, 7, 8, 9, 10]), 'deepdiff_kwargs': {}, 'to_delta_kwargs': {}, 'expected_delta_dict': { 'values_changed': { 'root[0]': { 'new_value': 5 }, 'root[1]': { 'new_value': 6 }, 'root[2]': { 'new_value': 7 }, 'root[3]': { 'new_value': 8 } }, 'iterable_item_added': { 'root[4]': 9, 'root[5]': 10 }, '_numpy_paths': { 'root': np.where((sys.maxsize > 2**32), 'int64', 'int32') } }, 'expected_result': 't2' }, 'delta_with_null_as_key': { 't1': { None: [1, 2], 'foo': [1, 2] }, 't2': { None: [1], 'foo': [1] }, 'deepdiff_kwargs': {}, 'to_delta_kwargs': {}, 'expected_delta_dict': { }, 'expected_result': 't2' }, } DELTA_NUMPY_TEST_PARAMS = parameterize_cases( 'test_name, t1, t2, deepdiff_kwargs, to_delta_kwargs, expected_delta_dict, expected_result', DELTA_NUMPY_TEST_CASES) class TestNumpyDelta: @pytest.mark.parametrize(**DELTA_NUMPY_TEST_PARAMS) def test_numpy_delta_cases(self, test_name, t1, t2, deepdiff_kwargs, to_delta_kwargs, expected_delta_dict, expected_result): diff = DeepDiff(t1, t2, **deepdiff_kwargs) delta_dict = diff._to_delta_dict(**to_delta_kwargs) if expected_delta_dict: assert expected_delta_dict == delta_dict, f"test_numpy_delta_cases {test_name} failed." delta = Delta(diff, bidirectional=False, raise_errors=True) if expected_result == 't2': result = delta + t1 assert np.array_equal(result, t2), f"test_numpy_delta_cases {test_name} failed." elif expected_result == 't2_via_deepdiff': result = delta + t1 diff = DeepDiff(result, t2, ignore_order=True, report_repetition=True) assert not diff, f"test_numpy_delta_cases {test_name} failed." elif expected_result is DeltaNumpyOperatorOverrideError: with pytest.raises(DeltaNumpyOperatorOverrideError): t1 + delta else: result = delta + t1 assert np.array_equal(result, expected_result), f"test_numpy_delta_cases {test_name} failed." def test_invalid_numpy_type(self): t1 = np.array([1, 2, 3], np.int8) delta_dict = {'iterable_item_added': {'root[2]': 5}, '_numpy_paths': {'root': 'int11'}} with pytest.raises(DeltaError) as excinfo: Delta(delta_dict, raise_errors=True) + t1 expected_msg = NOT_VALID_NUMPY_TYPE.format("'int11'") assert expected_msg == str(excinfo.value) class TestDeltaOther: def test_list_ignore_order_various_deltas1(self): t1 = [5, 1, 3, 1, 4, 4, 6] t2 = [7, 4, 4, 1, 3, 4, 8] delta_dict1 = {'iterable_items_added_at_indexes': {'root': {0: 7, 6: 8, 1: 4, 2: 4, 5: 4, 3: 1}}, 'iterable_items_removed_at_indexes': {'root': {0: 5, 6: 6}}} delta_dict2 = {'iterable_items_added_at_indexes': {'root': {1: 4, 2: 4, 5: 4, 3: 1}}, 'values_changed': {'root[6]': {'new_value': 7}, 'root[0]': {'new_value': 8}}} delta1 = Delta(delta_dict1) t1_plus_delta1 = t1 + delta1 assert t1_plus_delta1 == t2 delta2 = Delta(delta_dict2) t1_plus_delta2 = t1 + delta2 assert t1_plus_delta2 == [8, 4, 4, 1, 3, 4, 7] def test_list_ignore_order_various_deltas2(self): t1 = (5, 1, 3, 1, 4, 4, 6) t2 = (7, 4, 4, 1, 3, 4, 8, 1) delta_dict1 = {'iterable_items_added_at_indexes': {'root': {0: 7, 6: 8, 1: 4, 2: 4, 5: 4}}, 'iterable_items_removed_at_indexes': {'root': {6: 6, 0: 5}}} delta_dict2 = {'iterable_items_added_at_indexes': {'root': {1: 4, 2: 4, 5: 4}}, 'values_changed': {'root[6]': {'new_value': 7}, 'root[0]': {'new_value': 8}}} delta1 = Delta(delta_dict1) t1_plus_delta1 = t1 + delta1 assert t1_plus_delta1 == t2 delta2 = Delta(delta_dict2) t1_plus_delta2 = t1 + delta2 assert t1_plus_delta2 == (8, 4, 4, 1, 3, 4, 1, 7) flat_result1 = delta1.to_flat_rows() flat_expected1 = [ {'path': [0], 'value': 7, 'action': 'unordered_iterable_item_added', 'type': int}, {'path': [6], 'value': 8, 'action': 'unordered_iterable_item_added', 'type': int}, {'path': [1], 'value': 4, 'action': 'unordered_iterable_item_added', 'type': int}, {'path': [2], 'value': 4, 'action': 'unordered_iterable_item_added', 'type': int}, {'path': [5], 'value': 4, 'action': 'unordered_iterable_item_added', 'type': int}, {'path': [6], 'value': 6, 'action': 'unordered_iterable_item_removed', 'type': int}, {'path': [0], 'value': 5, 'action': 'unordered_iterable_item_removed', 'type': int}, ] flat_expected1 = [FlatDeltaRow(**i) for i in flat_expected1] assert flat_expected1 == flat_result1 delta1_again = Delta(flat_rows_list=flat_expected1) assert t1_plus_delta1 == t1 + delta1_again assert delta1.diff == delta1_again.diff flat_result2 = delta2.to_flat_rows() flat_expected2 = [ {'path': [1], 'value': 4, 'action': 'unordered_iterable_item_added', 'type': int}, {'path': [2], 'value': 4, 'action': 'unordered_iterable_item_added', 'type': int}, {'path': [5], 'value': 4, 'action': 'unordered_iterable_item_added', 'type': int}, {'path': [6], 'action': 'values_changed', 'value': 7, 'type': int}, {'path': [0], 'action': 'values_changed', 'value': 8, 'type': int}, ] flat_expected2 = [FlatDeltaRow(**i) for i in flat_expected2] assert flat_expected2 == flat_result2 delta2_again = Delta(flat_rows_list=flat_expected2) assert delta2.diff == delta2_again.diff def test_delta_view_and_to_delta_dict_are_equal_when_parameteres_passed(self): """ This is a test that passes parameters in a dictionary instead of kwargs. Note that when parameters are passed as a dictionary, all of them even the ones that have default values need to be passed. """ t1 = [4, 2, 2, 1] t2 = [4, 1, 1, 1] _parameters = { 'ignore_order': True, 'ignore_numeric_type_changes': False, 'ignore_string_type_changes': False, 'ignore_type_in_groups': [], 'report_repetition': True, 'use_enum_value': False, 'exclude_paths': None, 'include_paths': None, 'exclude_regex_paths': None, 'exclude_types': None, 'exclude_types_tuple': None, 'ignore_type_subclasses': False, 'ignore_string_case': False, 'include_obj_callback': None, 'include_obj_callback_strict': None, 'exclude_obj_callback': None, 'exclude_obj_callback_strict': None, 'ignore_uuid_types': False, 'ignore_private_variables': True, 'ignore_nan_inequality': False, 'hasher': None, 'significant_digits': None, 'number_format_notation': 'f', 'verbose_level': 1, 'view': DELTA_VIEW, 'max_passes': 10000000, 'max_diffs': None, 'number_to_string': number_to_string, 'cache_tuning_sample_size': 500, 'cache_size': 500, 'cutoff_intersection_for_pairs': 0.6, 'group_by': None, 'ignore_order_func': lambda *args, **kwargs: True, 'custom_operators': [], 'encodings': None, 'ignore_encoding_errors': False, 'iterable_compare_func': None, 'default_timezone': datetime.timezone.utc, } expected = {'iterable_items_added_at_indexes': {'root': {1: 1, 2: 1, 3: 1}}, 'iterable_items_removed_at_indexes': {'root': {1: 2, 2: 2}}} diff1 = DeepDiff(t1, t2, _parameters=_parameters) assert expected == diff1 _parameters['view'] = TEXT_VIEW diff2 = DeepDiff(t1, t2, _parameters=_parameters) assert expected == diff2._to_delta_dict() def test_verify_symmetry_and_get_elem_and_compare_to_old_value(self): """ Test a specific case where path was a list of elements (in the form of tuples) and the item could not be found. """ delta = Delta({}, bidirectional=True, raise_errors=True, log_errors=False) with pytest.raises(DeltaError) as excinfo: delta._get_elem_and_compare_to_old_value( obj={}, path_for_err_reporting=(('root', GETATTR),), expected_old_value='Expected Value', action=GET, elem='key') assert VERIFICATION_MSG.format('root', 'Expected Value', 'not found', "'key'") == str(excinfo.value) def test_apply_delta_to_incompatible_object1(self): t1 = {1: {2: [4, 5]}} t2 = {1: {2: [4]}, 0: 'new'} diff = DeepDiff(t1, t2) delta = Delta(diff, raise_errors=True) t3 = [] with pytest.raises(DeltaError) as excinfo: delta + t3 assert "Unable to get the item at root[1][2][1]: list index out of range" == str(excinfo.value) assert [] == t3 def test_apply_delta_to_incompatible_object3_errors_can_be_muted(self): t1 = {1: {2: [4]}} t2 = {1: {2: [4, 6]}} t3 = [] diff = DeepDiff(t1, t2) delta2 = Delta(diff, raise_errors=False) t4 = delta2 + t3 assert [] == t4 def test_apply_delta_to_incompatible_object4_errors_can_be_muted(self): t1 = {1: {2: [4, 5]}} t2 = {1: {2: [4]}, 0: 'new'} t3 = [] diff = DeepDiff(t1, t2) # The original delta was based on a diff between 2 dictionaries. # if we turn raise_errors=False, we can try to see what portions of the delta delta2 = Delta(diff, raise_errors=False) t4 = delta2 + t3 assert ['new'] == t4 def test_apply_delta_to_incompatible_object5_no_errors_detected(self): t1 = {3: {2: [4]}} t2 = {3: {2: [4]}, 0: 'new', 1: 'newer'} diff = DeepDiff(t1, t2) t3 = [] # The original delta was based on a diff between 2 dictionaries. # if we turn raise_errors=True, and there are no errors, a delta can be applied fully to another object! delta2 = Delta(diff, raise_errors=True) t4 = delta2 + t3 assert ['new', 'newer'] == t4 def test_apply_delta_to_incompatible_object6_value_change(self): t1 = {1: {2: [4]}} t2 = {1: {2: [5]}} t3 = [] diff = DeepDiff(t1, t2) delta2 = Delta(diff, raise_errors=False) t4 = delta2 + t3 assert [] == t4 flat_result2 = delta2.to_flat_rows() flat_expected2 = [{'path': [1, 2, 0], 'action': 'values_changed', 'value': 5, 'type': int}] flat_expected2 = [FlatDeltaRow(**i) for i in flat_expected2] assert flat_expected2 == flat_result2 delta2_again = Delta(flat_rows_list=flat_expected2) assert delta2.diff == delta2_again.diff delta3 = Delta(diff, raise_errors=False, bidirectional=True) flat_result3 = delta3.to_flat_rows() flat_expected3 = [{'path': [1, 2, 0], 'action': 'values_changed', 'value': 5, 'old_value': 4, 'type': int, 'old_type': int}] flat_expected3 = [FlatDeltaRow(**i) for i in flat_expected3] assert flat_expected3 == flat_result3 delta3_again = Delta(flat_rows_list=flat_expected3) assert delta3.diff == delta3_again.diff def test_apply_delta_to_incompatible_object7_type_change(self): t1 = ['1'] t2 = [1] t3 = ['a'] diff = DeepDiff(t1, t2) delta2 = Delta(diff, raise_errors=False) t4 = delta2 + t3 assert ['a'] == t4 @mock.patch('deepdiff.delta.logger.error') def test_apply_delta_to_incompatible_object7_verify_symmetry(self, mock_logger): t1 = [1] t2 = [2] t3 = [3] diff = DeepDiff(t1, t2) delta2 = Delta(diff, raise_errors=False, bidirectional=True) t4 = delta2 + t3 assert [2] == t4 expected_msg = VERIFICATION_MSG.format('root[0]', 1, 3, VERIFY_BIDIRECTIONAL_MSG) mock_logger.assert_called_once_with(expected_msg) @mock.patch('deepdiff.delta.logger.error') def test_apply_delta_to_incompatible_object8_verify_symmetry_ignore_order(self, mock_logger): t1 = [1, 2, 'B', 3] t2 = [1, 2, 3, 5] t3 = [] diff = DeepDiff(t1, t2, ignore_order=True, report_repetition=True) delta2 = Delta(diff, raise_errors=False, bidirectional=True) t4 = delta2 + t3 assert [5] == t4 expected_msg = INDEXES_NOT_FOUND_WHEN_IGNORE_ORDER.format({3: 5}) mock_logger.assert_called_once_with(expected_msg) @mock.patch('deepdiff.delta.logger.error') def test_apply_delta_to_incompatible_object9_ignore_order_and_verify_symmetry(self, mock_logger): t1 = [1, 2, 'B'] t2 = [1, 2] t3 = [1, 2, 'C'] diff = DeepDiff(t1, t2, ignore_order=True, report_repetition=True) delta = Delta(diff, raise_errors=False, bidirectional=True) t4 = delta + t3 assert [1, 2, 'C'] == t4 expected_msg = FAIL_TO_REMOVE_ITEM_IGNORE_ORDER_MSG.format(2, 'root', 'B', 'C') mock_logger.assert_called_once_with(expected_msg) @mock.patch('deepdiff.delta.logger.error') def test_apply_delta_to_incompatible_object10_ignore_order(self, mock_logger): t1 = [1, 2, 'B'] t2 = [1, 2] t3 = [1, 2, 'C'] diff = DeepDiff(t1, t2, ignore_order=True, report_repetition=True) # when bidirectional=False, we still won't remove the item that is different # than what we expect specifically when ignore_order=True when generating the diff. # The reason is that when ignore_order=True, we can' rely too much on the index # of the item alone to delete it. We need to make sure we are deleting the correct value. # The expected behavior is exactly the same as when bidirectional=True # specifically for when ignore_order=True AND an item is removed. delta = Delta(diff, raise_errors=False, bidirectional=False) t4 = delta + t3 assert [1, 2, 'C'] == t4 expected_msg = FAIL_TO_REMOVE_ITEM_IGNORE_ORDER_MSG.format(2, 'root', 'B', 'C') mock_logger.assert_called_once_with(expected_msg) @mock.patch('deepdiff.delta.logger.error') def test_apply_delta_to_incompatible_object11_ignore_order(self, mock_logger): t1 = [[1, 2, 'B']] t2 = [[1, 2]] t3 = {} diff = DeepDiff(t1, t2, ignore_order=True, report_repetition=True) delta = Delta(diff, raise_errors=False, bidirectional=False) t4 = delta + t3 assert {} == t4 expected_msg = UNABLE_TO_GET_PATH_MSG.format('root[0][0]') mock_logger.assert_called_once_with(expected_msg) def test_delta_to_dict(self): t1 = [1, 2, 'B'] t2 = [1, 2] diff = DeepDiff(t1, t2, ignore_order=True, report_repetition=True) delta = Delta(diff, raise_errors=False, bidirectional=False) result = delta.to_dict() expected = {'iterable_items_removed_at_indexes': {'root': {2: 'B'}}} assert expected == result flat_result = delta.to_flat_rows() flat_expected = [{'action': 'unordered_iterable_item_removed', 'path': [2], 'value': 'B', 'type': str}] flat_expected = [FlatDeltaRow(**i) for i in flat_expected] assert flat_expected == flat_result delta_again = Delta(flat_rows_list=flat_expected) assert delta.diff == delta_again.diff def test_class_type_change(self): t1 = CustomClass t2 = CustomClass2 diff = DeepDiff(t1, t2, view=DELTA_VIEW) expected = {'type_changes': {'root': {'new_type': CustomClass2, 'old_type': CustomClass}}} assert expected == diff def test_numpy_type_invalid(self): t1 = np.array([[1, 2, 3], [4, 2, 2]], np.int8) diff = { 'iterable_item_added': {'root[2]': [7, 8, 9]}, 'values_changed': { 'root[0][2]': { 'new_value': 5 }, 'root[1][1]': { 'new_value': 1 } }, '_numpy_paths': { 'root': 'int88' } } delta = Delta(diff, raise_errors=True) with pytest.raises(DeltaError) as excinfo: delta + t1 assert "'int88' is not a valid numpy type." == str(excinfo.value) def test_ignore_order_but_not_report_repetition(self): t1 = [1, 2, 'B', 3] t2 = [1, 2, 3, 5] with pytest.raises(ValueError) as excinfo: Delta(DeepDiff(t1, t2, ignore_order=True)) assert DELTA_IGNORE_ORDER_NEEDS_REPETITION_REPORT == str(excinfo.value) def test_none_in_delta_object(self): t1 = {"a": None} t2 = {"a": 1} dump = Delta(DeepDiff(t1, t2)).dumps() delta = Delta(dump) assert t2 == delta + t1 flat_result = delta.to_flat_rows() flat_expected = [{'path': ['a'], 'action': 'type_changes', 'value': 1, 'type': int, 'old_type': type(None)}] flat_expected = [FlatDeltaRow(**i) for i in flat_expected] assert flat_expected == flat_result delta_again = Delta(flat_rows_list=flat_expected) assert delta.diff == delta_again.diff with pytest.raises(ValueError) as exc_info: delta.to_flat_rows(report_type_changes=False) assert str(exc_info.value).startswith("When converting to flat dictionaries, if report_type_changes=False and there are type") delta2 = Delta(dump, always_include_values=True) flat_result2 = delta2.to_flat_rows(report_type_changes=False) flat_expected2 = [{'path': ['a'], 'action': 'values_changed', 'value': 1}] flat_expected2 = [FlatDeltaRow(**i) for i in flat_expected2] assert flat_expected2 == flat_result2 def test_delta_set_in_objects(self): t1 = [[1, SetOrdered(['A', 'B'])], {1}] t2 = [[2, SetOrdered([10, 'C', 'B'])], {1}] delta = Delta(DeepDiff(t1, t2)) flat_result = delta.to_flat_rows() flat_expected = [ {'path': [0, 1], 'value': 10, 'action': 'set_item_added', 'type': int}, {'path': [0, 0], 'action': 'values_changed', 'value': 2, 'type': int}, {'path': [0, 1], 'value': 'A', 'action': 'set_item_removed', 'type': str}, {'path': [0, 1], 'value': 'C', 'action': 'set_item_added', 'type': str}, ] flat_expected = [FlatDeltaRow(**i) for i in flat_expected] # Sorting because otherwise the order is not deterministic for sets, # even though we are using SetOrdered here. It still is converted to set at some point and loses its order. flat_result.sort(key=lambda x: str(x.value)) assert flat_expected == flat_result delta_again = Delta(flat_rows_list=flat_expected) assert delta.diff == delta_again.diff def test_delta_array_of_bytes(self): t1 = [] t2 = [b"hello"] diff = DeepDiff(t1, t2) expected_diff = {'iterable_item_added': {'root[0]': b'hello'}} assert expected_diff == diff delta = Delta(diff) flat_result = delta.to_flat_rows() flat_expected = [FlatDeltaRow(path=[0], action=FlatDataAction.iterable_item_added, value=b'hello', type=bytes)] assert flat_expected == flat_result delta_again = Delta(flat_rows_list=flat_expected) assert delta.diff == delta_again.diff assert t1 + delta_again == t2 def test_delta_with_json_serializer(self): t1 = {"a": 1} t2 = {"a": 2} diff = DeepDiff(t1, t2) delta = Delta(diff, serializer=json.dumps) dump = delta.dumps() delta_reloaded = Delta(dump, deserializer=json.loads) assert t2 == delta_reloaded + t1 the_file = io.StringIO() delta.dump(the_file) the_file.seek(0) delta_reloaded_again = Delta(delta_file=the_file, deserializer=json.loads) assert t2 == delta_reloaded_again + t1 def test_brackets_in_keys(self): """ Delta calculation not correct when bracket in Json key https://github.com/seperman/deepdiff/issues/265 """ t1 = "{ \ \"test\": \"test1\" \ }" t2 = "{ \ \"test\": \"test1\", \ \"test2 [uuu]\": \"test2\" \ }" json1 = json.loads(t1) json2 = json.loads(t2) ddiff = DeepDiff(json1, json2) delta = Delta(ddiff) original_json2 = delta + json1 assert json2 == original_json2 class TestDeltaCompareFunc: @staticmethod def compare_func(x, y, level): if (not isinstance(x, dict) or not isinstance(y, dict)): raise CannotCompare if(level.path() == "root['path2']"): if (x["ID"] == y["ID"]): return True return False if("id" in x and "id" in y): if (x["id"] == y["id"]): return True return False raise CannotCompare def test_compare_func1(self, compare_func_t1, compare_func_t2, compare_func_result1): ddiff = DeepDiff( compare_func_t1, compare_func_t2, iterable_compare_func=self.compare_func, verbose_level=1) assert compare_func_result1 == ddiff delta = Delta(ddiff) recreated_t2 = compare_func_t1 + delta assert compare_func_t2 == recreated_t2 def test_compare_func_with_duplicates_removed(self): t1 = [ { 'id': 1, 'val': 1, "nested": [ {"id": 1, "val": 1}, {"id": 2, "val": 2}, ] }, { 'id': 2, 'val': 2 }, { 'id': 1, 'val': 3 }, { 'id': 3, 'val': 3 } ] t2 = [ { 'id': 3, 'val': 3 }, { 'id': 2, 'val': 2 }, { 'id': 1, 'val': 3, "nested":[ { "id": 2, "val": 3 }, ] } ] ddiff = DeepDiff(t1, t2, iterable_compare_func=self.compare_func, verbose_level=2) expected = { "iterable_item_removed": { "root[2]": { "id": 1, "val": 3 }, "root[2]['nested'][0]": { "id": 1, "val": 1 } }, "iterable_item_moved": { "root[0]": { "new_path": "root[2]", "value": { "id": 1, "val": 3, "nested": [{"id": 2, "val": 3}, ] }, }, "root[0]['nested'][1]": { "new_path": "root[2]['nested'][0]", "value": { "id": 2, "val": 3 } }, "root[3]": { "new_path": "root[0]", "value": { "id": 3, "val": 3 } } }, 'values_changed': { "root[2]['nested'][0]['val']": { 'new_path': "root[0]['nested'][1]['val']", 'new_value': 3, 'old_value': 2 }, "root[2]['val']": { 'new_value': 3, 'old_value': 1, 'new_path': "root[0]['val']" } }, } assert expected == ddiff delta = Delta(ddiff) recreated_t2 = t1 + delta assert t2 == recreated_t2 flat_result = delta.to_flat_rows() flat_expected = [ {'path': [2, 'val'], 'value': 3, 'action': 'values_changed', 'type': int, 'new_path': [0, 'val']}, {'path': [2, 'nested', 0, 'val'], 'value': 3, 'action': 'values_changed', 'type': int, 'new_path': [0, 'nested', 1, 'val']}, {'path': [2, 'nested', 0], 'value': {'id': 1, 'val': 1}, 'action': 'iterable_item_removed', 'type': dict}, {'path': [2], 'value': {'id': 1, 'val': 3}, 'action': 'iterable_item_removed', 'type': dict}, {'path': [0], 'value': {'id': 1, 'val': 3, 'nested': [{'id': 2, 'val': 3}]}, 'action': 'iterable_item_removed', 'type': dict}, {'path': [0, 'nested', 1], 'value': {'id': 2, 'val': 3}, 'action': 'iterable_item_removed', 'type': dict}, {'path': [3], 'value': {'id': 3, 'val': 3}, 'action': 'iterable_item_removed', 'type': dict}, {'path': [0], 'action': 'iterable_item_moved', 'value': {'id': 1, 'val': 3, 'nested': [{'id': 2, 'val': 3}]}, 'new_path': [2], 'type': dict}, {'path': [0, 'nested', 1], 'value': {'id': 2, 'val': 3}, 'action': 'iterable_item_moved', 'type': dict, 'new_path': [2, 'nested', 0]}, {'path': [3], 'action': 'iterable_item_moved', 'value': {'id': 3, 'val': 3}, 'new_path': [0], 'type': dict}, ] flat_expected = [FlatDeltaRow(**i) for i in flat_expected] assert flat_expected == flat_result # Delta.DEBUG = True delta_again = Delta(flat_rows_list=flat_expected, iterable_compare_func_was_used=True) expected_delta_dict = { 'iterable_item_removed': { 'root[2]': { 'id': 1, 'val': 3 }, 'root[0]': { 'id': 1, 'val': 3, 'nested': [{'id': 2, 'val': 3}] }, 'root[3]': { 'id': 3, 'val': 3 }, "root[2]['nested'][0]": { "id": 1, "val": 1 }, "root[0]['nested'][1]": { "id": 2, "val": 3 } }, 'iterable_item_moved': { 'root[0]': { 'new_path': 'root[2]', 'value': { 'id': 1, 'val': 3, 'nested': [{'id': 2, 'val': 3}] } }, "root[0]['nested'][1]": { 'new_path': "root[2]['nested'][0]", 'value': { 'id': 2, 'val': 3 } }, 'root[3]': { 'new_path': 'root[0]', 'value': { 'id': 3, 'val': 3 } } }, 'values_changed': { "root[2]['val']": { 'new_value': 3, 'new_path': "root[0]['val']", }, "root[2]['nested'][0]['val']": { 'new_path': "root[0]['nested'][1]['val']", 'new_value': 3, }, } } assert expected_delta_dict == delta_again.diff assert t2 == t1 + delta_again def test_compare_func_with_duplicates_added(self): t1 = [{'id': 3, 'val': 3}, {'id': 2, 'val': 2}, {'id': 1, 'val': 3}] t2 = [{'id': 1, 'val': 1}, {'id': 2, 'val': 2}, {'id': 1, 'val': 3}, {'id': 3, 'val': 3}] ddiff = DeepDiff(t1, t2, iterable_compare_func=self.compare_func, verbose_level=2) expected = { 'iterable_item_added': { 'root[2]': { 'id': 1, 'val': 3 } }, 'iterable_item_moved': { 'root[0]': { 'new_path': 'root[3]', 'value': { 'id': 3, 'val': 3 } }, 'root[2]': { 'new_path': 'root[0]', 'value': { 'id': 1, 'val': 1 } } }, 'values_changed': { "root[0]['val']": { 'new_value': 1, 'old_value': 3, 'new_path': "root[2]['val']" } }, } assert expected == ddiff delta = Delta(ddiff) recreated_t2 = t1 + delta assert t2 == recreated_t2 def test_compare_func_swap(self): t1 = [{'id': 1, 'val': 1}, {'id': 1, 'val': 3}] t2 = [{'id': 1, 'val': 3}, {'id': 1, 'val': 1}] ddiff = DeepDiff(t1, t2, iterable_compare_func=self.compare_func, verbose_level=2) expected = {'values_changed': {"root[0]['val']": {'new_value': 3, 'old_value': 1}, "root[1]['val']": {'new_value': 1, 'old_value': 3}}} assert expected == ddiff delta = Delta(ddiff) recreated_t2 = t1 + delta assert t2 == recreated_t2 def test_compare_func_path_specific(self): t1 = {"path1": [{'id': 1, 'val': 1}, {'id': 2, 'val': 3}], "path2": [{'ID': 4, 'val': 3}, {'ID': 3, 'val': 1}, ], "path3": [{'no_id': 5, 'val': 1}, {'no_id': 6, 'val': 3}]} t2 = {"path1": [{'id': 1, 'val': 1}, {'id': 2, 'val': 3}], "path2": [{'ID': 3, 'val': 1}, {'ID': 4, 'val': 3}], "path3": [{'no_id': 5, 'val': 1}, {'no_id': 6, 'val': 3}]} ddiff = DeepDiff(t1, t2, iterable_compare_func=self.compare_func, verbose_level=2) expected = {'iterable_item_moved': {"root['path2'][0]": {'new_path': "root['path2'][1]", 'value': {'ID': 4, 'val': 3}},"root['path2'][1]": {'new_path': "root['path2'][0]", 'value': {'ID': 3, 'val': 1}}}} assert expected == ddiff delta = Delta(ddiff) recreated_t2 = t1 + delta assert t2 == recreated_t2 def test_compare_func_nested_changes(self): t1 = { "TestTable": [ { "id": "022fb580-800e-11ea-a361-39b3dada34b5", "name": "Max", "NestedTable": [ { "id": "022fb580-800e-11ea-a361-39b3dada34a6", "NestedField": "Test Field" } ] }, { "id": "022fb580-800e-11ea-a361-12354656532", "name": "Bob", "NestedTable": [ { "id": "022fb580-800e-11ea-a361-39b3dada34c7", "NestedField": "Test Field 2" }, ] }, ] } t2 = {"TestTable": [ { "id": "022fb580-800e-11ea-a361-12354656532", "name": "Bob (Changed Name)", "NestedTable": [ { "id": "022fb580-800e-11ea-a361-39b3dada34c7", "NestedField": "Test Field 2 (Changed Nested Field)" }, { "id": "new id", "NestedField": "Test Field 3" }, { "id": "newer id", "NestedField": "Test Field 4" }, ] }, { "id": "adding_some_random_id", "name": "New Name", "NestedTable": [ { "id": "random_nested_id_added", "NestedField": "New Nested Field" }, { "id": "random_nested_id_added2", "NestedField": "New Nested Field2" }, { "id": "random_nested_id_added3", "NestedField": "New Nested Field43" }, ] } ]} ddiff = DeepDiff(t1, t2, iterable_compare_func=self.compare_func, verbose_level=2) delta = Delta(ddiff) recreated_t2 = t1 + delta assert t2 == recreated_t2 def test_compare_func_deep_nested_changes(self): t1 = { "Locations": [ { "id": "c4fa7b12-f365-42a9-9544-3efc11963558", "Items": [ { "id": "2399528f-2556-4e2c-bf9b-c8ea17bc323f" }, { "id": "2399528f-2556-4e2c-bf9b-c8ea17bc323f1", }, { "id": "2399528f-2556-4e2c-bf9b-c8ea17bc323f2" }, { "id": "2399528f-2556-4e2c-bf9b-c8ea17bc323f3" } ] }, { "id": "d9095676-bc41-4cbf-9fd2-7148bb26bcc4", "Items": [ { "id": "26b78305-df71-40c0-8e98-dcd40b7f716d" }, { "id": "3235125d-0110-4d0e-847a-24912cf73feb" }, { "id": "7699552a-add9-4338-aeb9-662bec14c175" }, { "id": "015e74f0-2c2a-45c0-a172-21758d14bf3a" } ] }, { "id": "41b38757-8984-47fd-890d-8c4ed18c3c47", "Items": [ { "id": "494e839e-37b1-4cac-b1dc-a44f3e6e7ada" }, { "id": "60547ca6-3ef0-4b67-8826-2c7b76e67011" }, { "id": "cee762a0-fbd8-48bb-ba92-be32cf3cf250" }, { "id": "7a0da2b7-c1e6-45b4-8810-fec7b4b6186d" } ] }, { "id": "c0be071a-5457-497d-9a78-ff7cb561d4d3", "Items": [ { "id": "e54dcdff-ec99-4941-92eb-c12bb3cbeb91" } ] }, { "id": "dfe4b37b-8df3-4dc6-8686-0588937fbe10", "Items": [ { "id": "27a574ae-08db-47f9-a9dc-18df59287f4d" }, { "id": "23edf031-8c4e-43d6-b5bf-4d5ee9008a36", "Containers": [ {"id": "1", "val": 1}, {"id": "2", "val": 2}, {"id": "3", "val": 3}, ] }, { "id": "e1e54643-23ee-496d-b7d2-de67c4bb7d68" }, { "id": "2f910da3-8cd0-4cf5-81c9-23668fc9477f" }, { "id": "5e36d258-2a82-49ee-b4fc-db0a8c28b404" }, { "id": "4bf2ce8d-05ed-4718-a529-8c9e4704e38f" }, ] }, ] } t2 = { "Locations": [ { "id": "41b38757-8984-47fd-890d-8c4ed18c3c47", "Items": [ { "id": "60547ca6-3ef0-4b67-8826-2c7b76e67011" }, { "id": "cee762a0-fbd8-48bb-ba92-be32cf3cf250" }, { "id": "7a0da2b7-c1e6-45b4-8810-fec7b4b6186d" } ] }, { "id": "c0be071a-5457-497d-9a78-ff7cb561d4d3", "Items": [ { "id": "e54dcdff-ec99-4941-92eb-c12bb3cbeb91" } ] }, { "id": "dfe4b37b-8df3-4dc6-8686-0588937fbe10", "Items": [ { "id": "27a574ae-08db-47f9-a9dc-18df59287f4d" }, { "id": "27a574ae-08db-47f9-a9dc-88df59287f4d" }, { "id": "23edf031-8c4e-43d6-b5bf-4d5ee9008a36", "Containers": [ {"id": "1", "val": 1}, {"id": "3", "val": 3}, {"id": "2", "val": 2}, ] }, { "id": "e1e54643-23ee-496d-b7d2-de67c4bb7d68" }, { "id": "2f910da3-8cd0-4cf5-81c9-23668fc9477f" }, { "id": "5e36d258-2a82-49ee-b4fc-db0a8c28b404" }, { "id": "4bf2ce8d-05ed-4718-a529-8c9e4704e38f" }, ] }, ] } ddiff = DeepDiff(t1, t2, iterable_compare_func=self.compare_func, verbose_level=2) delta2 = Delta(ddiff) expected_move_1 = {'new_path': "root['Locations'][2]['Items'][2]['Containers'][2]", 'value': {'id': '2', 'val': 2}} expected_move_2 = {'new_path': "root['Locations'][2]['Items'][2]['Containers'][1]", 'value': {'id': '3', 'val': 3}} assert ddiff["iterable_item_moved"]["root['Locations'][4]['Items'][1]['Containers'][1]"] == expected_move_1 assert ddiff["iterable_item_moved"]["root['Locations'][4]['Items'][1]['Containers'][2]"] == expected_move_2 recreated_t2 = t1 + delta2 assert t2 == recreated_t2 def test_delta_force1(self): t1 = { 'x': { 'y': [1, 2, 3] }, 'q': { 'r': 'abc', } } t2 = { 'x': { 'y': [1, 2, 3, 4] }, 'q': { 'r': 'abc', 't': 0.5, } } diff = DeepDiff(t1, t2) delta = Delta(diff=diff, force=True) result = {} + delta expected = {'x': {'y': {3: 4}}, 'q': {'t': 0.5}} assert expected == result def test_delta_force_fill(self): t1 = { 'x': { 'y': [{"b": "c"}, {"b": "c"}, {"b": "c"}, {"b": "c"}] }, 'q': { 'r': 'abc', } } t2 = { 'x': { 'y': [{"b": "c"}, {"b": "c"}, {"b": "c"}, {"b": "c"}, {"b": "c"}, {"b": "c"}, {"b": "c"}] }, 'q': { 'r': 'abc', 't': 0.5, } } diff = DeepDiff(t1, t2) delta = Delta(diff=diff, force=True) result = {"x": {"y": [1,]}} + delta expected = {'x': {'y': [1]}, 'q': {'t': 0.5}} assert expected == result delta = Delta(diff=diff, force=True, fill=None) result = {"x": {"y": [1,]}} + delta expected = {'x': {'y': [1, None, None, None, {"b": "c"}, {"b": "c"}, {"b": "c"}]}, 'q': {'t': 0.5}} assert expected == result def fill_func(obj, value, path): return value.copy() delta = Delta(diff=diff, force=True, fill=fill_func) result = {"x": {"y": [1,]}} + delta expected = {'x': {'y': [1, {"b": "c"}, {"b": "c"}, {"b": "c"}, {"b": "c"}, {"b": "c"}, {"b": "c"}]}, 'q': {'t': 0.5}} assert expected == result def test_flatten_dict_with_one_key_added(self): t1 = {"field1": {"joe": "Joe"}} t2 = {"field1": {"joe": "Joe Nobody"}, "field2": {"jimmy": "Jimmy"}} diff = DeepDiff(t1, t2) delta = Delta(diff=diff, always_include_values=True) flat_result = delta.to_flat_rows(report_type_changes=False) flat_expected = [ {'path': ['field2', 'jimmy'], 'value': 'Jimmy', 'action': 'dictionary_item_added'}, {'path': ['field1', 'joe'], 'action': 'values_changed', 'value': 'Joe Nobody'}, ] flat_expected = [FlatDeltaRow(**i) for i in flat_expected] assert flat_expected == flat_result delta_again = Delta(flat_rows_list=flat_expected, force=True) # We need to enable force so it creates the dictionary when added to t1 expected_data_again_diff = {'dictionary_item_added': {"root['field2']['jimmy']": 'Jimmy'}, 'values_changed': {"root['field1']['joe']": {'new_value': 'Joe Nobody'}}} assert delta.diff != delta_again.diff, "Since a dictionary containing a single field was created, the flat dict acted like one key was added." assert expected_data_again_diff == delta_again.diff, "Since a dictionary containing a single field was created, the flat dict acted like one key was added." assert t2 == t1 + delta_again def test_flatten_dict_with_multiple_keys_added(self): t1 = {"field1": {"joe": "Joe"}} t2 = {"field1": {"joe": "Joe Nobody"}, "field2": {"jimmy": "Jimmy", "sar": "Sarah"}} diff = DeepDiff(t1, t2) delta = Delta(diff=diff, always_include_values=True) flat_result = delta.to_flat_rows(report_type_changes=False) flat_expected = [ {'path': ['field2'], 'value': {'jimmy': 'Jimmy', 'sar': 'Sarah'}, 'action': 'dictionary_item_added'}, {'path': ['field1', 'joe'], 'action': 'values_changed', 'value': 'Joe Nobody'}, ] flat_expected = [FlatDeltaRow(**i) for i in flat_expected] assert flat_expected == flat_result delta_again = Delta(flat_rows_list=flat_expected) assert delta.diff == delta_again.diff def test_flatten_list_with_one_item_added(self): t1 = {"field1": {"joe": "Joe"}} t2 = {"field1": {"joe": "Joe"}, "field2": ["James"]} t3 = {"field1": {"joe": "Joe"}, "field2": ["James", "Jack"]} diff = DeepDiff(t1, t2) delta = Delta(diff=diff, always_include_values=True) flat_result = delta.to_flat_rows(report_type_changes=False) flat_expected = [{'path': ['field2', 0], 'value': 'James', 'action': 'iterable_item_added'}] flat_expected = [FlatDeltaRow(**i) for i in flat_expected] assert flat_expected == flat_result delta_again = Delta(flat_rows_list=flat_expected, force=True) assert {'iterable_item_added': {"root['field2'][0]": 'James'}} == delta_again.diff # delta_again.DEBUG = True assert t2 == t1 + delta_again diff2 = DeepDiff(t2, t3) delta2 = Delta(diff=diff2, always_include_values=True) flat_result2 = delta2.to_flat_rows(report_type_changes=False) flat_expected2 = [{'path': ['field2', 1], 'value': 'Jack', 'action': 'iterable_item_added'}] flat_expected2 = [FlatDeltaRow(**i) for i in flat_expected2] assert flat_expected2 == flat_result2 delta_again2 = Delta(flat_rows_list=flat_expected2, force=True) assert {'iterable_item_added': {"root['field2'][1]": 'Jack'}} == delta_again2.diff assert t3 == t2 + delta_again2 def test_flatten_set_with_one_item_added(self): t1 = {"field1": {"joe": "Joe"}} t2 = {"field1": {"joe": "Joe"}, "field2": {"James"}} t3 = {"field1": {"joe": "Joe"}, "field2": {"James", "Jack"}} diff = DeepDiff(t1, t2) delta = Delta(diff=diff, always_include_values=True) assert t2 == t1 + delta flat_result = delta.to_flat_rows(report_type_changes=False) flat_expected = [{'path': ['field2'], 'value': 'James', 'action': 'set_item_added'}] flat_expected = [FlatDeltaRow(**i) for i in flat_expected] assert flat_expected == flat_result delta_again = Delta(flat_rows_list=flat_expected, force=True) assert {'set_item_added': {"root['field2']": {'James'}}} == delta_again.diff assert t2 == t1 + delta_again diff = DeepDiff(t2, t3) delta2 = Delta(diff=diff, always_include_values=True) flat_result2 = delta2.to_flat_rows(report_type_changes=False) flat_expected2 = [{'path': ['field2'], 'value': 'Jack', 'action': 'set_item_added'}] flat_expected2 = [FlatDeltaRow(**i) for i in flat_expected2] assert flat_expected2 == flat_result2 delta_again2 = Delta(flat_rows_list=flat_expected2, force=True) assert {'set_item_added': {"root['field2']": {'Jack'}}} == delta_again2.diff assert t3 == t2 + delta_again2 def test_flatten_tuple_with_one_item_added(self): t1 = {"field1": {"joe": "Joe"}} t2 = {"field1": {"joe": "Joe"}, "field2": ("James", )} t3 = {"field1": {"joe": "Joe"}, "field2": ("James", "Jack")} diff = DeepDiff(t1, t2) delta = Delta(diff=diff, always_include_values=True) assert t2 == t1 + delta flat_expected = delta.to_flat_rows(report_type_changes=False) expected_result = [{'path': ['field2', 0], 'value': 'James', 'action': 'iterable_item_added'}] expected_result = [FlatDeltaRow(**i) for i in expected_result] assert expected_result == flat_expected delta_again = Delta(flat_rows_list=flat_expected, force=True) assert {'iterable_item_added': {"root['field2'][0]": 'James'}} == delta_again.diff assert {'field1': {'joe': 'Joe'}, 'field2': ['James']} == t1 + delta_again, "We lost the information about tuple when we convert to flat dict." diff = DeepDiff(t2, t3) delta2 = Delta(diff=diff, always_include_values=True, force=True) flat_result2 = delta2.to_flat_rows(report_type_changes=False) expected_result2 = [{'path': ['field2', 1], 'value': 'Jack', 'action': 'iterable_item_added'}] expected_result2 = [FlatDeltaRow(**i) for i in expected_result2] assert expected_result2 == flat_result2 assert t3 == t2 + delta2 delta_again2 = Delta(flat_rows_list=flat_result2) assert {'iterable_item_added': {"root['field2'][1]": 'Jack'}} == delta_again2.diff assert t3 == t2 + delta_again2 def test_flatten_list_with_multiple_item_added(self): t1 = {"field1": {"joe": "Joe"}} t2 = {"field1": {"joe": "Joe"}, "field2": ["James", "Jack"]} diff = DeepDiff(t1, t2) delta = Delta(diff=diff, always_include_values=True) flat_result = delta.to_flat_rows(report_type_changes=False) expected_result = [{'path': ['field2'], 'value': ['James', 'Jack'], 'action': 'dictionary_item_added'}] expected_result = [FlatDeltaRow(**i) for i in expected_result] assert expected_result == flat_result delta2 = Delta(diff=diff, bidirectional=True, always_include_values=True) flat_result2 = delta2.to_flat_rows(report_type_changes=False) assert expected_result == flat_result2 delta_again = Delta(flat_rows_list=flat_result) assert delta.diff == delta_again.diff def test_flatten_attribute_added(self): t1 = picklalbe_obj_without_item t2 = PicklableClass(10) diff = DeepDiff(t1, t2) delta = Delta(diff=diff, always_include_values=True) flat_result = delta.to_flat_rows(report_type_changes=False) expected_result = [{'path': ['item'], 'value': 10, 'action': 'attribute_added'}] expected_result = [FlatDeltaRow(**i) for i in expected_result] assert expected_result == flat_result delta_again = Delta(flat_rows_list=flat_result) assert delta.diff == delta_again.diff def test_flatten_when_simple_type_change(self): t1 = [1, 2, '3'] t2 = [1, 2, 3] diff = DeepDiff(t1, t2) expected_diff = { 'type_changes': {'root[2]': {'old_type': str, 'new_type': int, 'old_value': '3', 'new_value': 3}} } assert expected_diff == diff delta = Delta(diff=diff) with pytest.raises(ValueError) as exc_info: delta.to_flat_rows(report_type_changes=False) assert str(exc_info.value).startswith("When converting to flat dictionaries") delta2 = Delta(diff=diff, always_include_values=True) flat_result2 = delta2.to_flat_rows(report_type_changes=False) expected_result2 = [{'path': [2], 'action': 'values_changed', 'value': 3}] expected_result2 = [FlatDeltaRow(**i) for i in expected_result2] assert expected_result2 == flat_result2 delta3 = Delta(diff=diff, always_include_values=True, bidirectional=True) flat_result3 = delta3.to_flat_rows(report_type_changes=False) expected_result3 = [{'path': [2], 'action': 'values_changed', 'value': 3, 'old_value': '3'}] expected_result3 = [FlatDeltaRow(**i) for i in expected_result3] assert expected_result3 == flat_result3 delta_again = Delta(flat_rows_list=flat_result3) assert {'values_changed': {'root[2]': {'new_value': 3, 'old_value': '3'}}} == delta_again.diff def test_subtract_delta1(self): t1 = {'field_name1': ['yyy']} t2 = {'field_name1': ['xxx', 'yyy']} delta_diff = {'iterable_items_removed_at_indexes': {"root['field_name1']": {(0, 'GET'): 'xxx'}}} expected_reverse_diff = {'iterable_items_added_at_indexes': {"root['field_name1']": {(0, 'GET'): 'xxx'}}} delta = Delta(delta_diff=delta_diff, bidirectional=True) reversed_diff = delta._get_reverse_diff() assert expected_reverse_diff == reversed_diff assert t2 != {'field_name1': ['yyy', 'xxx']} == t1 - delta, "Since iterable_items_added_at_indexes is used when ignore_order=True, the order is not necessarily the original order." def test_subtract_delta_made_from_flat_dicts1(self): t1 = {'field_name1': ['xxx', 'yyy']} t2 = {'field_name1': []} diff = DeepDiff(t1, t2) delta = Delta(diff=diff, bidirectional=True) flat_rows_list = delta.to_flat_rows(include_action_in_path=False, report_type_changes=True) expected_flat_dicts = [{ 'path': ['field_name1', 0], 'value': 'xxx', 'action': 'iterable_item_removed', 'type': str, }, { 'path': ['field_name1', 1], 'value': 'yyy', 'action': 'iterable_item_removed', 'type': str, }] expected_flat_dicts = [FlatDeltaRow(**i) for i in expected_flat_dicts] assert expected_flat_dicts == flat_rows_list delta1 = Delta(flat_rows_list=flat_rows_list, bidirectional=True, force=True) assert t1 == t2 - delta1 delta2 = Delta(flat_rows_list=[flat_rows_list[0]], bidirectional=True, force=True) middle_t = t2 - delta2 assert {'field_name1': ['xxx']} == middle_t delta3 = Delta(flat_rows_list=[flat_rows_list[1]], bidirectional=True, force=True) assert t1 == middle_t - delta3 def test_subtract_delta_made_from_flat_dicts2(self): t1 = {'field_name1': []} t2 = {'field_name1': ['xxx', 'yyy']} diff = DeepDiff(t1, t2) delta = Delta(diff=diff, bidirectional=True) flat_rows_list = delta.to_flat_rows(include_action_in_path=False, report_type_changes=True) expected_flat_dicts = [{ 'path': ['field_name1', 0], 'value': 'xxx', 'action': 'iterable_item_added', 'type': str, }, { 'path': ['field_name1', 1], 'value': 'yyy', 'action': 'iterable_item_added', 'type': str, }] expected_flat_dicts = [FlatDeltaRow(**i) for i in expected_flat_dicts] assert expected_flat_dicts == flat_rows_list delta1 = Delta(flat_rows_list=flat_rows_list, bidirectional=True, force=True) assert t1 == t2 - delta1 # We need to subtract the changes in the reverse order if we want to feed the flat dict rows individually to Delta delta2 = Delta(flat_rows_list=[flat_rows_list[0]], bidirectional=True, force=True) middle_t = t2 - delta2 assert {'field_name1': ['yyy']} == middle_t delta3 = Delta(flat_rows_list=[flat_rows_list[1]], bidirectional=True, force=True) delta3.DEBUG = True assert t1 == middle_t - delta3 def test_list_of_alphabet_and_its_delta(self): l1 = "A B C D E F G D H".split() l2 = "B C X D H Y Z".split() diff = DeepDiff(l1, l2) # Problem: The index of values_changed should be either all for AFTER removals or BEFORE removals. # What we have here is that F & G transformation to Y and Z is not compatible with A and E removal # it is really meant for the removals to happen first, and then have indexes in L2 for values changing # rather than indexes in L1. Here what we need to have is: # A B C D E F G D H # A B C-X-E # B C D F G D H # removal # What we really need is to report is as it is in difflib for delta specifically: # A B C D E F G D H # B C D E F G D H delete t1[0:1] --> t2[0:0] ['A'] --> [] # B C D E F G D H equal t1[1:3] --> t2[0:2] ['B', 'C'] --> ['B', 'C'] # B C X D H replace t1[3:7] --> t2[2:3] ['D', 'E', 'F', 'G'] --> ['X'] # B C X D H equal t1[7:9] --> t2[3:5] ['D', 'H'] --> ['D', 'H'] # B C X D H Y Z insert t1[9:9] --> t2[5:7] [] --> ['Y', 'Z'] # So in this case, it needs to also include information about what stays equal in the delta # NOTE: the problem is that these operations need to be performed in a specific order. # DeepDiff removes that order and just buckets all insertions vs. replace vs. delete in their own buckets. # For times that we use Difflib, we may want to keep the information for the array_change key # just for the sake of delta, but not for reporting in deepdiff itself. # that way we can re-apply the changes as they were reported in delta. delta = Delta(diff) assert l2 == l1 + delta with pytest.raises(ValueError) as exc_info: l1 == l2 - delta assert "Please recreate the delta with bidirectional=True" == str(exc_info.value) delta2 = Delta(diff, bidirectional=True) assert l2 == l1 + delta2 assert l1 == l2 - delta2 dump = Delta(diff, bidirectional=True).dumps() delta3 = Delta(dump, bidirectional=True) assert l2 == l1 + delta3 assert l1 == l2 - delta3 dump4 = Delta(diff, bidirectional=True, serializer=json_dumps).dumps() delta4 = Delta(dump4, bidirectional=True, deserializer=json_loads) assert l2 == l1 + delta4 assert l1 == l2 - delta4 flat_rows = delta2.to_flat_rows() expected_flat_rows = [ FlatDeltaRow(path=[3], action='values_changed', value='X', old_value='D', type=str, old_type=str, new_path=[2]), FlatDeltaRow(path=[6], action='values_changed', value='Z', old_value='G', type=str, old_type=str), FlatDeltaRow(path=[5], action='values_changed', value='Y', old_value='F', type=str, old_type=str), FlatDeltaRow(path=[], action=FlatDataAction.iterable_items_deleted, value=[], old_value=['A'], type=list, old_type=list, t1_from_index=0, t1_to_index=1, t2_from_index=0, t2_to_index=0), FlatDeltaRow(path=[], action=FlatDataAction.iterable_items_equal, value=None, old_value=None, type=type(None), old_type=type(None), t1_from_index=1, t1_to_index=3, t2_from_index=0, t2_to_index=2), FlatDeltaRow(path=[], action=FlatDataAction.iterable_items_replaced, value=['X'], old_value=['D', 'E', 'F', 'G'], type=list, old_type=list, t1_from_index=3, t1_to_index=7, t2_from_index=2, t2_to_index=3), FlatDeltaRow(path=[], action=FlatDataAction.iterable_items_equal, value=None, old_value=None, type=type(None), old_type=type(None), t1_from_index=7, t1_to_index=9, t2_from_index=3, t2_to_index=5), FlatDeltaRow(path=[], action=FlatDataAction.iterable_items_inserted, value=['Y', 'Z'], old_value=[], type=list, old_type=list, t1_from_index=9, t1_to_index=9, t2_from_index=5, t2_to_index=7) ] # The order of the first 3 items is not deterministic assert not DeepDiff(expected_flat_rows[:3], flat_rows[:3], ignore_order=True) assert expected_flat_rows[3:] == flat_rows[3:] delta5 = Delta(flat_rows_list=flat_rows, bidirectional=True, force=True) assert l2 == l1 + delta5 assert l1 == l2 - delta5 def test_delta_flat_rows(self): t1 = {"key1": "value1"} t2 = {"field2": {"key2": "value2"}} diff = DeepDiff(t1, t2, verbose_level=2) delta = Delta(diff, bidirectional=True) assert t1 + delta == t2 flat_rows = delta.to_flat_rows() # we need to set force=True because when we create flat rows, if a nested # dictionary with a single key is created, the path in the flat row will be # the path to the leaf node. delta2 = Delta(flat_rows_list=flat_rows, bidirectional=True, force=True) assert t1 + delta2 == t2 def test_delta_bool(self): flat_rows_list = [FlatDeltaRow(path=['dollar_to_cent'], action='values_changed', value=False, old_value=True, type=bool, old_type=bool)] value = {'dollar_to_cent': False} delta = Delta(flat_rows_list=flat_rows_list, bidirectional=True, force=True) assert {'dollar_to_cent': True} == value - delta def test_detla_add_to_empty_iterable_and_flatten(self): t1 = {'models': [], 'version_id': 0} t2 = {'models': ['Super Duty F-250'], 'version_id': 1} t3 = {'models': ['Super Duty F-250', 'Focus'], 'version_id': 1} diff = DeepDiff(t1, t2, verbose_level=2) delta = Delta(diff, bidirectional=True) assert t1 + delta == t2 flat_rows = delta.to_flat_rows() delta2 = Delta(flat_rows_list=flat_rows, bidirectional=True) # , force=True assert t1 + delta2 == t2 assert t2 - delta2 == t1 diff3 = DeepDiff(t2, t3) delta3 = Delta(diff3, bidirectional=True) flat_dicts3 = delta3.to_flat_dicts() delta3_again = Delta(flat_dict_list=flat_dicts3, bidirectional=True) assert t2 + delta3_again == t3 assert t3 - delta3_again == t2 def test_flat_dict_and_deeply_nested_dict(self): beforeImage = [ { "usage": "Mailing", "standardization": "YES", "primaryIndicator": True, "addressIdentifier": "Z8PDWBG42YC", "addressLines": ["871 PHILLIPS FERRY RD"], }, { "usage": "Residence", "standardization": "YES", "primaryIndicator": False, "addressIdentifier": "Z8PDWBG42YC", "addressLines": ["871 PHILLIPS FERRY RD"], }, { "usage": "Mailing", "standardization": None, "primaryIndicator": False, "addressIdentifier": "MHPP3BY0BYC", "addressLines": ["871 PHILLIPS FERRY RD", "APT RV92"], }, ] allAfterImage = [ { "usage": "Residence", "standardization": "NO", "primaryIndicator": False, "addressIdentifier": "Z8PDWBG42YC", "addressLines": ["871 PHILLIPS FERRY RD"], }, { "usage": "Mailing", "standardization": None, "primaryIndicator": False, "addressIdentifier": "MHPP3BY0BYC", "addressLines": ["871 PHILLIPS FERRY RD", "APT RV92"], }, { "usage": "Mailing", "standardization": "NO", "primaryIndicator": True, "addressIdentifier": "Z8PDWBG42YC", "addressLines": ["871 PHILLIPS FERRY RD"], }, ] diff = DeepDiff( beforeImage, allAfterImage, ignore_order=True, report_repetition=True, ) # reverse_diff = DeepDiff( # allAfterImage, # beforeImage, # ignore_order=True, # report_repetition=True, # ) delta = Delta( diff, always_include_values=True, bidirectional=True ) # reverse_delta = Delta( # reverse_diff, always_include_values=True, bidirectional=True # ) allAfterImageAgain = beforeImage + delta diff2 = DeepDiff(allAfterImage, allAfterImageAgain, ignore_order=True) assert not diff2 # print("\ndelta.diff") # pprint(delta.diff) # print("\ndelta._get_reverse_diff()") # pprint(delta._get_reverse_diff()) # print("\nreverse_delta.diff") # pprint(reverse_delta.diff) beforeImageAgain = allAfterImage - delta diff3 = DeepDiff(beforeImage, beforeImageAgain, ignore_order=True) assert not diff3 # ------ now let's recreate the delta from flat dicts ------- flat_dict_list = delta.to_flat_dicts() delta2 = Delta( flat_dict_list=flat_dict_list, always_include_values=True, bidirectional=True, raise_errors=False, force=True, ) # print("\ndelta from flat dicts") # pprint(delta2.diff) allAfterImageAgain2 = beforeImage + delta2 diff4 = DeepDiff(allAfterImage, allAfterImageAgain2, ignore_order=True) assert not diff4 beforeImageAgain2 = allAfterImage - delta2 diff4 = DeepDiff(beforeImage, beforeImageAgain2, ignore_order=True) assert not diff4 def test_moved_and_changed_flat(self): """Items that both move (due to prepended inserts) and change values should produce t2 with no phantom entries after delta replay.""" t1 = [{"id": "a", "val": 1}, {"id": "b", "val": 2}, {"id": "c", "val": 3}] t2 = [ {"id": "new1", "val": 10}, # inserted at [0] {"id": "new2", "val": 20}, # inserted at [1] {"id": "a", "val": 0.5}, # moved [0]→[2], val changed {"id": "b", "val": 1.0}, # moved [1]→[3], val changed {"id": "c", "val": 1.5}, # moved [2]→[4], val changed ] ddiff = DeepDiff(t1, t2, iterable_compare_func=self.compare_func, threshold_to_diff_deeper=0) result = t1 + Delta(ddiff, force=True, raise_errors=True, log_errors=False) assert [item for item in result if "id" not in item] == [], \ "Phantom entries (dicts missing 'id') found in result" assert result == t2 def test_moved_and_changed_nested(self): """Same bug in a nested structure: inner list items that both move and change values should produce no phantom entries after delta replay.""" t1 = {"rows": [ {"id": "r1", "items": [{"id": "a", "val": 1}, {"id": "b", "val": 2}]}, ]} t2 = {"rows": [ {"id": "r1", "items": [ {"id": "new1", "val": 99}, # inserted at [0] {"id": "a", "val": 0.5}, # moved [0]→[1], val changed {"id": "b", "val": 1.0}, # moved [1]→[2], val changed ]}, ]} ddiff = DeepDiff(t1, t2, iterable_compare_func=self.compare_func, threshold_to_diff_deeper=0) result = t1 + Delta(ddiff, force=True, raise_errors=True, log_errors=False) assert [item for item in result["rows"][0][ "items"] if "id" not in item] == [], \ "Phantom entries (dicts missing 'id') found in nested result" assert result == t2 def test_appended_only_no_movement_sanity_check(self): """ When new items are only appended (existing items keep their positions), stock Delta produces the correct result with no phantom entries. """ t1 = [{"id": "a", "val": 1}, {"id": "b", "val": 2}] t2 = [ {"id": "a", "val": 0.5}, # stays at [0], val changed {"id": "b", "val": 1.0}, # stays at [1], val changed {"id": "new1", "val": 10}, # appended {"id": "new2", "val": 20}, # appended ] ddiff = DeepDiff(t1, t2, iterable_compare_func=self.compare_func, threshold_to_diff_deeper=0) result = t1 + Delta(ddiff, force=True, raise_errors=True, log_errors=False) assert [item for item in result if "id" not in item] == [] assert result == t2 def test_moved_and_type_changed(self): """Items that both move and change type should apply correctly.""" t1 = [{"id": "a", "val": "1"}, {"id": "b", "val": "2"}] t2 = [ {"id": "new1", "val": 99}, {"id": "a", "val": 1}, # moved [0]->[1], val str->int {"id": "b", "val": 2}, # moved [1]->[2], val str->int ] ddiff = DeepDiff(t1, t2, iterable_compare_func=self.compare_func, threshold_to_diff_deeper=0) result = t1 + Delta(ddiff, force=True, raise_errors=True, log_errors=False) assert [item for item in result if "id" not in item] == [], \ "Phantom entries (dicts missing 'id') found in result" assert result == t2 qlustered-deepdiff-41c7265/tests/test_diff_datetime.py000066400000000000000000000103051516241264500231070ustar00rootroot00000000000000import pytz from datetime import date, datetime, time, timezone from deepdiff import DeepDiff class TestDiffDatetime: def test_datetime_diff(self): """Testing for the correct setting and usage of epsilon.""" d1 = {"a": datetime(2023, 7, 5, 10, 11, 12)} d2 = {"a": datetime(2023, 7, 5, 10, 11, 12)} res = DeepDiff(d1, d2) assert res == {} res = DeepDiff(d1, d2, ignore_numeric_type_changes=True) assert res == {} d1 = {"a": datetime(2023, 7, 5, 10, 11, 12)} d2 = {"a": datetime(2023, 7, 5, 11, 11, 12)} res = DeepDiff(d1, d2) expected = { "values_changed": { "root['a']": { "new_value": datetime(2023, 7, 5, 11, 11, 12, tzinfo=timezone.utc), "old_value": datetime(2023, 7, 5, 10, 11, 12, tzinfo=timezone.utc), } } } assert res == expected def test_date_diff(self): """Testing for the correct setting and usage of epsilon.""" d1 = {"a": date(2023, 7, 5)} d2 = {"a": date(2023, 7, 5)} res = DeepDiff(d1, d2) assert res == {} # this usage failed in version >=6.0, <=6.3.0 res = DeepDiff(d1, d2, ignore_numeric_type_changes=True) assert res == {} d1 = {"a": date(2023, 7, 5)} d2 = {"a": date(2023, 7, 6)} res = DeepDiff(d1, d2) expected = { "values_changed": { "root['a']": { "new_value": date(2023, 7, 6), "old_value": date(2023, 7, 5), } } } assert res == expected def test_time_diff(self): """Testing for the correct setting and usage of epsilon.""" d1 = {"a": time(10, 11, 12)} d2 = {"a": time(10, 11, 12)} res = DeepDiff(d1, d2) assert res == {} res = DeepDiff(d1, d2, ignore_numeric_type_changes=True) assert res == {} d1 = {"a": time(10, 11, 12)} d2 = {"a": time(11, 11, 12)} res = DeepDiff(d1, d2) expected = { "values_changed": { "root['a']": { "new_value": time(11, 11, 12), "old_value": time(10, 11, 12), } } } assert res == expected def test_diffs_datetimes_different_timezones(self): dt_utc = datetime(2025, 2, 3, 12, 0, 0, tzinfo=pytz.utc) # UTC timezone # Convert it to another timezone (e.g., New York) dt_ny = dt_utc.astimezone(pytz.timezone('America/New_York')) assert dt_utc == dt_ny diff = DeepDiff(dt_utc, dt_ny) assert not diff t1 = [dt_utc, dt_ny] t2 = [dt_ny, dt_utc] assert not DeepDiff(t1, t2) assert not DeepDiff(t1, t2, ignore_order=True) t2 = [dt_ny, dt_utc, dt_ny] assert not DeepDiff(t1, t2, ignore_order=True) def test_diffs_datetimes_in_different_timezones(self): dt_utc = datetime(2025, 2, 3, 12, 0, 0, tzinfo=pytz.utc) # UTC timezone dt_utc2 = datetime(2025, 2, 3, 11, 0, 0, tzinfo=pytz.utc) # UTC timezone dt_ny = dt_utc.astimezone(pytz.timezone('America/New_York')) dt_ny2 = dt_utc2.astimezone(pytz.timezone('America/New_York')) diff = DeepDiff(dt_ny, dt_ny2) assert { "values_changed": { "root": { "new_value": dt_utc2, "old_value": dt_utc, } } } == diff diff2 = DeepDiff(dt_ny, dt_ny2, default_timezone=pytz.timezone('America/New_York')) assert { "values_changed": { "root": { "new_value": dt_ny2, "old_value": dt_ny, } } } == diff2 def test_datetime_within_array_with_timezone_diff(self): d1 = [datetime(2020, 8, 31, 13, 14, 1)] d2 = [datetime(2020, 8, 31, 13, 14, 1, tzinfo=timezone.utc)] assert d1 != d2, "Python doesn't think these are the same datetimes" assert not DeepDiff(d1, d2) assert not DeepDiff(d1, d2, ignore_order=True) assert not DeepDiff(d1, d2, truncate_datetime='second') qlustered-deepdiff-41c7265/tests/test_diff_group_by.py000066400000000000000000000050051516241264500231420ustar00rootroot00000000000000"""Tests for the group_by parameter of Deepdiff""" import pytest from deepdiff import DeepDiff class TestGetKeyForGroupBy: def test_group_by_string(self): """Test where group_by is a single key (string).""" row = {'first': 'John', 'middle': 'Joe', 'last': 'Smith'} group_by = 'first' item_name = 't1' actual = DeepDiff._get_key_for_group_by(row, group_by, item_name) expected = 'John' assert actual == expected def test_group_by_callable(self): """Test where group_by is callable.""" row = {'id': 123, 'demographics': {'names': {'first': 'John', 'middle': 'Joe', 'last': 'Smith'}}} group_by = lambda x: x['demographics']['names']['first'] item_name = 't1' actual = DeepDiff._get_key_for_group_by(row, group_by, item_name) expected = 'John' assert actual == expected def test_group_by_key_error(self): """Test where group_by is a key that is not in the row.""" row = {'id': 123, 'demographics': {'names': {'first': 'John', 'middle': 'Joe', 'last': 'Smith'}}} group_by = 'someotherkey' item_name = 't1' with pytest.raises(KeyError): DeepDiff._get_key_for_group_by(row, group_by, item_name) class TestGroupBy: def test_group_by_callable(self): """Test where group_by is a callable.""" t1 = [ {'id': 'AA', 'demographics': {'names': {'first': 'Joe', 'middle': 'John', 'last': 'Nobody'}}}, {'id': 'BB', 'demographics': {'names': {'first': 'James', 'middle': 'Joyce', 'last': 'Blue'}}}, {'id': 'CC', 'demographics': {'names': {'first': 'Mike', 'middle': 'Mark', 'last': 'Apple'}}}, ] t2 = [ {'id': 'AA', 'demographics': {'names': {'first': 'Joe', 'middle': 'John', 'last': 'Nobody'}}}, {'id': 'BB', 'demographics': {'names': {'first': 'James', 'middle': 'Joyce', 'last': 'Brown'}}}, {'id': 'CC', 'demographics': {'names': {'first': 'Mike', 'middle': 'Charles', 'last': 'Apple'}}}, ] actual = DeepDiff(t1, t2, group_by=lambda x: x['demographics']['names']['first']) expected = { 'values_changed': { "root['James']['demographics']['names']['last']": {'new_value': 'Brown', 'old_value': 'Blue'}, "root['Mike']['demographics']['names']['middle']": {'new_value': 'Charles', 'old_value': 'Mark'}, }, } assert actual == expected qlustered-deepdiff-41c7265/tests/test_diff_include_paths.py000066400000000000000000000306251516241264500241440ustar00rootroot00000000000000import pytest from deepdiff import DeepDiff t1 = { "foo": { "bar": { "veg": "potato", "fruit": "apple" } }, "ingredients": [ { "lunch": [ "bread", "cheese" ] }, { "dinner": [ "soup", "meat" ] } ] } t2 = { "foo": { "bar": { "veg": "potato", "fruit": "peach" } }, "ingredients": [ { "lunch": [ "bread", "cheese" ] }, { "dinner": [ "soup", "meat" ] } ] } class TestDeepDiffIncludePaths: @staticmethod def deep_diff(dict1, dict2, include_paths): diff = DeepDiff(dict1, dict2, include_paths=include_paths) print(diff) return diff def test_include_paths_root_neg(self): expected = {'values_changed': {"root['foo']['bar']['fruit']": {'new_value': 'peach', 'old_value': 'apple'}}} actual = self.deep_diff(t1, t2, 'foo') assert expected == actual def test_include_paths_root_pos(self): expected = {} actual = self.deep_diff(t1, t2, 'ingredients') assert expected == actual def test_include_paths_nest00_neg(self): expected = {'values_changed': {"root['foo']['bar']['fruit']": {'new_value': 'peach', 'old_value': 'apple'}}} actual = self.deep_diff(t1, t2, "root['foo']['bar']") assert expected == actual def test_include_paths_nest01_neg(self): expected = {'values_changed': {"root['foo']['bar']['fruit']": {'new_value': 'peach', 'old_value': 'apple'}}} actual = self.deep_diff(t1, t2, "root['foo']['bar']['fruit']") assert expected == actual def test_include_paths_nest_pos(self): expected = {} actual = self.deep_diff(t1, t2, "root['foo']['bar']['veg']") assert expected == actual @pytest.mark.parametrize( "test_num, data", [ ( 1, # test_num { "old": { 'name': 'Testname Old', 'desciption': 'Desc Old', 'sub_path': { 'name': 'Testname Subpath old', 'desciption': 'Desc Subpath old', }, }, "new": { 'name': 'Testname New', 'desciption': 'Desc New', 'new_attribute': 'New Value', 'sub_path': { 'name': 'Testname Subpath old', 'desciption': 'Desc Subpath old', }, }, "include_paths": "root['sub_path']", "expected_result1": {'dictionary_item_added': ["root['new_attribute']"], 'values_changed': {"root['name']": {'new_value': 'Testname New', 'old_value': 'Testname Old'}, "root['desciption']": {'new_value': 'Desc New', 'old_value': 'Desc Old'}}}, "expected_result2": {}, }, ), ( 2, # test_num { "old": { 'name': 'Testname Old', 'desciption': 'Desc Old', 'sub_path': { 'name': 'Testname Subpath old', 'desciption': 'Desc Subpath old', }, }, "new": { 'name': 'Testname New', 'desciption': 'Desc New', 'new_attribute': 'New Value', 'sub_path': { 'name': 'Testname Subpath New', 'desciption': 'Desc Subpath old', }, }, "include_paths": "root['sub_path']", "expected_result1": {'dictionary_item_added': ["root['new_attribute']"], 'values_changed': {"root['name']": {'new_value': 'Testname New', 'old_value': 'Testname Old'}, "root['desciption']": {'new_value': 'Desc New', 'old_value': 'Desc Old'}, "root['sub_path']['name']": {'new_value': 'Testname Subpath New', 'old_value': 'Testname Subpath old'}}}, "expected_result2": {"values_changed": {"root['sub_path']['name']": {"old_value": "Testname Subpath old", "new_value": "Testname Subpath New"}}}, }, ), ( 3, # test_num { "old": { 'name': 'Testname Old', 'desciption': 'Desc Old', 'sub_path': { 'name': 'Testname Subpath old', 'desciption': 'Desc Subpath old', 'old_attr': 'old attr value', }, }, "new": { 'name': 'Testname New', 'desciption': 'Desc New', 'new_attribute': 'New Value', 'sub_path': { 'name': 'Testname Subpath old', 'desciption': 'Desc Subpath New', 'new_sub_path_attr': 'new sub path attr value', }, }, "include_paths": "root['sub_path']['name']", "expected_result1": {'dictionary_item_added': ["root['new_attribute']", "root['sub_path']['new_sub_path_attr']"], 'dictionary_item_removed': ["root['sub_path']['old_attr']"], 'values_changed': {"root['name']": {'new_value': 'Testname New', 'old_value': 'Testname Old'}, "root['desciption']": {'new_value': 'Desc New', 'old_value': 'Desc Old'}, "root['sub_path']['desciption']": {'new_value': 'Desc Subpath New', 'old_value': 'Desc Subpath old'}}}, "expected_result2": {}, }, ), ( 4, # test_num { "old": { 'name': 'Testname old', 'desciption': 'Desc old', 'new_attribute': 'old Value', 'sub_path': { 'name': 'Testname', 'removed_attr': 'revemod attr value', }, }, "new": { 'name': 'Testname new', 'desciption': 'Desc new', 'new_attribute': 'new Value', 'sub_path': { 'added_attr': 'Added Attr Value', 'name': 'Testname', }, }, "include_paths": "root['sub_path']['name']", "expected_result1": {'dictionary_item_added': ["root['sub_path']['added_attr']"], 'dictionary_item_removed': ["root['sub_path']['removed_attr']"], 'values_changed': {"root['name']": {'new_value': 'Testname new', 'old_value': 'Testname old'}, "root['desciption']": {'new_value': 'Desc new', 'old_value': 'Desc old'}, "root['new_attribute']": {'new_value': 'new Value', 'old_value': 'old Value'}}}, "expected_result2": {}, }, ), ( 5, # test_num { "old": { 'name': 'Testname', 'removed_attr': 'revemod attr value', }, "new": { 'added_attr': 'Added Attr Value', 'name': 'Testname', }, "include_paths": "root['name']", "expected_result1": {'dictionary_item_added': ["root['added_attr']"], 'dictionary_item_removed': ["root['removed_attr']"]}, "expected_result2": {}, }, ), ( 6, # test_num { "old": { 'name': 'Testname', 'removed_attr': 'revemod attr value', 'removed_attr_2': 'revemod attr value', }, "new": { 'added_attr': 'Added Attr Value', 'name': 'Testname', }, "include_paths": "root['name']", "expected_result1": {'values_changed': {'root': {'new_value': {'added_attr': 'Added Attr Value', 'name': 'Testname'}, 'old_value': {'name': 'Testname', 'removed_attr': 'revemod attr value', 'removed_attr_2': 'revemod attr value'}}}}, "expected_result2": {}, }, ), ( 7, # test_num { "old": { 'name': 'Testname old', 'desciption': 'Desc old', 'new_attribute': 'old Value', 'sub_path': { 'name': 'Testname', 'removed_attr': 'revemod attr value', 'removed_attr_2': 'blu', }, }, "new": { 'name': 'Testname new', 'desciption': 'Desc new', 'new_attribute': 'new Value', 'sub_path': { 'added_attr': 'Added Attr Value', 'name': 'Testname', }, }, "include_paths": "root['sub_path']['name']", "expected_result1": {'values_changed': {"root['name']": {'new_value': 'Testname new', 'old_value': 'Testname old'}, "root['desciption']": {'new_value': 'Desc new', 'old_value': 'Desc old'}, "root['new_attribute']": {'new_value': 'new Value', 'old_value': 'old Value'}, "root['sub_path']": {'new_value': {'added_attr': 'Added Attr Value', 'name': 'Testname'}, 'old_value': {'name': 'Testname', 'removed_attr': 'revemod attr value', 'removed_attr_2': 'blu'}}}}, "expected_result2": {}, }, ), ( 8, # test_num { "old": [{ 'name': 'Testname old', 'desciption': 'Desc old', 'new_attribute': 'old Value', 'sub_path': { 'name': 'Testname', 'removed_attr': 'revemod attr value', 'removed_attr_2': 'blu', }, }], "new": [{ 'name': 'Testname new', 'desciption': 'Desc new', 'new_attribute': 'new Value', 'sub_path': { 'added_attr': 'Added Attr Value', 'name': 'New Testname', }, }], "include_paths": "root[0]['sub_path']['name']", "expected_result1": {'values_changed': {"root[0]['name']": {'new_value': 'Testname new', 'old_value': 'Testname old'}, "root[0]['desciption']": {'new_value': 'Desc new', 'old_value': 'Desc old'}, "root[0]['new_attribute']": {'new_value': 'new Value', 'old_value': 'old Value'}, "root[0]['sub_path']": {'new_value': {'added_attr': 'Added Attr Value', 'name': 'New Testname'}, 'old_value': {'name': 'Testname', 'removed_attr': 'revemod attr value', 'removed_attr_2': 'blu'}}}}, "expected_result2": {'values_changed': {"root[0]['sub_path']['name']": {'new_value': 'New Testname', 'old_value': 'Testname'}}}, }, ), ] ) def test_diff_include_paths_root(self, test_num, data): diff1 = DeepDiff(data["old"], data["new"]) diff2 = DeepDiff(data["old"], data["new"], include_paths=data["include_paths"]) assert data['expected_result1'] == diff1, f"test_diff_include_paths_root test_num #{test_num} failed." assert data['expected_result2'] == diff2, f"test_diff_include_paths_root test_num #{test_num} failed." qlustered-deepdiff-41c7265/tests/test_diff_math.py000066400000000000000000000114261516241264500222510ustar00rootroot00000000000000from decimal import Decimal from deepdiff import DeepDiff class TestDiffMath: def test_math_diff(self): """Testing for the correct setting and usage of epsilon.""" d1 = {"a": Decimal("3.5")} d2 = {"a": Decimal("4")} ep = 0.5 res = DeepDiff(d1, d2, math_epsilon=ep) assert res == {} d1 = {"a": Decimal("2.5")} d2 = {"a": Decimal("3")} ep = 0.5 res = DeepDiff(d1, d2, math_epsilon=ep) assert res == {} d1 = {"a": Decimal("2.5")} d2 = {"a": Decimal("2")} ep = 0.5 res = DeepDiff(d1, d2, math_epsilon=ep) assert res == {} d1 = {"a": Decimal("7.175")} d2 = {"a": Decimal("7.174")} ep = 0.1 res = DeepDiff(d1, d2, math_epsilon=ep) assert res == {} d1 = {"a": Decimal("7.175")} d2 = {"a": Decimal("7.174")} ep = 0.01 res = DeepDiff(d1, d2, math_epsilon=ep) assert res == {} d1 = {"a": Decimal("7.175")} d2 = {"a": Decimal("7.174")} ep = 0.001 res = DeepDiff(d1, d2, math_epsilon=ep) assert res == {} d1 = {"a": Decimal("7.175")} d2 = {"a": Decimal("7.174")} ep = 0.0001 expected = { "values_changed": { "root['a']": { "new_value": Decimal("7.174"), "old_value": Decimal("7.175"), } } } res = DeepDiff(d1, d2, math_epsilon=ep) assert res == expected def test_math_diff_special_case(self): """Testing epsilon on a special Decimal case. Even though the Decimal looks different, math will evaluate it for us.""" d1 = {"a": Decimal("9.709999618320632")} d2 = {"a": Decimal("9.710000038146973")} ep = 0.001 res = DeepDiff(d1, d2, math_epsilon=ep) assert res == {} d1 = {"a": Decimal("9.709999618320632")} d2 = {"a": Decimal("9.710000038146973")} ep = 0.0001 res = DeepDiff(d1, d2, math_epsilon=ep) assert res == {} d1 = {"a": Decimal("9.709999618320632")} d2 = {"a": Decimal("9.710000038146973")} ep = 0.00001 res = DeepDiff(d1, d2, math_epsilon=ep) assert res == {} d1 = {"a": Decimal("9.709999618320632")} d2 = {"a": Decimal("9.710000038146973")} ep = 0.000001 res = DeepDiff(d1, d2, math_epsilon=ep) assert res == {} d1 = {"a": Decimal("9.709999618320632")} d2 = {"a": Decimal("9.710000038146973")} ep = 0.0000001 res = DeepDiff(d1, d2, math_epsilon=ep) expected = { "values_changed": { "root['a']": { "new_value": Decimal("9.710000038146973"), "old_value": Decimal("9.709999618320632"), } } } assert res == expected def test_math_diff_ignore_order(self): """math_close will not work with ignore_order=true. Items are hashed if order is ignored, that will not work.""" d1 = {"a": [Decimal("9.709999618320632"), Decimal("9.709999618320632")]} d2 = {"a": [Decimal("9.710000038146973"), Decimal("9.709999618320632")]} ep = 0.0001 res = DeepDiff(d1, d2, ignore_order=False, math_epsilon=ep) assert res == {} def test_math_diff_ignore_order_warning(self, caplog): """math_close will not work with ignore_order=true. Items are hashed if order is ignored, that will not work.""" d1 = {"a": [Decimal("9.709999618320632"), Decimal("9.709999618320632")]} d2 = {"a": [Decimal("9.710000038146973"), Decimal("9.709999618320632")]} ep = 0.0001 res = DeepDiff(d1, d2, ignore_order=True, math_epsilon=ep) expected = { "iterable_item_added": {"root['a'][0]": Decimal("9.710000038146973")} } assert res == expected # assert "math_epsilon will be ignored." in caplog.text def test_ignore_numeric_type_changes_with_numeric_keys_and_no_significant_digits(self): """Test that ignore_numeric_type_changes works with numeric keys when significant_digits is None. This test covers the bug fix in _get_clean_to_keys_mapping where significant_digits=None caused a crash when number_to_string was called without the required parameter. """ # Test case with numeric keys and ignore_numeric_type_changes=True, significant_digits=None d1 = {1: "value1", 2.5: "value2"} d2 = {1.0: "value1", 2.5: "value2"} # int vs float keys # This should not crash and should treat 1 and 1.0 as the same key result = DeepDiff(d1, d2, ignore_numeric_type_changes=True, significant_digits=None) assert result == {} qlustered-deepdiff-41c7265/tests/test_diff_numpy.py000066400000000000000000000152601516241264500224700ustar00rootroot00000000000000import pytest from deepdiff import DeepDiff from deepdiff.helper import np from tests import parameterize_cases """ These are numpy specific test cases. There are more numpy tests for delta additions in the test_delta.py """ NUMPY_CASES = { 'numpy_bools': { 't1': np.array([True, False, True, False], dtype=bool), 't2': np.array([False, True, True, False], dtype=bool), 'deepdiff_kwargs': {}, 'expected_result': {'values_changed': {'root[0]': {'new_value': False, 'old_value': True}, 'root[1]': {'new_value': True, 'old_value': False}}}, }, 'numpy_bools_ignore_order': { 't1': np.array([True, False, True, False], dtype=bool), 't2': np.array([False, True, True, False], dtype=bool), 'deepdiff_kwargs': {'ignore_order': True}, 'expected_result': {}, }, 'numpy_multi_dimensional1': { 't1': np.array([[[1, 2, 3], [4, 5, 6]]], np.int32), 't2': np.array([[[1, 2, 5], [3, 5, 6]]], np.int32), 'deepdiff_kwargs': {}, 'expected_result': {'values_changed': {'root[0][0][2]': {'new_value': 5, 'old_value': 3}, 'root[0][1][0]': {'new_value': 3, 'old_value': 4}}}, }, 'numpy_array2_type_change': { 't1': np.array([1, 2, 3], np.int8), 't2': np.array([1, 2, 5], np.int32), 'deepdiff_kwargs': {'verbose_level': 0}, 'expected_result': {'type_changes': {'root': {'old_type': np.int8, 'new_type': np.int32}}}, }, 'numpy_array3_ignore_number_type_changes': { 't1': np.array([1, 2, 3], np.int8), 't2': np.array([1, 2, 5], np.int32), 'deepdiff_kwargs': {'ignore_numeric_type_changes': True}, 'expected_result': {'values_changed': {'root[2]': {'new_value': 5, 'old_value': 3}}}, }, 'numpy_array4_ignore_number_type_changes_and_ignore_order': { 't1': np.array([1, 2, 3], np.int8), 't2': np.array([3, 1, 2], np.int32), 'deepdiff_kwargs': {'ignore_numeric_type_changes': True, 'ignore_order': True}, 'expected_result': {}, }, 'numpy_array5_ignore_number_type_changes_and_ignore_order': { 't1': np.array([1, 2, 4, 3], np.int8), 't2': np.array([3, 1, 2, 5], np.int32), 'deepdiff_kwargs': {'ignore_numeric_type_changes': True, 'ignore_order': True}, 'expected_result': {'values_changed': {'root[2]': {'new_value': 5, 'old_value': 4}}}, }, 'numpy_array6_ignore_order_and_report_repetition': { 't1': np.array([1, 2, 3, 3], np.int8), 't2': np.array([3, 1, 2, 5], np.int8), 'deepdiff_kwargs': {'report_repetition': True, 'ignore_order': True}, 'expected_result': {'iterable_item_added': {'root[3]': 5}, 'repetition_change': {'root[2]': {'old_repeat': 2, 'new_repeat': 1, 'old_indexes': [2, 3], 'new_indexes': [0], 'value': 3}}}, }, 'numpy_array7_ignore_order_multi_dimensional_array': { 't1': np.array([[1, 2, 3, 4], [4, 2, 2, 1]], np.int8), 't2': np.array([[4, 1, 1, 1], [1, 3, 2, 4]], np.int8), 'deepdiff_kwargs': {'report_repetition': True, 'ignore_order': True}, 'expected_result': { 'iterable_item_removed': { 'root[1][1]': 2, 'root[1][2]': 2 }, 'repetition_change': { 'root[1][3]': { 'old_repeat': 1, 'new_repeat': 3, 'old_indexes': [3], 'new_indexes': [1, 2, 3], 'value': 1 } } }, }, 'numpy_array8_ignore_order_multi_dimensional_array_converted_to_list': { 't1': np.array([[1, 2, 3, 4], [4, 2, 2, 1]], np.int8).tolist(), 't2': np.array([[4, 1, 1, 1], [1, 3, 2, 4]], np.int8).tolist(), 'deepdiff_kwargs': { 'report_repetition': True, 'ignore_order': True }, 'expected_result': { 'iterable_item_removed': { 'root[1][1]': 2, 'root[1][2]': 2 }, 'repetition_change': { 'root[1][3]': { 'old_repeat': 1, 'new_repeat': 3, 'old_indexes': [3], 'new_indexes': [1, 2, 3], 'value': 1 } } }, }, 'numpy_array9_ignore_nan_inequality_float32': { 't1': np.array([1, 2, 3, np.nan], np.float32), 't2': np.array([1, 2, 4, np.nan], np.float32), 'deepdiff_kwargs': { 'ignore_nan_inequality': True, }, 'expected_result': {'values_changed': {'root[2]': {'new_value': 4.0, 'old_value': 3.0}}} }, 'numpy_almost_equal': { 't1': np.array([1.0, 2.3333333333333]), 't2': np.array([1.0, 2.33333334]), 'deepdiff_kwargs': {'significant_digits': 3}, 'expected_result': {}, }, 'numpy_almost_equal2': { 't1': np.array(['a', 'b'], dtype=object), 't2': np.array(['a', 'b'], dtype=object), 'deepdiff_kwargs': {'significant_digits': 6}, 'expected_result': {}, }, 'numpy_different_shape': { 't1': np.array([[1, 1], [2, 3]]), 't2': np.array([1]), 'deepdiff_kwargs': {}, 'expected_result': { 'type_changes': { 'root[0]': { 'old_type': list, 'new_type': int, 'old_value': [1, 1], 'new_value': 1 } }, 'iterable_item_removed': { 'root[1]': [2, 3] } }, }, 'numpy_datetime_equal': { 't1': np.datetime64('2023-07-05T10:11:12'), 't2': np.datetime64('2023-07-05T10:11:12'), 'deepdiff_kwargs': {}, 'expected_result': {}, }, 'numpy_datetime_unequal': { 't1': np.datetime64('2023-07-05T10:11:12'), 't2': np.datetime64('2024-07-05T10:11:12'), 'deepdiff_kwargs': {}, 'expected_result': { 'values_changed': { 'root': { 'new_value': np.datetime64('2024-07-05T10:11:12'), 'old_value': np.datetime64('2023-07-05T10:11:12'), } }, }, }, } NUMPY_CASES_PARAMS = parameterize_cases('test_name, t1, t2, deepdiff_kwargs, expected_result', NUMPY_CASES) class TestNumpy: @pytest.mark.parametrize(**NUMPY_CASES_PARAMS) def test_numpy(self, test_name, t1, t2, deepdiff_kwargs, expected_result): diff = DeepDiff(t1, t2, **deepdiff_kwargs) assert expected_result == diff, f"test_numpy {test_name} failed." qlustered-deepdiff-41c7265/tests/test_diff_other.py000066400000000000000000000162561516241264500224470ustar00rootroot00000000000000import pytest import datetime from time import sleep from unittest import mock from functools import partial from collections import namedtuple from deepdiff import DeepHash from deepdiff.helper import pypy3 from deepdiff.model import DiffLevel from deepdiff.diff import ( DeepDiff, PROGRESS_MSG, INVALID_VIEW_MSG, VERBOSE_LEVEL_RANGE_MSG, PURGE_LEVEL_RANGE_MSG) from concurrent.futures.process import ProcessPoolExecutor from concurrent.futures import as_completed from ipaddress import IPv4Address # Only the prep part of DeepHash. We don't need to test the actual hash function. DeepHashPrep = partial(DeepHash, apply_hash=False) def prep_str(obj, ignore_string_type_changes=True): return obj if ignore_string_type_changes else 'str:{}'.format(obj) Point = namedtuple('Point', ["x"]) point_obj = Point(x=11) class SlowDiffLevel(DiffLevel): is_run = False def __init__(self, *args, **kwargs): sleep(.1) super().__init__(*args, **kwargs) class TestDiffOther: @mock.patch('deepdiff.diff.DiffLevel', side_effect=SlowDiffLevel) def test_repeated_timer(self, MockDiffLevel): t1 = [1, [2]] t2 = [1, [3]] progress_logger = mock.Mock() DeepDiff(t1, t2, log_frequency_in_sec=0.02, progress_logger=progress_logger) assert PROGRESS_MSG.format(0, 0, 0) == progress_logger.call_args[0][0] def test_invalid_view(self): t1 = [1] t2 = [2] with pytest.raises(ValueError) as excinfo: DeepDiff(t1, t2, view='blah') assert str(excinfo.value) == INVALID_VIEW_MSG.format('blah') def test_truncate_datetime(self): d1 = {'a': datetime.datetime(2020, 5, 17, 22, 15, 34, 913070)} d2 = {'a': datetime.datetime(2020, 5, 17, 22, 15, 39, 296583)} res = DeepDiff(d1, d2, truncate_datetime='minute') assert res == {} res = DeepDiff(d1, d2, truncate_datetime='second') expected = datetime.datetime(2020, 5, 17, 22, 15, 39, tzinfo=datetime.timezone.utc) assert res['values_changed']["root['a']"]['new_value'] == expected d1 = {'a': datetime.time(22, 15, 34, 913070)} d2 = {'a': datetime.time(22, 15, 39, 296583)} res = DeepDiff(d1, d2, truncate_datetime='minute') assert res == {} res = DeepDiff(d1, d2, truncate_datetime='second') assert res['values_changed']["root['a']"]['new_value'] == 80139 def test_invalid_verbose_level(self): with pytest.raises(ValueError) as excinfo: DeepDiff(1, 2, verbose_level=5) assert str(excinfo.value) == VERBOSE_LEVEL_RANGE_MSG def test_invalid_cache_purge_level(self): with pytest.raises(ValueError) as excinfo: DeepDiff(1, 2, cache_purge_level=5) assert str(excinfo.value) == PURGE_LEVEL_RANGE_MSG def test_cache_purge_level_max(self): diff = DeepDiff([1], [2], cache_purge_level=1) assert len(diff.__dict__.keys()) > 10 diff2 = DeepDiff([1], [2], cache_purge_level=2) assert not diff2.__dict__ expected = {'values_changed': {'root[0]': {'new_value': 2, 'old_value': 1}}} assert expected == diff2 diff2 = DeepDiff([1], [2], cache_purge_level=2, view='tree') assert not diff2.__dict__ assert list(diff2.keys()) == ['values_changed'] def test_path_cache(self): diff = DeepDiff([1], [2], cache_purge_level=2, view='tree') path1 = diff['values_changed'][0].path() path2 = diff['values_changed'][0].path() assert 'root[0]' == path1 == path2 def test_bool_str1(self): t1 = {'key1': True} t2 = {'key1': 'Yes'} diff = DeepDiff(t1, t2, ignore_type_in_groups=[(bool, str)], ignore_numeric_type_changes=True) expected = { 'values_changed': { "root['key1']": { 'new_value': 'Yes', 'old_value': True } } } assert diff == expected def test_bool_str2(self): t1 = {"default": True} t2 = {"default": "true"} diff = DeepDiff( t1, t2, ignore_type_in_groups=[(bool, str)], ignore_string_type_changes=True) expected = {'values_changed': {"root['default']": {'new_value': 'true', 'old_value': True}}} assert diff == expected diff2 = DeepDiff( t2, t1, ignore_type_in_groups=[(bool, str)], ignore_string_type_changes=True) expected2 = {'values_changed': {"root['default']": {'new_value': True, 'old_value': 'true'}}} assert diff2 == expected2 def test_get_distance_cache_key(self): result = DeepDiff._get_distance_cache_key(added_hash=5, removed_hash=20) assert b'0x14--0x5dc' == result def test_multi_processing1(self): t1 = [[1, 2, 3, 9], [1, 2, 4, 10]] t2 = [[1, 2, 4, 10], [1, 2, 3, 10]] futures = [] expected_result = { 'values_changed': { 'root[0][2]': { 'new_value': 4, 'old_value': 3 }, 'root[0][3]': { 'new_value': 10, 'old_value': 9 }, 'root[1][2]': { 'new_value': 3, 'old_value': 4 } } } with ProcessPoolExecutor(max_workers=1) as executor: futures.append(executor.submit(DeepDiff, t1, t2)) for future in as_completed(futures, timeout=10): assert not future._exception assert expected_result == future._result def test_multi_processing2_with_ignore_order(self): t1 = [[1, 2, 3, 9], [1, 2, 4, 10]] t2 = [[1, 2, 4, 10], [1, 2, 3, 10]] futures = [] expected_result = {'values_changed': {'root[0][3]': {'new_value': 10, 'old_value': 9}}} with ProcessPoolExecutor(max_workers=1) as executor: futures.append(executor.submit(DeepDiff, t1, t2, ignore_order=True)) for future in as_completed(futures, timeout=10): assert not future._exception assert expected_result == future._result @pytest.mark.skipif(pypy3, reason="pypy3 expected results are different") def test_multi_processing3_deephash(self): x = "x" x_prep = prep_str(x) expected_result = { x: x_prep, point_obj: "ntPoint:{%s:int:11}" % x, 11: 'int:11', } futures = [] with ProcessPoolExecutor(max_workers=1) as executor: futures.append(executor.submit(DeepHashPrep, point_obj, ignore_string_type_changes=True)) for future in as_completed(futures, timeout=10): assert not future._exception assert expected_result == future._result def test_ip_address_diff(self): ip1 = IPv4Address("128.0.0.1") ip2 = IPv4Address("128.0.0.2") diff = DeepDiff(ip1, ip2) assert {'values_changed': {'root': {'new_value': IPv4Address('128.0.0.2'), 'old_value': IPv4Address('128.0.0.1')}}} == diff qlustered-deepdiff-41c7265/tests/test_diff_text.py000077500000000000000000002246641516241264500223210ustar00rootroot00000000000000#!/usr/bin/env python import datetime import pytest import logging import re import uuid from enum import Enum from dataclasses import dataclass from typing import List from decimal import Decimal from deepdiff import DeepDiff from deepdiff.helper import pypy3, PydanticBaseModel, np_float64 from tests import CustomClass logging.disable(logging.CRITICAL) class MyEnum1(Enum): book = "book" cake = "cake" class MyEnum2(str, Enum): book = "book" cake = "cake" @dataclass(frozen=True) class MyDataClass: val: int val2: int class TestDeepDiffText: """DeepDiff Tests.""" def test_same_objects(self): t1 = {1: 1, 2: 2, 3: 3} t2 = t1 assert {} == DeepDiff(t1, t2) def test_item_type_change(self): t1 = {1: 1, 2: 2, 3: 3} t2 = {1: 1, 2: "2", 3: 3} ddiff = DeepDiff(t1, t2) assert { 'type_changes': { "root[2]": { "old_value": 2, "old_type": int, "new_value": "2", "new_type": str } } } == ddiff def test_item_type_change_less_verbose(self): t1 = {1: 1, 2: 2, 3: 3} t2 = {1: 1, 2: "2", 3: 3} assert {'type_changes': { "root[2]": { "old_type": int, "new_type": str } }} == DeepDiff(t1, t2, verbose_level=0) def test_item_type_change_for_strings_ignored_by_default(self): """ ignore_string_type_changes = True by default """ t1 = 'hello' t2 = b'hello' ddiff = DeepDiff(t1, t2, ignore_string_type_changes=True) assert not ddiff def test_item_type_change_for_strings_override(self): t1 = 'hello' t2 = b'hello' ddiff = DeepDiff(t1, t2, ignore_string_type_changes=False) assert { 'type_changes': { 'root': { 'old_type': str, 'new_type': bytes, 'old_value': 'hello', 'new_value': b'hello' } } } == ddiff def test_value_change(self): t1 = {1: 1, 2: 2, 3: 3} t2 = {1: 1, 2: 4, 3: 3} result = { 'values_changed': { 'root[2]': { "old_value": 2, "new_value": 4 } } } assert result == DeepDiff(t1, t2) def test_item_added_and_removed(self): t1 = {1: 1, 2: 2, 3: [3], 4: 4} t2 = {1: 1, 2: 4, 3: [3, 4], 5: 5, 6: 6} ddiff = DeepDiff(t1, t2, threshold_to_diff_deeper=0) result = { 'dictionary_item_added': ["root[5]", "root[6]"], 'dictionary_item_removed': ["root[4]"], 'values_changed': { 'root[2]': { 'new_value': 4, 'old_value': 2 } }, 'iterable_item_added': { 'root[3][1]': 4 } } assert result == ddiff assert {'root[4]', 'root[5]', 'root[6]', 'root[3][1]', 'root[2]'} == ddiff.affected_paths assert {4, 5, 6, 3, 2} == ddiff.affected_root_keys def test_item_added_and_removed_verbose(self): t1 = {1: 1, 3: 3, 4: 4} t2 = {1: 1, 3: 3, 5: 5, 6: 6} ddiff = DeepDiff(t1, t2, verbose_level=2) result = { 'dictionary_item_removed': { 'root[4]': 4 }, 'dictionary_item_added': { 'root[6]': 6, 'root[5]': 5 } } assert result == ddiff def test_diffs_dates(self): t1 = datetime.date(2016, 8, 8) t2 = datetime.date(2016, 8, 7) ddiff = DeepDiff(t1, t2) result = { 'values_changed': { 'root': { 'new_value': t2, 'old_value': t1 } } } assert result == ddiff def test_diffs_timedeltas(self): t1 = datetime.timedelta(days=1, seconds=12) t2 = datetime.timedelta(days=1, seconds=10) t3 = datetime.timedelta(seconds=(60*60*24) + 12) ddiff = DeepDiff(t1, t2) result = { 'values_changed': { 'root': { 'new_value': t2, 'old_value': t1 } } } assert result == ddiff ddiff = DeepDiff(t1, t3) result = {} assert result == ddiff def test_diffs_times(self): t1 = datetime.time(1, 1) t2 = datetime.time(1, 2) t3 = datetime.time(1, 1) expected_result = { 'values_changed': { 'root': { 'new_value': t2, 'old_value': t1 } } } assert DeepDiff(t1, t2) == expected_result assert DeepDiff(t1, t3) == {} def test_diffs_uuid1(self): t1 = uuid.uuid1() t2 = uuid.uuid1() ddiff = DeepDiff(t1, t2) result = { 'values_changed': { 'root': { 'new_value': t2, 'old_value': t1 } } } assert result == ddiff ddiff = DeepDiff(t1, t1) result = {} assert result == ddiff def test_diffs_uuid3(self): t1 = uuid.uuid3(uuid.NAMESPACE_DNS, 'python.org') t2 = uuid.uuid3(uuid.NAMESPACE_DNS, 'stackoverflow.com') ddiff = DeepDiff(t1, t2) result = { 'values_changed': { 'root': { 'new_value': t2, 'old_value': t1 } } } assert result == ddiff ddiff = DeepDiff(t1, t1) result = {} assert result == ddiff def test_diffs_uuid4(self): t1 = uuid.uuid4() t2 = uuid.uuid4() ddiff = DeepDiff(t1, t2) result = { 'values_changed': { 'root': { 'new_value': t2, 'old_value': t1 } } } assert result == ddiff ddiff = DeepDiff(t1, t1) result = {} assert result == ddiff def test_diffs_uuid5(self): t1 = uuid.uuid5(uuid.NAMESPACE_DNS, 'python.org') t2 = uuid.uuid5(uuid.NAMESPACE_DNS, 'stackoverflow.com') ddiff = DeepDiff(t1, t2) result = { 'values_changed': { 'root': { 'new_value': t2, 'old_value': t1 } } } assert result == ddiff ddiff = DeepDiff(t1, t1) result = {} assert result == ddiff def test_string_difference(self): t1 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": "world"}} t2 = {1: 1, 2: 4, 3: 3, 4: {"a": "hello", "b": "world!"}} ddiff = DeepDiff(t1, t2) result = { 'values_changed': { 'root[2]': { 'old_value': 2, 'new_value': 4 }, "root[4]['b']": { 'old_value': 'world', 'new_value': 'world!' } } } assert result == ddiff def test_diffs_equal_strings_when_not_identical(self): t1 = 'hello' t2 = 'hel' t2 += 'lo' assert t1 is not t2 ddiff = DeepDiff(t1, t2) assert {} == ddiff def test_string_difference2(self): t1 = { 1: 1, 2: 2, 3: 3, 4: { "a": "hello", "b": "world!\nGoodbye!\n1\n2\nEnd" } } t2 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": "world\n1\n2\nEnd"}} ddiff = DeepDiff(t1, t2) result = { 'values_changed': { "root[4]['b']": { 'diff': '--- \n+++ \n@@ -1,5 +1,4 @@\n-world!\n-Goodbye!\n+world\n 1\n 2\n End', 'new_value': 'world\n1\n2\nEnd', 'old_value': 'world!\nGoodbye!\n1\n2\nEnd' } } } assert result == ddiff def test_string_difference_ignore_case(self): t1 = "Hello" t2 = "hello" ddiff = DeepDiff(t1, t2) result = {'values_changed': {'root': {'new_value': 'hello', 'old_value': 'Hello'}}} assert result == ddiff ddiff = DeepDiff(t1, t2, ignore_string_case=True) result = {} assert result == ddiff def test_string_dict_key_ignore_case(self): t1 = {'User': {'AboutMe': 1, 'ALIAS': 1}} t2 = {'User': {'Alias': 1, 'AboutMe': 1}} ddiff = DeepDiff(t1, t2) result = {'dictionary_item_added': ["root['User']['Alias']"], 'dictionary_item_removed': ["root['User']['ALIAS']"]} assert result == ddiff ddiff = DeepDiff(t1, t2, ignore_string_case=True) result = {} assert result == ddiff def test_string_list_ignore_case(self): t1 = ['AboutMe', 'ALIAS'] t2 = ['aboutme', 'alias'] ddiff = DeepDiff(t1, t2) result = {'values_changed': {'root[0]': {'new_value': 'aboutme', 'old_value': 'AboutMe'}, 'root[1]': {'new_value': 'alias', 'old_value': 'ALIAS'}}} assert result == ddiff ddiff = DeepDiff(t1, t2, ignore_string_case=True) result = {} assert result == ddiff def test_diff_quote_in_string(self): t1 = { "a']['b']['c": 1 } t2 = { "a']['b']['c": 2 } diff = DeepDiff(t1, t2) expected = {'values_changed': {'''root["a']['b']['c"]''': {'new_value': 2, 'old_value': 1}}} assert expected == diff def test_diff_quote_and_double_quote_in_string(self): t1 = {'''a'"a''': 1} t2 = {'''a'"a''': 2} diff = DeepDiff(t1, t2) expected = {'values_changed': {'root["a\'"a"]': {'new_value': 2, 'old_value': 1}}} assert expected == diff def test_bytes(self): t1 = { 1: 1, 2: 2, 3: 3, 4: { "a": b"hello", "b": b"world!\nGoodbye!\n1\n2\nEnd", "c": b"\x80", } } t2 = { 1: 1, 2: 2, 3: 3, 4: { "a": b"hello", "b": b"world\n1\n2\nEnd", "c": b'\x81', } } ddiff = DeepDiff(t1, t2) result = { 'values_changed': { "root[4]['b']": { 'diff': '--- \n+++ \n@@ -1,5 +1,4 @@\n-world!\n-Goodbye!\n+world\n 1\n 2\n End', 'new_value': b'world\n1\n2\nEnd', 'old_value': b'world!\nGoodbye!\n1\n2\nEnd' }, "root[4]['c']": { 'new_value': b'\x81', 'old_value': b'\x80' } } } assert result == ddiff def test_unicode(self): t1 = { 1: 1, 2: 2, 3: 3, 4: { "a": "hello", "b": "world!\nGoodbye!\n1\n2\nEnd" } } t2 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": "world\n1\n2\nEnd"}} ddiff = DeepDiff(t1, t2) result = { 'values_changed': { "root[4]['b']": { 'diff': '--- \n+++ \n@@ -1,5 +1,4 @@\n-world!\n-Goodbye!\n+world\n 1\n 2\n End', 'new_value': 'world\n1\n2\nEnd', 'old_value': 'world!\nGoodbye!\n1\n2\nEnd' } } } assert result == ddiff def test_type_change(self): t1 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": [1, 2, 3]}} t2 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": "world\n\n\nEnd"}} ddiff = DeepDiff(t1, t2) result = { 'type_changes': { "root[4]['b']": { 'old_type': list, 'new_value': 'world\n\n\nEnd', 'old_value': [1, 2, 3], 'new_type': str } } } assert result == ddiff def test_list_difference(self): t1 = { 1: 1, 2: 2, 3: 3, 4: { "a": "hello", "b": [1, 2, 'to_be_removed', 'to_be_removed2'] } } t2 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": [1, 2]}} ddiff = DeepDiff(t1, t2) result = { 'iterable_item_removed': { "root[4]['b'][2]": "to_be_removed", "root[4]['b'][3]": 'to_be_removed2' } } assert result == ddiff def test_list_difference_add(self): t1 = [1, 2] t2 = [1, 2, 3, 5] ddiff = DeepDiff(t1, t2) result = {'iterable_item_added': {'root[2]': 3, 'root[3]': 5}} assert result == ddiff def test_list_difference2(self): t1 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": [1, 2, 3, 10, 12]}} t2 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": [1, 3, 2]}} result = { 'values_changed': { "root[4]['b'][2]": { 'new_value': 2, 'old_value': 3 }, "root[4]['b'][1]": { 'new_value': 3, 'old_value': 2 } }, 'iterable_item_removed': { "root[4]['b'][3]": 10, "root[4]['b'][4]": 12 } } ddiff = DeepDiff(t1, t2) assert result == ddiff def test_list_difference3(self): t1 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": [1, 2, 5]}} t2 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": [1, 3, 2, 5]}} ddiff = DeepDiff(t1, t2) expected = {'iterable_item_added': {"root[4]['b'][1]": 3}} assert expected == ddiff def test_list_difference4(self): t1 = ["a", "b", "d", "e"] t2 = ["a", "b", "c", "d", "e"] ddiff = DeepDiff(t1, t2) result = {'iterable_item_added': {'root[2]': 'c'}} assert result == ddiff def test_list_difference5(self): t1 = ["a", "b", "d", "e", "f", "g"] t2 = ["a", "b", "c", "d", "e", "f"] ddiff = DeepDiff(t1, t2) result = {'iterable_item_added': {'root[2]': 'c'}, 'iterable_item_removed': {'root[5]': 'g'}} assert result == ddiff def test_list_difference_with_tiny_variations(self): t1 = ['a', 'b', 'c', 'd'] t2 = ['f', 'b', 'a', 'g'] values = { 'a': 2.0000000000000027, 'b': 2.500000000000005, 'c': 2.000000000000002, 'd': 3.000000000000001, 'f': 2.000000000000003, 'g': 3.0000000000000027, } ddiff = DeepDiff(t1, t2) result = { 'values_changed': { 'root[0]': { 'new_value': 'f', 'old_value': 'a' }, 'root[2]': { 'new_value': 'a', 'old_value': 'c' }, 'root[3]': { 'new_value': 'g', 'old_value': 'd' } } } assert result == ddiff ddiff2 = DeepDiff(t1, t2, zip_ordered_iterables=True) assert result == ddiff2 # Now we change the characters with numbers with tiny variations t3 = [2.0000000000000027, 2.500000000000005, 2.000000000000002, 3.000000000000001] t4 = [2.000000000000003, 2.500000000000005, 2.0000000000000027, 3.0000000000000027] ddiff3 = DeepDiff(t3, t4) expected = {'values_changed': {}} for path, report in result['values_changed'].items(): expected['values_changed'][path] = { 'new_value': values[report['new_value']], 'old_value': values[report['old_value']], } assert expected == ddiff3 def test_list_of_booleans(self): t1 = [False, False, True, True] t2 = [False, False, False, True] ddiff = DeepDiff(t1, t2) assert {'values_changed': {'root[2]': {'new_value': False, 'old_value': True}}} == ddiff def test_set_of_none(self): """ https://github.com/seperman/deepdiff/issues/64 """ ddiff = DeepDiff(set(), {None}) assert {'set_item_added': {'root[None]'}} == ddiff def test_list_that_contains_dictionary(self): t1 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": [1, 2, {1: 1, 2: 2}]}} t2 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": [1, 2, {1: 3}]}} ddiff = DeepDiff(t1, t2) result = { 'dictionary_item_removed': {"root[4]['b'][2][2]"}, 'values_changed': { "root[4]['b'][2][1]": { 'old_value': 1, 'new_value': 3 } } } assert result == ddiff def test_set(self): t1 = {1, 2, 8} t2 = {1, 2, 3, 5} ddiff = DeepDiff(t1, t2) result = { 'set_item_added': {'root[3]', 'root[5]'}, 'set_item_removed': {'root[8]'} } assert result == ddiff def test_set_strings(self): t1 = {"veggies", "tofu"} t2 = {"veggies", "tofu", "seitan"} ddiff = DeepDiff(t1, t2) result = {'set_item_added': {"root['seitan']"}} assert result == ddiff def test_frozenset(self): t1 = frozenset([1, 2, 'B']) t2 = frozenset([1, 2, 3, 5]) ddiff = DeepDiff(t1, t2) result = { 'set_item_added': {'root[3]', 'root[5]'}, 'set_item_removed': {"root['B']"} } assert result == ddiff def test_tuple(self): t1 = (1, 2, 8) t2 = (1, 2, 3, 5) ddiff = DeepDiff(t1, t2) result = { 'values_changed': { 'root[2]': { 'old_value': 8, 'new_value': 3 } }, 'iterable_item_added': { 'root[3]': 5 } } assert result == ddiff def test_named_tuples(self): from collections import namedtuple Point = namedtuple('Point', ['x', 'y']) t1 = Point(x=11, y=22) t2 = Point(x=11, y=23) ddiff = DeepDiff(t1, t2) result = { 'values_changed': { 'root.y': { 'old_value': 22, 'new_value': 23 } } } assert result == ddiff def test_enums(self): class MyEnum(Enum): A = 1 B = 2 ddiff = DeepDiff(MyEnum.A, MyEnum(1)) result = {} assert ddiff == result ddiff = DeepDiff(MyEnum.A, MyEnum.B) result = { 'values_changed': { 'root.name': { 'old_value': 'A', 'new_value': 'B' }, 'root.value': { 'old_value': 1, 'new_value': 2 }, } } assert ddiff == result def test_enum_ignore_type_change(self): diff = DeepDiff("book", MyEnum1.book) expected = { 'type_changes': {'root': {'old_type': str, 'new_type': MyEnum1, 'old_value': 'book', 'new_value': MyEnum1.book}}} assert expected == diff diff2 = DeepDiff("book", MyEnum1.book, ignore_type_in_groups=[(Enum, str)]) assert not diff2 diff3 = DeepDiff("book", MyEnum2.book, ignore_type_in_groups=[(Enum, str)]) assert not diff3 def test_enum_use_enum_value1(self): diff = DeepDiff("book", MyEnum2.book, use_enum_value=True) assert not diff def test_enum_use_enum_value_in_dict_key(self): diff = DeepDiff({"book": 2}, {MyEnum2.book: 2}, use_enum_value=True) assert not diff def test_precompiled_regex(self): pattern_1 = re.compile('foo') pattern_2 = re.compile('foo') pattern_3 = re.compile('foo', flags=re.I) pattern_4 = re.compile('(foo)') pattern_5 = re.compile('bar') # same object ddiff = DeepDiff(pattern_1, pattern_1) result = {} assert ddiff == result # same pattern, different object ddiff = DeepDiff(pattern_1, pattern_2) result = {} assert ddiff == result # same pattern, different flags ddiff = DeepDiff(pattern_1, pattern_3) result = { 'values_changed': { 'root.flags': { 'new_value': 34, 'old_value': 32, }, } } assert ddiff == result # same pattern, different groups ddiff = DeepDiff(pattern_1, pattern_4) result = { 'values_changed': { 'root.pattern': { 'new_value': '(foo)', 'old_value': 'foo', }, 'root.groups': { 'new_value': 1, 'old_value': 0, }, } } assert ddiff == result # different pattern ddiff = DeepDiff(pattern_1, pattern_5) result = { 'values_changed': { 'root.pattern': { 'new_value': 'bar', 'old_value': 'foo', }, } } assert ddiff == result def test_custom_objects_change(self): t1 = CustomClass(1) t2 = CustomClass(2) ddiff = DeepDiff(t1, t2) result = { 'values_changed': { 'root.a': { 'old_value': 1, 'new_value': 2 } } } assert result == ddiff def test_custom_objects_slot_change(self): class ClassA: __slots__ = ('x', 'y') def __init__(self, x, y): self.x = x self.y = y t1 = ClassA(1, 1) t2 = ClassA(1, 2) ddiff = DeepDiff(t1, t2) result = { 'values_changed': { 'root.y': { 'old_value': 1, 'new_value': 2 } } } assert result == ddiff def test_custom_class_changes_with_slot_changes(self): class ClassA: __slots__ = ['x', 'y'] def __init__(self, x, y): self.x = x self.y = y class ClassB: __slots__ = ['x'] ddiff = DeepDiff(ClassA, ClassB) result = {'type_changes': {'root': {'old_type': ClassA, 'new_type': ClassB}}} assert result == ddiff def test_custom_class_changes_with_slot_change_when_ignore_type(self): class ClassA: __slots__ = ['x', 'y'] def __init__(self, x, y): self.x = x self.y = y class ClassB: __slots__ = ['x'] ddiff = DeepDiff(ClassA, ClassB, ignore_type_in_groups=[(ClassA, ClassB)]) result = {'attribute_removed': ['root.y']} assert result == ddiff def test_custom_objects_slot_in_group_change(self): class ClassA: __slots__ = ('x', 'y') def __init__(self, x, y): self.x = x self.y = y class ClassB(ClassA): pass t1 = ClassA(1, 1) t2 = ClassB(1, 1) ddiff = DeepDiff(t1, t2, ignore_type_in_groups=[(ClassA, ClassB)]) result = {} assert result == ddiff def test_custom_class_changes_none_when_ignore_type(self): ddiff1 = DeepDiff({'a': None}, {'a': 1}, ignore_type_subclasses=True, ignore_type_in_groups=[(int, float)]) result = { 'type_changes': { "root['a']": { 'old_type': type(None), 'new_type': int, 'old_value': None, 'new_value': 1 } } } assert result == ddiff1 ddiff2 = DeepDiff({'a': None}, {'a': '1'}, ignore_type_subclasses=True, ignore_type_in_groups=[(str, int, float, None)]) assert {'values_changed': {"root['a']": {'new_value': '1', 'old_value': None}}} == ddiff2 ddiff3 = DeepDiff({'a': '1'}, {'a': None}, ignore_type_subclasses=True, ignore_type_in_groups=[(str, int, float, None)]) assert {'values_changed': {"root['a']": {'new_value': None, 'old_value': '1'}}} == ddiff3 ddiff4 = DeepDiff({'a': {'b': None}}, {'a': {'b': '1'}}, ignore_type_subclasses=True, ignore_type_in_groups=[(str, int, float, None)]) assert {'values_changed': {"root['a']['b']": {'new_value': '1', 'old_value': None}}} == ddiff4 ddiff5 = DeepDiff({'a': {'b': '1'}}, {'a': {'b': None}}, ignore_type_subclasses=True, ignore_type_in_groups=[(str, int, float, None)]) assert {'values_changed': {"root['a']['b']": {'new_value': None, 'old_value': '1'}}} == ddiff5 def test_custom_object_changes_when_ignore_type_in_groups(self): class ClassA: def __init__(self, x, y): self.x = x self.y = y class ClassB: def __init__(self, x): self.x = x class ClassC(ClassB): def __repr__(self): return "obj c" obj_a = ClassA(1, 2) obj_c = ClassC(3) ddiff = DeepDiff(obj_a, obj_c, ignore_type_in_groups=[(ClassA, ClassB)], ignore_type_subclasses=True) result = {'type_changes': {'root': {'old_type': ClassA, 'new_type': ClassC, 'old_value': obj_a, 'new_value': obj_c}}} assert result == ddiff ddiff = DeepDiff(obj_a, obj_c, ignore_type_in_groups=[(ClassA, ClassB)], ignore_type_subclasses=False) result = {'values_changed': {'root.x': {'new_value': 3, 'old_value': 1}}, 'attribute_removed': ['root.y']} assert result == ddiff def test_custom_objects_slot_in_parent_class_change(self): class ClassA: __slots__ = ['x'] class ClassB(ClassA): __slots__ = ['y'] def __init__(self, x, y): self.x = x self.y = y t1 = ClassB(1, 1) t2 = ClassB(2, 1) ddiff = DeepDiff(t1, t2) result = { 'values_changed': { 'root.x': { 'old_value': 1, 'new_value': 2 } } } assert result == ddiff def test_custom_objects_with_single_protected_slot(self): class ClassA: __slots__ = '__a' def __init__(self): self.__a = 1 def __str__(self): return str(self.__a) t1 = ClassA() t2 = ClassA() ddiff = DeepDiff(t1, t2) assert {} == ddiff def test_custom_objects_with_weakref_in_slots(self): class ClassA: __slots__ = ['a', '__weakref__'] def __init__(self, a): self.a = a t1 = ClassA(1) t2 = ClassA(2) diff = DeepDiff(t1, t2) result = { 'values_changed': { 'root.a': { 'new_value': 2, 'old_value': 1 } }, } assert result == diff def get_custom_objects_add_and_remove(self): class ClassA: a = 1 def __init__(self, b): self.b = b self.d = 10 t1 = ClassA(1) t2 = ClassA(2) t2.c = "new attribute" del t2.d return t1, t2 def test_custom_objects_add_and_remove(self): t1, t2 = self.get_custom_objects_add_and_remove() ddiff = DeepDiff(t1, t2) result = { 'attribute_added': {'root.c'}, 'values_changed': { 'root.b': { 'new_value': 2, 'old_value': 1 } }, 'attribute_removed': {'root.d'} } assert result == ddiff def test_custom_objects_add_and_remove_verbose(self): t1, t2 = self.get_custom_objects_add_and_remove() ddiff = DeepDiff(t1, t2, verbose_level=2) result = { 'attribute_added': { 'root.c': 'new attribute' }, 'attribute_removed': { 'root.d': 10 }, 'values_changed': { 'root.b': { 'new_value': 2, 'old_value': 1 } } } assert result == ddiff def get_custom_object_with_added_removed_methods(self): class ClassA: VAL = 1 VAL2 = 2 def method_a(self): pass t1 = ClassA() t2 = ClassA() def method_b(self): pass def method_c(self): return "hello" t2.method_b = method_b t2.method_a = method_c # Note that we are comparing ClassA instances. method_a originally was in ClassA # But we also added another version of it to t2. So it comes up as # added attribute. return t1, t2 def test_custom_objects_add_and_remove_method(self): t1, t2 = self.get_custom_object_with_added_removed_methods() ddiff = DeepDiff(t1, t2) result = {'attribute_added': {'root.method_a', 'root.method_b'}} assert result == ddiff def test_custom_objects_add_and_remove_method_verbose(self): t1, t2 = self.get_custom_object_with_added_removed_methods() ddiff = DeepDiff(t1, t2, verbose_level=2) assert 'root.method_a' in ddiff['attribute_added'] assert 'root.method_b' in ddiff['attribute_added'] def test_set_of_custom_objects(self): member1 = CustomClass(13, 37) member2 = CustomClass(13, 37) t1 = {member1} t2 = {member2} ddiff = DeepDiff(t1, t2) result = {} assert result == ddiff def test_dictionary_of_custom_objects(self): member1 = CustomClass(13, 37) member2 = CustomClass(13, 37) t1 = {1: member1} t2 = {1: member2} ddiff = DeepDiff(t1, t2) result = {} assert result == ddiff def test_dictionary_with_string_keys1(self): t1 = {"veggie": "carrots"} t2 = {"meat": "carrots"} diff = DeepDiff(t1, t2, threshold_to_diff_deeper=0) assert {'dictionary_item_added': ["root['meat']"], 'dictionary_item_removed': ["root['veggie']"]} == diff def test_dictionary_with_string_keys_threshold_to_diff_deeper(self): t1 = {"veggie": "carrots"} t2 = {"meat": "carrots"} diff = DeepDiff(t1, t2, threshold_to_diff_deeper=0.33) assert {'values_changed': {'root': {'new_value': {'meat': 'carrots'}, 'old_value': {'veggie': 'carrots'}}}} == diff def test_dictionary_with_numeric_keys(self): t1 = {Decimal('10.01'): "carrots"} t2 = {10.01: "carrots"} diff = DeepDiff(t1, t2, threshold_to_diff_deeper=0) assert {'dictionary_item_added': ["root[10.01]"], 'dictionary_item_removed': ["root[Decimal('10.01')]"]} == diff diff2 = DeepDiff(t1, t2) assert {'values_changed': {'root': {'new_value': {10.01: 'carrots'}, 'old_value': {Decimal('10.01'): 'carrots'}}}} == diff2 def test_loop(self): class LoopTest: def __init__(self, a): self.loop = self self.a = a t1 = LoopTest(1) t2 = LoopTest(2) ddiff = DeepDiff(t1, t2) result = { 'values_changed': { 'root.a': { 'old_value': 1, 'new_value': 2 } } } assert result == ddiff def test_loop2(self): class LoopTestA: def __init__(self, a): self.loop = LoopTestB self.a = a class LoopTestB: def __init__(self, a): self.loop = LoopTestA self.a = a t1 = LoopTestA(1) t2 = LoopTestA(2) ddiff = DeepDiff(t1, t2) result = { 'values_changed': { 'root.a': { 'old_value': 1, 'new_value': 2 } } } assert result == ddiff def test_loop3(self): class LoopTest: def __init__(self, a): self.loop = LoopTest self.a = a t1 = LoopTest(1) t2 = LoopTest(2) ddiff = DeepDiff(t1, t2) result = { 'values_changed': { 'root.a': { 'old_value': 1, 'new_value': 2 } } } assert result == ddiff def test_loop_in_lists(self): t1 = [1, 2, 3] t1.append(t1) t2 = [1, 2, 4] t2.append(t2) ddiff = DeepDiff(t1, t2) result = { 'values_changed': { 'root[2]': { 'new_value': 4, 'old_value': 3 } } } assert result == ddiff def test_loop_in_lists2(self): t1 = [1, 2, [3]] t1[2].append(t1) t2 = [1, 2, [4]] t2[2].append(t2) ddiff = DeepDiff(t1, t2) result = { 'values_changed': { 'root[2][0]': { 'old_value': 3, 'new_value': 4 } } } assert result == ddiff def test_decimal(self): t1 = {1: Decimal('10.1')} t2 = {1: Decimal('2.2')} ddiff = DeepDiff(t1, t2) result = { 'values_changed': { 'root[1]': { 'new_value': Decimal('2.2'), 'old_value': Decimal('10.1') } } } assert result == ddiff def test_unicode_string_type_changes(self): """ These tests were written when DeepDiff was in Python 2. Writing b"你好" throws an exception in Python 3 so can't be used for testing. These tests are currently useless till they are rewritten properly. """ unicode_string = {"hello": "你好"} ascii_string = {"hello": "你好"} ddiff = DeepDiff(unicode_string, ascii_string) result = {} assert result == ddiff def test_unicode_string_value_changes(self): unicode_string = {"hello": "你好"} ascii_string = {"hello": "你好hello"} ddiff = DeepDiff(unicode_string, ascii_string) result = { 'values_changed': { "root['hello']": { 'old_value': '你好', 'new_value': '你好hello' } } } assert result == ddiff def test_unicode_string_value_and_type_changes(self): unicode_string = {"hello": "你好"} ascii_string = {"hello": "你好hello"} ddiff = DeepDiff(unicode_string, ascii_string) # In python3, all string is unicode, so these 2 strings only diff # in values result = { 'values_changed': { "root['hello']": { 'new_value': '你好hello', 'old_value': '你好' } } } assert result == ddiff def test_int_to_unicode_string(self): t1 = 1 ascii_string = "你好" ddiff = DeepDiff(t1, ascii_string) # In python3, all string is unicode, so these 2 strings only diff # in values result = { 'type_changes': { 'root': { 'old_type': int, 'new_type': str, 'old_value': 1, 'new_value': '你好' } } } assert result == ddiff def test_int_to_unicode(self): t1 = 1 unicode_string = "你好" ddiff = DeepDiff(t1, unicode_string) # In python3, all string is unicode, so these 2 strings only diff # in values result = { 'type_changes': { 'root': { 'old_type': int, 'new_type': str, 'old_value': 1, 'new_value': '你好' } } } assert result == ddiff @pytest.mark.parametrize("test_num, t1, t2, ignore_numeric_type_changes, significant_digits, number_format_notation, result", [ (1, 43.265798602382986, 43.71677762295505, False, 0, "f", {'values_changed': {'root': {'new_value': 43.71677762295505, 'old_value': 43.265798602382986}}}), # Note that it rounds the number so one becomes 43 and the other one is 44 (2, Decimal('2.5'), Decimal('1.5'), False, 0, "f", {}), (3, Decimal('2.5'), Decimal('1.5'), False, 1, "f", {'values_changed': {'root': {'new_value': Decimal('1.5'), 'old_value': Decimal('2.5')}}}), (4, Decimal('2.5'), Decimal(2.5), False, 3, "f", {}), (5, 1024, 1022, False, 2, "e", {}), (6, {"key": [Decimal('2.0001'), Decimal('20000.0001')]}, {"key": [2.0002, 20000.0002]}, True, 4, "e", {'values_changed': {"root['key'][0]": {'new_value': 2.0002, 'old_value': Decimal('2.0001')}}}), (7, [Decimal("999.99999999")], [Decimal("999.9999999999")], False, 6, "f", {}), ]) def test_significant_digits_and_notation(self, test_num, t1, t2, ignore_numeric_type_changes, significant_digits, number_format_notation, result): ddiff = DeepDiff(t1, t2, significant_digits=significant_digits, number_format_notation=number_format_notation, ignore_numeric_type_changes=ignore_numeric_type_changes) assert result == ddiff, f"test_significant_digits_and_notation #{test_num} failed." def test_significant_digits_for_complex_imaginary_part(self): t1 = 1.23 + 1.222254j t2 = 1.23 + 1.222256j ddiff = DeepDiff(t1, t2, significant_digits=4) assert {} == ddiff result = { 'values_changed': { 'root': { 'new_value': (1.23 + 1.222256j), 'old_value': (1.23 + 1.222254j) } } } ddiff = DeepDiff(t1, t2, significant_digits=5) assert result == ddiff def test_significant_digits_for_complex_real_part(self): t1 = 1.23446879 + 1.22225j t2 = 1.23446764 + 1.22225j ddiff = DeepDiff(t1, t2, significant_digits=5) assert {} == ddiff def test_significant_digits_for_list_of_floats(self): t1 = [1.2344, 5.67881, 6.778879] t2 = [1.2343, 5.67882, 6.778878] ddiff = DeepDiff(t1, t2, significant_digits=3) assert {} == ddiff ddiff = DeepDiff(t1, t2, significant_digits=4) result = { 'values_changed': { 'root[0]': { 'new_value': 1.2343, 'old_value': 1.2344 } } } assert result == ddiff ddiff = DeepDiff(t1, t2, significant_digits=5) result = { 'values_changed': { 'root[0]': { 'new_value': 1.2343, 'old_value': 1.2344 }, 'root[1]': { 'new_value': 5.67882, 'old_value': 5.67881 } } } assert result == ddiff ddiff = DeepDiff(t1, t2) ddiff2 = DeepDiff(t1, t2, significant_digits=6) assert ddiff2 == ddiff def test_negative_significant_digits(self): with pytest.raises(ValueError): DeepDiff(1, 1, significant_digits=-1) @pytest.mark.parametrize("t1, t2, significant_digits, expected_result", [ (10, 10.0, 5, {}), (10, 10.2, 5, {'values_changed': {'root': {'new_value': 10.2, 'old_value': 10}}}), (10, 10.2, 0, {}), (Decimal(10), 10, 0, {}), (Decimal(10), 10, 10, {}), (Decimal(10), 10.0, 0, {}), (Decimal(10), 10.0, 10, {}), (Decimal('10.0'), 10.0, 5, {}), (Decimal('10.01'), 10.01, 1, {}), (Decimal('10.01'), 10.01, 2, {}), (Decimal('10.01'), 10.01, 5, {}), (Decimal('10.01'), 10.01, 8, {}), (Decimal('10.010'), 10.01, 3, {}), (Decimal('100000.1'), 100000.1, 0, {}), (Decimal('100000.1'), 100000.1, 1, {}), (Decimal('100000.1'), 100000.1, 5, {}), (Decimal('100000'), 100000.1, 0, {}), (Decimal('100000'), 100000.1, 1, {'values_changed': {'root': {'new_value': 100000.1, 'old_value': Decimal('100000')}}}), (np_float64(123.93420232), 123.93420232, 0, {}), ]) def test_decimal_digits(self, t1, t2, significant_digits, expected_result): ddiff = DeepDiff(t1, t2, ignore_numeric_type_changes=True, ignore_string_type_changes=True, significant_digits=significant_digits) assert expected_result == ddiff @pytest.mark.parametrize('test_num, t1, t2, log_scale_similarity_threshold, expected', [ ( 1, {'foo': 110, 'bar': 306}, # t1 {'foo': 140, 'bar': 298}, # t2 0.01, # threshold {'values_changed': {"root['foo']": {'new_value': 140, 'old_value': 110}, "root['bar']": {'new_value': 298, 'old_value': 306}}}, # expected ), ( 2, {'foo': 110, 'bar': 306}, # t1 {'foo': 140, 'bar': 298}, # t2 0.1, # threshold {'values_changed': {"root['foo']": {'new_value': 140, 'old_value': 110}}}, # expected ), ( 2, {'foo': 110, 'bar': 306}, # t1 {'foo': 140, 'bar': 298}, # t2 0.3, # threshold {}, # expected ), ]) def test_log_scale(self, test_num, t1, t2, log_scale_similarity_threshold, expected): diff = DeepDiff(t1, t2, use_log_scale=True, log_scale_similarity_threshold=log_scale_similarity_threshold) assert expected == diff, f"test_log_scale #{test_num} failed." def test_ignore_type_in_groups(self): t1 = [1, 2, 3] t2 = [1.0, 2.0, 3.0] ddiff = DeepDiff(t1, t2, ignore_type_in_groups=DeepDiff.numbers) assert not ddiff def test_ignore_type_in_groups2(self): t1 = [1, 2, 3] t2 = [1.0, 2.0, 3.3] ddiff = DeepDiff(t1, t2, ignore_type_in_groups=DeepDiff.numbers) result = {'values_changed': {'root[2]': {'new_value': 3.3, 'old_value': 3}}} assert result == ddiff def test_ignore_type_in_groups3(self): t1 = {Decimal('10.01'): "carrots"} t2 = {10.01: "carrots"} diff1 = DeepDiff(t1, t2, threshold_to_diff_deeper=0) diff2 = DeepDiff(t1, t2, ignore_numeric_type_changes=True) diff3 = DeepDiff(t1, t2, ignore_type_in_groups=DeepDiff.numbers) assert {'dictionary_item_added': ["root[10.01]"], 'dictionary_item_removed': ["root[Decimal('10.01')]"]} == diff1 assert {} == diff2 == diff3 def test_ignore_type_in_groups_just_numbers(self): t1 = [1, 2, 3, 'a'] t2 = [1.0, 2.0, 3.3, b'a'] ddiff = DeepDiff(t1, t2, ignore_type_in_groups=[DeepDiff.numbers]) result = {'values_changed': {'root[2]': {'new_value': 3.3, 'old_value': 3}}, 'type_changes': {'root[3]': {'new_type': bytes, 'new_value': b'a', 'old_type': str, 'old_value': 'a'}} } assert result == ddiff def test_ignore_type_in_groups_numbers_and_strings(self): t1 = [1, 2, 3, 'a'] t2 = [1.0, 2.0, 3.3, b'a'] ddiff = DeepDiff(t1, t2, ignore_type_in_groups=[DeepDiff.numbers, DeepDiff.strings]) result = {'values_changed': {'root[2]': {'new_value': 3.3, 'old_value': 3}}} assert result == ddiff def test_ignore_type_in_groups_none_and_objects(self): t1 = [1, 2, 3, 'a', None] t2 = [1.0, 2.0, 3.3, b'a', 'hello'] ddiff = DeepDiff(t1, t2, ignore_type_in_groups=[(1, 1.0), (None, str, bytes)]) result = { 'values_changed': { 'root[2]': { 'new_value': 3.3, 'old_value': 3 }, 'root[4]': { 'new_value': 'hello', 'old_value': None } } } assert result == ddiff def test_ignore_type_in_groups_str_and_datetime(self): now = datetime.datetime(2022, 4, 10, 0, 40, 41, 357857) t1 = [1, 2, 3, 'a', now] t2 = [1, 2, 3, 'a', 'now'] ddiff = DeepDiff(t1, t2, ignore_type_in_groups=[(str, bytes, datetime.datetime)]) now_utc = now.replace(tzinfo=datetime.timezone.utc) result = {'values_changed': {'root[4]': {'new_value': 'now', 'old_value': now_utc}}} assert result == ddiff def test_ignore_type_in_groups_float_vs_decimal(self): diff = DeepDiff(float('0.1'), Decimal('0.1'), ignore_type_in_groups=[(float, Decimal)], significant_digits=2) assert not diff @pytest.mark.parametrize("t1, t2, significant_digits, result", [ ([0.1], [Decimal('0.10')], 55, {'values_changed': {'root[0]': {'new_value': Decimal('0.10'), 'old_value': 0.1}}}), # Due to floating point arithmetics with high significant digits. ([0.1], [Decimal('0.10')], 5, {}), # Same inputs as above but with significant digits that is low. ([-0.1], [-Decimal('0.10')], 5, {}), ([-Decimal('0.102')], [-Decimal('0.10')], 2, {}), ([1], [Decimal('1.00000002')], 3, {}), ]) def test_ignore_numeric_type_changes_numbers_when_decimal(self, t1, t2, significant_digits, result): ddiff = DeepDiff(t1, t2, ignore_numeric_type_changes=True, significant_digits=significant_digits) assert result == ddiff @pytest.mark.skip(reason="REMAPPING DISABLED UNTIL KEY NAMES CHANGE AGAIN IN FUTURE") def test_base_level_dictionary_remapping(self): """ Since subclassed dictionaries that override __getitem__ treat newdict.get(key) differently than newdict['key'], we are unable to create a unittest with self.assertIn() and have to resort to fetching the values of two keys and making sure they are the same value. """ t1 = {1: 1, 2: 2} t2 = {2: 2, 3: 3} ddiff = DeepDiff(t1, t2) assert ddiff['dic_item_added'] == ddiff['dictionary_item_added'] assert ddiff['dic_item_removed'] == ddiff['dictionary_item_removed'] @pytest.mark.skip(reason="REMAPPING DISABLED UNTIL KEY NAMES CHANGE AGAIN IN FUTURE") def test_index_and_repeat_dictionary_remapping(self): t1 = [1, 3, 1, 4] t2 = [4, 4, 1] ddiff = DeepDiff(t1, t2, ignore_order=True, report_repetition=True) assert ddiff['repetition_change']['root[0]']['newindexes'] == ddiff['repetition_change']['root[0]']['new_indexes'] assert ddiff['repetition_change']['root[0]']['newrepeat'] == ddiff['repetition_change']['root[0]']['new_repeat'] assert ddiff['repetition_change']['root[0]']['oldindexes'] == ddiff['repetition_change']['root[0]']['old_indexes'] assert ddiff['repetition_change']['root[0]']['oldrepeat'] == ddiff['repetition_change']['root[0]']['old_repeat'] @pytest.mark.skip(reason="REMAPPING DISABLED UNTIL KEY NAMES CHANGE AGAIN IN FUTURE") def test_value_and_type_dictionary_remapping(self): t1 = {1: 1, 2: 2} t2 = {1: 1, 2: '2'} ddiff = DeepDiff(t1, t2) assert ddiff['type_changes']['root[2]']['newtype'] == ddiff['type_changes']['root[2]']['new_type'] assert ddiff['type_changes']['root[2]']['newvalue'] == ddiff['type_changes']['root[2]']['new_value'] assert ddiff['type_changes']['root[2]']['oldtype'] == ddiff['type_changes']['root[2]']['old_type'] assert ddiff['type_changes']['root[2]']['oldvalue'] == ddiff['type_changes']['root[2]']['old_value'] def test_skip_type(self): l1 = logging.getLogger("test") l2 = logging.getLogger("test2") t1 = {"log": l1, 2: 1337} t2 = {"log": l2, 2: 1337} ddiff = DeepDiff(t1, t2, exclude_types={logging.Logger}) assert {} == ddiff t1 = {"log": "book", 2: 1337} t2 = {"log": l2, 2: 1337} ddiff = DeepDiff(t1, t2, exclude_types={logging.Logger}) assert {} == ddiff def test_skip_path1(self): t1 = { "for life": "vegan", "ingredients": ["no meat", "no eggs", "no dairy"] } t2 = { "for life": "vegan", "ingredients": ["veggies", "tofu", "soy sauce"] } ddiff = DeepDiff(t1, t2, exclude_paths={"root['ingredients']"}) assert {} == ddiff def test_skip_path2(self): t1 = { "for life": "vegan", "ingredients": ["no meat", "no eggs", "no dairy"] } t2 = {"for life": "vegan"} ddiff = DeepDiff(t1, t2, exclude_paths={"root['ingredients']"}) assert {} == ddiff def test_skip_path2_key_names(self): t1 = { "for life": "vegan", "ingredients": ["no meat", "no eggs", "no dairy"] } t2 = {"for life": "vegan"} ddiff = DeepDiff(t1, t2, exclude_paths={"ingredients"}) assert {} == ddiff def test_skip_path2_reverse(self): t1 = { "for life": "vegan", "ingredients": ["no meat", "no eggs", "no dairy"] } t2 = {"for life": "vegan"} ddiff = DeepDiff(t2, t1, exclude_paths={"root['ingredients']"}) assert {} == ddiff def test_exclude_path_when_prefix_of_exclude_path_matches1(self): diff = DeepDiff({}, {'foo': '', 'bar': ''}, exclude_paths=['foo', 'bar']) assert not diff def test_include_path3(self): t1 = { "for life": "vegan", "ingredients": ["no meat", "no eggs", "no dairy"] } t2 = {"for life": "vegan"} ddiff = DeepDiff(t2, t1, include_paths={"root['for_life']"}) assert {} == ddiff def test_include_path3_with_just_key_names(self): t1 = { "for life": "vegan", "ingredients": ["no meat", "no eggs", "no dairy"] } t2 = {"for life": "vegan"} ddiff = DeepDiff(t1, t2, include_paths={"for_life"}) assert {} == ddiff def test_include_path4_nested(self): t1 = { "foo": {"bar": "potato"}, "ingredients": ["no meat", "no eggs", "no dairy"] } t2 = { "foo": {"bar": "banana"}, "ingredients": ["bread", "cheese"] } ddiff = DeepDiff(t1, t2, include_paths="foo") assert { 'values_changed': { "root['foo']['bar']": { 'new_value': 'banana', 'old_value': 'potato' } } } == ddiff def test_include_path5(self): diff = DeepDiff( { 'name': 'Testname', 'code': 'bla', 'noneCode': 'blu', }, { 'uid': '12345', 'name': 'Testname2', }, ) diff2 = DeepDiff( { 'name': 'Testname', 'code': 'bla', 'noneCode': 'blu', }, { 'uid': '12345', 'name': 'Testname2', }, include_paths = "root['name']" ) expected = {'values_changed': {'root': {'new_value': {'uid': '12345', 'name': 'Testname2'}, 'old_value': {'name': 'Testname', 'code': 'bla', 'noneCode': 'blu'}}}} expected2 = {'values_changed': {"root['name']": {'new_value': 'Testname2', 'old_value': 'Testname'}}} assert expected == diff assert expected2 == diff2 def test_include_path6(self): t1 = [1, 2, 3, [4, 5, {6: 7}]] t2 = [1, 2, 3, [4, 5, {6: 1000}]] diff = DeepDiff( t1, t2, ) diff2 = DeepDiff( t1, t2, include_paths = "root[3]" ) diff3 = DeepDiff( t1, t2, include_paths = "root[4]" ) expected = {'values_changed': {'root[3][2][6]': {'new_value': 1000, 'old_value': 7}}} assert expected == diff assert diff == diff2 assert not diff3 def test_skip_path4(self): t1 = { "for life": "vegan", "ingredients": ["no meat", "no eggs", "no dairy"] } t2 = {"for life": "vegan", "zutaten": ["veggies", "tofu", "soy sauce"]} ddiff = DeepDiff(t1, t2, exclude_paths={"root['ingredients']"}) assert 'dictionary_item_added' in ddiff, {} assert 'dictionary_item_removed' not in ddiff, {} def test_skip_path5(self): t1 = [{'cn': 'tuser', 'first_name': 'Test', 'last_name': 'User', 'name': 'Test User', 'email': 'tuser@example.com'}] t2 = [{'name': 'Test User', 'email': 'tuser@example.com'}] diff = DeepDiff( t1, t2, ignore_order=True, exclude_paths={ "root[0]['cn']", "root[0]['first_name']", "root[0]['last_name']" }) assert not diff def test_skip_custom_object_path(self): t1 = CustomClass(1) t2 = CustomClass(2) ddiff = DeepDiff(t1, t2, exclude_paths=['root.a']) result = {} assert result == ddiff def test_skip_list_path(self): t1 = ['a', 'b'] t2 = ['a'] ddiff = DeepDiff(t1, t2, exclude_paths=['root[1]']) result = {} assert result == ddiff def test_skip_dictionary_path(self): t1 = {1: {2: "a"}} t2 = {1: {}} ddiff = DeepDiff(t1, t2, exclude_paths=['root[1][2]']) result = {} assert result == ddiff def test_skip_dictionary_path_with_custom_object(self): obj1 = CustomClass(1) obj2 = CustomClass(2) t1 = {1: {2: obj1}} t2 = {1: {2: obj2}} ddiff = DeepDiff(t1, t2, exclude_paths=['root[1][2].a']) result = {} assert result == ddiff # TODO: fix it for python 3.5, 3.6 and pypy3 def test_skip_regexp(self): t1 = [{'a': 1, 'b': 2}, {'c': 4, 'b': 5}] t2 = [{'a': 1, 'b': 3}, {'c': 4, 'b': 5}] ddiff = DeepDiff(t1, t2, exclude_regex_paths=[r"root\[\d+\]\['b'\]"]) result = {} assert result == ddiff def test_include_obj_callback(self): def include_obj_callback(obj, path): return True if "include" in path or isinstance(obj, int) else False t1 = {"x": 10, "y": "b", "z": "c", "include_me": "a"} t2 = {"x": 10, "y": "c", "z": "b", "include_me": "b"} ddiff = DeepDiff(t1, t2, include_obj_callback=include_obj_callback) result = {'values_changed': {"root['include_me']": {'new_value': "b", 'old_value': "a"}}} assert result == ddiff assert {"root['include_me']"} == ddiff.affected_paths assert {"include_me"} == ddiff.affected_root_keys def test_include_obj_callback_strict(self): def include_obj_callback_strict(obj, path): return True if isinstance(obj, int) and obj > 10 else False t1 = {"x": 11, "y": 10, "z": "c"} t2 = {"x": 12, "y": 12, "z": "c"} ddiff = DeepDiff(t1, t2, include_obj_callback_strict=include_obj_callback_strict) result = {'values_changed': {"root['x']": {'new_value': 12, 'old_value': 11}}} assert result == ddiff assert {"root['x']"} == ddiff.affected_paths assert {"x"} == ddiff.affected_root_keys def test_skip_exclude_obj_callback(self): def exclude_obj_callback(obj, path): return True if "skip" in path or isinstance(obj, int) else False t1 = {"x": 10, "y": "b", "z": "c", "skip_1": 0} t2 = {"x": 12, "y": "b", "z": "c", "skip_2": 0} ddiff = DeepDiff(t1, t2, exclude_obj_callback=exclude_obj_callback) result = {} assert result == ddiff def test_skip_exclude_obj_callback_strict(self): def exclude_obj_callback_strict(obj, path): return True if isinstance(obj, int) and obj > 10 else False t1 = {"x": 10, "y": "b", "z": "c"} t2 = {"x": 12, "y": "b", "z": "c"} ddiff = DeepDiff(t1, t2, exclude_obj_callback_strict=exclude_obj_callback_strict) result = {'values_changed': {"root['x']": {'new_value': 12, 'old_value': 10}}} assert result == ddiff assert {"root['x']"} == ddiff.affected_paths assert {"x"} == ddiff.affected_root_keys def test_skip_str_type_in_dictionary(self): t1 = {1: {2: "a"}} t2 = {1: {}} ddiff = DeepDiff(t1, t2, exclude_types=[str]) result = {} assert result == ddiff def test_skip_str_type_in_dict_on_list(self): t1 = [{1: "a"}] t2 = [{}] ddiff = DeepDiff(t1, t2, exclude_types=[str]) result = {} assert result == ddiff def test_unknown_parameters(self): with pytest.raises(ValueError): DeepDiff(1, 1, wrong_param=2) def test_bad_attribute(self): class Bad: __slots__ = ['x', 'y'] def __getattr__(self, key): raise AttributeError("Bad item") def __str__(self): return "Bad Object" t1 = Bad() t2 = Bad() ddiff = DeepDiff(t1, t2) result = {} assert result == ddiff def test_dict_none_item_removed(self): t1 = {1: None, 2: 2} t2 = {2: 2} ddiff = DeepDiff(t1, t2) result = { 'dictionary_item_removed': {'root[1]'} } assert result == ddiff def test_list_none_item_removed(self): t1 = [1, 2, None] t2 = [1, 2] ddiff = DeepDiff(t1, t2) result = { 'iterable_item_removed': {'root[2]': None} } assert result == ddiff assert {"root[2]"} == ddiff.affected_paths def test_list_item_removed_from_the_middle(self): t1 = [0, 1, 2, 3, 'bye', 5, 6, 7, 8, 'a', 'b', 'c'] t2 = [0, 1, 2, 3, 5, 6, 7, 8, 'a', 'b', 'c'] diff = DeepDiff(t1, t2) result = {'iterable_item_removed': {'root[4]': 'bye'}} assert result == diff assert {"root[4]"} == diff.affected_paths assert {4} == diff.affected_root_keys # TODO: we need to support reporting that items have been swapped # def test_item_moved(self): # # currently all the items in the list need to be hashables # t1 = [1, 2, 3, 4] # t2 = [4, 2, 3, 1] # diff = DeepDiff(t1, t2) # result = {} # it should show that those items are swapped. # assert result == diff def test_list_item_values_replace_in_the_middle(self): t1 = [0, 1, 2, 3, 'bye', 5, 6, 7, 8, 'a', 'b', 'c'] t2 = [0, 1, 2, 3, 'see', 'you', 'later', 5, 6, 7, 8, 'a', 'b', 'c'] diff = DeepDiff(t1, t2) result = { 'values_changed': { 'root[4]': { 'old_value': 'bye', 'new_value': 'see', } }, 'iterable_item_added': { 'root[5]': 'you', 'root[6]': 'later' } } assert result == diff assert {'root[5]', 'root[6]', 'root[4]'} == diff.affected_paths assert {4, 5, 6} == diff.affected_root_keys def test_non_subscriptable_iterable(self): def gen1(): yield 42 yield 1337 yield 31337 def gen2(): yield 42 yield 1337 t1 = gen1() t2 = gen2() ddiff = DeepDiff(t1, t2) result = {'iterable_item_removed': {'root[2]': 31337}} # Note: In text-style results, we currently pretend this stuff is subscriptable for readability assert result == ddiff assert {"root[2]"} == ddiff.affected_paths @pytest.mark.parametrize('t1, t2, params, expected_result', [ (float('nan'), float('nan'), {}, ['values_changed']), (float('nan'), float('nan'), {'ignore_nan_inequality': True}, []), ([1, float('nan')], [1, float('nan')], {'ignore_nan_inequality': True}, []), ]) @pytest.mark.skipif(pypy3, reason="some versions of pypy3 have nan==nan") def test_ignore_nan_inequality(self, t1, t2, params, expected_result): assert expected_result == list(DeepDiff(t1, t2, **params).keys()) @pytest.mark.parametrize('ignore_order, ignore_private_variables, expected', [ (True, True, {}), (False, True, {}), (True, False, {'values_changed': {"root[0]['schema']['items']['__ref']": {'new_value': 2, 'old_value': 1}}}), (False, False, {'values_changed': {"root[0]['schema']['items']['__ref']": {'new_value': 2, 'old_value': 1}}}), ]) def test_private_variables(self, ignore_order, ignore_private_variables, expected): t1 = [{'in': 'body', 'name': 'params', 'description': 'params', 'required': True, 'schema': {'type': 'array', 'items': {'__ref': 1}}}] t2 = [{'in': 'body', 'name': 'params', 'description': 'params', 'required': True, 'schema': {'type': 'array', 'items': {'__ref': 2}}}] diff = DeepDiff(t1, t2, ignore_order=ignore_order, ignore_private_variables=ignore_private_variables) assert expected == diff def test_group_by1(self): t1 = [ {'id': 'AA', 'name': 'Joe', 'last_name': 'Nobody'}, {'id': 'BB', 'name': 'James', 'last_name': 'Blue'}, {'id': 'CC', 'name': 'Mike', 'last_name': 'Apple'}, ] t2 = [ {'id': 'AA', 'name': 'Joe', 'last_name': 'Nobody'}, {'id': 'BB', 'name': 'James', 'last_name': 'Brown'}, {'id': 'CC', 'name': 'Mike', 'last_name': 'Apple'}, ] diff = DeepDiff(t1, t2) expected = {'values_changed': {"root[1]['last_name']": { 'new_value': 'Brown', 'old_value': 'Blue'}}} assert expected == diff diff = DeepDiff(t1, t2, group_by='id') expected_grouped = {'values_changed': {"root['BB']['last_name']": { 'new_value': 'Brown', 'old_value': 'Blue'}}} assert expected_grouped == diff def test_group_by2_when_repeats(self): t1 = [ {'id': 'AA', 'name': 'Joe', 'last_name': 'Nobody', 'int_id': 2}, {'id': 'BB', 'name': 'James', 'last_name': 'Blue', 'int_id': 20}, {'id': 'BB', 'name': 'Jimmy', 'last_name': 'Red', 'int_id': 3}, {'id': 'CC', 'name': 'Mike', 'last_name': 'Apple', 'int_id': 4}, ] t2 = [ {'id': 'AA', 'name': 'Joe', 'last_name': 'Nobody', 'int_id': 2}, {'id': 'BB', 'name': 'James', 'last_name': 'Brown', 'int_id': 20}, {'id': 'CC', 'name': 'Mike', 'last_name': 'Apple', 'int_id': 4}, ] diff = DeepDiff(t1, t2, group_by='id', group_by_sort_key='name') expected_grouped = { 'values_changed': { "root['BB'][0]['last_name']": { 'new_value': 'Brown', 'old_value': 'Blue' } }, 'iterable_item_removed': { "root['BB'][1]": { 'name': 'Jimmy', 'last_name': 'Red', 'int_id': 3 } } } assert expected_grouped == diff diff2 = DeepDiff(t1, t2, group_by='id', group_by_sort_key=lambda x: x['name']) assert expected_grouped == diff2 def test_group_by3_when_repeats_and_group_by_list(self): t1 = [ {'id': 'AA', 'name': 'Joe', 'last_name': 'Nobody', 'int_id': 2}, {'id': 'BB', 'name': 'James', 'last_name': 'Blue', 'int_id': 20}, {'id': 'BB', 'name': 'Jimmy', 'last_name': 'Red', 'int_id': 3}, {'id': 'CC', 'name': 'Mike', 'last_name': 'Apple', 'int_id': 4}, ] t2 = [ {'id': 'AA', 'name': 'Joe', 'last_name': 'Nobody', 'int_id': 2}, {'id': 'BB', 'name': 'James', 'last_name': 'Brown', 'int_id': 20}, {'id': 'CC', 'name': 'Mike', 'last_name': 'Apple', 'int_id': 4}, ] diff1 = DeepDiff(t1, t2, group_by=['id', 'name']) expected_grouped = { 'dictionary_item_removed': ["root['BB']['Jimmy']"], 'values_changed': { "root['BB']['James']['last_name']": { 'new_value': 'Brown', 'old_value': 'Blue' } } } assert expected_grouped == diff1 diff2 = DeepDiff(t1, t2, group_by=['id', 'name'], group_by_sort_key='int_id') expected_grouped = { 'dictionary_item_removed': ["root['BB']['Jimmy']"], 'values_changed': { "root['BB']['James'][0]['last_name']": { 'new_value': 'Brown', 'old_value': 'Blue' } } } assert expected_grouped == diff2 def test_group_by_key_missing(self): t1 = [ {'id': 'AA', 'name': 'Joe', 'last_name': 'Nobody'}, {'id': 'BB', 'name': 'James', 'last_name': 'Blue'}, {'name': 'Mike', 'last_name': 'Apple'}, ] t2 = [ {'id': 'AA', 'name': 'Joe', 'last_name': 'Nobody'}, {'id': 'BB', 'name': 'James', 'last_name': 'Blue'}, {'id': 'CC', 'name': 'Mike', 'last_name': 'Apple'}, ] diff = DeepDiff(t1, t2, group_by='id') expected = {'dictionary_item_added': ["root[2]['id']"]} assert expected == diff def test_group_by_not_dict1(self): t1 = [ {'id': 'AA', 'name': 'Joe', 'last_name': 'Nobody'}, None, {'id': 'CC', 'name': 'Mike', 'last_name': 'Apple'}, ] t2 = [ {'id': 'AA', 'name': 'Joe', 'last_name': 'Nobody'}, {'id': 'BB'}, {'id': 'CC', 'name': 'Mike', 'last_name': 'Apple'}, ] diff = DeepDiff(t1, t2, group_by='id') expected = { 'type_changes': { 'root[1]': { 'old_type': None.__class__, 'new_type': dict, 'old_value': None, 'new_value': { 'id': 'BB' } } }, } assert expected == diff def test_group_by_not_dict2(self): t1 = [ {'id': 'AA', 'name': 'Joe', 'last_name': 'Nobody'}, {'id': 'BB'}, {'id': 'CC', 'name': 'Mike', 'last_name': 'Apple'}, ] t2 = [ {'id': 'AA', 'name': 'Joe', 'last_name': 'Nobody'}, None, {'id': 'CC', 'name': 'Mike', 'last_name': 'Apple'}, ] diff = DeepDiff(t1, t2, group_by='id') expected = { 'type_changes': { 'root[1]': { 'old_type': dict, 'new_type': None.__class__, 'new_value': None, 'old_value': { 'id': 'BB' } } }, } assert expected == diff def test_group_by_not_list_of_dicts(self): t1 = {1: 2} t2 = {1: 3} diff = DeepDiff(t1, t2, group_by='id') expected = {'values_changed': {'root[1]': {'new_value': 3, 'old_value': 2}}} assert expected == diff assert {"root[1]"} == diff.affected_paths def test_datetime_in_key(self): now = datetime.datetime(2022, 4, 10, 0, 40, 41, 357857) t1 = {now: 1, now + datetime.timedelta(1): 4} t2 = {now: 2, now + datetime.timedelta(1): 4} diff = DeepDiff(t1, t2) expected = {'values_changed': {f'root[{repr(now)}]': {'new_value': 2, 'old_value': 1}}} assert expected == diff def test_property_values(self): class A: _thing = 0 def __init__(self, a): self.a = a @property def thing(self): A._thing += 1 return A._thing @property def __thing2(self): A._thing += 1 return A._thing diff = DeepDiff(A(1), A(1)) expected = { 'values_changed': { 'root._thing': { 'new_value': 1, 'old_value': 0 }, 'root.thing': { 'new_value': 2, 'old_value': 1 } } } assert expected == diff diff2 = DeepDiff(A(1), A(1), ignore_private_variables=False) expected2 = { 'values_changed': { 'root._A__thing2': { 'new_value': 5, 'old_value': 3 }, 'root._thing': { 'new_value': 5, 'old_value': 3 }, 'root.thing': { 'new_value': 6, 'old_value': 4 } } } assert expected2 == diff2 def test_diffs_rrules(self): from dateutil.rrule import MONTHLY, rrule d = DeepDiff( rrule(freq=MONTHLY, count=5, dtstart=datetime.datetime(2014, 12, 31)), rrule(freq=MONTHLY, count=4, dtstart=datetime.datetime(2011, 12, 31)), ) assert d == { "values_changed": { "root[0]": { "new_value": datetime.datetime(2011, 12, 31, 0, 0, tzinfo=datetime.timezone.utc), "old_value": datetime.datetime(2014, 12, 31, 0, 0, tzinfo=datetime.timezone.utc), }, "root[1]": { "new_value": datetime.datetime(2012, 1, 31, 0, 0, tzinfo=datetime.timezone.utc), "old_value": datetime.datetime(2015, 1, 31, 0, 0, tzinfo=datetime.timezone.utc), }, "root[2]": { "new_value": datetime.datetime(2012, 3, 31, 0, 0, tzinfo=datetime.timezone.utc), "old_value": datetime.datetime(2015, 3, 31, 0, 0, tzinfo=datetime.timezone.utc), }, "root[3]": { "new_value": datetime.datetime(2012, 5, 31, 0, 0, tzinfo=datetime.timezone.utc), "old_value": datetime.datetime(2015, 5, 31, 0, 0, tzinfo=datetime.timezone.utc), }, }, "iterable_item_removed": {"root[4]": datetime.datetime(2015, 7, 31, 0, 0)}, } def test_pydantic1(self): class Foo(PydanticBaseModel): thing: int = None that: str t1 = Foo(thing=1, that='yes') t2 = Foo(thing=2, that='yes') diff = DeepDiff(t1, t2) expected = {'values_changed': {'root.thing': {'new_value': 2, 'old_value': 1}}} assert expected == diff def test_pydantic2(self): class Foo(PydanticBaseModel): thing: int = None that: str class Bar(PydanticBaseModel): stuff: List[Foo] t1 = Bar(stuff=[Foo(thing=1, that='yes')]) t2 = Bar(stuff=[Foo(thing=2, that='yes')]) diff = DeepDiff(t1, t2) expected = {'values_changed': {'root.stuff[0].thing': {'new_value': 2, 'old_value': 1}}} assert expected == diff def test_dataclass1(self): t1 = MyDataClass(1, 4) t2 = MyDataClass(2, 4) diff = DeepDiff(t1, t2, exclude_regex_paths=["any"]) assert {'values_changed': {'root.val': {'new_value': 2, 'old_value': 1}}} == diff def test_dataclass2(self): @dataclass(frozen=True) class MyDataClass: val: int val2: int t1 = { MyDataClass(1, 4): 10, MyDataClass(2, 4): 20, } t2 = { MyDataClass(1, 4): 10, MyDataClass(2, 4): 10, } diff = DeepDiff(t1, t2, exclude_regex_paths=["any"]) assert {'values_changed': {'root[MyDataClass(val=2,val2=4)]': {'new_value': 10, 'old_value': 20}}} == diff def test_group_by_with_none_key_and_ignore_case(self): """Test that group_by works with None keys when ignore_string_case is True""" dict1 = [{'txt_field': 'FULL_NONE', 'group_id': None}, {'txt_field': 'FULL', 'group_id': 'a'}] dict2 = [{'txt_field': 'PARTIAL_NONE', 'group_id': None}, {'txt_field': 'PARTIAL', 'group_id': 'a'}] diff = DeepDiff( dict1, dict2, ignore_order=True, group_by='group_id', ignore_string_case=True ) expected = {'values_changed': {"root[None]['txt_field']": {'new_value': 'partial_none', 'old_value': 'full_none'}, "root['a']['txt_field']": {'new_value': 'partial', 'old_value': 'full'} } } assert expected == diff def test_affected_root_keys_when_dict_empty(self): diff = DeepDiff({}, {1:1, 2:2}, threshold_to_diff_deeper=0) assert [1, 2] == diff.affected_root_keys diff2 = DeepDiff({}, {1:1, 2:2}) assert [] == diff2.affected_root_keys def test_range1(self): range1 = range(0, 10) range2 = range(0, 8) diff = DeepDiff(range1, range2) assert {'iterable_item_removed': {'root[8]': 8, 'root[9]': 9}} == diff def test_group_by_that_has_integers(self): """Test that group_by with integer keys doesn't add type prefixes like 'int:33'""" t1 = [{'row_num_in_file': 33, 'value': 'old'}] t2 = [{'row_num_in_file': 33, 'value': 'new'}] diff = DeepDiff(t1, t2, group_by='row_num_in_file', ignore_string_type_changes=True) # Verify that the diff key contains the integer 33 without type prefix changes = diff.get('values_changed', {}) assert len(changes) == 1 key = list(changes.keys())[0] assert "int:" not in key assert "[33]" in key or "['33']" in key qlustered-deepdiff-41c7265/tests/test_diff_tree.py000066400000000000000000000141721516241264500222600ustar00rootroot00000000000000#!/usr/bin/env python import pytest from deepdiff import DeepDiff from deepdiff.helper import pypy3, notpresent from deepdiff.model import DictRelationship, NonSubscriptableIterableRelationship import logging logging.disable(logging.CRITICAL) class TestDeepDiffTree: """DeepDiff Tests.""" def test_same_objects(self): t1 = {1: 1, 2: 2, 3: 3} t2 = t1 ddiff = DeepDiff(t1, t2) res = ddiff.tree assert res == {} def test_significant_digits_signed_zero(self): t1 = 0.00001 t2 = -0.0001 ddiff = DeepDiff(t1, t2, significant_digits=2) res = ddiff.tree assert res == {} t1 = 1 * 10**-12 t2 = -1 * 10**-12 ddiff = DeepDiff(t1, t2, significant_digits=10) res = ddiff.tree assert res == {} def test_item_added_extensive(self): t1 = {'one': 1, 'two': 2, 'three': 3, 'four': 4} t2 = {'one': 1, 'two': 2, 'three': 3, 'four': 4, 'new': 1337} ddiff = DeepDiff(t1, t2) res = ddiff.tree (key, ) = res.keys() assert key == 'dictionary_item_added' assert len(res['dictionary_item_added']) == 1 (added1, ) = res['dictionary_item_added'] # assert added1 DiffLevel chain is valid at all assert added1.up.down == added1 assert added1.down is None assert added1.up.up is None assert added1.all_up == added1.up assert added1.up.all_down == added1 assert added1.report_type == 'dictionary_item_added' # assert DiffLevel chain points to the objects we entered assert added1.up.t1 == t1 assert added1.up.t2 == t2 assert added1.t1 is notpresent assert added1.t2 == 1337 # assert DiffLevel child relationships are correct assert added1.up.t1_child_rel is None assert isinstance(added1.up.t2_child_rel, DictRelationship) assert added1.up.t2_child_rel.parent == added1.up.t2 assert added1.up.t2_child_rel.child == added1.t2 assert added1.up.t2_child_rel.param == 'new' assert added1.up.path() == "root" assert added1.path() == "root['new']" def test_item_added_and_removed(self): t1 = {'one': 1, 'two': 2, 'three': 3, 'four': 4} t2 = {'one': 1, 'two': 4, 'three': 3, 'five': 5, 'six': 6} ddiff = DeepDiff(t1, t2, view='tree') assert set(ddiff.keys()) == { 'dictionary_item_added', 'dictionary_item_removed', 'values_changed' } assert len(ddiff['dictionary_item_added']) == 2 assert len(ddiff['dictionary_item_removed']) == 1 def test_item_added_and_removed2(self): t1 = {2: 2, 4: 4} t2 = {2: "b", 5: 5} ddiff = DeepDiff(t1, t2, view='tree') assert set(ddiff.keys()), { 'dictionary_item_added', 'dictionary_item_removed', 'type_changes' } assert len(ddiff['dictionary_item_added']) == 1 assert len(ddiff['dictionary_item_removed']) == 1 def test_non_subscriptable_iterable(self): t1 = (i for i in [42, 1337, 31337]) t2 = (i for i in [ 42, 1337, ]) ddiff = DeepDiff(t1, t2, view='tree') (change, ) = ddiff['iterable_item_removed'] assert set(ddiff.keys()) == {'iterable_item_removed'} assert len(ddiff['iterable_item_removed']) == 1 assert change.up.t1 == t1 assert change.up.t2 == t2 assert change.report_type == 'iterable_item_removed' assert change.t1 == 31337 assert change.t2 is notpresent assert isinstance(change.up.t1_child_rel, NonSubscriptableIterableRelationship) assert change.up.t2_child_rel is None def test_non_subscriptable_iterable_path(self): t1 = (i for i in [42, 1337, 31337]) t2 = (i for i in [42, 1337, ]) ddiff = DeepDiff(t1, t2, view='tree') (change, ) = ddiff['iterable_item_removed'] # testing path assert change.path() is None assert change.path(force='yes') == 'root(unrepresentable)' assert change.path(force='fake') == 'root[2]' def test_report_type_in_iterable(self): a = {"temp": ["a"]} b = {"temp": ["b"]} ddiff = DeepDiff(a, b, ignore_order=True, view="tree") report_type = ddiff['values_changed'][0].report_type assert 'values_changed' == report_type def test_significant_digits(self): ddiff = DeepDiff( [0.012, 0.98], [0.013, 0.99], significant_digits=1, view='tree') assert ddiff == {} def test_significant_digits_with_sets(self): ddiff = DeepDiff( {0.012, 0.98}, {0.013, 0.99}, significant_digits=1, view='tree') assert ddiff == {} def test_significant_digits_with_ignore_order(self): ddiff = DeepDiff( [0.012, 0.98], [0.013, 0.99], significant_digits=1, ignore_order=True, view='tree') assert ddiff == {} def test_repr(self): t1 = {1, 2, 8} t2 = {1, 2, 3, 5} ddiff = DeepDiff(t1, t2, view='tree') str(ddiff) class TestDeepDiffTreeWithNumpy: """DeepDiff Tests with Numpy.""" @pytest.mark.skipif(pypy3, reason="Numpy is not compatible with pypy3") def test_diff_with_numpy(self): import numpy as np a1 = np.array([1.23, 1.66, 1.98]) a2 = np.array([1.23, 1.66, 1.98]) d1 = {'np': a1} d2 = {'np': a2} ddiff = DeepDiff(d1, d2) res = ddiff.tree assert res == {} @pytest.mark.skipif(pypy3, reason="Numpy is not compatible with pypy3") def test_diff_with_empty_seq(self): a1 = {"empty": []} a2 = {"empty": []} ddiff = DeepDiff(a1, a2) assert ddiff == {} class TestDeepAdditions: """Tests for Additions and Subtractions.""" @pytest.mark.skip(reason="Not currently implemented") def test_adding_list_diff(self): t1 = [1, 2] t2 = [1, 2, 3, 5] ddiff = DeepDiff(t1, t2, view='tree') addition = ddiff + t1 assert addition == t2 qlustered-deepdiff-41c7265/tests/test_distance.py000066400000000000000000000217161516241264500221250ustar00rootroot00000000000000import pytest import datetime from decimal import Decimal from deepdiff import DeepDiff from deepdiff.helper import np from deepdiff.diff import DELTA_VIEW, CUTOFF_RANGE_ERROR_MSG from deepdiff.deephash import sha256hex from deepdiff.distance import ( _get_item_length, _get_numbers_distance, get_numeric_types_distance, _get_numpy_array_distance, DISTANCE_CALCS_NEEDS_CACHE) from tests import CustomClass class TestDeepDistance: @pytest.mark.parametrize('diff, expected_length', [ ( {'set_item_added': {'root[1]': {6}}}, 1 ), ( { 'iterable_items_added_at_indexes': { 'root': { 0: 7, 6: 8, 1: 4, 2: 4, 5: 4 } }, 'iterable_items_removed_at_indexes': { 'root': { 6: 6, 0: 5 } } }, 5 ), ( { 'type_changes': { 'root': { 'old_type': float, 'new_type': Decimal, 'new_value': Decimal('3.2') } } }, 2 ), ]) def test_item_length(self, diff, expected_length): length = _get_item_length(diff) assert expected_length == length def test_distance_of_the_same_objects(self): t1 = [{1, 2, 3}, {4, 5, 6}] t2 = [{4, 5, 6}, {1, 2, 3}] ddiff = DeepDiff(t1, t2, ignore_order=True, cache_purge_level=0) assert {} == ddiff assert 0 == _get_item_length(ddiff) assert '0' == str(ddiff._get_rough_distance())[:10] assert 9 == ddiff._DistanceMixin__get_item_rough_length(ddiff.t1) assert 9 == ddiff._DistanceMixin__get_item_rough_length(ddiff.t2) def test_distance_of_list_sets(self): t1 = [{1, 2, 3}, {4, 5}] t2 = [{4, 5, 6}, {1, 2, 3}] ddiff = DeepDiff(t1, t2, ignore_order=True, cache_purge_level=0) delta = ddiff._to_delta_dict(report_repetition_required=False) assert {'set_item_added': {'root[1]': {6}}} == delta assert 1 == _get_item_length(ddiff) assert '0.05882352' == str(ddiff._get_rough_distance())[:10] assert 8 == ddiff._DistanceMixin__get_item_rough_length(ddiff.t1) assert 9 == ddiff._DistanceMixin__get_item_rough_length(ddiff.t2) @pytest.mark.parametrize('verbose_level', [0, 1, 1]) def test_distance_of_list_sets2(self, verbose_level): t1 = [{1, 2, 3}, {4, 5}, {1}] t2 = [{4, 5, 6}, {1, 2, 3}, {1, 4}] ddiff = DeepDiff(t1, t2, ignore_order=True, verbose_level=verbose_level, get_deep_distance=True, cache_purge_level=0) delta = ddiff._to_delta_dict(report_repetition_required=False) assert {'set_item_added': {'root[2]': {4}, 'root[1]': {6}}} == delta assert 2 == _get_item_length(ddiff) assert '0.09090909' == str(ddiff['deep_distance'])[:10] assert 10 == ddiff._DistanceMixin__get_item_rough_length(ddiff.t1) assert 12 == ddiff._DistanceMixin__get_item_rough_length(ddiff.t2) @pytest.mark.parametrize('verbose_level', [0, 1, 1]) def test_distance_of_list_sets_and_strings(self, verbose_level): t1 = [{1, 2, 3}, {4, 5, 'hello', 'right!'}, {4, 5, (2, 4, 7)}] t2 = [{4, 5, 6, (2, )}, {1, 2, 3}, {5, 'hello', 'right!'}] ddiff = DeepDiff(t1, t2, ignore_order=True, view=DELTA_VIEW, verbose_level=verbose_level) delta = ddiff._to_delta_dict(report_repetition_required=False) expected = { 'set_item_removed': { 'root[1]': {4} }, 'iterable_items_added_at_indexes': { 'root': { 0: {(2, ), 4, 5, 6} } }, 'iterable_items_removed_at_indexes': { 'root': { 2: {4, 5, (2, 4, 7)} } } } assert expected == ddiff # If the diff was in delta view, spitting out another delta dict should produce identical results. assert delta == ddiff assert 10 == _get_item_length(ddiff) def test_distance_of_tuple_in_list(self): t1 = {(2,), 4, 5, 6} t2 = {'right!', 'hello', 4, 5} diff = DeepDiff(t1, t2, ignore_order=True, view=DELTA_VIEW, get_deep_distance=True) assert {'set_item_removed': {'root': {(2,), 6}}, 'set_item_added': {'root': {'hello', 'right!'}}} == diff # delta view should not have the distance info in it assert 'get_deep_distance' not in diff def test_get_item_length_when_loops1(self): t1 = [[1, 2, 1, 3]] t1.append(t1) item_length = _get_item_length(t1) assert 8 == item_length def test_get_item_length_when_loops2(self): t1 = {1: 1} t1[2] = t1 item_length = _get_item_length(t1) assert 2 == item_length def test_get_distance_works_event_when_ignore_order_is_false1(self): t1 = 10 t2 = 110 diff = DeepDiff(t1, t2, get_deep_distance=True) dist = diff['deep_distance'] assert dist == Decimal('0.25') def test_get_distance_works_event_when_ignore_order_is_false2(self): t1 = ["a", "b"] t2 = ["a", "b", "c"] diff = DeepDiff(t1, t2, get_deep_distance=True) dist = diff['deep_distance'] assert str(dist)[:4] == '0.14' assert set(diff.keys()) == {'iterable_item_added', 'deep_distance'} def test_get_distance_works_event_when_ignore_order_is_false3(self): t1 = ["a", "b"] t2 = ["a", "b", "c", "d"] diff = DeepDiff(t1, t2, get_deep_distance=True) dist = diff['deep_distance'] assert str(dist)[:4] == '0.25' def test_get_distance_works_event_when_ignore_order_and_different_hasher(self): t1 = ["a", "b", 2] t2 = ["a", "b", "c", 2.2] diff = DeepDiff(t1, t2, ignore_order=True, get_deep_distance=True, cache_size=100, hasher=sha256hex) dist = diff['deep_distance'] assert str(dist)[:4] == '0.33' def test_get_distance_does_not_care_about_the_size_of_string(self): t1 = ["a", "b"] t2 = ["a", "b", "c", "dddddd"] diff = DeepDiff(t1, t2, get_deep_distance=True) dist = diff['deep_distance'] assert str(dist)[:4] == '0.25' def test_get_item_length_custom_class1(self): item = CustomClass(a=10) item_length = _get_item_length(item) assert 2 == item_length def test_get_item_length_custom_class2_loop(self): item = CustomClass(a=10) item.b = item item_length = _get_item_length(item) assert 2 == item_length @pytest.mark.parametrize('num1, num2, max_, expected', [ (10.0, 10, 1, 0), (Decimal('10.1'), Decimal('10.2'), 1, 0.004926108374384236453201970443), (Decimal(10), Decimal(-10), 1, 1), (2, 3, 1, 0.2), (10, -10, .1, .1), (10, -10.1, .1, .1), (10, -10.1, .3, 0.3), ]) def test_get_numbers_distance(self, num1, num2, max_, expected): result = _get_numbers_distance(num1, num2, max_) assert abs(expected - result) < 0.0001 @pytest.mark.parametrize('arr1, arr2', [ (np.array([4.1978, 4.1979, 4.1980]), np.array([4.1971, 4.1879, 4.1981])), (np.array([1, 2, 4]), np.array([4, 2, 3])), ]) def test_numpy_distance_vs_get_numbers_distance(self, arr1, arr2): dist_arr = _get_numpy_array_distance(arr1, arr2) for i in range(3): assert dist_arr[i] == _get_numbers_distance(arr1[i], arr2[i]) @pytest.mark.parametrize('num1, num2, max_, expected', [ (10, -10.1, .3, 0.3), (datetime.datetime(2022, 4, 10, 0, 40, 41, 357857), datetime.datetime(2022, 4, 10, 0, 40, 41, 357857) + datetime.timedelta(days=100), 1, 0.002707370659621624), (1589703146.9556487, 1001589703146.9557, 1, 0.9968306702929068), (datetime.time(10, 11), datetime.time(12, 11), .5, 0.0447093889716), (datetime.timedelta(days=2), datetime.timedelta(12, 11), .5, 0.35714415626180646), (datetime.date(2018, 1, 1), datetime.date(2020, 1, 10), 1, 0.0005013129787148886), ]) def test_get_numeric_types_distance(self, num1, num2, max_, expected): result = get_numeric_types_distance(num1, num2, max_) assert abs(expected - result) < 0.0001 def test_get_rough_length_after_cache_purge(self): diff = DeepDiff([1], ['a']) with pytest.raises(RuntimeError) as excinfo: diff._get_rough_distance() assert DISTANCE_CALCS_NEEDS_CACHE == str(excinfo.value) def test_cutoff_distance_for_pairs_range(self): with pytest.raises(ValueError) as excinfo: DeepDiff(1, 2, cutoff_distance_for_pairs=2) assert CUTOFF_RANGE_ERROR_MSG == str(excinfo.value) qlustered-deepdiff-41c7265/tests/test_fraction.py000066400000000000000000000300641516241264500221340ustar00rootroot00000000000000#!/usr/bin/env python """Tests for fractions.Fraction support in DeepDiff.""" import pytest import logging from fractions import Fraction from decimal import Decimal from functools import partial from deepdiff import DeepDiff, DeepHash from deepdiff.deephash import prepare_string_for_hashing from deepdiff.helper import number_to_string from deepdiff.serialization import json_dumps, json_loads logging.disable(logging.CRITICAL) # Only the prep part of DeepHash. We don't need to test the actual hash function. DeepHashPrep = partial(DeepHash, apply_hash=False) class TestFractionDiff: """Tests for DeepDiff with Fraction objects.""" def test_fraction_value_change(self): t1 = {1: Fraction(1, 3)} t2 = {1: Fraction(2, 3)} ddiff = DeepDiff(t1, t2) result = { 'values_changed': { 'root[1]': { 'new_value': Fraction(2, 3), 'old_value': Fraction(1, 3) } } } assert result == ddiff def test_fraction_no_change(self): t1 = Fraction(1, 3) t2 = Fraction(1, 3) ddiff = DeepDiff(t1, t2) assert {} == ddiff def test_fraction_vs_float_type_change(self): t1 = Fraction(1, 2) t2 = 0.5 ddiff = DeepDiff(t1, t2) assert 'type_changes' in ddiff assert ddiff['type_changes']['root']['old_type'] == Fraction assert ddiff['type_changes']['root']['new_type'] == float def test_fraction_vs_int_type_change(self): t1 = Fraction(2, 1) t2 = 2 ddiff = DeepDiff(t1, t2) assert 'type_changes' in ddiff assert ddiff['type_changes']['root']['old_type'] == Fraction assert ddiff['type_changes']['root']['new_type'] == int def test_fraction_vs_decimal_type_change(self): t1 = Fraction(1, 2) t2 = Decimal('0.5') ddiff = DeepDiff(t1, t2) assert 'type_changes' in ddiff def test_fraction_in_dict(self): t1 = {"a": Fraction(1, 3), "b": Fraction(2, 3)} t2 = {"a": Fraction(1, 3), "b": Fraction(3, 4)} ddiff = DeepDiff(t1, t2) assert 'values_changed' in ddiff assert "root['b']" in ddiff['values_changed'] def test_fraction_in_list(self): t1 = [Fraction(1, 2), Fraction(1, 3)] t2 = [Fraction(1, 2), Fraction(1, 4)] ddiff = DeepDiff(t1, t2) result = { 'values_changed': { 'root[1]': { 'new_value': Fraction(1, 4), 'old_value': Fraction(1, 3) } } } assert result == ddiff def test_fraction_nested(self): t1 = {"data": [{"val": Fraction(1, 3)}]} t2 = {"data": [{"val": Fraction(2, 3)}]} ddiff = DeepDiff(t1, t2) assert 'values_changed' in ddiff assert "root['data'][0]['val']" in ddiff['values_changed'] class TestFractionIgnoreNumericTypeChanges: """Tests for ignore_numeric_type_changes with Fraction.""" def test_fraction_vs_float_ignored(self): t1 = Fraction(1, 2) t2 = 0.5 ddiff = DeepDiff(t1, t2, ignore_numeric_type_changes=True) assert {} == ddiff def test_fraction_vs_int_ignored(self): t1 = Fraction(2, 1) t2 = 2 ddiff = DeepDiff(t1, t2, ignore_numeric_type_changes=True) assert {} == ddiff def test_fraction_vs_decimal_ignored(self): t1 = Fraction(1, 2) t2 = Decimal('0.5') ddiff = DeepDiff(t1, t2, ignore_numeric_type_changes=True) assert {} == ddiff def test_fraction_vs_float_different_values(self): t1 = Fraction(1, 3) t2 = 0.5 ddiff = DeepDiff(t1, t2, ignore_numeric_type_changes=True) assert 'values_changed' in ddiff def test_fraction_vs_float_in_list_ignored(self): t1 = [Fraction(1, 2), Fraction(3, 4)] t2 = [0.5, 0.75] ddiff = DeepDiff(t1, t2, ignore_numeric_type_changes=True) assert {} == ddiff def test_fraction_vs_int_in_dict_ignored(self): t1 = {"a": Fraction(5, 1), "b": Fraction(10, 1)} t2 = {"a": 5, "b": 10} ddiff = DeepDiff(t1, t2, ignore_numeric_type_changes=True) assert {} == ddiff @pytest.mark.parametrize("t1, t2, significant_digits, result", [ ([0.5], [Fraction(1, 2)], 5, {}), ([Fraction(1, 3)], [0.333333], 5, {}), ([Fraction(1, 3)], [Decimal('0.33333')], 5, {}), ([1], [Fraction(1, 1)], 5, {}), ([-Fraction(1, 2)], [-0.5], 5, {}), ([Fraction(22, 7)], [3.14286], 4, {}), ]) def test_ignore_numeric_type_changes_with_fraction(self, t1, t2, significant_digits, result): ddiff = DeepDiff(t1, t2, ignore_numeric_type_changes=True, significant_digits=significant_digits) assert result == ddiff class TestFractionSignificantDigits: """Tests for significant_digits with Fraction.""" def test_fraction_significant_digits_equal(self): t1 = Fraction(1, 3) # 0.333... t2 = Fraction(334, 1000) # 0.334 ddiff = DeepDiff(t1, t2, significant_digits=2) assert {} == ddiff def test_fraction_significant_digits_different(self): t1 = Fraction(1, 3) # 0.333... t2 = Fraction(1, 2) # 0.5 ddiff = DeepDiff(t1, t2, significant_digits=1) assert 'values_changed' in ddiff @pytest.mark.parametrize("test_num, t1, t2, significant_digits, number_format_notation, result", [ (1, Fraction(1, 3), Fraction(334, 1000), 2, "f", {}), (2, Fraction(1, 2), Fraction(499, 1000), 2, "f", {}), (3, Fraction(1, 2), Fraction(1, 3), 0, "f", {}), (4, Fraction(1, 2), Fraction(1, 3), 1, "f", {'values_changed': {'root': {'new_value': Fraction(1, 3), 'old_value': Fraction(1, 2)}}}), (5, Fraction(22, 7), Fraction(355, 113), 2, "f", {}), # Two approximations of pi agree to 2 digits (6, Fraction(22, 7), Fraction(355, 113), 3, "f", {'values_changed': {'root': {'new_value': Fraction(355, 113), 'old_value': Fraction(22, 7)}}}), ]) def test_fraction_significant_digits_and_notation(self, test_num, t1, t2, significant_digits, number_format_notation, result): ddiff = DeepDiff(t1, t2, significant_digits=significant_digits, number_format_notation=number_format_notation) assert result == ddiff, f"test_fraction_significant_digits_and_notation #{test_num} failed." class TestFractionMathEpsilon: """Tests for math_epsilon with Fraction.""" def test_fraction_math_epsilon_close(self): d1 = {"a": Fraction(7175, 1000)} d2 = {"a": Fraction(7174, 1000)} res = DeepDiff(d1, d2, math_epsilon=0.01) assert res == {} def test_fraction_math_epsilon_not_close(self): d1 = {"a": Fraction(7175, 1000)} d2 = {"a": Fraction(7174, 1000)} res = DeepDiff(d1, d2, math_epsilon=0.0001) assert 'values_changed' in res def test_fraction_vs_float_math_epsilon(self): d1 = {"a": Fraction(1, 3)} d2 = {"a": 0.333} res = DeepDiff(d1, d2, math_epsilon=0.001, ignore_numeric_type_changes=True) assert res == {} class TestFractionIgnoreOrder: """Tests for ignore_order with Fraction.""" def test_fraction_ignore_order(self): t1 = [{1: Fraction(1, 3)}, {2: Fraction(2, 3)}] t2 = [{2: Fraction(2, 3)}, {1: Fraction(1, 3)}] ddiff = DeepDiff(t1, t2, ignore_order=True) assert {} == ddiff def test_fraction_ignore_order_with_change(self): t1 = [Fraction(1, 2), Fraction(1, 3)] t2 = [Fraction(1, 3), Fraction(1, 4)] ddiff = DeepDiff(t1, t2, ignore_order=True) assert ddiff != {} class TestFractionAsKey: """Tests for Fraction used as dictionary key.""" def test_fraction_as_dict_key(self): t1 = {Fraction(1, 2): "half"} t2 = {Fraction(1, 2): "one half"} ddiff = DeepDiff(t1, t2) assert 'values_changed' in ddiff def test_fraction_vs_float_key(self): # Fraction(1, 2) == 0.5 and hash(Fraction(1, 2)) == hash(0.5) in Python, # so they resolve to the same dict key. DeepDiff sees no difference. t1 = {Fraction(1, 2): "value"} t2 = {0.5: "value"} ddiff = DeepDiff(t1, t2) assert ddiff == {} def test_fraction_vs_float_key_ignore_numeric(self): t1 = {Fraction(1, 2): "value"} t2 = {0.5: "value"} ddiff = DeepDiff(t1, t2, ignore_numeric_type_changes=True) assert {} == ddiff class TestFractionNumberToString: """Tests for number_to_string with Fraction.""" @pytest.mark.parametrize("t1, t2, significant_digits, number_format_notation, expected_result", [ (Fraction(1, 3), 0.333333, 5, "f", True), (Fraction(1, 2), 0.5, 5, "f", True), (Fraction(1, 2), 0.5, 5, "e", True), (Fraction(1, 3), Fraction(1, 4), 1, "f", ('0.3', '0.2')), (Fraction(22, 7), 3.14286, 4, "f", True), (Fraction(0), 0.0, 5, "f", True), (Fraction(-1, 2), -0.5, 5, "f", True), ]) def test_number_to_string_fraction(self, t1, t2, significant_digits, number_format_notation, expected_result): st1 = number_to_string(t1, significant_digits=significant_digits, number_format_notation=number_format_notation) st2 = number_to_string(t2, significant_digits=significant_digits, number_format_notation=number_format_notation) if expected_result is True: assert st1 == st2 else: assert st1 == expected_result[0] assert st2 == expected_result[1] class TestFractionDeepHash: """Tests for DeepHash with Fraction.""" def test_fraction_hash(self): result = DeepHash(Fraction(1, 3)) assert result[Fraction(1, 3)] def test_fraction_same_value_same_hash(self): result1 = DeepHash(Fraction(1, 2)) result2 = DeepHash(Fraction(1, 2)) assert result1[Fraction(1, 2)] == result2[Fraction(1, 2)] def test_fraction_different_value_different_hash(self): result1 = DeepHash(Fraction(1, 2)) result2 = DeepHash(Fraction(1, 3)) assert result1[Fraction(1, 2)] != result2[Fraction(1, 3)] def test_fraction_vs_float_hash_different_by_default(self): result1 = DeepHash(Fraction(1, 2)) result2 = DeepHash(0.5) assert result1[Fraction(1, 2)] != result2[0.5] def test_fraction_vs_float_hash_same_with_ignore_numeric_type(self): result1 = DeepHash(Fraction(1, 2), ignore_numeric_type_changes=True) result2 = DeepHash(0.5, ignore_numeric_type_changes=True) assert result1[Fraction(1, 2)] == result2[0.5] def test_fraction_hash_prep(self): result = DeepHashPrep(Fraction(1, 3)) assert 'Fraction' in result[Fraction(1, 3)] def test_fraction_hash_prep_ignore_numeric_type(self): result = DeepHashPrep(Fraction(1, 2), ignore_numeric_type_changes=True) assert 'number' in result[Fraction(1, 2)] def test_fraction_hash_significant_digits(self): r1 = DeepHashPrep(Fraction(1, 3), significant_digits=2) r2 = DeepHashPrep(Fraction(334, 1000), significant_digits=2) assert r1[Fraction(1, 3)] == r2[Fraction(334, 1000)] class TestFractionSerialization: """Tests for JSON serialization of Fraction values.""" def test_fraction_to_json(self): t1 = Fraction(1, 3) t2 = Fraction(2, 3) ddiff = DeepDiff(t1, t2) json_str = ddiff.to_json() assert json_str is not None assert '"new_value"' in json_str def test_fraction_integer_value_serialization(self): """Fraction with denominator 1 should serialize as int.""" result = json_dumps(Fraction(5, 1)) assert result == '5' def test_fraction_float_value_serialization(self): """Fraction with denominator != 1 should serialize as float.""" result = json_dumps(Fraction(1, 2)) assert result == '0.5' def test_fraction_json_roundtrip(self): t1 = {"a": Fraction(1, 2), "b": [1, 2]} t2 = {"a": Fraction(3, 4), "b": [1, 3]} ddiff = DeepDiff(t1, t2) json_str = ddiff.to_json() loaded = json_loads(json_str) assert loaded is not None qlustered-deepdiff-41c7265/tests/test_hash.py000077500000000000000000001277771516241264500212770ustar00rootroot00000000000000#!/usr/bin/env python import re import pytest import pytz import uuid6 import logging import datetime import ipaddress from typing import Union from pathlib import Path from collections import namedtuple from functools import partial from enum import Enum from deepdiff import DeepDiff, DeepHash from deepdiff.deephash import ( prepare_string_for_hashing, unprocessed, UNPROCESSED_KEY, BoolObj, HASH_LOOKUP_ERR_MSG, combine_hashes_lists) from deepdiff.helper import pypy3, get_id, number_to_string, np, py_major_version, py_minor_version from tests import CustomClass2 logging.disable(logging.CRITICAL) class ClassC: class_attr = 0 def __init__(self, a, b=None): self.a = a self.b = b def __str__(self): return "({}, {})".format(self.a, self.b) __repr__ = __str__ # Only the prep part of DeepHash. We don't need to test the actual hash function. DeepHashPrep = partial(DeepHash, apply_hash=False) def prep_str(obj, ignore_string_type_changes=True): return obj if ignore_string_type_changes else 'str:{}'.format(obj) class TestDeepHash: def test_dictionary(self): obj = {1: 1} result = DeepHash(obj) assert set(result.keys()) == {1, get_id(obj)} def test_get_hash_by_obj_is_the_same_as_by_obj_get_id(self): a = "a" obj = {1: a} result = DeepHash(obj) assert result[a] def test_deephash_repr(self): obj = "a" result = DeepHash(obj) assert '{"a":"980410da9522db17c3ab8743541f192a5ab27772a6154dbc7795ee909e653a5c"}' == repr(result) def test_deephash_values(self): obj = "a" result = list(DeepHash(obj).values()) assert ['980410da9522db17c3ab8743541f192a5ab27772a6154dbc7795ee909e653a5c'] == result def test_deephash_keys(self): obj = "a" result = list(DeepHash(obj).keys()) assert ["a"] == result def test_deephash_items(self): obj = "a" result = list(DeepHash(obj).items()) assert [('a', '980410da9522db17c3ab8743541f192a5ab27772a6154dbc7795ee909e653a5c')] == result def test_get_hash_by_obj_when_does_not_exist(self): a = "a" obj = {1: a} result = DeepHash(obj) with pytest.raises(KeyError): result[2] def test_datetime(self): now = datetime.datetime.now() a = b = now a_hash = DeepHash(a) b_hash = DeepHash(b) assert a_hash[a] == b_hash[b] def test_date1(self): date = datetime.date(2024, 2, 1) date_hash = DeepHash(date) assert 'd90e95901f85ca09b2536d3cb81a49747c3a4fb14906d6fa0d492713ebb4309c' == date_hash[date] def test_date2(self): item = {'due_date': datetime.date(2024, 2, 1)} result = DeepHash( item, significant_digits=12, number_format_notation='f', ignore_numeric_type_changes=True, ignore_type_in_groups=[{int, float, complex, datetime.datetime, datetime.date, datetime.timedelta, datetime.time}], ignore_type_subclasses=False, ignore_encoding_errors=False, ignore_repetition=True, number_to_string_func=number_to_string, ) assert 'e0d7ec984a0eda44ceb1e3c595f9b805530d715c779483e63a72c67cbce68615' == result[item] def test_datetime_truncate(self): a = datetime.datetime(2020, 5, 17, 22, 15, 34, 913070) b = datetime.datetime(2020, 5, 17, 22, 15, 39, 296583) c = datetime.datetime(2020, 5, 17, 22, 15, 34, 500000) a_hash = DeepHash(a, truncate_datetime='minute') b_hash = DeepHash(b, truncate_datetime='minute') assert a_hash[a] == b_hash[b] a_hash = DeepHash(a, truncate_datetime='second') c_hash = DeepHash(c, truncate_datetime='second') assert a_hash[a] == c_hash[c] def test_get_reserved_keyword(self): hashes = {UNPROCESSED_KEY: 'full item', 'key1': ('item', 'count')} result = DeepHash._getitem(hashes, obj='key1') assert 'item' == result # For reserved keys, it should just grab the object instead of grabbing an item in the tuple object. result = DeepHash._getitem(hashes, obj=UNPROCESSED_KEY) assert 'full item' == result def test_get_key(self): hashes = {'key1': ('item', 'count')} result = DeepHash.get_key(hashes, key='key2', default='banana') assert 'banana' == result def test_list_of_sets(self): a = {1} b = {2} obj = [a, b] result = DeepHash(obj) expected_result = {1, 2, get_id(a), get_id(b), get_id(obj)} assert set(result.keys()) == expected_result def test_bad_attribute(self): class Bad: __slots__ = ['x', 'y'] def __getattr__(self, key): raise AttributeError("Bad item") def __str__(self): return "Bad Object" def __repr__(self): return "".format(id(self)) t1 = Bad() result = DeepHash(t1) expected_result = {t1: unprocessed, UNPROCESSED_KEY: [t1]} assert expected_result == result def test_built_in_hash_not_sensitive_to_bytecode_vs_unicode(self): a = 'hello' b = b'hello' a_hash = DeepHash(a, ignore_string_type_changes=True)[a] b_hash = DeepHash(b, ignore_string_type_changes=True)[b] assert a_hash == b_hash def test_sha1_hash_not_sensitive_to_bytecode_vs_unicode(self): a = 'hello' b = b'hello' a_hash = DeepHash(a, ignore_string_type_changes=True, hasher=DeepHash.sha1hex)[a] b_hash = DeepHash(b, ignore_string_type_changes=True, hasher=DeepHash.sha1hex)[b] assert a_hash == b_hash def test_path(self): a = Path('testdir') b = Path('testdir2') a_hash = DeepHash(a)[a] b_hash = DeepHash(b)[b] assert a_hash != b_hash def test_re(self): import re a = re.compile("asdf.?") a_hash = DeepHash(a)[a] assert not( a_hash is unprocessed) # https://github.com/seperman/deepdiff/issues/494 def test_numpy_bool(self): a = {'b': np.array([True], dtype='bool')} a_hash = DeepHash(a)[a] assert not( a_hash is unprocessed) def test_numpy_datetime64(self): now_dt = datetime.datetime.now() now = np.datetime64(now_dt) later = np.datetime64(now_dt + datetime.timedelta(seconds=10)) a = b = now a_hash = DeepHash(a) b_hash = DeepHash(b) assert a_hash[a] == b_hash[b] later_hash = DeepHash(later) assert a_hash[a] != later_hash[later] def test_uuid6_hash_positive(self): """Positive test case: Same UUID objects should produce the same hash.""" uuid_obj = uuid6.uuid7() hash1 = DeepHash(uuid_obj) hash2 = DeepHash(uuid_obj) assert hash1[uuid_obj] == hash2[uuid_obj] import uuid regular_uuid = uuid.uuid4() hash_regular = DeepHash(regular_uuid) assert hash_regular[regular_uuid] is not unprocessed def test_uuid6_deepdiff_negative(self): """Negative test case: DeepDiff should detect differences between sets containing different UUID objects.""" dummy_id_1 = uuid6.uuid7() dummy_id_2 = uuid6.uuid7() set1 = {dummy_id_1} set2 = {dummy_id_1, dummy_id_2} diff = DeepDiff(set1, set2) assert diff != {} assert 'set_item_added' in diff assert str(dummy_id_2) in str(diff) def test_slots_with_uninitialized_attributes(self): """Test that DeepHash handles classes with __slots__ where not all slots are initialized.""" class PartiallyInitialized: __slots__ = ['x', 'y', 'z'] def __init__(self, x): self.x = x obj = PartiallyInitialized(10) result = DeepHash(obj) # Should not raise AttributeError iternally, and should successfully hash the object assert result[obj] is not unprocessed def test_slots_fully_initialized(self): """Test that DeepHash correctly hashes classes with __slots__ where all slots are initialized.""" class FullyInitialized: __slots__ = ['x', 'y'] def __init__(self, x, y): self.x = x self.y = y obj1 = FullyInitialized(1, 2) obj2 = FullyInitialized(1, 2) obj3 = FullyInitialized(3, 4) hash1 = DeepHash(obj1) hash2 = DeepHash(obj2) hash3 = DeepHash(obj3) # Same values should produce same hash assert hash1[obj1] == hash2[obj2] # Different values should produce different hash assert hash1[obj1] != hash3[obj3] def test_slots_with_no_initialized_attributes(self): """Test that DeepHash handles classes with __slots__ where no slots are initialized.""" class EmptySlots: __slots__ = ['a', 'b', 'c'] def __init__(self): pass obj = EmptySlots() result = DeepHash(obj) assert result[obj] is not unprocessed def test_slots_inheritance(self): """Test that DeepHash handles inherited __slots__ correctly.""" class Base: __slots__ = ['x'] def __init__(self, x): self.x = x class Derived(Base): __slots__ = ['y', 'z'] def __init__(self, x, y): super().__init__(x) self.y = y obj = Derived(1, 2) result = DeepHash(obj) # Should still not raise AttributeError internally for uninitialized 'z' assert result[obj] is not unprocessed def test_slots_deepdiff_comparison(self): """Test that DeepDiff also works correctly with __slots__ classes (incl inherited and uninitialized attributes).""" class Base: __slots__ = ['x'] def __init__(self, x): self.x = x class Derived(Base): __slots__ = ['y', 'z'] def __init__(self, x, y): super().__init__(x) self.y = y obj1 = Derived(1, 2) obj2 = Derived(1, 2) obj3 = Derived(3, 4) # Same initialized values should show no difference diff1 = DeepDiff(obj1, obj2) assert diff1 == {} # Different values should show difference diff2 = DeepDiff(obj1, obj3) assert diff2 == { 'values_changed': { 'root.x': {'new_value': 3, 'old_value': 1}, 'root.y': {'new_value': 4, 'old_value': 2}, } } class TestDeepHashPrep: """DeepHashPrep Tests covering object serialization.""" def test_prep_bool_vs_num1(self): assert {BoolObj.TRUE: 'bool:true'} == DeepHashPrep(True) assert {1: 'int:1'} == DeepHashPrep(1) def test_prep_bool_vs_num2(self): item1 = { "Value One": True, "Value Two": 1, } item2 = { "Value Two": 1, "Value One": True, } assert DeepHashPrep(item1)[item1] == DeepHashPrep(item2)[item2] def test_prep_str1(self): obj = "a" obj2 = b"a" expected_result = {obj: prep_str(obj)} result = DeepHashPrep(obj, ignore_string_type_changes=True) assert expected_result == result assert result[obj] == 'a' expected_result2 = {obj: prep_str(obj, ignore_string_type_changes=False)} result = DeepHashPrep(obj, ignore_string_type_changes=False) assert expected_result2 == result assert result[obj] == 'str:a' expected_result3 = {obj2: prep_str(obj2, ignore_string_type_changes=True)} result = DeepHashPrep(obj2, ignore_string_type_changes=True) assert expected_result3 != result assert result[obj2] == 'a' result = DeepHashPrep(obj2, ignore_string_type_changes=False) assert {b'a': 'bytes:a'} == result assert result[obj2] == 'bytes:a' def test_prep_number1(self): obj_int = 1 obj_float = 1.0 # By default, type is included in the prep result_int = DeepHashPrep(obj_int) result_float = DeepHashPrep(obj_float) assert result_int[obj_int] == 'int:1' assert result_float[obj_float] == 'float:1.0' # When ignoring numeric type changes, both get the same "number:" prefix result_int2 = DeepHashPrep(obj_int, ignore_numeric_type_changes=True) result_float2 = DeepHashPrep(obj_float, ignore_numeric_type_changes=True) assert result_int2[obj_int] == result_float2[obj_float] assert result_int2[obj_int] == 'number:1.000000000000' def test_dictionary_key_type_change(self): obj1 = {"b": 10} obj2 = {b"b": 10} result1 = DeepHashPrep(obj1, ignore_string_type_changes=True) result2 = DeepHashPrep(obj2, ignore_string_type_changes=True) assert result1[obj1] == result2[obj2] assert result1["b"] == result2[b"b"] def test_number_type_change(self): obj1 = 10 obj2 = 10.0 result1 = DeepHashPrep(obj1) result2 = DeepHashPrep(obj2) assert result1[obj1] != result2[obj2] result1 = DeepHashPrep(obj1, ignore_numeric_type_changes=True) result2 = DeepHashPrep(obj2, ignore_numeric_type_changes=True) assert result1[obj1] == result2[obj2] def test_prep_str_fail_if_deephash_leaks_results(self): """ This test fails if DeepHash is getting a mutable copy of hashes which means each init of the DeepHash will have hashes from the previous init. """ obj1 = "a" expected_result = {obj1: prep_str(obj1)} result = DeepHashPrep(obj1, ignore_string_type_changes=True) assert expected_result == result obj2 = "b" result = DeepHashPrep(obj2, ignore_string_type_changes=True) assert obj1 not in result def test_dict_in_dict(self): obj2 = {2: 3} obj = {'a': obj2} result = DeepHashPrep(obj, ignore_string_type_changes=True) assert 'a' in result assert obj2 in result def do_list_or_tuple(self, func, func_str): string1 = "a" obj = func([string1, 10, 20]) if func is list: obj_id = get_id(obj) else: obj_id = obj string1_prepped = prep_str(string1) expected_result = { 10: 'int:10', 20: 'int:20', string1: string1_prepped, obj_id: '{}:{},int:10,int:20'.format(func_str, string1_prepped), } result = DeepHashPrep(obj, ignore_string_type_changes=True) assert expected_result == result def test_list_and_tuple(self): for func, func_str in ((list, 'list'), (tuple, 'tuple')): self.do_list_or_tuple(func, func_str) def test_named_tuples(self): # checking if pypy3 is running the test # in that case due to a difference of string interning implementation # the id of x inside the named tuple changes. x = "x" x_prep = prep_str(x) Point = namedtuple('Point', [x]) obj = Point(x=11) result = DeepHashPrep(obj, ignore_string_type_changes=True) if pypy3: assert result[obj] == "ntPoint:{%s:int:11}" % x else: expected_result = { x: x_prep, obj: "ntPoint:{%s:int:11}" % x, 11: 'int:11', } assert expected_result == result def test_hash_enum(self): class MyEnum(Enum): A = 1 B = 2 if (py_major_version, py_minor_version) >= (3, 11): assert DeepHashPrep(MyEnum.A)[MyEnum.A] == r'objMyEnum:{str:_name_:str:A;str:_sort_order_:int:0;str:_value_:int:1}' else: assert DeepHashPrep(MyEnum.A)[MyEnum.A] == r'objMyEnum:{str:_name_:str:A;str:_value_:int:1}' assert DeepHashPrep(MyEnum.A) == DeepHashPrep(MyEnum(1)) assert DeepHashPrep(MyEnum.A) != DeepHashPrep(MyEnum.A.name) assert DeepHashPrep(MyEnum.A) != DeepHashPrep(MyEnum.A.value) assert DeepHashPrep(MyEnum.A) != DeepHashPrep(MyEnum.B) assert DeepHashPrep(MyEnum.A, use_enum_value=True)[MyEnum.A] == 'int:1' def test_dict_hash(self): string1 = "a" string1_prepped = prep_str(string1) key1 = "key1" key1_prepped = prep_str(key1) obj = {key1: string1, 1: 10, 2: 20} expected_result = { 1: 'int:1', 10: 'int:10', 2: 'int:2', 20: 'int:20', key1: key1_prepped, string1: string1_prepped, get_id(obj): 'dict:{{int:1:int:10;int:2:int:20;{}:{}}}'.format(key1, string1) } result = DeepHashPrep(obj, ignore_string_type_changes=True) assert expected_result == result def test_dict_in_list(self): string1 = "a" key1 = "key1" dict1 = {key1: string1, 1: 10, 2: 20} obj = [0, dict1] expected_result = { 0: 'int:0', 1: 'int:1', 10: 'int:10', 2: 'int:2', 20: 'int:20', key1: key1, string1: string1, get_id(dict1): 'dict:{int:1:int:10;int:2:int:20;%s:%s}' % (key1, string1), get_id(obj): 'list:dict:{int:1:int:10;int:2:int:20;%s:%s},int:0' % (key1, string1) } result = DeepHashPrep(obj, ignore_string_type_changes=True) assert expected_result == result def test_nested_lists_same_hash(self): t1 = [1, 2, [3, 4]] t2 = [[4, 3], 2, 1] t1_hash = DeepHashPrep(t1) t2_hash = DeepHashPrep(t2) assert t1_hash[get_id(t1)] == t2_hash[get_id(t2)] def test_nested_lists_same_hash2(self): t1 = [1, 2, [3, [4, 5]]] t2 = [[[5, 4], 3], 2, 1] t1_hash = DeepHashPrep(t1) t2_hash = DeepHashPrep(t2) assert t1_hash[get_id(t1)] == t2_hash[get_id(t2)] def test_nested_lists_same_hash3(self): t1 = [{1: [2, 3], 4: [5, [6, 7]]}] t2 = [{4: [[7, 6], 5], 1: [3, 2]}] t1_hash = DeepHashPrep(t1) t2_hash = DeepHashPrep(t2) assert t1_hash[get_id(t1)] == t2_hash[get_id(t2)] def test_nested_lists_in_dictionary_same_hash(self): t1 = [{"c": 4}, {"c": 3}] t2 = [{"c": 3}, {"c": 4}] t1_hash = DeepHashPrep(t1) t2_hash = DeepHashPrep(t2) assert t1_hash[get_id(t1)] == t2_hash[get_id(t2)] def test_same_sets_same_hash(self): t1 = {1, 3, 2} t2 = {2, 3, 1} t1_hash = DeepHashPrep(t1) t2_hash = DeepHashPrep(t2) assert t1_hash[get_id(t1)] == t2_hash[get_id(t2)] @pytest.mark.parametrize("list1, list2, ignore_iterable_order, is_equal", [ ([1, 2], [2, 1], False, False), ([1, 2], [2, 1], True, True), ([1, 2, 3], [1, 3, 2], False, False), ([1, [1, 2, 3]], [1, [3, 2, 1]], False, False), ([1, [1, 2, 3]], [1, [3, 2, 1]], True, True), ((1, 2), (2, 1), False, False), ((1, 2), (2, 1), True, True), ]) def test_ignore_iterable_order(self, list1, list2, ignore_iterable_order, is_equal): list1_hash = DeepHash(list1, ignore_iterable_order=ignore_iterable_order) list2_hash = DeepHash(list2, ignore_iterable_order=ignore_iterable_order) assert is_equal == (list1_hash[list1] == list2_hash[list2]) @pytest.mark.parametrize("t1, t2, significant_digits, number_format_notation, result", [ ({0.012, 0.98}, {0.013, 0.99}, 1, "f", 'set:float:0.0,float:1.0'), (100000, 100021, 3, "e", 'int:1.000e+5'), ]) def test_similar_significant_hash(self, t1, t2, significant_digits, number_format_notation, result): t1_hash = DeepHashPrep(t1, significant_digits=significant_digits, number_format_notation=number_format_notation) t2_hash = DeepHashPrep(t2, significant_digits=significant_digits, number_format_notation=number_format_notation) if result: assert result == t1_hash[t1] == t2_hash[t2] else: assert t1_hash[t1] != t2_hash[t2] def test_number_to_string_func(self): def custom_number_to_string(number, *args, **kwargs): number = 100 if number < 100 else number return number_to_string(number, *args, **kwargs) t1 = [10, 12, 100000] t2 = [50, 63, 100021] t1_hash = DeepHashPrep(t1, significant_digits=4, number_format_notation="e", number_to_string_func=custom_number_to_string) t2_hash = DeepHashPrep(t2, significant_digits=4, number_format_notation="e", number_to_string_func=custom_number_to_string) assert t1_hash[10] == t2_hash[50] == t1_hash[12] == t2_hash[63] != t1_hash[100000] def test_same_sets_in_lists_same_hash(self): t1 = ["a", {1, 3, 2}] t2 = [{2, 3, 1}, "a"] t1_hash = DeepHashPrep(t1) t2_hash = DeepHashPrep(t2) assert t1_hash[get_id(t1)] == t2_hash[get_id(t2)] def test_unknown_parameters(self): with pytest.raises(ValueError): DeepHashPrep(1, wrong_param=2) def test_bad_attribute_prep(self): class Bad: __slots__ = ['x', 'y'] def __getattr__(self, key): raise AttributeError("Bad item") def __str__(self): return "Bad Object" t1 = Bad() result = DeepHashPrep(t1) expected_result = {t1: unprocessed, UNPROCESSED_KEY: [t1]} assert expected_result == result class Burrito: bread = 'flour' def __init__(self): self.spicy = True class Taco: bread = 'flour' def __init__(self): self.spicy = True class ClassA: def __init__(self, x, y): self.x = x self.y = y class ClassB: def __init__(self, x, y): self.x = x self.y = y class ClassC(ClassB): pass obj_a = ClassA(1, 2) obj_b = ClassB(1, 2) obj_c = ClassC(1, 2) burrito = Burrito() taco = Taco() @pytest.mark.parametrize("test_num, t1, t2, ignore_type_in_groups, ignore_type_subclasses, is_qual", [ (1, taco, burrito, [], False, False), (2, taco, burrito, [(Taco, Burrito)], False, True), (3, [taco], [burrito], [(Taco, Burrito)], False, True), (4, [obj_a], [obj_c], [(ClassA, ClassB)], False, True), (5, [obj_a], [obj_c], [(ClassA, ClassB)], True, False), (6, [obj_b], [obj_c], [(ClassB, )], True, False), ]) def test_objects_with_same_content(self, test_num, t1, t2, ignore_type_in_groups, ignore_type_subclasses, is_qual): t1_result = DeepHashPrep(t1, ignore_type_in_groups=ignore_type_in_groups, ignore_type_subclasses=ignore_type_subclasses) t2_result = DeepHashPrep(t2, ignore_type_in_groups=ignore_type_in_groups, ignore_type_subclasses=ignore_type_subclasses) assert is_qual == (t1_result[t1] == t2_result[t2]), f"test_objects_with_same_content #{test_num} failed." def test_custom_object(self): cc_a = CustomClass2(prop1=["a"], prop2=["b"]) t1 = [cc_a, CustomClass2(prop1=["c"], prop2=["d"])] t1_result = DeepHashPrep(t1) expected = 'list:objCustomClass2:{str:prop1:list:str:a;str:prop2:list:str:b},objCustomClass2:{str:prop1:list:str:c;str:prop2:list:str:d}' # NOQA assert expected == t1_result[t1] def test_repetition_by_default_does_not_effect(self): list1 = [3, 4] list1_id = get_id(list1) a = [1, 2, list1] a_id = get_id(a) list2 = [4, 3, 3] list2_id = get_id(list2) b = [list2, 2, 1] b_id = get_id(b) hash_a = DeepHashPrep(a) hash_b = DeepHashPrep(b) assert hash_a[list1_id] == hash_b[list2_id] assert hash_a[a_id] == hash_b[b_id] def test_setting_repetition_off_unequal_hash(self): list1 = [3, 4] list1_id = get_id(list1) a = [1, 2, list1] a_id = get_id(a) list2 = [4, 3, 3] list2_id = get_id(list2) b = [list2, 2, 1] b_id = get_id(b) hash_a = DeepHashPrep(a, ignore_repetition=False) hash_b = DeepHashPrep(b, ignore_repetition=False) assert not hash_a[list1_id] == hash_b[list2_id] assert not hash_a[a_id] == hash_b[b_id] assert hash_a[list1_id].replace('3|1', '3|2') == hash_b[list2_id] def test_already_calculated_hash_wont_be_recalculated(self): hashes = (i for i in range(10)) def hasher(obj): return str(next(hashes)) obj = "a" expected_result = {obj: '0'} result = DeepHash(obj, hasher=hasher) assert expected_result == result # we simply feed the last result to DeepHash # So it can re-use the results. result2 = DeepHash(obj, hasher=hasher, hashes=result) # if hashes are not cached and re-used, # then the next time hasher runs, it returns # number 1 instead of 0. assert expected_result == result2 result3 = DeepHash(obj, hasher=hasher) expected_result = {obj: '1'} assert expected_result == result3 def test_skip_type(self): l1 = logging.getLogger("test") obj = {"log": l1, 2: 1337} result = DeepHashPrep(obj, exclude_types={logging.Logger}) assert get_id(l1) not in result def test_skip_type2(self): l1 = logging.getLogger("test") result = DeepHashPrep(l1, exclude_types={logging.Logger}) assert not result def test_prep_dic_with_loop(self): obj = {2: 1337} obj[1] = obj result = DeepHashPrep(obj) expected_result = {get_id(obj): 'dict:{int:2:int:1337}', 1: 'int:1', 2: 'int:2', 1337: 'int:1337'} assert expected_result == result def test_prep_iterable_with_loop(self): obj = [1] obj.append(obj) result = DeepHashPrep(obj) expected_result = {get_id(obj): 'list:int:1', 1: 'int:1'} assert expected_result == result def test_prep_iterable_with_excluded_type(self): l1 = logging.getLogger("test") obj = [1, l1] result = DeepHashPrep(obj, exclude_types={logging.Logger}) assert get_id(l1) not in result def test_skip_str_type_in_dict_on_list(self): dic1 = {1: "a"} t1 = [dic1] dic2 = {} t2 = [dic2] t1_hash = DeepHashPrep(t1, exclude_types=[str]) t2_hash = DeepHashPrep(t2, exclude_types=[str]) assert 1 in t1_hash assert t1_hash[dic1] == t2_hash[dic2] def test_skip_path_in_hash(self): dic1 = {1: "a"} t1 = [dic1, 2] dic2 = {} t2 = [dic2, 2] t1_hash = DeepHashPrep(t1, exclude_paths=['root[0]']) t2_hash = DeepHashPrep(t2, exclude_paths='root[0]') t2_hash_again = DeepHashPrep(t2, include_paths='1') assert 1 not in t1_hash assert 2 in t1_hash assert t1_hash[2] == t2_hash[2] assert t1_hash[2] == t2_hash_again[2] def test_skip_path2(self): obj10 = {'a': 1, 'b': 'f', 'e': "1111", 'foo': {'bar': 'baz'}} obj11 = {'c': 1, 'd': 'f', 'e': 'Cool'} obj20 = {'a': 1, 'b': 'f', 'e': 'Cool', 'foo': {'bar': 'baz2'}} obj21 = {'c': 1, 'd': 'f', 'e': "2222"} t1 = [obj10, obj11] t2 = [obj20, obj21] exclude_paths = ["root[0]['e']", "root[1]['e']", "root[0]['foo']['bar']"] t1_hash = DeepHashPrep(t1, exclude_paths=exclude_paths) t2_hash = DeepHashPrep(t2, exclude_paths=exclude_paths) assert t1_hash[t1] == t2_hash[t2] def test_hash_include_path_nested(self): obj10 = {'a': 1, 'b': 'f', 'e': "1111", 'foo': {'bar': 'baz'}} obj11 = {'c': 1, 'd': 'f', 'e': 'Cool'} obj20 = {'a': 1, 'b': 'f', 'e': 'Cool', 'foo': {'bar': 'baz'}} obj21 = {'c': 1, 'd': 'f', 'e': "2222"} t1 = [obj10, obj11] t2 = [obj20, obj21] include_paths = ["root[0]['foo']['bar']"] t1_hash = DeepHashPrep(t1, include_paths=include_paths) t2_hash = DeepHashPrep(t2, include_paths=include_paths) assert t1_hash[t1] == t2_hash[t2] def test_skip_regex_path(self): dic1 = {1: "a"} t1 = [dic1, 2] exclude_re = re.compile(r'\[0\]') t1_hash = DeepHashPrep(t1, exclude_regex_paths=r'\[0\]') t2_hash = DeepHashPrep(t1, exclude_regex_paths=[exclude_re]) assert 1 not in t1_hash assert 2 in t1_hash assert t1_hash[2] == t2_hash[2] def test_skip_hash_exclude_obj_callback(self): def exclude_obj_callback(obj, parent): return True if parent == "root[0]['x']" or obj == 2 else False dic1 = {"x": 1, "y": 2, "z": 3} t1 = [dic1] t1_hash = DeepHashPrep(t1, exclude_obj_callback=exclude_obj_callback) assert t1_hash == {'y': 'str:y', 'z': 'str:z', 3: 'int:3', get_id(dic1): 'dict:{str:z:int:3}', get_id(t1): 'list:dict:{str:z:int:3}'} dic2 = {"z": 3} t2 = [dic2] t2_hash = DeepHashPrep(t2, exclude_obj_callback=exclude_obj_callback) assert t1_hash[t1] == t2_hash[t2] def test_string_case(self): t1 = "Hello" t1_hash = DeepHashPrep(t1) assert t1_hash == {'Hello': 'str:Hello'} t1_hash = DeepHashPrep(t1, ignore_string_case=True) assert t1_hash == {'Hello': 'str:hello'} def test_hash_class(self): t1 = ClassC t1_hash = DeepHashPrep(t1) assert t1_hash['class_attr'] == 'str:class_attr' assert t1_hash[0] == 'int:0' # Note: we ignore private names in calculating hashes now. So you dont see __init__ here for example. assert t1_hash[t1] == r'objClassC:{str:class_attr:int:0}' def test_hash_set_in_list(self): t1 = [{1, 2, 3}, {4, 5}] t1_hash = DeepHashPrep(t1) assert t1_hash[t1] == 'list:set:int:1,int:2,int:3,set:int:4,int:5' def test_hash_numpy_array1(self): t1 = np.array([[1, 2]], np.int8) t2 = np.array([[2, 1]], np.int8) t1_hash = DeepHashPrep(t1) t2_hash = DeepHashPrep(t2) assert t1_hash[t1] == 'ndarray:ndarray:int8:1,int8:2' assert t2_hash[t2] == t1_hash[t1] def test_hash_numpy_array_ignore_numeric_type_changes(self): t1 = np.array([[1, 2]], np.int8) t1_hash = DeepHashPrep(t1, ignore_numeric_type_changes=True) assert t1_hash[t1] == 'ndarray:ndarray:number:1.000000000000,number:2.000000000000' def test_hash_numpy_array2_multi_dimensional_can_not_retrieve_individual_array_item_hashes(self): """ This is a very interesting case. When DeepHash extracts t1[0] to create a hash for it, Numpy creates an array. But that array will only be technically available during the DeepHash run. Once DeepHash is run, the array is marked to be deleted by the garbage collector. However depending on the version of the python and the machine that runs it, by the time we get to the line that is t1_hash[t1[0]], the t1[0] may or may not be still in memory. If it is still in the memory, t1_hash[t1[0]] works without a problem. If it is already garbage collected, t1_hash[t1[0]] will throw a key error since there will be a new t1[0] by the time t1_hash[t1[0]] is called. Hence it will have a new ID and thus it will not be available anymore in t1_hash. Remember that since Numpy arrays are not hashable, the ID of the array is stored in t1_hash as a key and not the object itself. """ t1 = np.array([[1, 2, 3, 4], [4, 2, 2, 1]], np.int8) t1_hash = DeepHashPrep(t1) try: t1_hash[t1[0]] except Exception as e: assert str(e).strip("'") == HASH_LOOKUP_ERR_MSG.format(t1[0]) def test_pandas(self): import pandas as pd df = pd.DataFrame({"a": [1]}) equal_df = pd.DataFrame({"a": [1]}) df_same_column_names = pd.DataFrame({"a": [1, 2]}) other_df = pd.DataFrame({"b": [1]}) df_hash = DeepHashPrep(df)[df] equal_df_hash = DeepHashPrep(equal_df)[equal_df] df_same_column_names_hash = DeepHashPrep(df_same_column_names)[df_same_column_names] other_df_hash = DeepHashPrep(other_df)[other_df] assert df_hash == equal_df_hash assert df_hash != df_same_column_names_hash assert df_hash != other_df_hash df_mixed = pd.DataFrame({'a': [1], 'b': ['two'], 'c': [(1, 2)]}) df_mixed_2 = pd.DataFrame({'a': [1], 'b': ['two'], 'c': [(1, 2)]}) df_mixed_3 = pd.DataFrame({'a': [1], 'b': ['one'], 'c': [(1, 2)]}) df_mixed_4 = pd.DataFrame({'a': [1], 'b': ['two'], 'c': [(1, 3)]}) df_mixed_hash = DeepHashPrep(df_mixed)[df_mixed] df_mixed_2_hash = DeepHashPrep(df_mixed_2)[df_mixed_2] df_mixed_3_hash = DeepHashPrep(df_mixed_3)[df_mixed_3] df_mixed_4_hash = DeepHashPrep(df_mixed_4)[df_mixed_4] assert df_mixed_hash == df_mixed_2_hash assert df_mixed_hash != df_mixed_3_hash assert df_mixed_hash != df_mixed_4_hash df_u8 = pd.DataFrame({'a': np.array([1], dtype=np.uint8)}) df_u16 = pd.DataFrame({'a': np.array([1], dtype=np.uint16)}) df_float = pd.DataFrame({'a': np.array([1], dtype=np.float32)}) df_u8_hash = DeepHashPrep(df_u8)[df_u8] df_u16_hash = DeepHashPrep(df_u16)[df_u16] df_float_hash = DeepHashPrep(df_float)[df_float] assert df_u8_hash != df_float_hash assert df_u8_hash != df_u16_hash df_index = pd.DataFrame({'a': [1, 2, 3]}, index=[1, 2, 3]) df_index_diff = pd.DataFrame({'a': [1, 2, 3]}, index=[1, 2, 4]) df_index_hash = DeepHashPrep(df_index)[df_index] df_index_diff_hash = DeepHashPrep(df_index_diff)[df_index_diff] assert df_index_hash != df_index_diff_hash def test_polars(self): import polars as pl df = pl.DataFrame({"a": [1]}) equal_df = pl.DataFrame({"a": [1]}) df_same_column_names = pl.DataFrame({"a": [1, 2]}) other_df = pl.DataFrame({"b": [1]}) df_hash = DeepHashPrep(df)[df] equal_df_hash = DeepHashPrep(equal_df)[equal_df] df_same_column_names_hash = DeepHashPrep(df_same_column_names)[df_same_column_names] other_df_hash = DeepHashPrep(other_df)[other_df] assert df_hash == equal_df_hash assert df_hash != df_same_column_names_hash assert df_hash != other_df_hash df_mixed = pl.DataFrame({'a': [1], 'b': ['two'], 'c': [(1, 2)]}) df_mixed_2 = pl.DataFrame({'a': [1], 'b': ['two'], 'c': [(1, 2)]}) df_mixed_3 = pl.DataFrame({'a': [1], 'b': ['one'], 'c': [(1, 2)]}) df_mixed_4 = pl.DataFrame({'a': [1], 'b': ['two'], 'c': [(1, 3)]}) df_mixed_hash = DeepHashPrep(df_mixed)[df_mixed] df_mixed_2_hash = DeepHashPrep(df_mixed_2)[df_mixed_2] df_mixed_3_hash = DeepHashPrep(df_mixed_3)[df_mixed_3] df_mixed_4_hash = DeepHashPrep(df_mixed_4)[df_mixed_4] assert df_mixed_hash == df_mixed_2_hash assert df_mixed_hash != df_mixed_3_hash assert df_mixed_hash != df_mixed_4_hash df_u8 = pl.DataFrame({'a': np.array([1], dtype=np.uint8)}) df_u16 = pl.DataFrame({'a': np.array([1], dtype=np.uint16)}) df_float = pl.DataFrame({'a': np.array([1], dtype=np.float32)}) df_u8_hash = DeepHashPrep(df_u8)[df_u8] df_u16_hash = DeepHashPrep(df_u16)[df_u16] df_float_hash = DeepHashPrep(df_float)[df_float] assert df_u8_hash != df_float_hash assert df_u8_hash != df_u16_hash lazy_1 = pl.DataFrame({"foo": ["a", "b", "c"], "bar": [0, 1, 2]}).lazy() lazy_2 = pl.DataFrame({"foo": ["a", "b", "c"], "bar": [0, 1, 2]}).lazy() lazy_3 = pl.DataFrame({"foo": ["a", "b", "c"], "bar": [0, 1, 2], "foobar": 5}).lazy() with pytest.raises(TypeError): DeepHashPrep(lazy_1)[lazy_1] # lazy dfs can not be compared df_1 = lazy_1.collect() df_2 = lazy_2.collect() df_3 = lazy_3.collect() df_1_hash = DeepHashPrep(df_1)[df_1] df_2_hash = DeepHashPrep(df_2)[df_2] df_3_hash = DeepHashPrep(df_3)[df_3] assert df_1_hash == df_2_hash assert df_1_hash != df_3_hash class TestDeepHashSHA: """DeepHash with SHA Tests.""" def test_str_sha1(self): obj = "a" expected_result = { obj: '86f7e437faa5a7fce15d1ddcb9eaeaea377667b8' } result = DeepHash(obj, ignore_string_type_changes=True, hasher=DeepHash.sha1hex) assert expected_result == result def test_str_sha256(self): obj = "a" expected_result = { obj: 'ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb' } result = DeepHash(obj, ignore_string_type_changes=True, hasher=DeepHash.sha256hex) assert expected_result == result def test_prep_str_sha1_fail_if_mutable(self): """ This test fails if DeepHash is getting a mutable copy of hashes which means each init of the DeepHash will have hashes from the previous init. """ obj1 = "a" expected_result = { obj1: '86f7e437faa5a7fce15d1ddcb9eaeaea377667b8' } result = DeepHash(obj1, ignore_string_type_changes=True, hasher=DeepHash.sha1hex) assert expected_result == result obj2 = "b" result = DeepHash(obj2, ignore_string_type_changes=True, hasher=DeepHash.sha1hex) assert obj1 not in result def test_bytecode(self): obj = b"a" expected_result = { obj: '86f7e437faa5a7fce15d1ddcb9eaeaea377667b8' } result = DeepHash(obj, ignore_string_type_changes=True, hasher=DeepHash.sha1hex) assert expected_result == result def test_list1(self): string1 = "a" obj = [string1, 10, 20] expected_result = { string1: '86f7e437faa5a7fce15d1ddcb9eaeaea377667b8', get_id(obj): 'eac61cbd194e5e03c210a3dce67b9bfd6a7b7acb', 10: DeepHash.sha1hex('int:10'), 20: DeepHash.sha1hex('int:20'), } result = DeepHash(obj, ignore_string_type_changes=True, hasher=DeepHash.sha1hex) assert expected_result == result def test_datetime_hash(self): dt_utc = datetime.datetime(2025, 2, 3, 12, 0, 0, tzinfo=pytz.utc) # UTC timezone # Convert it to another timezone (e.g., New York) dt_ny = dt_utc.astimezone(pytz.timezone('America/New_York')) assert dt_utc == dt_ny result_utc = DeepHash(dt_utc, ignore_string_type_changes=True, hasher=DeepHash.sha1hex) result_ny = DeepHash(dt_ny, ignore_string_type_changes=True, hasher=DeepHash.sha1hex) assert result_utc[dt_utc] == result_ny[dt_ny] def test_dict1(self): string1 = "a" key1 = "key1" obj = {key1: string1, 1: 10, 2: 20} expected_result = { 1: DeepHash.sha1hex('int:1'), 10: DeepHash.sha1hex('int:10'), 2: DeepHash.sha1hex('int:2'), 20: DeepHash.sha1hex('int:20'), key1: '1073ab6cda4b991cd29f9e83a307f34004ae9327', string1: '86f7e437faa5a7fce15d1ddcb9eaeaea377667b8', get_id(obj): '11e23f096df81b1ccab0c309cdf8b4ba5a0a6895' } result = DeepHash(obj, ignore_string_type_changes=True, hasher=DeepHash.sha1hex) assert expected_result == result class TestCleaningString: @pytest.mark.parametrize("text, ignore_string_type_changes, expected_result", [ (b'hello', True, 'hello'), (b'hello', False, 'bytes:hello'), ('hello', True, 'hello'), ('hello', False, 'str:hello'), ]) def test_clean_type(self, text, ignore_string_type_changes, expected_result): result = prepare_string_for_hashing(text, ignore_string_type_changes=ignore_string_type_changes) assert expected_result == result class TestCounts: @pytest.mark.parametrize('obj, expected_count', [ ( {1: 1, 2: 3}, 5 ), ( {"key": {1: 1, 2: 4}, "key2": ["a", "b"]}, 11 ), ( [{1}], 3 ), ( [ClassC(a=10, b=11)], 6 ) ]) def test_dict_count(self, obj, expected_count): """ How many object went to build this dict? """ result = DeepHash(obj).get(obj, extract_index=1) assert expected_count == result class TestOtherHashFuncs: @pytest.mark.parametrize('items, prefix, expected', [ ([[1], [2]], 'pre', 'pre583852d84b3482edf53408b64724a37289d7af458c44bb989a8abbffe24d2d2b'), ([[1], [2]], b'pre', 'pre583852d84b3482edf53408b64724a37289d7af458c44bb989a8abbffe24d2d2b'), ]) def test_combine_hashes_lists(self, items, prefix, expected): result = combine_hashes_lists(items, prefix) assert expected == result EXPECTED_MESSAGE1 = ( "'utf-8' codec can't decode byte 0xc3 in position 0: invalid continuation byte in '('. " "Please either pass ignore_encoding_errors=True or pass the encoding via encodings=['utf-8', '...'].") EXPECTED_MESSAGE2 = ( "'utf-8' codec can't decode byte 0xbc in position 0: invalid start byte in ' cup of flour'. " "Please either pass ignore_encoding_errors=True or pass the encoding via encodings=['utf-8', '...'].") EXPECTED_MESSAGE3 = ( "'utf-8' codec can't decode byte 0xc3 in position 34: invalid continuation byte in '...up of potatos. Then ( cup of flour'. Please either pass ignore_encoding_errors=True or " "pass the encoding via encodings=['utf-8', '...']." ) @pytest.mark.parametrize('test_num, item, encodings, ignore_encoding_errors, expected_result, expected_message', [ (1, b'\xc3\x28', None, False, UnicodeDecodeError, EXPECTED_MESSAGE1), (2, b'\xc3\x28', ['utf-8'], False, UnicodeDecodeError, EXPECTED_MESSAGE1), (3, b'\xc3\x28', ['utf-8'], True, {b'\xc3(': '640da73f0d9b268a0a7ae884d77063d1193f43a651352f9032d99a8fe1705546'}, None), (4, b"\xbc cup of flour", ['utf-8'], False, UnicodeDecodeError, EXPECTED_MESSAGE2), (5, b"\xbc cup of flour", ['utf-8'], True, {b'\xbc cup of flour': '86ac12eb5e35db88cf93baca1d62098023b2d93d634e75fb4e37657e514f3d51'}, None), (6, b"\xbc cup of flour", ['utf-8', 'latin-1'], False, {b'\xbc cup of flour': 'cfc354ae2232a8983bf59b2004f44fcb4036f57df1d08b9cde9950adea3f8d3e'}, None), (7, b"First have a cup of potatos. Then \xc3\x28 cup of flour", None, False, UnicodeDecodeError, EXPECTED_MESSAGE3), ]) def test_hash_encodings(self, test_num, item, encodings, ignore_encoding_errors, expected_result, expected_message): if UnicodeDecodeError == expected_result: # NOQA with pytest.raises(expected_result) as exc_info: DeepHash(item, encodings=encodings, ignore_encoding_errors=ignore_encoding_errors) assert expected_message == str(exc_info.value), f"test_encodings test #{test_num} failed." else: result = DeepHash(item, encodings=encodings, ignore_encoding_errors=ignore_encoding_errors) assert expected_result == result, f"test_encodings test #{test_num} failed." def test_ip_addresses(self): class ClassWithIp: """Class containing single data member to demonstrate deepdiff infinite iterate over IPv6Interface""" def __init__(self, addr: str): self.field: Union[ ipaddress.IPv4Network, ipaddress.IPv6Network, ipaddress.IPv4Interface, ipaddress.IPv6Interface, ] = ipaddress.IPv6Network(addr) obj1 = ClassWithIp("2002:db8::/30") obj1_hash = DeepHashPrep(obj1) repr(obj1_hash) # shouldn't raise error assert r"objClassWithIp:{str:field:iprange:2002:db8::/30}" == obj1_hash[obj1] obj2 = ClassWithIp("2001:db8::/32") diff = DeepDiff(obj1, obj2) assert { "values_changed": { "root.field": { "new_value": ipaddress.IPv6Network("2001:db8::/32"), "old_value": ipaddress.IPv6Network("2002:db8::/30"), } } } == diff qlustered-deepdiff-41c7265/tests/test_helper.py000066400000000000000000000405121516241264500216050ustar00rootroot00000000000000#!/usr/bin/env python import pytest import datetime import numpy as np from enum import Enum from decimal import Decimal from deepdiff.helper import ( short_repr, number_to_string, get_numpy_ndarray_rows, cartesian_product_of_shape, literal_eval_extended, not_found, diff_numpy_array, cartesian_product_numpy, get_truncate_datetime, datetime_normalize, detailed__dict__, ENUM_INCLUDE_KEYS, add_root_to_paths, get_semvar_as_integer, ) class MyEnum(Enum): A = 1 B = 2 class TestHelper: """Helper Tests.""" def test_short_repr_when_short(self): item = {1: 2} output = short_repr(item) assert output == '{1: 2}' def test_short_repr_when_long(self): item = {'Eat more': 'burritos'} output = short_repr(item) assert output == "{'Eat more':...}" @pytest.mark.parametrize("t1, t2, significant_digits, number_format_notation, expected_result", [ (10, 10.0, 5, "f", True), (10, 10.0, 5, "e", True), (10, 10.2, 5, "f", ('10.00000', '10.20000')), (10, 10.2, 5, "e", ('1.00000e+1', '1.02000e+1')), (10, 10.2, 0, "f", True), (10, 10.2, 0, "e", True), (Decimal(10), 10, 0, "f", True), (Decimal(10), 10, 0, "e", True), (Decimal(10), 10, 10, "f", True), (Decimal(10), 10, 10, "e", True), (Decimal(10), 10.0, 0, "f", True), (Decimal(10), 10.0, 0, "e", True), (Decimal(10), 10.0, 10, "f", True), (Decimal(10), 10.0, 10, "e", True), (Decimal('10.0'), 10.0, 5, "f", True), (Decimal('10.0'), 10.0, 5, "e", True), (Decimal('10.01'), 10.01, 1, "f", True), (Decimal('10.01'), 10.01, 1, "e", True), (Decimal('10.01'), 10.01, 2, "f", True), (Decimal('10.01'), 10.01, 2, "e", True), (Decimal('10.01'), 10.01, 5, "f", True), (Decimal('10.01'), 10.01, 5, "e", True), (Decimal('10.01'), 10.01, 8, "f", True), (Decimal('10.01'), 10.01, 8, "e", True), (Decimal('10.010'), 10.01, 3, "f", True), (Decimal('10.010'), 10.01, 3, "e", True), (Decimal('100000.1'), 100000.1, 0, "f", True), (Decimal('100000.1'), 100000.1, 0, "e", True), (Decimal('100000.1'), 100000.1, 1, "f", True), (Decimal('100000.1'), 100000.1, 1, "e", True), (Decimal('100000.1'), 100000.1, 5, "f", True), (Decimal('100000.1'), 100000.1, 5, "e", True), (Decimal('100000'), 100000.1, 0, "f", True), (Decimal('100000'), 100000.1, 0, "e", True), (Decimal('100000'), 100000.1, 1, "f", ('100000.0', '100000.1')), (Decimal('100000'), 100000.1, 1, "e", True), (Decimal('-100000'), 100000.1, 1, "f", ('-100000.0', '100000.1')), (Decimal('-100000'), 100000.1, 1, "e", ("-1.0e+5","1.0e+5")), (0, 0.0, 5, "f", True), (0, 0.0, 5, "e", True), (0, 0.2, 5, "f", ('0.00000', '0.20000')), (0, 0.2, 5, "e", ('0.00000e+0', '2.00000e-1')), (0, 0.2, 0, "f", True), (0, 0.2, 0, "e", True), (Decimal(0), 0, 0, "f", True), (Decimal(0), 0, 0, "e", True), (Decimal(0), 0, 10, "f", True), (Decimal(0), 0, 10, "e", True), (Decimal(0), 0.0, 0, "f", True), (Decimal(0), 0.0, 0, "e", True), (Decimal(0), 0.0, 10, "f", True), (Decimal(0), 0.0, 10, "e", True), (Decimal('0.0'), 0.0, 5, "f", True), (Decimal('0.0'), 0.0, 5, "e", True), (Decimal('0.01'), 0.01, 1, "f", True), (Decimal('0.01'), 0.01, 1, "e", True), (Decimal('0.01'), 0.01, 2, "f", True), (Decimal('0.01'), 0.01, 2, "e", True), (Decimal('0.01'), 0.01, 5, "f", True), (Decimal('0.01'), 0.01, 5, "e", True), (Decimal('0.01'), 0.01, 8, "f", True), (Decimal('0.01'), 0.01, 8, "e", True), (Decimal('0.010'), 0.01, 3, "f", True), (Decimal('0.010'), 0.01, 3, "e", True), (Decimal('0.00002'), 0.00001, 0, "f", True), (Decimal('0.00002'), 0.00001, 0, "e", True), (Decimal('0.00002'), 0.00001, 1, "f", True), (Decimal('0.00002'), 0.00001, 1, "e", True), (Decimal('0.00002'), 0.00001, 5, "f", ('0.00002', '0.00001')), (Decimal('0.00002'), 0.00001, 5, "e", ('2.00000e-5', '1.00000e-5')), (Decimal('0.00002'), 0.00001, 6, "f", ('0.000020', '0.000010')), (Decimal('0.00002'), 0.00001, 6, "e", ('2.000000e-5', '1.000000e-5')), (Decimal('0'), 0.1, 0, "f", True), (Decimal('0'), 0.1, 0, "e", True), (Decimal('0'), 0.1, 1, "f", ('0.0', '0.1')), (Decimal('0'), 0.1, 1, "e", ('0.0e+0', '1.0e-1')), (-0, 0.0, 5, "f", True), (-0, 0.0, 5, "e", True), (-0, 0.2, 5, "f", ('0.00000', '0.20000')), (-0, 0.2, 5, "e", ('0.00000e+0', '2.00000e-1')), (-0, 0.2, 0, "f", True), (-0, 0.2, 0, "e", True), (Decimal(-0), 0, 0, "f", True), (Decimal(-0), 0, 0, "e", True), (Decimal(-0), 0, 10, "f", True), (Decimal(-0), 0, 10, "e", True), (Decimal(-0), 0.0, 0, "f", True), (Decimal(-0), 0.0, 0, "e", True), (Decimal(-0), 0.0, 10, "f", True), (Decimal(-0), 0.0, 10, "e", True), (Decimal('-0.0'), 0.0, 5, "f", True), (Decimal('-0.0'), 0.0, 5, "e", True), (Decimal('-0.01'), 0.01, 1, "f", True), (Decimal('-0.01'), 0.01, 1, "e", True), (Decimal('-0.01'), 0.01, 2, "f", ('-0.01', '0.01')), (Decimal('-0.01'), 0.01, 2, "e", ('-1.00e-2', '1.00e-2')), (Decimal('-0.00002'), 0.00001, 0, "f", True), (Decimal('-0.00002'), 0.00001, 0, "e", True), (Decimal('-0.00002'), 0.00001, 1, "f", True), (Decimal('-0.00002'), 0.00001, 1, "e", True), (Decimal('-0.00002'), 0.00001, 5, "f", ('-0.00002', '0.00001')), (Decimal('-0.00002'), 0.00001, 5, "e", ('-2.00000e-5', '1.00000e-5')), (Decimal('-0.00002'), 0.00001, 6, "f", ('-0.000020', '0.000010')), (Decimal('-0.00002'), 0.00001, 6, "e", ('-2.000000e-5', '1.000000e-5')), (Decimal('-0'), 0.1, 0, "f", True), (Decimal('-0'), 0.1, 0, "e", True), (Decimal('-0'), 0.1, 1, "f", ('0.0', '0.1')), (Decimal('-0'), 0.1, 1, "e", ('0.0e+0', '1.0e-1')), ]) def test_number_to_string_decimal_digits(self, t1, t2, significant_digits, number_format_notation, expected_result): st1 = number_to_string(t1, significant_digits=significant_digits, number_format_notation=number_format_notation) st2 = number_to_string(t2, significant_digits=significant_digits, number_format_notation=number_format_notation) if expected_result is True: assert st1 == st2 else: assert st1 == expected_result[0] assert st2 == expected_result[1] @pytest.mark.parametrize("t1, t2, significant_digits, number_format_notation, expected_result", [ (10j, 10.0j, 5, "f", True), (10j, 10.0j, 5, "e", True), (4+10j, 4.0000002+10.0000002j, 5, "f", True), (4+10j, 4.0000002+10.0000002j, 5, "e", True), (4+10j, 4.0000002+10.0000002j, 7, "f", ('4.0000000+10.0000000j', '4.0000002+10.0000002j')), (4+10j, 4.0000002+10.0000002j, 7, "e", ('4.0000000e+0+1.0000000e+1j', '4.0000002e+0+1.0000000e+1j')), (0.00002+0.00002j, 0.00001+0.00001j, 0, "f", True), (0.00002+0.00002j, 0.00001+0.00001j, 0, "e", True), (0.00002+0.00002j, 0.00001+0.00001j, 5, "f", ('0.00002+0.00002j', '0.00001+0.00001j')), (0.00002+0.00002j, 0.00001+0.00001j, 5, "e", ('2.00000e-5+2.00000e-5j', '1.00000e-5+1.00000e-5j')), (-0.00002-0.00002j, 0.00001+0.00001j, 0, "f", True), (-0.00002-0.00002j, 0.00001+0.00001j, 0, "e", True), (10j, 10.2j, 5, "f", ('0.00000+10.00000j', '0.00000+10.20000j')), (10j, 10.2j, 5, "e", ('0.00000e+0+1.00000e+1j', '0.00000e+0+1.02000e+1j')), (10j, 10.2j, 0, "f", True), (10j, 10.2j, 0, "e", True), (0j, 0.0j, 5, "f", True), (0j, 0.0j, 5, "e", True), (0j, 0.2j, 5, "f", ('0.00000', '0.00000+0.20000j')), (0j, 0.2j, 5, "e", ('0.00000e+0', '0.00000e+0+2.00000e-1j')), (0j, 0.2j, 0, "f", True), (0j, 0.2j, 0, "e", True), (-0j, 0.0j, 5, "f", True), (-0j, 0.0j, 5, "e", True), (-0j, 0.2j, 5, "f", ('0.00000', '0.00000+0.20000j')), (-0j, 0.2j, 5, "e", ('0.00000e+0', '0.00000e+0+2.00000e-1j')), (-0j, 0.2j, 0, "f", True), (-0j, 0.2j, 0, "e", True), ]) def test_number_to_string_complex_digits(self, t1, t2, significant_digits, number_format_notation, expected_result): st1 = number_to_string(t1, significant_digits=significant_digits, number_format_notation=number_format_notation) st2 = number_to_string(t2, significant_digits=significant_digits, number_format_notation=number_format_notation) if expected_result is True: assert st1 == st2 else: assert st1 == expected_result[0] assert st2 == expected_result[1] def test_number_to_string_with_invalid_notation(self): with pytest.raises(ValueError): number_to_string(10, significant_digits=4, number_format_notation='blah') def test_cartesian_product_of_shape(self): result = list(cartesian_product_of_shape([2, 1, 3])) assert [(0, 0, 0), (0, 0, 1), (0, 0, 2), (1, 0, 0), (1, 0, 1), (1, 0, 2)] == result def test_get_numpy_ndarray_rows(self): obj = np.array([[[1, 2, 3], [4, 5, 6]]], np.int32) path0 = (0, 0) row0 = np.array([1, 2, 3], dtype=np.int32) (path0, row0) = next(get_numpy_ndarray_rows(obj)) path1 = (0, 1) row1 = np.array([4, 5, 6], dtype=np.int32) (path1, row1) = next(get_numpy_ndarray_rows(obj)) @pytest.mark.parametrize('item, expected', [ ('10', 10), ("Decimal('10.1')", Decimal('10.1')), ("datetime.datetime(2021, 10, 13, 4, 54, 48, 959835)", datetime.datetime(2021, 10, 13, 4, 54, 48, 959835)), ("datetime.date(2021, 10, 13)", datetime.date(2021, 10, 13)), ]) def test_literal_eval_extended(self, item, expected): result = literal_eval_extended(item) assert expected == result def test_not_found_inequality(self): assert not_found != not_found @pytest.mark.parametrize('array1, array2, expected', [ (np.array([3, 1, 2, 4, 3]), np.array([5, 2, 4]), [3, 1, 3]), (np.array([5, 2, 4]), np.array([3, 1, 2, 4, 3]), [5]), ]) def test_diff_numpy_array(self, array1, array2, expected): result = diff_numpy_array(array1, array2) assert expected == result.tolist() def test_cartesian_product_numpy(self): result = cartesian_product_numpy(np.array([3, 1, 2, 4, 3]), np.array([5, 2, 4])) expected = [ [3, 5], [3, 2], [3, 4], [1, 5], [1, 2], [1, 4], [2, 5], [2, 2], [2, 4], [4, 5], [4, 2], [4, 4], [3, 5], [3, 2], [3, 4]] assert expected == result.tolist() def test_get_truncate_datetime(self): result = get_truncate_datetime('hour') assert 'hour' == result with pytest.raises(ValueError): get_truncate_datetime('blah') @pytest.mark.parametrize('truncate_datetime, obj, expected', [ ('hour', datetime.datetime(2020, 5, 30, 7, 28, 51, 698308), datetime.datetime(2020, 5, 30, 7, 0, tzinfo=datetime.timezone.utc)), ('day', datetime.datetime(2020, 5, 30, 7, 28, 51, 698308), datetime.datetime(2020, 5, 30, 0, 0, tzinfo=datetime.timezone.utc)), ]) def test_datetime_normalize(self, truncate_datetime, obj, expected): result = datetime_normalize(truncate_datetime, obj) assert expected == result @pytest.mark.parametrize('obj, include_keys, expected', [ ( MyEnum.A, ENUM_INCLUDE_KEYS, {'__objclass__': MyEnum, 'name': 'A', 'value': 1}, ) ]) def test_detailed__dict__(self, obj, include_keys, expected): result = detailed__dict__(obj, ignore_private_variables=True, include_keys=include_keys) assert expected == result, f"test_detailed__dict__ failed for {obj}" @pytest.mark.parametrize('test_num, value, expected', [ (1, ['ab'], {'root.ab', "root['ab']"}), (2, ['11'], {"root['11']", 'root[11]'}), (3, ['1a'], {"root['1a']"}), ]) def test_add_root_to_paths(self, test_num, value, expected): result = add_root_to_paths(value) assert expected == result, f"test_add_root_to_paths #{test_num} failed." @pytest.mark.parametrize('test_num, value, expected', [ (1, '1.2.3', 1002003), (2, '1.22.3', 1022003), (3, '1.22.3c', 1022003), (4, '2.4', 2004000), (5, '1.19.0', 1019000), ]) def test_get_semvar_as_integer(self, test_num, value, expected): result = get_semvar_as_integer(value) assert expected == result, f"test_get_semvar_as_integer #{test_num} failed." qlustered-deepdiff-41c7265/tests/test_ignore_order.py000066400000000000000000001460011516241264500230040ustar00rootroot00000000000000import pytest import re import datetime from unittest import mock from deepdiff.helper import number_to_string, CannotCompare from deepdiff import DeepDiff from decimal import Decimal from deepdiff.deephash import sha256hex from tests import CustomClass2 class TestIgnoreOrder: @pytest.mark.parametrize("t1, t2, significant_digits, ignore_order, result", [ (10, 10.0, 5, False, {}), ({10: 'a', 11.1: 'b'}, {10.0: 'a', Decimal('11.1000003'): 'b'}, 5, False, {}), ]) def test_type_change_numeric_ignored(self, t1, t2, significant_digits, ignore_order, result): ddiff = DeepDiff(t1, t2, ignore_numeric_type_changes=True, significant_digits=significant_digits, ignore_order=ignore_order) assert result == ddiff @pytest.mark.parametrize("t1, t2, expected_result", [ (10, 10.0, {}), (10, 10.2, {'values_changed': {'root': {'new_value': 10.2, 'old_value': 10}}}), (Decimal(10), 10.0, {}), ({"a": Decimal(10), "b": 12, 11.0: None}, {b"b": 12, "a": 10.0, Decimal(11): None}, {}), ]) def test_type_change_numeric_when_ignore_order(self, t1, t2, expected_result): ddiff = DeepDiff(t1, t2, ignore_order=True, ignore_numeric_type_changes=True, ignore_string_type_changes=True, threshold_to_diff_deeper=0) assert expected_result == ddiff def test_ignore_order_depth1(self): t1 = [{1, 2, 3}, {4, 5}] t2 = [{4, 5, 6}, {1, 2, 3}] ddiff = DeepDiff(t1, t2, ignore_order=True) assert {'set_item_added': ["root[1][6]"]} == ddiff def test_ignore_order_depth2(self): t1 = [[1, 2, 3], [4, 5]] t2 = [[4, 5, 6], [1, 2, 3]] ddiff = DeepDiff(t1, t2, ignore_order=True) assert {'iterable_item_added': {'root[1][2]': 6}} == ddiff def test_ignore_order_depth3(self): t1 = [{1, 2, 3}, [{4, 5}]] t2 = [[{4, 5, 6}], {1, 2, 3}] ddiff = DeepDiff(t1, t2, ignore_order=True) assert {'set_item_added': ["root[1][0][6]"]} == ddiff assert {"root[1][0][6]"} == ddiff.affected_paths def test_ignore_order_depth4(self): t1 = [[1, 2, 3, 4], [4, 2, 2, 1]] t2 = [[4, 1, 1, 1], [1, 3, 2, 4]] ddiff = DeepDiff(t1, t2, ignore_order=True) assert {'iterable_item_removed': {'root[1][1]': 2}} == ddiff def test_ignore_order_depth5(self): t1 = [4, 2, 2, 1] t2 = [4, 1, 1, 1] ddiff = DeepDiff(t1, t2, ignore_order=True, report_repetition=True, cache_purge_level=0) expected = { 'iterable_item_removed': { 'root[1]': 2, 'root[2]': 2 }, 'repetition_change': { 'root[3]': { 'old_repeat': 1, 'new_repeat': 3, 'old_indexes': [3], 'new_indexes': [1, 2, 3], 'value': 1 } } } assert expected == ddiff assert {"root[1]", "root[2]", "root[3]"} == ddiff.affected_paths ddiff = DeepDiff(t1, t2, ignore_order=True, report_repetition=False, cache_purge_level=0) dist = ddiff._get_rough_distance() assert 0.1 == dist def test_ignore_order_depth6(self): t1 = [[1, 2, 3, 4], [4, 2, 2, 1]] t2 = [[4, 1, 1, 1], [1, 3, 2, 4]] ddiff = DeepDiff(t1, t2, ignore_order=True, report_repetition=True) expected = { 'iterable_item_removed': { 'root[1][1]': 2, 'root[1][2]': 2 }, 'repetition_change': { 'root[1][3]': { 'old_repeat': 1, 'new_repeat': 3, 'old_indexes': [3], 'new_indexes': [1, 2, 3], 'value': 1 } } } assert expected == ddiff def test_list_difference_ignore_order(self): t1 = {1: 1, 4: {"a": "hello", "b": [1, 2, 3]}} t2 = {1: 1, 4: {"a": "hello", "b": [1, 3, 2, 3]}} ddiff = DeepDiff(t1, t2, ignore_order=True) assert {} == ddiff @pytest.mark.parametrize('t1_0, t2_0', [ (1, 2), (True, False), ('a', 'b'), ]) def test_list_difference_of_bool_only_ignore_order(self, t1_0, t2_0): t1 = [t1_0] t2 = [t2_0] ddiff = DeepDiff(t1, t2, ignore_order=True) result = {'values_changed': {'root[0]': {'new_value': t2_0, 'old_value': t1_0}}} assert result == ddiff def test_dictionary_difference_ignore_order(self): t1 = {"a": [[{"b": 2, "c": 4}, {"b": 2, "c": 3}]]} t2 = {"a": [[{"b": 2, "c": 3}, {"b": 2, "c": 4}]]} ddiff = DeepDiff(t1, t2, ignore_order=True) assert {} == ddiff assert set() == ddiff.affected_paths def test_nested_list_ignore_order(self): t1 = [1, 2, [3, 4]] t2 = [[4, 3, 3], 2, 1] ddiff = DeepDiff(t1, t2, ignore_order=True) assert {} == ddiff def test_nested_list_difference_ignore_order(self): t1 = [1, 2, [3, 4]] t2 = [[4, 3], 2, 1] ddiff = DeepDiff(t1, t2, ignore_order=True) assert {} == ddiff def test_nested_list_with_dictionarry_difference_ignore_order(self): t1 = [1, 2, [3, 4, {1: 2}]] t2 = [[4, 3, {1: 2}], 2, 1] ddiff = DeepDiff(t1, t2, ignore_order=True) result = {} assert result == ddiff def test_list_difference_ignore_order_report_repetition1(self): t1 = [1, 3, 1, 4] t2 = [4, 4, 1] ddiff = DeepDiff(t1, t2, ignore_order=True, report_repetition=True) result = { 'iterable_item_removed': { 'root[1]': 3 }, 'repetition_change': { 'root[0]': { 'old_repeat': 2, 'old_indexes': [0, 2], 'new_indexes': [2], 'value': 1, 'new_repeat': 1 }, 'root[3]': { 'old_repeat': 1, 'old_indexes': [3], 'new_indexes': [0, 1], 'value': 4, 'new_repeat': 2 } } } assert result == ddiff @pytest.mark.skip def test_list_difference_ignore_order_report_repetition2(self): t1 = [1, 1, 1] t2 = [2, 2] ddiff = DeepDiff(t1, t2, ignore_order=True) result = {'values_changed': {'root[0]': {'new_value': 2, 'old_value': 1}}} assert result == ddiff ddiff2 = DeepDiff(t1, t2, ignore_order=True, report_repetition=True, cutoff_intersection_for_pairs=1, cutoff_distance_for_pairs=1) result2 = { 'iterable_item_removed': { 'root[0]': 1, 'root[1]': 1, 'root[2]': 1 }, 'iterable_item_added': { 'root[0]': 2, 'root[1]': 2, }, } assert result2 == ddiff2 @pytest.mark.skip def test_list_difference_ignore_order_report_repetition3(self): t1 = [{"id": 1}, {"id": 1}, {"id": 1}] t2 = [{"id": 1, "name": 1}] ddiff2 = DeepDiff(t1, t2, ignore_order=True, report_repetition=True, cutoff_intersection_for_pairs=1, cutoff_distance_for_pairs=1) result2 = { 'iterable_item_removed': { 'root[1]': {"id": 1}, 'root[2]': {"id": 1}, }, 'dictionary_item_added': ["root[0]['name']"] } assert result2 == ddiff2 @pytest.mark.skip def test_list_difference_ignore_order_report_repetition4(self): t1 = [{"id": 1}, {"id": 1}, {"id": 1}, {"name": "Joe"}, {"name": "Joe"}] t2 = [{"id": 1, "name": 1}, {"id": 1, "name": "Joe"}] ddiff2 = DeepDiff(t1, t2, ignore_order=True, report_repetition=True, cutoff_intersection_for_pairs=1, cutoff_distance_for_pairs=1) result2 = { 'iterable_item_removed': { 'root[2]': {"id": 1}, 'root[3]': {"name": "Joe"}, 'root[4]': {"name": "Joe"}, }, 'dictionary_item_added': ["root[0]['name']", "root[1]['name']"] } assert result2 == ddiff2 def test_nested_list_ignore_order_report_repetition(self): t1 = [1, 2, [3, 4]] t2 = [[4, 3, 3], 2, 1] ddiff = DeepDiff(t1, t2, ignore_order=True, report_repetition=False) assert not ddiff ddiff2 = DeepDiff(t1, t2, ignore_order=True, report_repetition=True) result = { 'repetition_change': { 'root[2][0]': { 'old_repeat': 1, 'new_repeat': 2, 'old_indexes': [0], 'new_indexes': [1, 2], 'value': 3 } } } assert result == ddiff2 assert {"root[2][0]"} == ddiff2.affected_paths @pytest.mark.skip def test_nested_list_and_dict_ignore_order_report_repetition(self): """ This test shows that ignore order is not doing the right thing. It should have said that root[1] and root[2] are removed. """ t1 = [{"id": 1}, {"id": 1}, {"id": 1}] t2 = [{"id": 1, "name": 1}] ddiff = DeepDiff(t1, t2, ignore_order=True) result = {'dictionary_item_added': ["root[0]['name']"]} assert result == ddiff # Here there is nothing that is "repeated" in an iterable ddiff2 = DeepDiff(t1, t2, ignore_order=True, report_repetition=True) assert result == ddiff2 assert {"root[2][0]"} == ddiff2.affected_paths def test_list_of_unhashable_difference_ignore_order(self): t1 = [{"a": 2}, {"b": [3, 4, {1: 1}]}] t2 = [{"b": [3, 4, {1: 1}]}, {"a": 2}] ddiff = DeepDiff(t1, t2, ignore_order=True) assert {} == ddiff def test_list_of_unhashable_difference_ignore_order2(self): t1 = [1, {"a": 2}, {"b": [3, 4, {1: 1}]}, "B"] t2 = [{"b": [3, 4, {1: 1}]}, {"a": 2}, {1: 1}] ddiff = DeepDiff(t1, t2, ignore_order=True) result = { 'iterable_item_added': { 'root[2]': { 1: 1 } }, 'iterable_item_removed': { 'root[3]': 'B', 'root[0]': 1 } } assert result == ddiff def test_list_of_unhashable_difference_ignore_order3(self): t1 = [1, {"a": 2}, {"a": 2}, {"b": [3, 4, {1: 1}]}, "B"] t2 = [{"b": [3, 4, {1: 1}]}, {1: 1}] ddiff = DeepDiff(t1, t2, ignore_order=True) result = { 'values_changed': { 'root[1]': { 'new_value': { 1: 1 }, 'old_value': { 'a': 2 } } }, 'iterable_item_removed': { 'root[0]': 1, 'root[4]': 'B' } } assert result == ddiff def test_list_of_unhashable_difference_ignore_order_report_repetition( self): t1 = [1, {"a": 2}, {"a": 2}, {"b": [3, 4, {1: 1}]}, "B"] t2 = [{"b": [3, 4, {1: 1}]}, {1: 1}] ddiff = DeepDiff(t1, t2, ignore_order=True, report_repetition=True, threshold_to_diff_deeper=0) result = { 'iterable_item_added': { 'root[1]': { 1: 1 } }, 'iterable_item_removed': { 'root[4]': 'B', 'root[0]': 1, 'root[1]': { 'a': 2 }, 'root[2]': { 'a': 2 } } } assert result == ddiff def test_list_of_unhashable_difference_ignore_order4(self): t1 = [{"a": 2}, {"a": 2}] t2 = [{"a": 2}] ddiff = DeepDiff(t1, t2, ignore_order=True) result = {} assert result == ddiff def test_list_of_unhashable_difference_ignore_order_report_repetition2( self): t1 = [{"a": 2}, {"a": 2}] t2 = [{"a": 2}] ddiff = DeepDiff(t1, t2, ignore_order=True, report_repetition=True) result = { 'repetition_change': { 'root[0]': { 'old_repeat': 2, 'new_indexes': [0], 'old_indexes': [0, 1], 'value': { 'a': 2 }, 'new_repeat': 1 } } } assert result == ddiff def test_list_ignore_order_report_repetition(self): t1 = [5, 1, 3, 1, 4, 4, 6] t2 = [7, 4, 4, 1, 3, 4, 8] ddiff = DeepDiff(t1, t2, ignore_order=True, report_repetition=True) result = { 'values_changed': { 'root[6]': { 'new_value': 7, 'old_value': 6 }, 'root[0]': { 'new_value': 8, 'old_value': 5 } }, 'repetition_change': { 'root[4]': { 'old_repeat': 2, 'new_repeat': 3, 'old_indexes': [4, 5], 'new_indexes': [1, 2, 5], 'value': 4 }, 'root[1]': { 'old_repeat': 2, 'new_repeat': 1, 'old_indexes': [1, 3], 'new_indexes': [3], 'value': 1 } } } assert result == ddiff def test_list_of_sets_difference_ignore_order(self): t1 = [{1}, {2}, {3}] t2 = [{4}, {1}, {2}, {3}] ddiff = DeepDiff(t1, t2, ignore_order=True) result = {'iterable_item_added': {'root[0]': {4}}} assert result == ddiff def test_list_of_sets_difference_ignore_order_when_there_is_duplicate( self): t1 = [{1}, {2}, {3}] t2 = [{4}, {1}, {2}, {3}, {3}] ddiff = DeepDiff(t1, t2, ignore_order=True) result = {'iterable_item_added': {'root[0]': {4}}} assert result == ddiff def test_list_of_sets_difference_ignore_order_when_there_is_duplicate_and_mix_of_hashable_unhashable( self): t1 = [1, 1, {2}, {3}] t2 = [{4}, 1, {2}, {3}, {3}, 1, 1] ddiff = DeepDiff(t1, t2, ignore_order=True) result = {'iterable_item_added': {'root[0]': {4}}} assert result == ddiff def test_dictionary_of_list_of_dictionary_ignore_order(self): t1 = { 'item': [{ 'title': 1, 'http://purl.org/rss/1.0/modules/content/:encoded': '1' }, { 'title': 2, 'http://purl.org/rss/1.0/modules/content/:encoded': '2' }] } t2 = { 'item': [{ 'http://purl.org/rss/1.0/modules/content/:encoded': '1', 'title': 1 }, { 'http://purl.org/rss/1.0/modules/content/:encoded': '2', 'title': 2 }] } ddiff = DeepDiff(t1, t2, ignore_order=True) assert {} == ddiff def test_comprehensive_ignore_order(self): t1 = { 'key1': 'val1', 'key2': [ { 'key3': 'val3', 'key4': 'val4', }, { 'key5': 'val5', 'key6': 'val6', }, ], } t2 = { 'key1': 'val1', 'key2': [ { 'key5': 'val5', 'key6': 'val6', }, { 'key3': 'val3', 'key4': 'val4', }, ], } ddiff = DeepDiff(t1, t2, ignore_order=True) assert {} == ddiff def test_ignore_order_when_objects_similar(self): t1 = { 'key1': 'val1', 'key2': [ { 'key3': 'val3', 'key4': 'val4', }, { 'key5': 'val5', 'key6': 'val6', }, ], } t2 = { 'key1': 'val1', 'key2': [ { 'key5': 'CHANGE', 'key6': 'val6', }, { 'key3': 'val3', 'key4': 'val4', }, ], } ddiff = DeepDiff(t1, t2, ignore_order=True) assert {'values_changed': {"root['key2'][1]['key5']": {'new_value': 'CHANGE', 'old_value': 'val5'}}} == ddiff def test_set_ignore_order_report_repetition(self): """ If this test fails, it means that DeepDiff is not checking for set types before general iterables. So it forces creating the hashtable because of report_repetition=True. """ t1 = {2, 1, 8} t2 = {1, 2, 3, 5} ddiff = DeepDiff(t1, t2, ignore_order=True, report_repetition=True) result = { 'set_item_added': {'root[3]', 'root[5]'}, 'set_item_removed': {'root[8]'} } assert result == ddiff def test_custom_objects2(self): cc_a = CustomClass2(prop1=["a"], prop2=["b"]) cc_b = CustomClass2(prop1=["b"], prop2=["b"]) t1 = [cc_a, CustomClass2(prop1=["c"], prop2=["d"])] t2 = [cc_b, CustomClass2(prop1=["c"], prop2=["d"])] ddiff = DeepDiff(t1, t2, ignore_order=True) result = {'values_changed': {'root[0].prop1[0]': {'new_value': 'b', 'old_value': 'a'}}} assert result == ddiff def test_custom_object_type_change_when_ignore_order(self): class Burrito: bread = 'flour' def __init__(self): self.spicy = True class Taco: bread = 'flour' def __init__(self): self.spicy = True burrito = Burrito() taco = Taco() burritos = [burrito] tacos = [taco] assert not DeepDiff(burritos, tacos, ignore_type_in_groups=[(Taco, Burrito)], ignore_order=True) def test_decimal_ignore_order(self): t1 = [{1: Decimal('10.1')}, {2: Decimal('10.2')}] t2 = [{2: Decimal('10.2')}, {1: Decimal('10.1')}] ddiff = DeepDiff(t1, t2, ignore_order=True) result = {} assert result == ddiff @pytest.mark.parametrize('log_scale_similarity_threshold, expected', [ ( 0.1, {} ), ( 0.01, {'values_changed': {'root[1][2]': {'new_value': Decimal('268'), 'old_value': Decimal('290.2')}}} ), ]) def test_decimal_log_scale_ignore_order1(self, log_scale_similarity_threshold, expected): t1 = [{1: Decimal('10.143')}, {2: Decimal('290.2')}] t2 = [{2: Decimal('268')}, {1: Decimal('10.23')}] ddiff = DeepDiff(t1, t2, ignore_order=True, use_log_scale=True, log_scale_similarity_threshold=log_scale_similarity_threshold, cutoff_intersection_for_pairs=1) assert expected == ddiff @pytest.mark.parametrize("t1, t2, significant_digits, ignore_order", [ (100000, 100021, 3, False), ([10, 12, 100000], [50, 63, 100021], 3, False), ([10, 12, 100000], [50, 63, 100021], 3, True), ]) def test_number_to_string_func(self, t1, t2, significant_digits, ignore_order): def custom_number_to_string(number, *args, **kwargs): number = 100 if number < 100 else number return number_to_string(number, *args, **kwargs) ddiff = DeepDiff(t1, t2, significant_digits=3, number_format_notation="e", number_to_string_func=custom_number_to_string) assert {} == ddiff def test_ignore_type_in_groups_numbers_and_strings_when_ignore_order(self): t1 = [1, 2, 3, 'a'] t2 = [1.0, 2.0, 3.3, b'a'] ddiff = DeepDiff(t1, t2, ignore_numeric_type_changes=True, ignore_string_type_changes=True, ignore_order=True) result = {'values_changed': {'root[2]': {'new_value': 3.3, 'old_value': 3}}} assert result == ddiff def test_ignore_string_type_changes_when_dict_keys_merge_is_not_deterministic(self): t1 = {'a': 10, b'a': 20} t2 = {'a': 11, b'a': 22} ddiff = DeepDiff(t1, t2, ignore_numeric_type_changes=True, ignore_string_type_changes=True, ignore_order=True) result = {'values_changed': {"root['a']": {'new_value': 22, 'old_value': 20}}} alternative_result = {'values_changed': {"root['a']": {'new_value': 11, 'old_value': 10}}} assert result == ddiff or alternative_result == ddiff def test_skip_exclude_path5(self): exclude_paths = ["root[0]['e']", "root[1]['e']"] t1 = [{'a': 1, 'b': 'randomString', 'e': "1111"}] t2 = [{'a': 1, 'b': 'randomString', 'e': "2222"}] ddiff = DeepDiff(t1, t2, exclude_paths=exclude_paths, ignore_order=True, report_repetition=False) result = {} assert result == ddiff def test_skip_str_type_in_dict_on_list_when_ignored_order(self): t1 = [{1: "a"}] t2 = [{}] ddiff = DeepDiff(t1, t2, exclude_types=[str], ignore_order=True) result = {} assert result == ddiff @mock.patch('deepdiff.diff.logger') @mock.patch('deepdiff.diff.DeepHash') def test_diff_when_hash_fails(self, mock_DeepHash, mock_logger): mock_DeepHash.side_effect = ValueError('Boom!') t1 = {"blah": {4}, 2: 1337} t2 = {"blah": {4}, 2: 1337} with pytest.raises(ValueError) as exp: DeepDiff(t1, t2, ignore_order=True) assert 'Boom!' == str(exp.value) def test_bool_vs_number(self): t1 = { "A List": [ { "Value One": True, "Value Two": 1 } ], } t2 = { "A List": [ { "Value Two": 1, "Value One": True } ], } ddiff = DeepDiff(t1, t2, ignore_order=True, cutoff_intersection_for_pairs=1) assert ddiff == {} @pytest.mark.parametrize('max_passes, expected', [ (0, {'values_changed': {'root[0]': {'new_value': {'key5': 'CHANGE', 'key6': 'val6'}, 'old_value': {'key3': [[[[[1, 2, 4, 5]]]]], 'key4': [7, 8]}}, 'root[1]': {'new_value': {'key3': [[[[[1, 3, 5, 4]]]]], 'key4': [7, 8]}, 'old_value': {'key5': 'val5', 'key6': 'val6'}}}}), (1, {'values_changed': {"root[1]['key5']": {'new_value': 'CHANGE', 'old_value': 'val5', 'new_path': "root[0]['key5']"}, "root[0]['key3'][0]": {'new_value': [[[[1, 3, 5, 4]]]], 'old_value': [[[[1, 2, 4, 5]]]], 'new_path': "root[1]['key3'][0]"}}}), (22, {'values_changed': {"root[1]['key5']": {'new_value': 'CHANGE', 'old_value': 'val5', 'new_path': "root[0]['key5']"}, "root[0]['key3'][0][0][0][0][1]": {'new_value': 3, 'old_value': 2, 'new_path': "root[1]['key3'][0][0][0][0][1]"}}}) ]) def test_ignore_order_max_passes(self, max_passes, expected): t1 = [ { 'key3': [[[[[1, 2, 4, 5]]]]], 'key4': [7, 8], }, { 'key5': 'val5', 'key6': 'val6', }, ] t2 = [ { 'key5': 'CHANGE', 'key6': 'val6', }, { 'key3': [[[[[1, 3, 5, 4]]]]], 'key4': [7, 8], }, ] ddiff = DeepDiff(t1, t2, ignore_order=True, max_passes=max_passes, verbose_level=2, cache_size=5000, cutoff_intersection_for_pairs=1, threshold_to_diff_deeper=0) assert expected == ddiff @pytest.mark.parametrize('max_diffs, expected', [ (1, {}), (65, {'values_changed': {"root[1]['key5']": {'new_value': 'CHANGE', 'old_value': 'val5', 'new_path': "root[0]['key5']"}}}), (80, {'values_changed': {"root[1]['key5']": {'new_value': 'CHANGE', 'old_value': 'val5', 'new_path': "root[0]['key5']"}, "root[0]['key3'][0][0][0][0][1]": {'new_value': 3, 'old_value': 2, 'new_path': "root[1]['key3'][0][0][0][0][1]"}}}), ]) def test_ignore_order_max_diffs(self, max_diffs, expected): t1 = [ { 'key3': [[[[[1, 2, 4, 5]]]]], 'key4': [7, 8], }, { 'key5': 'val5', 'key6': 'val6', }, ] t2 = [ { 'key5': 'CHANGE', 'key6': 'val6', }, { 'key3': [[[[[1, 3, 5, 4]]]]], 'key4': [7, 8], }, ] # Note: these tests are not exactly deterministic ddiff = DeepDiff(t1, t2, ignore_order=True, max_diffs=max_diffs, verbose_level=2, cache_size=5000, cutoff_intersection_for_pairs=1) assert expected == ddiff def test_stats_that_include_distance_cache_hits(self): t1 = [ [1, 2, 3, 9], [9, 8, 5, 9] ] t2 = [ [1, 2, 4, 10], [4, 2, 5] ] diff = DeepDiff(t1, t2, ignore_order=True, cache_size=5000, cutoff_intersection_for_pairs=1) expected = { 'PASSES COUNT': 7, 'DIFF COUNT': 37, 'DISTANCE CACHE HIT COUNT': 0, 'MAX PASS LIMIT REACHED': False, 'MAX DIFF LIMIT REACHED': False, } assert expected == diff.get_stats() def test_ignore_order_report_repetition_and_self_loop(self): t1 = [[1, 2, 1, 3]] t1.append(t1) t2 = [[1, 2, 2, 2, 4]] t2.append(t2) diff = DeepDiff(t1, t2, ignore_order=True, cutoff_intersection_for_pairs=1) expected = { 'values_changed': { 'root[0][3]': { 'new_value': 4, 'old_value': 3 }, 'root[1]': { 'new_value': t2, 'old_value': t1 } } } assert expected == diff diff2 = DeepDiff(t1, t2, ignore_order=True, cache_size=5000, cutoff_intersection_for_pairs=1) assert expected == diff2 def test_ignore_order_with_sha256_hash(self): t1 = [ [1, 2, 3, 9], [9, 8, 5, 9] ] t2 = [ [1, 2, 3, 10], [8, 2, 5] ] diff = DeepDiff(t1, t2, ignore_order=True, hasher=sha256hex, cutoff_intersection_for_pairs=1) expected = { 'values_changed': { 'root[0][3]': { 'new_value': 10, 'old_value': 9 }, 'root[1][0]': { 'new_value': 2, 'old_value': 9 } } } assert expected == diff def test_ignore_order_cache_for_individual_distances(self): t1 = [[1, 2, 'B', 3], 'B'] t2 = [[1, 2, 3, 5], 5] diff = DeepDiff(t1, t2, ignore_order=True, cache_size=5000, cutoff_intersection_for_pairs=1) expected = { 'values_changed': { 'root[1]': { 'new_value': 5, 'old_value': 'B' } }, 'iterable_item_added': { 'root[0][3]': 5 }, 'iterable_item_removed': { 'root[0][2]': 'B' } } assert expected == diff stats = diff.get_stats() expected_stats = { 'PASSES COUNT': 3, 'DIFF COUNT': 13, 'DISTANCE CACHE HIT COUNT': 1, 'MAX PASS LIMIT REACHED': False, 'MAX DIFF LIMIT REACHED': False } assert expected_stats == stats t1 = [[1, 2, 'B', 3], 5] t2 = [[1, 2, 3, 5], 'B'] diff2 = DeepDiff(t1, t2, ignore_order=True, cache_size=5000, cutoff_intersection_for_pairs=1) assert expected_stats == diff2.get_stats() def test_cutoff_distance_for_pairs(self): t1 = [[1.0]] t2 = [[20.0]] diff1 = DeepDiff(t1, t2, ignore_order=True, cutoff_distance_for_pairs=0.3) expected1 = {'values_changed': {'root[0][0]': {'new_value': 20.0, 'old_value': 1.0}}} assert expected1 == diff1 diff2 = DeepDiff(t1, t2, ignore_order=True, cutoff_distance_for_pairs=0.1) expected2 = {'values_changed': {'root[0]': {'new_value': [20.0], 'old_value': [1.0]}}} assert expected2 == diff2 diff_with_dist = DeepDiff(1.0, 20.0, get_deep_distance=True) expected = {'values_changed': {'root': {'new_value': 20.0, 'old_value': 1.0}}, 'deep_distance': 0.2714285714285714} assert expected == diff_with_dist def test_ignore_order_and_group_by1(self): t1 = [ {'id': 'AA', 'name': 'Joe', 'ate': ['Nothing']}, {'id': 'BB', 'name': 'James', 'ate': ['Chips', 'Cheese']}, {'id': 'CC', 'name': 'Mike', 'ate': ['Apple']}, ] t2 = [ {'id': 'BB', 'name': 'James', 'ate': ['Chips', 'Brownies', 'Cheese']}, {'id': 'AA', 'name': 'Joe', 'ate': ['Nothing']}, {'id': 'CC', 'name': 'Mike', 'ate': ['Apple', 'Apple']}, ] diff = DeepDiff(t1, t2, group_by='id', ignore_order=False) expected = {'iterable_item_added': {"root['BB']['ate'][1]": 'Brownies', "root['CC']['ate'][1]": 'Apple'}} assert expected == diff diff2 = DeepDiff(t1, t2, group_by='id', ignore_order=True) expected2 = {'iterable_item_added': {"root['BB']['ate'][1]": 'Brownies'}} assert expected2 == diff2 def test_ignore_order_and_group_by2(self): t1_data = [{'id': '1', 'codes': ['1', '2', '3']}] t2_data = [{'id': '1', 'codes': ['1', '2', '4']}] diff = DeepDiff(t1_data, t2_data, group_by='id', ignore_order=True) expected = {'values_changed': {"root['1']['codes'][2]": {'new_value': '4', 'old_value': '3'}}} assert expected == diff def test_ignore_order_and_group_by3(self): t1 = [{ 'id': '5ec52e', 'products': [{ 'lineNumber': 1, 'productPrice': '2.39', 'productQuantity': 2 }, { 'lineNumber': 2, 'productPrice': '4.44', 'productQuantity': 1 }], }] t2 = [{ 'id': '5ec52e', 'products': [ { 'lineNumber': 2, 'productPrice': '4.44', 'productQuantity': 1 }, { 'lineNumber': 1, 'productPrice': '2.39', 'productQuantity': 2 }, ], }] diff = DeepDiff(t1, t2, group_by='id', ignore_order=True) assert {} == diff def test_ignore_order_and_group_by4(self): t1 = [ { "id": "1", "field_01": { "subfield_01": { "subfield_02": {"subfield_03": "1"}, } }, }, {"id": "2", "field_01": ["1", "2", "3"]}, {"id": "3", "field_01": ["1", "2", "3"]}, ] t2 = [ { "id": "1", "field_01": { "subfield_01": { "subfield_02": {"subfield_03": "2"}, } }, }, {"id": "2", "field_01": ["4", "5", "6"]}, {"id": "3", "field_01": ["7", "8", "9"]}, ] diff = DeepDiff(t1, t2, group_by='id', ignore_order=True) expected = { 'values_changed': { "root['1']['field_01']['subfield_01']['subfield_02']['subfield_03']": { 'new_value': '2', 'old_value': '1' }, "root['2']['field_01'][1]": { 'new_value': '5', 'old_value': '2' }, "root['3']['field_01'][2]": { 'new_value': '9', 'old_value': '3' }, "root['2']['field_01'][0]": { 'new_value': '4', 'old_value': '1' }, "root['3']['field_01'][1]": { 'new_value': '8', 'old_value': '2' }, "root['3']['field_01'][0]": { 'new_value': '7', 'old_value': '1' }, "root['2']['field_01'][2]": { 'new_value': '6', 'old_value': '3' } } } assert expected == diff def test_math_epsilon_when_ignore_order_in_dictionary(self): a = {'x': 0.001} b = {'x': 0.0011} diff = DeepDiff(a, b, ignore_order=True) assert {'values_changed': {"root['x']": {'new_value': 0.0011, 'old_value': 0.001}}} == diff diff2 = DeepDiff(a, b, ignore_order=True, math_epsilon=0.01) assert {} == diff2 def test_math_epsilon_when_ignore_order_in_list(self): a = [0.001, 2] b = [2, 0.0011] diff = DeepDiff(a, b, ignore_order=True) assert {'values_changed': {'root[0]': {'new_value': 0.0011, 'old_value': 0.001}}} == diff diff2 = DeepDiff(a, b, ignore_order=True, math_epsilon=0.01) assert {} == diff2 def test_math_epsilon_when_ignore_order_in_nested_list(self): a = [{'x': 0.001}, {'y': 2.00002}] b = [{'x': 0.0011}, {'y': 2}] diff = DeepDiff(a, b, ignore_order=True, math_epsilon=0.01) expected = {'values_changed': {'root[0]': {'new_value': {'x': 0.0011}, 'old_value': {'x': 0.001}}, 'root[1]': {'new_value': {'y': 2}, 'old_value': {'y': 2.00002}}}} assert expected == diff def test_datetime_and_ignore_order(self): diff = DeepDiff( [{'due_date': datetime.date(2024, 2, 1)}], [{'due_date': datetime.date(2024, 2, 2)}], ignore_order=True, ignore_numeric_type_changes=True ) assert {} != diff class TestCompareFuncIgnoreOrder: def test_ignore_order_with_compare_func_to_guide_comparison(self): t1 = [ { 'id': 1, 'value': [1] }, { 'id': 2, 'value': [7, 8, 1] }, { 'id': 3, 'value': [7, 8], }, ] t2 = [ { 'id': 2, 'value': [7, 8] }, { 'id': 3, 'value': [7, 8, 1], }, { 'id': 1, 'value': [1] }, ] expected = { 'values_changed': { "root[2]['id']": { 'new_value': 2, 'old_value': 3 }, "root[1]['id']": { 'new_value': 3, 'old_value': 2 } } } expected_with_compare_func = { 'iterable_item_added': { "root[2]['value'][2]": 1 }, 'iterable_item_removed': { "root[1]['value'][2]": 1 } } ddiff = DeepDiff(t1, t2, ignore_order=True) assert expected == ddiff def compare_func(x, y, level=None): try: return x['id'] == y['id'] except Exception: raise CannotCompare() from None ddiff2 = DeepDiff(t1, t2, ignore_order=True, iterable_compare_func=compare_func) assert expected_with_compare_func == ddiff2 assert ddiff != ddiff2 ddiff3 = DeepDiff(t1, t2, ignore_order=True, iterable_compare_func=compare_func, view='tree') assert 1 == ddiff3['iterable_item_removed'][0].t1 assert 1 == ddiff3['iterable_item_added'][0].t2 def test_ignore_order_with_compare_func_can_throw_cannot_compare(self): t1 = [ {1}, { 'id': 2, 'value': [7, 8, 1] }, { 'id': 3, 'value': [7, 8], }, ] t2 = [ { 'id': 2, 'value': [7, 8] }, { 'id': 3, 'value': [7, 8, 1], }, {}, ] expected = { 'type_changes': { 'root[0]': { 'old_type': set, 'new_type': dict, 'old_value': {1}, 'new_value': {} } }, 'values_changed': { "root[2]['id']": { 'new_value': 2, 'old_value': 3 }, "root[1]['id']": { 'new_value': 3, 'old_value': 2 } } } expected_with_compare_func = { 'type_changes': { 'root[0]': { 'old_type': set, 'new_type': dict, 'old_value': {1}, 'new_value': {} } }, 'iterable_item_added': { "root[2]['value'][2]": 1 }, 'iterable_item_removed': { "root[1]['value'][2]": 1 } } ddiff = DeepDiff(t1, t2, cutoff_intersection_for_pairs=1, cutoff_distance_for_pairs=1, ignore_order=True, threshold_to_diff_deeper=0) assert expected == ddiff def compare_func(x, y, level=None): try: return x['id'] == y['id'] except Exception: raise CannotCompare() from None ddiff2 = DeepDiff(t1, t2, ignore_order=True, cutoff_intersection_for_pairs=1, cutoff_distance_for_pairs=1, iterable_compare_func=compare_func, threshold_to_diff_deeper=0) assert expected_with_compare_func == ddiff2 assert ddiff != ddiff2 def test_ignore_order_with_compare_func_with_one_each_hashes_added_hashes_removed(self): """ Scenario: In this example which demonstrates the problem... We have two dictionaries containing lists for individualNames. Each list contains exactly 2 elements. The effective change is that we are replacing the 2nd element in the list. NOTE: This is considered a REPLACEMENT of the second element and not an UPDATE of the element because we are providing a custom compare_func which will determine matching elements based on the value of the nameIdentifier field. If the custom compare_func is not used, then deepdiff.diff will mistakenly treat the difference as being individual field updates for every field in the second element of the list. Intent: Use our custom compare_func, since we have provided it. We need to fall into self._precalculate_distance_by_custom_compare_func To do this, we are proposing a change to deepdiff.diff line 1128: Original: if hashes_added and hashes_removed and self.iterable_compare_func and len(hashes_added) > 1 and len(hashes_removed) > 1: Proposed/Updated: if hashes_added and hashes_removed \ and self.iterable_compare_func \ and len(hashes_added) > 0 and len(hashes_removed) > 0: NOTE: It is worth mentioning that deepdiff.diff line 1121, might also benefit by changing the length conditions to evaluate for > 0 (rather than > 1). """ t1 = { "individualNames": [ { "firstName": "Johnathan", "lastName": "Doe", "prefix": "COLONEL", "middleName": "A", "primaryIndicator": True, "professionalDesignation": "PHD", "suffix": "SR", "nameIdentifier": "00001" }, { "firstName": "John", "lastName": "Doe", "prefix": "", "middleName": "", "primaryIndicator": False, "professionalDesignation": "", "suffix": "SR", "nameIdentifier": "00002" } ] } t2 = { "individualNames": [ { "firstName": "Johnathan", "lastName": "Doe", "prefix": "COLONEL", "middleName": "A", "primaryIndicator": True, "professionalDesignation": "PHD", "suffix": "SR", "nameIdentifier": "00001" }, { "firstName": "Johnny", "lastName": "Doe", "prefix": "", "middleName": "A", "primaryIndicator": False, "professionalDesignation": "", "suffix": "SR", "nameIdentifier": "00003" } ] } def compare_func(item1, item2, level=None): print("*** inside compare ***") it1_keys = item1.keys() try: # --- individualNames --- if 'nameIdentifier' in it1_keys and 'lastName' in it1_keys: match_result = item1['nameIdentifier'] == item2['nameIdentifier'] print("individualNames - matching result:", match_result) return match_result else: print("Unknown list item...", "matching result:", item1 == item2) return item1 == item2 except Exception: raise CannotCompare() from None # ---------------------------- End of nested function actual_diff = DeepDiff(t1, t2, report_repetition=True, ignore_order=True, iterable_compare_func=compare_func, cutoff_intersection_for_pairs=1) old_invalid_diff = { 'values_changed': {"root['individualNames'][1]['firstName']": {'new_value': 'Johnny', 'old_value': 'John'}, "root['individualNames'][1]['middleName']": {'new_value': 'A', 'old_value': ''}, "root['individualNames'][1]['nameIdentifier']": {'new_value': '00003', 'old_value': '00002'}}} new_expected_diff = {'iterable_item_added': { "root['individualNames'][1]": {'firstName': 'Johnny', 'lastName': 'Doe', 'prefix': '', 'middleName': 'A', 'primaryIndicator': False, 'professionalDesignation': '', 'suffix': 'SR', 'nameIdentifier': '00003'}}, 'iterable_item_removed': { "root['individualNames'][1]": {'firstName': 'John', 'lastName': 'Doe', 'prefix': '', 'middleName': '', 'primaryIndicator': False, 'professionalDesignation': '', 'suffix': 'SR', 'nameIdentifier': '00002'}}} assert old_invalid_diff != actual_diff assert new_expected_diff == actual_diff class TestDynamicIgnoreOrder: def test_ignore_order_func(self): t1 = { "order_matters": [ {1}, { 'id': 2, 'value': [7, 8, 1] }, { 'id': 3, 'value': [7, 8], }, ], "order_does_not_matter": [ {1}, { 'id': 2, 'value': [7, 8, 1] }, { 'id': 3, 'value': [7, 8], }, ] } t2 = { "order_matters": [ { 'id': 2, 'value': [7, 8] }, { 'id': 3, 'value': [7, 8, 1], }, {}, ], "order_does_not_matter": [ { 'id': 2, 'value': [7, 8] }, { 'id': 3, 'value': [7, 8, 1], }, {}, ] } def ignore_order_func(level): return "order_does_not_matter" in level.path() ddiff = DeepDiff(t1, t2, cutoff_intersection_for_pairs=1, cutoff_distance_for_pairs=1, ignore_order_func=ignore_order_func, threshold_to_diff_deeper=0) expected = { 'type_changes': { "root['order_matters'][0]": { 'old_type': set, 'new_type': dict, 'old_value': {1}, 'new_value': {'id': 2, 'value': [7, 8]} }, "root['order_does_not_matter'][0]": { 'old_type': set, 'new_type': dict, 'old_value': {1}, 'new_value': {} } }, 'dictionary_item_removed': [ "root['order_matters'][2]['id']", "root['order_matters'][2]['value']" ], 'values_changed': { "root['order_matters'][1]['id']": {'new_value': 3, 'old_value': 2}, "root['order_does_not_matter'][2]['id']": {'new_value': 2, 'old_value': 3}, "root['order_does_not_matter'][1]['id']": {'new_value': 3, 'old_value': 2} } } assert expected == ddiff class TestDecodingErrorIgnoreOrder: EXPECTED_MESSAGE1 = ( "'utf-8' codec can't decode byte 0xc3 in position 0: Can not produce a hash for root: invalid continuation byte in '('. " "Please either pass ignore_encoding_errors=True or pass the encoding via encodings=['utf-8', '...'].") EXPECTED_MESSAGE2 = ( "'utf-8' codec can't decode byte 0xbc in position 0: Can not produce a hash for root: invalid start byte in ' cup of flour'. " "Please either pass ignore_encoding_errors=True or pass the encoding via encodings=['utf-8', '...'].") @pytest.mark.parametrize('test_num, item, encodings, ignore_encoding_errors, expected_result, expected_message', [ (1, b'\xc3\x28', None, False, UnicodeDecodeError, EXPECTED_MESSAGE1), (2, b'\xc3\x28', ['utf-8'], False, UnicodeDecodeError, EXPECTED_MESSAGE1), (3, b'\xc3\x28', ['utf-8'], True, {'values_changed': {'root[0]': {'new_value': b'\xc3(', 'old_value': b'foo'}}}, None), (4, b"\xbc cup of flour", ['utf-8'], False, UnicodeDecodeError, EXPECTED_MESSAGE2), (5, b"\xbc cup of flour", ['utf-8'], True, {'values_changed': {'root[0]': {'new_value': b'\xbc cup of flour', 'old_value': b'foo'}}}, None), (6, b"\xbc cup of flour", ['utf-8', 'latin-1'], False, {'values_changed': {'root[0]': {'new_value': b'\xbc cup of flour', 'old_value': b'foo'}}}, None), ]) @mock.patch('deepdiff.diff.logger') def test_diff_encodings(self, mock_logger, test_num, item, encodings, ignore_encoding_errors, expected_result, expected_message): if UnicodeDecodeError == expected_result: with pytest.raises(expected_result) as exc_info: DeepDiff([b'foo'], [item], encodings=encodings, ignore_encoding_errors=ignore_encoding_errors, ignore_order=True) assert expected_message == str(exc_info.value), f"test_diff_encodings test #{test_num} failed." else: result = DeepDiff([b'foo'], [item], encodings=encodings, ignore_encoding_errors=ignore_encoding_errors, ignore_order=True) assert expected_result == result, f"test_diff_encodings test #{test_num} failed." class TestErrorMessagesWhenIgnoreOrder: @mock.patch('deepdiff.diff.logger') def test_error_messages_when_ignore_order(self, mock_logger): t1 = {'x': 0, 'y': [0, 'a', 'b', 'c']} t2 = {'x': 1, 'y': [1, 'c', 'b', 'a']} exclude = [re.compile(r"\['x'\]"), re.compile(r"\['y'\]\[0\]")] result = DeepDiff(t1, t2, ignore_order=True, exclude_regex_paths=exclude) assert {} == result assert not mock_logger.error.called class TestIgnoreOrderNumericTypeChange: """Regression tests for GitHub issue #485. When ignore_order=True, numerically-equal values of different numeric types (e.g. int 1 vs float 1.0) must still be reported as type_changes. Root cause: Python's hash equality (hash(1) == hash(1.0)) and value equality (1 == 1.0) caused both items to land in the same DeepHash bucket, so they were treated as identical and silently dropped from the diff result. """ def test_int_vs_float_in_list_of_dicts(self): """Core regression: type change inside a dict nested in a list.""" result = DeepDiff([{"a": 1}], [{"a": 1.0}], ignore_order=True) assert "type_changes" in result, ( "Expected type_changes between int 1 and float 1.0, got: %s" % result ) assert result["type_changes"]["root[0]['a']"]["old_type"] is int assert result["type_changes"]["root[0]['a']"]["new_type"] is float def test_ignore_numeric_type_changes_suppresses_report(self): """When ignore_numeric_type_changes=True the type change must be hidden.""" result = DeepDiff( [{"a": 1}], [{"a": 1.0}], ignore_order=True, ignore_numeric_type_changes=True, ) assert result == {}, ( "With ignore_numeric_type_changes=True there should be no diff, got: %s" % result ) def test_value_change_still_detected(self): """Ordinary value differences must still be detected.""" result = DeepDiff([1], [2], ignore_order=True) assert result != {}, "Expected a diff between [1] and [2]" def test_reorder_no_false_positive(self): """A simple reorder of identical values must not trigger type_changes.""" result = DeepDiff([1, 2, 3], [3, 2, 1], ignore_order=True) assert result == {}, "Reordering identical ints must not produce a diff" def test_mixed_list_one_type_change(self): """Only the item with a type change should appear in the diff.""" result = DeepDiff( [{"a": 1}, {"b": 2}], [{"b": 2}, {"a": 1.0}], ignore_order=True, ) assert "type_changes" in result assert "root[0]['a']" in result["type_changes"] def test_ignore_order_false_unchanged(self): """The ignore_order=False path must continue to work as before.""" result = DeepDiff([{"a": 1}], [{"a": 1.0}], ignore_order=False) assert "type_changes" in result qlustered-deepdiff-41c7265/tests/test_ignore_uuid_types.py000066400000000000000000000133521516241264500240650ustar00rootroot00000000000000#!/usr/bin/env python import uuid import unittest from deepdiff import DeepDiff class TestIgnoreUuidTypes(unittest.TestCase): """Test ignore_uuid_types functionality""" def test_uuid_vs_string_without_ignore(self): """Test that UUID vs string reports type change by default""" test_uuid = uuid.UUID('12345678-1234-5678-1234-567812345678') uuid_str = '12345678-1234-5678-1234-567812345678' result = DeepDiff(test_uuid, uuid_str) assert 'type_changes' in result assert result['type_changes']['root']['old_type'] == uuid.UUID assert result['type_changes']['root']['new_type'] == str assert result['type_changes']['root']['old_value'] == test_uuid assert result['type_changes']['root']['new_value'] == uuid_str def test_uuid_vs_string_with_ignore(self): """Test that UUID vs string is ignored when ignore_uuid_types=True""" test_uuid = uuid.UUID('12345678-1234-5678-1234-567812345678') uuid_str = '12345678-1234-5678-1234-567812345678' result = DeepDiff(test_uuid, uuid_str, ignore_uuid_types=True) assert result == {} def test_string_vs_uuid_with_ignore(self): """Test that string vs UUID is ignored when ignore_uuid_types=True (reverse order)""" test_uuid = uuid.UUID('12345678-1234-5678-1234-567812345678') uuid_str = '12345678-1234-5678-1234-567812345678' result = DeepDiff(uuid_str, test_uuid, ignore_uuid_types=True) assert result == {} def test_different_uuid_values_with_ignore(self): """Test that different UUID values are still reported""" uuid1 = uuid.UUID('12345678-1234-5678-1234-567812345678') uuid2 = uuid.UUID('87654321-4321-8765-4321-876543218765') result = DeepDiff(uuid1, uuid2, ignore_uuid_types=True) assert 'values_changed' in result assert result['values_changed']['root']['old_value'] == uuid1 assert result['values_changed']['root']['new_value'] == uuid2 def test_uuid_vs_different_string_with_ignore(self): """Test that UUID vs different UUID string reports value change""" test_uuid = uuid.UUID('12345678-1234-5678-1234-567812345678') different_str = '87654321-4321-8765-4321-876543218765' result = DeepDiff(test_uuid, different_str, ignore_uuid_types=True) assert 'values_changed' in result assert result['values_changed']['root']['old_value'] == test_uuid assert result['values_changed']['root']['new_value'] == different_str def test_uuid_vs_invalid_string_with_ignore(self): """Test that UUID vs invalid UUID string reports value change""" test_uuid = uuid.UUID('12345678-1234-5678-1234-567812345678') invalid_str = 'not-a-uuid' result = DeepDiff(test_uuid, invalid_str, ignore_uuid_types=True) assert 'values_changed' in result assert result['values_changed']['root']['old_value'] == test_uuid assert result['values_changed']['root']['new_value'] == invalid_str def test_uuid_in_dict_with_ignore(self): """Test that UUID vs string in dictionaries works correctly""" test_uuid = uuid.UUID('12345678-1234-5678-1234-567812345678') uuid_str = '12345678-1234-5678-1234-567812345678' dict1 = {'id': test_uuid, 'name': 'test', 'count': 42} dict2 = {'id': uuid_str, 'name': 'test', 'count': 42} result = DeepDiff(dict1, dict2, ignore_uuid_types=True) assert result == {} def test_uuid_in_list_with_ignore(self): """Test that UUID vs string in lists works correctly""" test_uuid = uuid.UUID('12345678-1234-5678-1234-567812345678') uuid_str = '12345678-1234-5678-1234-567812345678' list1 = [test_uuid, 'test', 42] list2 = [uuid_str, 'test', 42] result = DeepDiff(list1, list2, ignore_uuid_types=True) assert result == {} def test_mixed_uuid_comparisons_with_ignore(self): """Test mixed UUID/string comparisons in nested structures""" uuid1 = uuid.UUID('12345678-1234-5678-1234-567812345678') uuid2 = uuid.UUID('87654321-4321-8765-4321-876543218765') data1 = { 'uuid_obj': uuid1, 'uuid_str': '12345678-1234-5678-1234-567812345678', 'nested': { 'id': uuid2, 'items': [uuid1, 'test'] } } data2 = { 'uuid_obj': '12345678-1234-5678-1234-567812345678', # string version 'uuid_str': uuid1, # UUID object version 'nested': { 'id': '87654321-4321-8765-4321-876543218765', # string version 'items': ['12345678-1234-5678-1234-567812345678', 'test'] # string version } } result = DeepDiff(data1, data2, ignore_uuid_types=True) assert result == {} def test_uuid_with_other_ignore_flags(self): """Test that ignore_uuid_types works with other ignore flags""" test_uuid = uuid.UUID('12345678-1234-5678-1234-567812345678') data1 = { 'id': test_uuid, 'name': 'TEST', 'count': 42 } data2 = { 'id': '12345678-1234-5678-1234-567812345678', 'name': 'test', # different case 'count': 42.0 # different numeric type } result = DeepDiff(data1, data2, ignore_uuid_types=True, ignore_string_case=True, ignore_numeric_type_changes=True) assert result == {} if __name__ == '__main__': unittest.main()qlustered-deepdiff-41c7265/tests/test_lfucache.py000066400000000000000000000036111516241264500220770ustar00rootroot00000000000000import random import pytest import concurrent.futures from deepdiff.lfucache import LFUCache class TestLFUcache: @pytest.mark.parametrize("items, size, expected_results, expected_freq", [ (['a', 'a', 'b', 'a', 'c', 'b', 'd'], 3, [('b', 2), ('c', 1), ('d', 1)], '1.333'), (['a', 'a', 'b', 'a', 'c', 'b', 'd', 'e', 'c', 'b'], 3, [('b', 3), ('d', 1), ('e', 1)], '1.666'), (['a', 'a', 'b', 'a', 'c', 'b', 'd', 'e', 'c', 'b', 'b', 'c', 'd', 'b'], 3, [('b', 5), ('c', 3), ('d', 2)], '3.333'), ]) def test_lfu(self, items, size, expected_results, expected_freq, benchmark): benchmark(self._test_lfu, items, size, expected_results, expected_freq) def _test_lfu(self, items, size, expected_results, expected_freq): lfucache = LFUCache(size) for item in items: lfucache.set(item, value='{}_cached'.format(item)) for item in items: lfucache.get(item) results = lfucache.get_sorted_cache_keys() assert expected_results == results freq = lfucache.get_average_frequency() assert expected_freq == str(freq)[:5] def test_get_multithreading(self): keys = 'aaaaaaaaaaaaaaaaaaaaaaaaaaabbc' lfucache = LFUCache(2) def _do_set(cache, key): cache.set(key, value='{}_cached'.format(key)) def _do_get(cache, key): return cache.get(key) def _key_gen(): i = 0 while i < 30000: i += 1 yield random.choice(keys) def _random_func(cache, key): return random.choice([_do_get, _do_get, _do_set])(cache, key) with concurrent.futures.ThreadPoolExecutor(max_workers=30) as executor: futures = (executor.submit(_random_func, lfucache, key) for key in _key_gen()) for future in concurrent.futures.as_completed(futures): future.result() qlustered-deepdiff-41c7265/tests/test_memoryview.py000066400000000000000000000134031516241264500225300ustar00rootroot00000000000000#!/usr/bin/env python import pytest from deepdiff import DeepDiff class TestMemoryView: """Test memoryview support in DeepDiff""" def test_memoryview_basic_comparison(self): """Test basic memoryview comparison without ignore_string_type_changes""" t1 = memoryview(b"hello") t2 = memoryview(b"world") diff = DeepDiff(t1, t2) assert 'values_changed' in diff assert diff['values_changed']['root']['old_value'] == t1 assert diff['values_changed']['root']['new_value'] == t2 def test_memoryview_with_bytes_type_change(self): """Test memoryview vs bytes comparison shows type change""" t1 = memoryview(b"hello") t2 = b"hello" diff = DeepDiff(t1, t2) assert 'type_changes' in diff assert diff['type_changes']['root']['old_type'] == memoryview assert diff['type_changes']['root']['new_type'] == bytes assert diff['type_changes']['root']['old_value'] == t1 assert diff['type_changes']['root']['new_value'] == t2 def test_memoryview_with_str_type_change(self): """Test memoryview vs str comparison shows type change""" t1 = memoryview(b"hello") t2 = "hello" diff = DeepDiff(t1, t2) assert 'type_changes' in diff assert diff['type_changes']['root']['old_type'] == memoryview assert diff['type_changes']['root']['new_type'] == str assert diff['type_changes']['root']['old_value'] == t1 assert diff['type_changes']['root']['new_value'] == t2 def test_memoryview_ignore_string_type_changes_with_bytes(self): """Test memoryview vs bytes with ignore_string_type_changes=True""" t1 = memoryview(b"hello") t2 = b"hello" diff = DeepDiff(t1, t2, ignore_string_type_changes=True) assert diff == {} def test_memoryview_ignore_string_type_changes_with_str(self): """Test memoryview vs str with ignore_string_type_changes=True""" t1 = memoryview(b"hello") t2 = "hello" diff = DeepDiff(t1, t2, ignore_string_type_changes=True) assert diff == {} def test_memoryview_different_content_with_ignore_string_type_changes(self): """Test memoryview with different content still shows value change""" t1 = memoryview(b"hello") t2 = "world" diff = DeepDiff(t1, t2, ignore_string_type_changes=True) assert 'values_changed' in diff # The values in the diff are the original objects, not converted strings assert diff['values_changed']['root']['old_value'] == t1 assert diff['values_changed']['root']['new_value'] == t2 def test_memoryview_in_dict_keys(self): """Test memoryview as dictionary keys""" t1 = {memoryview(b"key1"): "value1", memoryview(b"key2"): "value2"} t2 = {b"key1": "value1", "key2": "value2"} # Without ignore_string_type_changes, should show differences diff = DeepDiff(t1, t2) assert 'dictionary_item_removed' in diff or 'dictionary_item_added' in diff # With ignore_string_type_changes, should be equal diff = DeepDiff(t1, t2, ignore_string_type_changes=True) assert diff == {} def test_memoryview_in_list(self): """Test memoryview in lists""" t1 = [memoryview(b"hello"), memoryview(b"world")] t2 = ["hello", b"world"] diff = DeepDiff(t1, t2, ignore_string_type_changes=True) assert diff == {} def test_memoryview_in_nested_structure(self): """Test memoryview in nested structures""" t1 = { "data": { "items": [memoryview(b"item1"), memoryview(b"item2")], "metadata": {memoryview(b"key"): "value"} } } t2 = { "data": { "items": ["item1", b"item2"], "metadata": {"key": "value"} } } diff = DeepDiff(t1, t2, ignore_string_type_changes=True) assert diff == {} def test_memoryview_with_non_ascii_bytes(self): """Test memoryview with non-ASCII bytes""" t1 = memoryview(b"\x80\x81\x82") t2 = b"\x80\x81\x82" diff = DeepDiff(t1, t2, ignore_string_type_changes=True) assert diff == {} def test_memoryview_text_diff(self): """Test that text diff works with memoryview""" t1 = {"data": memoryview(b"hello\nworld")} t2 = {"data": memoryview(b"hello\nearth")} diff = DeepDiff(t1, t2) assert 'values_changed' in diff assert "root['data']" in diff['values_changed'] # Should contain diff output assert 'diff' in diff['values_changed']["root['data']"] def test_memoryview_with_ignore_type_in_groups(self): """Test memoryview with ignore_type_in_groups parameter""" from deepdiff.helper import strings t1 = memoryview(b"hello") t2 = "hello" # Using ignore_type_in_groups with strings tuple diff = DeepDiff(t1, t2, ignore_type_in_groups=[strings]) assert diff == {} def test_memoryview_hash(self): """Test that DeepHash works with memoryview""" from deepdiff import DeepHash # Test basic hashing obj1 = memoryview(b"hello") hash1 = DeepHash(obj1) assert hash1[obj1] # Test with ignore_string_type_changes obj2 = "hello" hash2 = DeepHash(obj2, ignore_string_type_changes=True) hash1_ignore = DeepHash(obj1, ignore_string_type_changes=True) # When ignoring string type changes, memoryview and str of same content should hash the same assert hash1_ignore[obj1] == hash2[obj2]qlustered-deepdiff-41c7265/tests/test_model.py000066400000000000000000000245641516241264500214370ustar00rootroot00000000000000#!/usr/bin/env python import datetime import logging import pytest from tests import CustomClass, CustomClassMisleadingRepr from deepdiff import DeepDiff from deepdiff.model import (DiffLevel, ChildRelationship, DictRelationship, SubscriptableIterableRelationship, AttributeRelationship) logging.disable(logging.CRITICAL) class WorkingChildRelationship(ChildRelationship): pass class TestDictRelationship: def setup_class(cls): cls.customkey = CustomClass(a=13, b=37) cls.customkey_misleading = CustomClassMisleadingRepr(a=11, b=20) cls.d = { 42: 'answer', 'vegan': 'for life', cls.customkey: 1337, cls.customkey_misleading: 'banana' } def test_numkey(self): rel = DictRelationship(parent=self.d, child=self.d[42], param=42) assert rel.get_param_repr() == "[42]" def test_strkey(self): rel = ChildRelationship.create( klass=DictRelationship, parent=self.d, child=self.d['vegan'], param='vegan') result = rel.get_param_repr() assert result == "['vegan']" def test_objkey(self): rel = DictRelationship( parent=self.d, child=self.d[self.customkey], param=self.customkey) assert rel.get_param_repr() is None def test_objkey_misleading_repr(self): rel = DictRelationship( parent=self.d, child=self.d[self.customkey_misleading], param=self.customkey_misleading) assert rel.get_param_repr() is None class TestListRelationship: def setup_class(cls): cls.custom = CustomClass(13, 37) cls.l = [1337, 'vegan', cls.custom] def test_min(self): rel = SubscriptableIterableRelationship(self.l, self.l[0], 0) result = rel.get_param_repr() assert result == "[0]" def test_max(self): rel = ChildRelationship.create(SubscriptableIterableRelationship, self.l, self.custom, 2) assert rel.get_param_repr() == "[2]" class TestAttributeRelationship: def setup_class(cls): cls.custom = CustomClass(13, 37) def test_a(self): rel = AttributeRelationship(self.custom, 13, "a") result = rel.get_param_repr() assert result == ".a" class TestDiffLevel: def setup_class(cls): # Test data cls.custom1 = CustomClass(a='very long text here, much longer than you can ever imagine. The longest text here.', b=37) cls.custom2 = CustomClass(a=313, b=37) cls.t1 = {42: 'answer', 'vegan': 'for life', 1337: cls.custom1} cls.t2 = { 42: 'answer', 'vegan': 'for the animals', 1337: cls.custom2 } # Manually build diff, bottom up cls.lowest = DiffLevel( cls.custom1.a, cls.custom2.a, report_type='values_changed') # Test manual child relationship rel_int_low_t1 = AttributeRelationship( parent=cls.custom1, child=cls.custom1.a, param="a") rel_int_low_t2 = AttributeRelationship( parent=cls.custom2, child=cls.custom2.a, param="a") cls.intermediate = DiffLevel( cls.custom1, cls.custom2, down=cls.lowest, child_rel1=rel_int_low_t1, child_rel2=rel_int_low_t2) cls.lowest.up = cls.intermediate # Test automatic child relationship t1_child_rel = ChildRelationship.create( klass=DictRelationship, parent=cls.t1, child=cls.intermediate.t1, param=1337) t2_child_rel = ChildRelationship.create( klass=DictRelationship, parent=cls.t2, child=cls.intermediate.t2, param=1337) cls.highest = DiffLevel( cls.t1, cls.t2, down=cls.intermediate, child_rel1=t1_child_rel, child_rel2=t2_child_rel) cls.intermediate.up = cls.highest def test_all_up(self): assert self.lowest.all_up == self.highest def test_all_down(self): assert self.highest.all_down == self.lowest def test_automatic_child_rel(self): assert isinstance(self.highest.t1_child_rel, DictRelationship) assert isinstance(self.highest.t2_child_rel, DictRelationship) assert self.highest.t1_child_rel.parent == self.highest.t1 assert self.highest.t2_child_rel.parent == self.highest.t2 assert self.highest.t1_child_rel.parent == self.highest.t1 assert self.highest.t2_child_rel.parent == self.highest.t2 # Provides textual relationship from t1 to t1[1337] assert '[1337]' == self.highest.t2_child_rel.get_param_repr() def test_path(self): # Provides textual path all the way through assert self.lowest.path("self.t1") == "self.t1[1337].a" def test_path_output_list(self): # Provides textual path all the way through assert self.lowest.path(output_format="list") == [1337, 'a'] def test_change_of_path_root(self): assert self.lowest.path("root") == "root[1337].a" assert self.lowest.path("") == "[1337].a" def test_path_when_both_children_empty(self): """ This is a situation that should never happen. But we are creating it artificially. """ t1 = {1: 1} t2 = {1: 2} child_t1 = {} child_t2 = {} up = DiffLevel(t1, t2) down = up.down = DiffLevel(child_t1, child_t2) path = down.path() assert path == 'root' assert down.path(output_format='list') == [] def test_t2_path_when_nested(self): t1 = { "type": "struct", "fields": [ {"name": "Competition", "metadata": {}, "nullable": True, "type": "string"}, {"name": "TeamName", "metadata": {}, "nullable": True, "type": "string"}, { "name": "Contents", "metadata": {}, "nullable": True, "type": { "type": "struct", "fields": [ {"name": "Date", "metadata": {}, "nullable": True, "type": "string"}, {"name": "Player1", "metadata": {}, "nullable": True, "type": "string"} ] } } ] } t2 = { "type": "struct", "fields": [ {"name": "Competition", "metadata": {}, "nullable": True, "type": "string"}, {"name": "GlobalId", "metadata": {}, "nullable": True, "type": "string"}, {"name": "TeamName", "metadata": {}, "nullable": True, "type": "string"}, { "name": "Contents", "metadata": {}, "nullable": True, "type": { "type": "struct", "fields": [ {"name": "Date", "metadata": {}, "nullable": True, "type": "string"}, {"name": "Player1", "metadata": {}, "nullable": True, "type": "string"}, {"name": "Player2", "metadata": {}, "nullable": True, "type": "string"} ] } } ] } diff = DeepDiff(t1=t1, t2=t2, ignore_order=True, verbose_level=2, view='tree') expected_diff = { "iterable_item_added": { "root['fields'][1]": { "name": "GlobalId", "metadata": {}, "nullable": True, "type": "string", }, "root['fields'][2]['type']['fields'][2]": { "name": "Player2", "metadata": {}, "nullable": True, "type": "string", }, } } path = diff['iterable_item_added'][1].path() assert "root['fields'][2]['type']['fields'][2]" == path path_t2 = diff['iterable_item_added'][1].path(use_t2=True) assert "root['fields'][3]['type']['fields'][2]" == path_t2 def test_repr_short(self): level = self.lowest.verbose_level try: self.lowest.verbose_level = 0 item_repr = repr(self.lowest) finally: self.lowest.verbose_level = level assert item_repr == '' def test_repr_long(self): level = self.lowest.verbose_level try: self.lowest.verbose_level = 1 item_repr = repr(self.lowest) finally: self.lowest.verbose_level = level assert item_repr == '' def test_repr_very_long(self): level = self.lowest.verbose_level try: self.lowest.verbose_level = 2 item_repr = repr(self.lowest) finally: self.lowest.verbose_level = level assert item_repr == '' def test_repetition_attribute_and_repr(self): t1 = [1, 1] t2 = [1] some_repetition = 'some repetition' node = DiffLevel(t1, t2) node.additional['repetition'] = some_repetition assert node.repetition == some_repetition assert repr(node) == '' class TestChildRelationship: def test_create_invalid_klass(self): with pytest.raises(TypeError): ChildRelationship.create(DiffLevel, "hello", 42) def test_rel_repr_short(self): rel = WorkingChildRelationship(parent="that parent", child="this child", param="some param") rel_repr = repr(rel) expected = '' assert rel_repr == expected def test_rel_repr_long(self): rel = WorkingChildRelationship( parent="that parent who has a long path, still going on. Yes, a very long path indeed.", child="this child", param="some param") rel_repr = repr(rel) expected = '' assert rel_repr == expected qlustered-deepdiff-41c7265/tests/test_operators.py000066400000000000000000000366241516241264500223550ustar00rootroot00000000000000import re import math import pytest from copy import deepcopy from typing import List, Any from deepdiff import DeepDiff from deepdiff.operator import BaseOperator, PrefixOrSuffixOperator, BaseOperatorPlus class TestOperators: def test_custom_operators_prevent_default(self): t1 = { "coordinates": [ {"x": 5, "y": 5}, {"x": 8, "y": 8} ] } t2 = { "coordinates": [ {"x": 6, "y": 6}, {"x": 88, "y": 88} ] } class L2DistanceDifferWithPreventDefault(BaseOperator): def __init__(self, regex_paths: List[str], distance_threshold: float): super().__init__(regex_paths) self.distance_threshold = distance_threshold def _l2_distance(self, c1, c2): return math.sqrt( (c1["x"] - c2["x"]) ** 2 + (c1["y"] - c2["y"]) ** 2 ) def give_up_diffing(self, level, diff_instance) -> bool: l2_distance = self._l2_distance(level.t1, level.t2) if l2_distance > self.distance_threshold: diff_instance.custom_report_result('distance_too_far', level, { "l2_distance": l2_distance }) return True ddiff = DeepDiff(t1, t2, custom_operators=[L2DistanceDifferWithPreventDefault( ["^root\\['coordinates'\\]\\[\\d+\\]$"], 1 )]) expected = { 'distance_too_far': { "root['coordinates'][0]": {'l2_distance': 1.4142135623730951}, "root['coordinates'][1]": {'l2_distance': 113.13708498984761} } } assert expected == ddiff def test_custom_operators_not_prevent_default(self): t1 = { "coordinates": [ {"x": 5, "y": 5}, {"x": 8, "y": 8} ] } t2 = { "coordinates": [ {"x": 6, "y": 6}, {"x": 88, "y": 88} ] } class L2DistanceDifferWithPreventDefault(BaseOperator): def __init__(self, regex_paths, distance_threshold): super().__init__(regex_paths) self.distance_threshold = distance_threshold def _l2_distance(self, c1, c2): return math.sqrt( (c1["x"] - c2["x"]) ** 2 + (c1["y"] - c2["y"]) ** 2 ) def give_up_diffing(self, level, diff_instance) -> bool: l2_distance = self._l2_distance(level.t1, level.t2) if l2_distance > self.distance_threshold: diff_instance.custom_report_result('distance_too_far', level, { "l2_distance": l2_distance }) # return False ddiff = DeepDiff(t1, t2, custom_operators=[L2DistanceDifferWithPreventDefault( ["^root\\['coordinates'\\]\\[\\d+\\]$"], 1 ) ]) expected = { 'values_changed': { "root['coordinates'][0]['x']": {'new_value': 6, 'old_value': 5}, "root['coordinates'][0]['y']": {'new_value': 6, 'old_value': 5}, "root['coordinates'][1]['x']": {'new_value': 88, 'old_value': 8}, "root['coordinates'][1]['y']": {'new_value': 88, 'old_value': 8} }, 'distance_too_far': { "root['coordinates'][0]": {'l2_distance': 1.4142135623730951}, "root['coordinates'][1]": {'l2_distance': 113.13708498984761} } } assert expected == ddiff def test_custom_operators_should_not_equal(self): t1 = { "id": 5, "expect_change_pos": 10, "expect_change_neg": 10, } t2 = { "id": 5, "expect_change_pos": 100, "expect_change_neg": 10, } class ExpectChangeOperator(BaseOperator): def __init__(self, regex_paths): super().__init__(regex_paths) def give_up_diffing(self, level, diff_instance) -> bool: if level.t1 == level.t2: diff_instance.custom_report_result('unexpected:still', level, { "old": level.t1, "new": level.t2 }) return True ddiff = DeepDiff(t1, t2, custom_operators=[ ExpectChangeOperator(regex_paths=["root\\['expect_change.*'\\]"]) ]) assert ddiff == {'unexpected:still': {"root['expect_change_neg']": {'old': 10, 'new': 10}}} def test_custom_operator2(self): class CustomClass: def __init__(self, d: dict, l: list): self.dict = d self.dict['list'] = l def __repr__(self): return "Class list is " + str(self.dict['list']) custom1 = CustomClass(d=dict(a=1, b=2), l=[1, 2, 3]) custom2 = CustomClass(d=dict(c=3, d=4), l=[1, 2, 3, 2]) custom3 = CustomClass(d=dict(a=1, b=2), l=[1, 2, 3, 4]) class ListMatchOperator(BaseOperator): def give_up_diffing(self, level, diff_instance) -> bool: if set(level.t1.dict['list']) == set(level.t2.dict['list']): return True return False ddiff = DeepDiff(custom1, custom2, custom_operators=[ ListMatchOperator(types=[CustomClass]) ]) assert {} == ddiff ddiff2 = DeepDiff(custom2, custom3, threshold_to_diff_deeper=0, custom_operators=[ ListMatchOperator(types=[CustomClass]) ]) expected = { 'dictionary_item_added': ["root.dict['a']", "root.dict['b']"], 'dictionary_item_removed': ["root.dict['c']", "root.dict['d']"], 'values_changed': {"root.dict['list'][3]": {'new_value': 4, 'old_value': 2}}} assert expected == ddiff2 def test_include_only_certain_path(self): class MyOperator: def __init__(self, include_paths): self.include_paths = include_paths def match(self, level) -> bool: return True def give_up_diffing(self, level, diff_instance) -> bool: return level.path() not in self.include_paths t1 = {'a': [10, 11], 'b': [20, 21], 'c': [30, 31]} t2 = {'a': [10, 22], 'b': [20, 33], 'c': [30, 44]} ddiff = DeepDiff(t1, t2, custom_operators=[ MyOperator(include_paths="root['a'][1]") ]) expected = {'values_changed': {"root['a'][1]": {'new_value': 22, 'old_value': 11}}} assert expected == ddiff def test_give_up_diffing_on_first_diff(self): class MyOperator: def match(self, level) -> bool: return True def give_up_diffing(self, level, diff_instance) -> bool: return any(diff_instance.tree.values()) t1 = [[1, 2], [3, 4], [5, 6]] t2 = [[1, 3], [3, 5], [5, 7]] ddiff = DeepDiff(t1, t2, custom_operators=[ MyOperator() ]) expected = {'values_changed': {'root[0][1]': {'new_value': 3, 'old_value': 2}}} assert expected == ddiff def test_prefix_or_suffix_diff(self): t1 = { "key1": ["foo", "bar's food", "jack", "joe"] } t2 = { "key1": ["foo", "bar", "jill", "joe'car"] } ddiff = DeepDiff(t1, t2, custom_operators=[ PrefixOrSuffixOperator() ]) expected = {'values_changed': {"root['key1'][2]": {'new_value': 'jill', 'old_value': 'jack'}}} assert expected == ddiff with pytest.raises(NotImplementedError) as exp: DeepDiff(t1, t2, ignore_order=True, custom_operators=[ PrefixOrSuffixOperator() ]) expected2 = 'PrefixOrSuffixOperator needs to define a normalize_value_for_hashing method to be compatible with ignore_order=True or iterable_compare_func.' assert expected2 == str(exp.value) def test_custom_operator3_small_numbers(self): x = [2.0000000000000027, 2.500000000000005, 2.000000000000002, 3.000000000000001] y = [2.000000000000003, 2.500000000000005, 2.0000000000000027, 3.0000000000000027] result = DeepDiff(x, y) expected = { 'values_changed': { 'root[0]': {'new_value': 2.000000000000003, 'old_value': 2.0000000000000027}, 'root[2]': {'new_value': 2.0000000000000027, 'old_value': 2.000000000000002}, 'root[3]': {'new_value': 3.0000000000000027, 'old_value': 3.000000000000001}}} assert expected == result class CustomCompare(BaseOperatorPlus): def __init__(self, tolerance, types): self.tolerance = tolerance self.types = types def match(self, level) -> bool: if type(level.t1) in self.types: return True return False def give_up_diffing(self, level, diff_instance) -> bool: relative = abs(abs(level.t1 - level.t2) / level.t1) if not max(relative, self.tolerance) == self.tolerance: custom_report = f'relative diff: {relative:.8e}' diff_instance.custom_report_result('diff', level, custom_report) return True def normalize_value_for_hashing(self, parent: Any, obj: Any) -> Any: return obj def compare_func(x, y, level): return True operators = [CustomCompare(types=[float], tolerance=5.5e-5)] result2 = DeepDiff(x, y, custom_operators=operators, iterable_compare_func=compare_func) assert {} == result2 result3 = DeepDiff(x, y, custom_operators=operators, zip_ordered_iterables=True) assert {} == result3, "We should get the same result as result2 when zip_ordered_iterables is True." def test_custom_operator_and_ignore_order1_using_base_operator_plus(self): d1 = { "Name": "SUB_OBJECT_FILES", "Values": { "Value": [ "{f254498b-b752-4f35-bef5-6f1844b61eb7}", "{7fb2a550-1849-45c0-b273-9aa5e4eb9f2b}", "{3a614c62-4252-48eb-b279-1450ee8af182}", "{208f22c4-c256-4311-9a45-e6c37d343458}", "{1fcf5d37-ef19-43a7-a1ad-d17c7c1713c6}", ] } } d2 = { "Name": "SUB_OBJECT_FILES", "Values": { "Value": [ "{e5d18917-1a2c-4abe-b601-8ec002629953}", "{ea71ba1f-1339-4fae-bc28-a9ce9b8a8c67}", "{66bb6192-9cd2-4074-8be1-f2ac52877c70}", "{0c88b900-3755-4d10-93ef-b6a96dbcba90}", "{e39fdfc5-be6c-4f97-9345-9a8286381fe7}" ] } } class RemoveGUIDsOperator(BaseOperatorPlus): _pattern = r"[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}" _substitute = "guid" def match(self, level) -> bool: return isinstance(level.t1, str) and isinstance(level.t2, str) @classmethod def _remove_pattern(cls, t: str): return re.sub(cls._pattern, cls._substitute, t) def give_up_diffing(self, level, diff_instance): t1 = self._remove_pattern(level.t1) t2 = self._remove_pattern(level.t2) return t1 == t2 def normalize_value_for_hashing(self, parent: Any, obj: Any) -> Any: """ Used for ignore_order=True """ if isinstance(obj, str): return self._remove_pattern(obj) return obj operator = RemoveGUIDsOperator() diff1 = DeepDiff(d1, d2, custom_operators=[operator], log_stacktrace=True) assert not diff1 diff2 = DeepDiff(d1, d2, ignore_order=True, custom_operators=[operator], log_stacktrace=True) assert not diff2 def test_custom_operator_and_ignore_order2(self): d1 = { "Entity": { "Property": { "Name": "SUB_OBJECT_FILES", "Values": { "Value": [ "{f254498b-b752-4f35-bef5-6f1844b61eb7}", "{7fb2a550-1849-45c0-b273-9aa5e4eb9f2b}", "{3a614c62-4252-48eb-b279-1450ee8af182}", "{208f22c4-c256-4311-9a45-e6c37d343458}", "{1fcf5d37-ef19-43a7-a1ad-d17c7c1713c6}", "{a9cbecc0-21dc-49ce-8b2c-d36352dae139}" ] } } } } d2 = { "Entity": { "Property": { "Name": "SUB_OBJECT_FILES", "Values": { "Value": [ "{e5d18917-1a2c-4abe-b601-8ec002629953}", "{ea71ba1f-1339-4fae-bc28-a9ce9b8a8c67}", "{d7778018-a7b5-4246-8caa-f590138d99e5}", "{66bb6192-9cd2-4074-8be1-f2ac52877c70}", "{0c88b900-3755-4d10-93ef-b6a96dbcba90}", "{e39fdfc5-be6c-4f97-9345-9a8286381fe7}" ] } } } } class RemovePatternOperator(BaseOperator): _pattern: str = "" _substitute: str = "" @classmethod def _remove_pattern(cls, t: str): return re.sub(cls._pattern, cls._substitute, t) def give_up_diffing(self, level, diff_instance): if isinstance(level.t1, str) and isinstance(level.t2, str): t1 = self._remove_pattern(level.t1) t2 = self._remove_pattern(level.t2) return t1 == t2 return False class RemoveGUIDsOperator(RemovePatternOperator): _pattern = r"[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}" _substitute = "guid" diff1 = DeepDiff(deepcopy(d1), deepcopy(d2), ignore_order=False, custom_operators=[RemoveGUIDsOperator(types=[str])]) assert not diff1 with pytest.raises(NotImplementedError) as exp: DeepDiff(deepcopy(d1), deepcopy(d2), ignore_order=True, custom_operators=[RemoveGUIDsOperator(types=[str])]) expected2 = 'RemoveGUIDsOperator needs to define a normalize_value_for_hashing method to be compatible with ignore_order=True or iterable_compare_func.' assert expected2 == str(exp.value) # --------- Let's implement the normalize_value_for_hashing to make it work with ignore_order=True --------- class RemoveGUIDsOperatorIgnoreOrderReady(RemoveGUIDsOperator): def normalize_value_for_hashing(self, parent: Any, obj: Any) -> Any: if isinstance(obj, str): return self._remove_pattern(obj) return obj diff3 = DeepDiff(deepcopy(d1), deepcopy(d2), ignore_order=True, custom_operators=[RemoveGUIDsOperatorIgnoreOrderReady(types=[str])]) assert not diff3, "We shouldn't have a diff because we have normalized the string values to be all the same vlues." qlustered-deepdiff-41c7265/tests/test_path.py000066400000000000000000000046471516241264500212730ustar00rootroot00000000000000import pytest from deepdiff.path import _path_to_elements, GET, GETATTR, extract, parse_path, stringify_path, _add_to_elements @pytest.mark.parametrize('test_num, path, expected', [ (1, "root[4]['b'][3]", [(4, GET), ('b', GET), (3, GET)]), (2, "root[4].b[3]", [(4, GET), ('b', GETATTR), (3, GET)]), (3, "root[4].b['a3']", [(4, GET), ('b', GETATTR), ('a3', GET)]), (4, "root[4.3].b['a3']", [(4.3, GET), ('b', GETATTR), ('a3', GET)]), (5, "root.a.b", [('a', GETATTR), ('b', GETATTR)]), (6, "root.hello", [('hello', GETATTR)]), (7, "root['h']", [('h', GET)]), (8, "root['a\rb']", [('a\rb', GET)]), (9, "root['a\\rb']", [('a\\rb', GET)]), (10, "root", []), (11, ((4, GET), ('b', GET)), ((4, GET), ('b', GET))), ]) def test_path_to_elements(test_num, path, expected): result = _path_to_elements(path, root_element=None) assert tuple(expected) == result, f"test_path_to_elements #{test_num} failed" if isinstance(path, str): path_again = stringify_path(path=result) assert path == path_again, f"test_path_to_elements #{test_num} failed" @pytest.mark.parametrize('obj, path, expected', [ ({1: [2, 3], 2: [4, 5]}, "root[2][1]", 5), ({1: [{'2': 'b'}, 3], 2: {4, 5}}, "root[1][0]['2']", 'b' ), ({'test [a]': 'b'}, "root['test [a]']", 'b' ), ({"a']['b']['c": 1}, """root["a']['b']['c"]""", 1 ), ]) def test_get_item(obj, path, expected): result = extract(obj, path) assert expected == result def test_parse_path(): result = parse_path("root[1][2]['age']") assert [1, 2, 'age'] == result result2 = parse_path("root[1][2]['age']", include_actions=True) assert [{'element': 1, 'action': 'GET'}, {'element': 2, 'action': 'GET'}, {'element': 'age', 'action': 'GET'}] == result2 result3 = parse_path("root['joe'].age") assert ['joe', 'age'] == result3 result4 = parse_path("root['joe'].age", include_actions=True) assert [{'element': 'joe', 'action': 'GET'}, {'element': 'age', 'action': 'GETATTR'}] == result4 @pytest.mark.parametrize('test_num, elem, inside, expected', [ ( 1, "'hello'", None, [('hello', GET)], ), ( 2, "'a\rb'", None, [('a\rb', GET)], ), ]) def test__add_to_elements(test_num, elem, inside, expected): elements = [] _add_to_elements(elements, elem, inside) assert expected == elements qlustered-deepdiff-41c7265/tests/test_search.py000066400000000000000000000435251516241264500216020ustar00rootroot00000000000000#!/usr/bin/env python import pytest import ipaddress import logging from typing import Union from deepdiff import DeepSearch, grep from datetime import datetime logging.disable(logging.CRITICAL) item = "somewhere" class CustomClass: def __init__(self, a, b=None): self.a = a self.b = b def __str__(self): return "({}, {})".format(self.a, self.b) def __repr__(self): return self.__str__() class ClassWithIp: """Class containing single data member to demonstrate deepdiff infinite iterate over IPv6Interface""" def __init__(self, addr: str): self.field: Union[ ipaddress.IPv4Network, ipaddress.IPv6Network, ipaddress.IPv4Interface, ipaddress.IPv6Interface, ] = ipaddress.IPv6Network(addr) class TestDeepSearch: """DeepSearch Tests.""" def test_number_in_list(self): obj = ["a", 10, 20] item = 10 result = {"matched_values": {'root[1]'}} assert DeepSearch(obj, item, verbose_level=1) == result def test_number_in_list2(self): obj = ["a", "10", 10, 20] item = 10 result = {"matched_values": {'root[2]'}} assert DeepSearch(obj, item, verbose_level=1) == result def test_number_in_list3(self): obj = ["a", "10", 10, 20] item = "10" result = {"matched_values": {'root[1]'}} assert DeepSearch(obj, item, verbose_level=1) == result def test_number_in_list_strict_false(self): obj = ["a", "10", 10, 20] item = "20" result = {"matched_values": {'root[3]'}} assert DeepSearch(obj, item, verbose_level=1, strict_checking=False) == result def test_string_in_root(self): obj = "long string somewhere" result = {"matched_values": {'root'}} assert DeepSearch(obj, item, verbose_level=1) == result def test_string_in_root_verbose(self): obj = "long string somewhere" result = {"matched_values": {'root': "long string somewhere"}} assert DeepSearch(obj, item, verbose_level=2) == result def test_string_in_tuple(self): obj = ("long", "string", 0, "somewhere") result = {"matched_values": {'root[3]'}} assert DeepSearch(obj, item, verbose_level=1) == result def test_string_in_list(self): obj = ["long", "string", 0, "somewhere"] result = {"matched_values": {'root[3]'}} assert DeepSearch(obj, item, verbose_level=1) == result def test_string_in_list_verbose(self): obj = ["long", "string", 0, "somewhere"] result = {"matched_values": {'root[3]': "somewhere"}} assert DeepSearch(obj, item, verbose_level=2) == result def test_string_in_list_verbose2(self): obj = ["long", "string", 0, "somewhere great!"] result = {"matched_values": {'root[3]': "somewhere great!"}} assert DeepSearch(obj, item, verbose_level=2) == result def test_string_in_list_verbose3(self): obj = ["long somewhere", "string", 0, "somewhere great!"] result = { "matched_values": { 'root[0]': 'long somewhere', 'root[3]': "somewhere great!" } } assert DeepSearch(obj, item, verbose_level=2) == result def test_int_in_dictionary(self): obj = {"long": "somewhere", "num": 2, 0: 0, "somewhere": "around"} item = 2 result = {'matched_values': {"root['num']"}} ds = DeepSearch(obj, item, verbose_level=1) assert ds == result def test_string_in_dictionary(self): obj = {"long": "somewhere", "string": 2, 0: 0, "somewhere": "around"} result = { 'matched_paths': {"root['somewhere']"}, 'matched_values': {"root['long']"} } ds = DeepSearch(obj, item, verbose_level=1) assert ds == result def test_string_in_dictionary_case_insensitive(self): obj = {"long": "Somewhere over there!", "string": 2, 0: 0, "SOMEWHERE": "around"} result = { 'matched_paths': {"root['SOMEWHERE']"}, 'matched_values': {"root['long']"} } ds = DeepSearch(obj, item, verbose_level=1, case_sensitive=False) assert ds == result def test_string_in_dictionary_key_case_insensitive_partial(self): obj = {"SOMEWHERE here": "around"} result = { 'matched_paths': {"root['SOMEWHERE here']"} } ds = DeepSearch(obj, item, verbose_level=1, case_sensitive=False) assert ds == result def test_string_in_dictionary_verbose(self): obj = {"long": "somewhere", "string": 2, 0: 0, "somewhere": "around"} result = { 'matched_paths': { "root['somewhere']": "around" }, 'matched_values': { "root['long']": "somewhere" } } ds = DeepSearch(obj, item, verbose_level=2) assert ds == result def test_string_in_dictionary_in_list_verbose(self): obj = [ "something somewhere", { "long": "somewhere", "string": 2, 0: 0, "somewhere": "around" } ] result = { 'matched_paths': { "root[1]['somewhere']": "around" }, 'matched_values': { "root[1]['long']": "somewhere", "root[0]": "something somewhere" } } ds = DeepSearch(obj, item, verbose_level=2) assert ds == result def test_custom_object(self): obj = CustomClass('here, something', 'somewhere') result = {'matched_values': {'root.b'}} ds = DeepSearch(obj, item, verbose_level=1) assert ds == result def test_custom_object_verbose(self): obj = CustomClass('here, something', 'somewhere out there') result = {'matched_values': {'root.b': 'somewhere out there'}} ds = DeepSearch(obj, item, verbose_level=2) assert ds == result def test_custom_object_in_dictionary_verbose(self): obj = {1: CustomClass('here, something', 'somewhere out there')} result = {'matched_values': {'root[1].b': 'somewhere out there'}} ds = DeepSearch(obj, item, verbose_level=2) assert ds == result def test_named_tuples_verbose(self): from collections import namedtuple Point = namedtuple('Point', ['x', 'somewhere_good']) obj = Point(x="my keys are somewhere", somewhere_good=22) ds = DeepSearch(obj, item, verbose_level=2) result = { 'matched_values': { 'root.x': 'my keys are somewhere' }, 'matched_paths': { 'root.somewhere_good': 22 } } assert ds == result def test_string_in_set_verbose(self): obj = {"long", "string", 0, "somewhere"} # result = {"matched_values": {'root[3]': "somewhere"}} ds = DeepSearch(obj, item, verbose_level=2) assert list(ds["matched_values"].values())[0] == item def test_loop(self): class LoopTest: def __init__(self, a): self.loop = self self.a = a obj = LoopTest("somewhere around here.") ds = DeepSearch(obj, item, verbose_level=1) result = {'matched_values': {'root.a'}} assert ds == result def test_loop_in_lists(self): obj = [1, 2, 'somewhere'] obj.append(obj) ds = DeepSearch(obj, item, verbose_level=1) result = {'matched_values': {'root[2]'}} assert ds == result def test_skip_path1(self): obj = { "for life": "vegan", "ingredients": ["no meat", "no eggs", "no dairy", "somewhere"] } ds = DeepSearch(obj, item, exclude_paths={"root['ingredients']"}) assert ds == {} def test_custom_object_skip_path(self): obj = CustomClass('here, something', 'somewhere') result = {} ds = DeepSearch(obj, item, verbose_level=1, exclude_paths=['root.b']) assert ds == result def test_skip_list_path(self): obj = ['a', 'somewhere'] ds = DeepSearch(obj, item, exclude_paths=['root[1]']) result = {} assert ds == result def test_skip_dictionary_path(self): obj = {1: {2: "somewhere"}} ds = DeepSearch(obj, item, exclude_paths=['root[1][2]']) result = {} assert ds == result def test_skip_type_str(self): obj = "long string somewhere" result = {} ds = DeepSearch(obj, item, verbose_level=1, exclude_types=[str]) assert ds == result def test_skip_regexp(self): obj = [{'a': 1, 'b': "somewhere"}, {'c': 4, 'b': "somewhere"}] ds = DeepSearch(obj, item, exclude_regex_paths=[r"root\[\d+\]"]) result = {} assert ds == result def test_skip_regexp2(self): obj = {'a': [1, 2, [3, [item]]]} ds = DeepSearch(obj, item, exclude_regex_paths=[r"\[\d+\]"]) result = {} assert ds == result def test_unknown_parameters(self): with pytest.raises(ValueError): DeepSearch(1, 1, wrong_param=2) def test_bad_attribute(self): class Bad: __slots__ = ['x', 'y'] def __getattr__(self, key): raise AttributeError("Bad item") def __str__(self): return "Bad Object" obj = Bad() ds = DeepSearch(obj, item, verbose_level=1) result = {'unprocessed': ['root']} assert ds == result ds = DeepSearch(obj, item, verbose_level=2) assert ds == result def test_case_insensitive_of_str_in_list(self): obj = ["a", "bb", "BBC", "aBbB"] item = "BB" result = {"matched_values": {'root[1]', 'root[2]', 'root[3]'}} assert DeepSearch(obj, item, verbose_level=1, case_sensitive=False) == result def test_case_sensitive_of_str_in_list(self): obj = ["a", "bb", "BBC", "aBbB"] item = "BB" result = {"matched_values": {'root[2]'}} assert DeepSearch(obj, item, verbose_level=1, case_sensitive=True) == result def test_case_sensitive_of_str_in_one_liner(self): obj = "Hello, what's up?" item = "WHAT" result = {} assert DeepSearch(obj, item, verbose_level=1, case_sensitive=True) == result def test_case_insensitive_of_str_in_one_liner(self): obj = "Hello, what's up?" item = "WHAT" result = {'matched_values': {'root'}} assert DeepSearch(obj, item, verbose_level=1, case_sensitive=False) == result def test_none(self): obj = item = None result = {'matched_values': {'root'}} assert DeepSearch(obj, item, verbose_level=1) == result def test_complex_obj(self): obj = datetime(2017, 5, 4, 1, 1, 1) item = datetime(2017, 5, 4, 1, 1, 1) result = {'matched_values': {'root'}} assert DeepSearch(obj, item, verbose_level=1) == result def test_keep_searching_after_obj_match(self): class AlwaysEqual: def __init__(self, recurse=True): if recurse: self.some_attr = AlwaysEqual(recurse=False) def __eq__(self, other): return True obj = AlwaysEqual() item = AlwaysEqual() result = {'matched_values': {'root', 'root.some_attr'}} assert DeepSearch(obj, item, verbose_level=1) == result def test_search_inherited_attributes(self): class Parent: a = 1 class Child(Parent): b = 2 obj = Child() item = 1 result = {'matched_values': {'root.a'}} assert DeepSearch(obj, item, verbose_level=1) == result def test_dont_use_regex_by_default(self): obj = "long string somewhere" item = "some.*" result = {} assert DeepSearch(obj, item, verbose_level=1) == result def test_regex_in_string(self): obj = "long string somewhere" item = "some.*" result = {"matched_values": {"root"}} assert DeepSearch(obj, item, verbose_level=1, use_regexp=True) == result def test_regex_does_not_match_the_regex_string_itself(self): obj = ["We like python", "but not (?:p|t)ython"] item = "(?:p|t)ython" result = {'matched_values': ['root[0]']} assert DeepSearch(obj, item, verbose_level=1, use_regexp=True) == result def test_regex_in_string_in_tuple(self): obj = ("long", "string", 0, "somewhere") item = "some.*" result = {"matched_values": {"root[3]"}} assert DeepSearch(obj, item, verbose_level=1, use_regexp=True) == result def test_regex_in_string_in_list(self): obj = ["long", "string", 0, "somewhere"] item = "some.*" result = {"matched_values": {"root[3]"}} assert DeepSearch(obj, item, verbose_level=1, use_regexp=True) == result def test_regex_in_string_in_dictionary(self): obj = {"long": "somewhere", "string": 2, 0: 0, "somewhere": "around"} result = { "matched_paths": {"root['somewhere']"}, "matched_values": {"root['long']"}, } item = "some.*" ds = DeepSearch(obj, item, verbose_level=1, use_regexp=True) assert ds == result def test_regex_in_string_in_dictionary_in_list_verbose(self): obj = [ "something somewhere", {"long": "somewhere", "string": 2, 0: 0, "somewhere": "around"}, ] result = { "matched_paths": {"root[1]['somewhere']": "around"}, "matched_values": { "root[1]['long']": "somewhere", "root[0]": "something somewhere", }, } item = "some.*" ds = DeepSearch(obj, item, verbose_level=2, use_regexp=True) assert ds == result def test_regex_in_custom_object(self): obj = CustomClass("here, something", "somewhere") result = {"matched_values": {"root.b"}} item = "somew.*" ds = DeepSearch(obj, item, verbose_level=1, use_regexp=True) assert ds == result def test_regex_in_custom_object_in_dictionary_verbose(self): obj = {1: CustomClass("here, something", "somewhere out there")} result = {"matched_values": {"root[1].b": "somewhere out there"}} item = "somew.*" ds = DeepSearch(obj, item, verbose_level=2, use_regexp=True) assert ds == result def test_regex_in_named_tuples_verbose(self): from collections import namedtuple Point = namedtuple("Point", ["x", "somewhere_good"]) obj = Point(x="my keys are somewhere", somewhere_good=22) item = "some.*" ds = DeepSearch(obj, item, verbose_level=2, use_regexp=True) result = { "matched_values": {"root.x": "my keys are somewhere"}, "matched_paths": {"root.somewhere_good": 22}, } assert ds == result def test_regex_in_string_in_set_verbose(self): obj = {"long", "string", 0, "somewhere"} item = "some.*" ds = DeepSearch(obj, item, verbose_level=2, use_regexp=True) assert list(ds["matched_values"].values())[0] == "somewhere" def test_regex_in_int_in_dictionary_with_strict_checking(self): obj = {"long": "somewhere", "num": 232, 0: 0, "somewhere": "around"} item = "2.*" result = {} ds = DeepSearch(obj, item, verbose_level=1, use_regexp=True) assert ds == result def test_regex_in_int_in_dictionary(self): obj = {"long": "somewhere", "num": 232, 0: 0, "somewhere": "around"} item = "2.*" result = {"matched_values": {"root['num']"}} ds = DeepSearch(obj, item, verbose_level=1, use_regexp=True, strict_checking=False) assert ds == result def test_regex_in_int_in_dictionary_returns_partial_match(self): obj = {"long": "somewhere", "num": 1123456, 0: 0, "somewhere": "around"} item = "1234" result = {"matched_values": {"root['num']"}} ds = DeepSearch(obj, item, verbose_level=1, use_regexp=True, strict_checking=False) assert ds == result def test_int_cant_become_regex(self): obj = {"long": "somewhere", "num": "1123456", 0: 0, "somewhere": "around"} item = CustomClass(a=10) with pytest.raises(TypeError) as exp: DeepSearch(obj, item, verbose_level=1, use_regexp=True, strict_checking=False) assert str(exp.value).startswith("The passed item of (10, None) is not usable for regex") def test_searching_for_int_in_dictionary_when_strict_false(self): obj = {"long": "somewhere", "num": "1234", 0: 0, "somewhere": "around"} item = 1234 result = {"matched_values": {"root['num']"}} ds = DeepSearch(obj, item, verbose_level=1, strict_checking=False) assert ds == result class TestGrep: def test_grep_dict(self): obj = { "for life": "vegan", "ingredients": ["no meat", "no eggs", "no dairy", "somewhere"] } ds = obj | grep(item) assert ds == {'matched_values': {"root['ingredients'][3]"}} def test_grep_dict_in_dict(self): obj = { "x": { "y": [ "aaaaaa\u0142 bbbbb" ] }, "z": "z", } item = {"z": "z"} result = obj | grep(item) assert {} == result def test_grep_with_non_utf8_chars(self): obj = "aaaaaa\u0142 bbbbb" item = {"z": "z"} result = obj | grep(item) assert {} == result def test_grep_regex_in_string_in_tuple(self): obj = ("long", "string", 0, "somewhere") item = "some.*" result = {"matched_values": {"root[3]"}} assert obj | grep(item, verbose_level=1, use_regexp=True) == result def test_search_ip_addresses(self): obj1 = [ClassWithIp("2002:db8::/30"), ClassWithIp("2002:db8::/32")] assert obj1 | grep("2002:db8::/32") == {'matched_values': ['root[1].field']} qlustered-deepdiff-41c7265/tests/test_security.py000066400000000000000000000104001516241264500221660ustar00rootroot00000000000000import os import pickle import pytest from deepdiff import Delta from deepdiff.helper import Opcode from deepdiff.serialization import ForbiddenModule class TestDeltaClassPollution: def test_builtins_int(self): pollute_int = pickle.dumps( { "values_changed": {"root['tmp']": {"new_value": Opcode("", 0, 0, 0, 0)}}, "dictionary_item_added": { ( ("root", "GETATTR"), ("tmp", "GET"), ("__repr__", "GETATTR"), ("__globals__", "GETATTR"), ("__builtins__", "GET"), ("int", "GET"), ): "no longer a class" }, } ) assert isinstance(pollute_int, bytes) # ------------[ Exploit ]------------ # This could be some example, vulnerable, application. # The inputs above could be sent via HTTP, for example. # Existing dictionary; it is assumed that it contains # at least one entry, otherwise a different Delta needs to be # applied first, adding an entry to the dictionary. mydict = {"tmp": "foobar"} # Before pollution assert 42 == int("41") + 1 # Apply Delta to mydict result = mydict + Delta(pollute_int) assert 1337 == int("1337") def test_remote_code_execution(self): if os.path.exists('/tmp/pwned'): os.remove('/tmp/pwned') pollute_safe_to_import = pickle.dumps( { "values_changed": {"root['tmp']": {"new_value": Opcode("", 0, 0, 0, 0)}}, "set_item_added": { ( ("root", "GETATTR"), ("tmp", "GET"), ("__repr__", "GETATTR"), ("__globals__", "GETATTR"), ("sys", "GET"), ("modules", "GETATTR"), ("deepdiff.serialization", "GET"), ("SAFE_TO_IMPORT", "GETATTR"), ): set(["posix.system"]) }, } ) # From https://davidhamann.de/2020/04/05/exploiting-python-pickle/ class RCE: def __reduce__(self): cmd = "id > /tmp/pwned" return os.system, (cmd,) # Wrap object with dictionary so that Delta does not crash rce_pickle = pickle.dumps({"_": RCE()}) assert isinstance(pollute_safe_to_import, bytes) assert isinstance(rce_pickle, bytes) # ------------[ Exploit ]------------ # This could be some example, vulnerable, application. # The inputs above could be sent via HTTP, for example. # Existing dictionary; it is assumed that it contains # at least one entry, otherwise a different Delta needs to be # applied first, adding an entry to the dictionary. mydict = {"tmp": "foobar"} # Apply Delta to mydict with pytest.raises(ValueError) as exc_info: mydict + Delta(pollute_safe_to_import) assert "traversing dunder attributes is not allowed" == str(exc_info.value) with pytest.raises(ForbiddenModule) as exc_info: Delta(rce_pickle) # no need to apply this Delta assert "Module 'posix.system' is forbidden. You need to explicitly pass it by passing a safe_to_import parameter" == str(exc_info.value) assert not os.path.exists('/tmp/pwned'), "We should not have created this file" def test_delta_should_not_access_globals(self): pollute_global = pickle.dumps( { "dictionary_item_added": { ( ("root", "GETATTR"), ("myfunc", "GETATTR"), ("__globals__", "GETATTR"), ("PWNED", "GET"), ): 1337 } } ) # demo application class Foo: def __init__(self): pass def myfunc(self): pass PWNED = False delta = Delta(pollute_global) assert PWNED is False b = Foo() + delta assert PWNED is False qlustered-deepdiff-41c7265/tests/test_serialization.py000066400000000000000000001046011516241264500232030ustar00rootroot00000000000000#!/usr/bin/env python from ipaddress import IPv4Address import os import json import sys import pytest import datetime import numpy as np import hashlib import base64 from typing import NamedTuple, Optional, Union, List, Dict from pickle import UnpicklingError from decimal import Decimal from collections import Counter from pydantic import BaseModel, IPvAnyAddress from deepdiff import DeepDiff from deepdiff.helper import pypy3, py_current_version, np_ndarray, Opcode, SetOrdered from deepdiff.serialization import ( pickle_load, pickle_dump, ForbiddenModule, ModuleNotFoundError, MODULE_NOT_FOUND_MSG, FORBIDDEN_MODULE_MSG, pretty_print_diff, load_path_content, UnsupportedFormatErr, json_dumps, json_loads) from conftest import FIXTURES_DIR from tests import PicklableClass import logging logging.disable(logging.CRITICAL) t1 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": [1, 2, 3]}} t2 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": "world\n\n\nEnd"}} class SampleSchema(BaseModel): works: bool = False ips: List[IPvAnyAddress] class SomeStats(NamedTuple): counter: Optional[Counter] context_aware_counter: Optional[Counter] = None min_int: Optional[int] = 0 max_int: Optional[int] = 0 field_stats1 = SomeStats( counter=Counter(["a", "a", "b"]), max_int=10 ) class TestSerialization: """Tests for Serializations.""" def test_serialization_text(self): ddiff = DeepDiff(t1, t2) assert "builtins.list" in ddiff.to_json_pickle() jsoned = ddiff.to_json() assert "world" in jsoned def test_serialization_text_force_builtin_json(self): ddiff = DeepDiff(t1, t2) with pytest.raises(TypeError) as excinfo: jsoned = ddiff.to_json(sort_keys=True) assert str(excinfo.value).startswith("orjson does not accept the sort_keys parameter.") jsoned = ddiff.to_json(sort_keys=True, force_use_builtin_json=True) assert "world" in jsoned def test_deserialization(self): ddiff = DeepDiff(t1, t2) jsoned = ddiff.to_json_pickle() ddiff2 = DeepDiff.from_json_pickle(jsoned) assert ddiff == ddiff2 def test_serialization_tree(self): ddiff = DeepDiff(t1, t2, view='tree') pickle_jsoned = ddiff.to_json_pickle() assert "world" in pickle_jsoned jsoned = ddiff.to_json() assert "world" in jsoned def test_deserialization_tree(self): ddiff = DeepDiff(t1, t2, view='tree') jsoned = ddiff.to_json_pickle() ddiff2 = DeepDiff.from_json_pickle(jsoned) assert 'type_changes' in ddiff2 def test_serialize_custom_objects_throws_error(self): class A: pass class B: pass t1 = A() t2 = B() ddiff = DeepDiff(t1, t2) assert r'{"type_changes":{"root":{"old_type":"A","new_type":"B","old_value":{},"new_value":{}}}}' == ddiff.to_json() def test_serialize_custom_objects_with_default_mapping(self): class A: pass class B: pass t1 = A() t2 = B() ddiff = DeepDiff(t1, t2) default_mapping = {A: lambda x: 'obj A', B: lambda x: 'obj B'} result = ddiff.to_json(default_mapping=default_mapping) expected_result = {"type_changes": {"root": {"old_type": "A", "new_type": "B", "old_value": "obj A", "new_value": "obj B"}}} assert expected_result == json.loads(result) # These lines are long but make it easier to notice the difference: @pytest.mark.parametrize('verbose_level, expected', [ (0, {"type_changes": {"root[0]": {"old_type": str, "new_type": int}}, "dictionary_item_added": ["root[1][5]"], "dictionary_item_removed": ["root[1][3]"], "iterable_item_added": {"root[2]": "d"}}), (1, {"type_changes": {"root[0]": {"old_type": str, "new_type": int, "old_value": "a", "new_value": 10}}, "dictionary_item_added": ["root[1][5]"], "dictionary_item_removed": ["root[1][3]"], "values_changed": {"root[1][1]": {"new_value": 2, "old_value": 1}}, "iterable_item_added": {"root[2]": "d"}}), (2, {"type_changes": {"root[0]": {"old_type": str, "new_type": int, "old_value": "a", "new_value": 10}}, "dictionary_item_added": {"root[1][5]": 6}, "dictionary_item_removed": {"root[1][3]": 4}, "values_changed": {"root[1][1]": {"new_value": 2, "old_value": 1}}, "iterable_item_added": {"root[2]": "d"}}), ]) def test_to_dict_at_different_verbose_level(self, verbose_level, expected): t1 = ['a', {1: 1, 3: 4}, ] t2 = [10, {1: 2, 5: 6}, 'd'] ddiff = DeepDiff(t1, t2, verbose_level=verbose_level) assert expected == ddiff.to_dict() def test_to_dict_tree_view_defaults_to_verbose_level_2(self): t1 = ['a', {1: 1, 3: 4}] t2 = [10, {1: 2, 5: 6}, 'd'] ddiff = DeepDiff(t1, t2, view='tree') result = ddiff.to_dict() # tree view defaults to verbose_level=2: added/removed are dicts, not sets expected = { "type_changes": {"root[0]": {"old_type": str, "new_type": int, "old_value": "a", "new_value": 10}}, "dictionary_item_added": {"root[1][5]": 6}, "dictionary_item_removed": {"root[1][3]": 4}, "values_changed": {"root[1][1]": {"new_value": 2, "old_value": 1}}, "iterable_item_added": {"root[2]": "d"}, } assert expected == result @pytest.mark.parametrize('verbose_level, expected', [ (0, {"type_changes": {"root[0]": {"old_type": str, "new_type": int}}, "dictionary_item_added": ["root[1][5]"], "dictionary_item_removed": ["root[1][3]"], "iterable_item_added": {"root[2]": "d"}}), (1, {"type_changes": {"root[0]": {"old_type": str, "new_type": int, "old_value": "a", "new_value": 10}}, "dictionary_item_added": ["root[1][5]"], "dictionary_item_removed": ["root[1][3]"], "values_changed": {"root[1][1]": {"new_value": 2, "old_value": 1}}, "iterable_item_added": {"root[2]": "d"}}), ]) def test_to_dict_tree_view_with_verbose_level_override(self, verbose_level, expected): t1 = ['a', {1: 1, 3: 4}] t2 = [10, {1: 2, 5: 6}, 'd'] ddiff = DeepDiff(t1, t2, view='tree') result = ddiff.to_dict(verbose_level=verbose_level) assert expected == result def test_to_dict_text_view_preserves_original_verbose_level(self): t1 = ['a', {1: 1, 3: 4}] t2 = [10, {1: 2, 5: 6}, 'd'] # verbose_level=2 at init, text view ddiff = DeepDiff(t1, t2, verbose_level=2) result = ddiff.to_dict() # Should preserve verbose_level=2 from init assert isinstance(result.get("dictionary_item_added"), dict) assert result["dictionary_item_added"] == {"root[1][5]": 6} def test_to_dict_text_view_with_verbose_level_override(self): t1 = ['a', {1: 1, 3: 4}] t2 = [10, {1: 2, 5: 6}, 'd'] # verbose_level=2 at init, but override to 1 in to_dict ddiff = DeepDiff(t1, t2, verbose_level=2) result = ddiff.to_dict(verbose_level=1) # verbose_level=1: dictionary_item_added is not a dict (it's a SetOrdered) assert not isinstance(result.get("dictionary_item_added"), dict) def test_to_dict_invalid_verbose_level(self): t1 = [1] t2 = [2] ddiff = DeepDiff(t1, t2) with pytest.raises(ValueError): ddiff.to_dict(verbose_level=3) def test_to_json_with_verbose_level(self): t1 = ['a', {1: 1, 3: 4}] t2 = [10, {1: 2, 5: 6}, 'd'] ddiff = DeepDiff(t1, t2, view='tree') result = json.loads(ddiff.to_json()) # tree view defaults to verbose_level=2 in to_json too assert "root[1][5]" in result.get("dictionary_item_added", {}) def test_to_json_with_verbose_level_override(self): t1 = ['a', {1: 1, 3: 4}] t2 = [10, {1: 2, 5: 6}, 'd'] ddiff = DeepDiff(t1, t2, view='tree') result = json.loads(ddiff.to_json(verbose_level=1)) # verbose_level=1: dictionary_item_added is a list assert isinstance(result.get("dictionary_item_added"), list) def test_serialize_pydantic_model(self): obj = SampleSchema( works=True, ips=["128.0.0.1"] ) serialized = json_dumps(obj) obj_again = json_loads(serialized) assert {'works': True, 'ips': ['128.0.0.1']} == obj_again assert {'works': True, 'ips': [IPv4Address('128.0.0.1')]} == obj.model_dump() @pytest.mark.skipif(pypy3, reason='clevercsv is not supported in pypy3') class TestLoadContet: @pytest.mark.parametrize('path1, validate', [ ('t1.json', lambda x: x[0]['key1'] == 'value1'), ('t1.yaml', lambda x: x[0][0] == 'name'), ('t1.toml', lambda x: x['servers']['alpha']['ip'] == '10.0.0.1'), ('t1.csv', lambda x: x[0]['last_name'] == 'Nobody'), ('t1.pickle', lambda x: x[1] == 1), ]) def test_load_path_content(self, path1, validate): path = os.path.join(FIXTURES_DIR, path1) result = load_path_content(path) assert validate(result) def test_load_path_content_when_unsupported_format(self): path = os.path.join(FIXTURES_DIR, 't1.unsupported') with pytest.raises(UnsupportedFormatErr): load_path_content(path) class TestPicklingSecurity: @pytest.mark.skipif(sys.platform == "win32", reason="Resource module is Unix-only") def test_restricted_unpickler_memory_exhaustion_cve(self): """CVE-2026-33155: Prevent DoS via massive allocation through REDUCE opcode. The payload calls bytes(10_000_000_000) which is allowed by find_class but would allocate ~9.3GB of memory. The fix should reject this before the allocation happens. """ import resource # 1. Cap memory to 500MB to prevent system freezes during the test soft, hard = resource.getrlimit(resource.RLIMIT_AS) maxsize_bytes = 500 * 1024 * 1024 resource.setrlimit(resource.RLIMIT_AS, (maxsize_bytes, hard)) try: # 2. Malicious payload: attempts to allocate ~9.3GB via bytes(10000000000) # This uses allowed builtins but passes a massive integer via REDUCE payload = ( b"(dp0\n" b"S'_'\n" b"cbuiltins\nbytes\n" b"(I10000000000\n" b"tR" b"s." ) # 3. After the patch, deepdiff should catch the size violation # and raise UnpicklingError before attempting allocation. with pytest.raises((ValueError, UnpicklingError)): pickle_load(payload) finally: # Restore original memory limit so other tests are not affected resource.setrlimit(resource.RLIMIT_AS, (soft, hard)) def test_restricted_unpickler_allows_small_bytes(self): """Ensure legitimate small bytes objects can still be deserialized.""" # Payload: {'_': bytes(100)} — well within the 128MB limit payload = ( b"(dp0\n" b"S'_'\n" b"cbuiltins\nbytes\n" b"(I100\n" b"tR" b"s." ) result = pickle_load(payload) assert result == {'_': bytes(100)} class TestPickling: def test_serialize(self): obj = [1, 2, 3, None, {10: 11E2}, frozenset(['a', 'c']), SetOrdered([2, 1]), datetime.datetime(2022, 4, 10, 0, 40, 41, 357857), datetime.time(11), Decimal('11.2'), 123.11] serialized = pickle_dump(obj) loaded = pickle_load(serialized) assert obj == loaded @pytest.mark.skipif(pypy3, reason='short pickle not supported in pypy3') def test_pickle_that_is_string(self): serialized_str = 'DeepDiff Delta Payload v0-0-1\nBlah' with pytest.raises(UnpicklingError): pickle_load(serialized_str) def test_custom_object_deserialization_fails_without_explicit_permission(self): obj = PicklableClass(10) module_dot_name = 'tests.{}'.format(PicklableClass.__name__) serialized = pickle_dump(obj) expected_msg = FORBIDDEN_MODULE_MSG.format(module_dot_name) with pytest.raises(ForbiddenModule) as excinfo: pickle_load(serialized) assert expected_msg == str(excinfo.value) # Explicitly allowing the module to be loaded loaded = pickle_load(serialized, safe_to_import={module_dot_name}) assert obj == loaded # Explicitly allowing the module to be loaded. It can take a list instead of a set. loaded2 = pickle_load(serialized, safe_to_import=[module_dot_name]) assert obj == loaded2 def test_unpickling_object_that_is_not_imported_raises_error(self): def get_the_pickle(): import wave obj = wave.Error return pickle_dump(obj) serialized = get_the_pickle() # Making sure that the module is unloaded. del sys.modules['wave'] module_dot_name = 'wave.Error' expected_msg = MODULE_NOT_FOUND_MSG.format(module_dot_name) with pytest.raises(ModuleNotFoundError) as excinfo: pickle_load(serialized, safe_to_import=module_dot_name) assert expected_msg == str(excinfo.value) def test_seriaize_property(self): class Sample: @property def something(self): return 10 sample = Sample() serialized = json_dumps(sample) assert '{"something":10}' == serialized class TestDeepDiffPretty: """Tests for pretty() method of DeepDiff""" class TestingClass: one = 1 testing_class = TestingClass @pytest.mark.parametrize('t1, t2, item_path, old_type, new_type, old_val_displayed, new_val_displayed', [ [{2: 2, 4: 4}, {2: 'b', 4: 4}, 'root[2]', 'int', 'str', '2', '"b"'], [[1, 2, 3], [1, '2', 3], 'root[1]', 'int', 'str', '2', '"2"'], [[1, 2, 3], {1, 2, 3}, 'root', 'list', 'set', '[1, 2, 3]', '{1, 2, 3}'] ]) def test_pretty_print_diff_type_changes(self, t1, t2, item_path, old_type, new_type, old_val_displayed, new_val_displayed): ddiff = DeepDiff(t1, t2, view='tree') result = pretty_print_diff(ddiff.tree['type_changes'][0]) assert result == 'Type of {} changed from {} to {} and value changed from {} to {}.'.format(item_path, old_type, new_type, old_val_displayed, new_val_displayed) @pytest.mark.parametrize('t1, t2, item_path, verbose_level', [ [{2: 2, 4: 4}, {2: 2, 4: 4, 5: 5}, 'root[5]', 1], [{2: 2, 4: 4}, {2: 2, 4: 4, 5: 5}, 'root[5] (5)', 2], [{"foo": "bar", "foo1": "bar1"}, {"foo": "bar", "foo1": "bar1", "foo2": "bar2"}, 'root[\'foo2\']', 0], [{"foo": "bar", "foo1": "bar1"}, {"foo": "bar", "foo1": "bar1", "foo2": "bar2"}, 'root[\'foo2\'] ("bar2")', 2] ]) def test_pretty_print_diff_dictionary_item_added(self, t1, t2, item_path, verbose_level): ddiff = DeepDiff(t1, t2, view='tree', verbose_level=verbose_level) result = pretty_print_diff(ddiff.tree['dictionary_item_added'][0]) assert result == 'Item {} added to dictionary.'.format(item_path) @pytest.mark.parametrize('t1, t2, item_path, verbose_level', [ [{2: 2, 4: 4}, {2: 2}, 'root[4]', 0], [{2: 2, 4: 4}, {2: 2}, 'root[4] (4)', 2], [{"foo": "bar", "foo1": "bar1"}, {"foo": "bar"}, 'root[\'foo1\']', 1], [{"foo": "bar", "foo1": "bar1"}, {"foo": "bar"}, 'root[\'foo1\'] ("bar1")', 2], ]) def test_pretty_print_diff_dictionary_item_removed(self, t1, t2, item_path, verbose_level): ddiff = DeepDiff(t1, t2, view='tree', verbose_level=verbose_level) result = pretty_print_diff(ddiff.tree['dictionary_item_removed'][0]) assert result == 'Item {} removed from dictionary.'.format(item_path) @pytest.mark.parametrize('t1, t2, item_path, old_val_displayed, new_val_displayed', [ [{2: 2, 4: 4}, {2: 3, 4: 4}, 'root[2]', '2', '3'], [['a', 'b', 'c'], ['a', 'b', 'd'], 'root[2]', '"c"', '"d"'] ]) def test_pretty_print_diff_values_changed(self, t1, t2, item_path, old_val_displayed, new_val_displayed): ddiff = DeepDiff(t1, t2, view='tree') result = pretty_print_diff(ddiff.tree['values_changed'][0]) assert result == 'Value of {} changed from {} to {}.'.format(item_path, old_val_displayed, new_val_displayed) @pytest.mark.parametrize('t1, t2, item_path, verbose_level', [ [[1, 2, 3], [1, 2, 3, 4], 'root[3]', 1], [[1, 2, 3], [1, 2, 3, 4], 'root[3] (4)', 2], [["foo", "bar"], ["foo", "bar", "barbar"], 'root[2]', 0], [["foo", "bar"], ["foo", "bar", "barbar"], 'root[2] ("barbar")', 2] ]) def test_pretty_print_diff_iterable_item_added(self, t1, t2, item_path, verbose_level): ddiff = DeepDiff(t1, t2, view='tree', verbose_level=verbose_level) result = pretty_print_diff(ddiff.tree['iterable_item_added'][0]) assert result == 'Item {} added to iterable.'.format(item_path) @pytest.mark.parametrize('t1, t2, item_path, verbose_level', [ [[1, 2, 3], [1, 2], 'root[2]', 0], [[1, 2, 3], [1, 2], 'root[2] (3)', 2], [["foo", "bar", "barbar"], ["foo", "bar"], 'root[2]', 1], [["foo", "bar", "barbar"], ["foo", "bar"], 'root[2] ("barbar")', 2] ]) def test_pretty_print_diff_iterable_item_removed(self, t1, t2, item_path, verbose_level): ddiff = DeepDiff(t1, t2, view='tree', verbose_level=verbose_level) result = pretty_print_diff(ddiff.tree['iterable_item_removed'][0]) assert result == 'Item {} removed from iterable.'.format(item_path) @pytest.mark.parametrize("verbose_level", range(3)) def test_pretty_print_diff_attribute_added(self, verbose_level): t1 = self.testing_class() t2 = self.testing_class() t2.two = 2 ddiff = DeepDiff(t1, t2, view='tree', verbose_level=verbose_level) result = pretty_print_diff(ddiff.tree['attribute_added'][0]) assert result == 'Attribute root.two (2) added.' if verbose_level == 2 else 'Attribute root.two added.' @pytest.mark.parametrize("verbose_level", range(3)) def test_pretty_print_diff_attribute_removed(self, verbose_level): t1 = self.testing_class() t1.two = 2 t2 = self.testing_class() ddiff = DeepDiff(t1, t2, view='tree', verbose_level=verbose_level) result = pretty_print_diff(ddiff.tree['attribute_removed'][0]) assert result == 'Attribute root.two (2) removed.' if verbose_level == 2 else 'Attribute root.two removed.' @pytest.mark.parametrize('t1, t2, item_path', [ [{1, 2}, {1, 2, 3}, 'root[3]'], ]) def test_pretty_print_diff_set_item_added(self, t1, t2, item_path): ddiff = DeepDiff(t1, t2, view='tree') result = pretty_print_diff(ddiff.tree['set_item_added'][0]) assert result == 'Item {} added to set.'.format(item_path) @pytest.mark.parametrize('t1, t2, item_path', [ [{1, 2, 3}, {1, 2}, 'root[3]'], ]) def test_pretty_print_diff_set_item_removed(self, t1, t2, item_path): ddiff = DeepDiff(t1, t2, view='tree') result = pretty_print_diff(ddiff.tree['set_item_removed'][0]) assert result == 'Item {} removed from set.'.format(item_path) @pytest.mark.parametrize('t1, t2, item_path', [ [[1, 2, 3, 2], [1, 2, 3], 'root[1]'], ]) def test_pretty_print_diff_repetition_change(self, t1, t2, item_path): ddiff = DeepDiff(t1, t2, view='tree', ignore_order=True, report_repetition=True) result = pretty_print_diff(ddiff.tree['repetition_change'][0]) assert result == 'Repetition change for item {}.'.format(item_path) @pytest.mark.parametrize("expected, verbose_level", ( ('Item root[5] added to dictionary.' '\nItem root[3] removed from dictionary.' '\nType of root[2] changed from int to str and value changed from 2 to "b".' '\nValue of root[4] changed from 4 to 5.', 0), ('Item root[5] (5) added to dictionary.' '\nItem root[3] (3) removed from dictionary.' '\nType of root[2] changed from int to str and value changed from 2 to "b".' '\nValue of root[4] changed from 4 to 5.', 2), ), ids=("verbose=0", "verbose=2") ) def test_pretty_form_method(self, expected, verbose_level): t1 = {2: 2, 3: 3, 4: 4} t2 = {2: 'b', 4: 5, 5: 5} ddiff = DeepDiff(t1, t2, view='tree', verbose_level=verbose_level) result = ddiff.pretty() assert result == expected @pytest.mark.parametrize("expected, verbose_level", ( ('\t\tItem root[5] added to dictionary.' '\n\t\tItem root[3] removed from dictionary.' '\n\t\tType of root[2] changed from int to str and value changed from 2 to "b".' '\n\t\tValue of root[4] changed from 4 to 5.', 0), ('\t\tItem root[5] (5) added to dictionary.' '\n\t\tItem root[3] (3) removed from dictionary.' '\n\t\tType of root[2] changed from int to str and value changed from 2 to "b".' '\n\t\tValue of root[4] changed from 4 to 5.', 2), ), ids=("verbose=0", "verbose=2") ) def test_pretty_form_method_prefixed_simple(self, expected, verbose_level): t1 = {2: 2, 3: 3, 4: 4} t2 = {2: 'b', 4: 5, 5: 5} ddiff = DeepDiff(t1, t2, verbose_level=verbose_level) result = ddiff.pretty(prefix="\t\t") assert result == expected @pytest.mark.parametrize("expected, verbose_level", ( ('Diff #1: Item root[5] added to dictionary.' '\nDiff #2: Item root[3] removed from dictionary.' '\nDiff #3: Type of root[2] changed from int to str and value changed from 2 to "b".' '\nDiff #4: Value of root[4] changed from 4 to 5.', 0), ('Diff #1: Item root[5] (5) added to dictionary.' '\nDiff #2: Item root[3] (3) removed from dictionary.' '\nDiff #3: Type of root[2] changed from int to str and value changed from 2 to "b".' '\nDiff #4: Value of root[4] changed from 4 to 5.', 2), ), ids=("verbose=0", "verbose=2") ) def test_pretty_form_method_prefixed_callback(self, expected, verbose_level): def prefix_callback(**kwargs): """Helper function using a hidden variable on the diff that tracks which count prints next""" kwargs['diff']._diff_count = 1 + getattr(kwargs['diff'], '_diff_count', 0) return f"Diff #{kwargs['diff']._diff_count}: " t1 = {2: 2, 3: 3, 4: 4} t2 = {2: 'b', 4: 5, 5: 5} ddiff = DeepDiff(t1, t2, verbose_level=verbose_level) result = ddiff.pretty(prefix=prefix_callback) assert result == expected def sig_to_bytes(inp: Dict[str, Union[str, bytes]]): inp['signature'] = inp['signature'].encode('utf-8') return inp @pytest.mark.parametrize('test_num, value, func_to_convert_back', [ (1, {'10': None}, None), (2, {"type_changes": {"root": {"old_type": None, "new_type": list, "new_value": ["你好", 2, 3, 5]}}}, None), (3, {'10': Decimal(2017)}, None), (4, Decimal(2017.1), None), (5, {1, 2, 10}, set), (6, datetime.datetime(2023, 10, 11), datetime.datetime.fromisoformat), (7, datetime.datetime.utcnow(), datetime.datetime.fromisoformat), (8, field_stats1, lambda x: SomeStats(**x)), (9, np.array([[ 101, 3533, 1998, 4532, 2024, 3415, 1012, 102]]), np.array), (10, memoryview(b"hello"), lambda x: memoryview(x.encode('utf-8'))), (11, {'file_type': 'xlsx', 'signature': b'52bd9907785'}, sig_to_bytes) ]) def test_json_dumps_and_loads(self, test_num, value, func_to_convert_back): serialized = json_dumps(value) back = json_loads(serialized) if func_to_convert_back: back = func_to_convert_back(back) if isinstance(back, np_ndarray): assert np.array_equal(value, back), f"test_json_dumps_and_loads test #{test_num} failed" else: assert value == back, f"test_json_dumps_and_loads test #{test_num} failed" def test_namedtuple_seriazliation(self): op_code = Opcode(tag="replace", t1_from_index=0, t1_to_index=1, t2_from_index=10, t2_to_index=20) serialized = json_dumps(op_code) expected = '{"tag":"replace","t1_from_index":0,"t1_to_index":1,"t2_from_index":10,"t2_to_index":20,"old_values":null,"new_values":null}' assert serialized == expected def test_reversed_list(self): items = reversed([1, 2, 3]) serialized = json_dumps(items) serialized2 = json_dumps(items) assert '[3,2,1]' == serialized assert '[3,2,1]' == serialized2, "We should have copied the original list. If this returns empty, it means we exhausted the original list." def test_dict_keys(self): dic = {"foo": "bar", "apple": "too sweet"} serialized = json_dumps(dic.keys()) assert '["foo","apple"]' == serialized def test_non_utf8_bytes_serialization(self): """Test that non-UTF-8 bytes are properly base64 encoded""" # Create binary data that cannot be decoded as UTF-8 binary_data = b'\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f' # Verify it's not UTF-8 decodable with pytest.raises(UnicodeDecodeError): binary_data.decode('utf-8') # Test serialization test_data = {"binary": binary_data} serialized = json_dumps(test_data) # Should contain base64 encoded data expected_b64 = base64.b64encode(binary_data).decode('ascii') assert expected_b64 in serialized # Should be deserializable deserialized = json_loads(serialized) assert deserialized == {"binary": expected_b64} def test_hash_bytes_serialization(self): """Test serialization of hash-like binary data (blake3, sha256, etc.)""" # Generate various hash-like byte sequences test_cases = [ hashlib.md5(b"test").digest(), hashlib.sha1(b"test").digest(), hashlib.sha256(b"test").digest(), hashlib.sha512(b"test").digest()[:16], # Truncated b'\xff\xfe\xfd\xfc' * 8, # Artificial binary pattern ] for i, hash_bytes in enumerate(test_cases): test_data = {"hash": hash_bytes} # Should not raise UnicodeDecodeError serialized = json_dumps(test_data) assert serialized # Should produce valid JSON # Should contain base64 if not UTF-8 decodable, or string if UTF-8 decodable try: utf8_decoded = hash_bytes.decode('utf-8') # If UTF-8 decodable, should be in JSON as string assert utf8_decoded in serialized except UnicodeDecodeError: # If not UTF-8 decodable, should be base64 encoded expected_b64 = base64.b64encode(hash_bytes).decode('ascii') assert expected_b64 in serialized def test_mixed_utf8_and_binary_bytes(self): """Test data structure with both UTF-8 decodable and binary bytes""" test_data = { "utf8_text": b"hello world", # UTF-8 decodable "binary_hash": hashlib.sha256(b"secret").digest(), # Binary "empty_bytes": b"", # Edge case "utf8_unicode": "café".encode('utf-8'), # UTF-8 with unicode "non_utf8_byte": b"\xff\xfe\xfd", # Non-UTF-8 bytes } # Should serialize without errors serialized = json_dumps(test_data) deserialized = json_loads(serialized) # UTF-8 decodable bytes should remain as strings assert "hello world" in serialized assert deserialized["utf8_text"] == "hello world" # Unicode UTF-8 should work assert deserialized["utf8_unicode"] == "café" # Binary data should be base64 encoded expected_hash_b64 = base64.b64encode(test_data["binary_hash"]).decode('ascii') assert expected_hash_b64 in serialized assert deserialized["binary_hash"] == expected_hash_b64 # Empty bytes should be empty string assert deserialized["empty_bytes"] == "" # Non-UTF-8 bytes should be base64 encoded expected_non_utf8_b64 = base64.b64encode(test_data["non_utf8_byte"]).decode('ascii') assert expected_non_utf8_b64 in serialized assert deserialized["non_utf8_byte"] == expected_non_utf8_b64 def test_json_dumps_large_int_exceeding_64bit(self): """Test that integers exceeding 64-bit range are serialized as strings. orjson cannot handle integers larger than 64-bit (9223372036854775807), so json_dumps should convert them to strings.""" large_int = 59579472846392086780 assert large_int > 9223372036854775807 # larger than max int64 data = {'max_int': large_int, 'min_int': 1336138} serialized = json_dumps(data) back = json_loads(serialized) assert back['max_int'] == str(large_int) assert back['min_int'] == 1336138 def test_json_dumps_large_int_in_nested_structure(self): """Test large integers in nested data structures are converted to strings.""" large_int = 59579472846392086780 data = { 'stats': { 'counter': {'HasInt': 499}, 'max_int': large_int, 'min_int': 1336138, }, 'name': 'policy_number', } serialized = json_dumps(data) back = json_loads(serialized) assert back['stats']['max_int'] == str(large_int) def test_json_dumps_large_negative_int(self): """Test that large negative integers are also converted to strings.""" large_neg_int = -59579472846392086780 data = {'value': large_neg_int} serialized = json_dumps(data) back = json_loads(serialized) assert back['value'] == str(large_neg_int) def test_json_dumps_namedtuple_with_large_int(self): """Test that a NamedTuple containing an oversized int is properly serialized. _convert_oversized_ints must reconstruct the NamedTuple via _fields, not pass a flat list to its constructor (which fails for NamedTuples with required keyword arguments).""" large_int = 59579472846392086780 stats = SomeStats( counter=Counter(["a", "b"]), context_aware_counter=Counter(), min_int=0, max_int=large_int, ) data = {'stats': stats} serialized = json_dumps(data) back = json_loads(serialized) assert back['stats']['max_int'] == str(large_int) assert back['stats']['min_int'] == 0 def test_json_dumps_dict_of_namedtuples_with_large_int(self): """Test a dict of NamedTuples where one contains an oversized int. This mirrors the real-world pattern of field stats keyed by column name.""" large_int = 59579472846392086780 stats_map = { 'normal_field': SomeStats(counter=Counter(["x"]), max_int=10), 'big_field': SomeStats(counter=Counter(["y"]), max_int=large_int), } serialized = json_dumps(stats_map) back = json_loads(serialized) assert back['normal_field']['max_int'] == 10 assert back['big_field']['max_int'] == str(large_int) def test_json_dumps_namedtuple_with_large_int_in_list(self): """Test a list of NamedTuples where one has an oversized int.""" large_int = 59579472846392086780 data = [ SomeStats(counter=Counter(), max_int=5), SomeStats(counter=Counter(), max_int=large_int), ] serialized = json_dumps(data) back = json_loads(serialized) assert back[0]['max_int'] == 5 assert back[1]['max_int'] == str(large_int) def test_bytes_in_deepdiff_serialization(self): """Test that bytes work correctly in DeepDiff JSON serialization""" t1 = { "text": b"hello", "hash": hashlib.sha256(b"data1").digest(), } t2 = { "text": b"world", "hash": hashlib.sha256(b"data2").digest(), } diff = DeepDiff(t1, t2) # Should serialize without errors json_output = diff.to_json() assert json_output # Should contain both UTF-8 decoded strings and base64 encoded hashes assert "hello" in json_output assert "world" in json_output # Hash values should be base64 encoded expected_hash1 = base64.b64encode(t1["hash"]).decode('ascii') expected_hash2 = base64.b64encode(t2["hash"]).decode('ascii') assert expected_hash1 in json_output assert expected_hash2 in json_output qlustered-deepdiff-41c7265/tests/test_summarize.py000066400000000000000000000157101516241264500223440ustar00rootroot00000000000000from copy import deepcopy from deepdiff.summarize import summarize, _truncate class TestSummarize: def test_empty_dict(self): summary = summarize({}, max_length=50) assert summary == "{}", "Empty dict should be summarized as {}" def test_empty_list(self): summary = summarize([], max_length=50) assert summary == "[]", "Empty list should be summarized as []" def test_primitive_int_truncation(self): summary = summarize(1234567890123, max_length=10) # The summary should be the string representation, truncated to max_length assert isinstance(summary, str) assert len(summary) <= 10 def test_primitive_string_no_truncation(self): summary = summarize("short", max_length=50) assert '"short"' == summary, "Short strings should not be truncated, but we are adding double quotes to it." def test_small_dict_summary(self): data = {"a": "alpha", "b": "beta"} summary = summarize(data, max_length=50) # Should be JSON-like, start with { and end with } and not exceed the max length. assert summary.startswith("{") and summary.endswith("}") assert len(summary) <= 50 def test_long_value_truncation_in_dict(self): data = { "key1": "a" * 100, "key2": "b" * 50, "key3": "c" * 150 } summary = summarize(data, max_length=100) # The summary should be under 100 characters and include ellipsis to indicate truncation. assert len(summary) == 113, "Yes we are going slightly above" assert "..." in summary def test_nested_structure_summary1(self): data = { "RecordType": "CID", "RecordNumber": 2719, "RecordTitle": "Chloroquine", "Section": [ { "TOCHeading": "Structures", "Description": "Structure depictions and information for 2D, 3D, and crystal related", "Section": [ { "TOCHeading": "2D Structure", "Description": "A two-dimensional representation of the compound", "DisplayControls": {"MoveToTop": True}, "Information": [ { "ReferenceNumber": 69, "Value": {"Boolean": [True]} } ] }, { "TOCHeading": "3D Conformer", "Description": ("A three-dimensional representation of the compound. " "The 3D structure is not experimentally determined, but computed by PubChem. " "More detailed information on this conformer model is described in the PubChem3D thematic series published in the Journal of Cheminformatics."), "DisplayControls": {"MoveToTop": True}, "Information": [ { "ReferenceNumber": 69, "Description": "Chloroquine", "Value": {"Number": [2719]} } ] } ] }, { "TOCHeading": "Chemical Safety", "Description": "Launch the Laboratory Chemical Safety Summary datasheet, and link to the safety and hazard section", "DisplayControls": {"HideThisSection": True, "MoveToTop": True}, "Information": [ { "ReferenceNumber": 69, "Name": "Chemical Safety", "Value": { "StringWithMarkup": [ { "String": " ", "Markup": [ { "Start": 0, "Length": 1, "URL": "https://pubchem.ncbi.nlm.nih.gov/images/ghs/GHS07.svg", "Type": "Icon", "Extra": "Irritant" } ] } ] } } ] } ] } data_copy = deepcopy(data) summary = summarize(data_copy, max_length=200) assert len(summary) == 240, "Yes slightly above" # Check that some expected keys are in the summary assert '"RecordType"' in summary assert '"RecordNumber"' in summary assert '"RecordTitle"' in summary expected = '{"Section":[{"Section":[{"Description":""},{"Description":""}],"Description":"Structure depictions a...ed"},{"Information":[{"Name":"C"}],"Description":"Launch the ...on"}],"RecordTitle":"Chloroquine","RecordNumber":2719,"RecordType":"CID"}' assert expected == summary assert data_copy == data, "We should not have modified the original data" def test_nested_structure_summary2(self, compounds): summary = summarize(compounds, max_length=200) assert len(summary) == 319, "Ok yeah max_length is more like a guide" data_copy = deepcopy(compounds) expected = '{"Section":[{"Section":[{"Description":""},{"Description":""}],"Description":"Toxicity information r...y."},{"Section":[{"Section":["..."]},{"Section":["..."]}],"Description":"Spectral ...ds"},"..."],"Reference":[{"LicenseNote":"Use of th...e.","Description":"T...s."},{"LicenseNote":"U...e.","Description":"T"},"..."]}' assert expected == summary assert data_copy == compounds, "We should not have modified the original data" def test_list_summary(self): data = [1, 2, 3, 4] summary = summarize(data, max_length=50) # The summary should start with '[' and end with ']' assert summary.startswith("[") and summary.endswith("]") # When more than one element exists, expect a trailing ellipsis or indication of more elements assert "..." not in summary data2 = list(range(1, 200)) summary2 = summarize(data2, max_length=14) assert "..." in summary2 expected = '[100,101,102,103,10,"..."]' assert expected == summary2 def test_direct_truncate_function(self): s = "abcdefghijklmnopqrstuvwxyz" truncated = _truncate(s, 20) assert len(truncated) == 20 assert "..." in truncated qlustered-deepdiff-41c7265/uv.lock000066400000000000000000012110041516241264500170540ustar00rootroot00000000000000version = 1 revision = 3 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version < '3.11'", ] [[package]] name = "accessible-pygments" version = "0.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bc/c1/bbac6a50d02774f91572938964c582fff4270eee73ab822a4aeea4d8b11b/accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872", size = 1377899, upload-time = "2024-05-10T11:23:10.216Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8d/3f/95338030883d8c8b91223b4e21744b04d11b161a3ef117295d8241f50ab4/accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7", size = 1395903, upload-time = "2024-05-10T11:23:08.421Z" }, ] [[package]] name = "alabaster" version = "1.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, ] [[package]] name = "annotated-types" version = "0.7.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] [[package]] name = "argcomplete" version = "3.6.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/38/61/0b9ae6399dd4a58d8c1b1dc5a27d6f2808023d0b5dd3104bb99f45a33ff6/argcomplete-3.6.3.tar.gz", hash = "sha256:62e8ed4fd6a45864acc8235409461b72c9a28ee785a2011cc5eb78318786c89c", size = 73754, upload-time = "2025-10-20T03:33:34.741Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/74/f5/9373290775639cb67a2fce7f629a1c240dce9f12fe927bc32b2736e16dfc/argcomplete-3.6.3-py3-none-any.whl", hash = "sha256:f5007b3a600ccac5d25bbce33089211dfd49eab4a7718da3f10e3082525a92ce", size = 43846, upload-time = "2025-10-20T03:33:33.021Z" }, ] [[package]] name = "asttokens" version = "3.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, ] [[package]] name = "attrs" version = "26.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, ] [[package]] name = "babel" version = "2.18.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, ] [[package]] name = "beautifulsoup4" version = "4.14.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "soupsieve" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, ] [[package]] name = "bump2version" version = "1.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/29/2a/688aca6eeebfe8941235be53f4da780c6edee05dbbea5d7abaa3aab6fad2/bump2version-1.0.1.tar.gz", hash = "sha256:762cb2bfad61f4ec8e2bdf452c7c267416f8c70dd9ecb1653fd0bbb01fa936e6", size = 36236, upload-time = "2020-10-07T18:38:40.119Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1d/e3/fa60c47d7c344533142eb3af0b73234ef8ea3fb2da742ab976b947e717df/bump2version-1.0.1-py2.py3-none-any.whl", hash = "sha256:37f927ea17cde7ae2d7baf832f8e80ce3777624554a653006c9144f8017fe410", size = 22030, upload-time = "2020-10-07T18:38:38.148Z" }, ] [[package]] name = "certifi" version = "2026.2.25" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, ] [[package]] name = "charset-normalizer" version = "3.4.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7b/60/e3bec1881450851b087e301bedc3daa9377a4d45f1c26aa90b0b235e38aa/charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6", size = 143363, upload-time = "2026-03-15T18:53:25.478Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e6/8c/2c56124c6dc53a774d435f985b5973bc592f42d437be58c0c92d65ae7296/charset_normalizer-3.4.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2e1d8ca8611099001949d1cdfaefc510cf0f212484fe7c565f735b68c78c3c95", size = 298751, upload-time = "2026-03-15T18:50:00.003Z" }, { url = "https://files.pythonhosted.org/packages/86/2a/2a7db6b314b966a3bcad8c731c0719c60b931b931de7ae9f34b2839289ee/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e25369dc110d58ddf29b949377a93e0716d72a24f62bad72b2b39f155949c1fd", size = 200027, upload-time = "2026-03-15T18:50:01.702Z" }, { url = "https://files.pythonhosted.org/packages/68/f2/0fe775c74ae25e2a3b07b01538fc162737b3e3f795bada3bc26f4d4d495c/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:259695e2ccc253feb2a016303543d691825e920917e31f894ca1a687982b1de4", size = 220741, upload-time = "2026-03-15T18:50:03.194Z" }, { url = "https://files.pythonhosted.org/packages/10/98/8085596e41f00b27dd6aa1e68413d1ddda7e605f34dd546833c61fddd709/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dda86aba335c902b6149a02a55b38e96287157e609200811837678214ba2b1db", size = 215802, upload-time = "2026-03-15T18:50:05.859Z" }, { url = "https://files.pythonhosted.org/packages/fd/ce/865e4e09b041bad659d682bbd98b47fb490b8e124f9398c9448065f64fee/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51fb3c322c81d20567019778cb5a4a6f2dc1c200b886bc0d636238e364848c89", size = 207908, upload-time = "2026-03-15T18:50:07.676Z" }, { url = "https://files.pythonhosted.org/packages/a8/54/8c757f1f7349262898c2f169e0d562b39dcb977503f18fdf0814e923db78/charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:4482481cb0572180b6fd976a4d5c72a30263e98564da68b86ec91f0fe35e8565", size = 194357, upload-time = "2026-03-15T18:50:09.327Z" }, { url = "https://files.pythonhosted.org/packages/6f/29/e88f2fac9218907fc7a70722b393d1bbe8334c61fe9c46640dba349b6e66/charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39f5068d35621da2881271e5c3205125cc456f54e9030d3f723288c873a71bf9", size = 205610, upload-time = "2026-03-15T18:50:10.732Z" }, { url = "https://files.pythonhosted.org/packages/4c/c5/21d7bb0cb415287178450171d130bed9d664211fdd59731ed2c34267b07d/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8bea55c4eef25b0b19a0337dc4e3f9a15b00d569c77211fa8cde38684f234fb7", size = 203512, upload-time = "2026-03-15T18:50:12.535Z" }, { url = "https://files.pythonhosted.org/packages/a4/be/ce52f3c7fdb35cc987ad38a53ebcef52eec498f4fb6c66ecfe62cfe57ba2/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f0cdaecd4c953bfae0b6bb64910aaaca5a424ad9c72d85cb88417bb9814f7550", size = 195398, upload-time = "2026-03-15T18:50:14.236Z" }, { url = "https://files.pythonhosted.org/packages/81/a0/3ab5dd39d4859a3555e5dadfc8a9fa7f8352f8c183d1a65c90264517da0e/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:150b8ce8e830eb7ccb029ec9ca36022f756986aaaa7956aad6d9ec90089338c0", size = 221772, upload-time = "2026-03-15T18:50:15.581Z" }, { url = "https://files.pythonhosted.org/packages/04/6e/6a4e41a97ba6b2fa87f849c41e4d229449a586be85053c4d90135fe82d26/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e68c14b04827dd76dcbd1aeea9e604e3e4b78322d8faf2f8132c7138efa340a8", size = 205759, upload-time = "2026-03-15T18:50:17.047Z" }, { url = "https://files.pythonhosted.org/packages/db/3b/34a712a5ee64a6957bf355b01dc17b12de457638d436fdb05d01e463cd1c/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:3778fd7d7cd04ae8f54651f4a7a0bd6e39a0cf20f801720a4c21d80e9b7ad6b0", size = 216938, upload-time = "2026-03-15T18:50:18.44Z" }, { url = "https://files.pythonhosted.org/packages/cb/05/5bd1e12da9ab18790af05c61aafd01a60f489778179b621ac2a305243c62/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dad6e0f2e481fffdcf776d10ebee25e0ef89f16d691f1e5dee4b586375fdc64b", size = 210138, upload-time = "2026-03-15T18:50:19.852Z" }, { url = "https://files.pythonhosted.org/packages/bd/8e/3cb9e2d998ff6b21c0a1860343cb7b83eba9cdb66b91410e18fc4969d6ab/charset_normalizer-3.4.6-cp310-cp310-win32.whl", hash = "sha256:74a2e659c7ecbc73562e2a15e05039f1e22c75b7c7618b4b574a3ea9118d1557", size = 144137, upload-time = "2026-03-15T18:50:21.505Z" }, { url = "https://files.pythonhosted.org/packages/d8/8f/78f5489ffadb0db3eb7aff53d31c24531d33eb545f0c6f6567c25f49a5ff/charset_normalizer-3.4.6-cp310-cp310-win_amd64.whl", hash = "sha256:aa9cccf4a44b9b62d8ba8b4dd06c649ba683e4bf04eea606d2e94cfc2d6ff4d6", size = 154244, upload-time = "2026-03-15T18:50:22.81Z" }, { url = "https://files.pythonhosted.org/packages/e4/74/e472659dffb0cadb2f411282d2d76c60da1fc94076d7fffed4ae8a93ec01/charset_normalizer-3.4.6-cp310-cp310-win_arm64.whl", hash = "sha256:e985a16ff513596f217cee86c21371b8cd011c0f6f056d0920aa2d926c544058", size = 143312, upload-time = "2026-03-15T18:50:24.074Z" }, { url = "https://files.pythonhosted.org/packages/62/28/ff6f234e628a2de61c458be2779cb182bc03f6eec12200d4a525bbfc9741/charset_normalizer-3.4.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e", size = 293582, upload-time = "2026-03-15T18:50:25.454Z" }, { url = "https://files.pythonhosted.org/packages/1c/b7/b1a117e5385cbdb3205f6055403c2a2a220c5ea80b8716c324eaf75c5c95/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9", size = 197240, upload-time = "2026-03-15T18:50:27.196Z" }, { url = "https://files.pythonhosted.org/packages/a1/5f/2574f0f09f3c3bc1b2f992e20bce6546cb1f17e111c5be07308dc5427956/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6e4333fb15c83f7d1482a76d45a0818897b3d33f00efd215528ff7c51b8e35d", size = 217363, upload-time = "2026-03-15T18:50:28.601Z" }, { url = "https://files.pythonhosted.org/packages/4a/d1/0ae20ad77bc949ddd39b51bf383b6ca932f2916074c95cad34ae465ab71f/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bc72863f4d9aba2e8fd9085e63548a324ba706d2ea2c83b260da08a59b9482de", size = 212994, upload-time = "2026-03-15T18:50:30.102Z" }, { url = "https://files.pythonhosted.org/packages/60/ac/3233d262a310c1b12633536a07cde5ddd16985e6e7e238e9f3f9423d8eb9/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73", size = 204697, upload-time = "2026-03-15T18:50:31.654Z" }, { url = "https://files.pythonhosted.org/packages/25/3c/8a18fc411f085b82303cfb7154eed5bd49c77035eb7608d049468b53f87c/charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:0c173ce3a681f309f31b87125fecec7a5d1347261ea11ebbb856fa6006b23c8c", size = 191673, upload-time = "2026-03-15T18:50:33.433Z" }, { url = "https://files.pythonhosted.org/packages/ff/a7/11cfe61d6c5c5c7438d6ba40919d0306ed83c9ab957f3d4da2277ff67836/charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c907cdc8109f6c619e6254212e794d6548373cc40e1ec75e6e3823d9135d29cc", size = 201120, upload-time = "2026-03-15T18:50:35.105Z" }, { url = "https://files.pythonhosted.org/packages/b5/10/cf491fa1abd47c02f69687046b896c950b92b6cd7337a27e6548adbec8e4/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:404a1e552cf5b675a87f0651f8b79f5f1e6fd100ee88dc612f89aa16abd4486f", size = 200911, upload-time = "2026-03-15T18:50:36.819Z" }, { url = "https://files.pythonhosted.org/packages/28/70/039796160b48b18ed466fde0af84c1b090c4e288fae26cd674ad04a2d703/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e3c701e954abf6fc03a49f7c579cc80c2c6cc52525340ca3186c41d3f33482ef", size = 192516, upload-time = "2026-03-15T18:50:38.228Z" }, { url = "https://files.pythonhosted.org/packages/ff/34/c56f3223393d6ff3124b9e78f7de738047c2d6bc40a4f16ac0c9d7a1cb3c/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7a6967aaf043bceabab5412ed6bd6bd26603dae84d5cb75bf8d9a74a4959d398", size = 218795, upload-time = "2026-03-15T18:50:39.664Z" }, { url = "https://files.pythonhosted.org/packages/e8/3b/ce2d4f86c5282191a041fdc5a4ce18f1c6bd40a5bd1f74cf8625f08d51c1/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5feb91325bbceade6afab43eb3b508c63ee53579fe896c77137ded51c6b6958e", size = 201833, upload-time = "2026-03-15T18:50:41.552Z" }, { url = "https://files.pythonhosted.org/packages/3b/9b/b6a9f76b0fd7c5b5ec58b228ff7e85095370282150f0bd50b3126f5506d6/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f820f24b09e3e779fe84c3c456cb4108a7aa639b0d1f02c28046e11bfcd088ed", size = 213920, upload-time = "2026-03-15T18:50:43.33Z" }, { url = "https://files.pythonhosted.org/packages/ae/98/7bc23513a33d8172365ed30ee3a3b3fe1ece14a395e5fc94129541fc6003/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021", size = 206951, upload-time = "2026-03-15T18:50:44.789Z" }, { url = "https://files.pythonhosted.org/packages/32/73/c0b86f3d1458468e11aec870e6b3feac931facbe105a894b552b0e518e79/charset_normalizer-3.4.6-cp311-cp311-win32.whl", hash = "sha256:9ca4c0b502ab399ef89248a2c84c54954f77a070f28e546a85e91da627d1301e", size = 143703, upload-time = "2026-03-15T18:50:46.103Z" }, { url = "https://files.pythonhosted.org/packages/c6/e3/76f2facfe8eddee0bbd38d2594e709033338eae44ebf1738bcefe0a06185/charset_normalizer-3.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:a9e68c9d88823b274cf1e72f28cb5dc89c990edf430b0bfd3e2fb0785bfeabf4", size = 153857, upload-time = "2026-03-15T18:50:47.563Z" }, { url = "https://files.pythonhosted.org/packages/e2/dc/9abe19c9b27e6cd3636036b9d1b387b78c40dedbf0b47f9366737684b4b0/charset_normalizer-3.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:97d0235baafca5f2b09cf332cc275f021e694e8362c6bb9c96fc9a0eb74fc316", size = 142751, upload-time = "2026-03-15T18:50:49.234Z" }, { url = "https://files.pythonhosted.org/packages/e5/62/c0815c992c9545347aeea7859b50dc9044d147e2e7278329c6e02ac9a616/charset_normalizer-3.4.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab", size = 295154, upload-time = "2026-03-15T18:50:50.88Z" }, { url = "https://files.pythonhosted.org/packages/a8/37/bdca6613c2e3c58c7421891d80cc3efa1d32e882f7c4a7ee6039c3fc951a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21", size = 199191, upload-time = "2026-03-15T18:50:52.658Z" }, { url = "https://files.pythonhosted.org/packages/6c/92/9934d1bbd69f7f398b38c5dae1cbf9cc672e7c34a4adf7b17c0a9c17d15d/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2", size = 218674, upload-time = "2026-03-15T18:50:54.102Z" }, { url = "https://files.pythonhosted.org/packages/af/90/25f6ab406659286be929fd89ab0e78e38aa183fc374e03aa3c12d730af8a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff", size = 215259, upload-time = "2026-03-15T18:50:55.616Z" }, { url = "https://files.pythonhosted.org/packages/4e/ef/79a463eb0fff7f96afa04c1d4c51f8fc85426f918db467854bfb6a569ce3/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5", size = 207276, upload-time = "2026-03-15T18:50:57.054Z" }, { url = "https://files.pythonhosted.org/packages/f7/72/d0426afec4b71dc159fa6b4e68f868cd5a3ecd918fec5813a15d292a7d10/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0", size = 195161, upload-time = "2026-03-15T18:50:58.686Z" }, { url = "https://files.pythonhosted.org/packages/bf/18/c82b06a68bfcb6ce55e508225d210c7e6a4ea122bfc0748892f3dc4e8e11/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a", size = 203452, upload-time = "2026-03-15T18:51:00.196Z" }, { url = "https://files.pythonhosted.org/packages/44/d6/0c25979b92f8adafdbb946160348d8d44aa60ce99afdc27df524379875cb/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2", size = 202272, upload-time = "2026-03-15T18:51:01.703Z" }, { url = "https://files.pythonhosted.org/packages/2e/3d/7fea3e8fe84136bebbac715dd1221cc25c173c57a699c030ab9b8900cbb7/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5", size = 195622, upload-time = "2026-03-15T18:51:03.526Z" }, { url = "https://files.pythonhosted.org/packages/57/8a/d6f7fd5cb96c58ef2f681424fbca01264461336d2a7fc875e4446b1f1346/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6", size = 220056, upload-time = "2026-03-15T18:51:05.269Z" }, { url = "https://files.pythonhosted.org/packages/16/50/478cdda782c8c9c3fb5da3cc72dd7f331f031e7f1363a893cdd6ca0f8de0/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d", size = 203751, upload-time = "2026-03-15T18:51:06.858Z" }, { url = "https://files.pythonhosted.org/packages/75/fc/cc2fcac943939c8e4d8791abfa139f685e5150cae9f94b60f12520feaa9b/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2", size = 216563, upload-time = "2026-03-15T18:51:08.564Z" }, { url = "https://files.pythonhosted.org/packages/a8/b7/a4add1d9a5f68f3d037261aecca83abdb0ab15960a3591d340e829b37298/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923", size = 209265, upload-time = "2026-03-15T18:51:10.312Z" }, { url = "https://files.pythonhosted.org/packages/6c/18/c094561b5d64a24277707698e54b7f67bd17a4f857bbfbb1072bba07c8bf/charset_normalizer-3.4.6-cp312-cp312-win32.whl", hash = "sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4", size = 144229, upload-time = "2026-03-15T18:51:11.694Z" }, { url = "https://files.pythonhosted.org/packages/ab/20/0567efb3a8fd481b8f34f739ebddc098ed062a59fed41a8d193a61939e8f/charset_normalizer-3.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb", size = 154277, upload-time = "2026-03-15T18:51:13.004Z" }, { url = "https://files.pythonhosted.org/packages/15/57/28d79b44b51933119e21f65479d0864a8d5893e494cf5daab15df0247c17/charset_normalizer-3.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4", size = 142817, upload-time = "2026-03-15T18:51:14.408Z" }, { url = "https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f", size = 294823, upload-time = "2026-03-15T18:51:15.755Z" }, { url = "https://files.pythonhosted.org/packages/47/7b/20e809b89c69d37be748d98e84dce6820bf663cf19cf6b942c951a3e8f41/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843", size = 198527, upload-time = "2026-03-15T18:51:17.177Z" }, { url = "https://files.pythonhosted.org/packages/37/a6/4f8d27527d59c039dce6f7622593cdcd3d70a8504d87d09eb11e9fdc6062/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf", size = 218388, upload-time = "2026-03-15T18:51:18.934Z" }, { url = "https://files.pythonhosted.org/packages/f6/9b/4770ccb3e491a9bacf1c46cc8b812214fe367c86a96353ccc6daf87b01ec/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8", size = 214563, upload-time = "2026-03-15T18:51:20.374Z" }, { url = "https://files.pythonhosted.org/packages/2b/58/a199d245894b12db0b957d627516c78e055adc3a0d978bc7f65ddaf7c399/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9", size = 206587, upload-time = "2026-03-15T18:51:21.807Z" }, { url = "https://files.pythonhosted.org/packages/7e/70/3def227f1ec56f5c69dfc8392b8bd63b11a18ca8178d9211d7cc5e5e4f27/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88", size = 194724, upload-time = "2026-03-15T18:51:23.508Z" }, { url = "https://files.pythonhosted.org/packages/58/ab/9318352e220c05efd31c2779a23b50969dc94b985a2efa643ed9077bfca5/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84", size = 202956, upload-time = "2026-03-15T18:51:25.239Z" }, { url = "https://files.pythonhosted.org/packages/75/13/f3550a3ac25b70f87ac98c40d3199a8503676c2f1620efbf8d42095cfc40/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd", size = 201923, upload-time = "2026-03-15T18:51:26.682Z" }, { url = "https://files.pythonhosted.org/packages/1b/db/c5c643b912740b45e8eec21de1bbab8e7fc085944d37e1e709d3dcd9d72f/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c", size = 195366, upload-time = "2026-03-15T18:51:28.129Z" }, { url = "https://files.pythonhosted.org/packages/5a/67/3b1c62744f9b2448443e0eb160d8b001c849ec3fef591e012eda6484787c/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194", size = 219752, upload-time = "2026-03-15T18:51:29.556Z" }, { url = "https://files.pythonhosted.org/packages/f6/98/32ffbaf7f0366ffb0445930b87d103f6b406bc2c271563644bde8a2b1093/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc", size = 203296, upload-time = "2026-03-15T18:51:30.921Z" }, { url = "https://files.pythonhosted.org/packages/41/12/5d308c1bbe60cabb0c5ef511574a647067e2a1f631bc8634fcafaccd8293/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f", size = 215956, upload-time = "2026-03-15T18:51:32.399Z" }, { url = "https://files.pythonhosted.org/packages/53/e9/5f85f6c5e20669dbe56b165c67b0260547dea97dba7e187938833d791687/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2", size = 208652, upload-time = "2026-03-15T18:51:34.214Z" }, { url = "https://files.pythonhosted.org/packages/f1/11/897052ea6af56df3eef3ca94edafee410ca699ca0c7b87960ad19932c55e/charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d", size = 143940, upload-time = "2026-03-15T18:51:36.15Z" }, { url = "https://files.pythonhosted.org/packages/a1/5c/724b6b363603e419829f561c854b87ed7c7e31231a7908708ac086cdf3e2/charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389", size = 154101, upload-time = "2026-03-15T18:51:37.876Z" }, { url = "https://files.pythonhosted.org/packages/01/a5/7abf15b4c0968e47020f9ca0935fb3274deb87cb288cd187cad92e8cdffd/charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f", size = 143109, upload-time = "2026-03-15T18:51:39.565Z" }, { url = "https://files.pythonhosted.org/packages/25/6f/ffe1e1259f384594063ea1869bfb6be5cdb8bc81020fc36c3636bc8302a1/charset_normalizer-3.4.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8", size = 294458, upload-time = "2026-03-15T18:51:41.134Z" }, { url = "https://files.pythonhosted.org/packages/56/60/09bb6c13a8c1016c2ed5c6a6488e4ffef506461aa5161662bd7636936fb1/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421", size = 199277, upload-time = "2026-03-15T18:51:42.953Z" }, { url = "https://files.pythonhosted.org/packages/00/50/dcfbb72a5138bbefdc3332e8d81a23494bf67998b4b100703fd15fa52d81/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2", size = 218758, upload-time = "2026-03-15T18:51:44.339Z" }, { url = "https://files.pythonhosted.org/packages/03/b3/d79a9a191bb75f5aa81f3aaaa387ef29ce7cb7a9e5074ba8ea095cc073c2/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30", size = 215299, upload-time = "2026-03-15T18:51:45.871Z" }, { url = "https://files.pythonhosted.org/packages/76/7e/bc8911719f7084f72fd545f647601ea3532363927f807d296a8c88a62c0d/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db", size = 206811, upload-time = "2026-03-15T18:51:47.308Z" }, { url = "https://files.pythonhosted.org/packages/e2/40/c430b969d41dda0c465aa36cc7c2c068afb67177bef50905ac371b28ccc7/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8", size = 193706, upload-time = "2026-03-15T18:51:48.849Z" }, { url = "https://files.pythonhosted.org/packages/48/15/e35e0590af254f7df984de1323640ef375df5761f615b6225ba8deb9799a/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815", size = 202706, upload-time = "2026-03-15T18:51:50.257Z" }, { url = "https://files.pythonhosted.org/packages/5e/bd/f736f7b9cc5e93a18b794a50346bb16fbfd6b37f99e8f306f7951d27c17c/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a", size = 202497, upload-time = "2026-03-15T18:51:52.012Z" }, { url = "https://files.pythonhosted.org/packages/9d/ba/2cc9e3e7dfdf7760a6ed8da7446d22536f3d0ce114ac63dee2a5a3599e62/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43", size = 193511, upload-time = "2026-03-15T18:51:53.723Z" }, { url = "https://files.pythonhosted.org/packages/9e/cb/5be49b5f776e5613be07298c80e1b02a2d900f7a7de807230595c85a8b2e/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0", size = 220133, upload-time = "2026-03-15T18:51:55.333Z" }, { url = "https://files.pythonhosted.org/packages/83/43/99f1b5dad345accb322c80c7821071554f791a95ee50c1c90041c157ae99/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1", size = 203035, upload-time = "2026-03-15T18:51:56.736Z" }, { url = "https://files.pythonhosted.org/packages/87/9a/62c2cb6a531483b55dddff1a68b3d891a8b498f3ca555fbcf2978e804d9d/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f", size = 216321, upload-time = "2026-03-15T18:51:58.17Z" }, { url = "https://files.pythonhosted.org/packages/6e/79/94a010ff81e3aec7c293eb82c28f930918e517bc144c9906a060844462eb/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815", size = 208973, upload-time = "2026-03-15T18:51:59.998Z" }, { url = "https://files.pythonhosted.org/packages/2a/57/4ecff6d4ec8585342f0c71bc03efaa99cb7468f7c91a57b105bcd561cea8/charset_normalizer-3.4.6-cp314-cp314-win32.whl", hash = "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d", size = 144610, upload-time = "2026-03-15T18:52:02.213Z" }, { url = "https://files.pythonhosted.org/packages/80/94/8434a02d9d7f168c25767c64671fead8d599744a05d6a6c877144c754246/charset_normalizer-3.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f", size = 154962, upload-time = "2026-03-15T18:52:03.658Z" }, { url = "https://files.pythonhosted.org/packages/46/4c/48f2cdbfd923026503dfd67ccea45c94fd8fe988d9056b468579c66ed62b/charset_normalizer-3.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e", size = 143595, upload-time = "2026-03-15T18:52:05.123Z" }, { url = "https://files.pythonhosted.org/packages/31/93/8878be7569f87b14f1d52032946131bcb6ebbd8af3e20446bc04053dc3f1/charset_normalizer-3.4.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866", size = 314828, upload-time = "2026-03-15T18:52:06.831Z" }, { url = "https://files.pythonhosted.org/packages/06/b6/fae511ca98aac69ecc35cde828b0a3d146325dd03d99655ad38fc2cc3293/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc", size = 208138, upload-time = "2026-03-15T18:52:08.239Z" }, { url = "https://files.pythonhosted.org/packages/54/57/64caf6e1bf07274a1e0b7c160a55ee9e8c9ec32c46846ce59b9c333f7008/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e", size = 224679, upload-time = "2026-03-15T18:52:10.043Z" }, { url = "https://files.pythonhosted.org/packages/aa/cb/9ff5a25b9273ef160861b41f6937f86fae18b0792fe0a8e75e06acb08f1d/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077", size = 223475, upload-time = "2026-03-15T18:52:11.854Z" }, { url = "https://files.pythonhosted.org/packages/fc/97/440635fc093b8d7347502a377031f9605a1039c958f3cd18dcacffb37743/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f", size = 215230, upload-time = "2026-03-15T18:52:13.325Z" }, { url = "https://files.pythonhosted.org/packages/cd/24/afff630feb571a13f07c8539fbb502d2ab494019492aaffc78ef41f1d1d0/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e", size = 199045, upload-time = "2026-03-15T18:52:14.752Z" }, { url = "https://files.pythonhosted.org/packages/e5/17/d1399ecdaf7e0498c327433e7eefdd862b41236a7e484355b8e0e5ebd64b/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484", size = 211658, upload-time = "2026-03-15T18:52:16.278Z" }, { url = "https://files.pythonhosted.org/packages/b5/38/16baa0affb957b3d880e5ac2144caf3f9d7de7bc4a91842e447fbb5e8b67/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7", size = 210769, upload-time = "2026-03-15T18:52:17.782Z" }, { url = "https://files.pythonhosted.org/packages/05/34/c531bc6ac4c21da9ddfddb3107be2287188b3ea4b53b70fc58f2a77ac8d8/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff", size = 201328, upload-time = "2026-03-15T18:52:19.553Z" }, { url = "https://files.pythonhosted.org/packages/fa/73/a5a1e9ca5f234519c1953608a03fe109c306b97fdfb25f09182babad51a7/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e", size = 225302, upload-time = "2026-03-15T18:52:21.043Z" }, { url = "https://files.pythonhosted.org/packages/ba/f6/cd782923d112d296294dea4bcc7af5a7ae0f86ab79f8fefbda5526b6cfc0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659", size = 211127, upload-time = "2026-03-15T18:52:22.491Z" }, { url = "https://files.pythonhosted.org/packages/0e/c5/0b6898950627af7d6103a449b22320372c24c6feda91aa24e201a478d161/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602", size = 222840, upload-time = "2026-03-15T18:52:24.113Z" }, { url = "https://files.pythonhosted.org/packages/7d/25/c4bba773bef442cbdc06111d40daa3de5050a676fa26e85090fc54dd12f0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407", size = 216890, upload-time = "2026-03-15T18:52:25.541Z" }, { url = "https://files.pythonhosted.org/packages/35/1a/05dacadb0978da72ee287b0143097db12f2e7e8d3ffc4647da07a383b0b7/charset_normalizer-3.4.6-cp314-cp314t-win32.whl", hash = "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579", size = 155379, upload-time = "2026-03-15T18:52:27.05Z" }, { url = "https://files.pythonhosted.org/packages/5d/7a/d269d834cb3a76291651256f3b9a5945e81d0a49ab9f4a498964e83c0416/charset_normalizer-3.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4", size = 169043, upload-time = "2026-03-15T18:52:28.502Z" }, { url = "https://files.pythonhosted.org/packages/23/06/28b29fba521a37a8932c6a84192175c34d49f84a6d4773fa63d05f9aff22/charset_normalizer-3.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c", size = 148523, upload-time = "2026-03-15T18:52:29.956Z" }, { url = "https://files.pythonhosted.org/packages/2a/68/687187c7e26cb24ccbd88e5069f5ef00eba804d36dde11d99aad0838ab45/charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69", size = 61455, upload-time = "2026-03-15T18:53:23.833Z" }, ] [[package]] name = "click" version = "8.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] name = "colorlog" version = "6.10.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a2/61/f083b5ac52e505dfc1c624eafbf8c7589a0d7f32daa398d2e7590efa5fda/colorlog-6.10.1.tar.gz", hash = "sha256:eb4ae5cb65fe7fec7773c2306061a8e63e02efc2c72eba9d27b0fa23c94f1321", size = 17162, upload-time = "2025-10-16T16:14:11.978Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/6d/c1/e419ef3723a074172b68aaa89c9f3de486ed4c2399e2dbd8113a4fdcaf9e/colorlog-6.10.1-py3-none-any.whl", hash = "sha256:2d7e8348291948af66122cff006c9f8da6255d224e7cf8e37d8de2df3bad8c9c", size = 11743, upload-time = "2025-10-16T16:14:10.512Z" }, ] [[package]] name = "coverage" version = "7.13.5" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/69/33/e8c48488c29a73fd089f9d71f9653c1be7478f2ad6b5bc870db11a55d23d/coverage-7.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0723d2c96324561b9aa76fb982406e11d93cdb388a7a7da2b16e04719cf7ca5", size = 219255, upload-time = "2026-03-17T10:29:51.081Z" }, { url = "https://files.pythonhosted.org/packages/da/bd/b0ebe9f677d7f4b74a3e115eec7ddd4bcf892074963a00d91e8b164a6386/coverage-7.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52f444e86475992506b32d4e5ca55c24fc88d73bcbda0e9745095b28ef4dc0cf", size = 219772, upload-time = "2026-03-17T10:29:52.867Z" }, { url = "https://files.pythonhosted.org/packages/48/cc/5cb9502f4e01972f54eedd48218bb203fe81e294be606a2bc93970208013/coverage-7.13.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:704de6328e3d612a8f6c07000a878ff38181ec3263d5a11da1db294fa6a9bdf8", size = 246532, upload-time = "2026-03-17T10:29:54.688Z" }, { url = "https://files.pythonhosted.org/packages/7d/d8/3217636d86c7e7b12e126e4f30ef1581047da73140614523af7495ed5f2d/coverage-7.13.5-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a1a6d79a14e1ec1832cabc833898636ad5f3754a678ef8bb4908515208bf84f4", size = 248333, upload-time = "2026-03-17T10:29:56.221Z" }, { url = "https://files.pythonhosted.org/packages/2b/30/2002ac6729ba2d4357438e2ed3c447ad8562866c8c63fc16f6dfc33afe56/coverage-7.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79060214983769c7ba3f0cee10b54c97609dca4d478fa1aa32b914480fd5738d", size = 250211, upload-time = "2026-03-17T10:29:57.938Z" }, { url = "https://files.pythonhosted.org/packages/6c/85/552496626d6b9359eb0e2f86f920037c9cbfba09b24d914c6e1528155f7d/coverage-7.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:356e76b46783a98c2a2fe81ec79df4883a1e62895ea952968fb253c114e7f930", size = 252125, upload-time = "2026-03-17T10:29:59.388Z" }, { url = "https://files.pythonhosted.org/packages/44/21/40256eabdcbccdb6acf6b381b3016a154399a75fe39d406f790ae84d1f3c/coverage-7.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0cef0cdec915d11254a7f549c1170afecce708d30610c6abdded1f74e581666d", size = 247219, upload-time = "2026-03-17T10:30:01.199Z" }, { url = "https://files.pythonhosted.org/packages/b1/e8/96e2a6c3f21a0ea77d7830b254a1542d0328acc8d7bdf6a284ba7e529f77/coverage-7.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dc022073d063b25a402454e5712ef9e007113e3a676b96c5f29b2bda29352f40", size = 248248, upload-time = "2026-03-17T10:30:03.317Z" }, { url = "https://files.pythonhosted.org/packages/da/ba/8477f549e554827da390ec659f3c38e4b6d95470f4daafc2d8ff94eaa9c2/coverage-7.13.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9b74db26dfea4f4e50d48a4602207cd1e78be33182bc9cbf22da94f332f99878", size = 246254, upload-time = "2026-03-17T10:30:04.832Z" }, { url = "https://files.pythonhosted.org/packages/55/59/bc22aef0e6aa179d5b1b001e8b3654785e9adf27ef24c93dc4228ebd5d68/coverage-7.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ad146744ca4fd09b50c482650e3c1b1f4dfa1d4792e0a04a369c7f23336f0400", size = 250067, upload-time = "2026-03-17T10:30:06.535Z" }, { url = "https://files.pythonhosted.org/packages/de/1b/c6a023a160806a5137dca53468fd97530d6acad24a22003b1578a9c2e429/coverage-7.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c555b48be1853fe3997c11c4bd521cdd9a9612352de01fa4508f16ec341e6fe0", size = 246521, upload-time = "2026-03-17T10:30:08.486Z" }, { url = "https://files.pythonhosted.org/packages/2d/3f/3532c85a55aa2f899fa17c186f831cfa1aa434d88ff792a709636f64130e/coverage-7.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7034b5c56a58ae5e85f23949d52c14aca2cfc6848a31764995b7de88f13a1ea0", size = 247126, upload-time = "2026-03-17T10:30:09.966Z" }, { url = "https://files.pythonhosted.org/packages/aa/2e/b9d56af4a24ef45dfbcda88e06870cb7d57b2b0bfa3a888d79b4c8debd76/coverage-7.13.5-cp310-cp310-win32.whl", hash = "sha256:eb7fdf1ef130660e7415e0253a01a7d5a88c9c4d158bcf75cbbd922fd65a5b58", size = 221860, upload-time = "2026-03-17T10:30:11.393Z" }, { url = "https://files.pythonhosted.org/packages/9f/cc/d938417e7a4d7f0433ad4edee8bb2acdc60dc7ac5af19e2a07a048ecbee3/coverage-7.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:3e1bb5f6c78feeb1be3475789b14a0f0a5b47d505bfc7267126ccbd50289999e", size = 222788, upload-time = "2026-03-17T10:30:12.886Z" }, { url = "https://files.pythonhosted.org/packages/4b/37/d24c8f8220ff07b839b2c043ea4903a33b0f455abe673ae3c03bbdb7f212/coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d", size = 219381, upload-time = "2026-03-17T10:30:14.68Z" }, { url = "https://files.pythonhosted.org/packages/35/8b/cd129b0ca4afe886a6ce9d183c44d8301acbd4ef248622e7c49a23145605/coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587", size = 219880, upload-time = "2026-03-17T10:30:16.231Z" }, { url = "https://files.pythonhosted.org/packages/55/2f/e0e5b237bffdb5d6c530ce87cc1d413a5b7d7dfd60fb067ad6d254c35c76/coverage-7.13.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642", size = 250303, upload-time = "2026-03-17T10:30:17.748Z" }, { url = "https://files.pythonhosted.org/packages/92/be/b1afb692be85b947f3401375851484496134c5554e67e822c35f28bf2fbc/coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b", size = 252218, upload-time = "2026-03-17T10:30:19.804Z" }, { url = "https://files.pythonhosted.org/packages/da/69/2f47bb6fa1b8d1e3e5d0c4be8ccb4313c63d742476a619418f85740d597b/coverage-7.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686", size = 254326, upload-time = "2026-03-17T10:30:21.321Z" }, { url = "https://files.pythonhosted.org/packages/d5/d0/79db81da58965bd29dabc8f4ad2a2af70611a57cba9d1ec006f072f30a54/coverage-7.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743", size = 256267, upload-time = "2026-03-17T10:30:23.094Z" }, { url = "https://files.pythonhosted.org/packages/e5/32/d0d7cc8168f91ddab44c0ce4806b969df5f5fdfdbb568eaca2dbc2a04936/coverage-7.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75", size = 250430, upload-time = "2026-03-17T10:30:25.311Z" }, { url = "https://files.pythonhosted.org/packages/4d/06/a055311d891ddbe231cd69fdd20ea4be6e3603ffebddf8704b8ca8e10a3c/coverage-7.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209", size = 252017, upload-time = "2026-03-17T10:30:27.284Z" }, { url = "https://files.pythonhosted.org/packages/d6/f6/d0fd2d21e29a657b5f77a2fe7082e1568158340dceb941954f776dce1b7b/coverage-7.13.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb07647a5738b89baab047f14edd18ded523de60f3b30e75c2acc826f79c839a", size = 250080, upload-time = "2026-03-17T10:30:29.481Z" }, { url = "https://files.pythonhosted.org/packages/4e/ab/0d7fb2efc2e9a5eb7ddcc6e722f834a69b454b7e6e5888c3a8567ecffb31/coverage-7.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e", size = 253843, upload-time = "2026-03-17T10:30:31.301Z" }, { url = "https://files.pythonhosted.org/packages/ba/6f/7467b917bbf5408610178f62a49c0ed4377bb16c1657f689cc61470da8ce/coverage-7.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd", size = 249802, upload-time = "2026-03-17T10:30:33.358Z" }, { url = "https://files.pythonhosted.org/packages/75/2c/1172fb689df92135f5bfbbd69fc83017a76d24ea2e2f3a1154007e2fb9f8/coverage-7.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8", size = 250707, upload-time = "2026-03-17T10:30:35.2Z" }, { url = "https://files.pythonhosted.org/packages/67/21/9ac389377380a07884e3b48ba7a620fcd9dbfaf1d40565facdc6b36ec9ef/coverage-7.13.5-cp311-cp311-win32.whl", hash = "sha256:259b69bb83ad9894c4b25be2528139eecba9a82646ebdda2d9db1ba28424a6bf", size = 221880, upload-time = "2026-03-17T10:30:36.775Z" }, { url = "https://files.pythonhosted.org/packages/af/7f/4cd8a92531253f9d7c1bbecd9fa1b472907fb54446ca768c59b531248dc5/coverage-7.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9", size = 222816, upload-time = "2026-03-17T10:30:38.891Z" }, { url = "https://files.pythonhosted.org/packages/12/a6/1d3f6155fb0010ca68eba7fe48ca6c9da7385058b77a95848710ecf189b1/coverage-7.13.5-cp311-cp311-win_arm64.whl", hash = "sha256:bff95879c33ec8da99fc9b6fe345ddb5be6414b41d6d1ad1c8f188d26f36e028", size = 221483, upload-time = "2026-03-17T10:30:40.463Z" }, { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" }, { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" }, { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" }, { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" }, { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" }, { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" }, { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" }, { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" }, { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" }, { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" }, { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" }, { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" }, { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" }, { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" }, { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" }, { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" }, { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" }, { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" }, { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" }, { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, ] [package.optional-dependencies] toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] [[package]] name = "decorator" version = "5.2.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, ] [[package]] name = "deepdiff" version = "8.7.0" source = { editable = "." } dependencies = [ { name = "orderly-set" }, ] [package.optional-dependencies] cli = [ { name = "click" }, { name = "pyyaml" }, ] coverage = [ { name = "coverage" }, ] dev = [ { name = "bump2version" }, { name = "flit-core" }, { name = "ipdb" }, { name = "jsonpickle" }, { name = "nox" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, { name = "orjson" }, { name = "pandas", version = "2.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "pandas", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "polars" }, { name = "python-dateutil" }, { name = "pytz" }, { name = "tomli" }, { name = "tomli-w" }, { name = "uuid6" }, ] docs = [ { name = "furo" }, { name = "sphinx" }, { name = "sphinx-sitemap" }, { name = "sphinxemoji" }, ] optimize = [ { name = "orjson" }, ] static = [ { name = "flake8" }, { name = "flake8-pyproject" }, { name = "pydantic" }, ] test = [ { name = "pytest" }, { name = "pytest-benchmark" }, { name = "pytest-cov" }, { name = "python-dotenv" }, ] [package.metadata] requires-dist = [ { name = "bump2version", marker = "extra == 'dev'", specifier = "~=1.0.1" }, { name = "click", marker = "extra == 'cli'", specifier = "~=8.3.1" }, { name = "coverage", marker = "extra == 'coverage'", specifier = "~=7.13.5" }, { name = "flake8", marker = "extra == 'static'", specifier = "~=7.3.0" }, { name = "flake8-pyproject", marker = "extra == 'static'", specifier = "~=1.2.4" }, { name = "flit-core", marker = "extra == 'dev'", specifier = "==3.12.0" }, { name = "furo", marker = "extra == 'docs'", specifier = ">=2024.8.6" }, { name = "ipdb", marker = "extra == 'dev'", specifier = "~=0.13.13" }, { name = "jsonpickle", marker = "extra == 'dev'", specifier = "~=4.1.1" }, { name = "nox", marker = "extra == 'dev'", specifier = "==2026.2.9" }, { name = "numpy", marker = "python_full_version >= '3.14' and extra == 'dev'", specifier = "~=2.4.3" }, { name = "numpy", marker = "python_full_version < '3.14' and extra == 'dev'", specifier = "~=2.2.0" }, { name = "orderly-set", specifier = ">=5.5.0,<6" }, { name = "orjson", marker = "extra == 'dev'", specifier = "~=3.11.7" }, { name = "orjson", marker = "extra == 'optimize'" }, { name = "pandas", marker = "python_full_version >= '3.11' and extra == 'dev'", specifier = "~=3.0.1" }, { name = "pandas", marker = "python_full_version < '3.11' and extra == 'dev'", specifier = "~=2.2.0" }, { name = "polars", marker = "extra == 'dev'", specifier = "~=1.39.3" }, { name = "pydantic", marker = "extra == 'static'", specifier = "~=2.12.5" }, { name = "pytest", marker = "extra == 'test'", specifier = "~=9.0.2" }, { name = "pytest-benchmark", marker = "extra == 'test'", specifier = "~=5.2.3" }, { name = "pytest-cov", marker = "extra == 'test'", specifier = "~=7.1.0" }, { name = "python-dateutil", marker = "extra == 'dev'", specifier = "~=2.9.0.post0" }, { name = "python-dotenv", marker = "extra == 'test'", specifier = "~=1.2.2" }, { name = "pytz", marker = "extra == 'dev'" }, { name = "pyyaml", marker = "extra == 'cli'", specifier = "~=6.0.3" }, { name = "sphinx", marker = "extra == 'docs'", specifier = "~=8.1.3" }, { name = "sphinx-sitemap", marker = "extra == 'docs'", specifier = "~=2.9.0" }, { name = "sphinxemoji", marker = "extra == 'docs'", specifier = "~=0.3.2" }, { name = "tomli", marker = "extra == 'dev'", specifier = "~=2.4.0" }, { name = "tomli-w", marker = "extra == 'dev'", specifier = "~=1.2.0" }, { name = "uuid6", marker = "extra == 'dev'", specifier = "==2025.0.1" }, ] provides-extras = ["coverage", "cli", "dev", "docs", "static", "test", "optimize"] [[package]] name = "dependency-groups" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/62/55/f054de99871e7beb81935dea8a10b90cd5ce42122b1c3081d5282fdb3621/dependency_groups-1.3.1.tar.gz", hash = "sha256:78078301090517fd938c19f64a53ce98c32834dfe0dee6b88004a569a6adfefd", size = 10093, upload-time = "2025-05-02T00:34:29.452Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/99/c7/d1ec24fb280caa5a79b6b950db565dab30210a66259d17d5bb2b3a9f878d/dependency_groups-1.3.1-py3-none-any.whl", hash = "sha256:51aeaa0dfad72430fcfb7bcdbefbd75f3792e5919563077f30bc0d73f4493030", size = 8664, upload-time = "2025-05-02T00:34:27.085Z" }, ] [[package]] name = "distlib" version = "0.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, ] [[package]] name = "docutils" version = "0.21.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, ] [[package]] name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, ] [[package]] name = "executing" version = "2.2.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, ] [[package]] name = "filelock" version = "3.25.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/94/b8/00651a0f559862f3bb7d6f7477b192afe3f583cc5e26403b44e59a55ab34/filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", size = 40480, upload-time = "2026-03-11T20:45:38.487Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" }, ] [[package]] name = "flake8" version = "7.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mccabe" }, { name = "pycodestyle" }, { name = "pyflakes" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9b/af/fbfe3c4b5a657d79e5c47a2827a362f9e1b763336a52f926126aa6dc7123/flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872", size = 48326, upload-time = "2025-06-20T19:31:35.838Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e", size = 57922, upload-time = "2025-06-20T19:31:34.425Z" }, ] [[package]] name = "flake8-pyproject" version = "1.2.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "flake8" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/85/6a/cdee9ff7f2b7c6ddc219fd95b7c70c0a3d9f0367a506e9793eedfc72e337/flake8_pyproject-1.2.4-py3-none-any.whl", hash = "sha256:ea34c057f9a9329c76d98723bb2bb498cc6ba8ff9872c4d19932d48c91249a77", size = 5694, upload-time = "2025-11-28T21:40:01.309Z" }, ] [[package]] name = "flit-core" version = "3.12.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/69/59/b6fc2188dfc7ea4f936cd12b49d707f66a1cb7a1d2c16172963534db741b/flit_core-3.12.0.tar.gz", hash = "sha256:18f63100d6f94385c6ed57a72073443e1a71a4acb4339491615d0f16d6ff01b2", size = 53690, upload-time = "2025-03-25T08:03:23.969Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f2/65/b6ba90634c984a4fcc02c7e3afe523fef500c4980fec67cc27536ee50acf/flit_core-3.12.0-py3-none-any.whl", hash = "sha256:e7a0304069ea895172e3c7bb703292e992c5d1555dd1233ab7b5621b5b69e62c", size = 45594, upload-time = "2025-03-25T08:03:20.772Z" }, ] [[package]] name = "furo" version = "2025.12.19" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "accessible-pygments" }, { name = "beautifulsoup4" }, { name = "pygments" }, { name = "sphinx" }, { name = "sphinx-basic-ng" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ec/20/5f5ad4da6a5a27c80f2ed2ee9aee3f9e36c66e56e21c00fde467b2f8f88f/furo-2025.12.19.tar.gz", hash = "sha256:188d1f942037d8b37cd3985b955839fea62baa1730087dc29d157677c857e2a7", size = 1661473, upload-time = "2025-12-19T17:34:40.889Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f4/b2/50e9b292b5cac13e9e81272c7171301abc753a60460d21505b606e15cf21/furo-2025.12.19-py3-none-any.whl", hash = "sha256:bb0ead5309f9500130665a26bee87693c41ce4dbdff864dbfb6b0dae4673d24f", size = 339262, upload-time = "2025-12-19T17:34:38.905Z" }, ] [[package]] name = "humanize" version = "4.15.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ba/66/a3921783d54be8a6870ac4ccffcd15c4dc0dd7fcce51c6d63b8c63935276/humanize-4.15.0.tar.gz", hash = "sha256:1dd098483eb1c7ee8e32eb2e99ad1910baefa4b75c3aff3a82f4d78688993b10", size = 83599, upload-time = "2025-12-20T20:16:13.19Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c5/7b/bca5613a0c3b542420cf92bd5e5fb8ebd5435ce1011a091f66bb7693285e/humanize-4.15.0-py3-none-any.whl", hash = "sha256:b1186eb9f5a9749cd9cb8565aee77919dd7c8d076161cf44d70e59e3301e1769", size = 132203, upload-time = "2025-12-20T20:16:11.67Z" }, ] [[package]] name = "idna" version = "3.11" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] [[package]] name = "imagesize" version = "2.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/6c/e6/7bf14eeb8f8b7251141944835abd42eb20a658d89084b7e1f3e5fe394090/imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3", size = 1773045, upload-time = "2026-03-03T14:18:29.941Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96", size = 9441, upload-time = "2026-03-03T14:18:27.892Z" }, ] [[package]] name = "iniconfig" version = "2.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] [[package]] name = "ipdb" version = "0.13.13" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "decorator" }, { name = "ipython", version = "8.38.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "ipython", version = "9.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "ipython", version = "9.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3d/1b/7e07e7b752017f7693a0f4d41c13e5ca29ce8cbcfdcc1fd6c4ad8c0a27a0/ipdb-0.13.13.tar.gz", hash = "sha256:e3ac6018ef05126d442af680aad863006ec19d02290561ac88b8b1c0b0cfc726", size = 17042, upload-time = "2023-03-09T15:40:57.487Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0c/4c/b075da0092003d9a55cf2ecc1cae9384a1ca4f650d51b00fc59875fe76f6/ipdb-0.13.13-py3-none-any.whl", hash = "sha256:45529994741c4ab6d2388bfa5d7b725c2cf7fe9deffabdb8a6113aa5ed449ed4", size = 12130, upload-time = "2023-03-09T15:40:55.021Z" }, ] [[package]] name = "ipython" version = "8.38.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, { name = "decorator", marker = "python_full_version < '3.11'" }, { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "jedi", marker = "python_full_version < '3.11'" }, { name = "matplotlib-inline", marker = "python_full_version < '3.11'" }, { name = "pexpect", marker = "python_full_version < '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, { name = "prompt-toolkit", marker = "python_full_version < '3.11'" }, { name = "pygments", marker = "python_full_version < '3.11'" }, { name = "stack-data", marker = "python_full_version < '3.11'" }, { name = "traitlets", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e5/61/1810830e8b93c72dcd3c0f150c80a00c3deb229562d9423807ec92c3a539/ipython-8.38.0.tar.gz", hash = "sha256:9cfea8c903ce0867cc2f23199ed8545eb741f3a69420bfcf3743ad1cec856d39", size = 5513996, upload-time = "2026-01-05T10:59:06.901Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9f/df/db59624f4c71b39717c423409950ac3f2c8b2ce4b0aac843112c7fb3f721/ipython-8.38.0-py3-none-any.whl", hash = "sha256:750162629d800ac65bb3b543a14e7a74b0e88063eac9b92124d4b2aa3f6d8e86", size = 831813, upload-time = "2026-01-05T10:59:04.239Z" }, ] [[package]] name = "ipython" version = "9.10.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ { name = "colorama", marker = "python_full_version == '3.11.*' and sys_platform == 'win32'" }, { name = "decorator", marker = "python_full_version == '3.11.*'" }, { name = "ipython-pygments-lexers", marker = "python_full_version == '3.11.*'" }, { name = "jedi", marker = "python_full_version == '3.11.*'" }, { name = "matplotlib-inline", marker = "python_full_version == '3.11.*'" }, { name = "pexpect", marker = "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, { name = "prompt-toolkit", marker = "python_full_version == '3.11.*'" }, { name = "pygments", marker = "python_full_version == '3.11.*'" }, { name = "stack-data", marker = "python_full_version == '3.11.*'" }, { name = "traitlets", marker = "python_full_version == '3.11.*'" }, { name = "typing-extensions", marker = "python_full_version == '3.11.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a6/60/2111715ea11f39b1535bed6024b7dec7918b71e5e5d30855a5b503056b50/ipython-9.10.0.tar.gz", hash = "sha256:cd9e656be97618a0676d058134cd44e6dc7012c0e5cb36a9ce96a8c904adaf77", size = 4426526, upload-time = "2026-02-02T10:00:33.594Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3d/aa/898dec789a05731cd5a9f50605b7b44a72bd198fd0d4528e11fc610177cc/ipython-9.10.0-py3-none-any.whl", hash = "sha256:c6ab68cc23bba8c7e18e9b932797014cc61ea7fd6f19de180ab9ba73e65ee58d", size = 622774, upload-time = "2026-02-02T10:00:31.503Z" }, ] [[package]] name = "ipython" version = "9.11.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, { name = "decorator", marker = "python_full_version >= '3.12'" }, { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.12'" }, { name = "jedi", marker = "python_full_version >= '3.12'" }, { name = "matplotlib-inline", marker = "python_full_version >= '3.12'" }, { name = "pexpect", marker = "python_full_version >= '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, { name = "prompt-toolkit", marker = "python_full_version >= '3.12'" }, { name = "pygments", marker = "python_full_version >= '3.12'" }, { name = "stack-data", marker = "python_full_version >= '3.12'" }, { name = "traitlets", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/86/28/a4698eda5a8928a45d6b693578b135b753e14fa1c2b36ee9441e69a45576/ipython-9.11.0.tar.gz", hash = "sha256:2a94bc4406b22ecc7e4cb95b98450f3ea493a76bec8896cda11b78d7752a6667", size = 4427354, upload-time = "2026-03-05T08:57:30.549Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b2/90/45c72becc57158facc6a6404f663b77bbcea2519ca57f760e2879ae1315d/ipython-9.11.0-py3-none-any.whl", hash = "sha256:6922d5bcf944c6e525a76a0a304451b60a2b6f875e86656d8bc2dfda5d710e19", size = 624222, upload-time = "2026-03-05T08:57:28.94Z" }, ] [[package]] name = "ipython-pygments-lexers" version = "1.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pygments", marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, ] [[package]] name = "jedi" version = "0.19.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "parso" }, ] sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, ] [[package]] name = "jinja2" version = "3.1.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] [[package]] name = "jsonpickle" version = "4.1.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e4/a6/d07afcfdef402900229bcca795f80506b207af13a838d4d99ad45abf530c/jsonpickle-4.1.1.tar.gz", hash = "sha256:f86e18f13e2b96c1c1eede0b7b90095bbb61d99fedc14813c44dc2f361dbbae1", size = 316885, upload-time = "2025-06-02T20:36:11.57Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c1/73/04df8a6fa66d43a9fd45c30f283cc4afff17da671886e451d52af60bdc7e/jsonpickle-4.1.1-py3-none-any.whl", hash = "sha256:bb141da6057898aa2438ff268362b126826c812a1721e31cf08a6e142910dc91", size = 47125, upload-time = "2025-06-02T20:36:08.647Z" }, ] [[package]] name = "markupsafe" version = "3.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] [[package]] name = "matplotlib-inline" version = "0.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "traitlets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" }, ] [[package]] name = "mccabe" version = "0.7.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, ] [[package]] name = "nox" version = "2026.2.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "argcomplete" }, { name = "attrs" }, { name = "colorlog" }, { name = "dependency-groups" }, { name = "humanize" }, { name = "packaging" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "virtualenv" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6e/8e/55a9679b31f1efc48facedd2448eb53c7f1e647fb592aa1403c9dd7a4590/nox-2026.2.9.tar.gz", hash = "sha256:1bc8a202ee8cd69be7aaada63b2a7019126899a06fc930a7aee75585bf8ee41b", size = 4031165, upload-time = "2026-02-10T04:38:58.878Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8d/58/0d5e5a044f1868bdc45f38afdc2d90ff9867ce398b4e8fa9e666bfc9bfba/nox-2026.2.9-py3-none-any.whl", hash = "sha256:1b7143bc8ecdf25f2353201326152c5303ae4ae56ca097b1fb6179ad75164c47", size = 74615, upload-time = "2026-02-10T04:38:57.266Z" }, ] [[package]] name = "numpy" version = "2.2.6" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version < '3.11'", ] sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, ] [[package]] name = "numpy" version = "2.4.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] sdist = { url = "https://files.pythonhosted.org/packages/10/8b/c265f4823726ab832de836cdd184d0986dcf94480f81e8739692a7ac7af2/numpy-2.4.3.tar.gz", hash = "sha256:483a201202b73495f00dbc83796c6ae63137a9bdade074f7648b3e32613412dd", size = 20727743, upload-time = "2026-03-09T07:58:53.426Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f9/51/5093a2df15c4dc19da3f79d1021e891f5dcf1d9d1db6ba38891d5590f3fe/numpy-2.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:33b3bf58ee84b172c067f56aeadc7ee9ab6de69c5e800ab5b10295d54c581adb", size = 16957183, upload-time = "2026-03-09T07:55:57.774Z" }, { url = "https://files.pythonhosted.org/packages/b5/7c/c061f3de0630941073d2598dc271ac2f6cbcf5c83c74a5870fea07488333/numpy-2.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8ba7b51e71c05aa1f9bc3641463cd82308eab40ce0d5c7e1fd4038cbf9938147", size = 14968734, upload-time = "2026-03-09T07:56:00.494Z" }, { url = "https://files.pythonhosted.org/packages/ef/27/d26c85cbcd86b26e4f125b0668e7a7c0542d19dd7d23ee12e87b550e95b5/numpy-2.4.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a1988292870c7cb9d0ebb4cc96b4d447513a9644801de54606dc7aabf2b7d920", size = 5475288, upload-time = "2026-03-09T07:56:02.857Z" }, { url = "https://files.pythonhosted.org/packages/2b/09/3c4abbc1dcd8010bf1a611d174c7aa689fc505585ec806111b4406f6f1b1/numpy-2.4.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:23b46bb6d8ecb68b58c09944483c135ae5f0e9b8d8858ece5e4ead783771d2a9", size = 6805253, upload-time = "2026-03-09T07:56:04.53Z" }, { url = "https://files.pythonhosted.org/packages/21/bc/e7aa3f6817e40c3f517d407742337cbb8e6fc4b83ce0b55ab780c829243b/numpy-2.4.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a016db5c5dba78fa8fe9f5d80d6708f9c42ab087a739803c0ac83a43d686a470", size = 15969479, upload-time = "2026-03-09T07:56:06.638Z" }, { url = "https://files.pythonhosted.org/packages/78/51/9f5d7a41f0b51649ddf2f2320595e15e122a40610b233d51928dd6c92353/numpy-2.4.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:715de7f82e192e8cae5a507a347d97ad17598f8e026152ca97233e3666daaa71", size = 16901035, upload-time = "2026-03-09T07:56:09.405Z" }, { url = "https://files.pythonhosted.org/packages/64/6e/b221dd847d7181bc5ee4857bfb026182ef69499f9305eb1371cbb1aea626/numpy-2.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2ddb7919366ee468342b91dea2352824c25b55814a987847b6c52003a7c97f15", size = 17325657, upload-time = "2026-03-09T07:56:12.067Z" }, { url = "https://files.pythonhosted.org/packages/eb/b8/8f3fd2da596e1063964b758b5e3c970aed1949a05200d7e3d46a9d46d643/numpy-2.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a315e5234d88067f2d97e1f2ef670a7569df445d55400f1e33d117418d008d52", size = 18635512, upload-time = "2026-03-09T07:56:14.629Z" }, { url = "https://files.pythonhosted.org/packages/5c/24/2993b775c37e39d2f8ab4125b44337ab0b2ba106c100980b7c274a22bee7/numpy-2.4.3-cp311-cp311-win32.whl", hash = "sha256:2b3f8d2c4589b1a2028d2a770b0fc4d1f332fb5e01521f4de3199a896d158ddd", size = 6238100, upload-time = "2026-03-09T07:56:17.243Z" }, { url = "https://files.pythonhosted.org/packages/76/1d/edccf27adedb754db7c4511d5eac8b83f004ae948fe2d3509e8b78097d4c/numpy-2.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:77e76d932c49a75617c6d13464e41203cd410956614d0a0e999b25e9e8d27eec", size = 12609816, upload-time = "2026-03-09T07:56:19.089Z" }, { url = "https://files.pythonhosted.org/packages/92/82/190b99153480076c8dce85f4cfe7d53ea84444145ffa54cb58dcd460d66b/numpy-2.4.3-cp311-cp311-win_arm64.whl", hash = "sha256:eb610595dd91560905c132c709412b512135a60f1851ccbd2c959e136431ff67", size = 10485757, upload-time = "2026-03-09T07:56:21.753Z" }, { url = "https://files.pythonhosted.org/packages/a9/ed/6388632536f9788cea23a3a1b629f25b43eaacd7d7377e5d6bc7b9deb69b/numpy-2.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:61b0cbabbb6126c8df63b9a3a0c4b1f44ebca5e12ff6997b80fcf267fb3150ef", size = 16669628, upload-time = "2026-03-09T07:56:24.252Z" }, { url = "https://files.pythonhosted.org/packages/74/1b/ee2abfc68e1ce728b2958b6ba831d65c62e1b13ce3017c13943f8f9b5b2e/numpy-2.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7395e69ff32526710748f92cd8c9849b361830968ea3e24a676f272653e8983e", size = 14696872, upload-time = "2026-03-09T07:56:26.991Z" }, { url = "https://files.pythonhosted.org/packages/ba/d1/780400e915ff5638166f11ca9dc2c5815189f3d7cf6f8759a1685e586413/numpy-2.4.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:abdce0f71dcb4a00e4e77f3faf05e4616ceccfe72ccaa07f47ee79cda3b7b0f4", size = 5203489, upload-time = "2026-03-09T07:56:29.414Z" }, { url = "https://files.pythonhosted.org/packages/0b/bb/baffa907e9da4cc34a6e556d6d90e032f6d7a75ea47968ea92b4858826c4/numpy-2.4.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:48da3a4ee1336454b07497ff7ec83903efa5505792c4e6d9bf83d99dc07a1e18", size = 6550814, upload-time = "2026-03-09T07:56:32.225Z" }, { url = "https://files.pythonhosted.org/packages/7b/12/8c9f0c6c95f76aeb20fc4a699c33e9f827fa0d0f857747c73bb7b17af945/numpy-2.4.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32e3bef222ad6b052280311d1d60db8e259e4947052c3ae7dd6817451fc8a4c5", size = 15666601, upload-time = "2026-03-09T07:56:34.461Z" }, { url = "https://files.pythonhosted.org/packages/bd/79/cc665495e4d57d0aa6fbcc0aa57aa82671dfc78fbf95fe733ed86d98f52a/numpy-2.4.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7dd01a46700b1967487141a66ac1a3cf0dd8ebf1f08db37d46389401512ca97", size = 16621358, upload-time = "2026-03-09T07:56:36.852Z" }, { url = "https://files.pythonhosted.org/packages/a8/40/b4ecb7224af1065c3539f5ecfff879d090de09608ad1008f02c05c770cb3/numpy-2.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:76f0f283506c28b12bba319c0fab98217e9f9b54e6160e9c79e9f7348ba32e9c", size = 17016135, upload-time = "2026-03-09T07:56:39.337Z" }, { url = "https://files.pythonhosted.org/packages/f7/b1/6a88e888052eed951afed7a142dcdf3b149a030ca59b4c71eef085858e43/numpy-2.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737f630a337364665aba3b5a77e56a68cc42d350edd010c345d65a3efa3addcc", size = 18345816, upload-time = "2026-03-09T07:56:42.31Z" }, { url = "https://files.pythonhosted.org/packages/f3/8f/103a60c5f8c3d7fc678c19cd7b2476110da689ccb80bc18050efbaeae183/numpy-2.4.3-cp312-cp312-win32.whl", hash = "sha256:26952e18d82a1dbbc2f008d402021baa8d6fc8e84347a2072a25e08b46d698b9", size = 5960132, upload-time = "2026-03-09T07:56:44.851Z" }, { url = "https://files.pythonhosted.org/packages/d7/7c/f5ee1bf6ed888494978046a809df2882aad35d414b622893322df7286879/numpy-2.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:65f3c2455188f09678355f5cae1f959a06b778bc66d535da07bf2ef20cd319d5", size = 12316144, upload-time = "2026-03-09T07:56:47.057Z" }, { url = "https://files.pythonhosted.org/packages/71/46/8d1cb3f7a00f2fb6394140e7e6623696e54c6318a9d9691bb4904672cf42/numpy-2.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:2abad5c7fef172b3377502bde47892439bae394a71bc329f31df0fd829b41a9e", size = 10220364, upload-time = "2026-03-09T07:56:49.849Z" }, { url = "https://files.pythonhosted.org/packages/b6/d0/1fe47a98ce0df229238b77611340aff92d52691bcbc10583303181abf7fc/numpy-2.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b346845443716c8e542d54112966383b448f4a3ba5c66409771b8c0889485dd3", size = 16665297, upload-time = "2026-03-09T07:56:52.296Z" }, { url = "https://files.pythonhosted.org/packages/27/d9/4e7c3f0e68dfa91f21c6fb6cf839bc829ec920688b1ce7ec722b1a6202fb/numpy-2.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2629289168f4897a3c4e23dc98d6f1731f0fc0fe52fb9db19f974041e4cc12b9", size = 14691853, upload-time = "2026-03-09T07:56:54.992Z" }, { url = "https://files.pythonhosted.org/packages/3a/66/bd096b13a87549683812b53ab211e6d413497f84e794fb3c39191948da97/numpy-2.4.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:bb2e3cf95854233799013779216c57e153c1ee67a0bf92138acca0e429aefaee", size = 5198435, upload-time = "2026-03-09T07:56:57.184Z" }, { url = "https://files.pythonhosted.org/packages/a2/2f/687722910b5a5601de2135c891108f51dfc873d8e43c8ed9f4ebb440b4a2/numpy-2.4.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:7f3408ff897f8ab07a07fbe2823d7aee6ff644c097cc1f90382511fe982f647f", size = 6546347, upload-time = "2026-03-09T07:56:59.531Z" }, { url = "https://files.pythonhosted.org/packages/bf/ec/7971c4e98d86c564750393fab8d7d83d0a9432a9d78bb8a163a6dc59967a/numpy-2.4.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:decb0eb8a53c3b009b0962378065589685d66b23467ef5dac16cbe818afde27f", size = 15664626, upload-time = "2026-03-09T07:57:01.385Z" }, { url = "https://files.pythonhosted.org/packages/7e/eb/7daecbea84ec935b7fc732e18f532073064a3816f0932a40a17f3349185f/numpy-2.4.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5f51900414fc9204a0e0da158ba2ac52b75656e7dce7e77fb9f84bfa343b4cc", size = 16608916, upload-time = "2026-03-09T07:57:04.008Z" }, { url = "https://files.pythonhosted.org/packages/df/58/2a2b4a817ffd7472dca4421d9f0776898b364154e30c95f42195041dc03b/numpy-2.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6bd06731541f89cdc01b261ba2c9e037f1543df7472517836b78dfb15bd6e476", size = 17015824, upload-time = "2026-03-09T07:57:06.347Z" }, { url = "https://files.pythonhosted.org/packages/4a/ca/627a828d44e78a418c55f82dd4caea8ea4a8ef24e5144d9e71016e52fb40/numpy-2.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22654fe6be0e5206f553a9250762c653d3698e46686eee53b399ab90da59bd92", size = 18334581, upload-time = "2026-03-09T07:57:09.114Z" }, { url = "https://files.pythonhosted.org/packages/cd/c0/76f93962fc79955fcba30a429b62304332345f22d4daec1cb33653425643/numpy-2.4.3-cp313-cp313-win32.whl", hash = "sha256:d71e379452a2f670ccb689ec801b1218cd3983e253105d6e83780967e899d687", size = 5958618, upload-time = "2026-03-09T07:57:11.432Z" }, { url = "https://files.pythonhosted.org/packages/b1/3c/88af0040119209b9b5cb59485fa48b76f372c73068dbf9254784b975ac53/numpy-2.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:0a60e17a14d640f49146cb38e3f105f571318db7826d9b6fef7e4dce758faecd", size = 12312824, upload-time = "2026-03-09T07:57:13.586Z" }, { url = "https://files.pythonhosted.org/packages/58/ce/3d07743aced3d173f877c3ef6a454c2174ba42b584ab0b7e6d99374f51ed/numpy-2.4.3-cp313-cp313-win_arm64.whl", hash = "sha256:c9619741e9da2059cd9c3f206110b97583c7152c1dc9f8aafd4beb450ac1c89d", size = 10221218, upload-time = "2026-03-09T07:57:16.183Z" }, { url = "https://files.pythonhosted.org/packages/62/09/d96b02a91d09e9d97862f4fc8bfebf5400f567d8eb1fe4b0cc4795679c15/numpy-2.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7aa4e54f6469300ebca1d9eb80acd5253cdfa36f2c03d79a35883687da430875", size = 14819570, upload-time = "2026-03-09T07:57:18.564Z" }, { url = "https://files.pythonhosted.org/packages/b5/ca/0b1aba3905fdfa3373d523b2b15b19029f4f3031c87f4066bd9d20ef6c6b/numpy-2.4.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d1b90d840b25874cf5cd20c219af10bac3667db3876d9a495609273ebe679070", size = 5326113, upload-time = "2026-03-09T07:57:21.052Z" }, { url = "https://files.pythonhosted.org/packages/c0/63/406e0fd32fcaeb94180fd6a4c41e55736d676c54346b7efbce548b94a914/numpy-2.4.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a749547700de0a20a6718293396ec237bb38218049cfce788e08fcb716e8cf73", size = 6646370, upload-time = "2026-03-09T07:57:22.804Z" }, { url = "https://files.pythonhosted.org/packages/b6/d0/10f7dc157d4b37af92720a196be6f54f889e90dcd30dce9dc657ed92c257/numpy-2.4.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f3c4a151a2e529adf49c1d54f0f57ff8f9b233ee4d44af623a81553ab86368", size = 15723499, upload-time = "2026-03-09T07:57:24.693Z" }, { url = "https://files.pythonhosted.org/packages/66/f1/d1c2bf1161396629701bc284d958dc1efa3a5a542aab83cf11ee6eb4cba5/numpy-2.4.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22c31dc07025123aedf7f2db9e91783df13f1776dc52c6b22c620870dc0fab22", size = 16657164, upload-time = "2026-03-09T07:57:27.676Z" }, { url = "https://files.pythonhosted.org/packages/1a/be/cca19230b740af199ac47331a21c71e7a3d0ba59661350483c1600d28c37/numpy-2.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:148d59127ac95979d6f07e4d460f934ebdd6eed641db9c0db6c73026f2b2101a", size = 17081544, upload-time = "2026-03-09T07:57:30.664Z" }, { url = "https://files.pythonhosted.org/packages/b9/c5/9602b0cbb703a0936fb40f8a95407e8171935b15846de2f0776e08af04c7/numpy-2.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a97cbf7e905c435865c2d939af3d93f99d18eaaa3cabe4256f4304fb51604349", size = 18380290, upload-time = "2026-03-09T07:57:33.763Z" }, { url = "https://files.pythonhosted.org/packages/ed/81/9f24708953cd30be9ee36ec4778f4b112b45165812f2ada4cc5ea1c1f254/numpy-2.4.3-cp313-cp313t-win32.whl", hash = "sha256:be3b8487d725a77acccc9924f65fd8bce9af7fac8c9820df1049424a2115af6c", size = 6082814, upload-time = "2026-03-09T07:57:36.491Z" }, { url = "https://files.pythonhosted.org/packages/e2/9e/52f6eaa13e1a799f0ab79066c17f7016a4a8ae0c1aefa58c82b4dab690b4/numpy-2.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1ec84fd7c8e652b0f4aaaf2e6e9cc8eaa9b1b80a537e06b2e3a2fb176eedcb26", size = 12452673, upload-time = "2026-03-09T07:57:38.281Z" }, { url = "https://files.pythonhosted.org/packages/c4/04/b8cece6ead0b30c9fbd99bb835ad7ea0112ac5f39f069788c5558e3b1ab2/numpy-2.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:120df8c0a81ebbf5b9020c91439fccd85f5e018a927a39f624845be194a2be02", size = 10290907, upload-time = "2026-03-09T07:57:40.747Z" }, { url = "https://files.pythonhosted.org/packages/70/ae/3936f79adebf8caf81bd7a599b90a561334a658be4dcc7b6329ebf4ee8de/numpy-2.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5884ce5c7acfae1e4e1b6fde43797d10aa506074d25b531b4f54bde33c0c31d4", size = 16664563, upload-time = "2026-03-09T07:57:43.817Z" }, { url = "https://files.pythonhosted.org/packages/9b/62/760f2b55866b496bb1fa7da2a6db076bef908110e568b02fcfc1422e2a3a/numpy-2.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:297837823f5bc572c5f9379b0c9f3a3365f08492cbdc33bcc3af174372ebb168", size = 14702161, upload-time = "2026-03-09T07:57:46.169Z" }, { url = "https://files.pythonhosted.org/packages/32/af/a7a39464e2c0a21526fb4fb76e346fb172ebc92f6d1c7a07c2c139cc17b1/numpy-2.4.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:a111698b4a3f8dcbe54c64a7708f049355abd603e619013c346553c1fd4ca90b", size = 5208738, upload-time = "2026-03-09T07:57:48.506Z" }, { url = "https://files.pythonhosted.org/packages/29/8c/2a0cf86a59558fa078d83805589c2de490f29ed4fb336c14313a161d358a/numpy-2.4.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:4bd4741a6a676770e0e97fe9ab2e51de01183df3dcbcec591d26d331a40de950", size = 6543618, upload-time = "2026-03-09T07:57:50.591Z" }, { url = "https://files.pythonhosted.org/packages/aa/b8/612ce010c0728b1c363fa4ea3aa4c22fe1c5da1de008486f8c2f5cb92fae/numpy-2.4.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54f29b877279d51e210e0c80709ee14ccbbad647810e8f3d375561c45ef613dd", size = 15680676, upload-time = "2026-03-09T07:57:52.34Z" }, { url = "https://files.pythonhosted.org/packages/a9/7e/4f120ecc54ba26ddf3dc348eeb9eb063f421de65c05fc961941798feea18/numpy-2.4.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:679f2a834bae9020f81534671c56fd0cc76dd7e5182f57131478e23d0dc59e24", size = 16613492, upload-time = "2026-03-09T07:57:54.91Z" }, { url = "https://files.pythonhosted.org/packages/2c/86/1b6020db73be330c4b45d5c6ee4295d59cfeef0e3ea323959d053e5a6909/numpy-2.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d84f0f881cb2225c2dfd7f78a10a5645d487a496c6668d6cc39f0f114164f3d0", size = 17031789, upload-time = "2026-03-09T07:57:57.641Z" }, { url = "https://files.pythonhosted.org/packages/07/3a/3b90463bf41ebc21d1b7e06079f03070334374208c0f9a1f05e4ae8455e7/numpy-2.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d213c7e6e8d211888cc359bab7199670a00f5b82c0978b9d1c75baf1eddbeac0", size = 18339941, upload-time = "2026-03-09T07:58:00.577Z" }, { url = "https://files.pythonhosted.org/packages/a8/74/6d736c4cd962259fd8bae9be27363eb4883a2f9069763747347544c2a487/numpy-2.4.3-cp314-cp314-win32.whl", hash = "sha256:52077feedeff7c76ed7c9f1a0428558e50825347b7545bbb8523da2cd55c547a", size = 6007503, upload-time = "2026-03-09T07:58:03.331Z" }, { url = "https://files.pythonhosted.org/packages/48/39/c56ef87af669364356bb011922ef0734fc49dad51964568634c72a009488/numpy-2.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:0448e7f9caefb34b4b7dd2b77f21e8906e5d6f0365ad525f9f4f530b13df2afc", size = 12444915, upload-time = "2026-03-09T07:58:06.353Z" }, { url = "https://files.pythonhosted.org/packages/9d/1f/ab8528e38d295fd349310807496fabb7cf9fe2e1f70b97bc20a483ea9d4a/numpy-2.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:b44fd60341c4d9783039598efadd03617fa28d041fc37d22b62d08f2027fa0e7", size = 10494875, upload-time = "2026-03-09T07:58:08.734Z" }, { url = "https://files.pythonhosted.org/packages/e6/ef/b7c35e4d5ef141b836658ab21a66d1a573e15b335b1d111d31f26c8ef80f/numpy-2.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0a195f4216be9305a73c0e91c9b026a35f2161237cf1c6de9b681637772ea657", size = 14822225, upload-time = "2026-03-09T07:58:11.034Z" }, { url = "https://files.pythonhosted.org/packages/cd/8d/7730fa9278cf6648639946cc816e7cc89f0d891602584697923375f801ed/numpy-2.4.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:cd32fbacb9fd1bf041bf8e89e4576b6f00b895f06d00914820ae06a616bdfef7", size = 5328769, upload-time = "2026-03-09T07:58:13.67Z" }, { url = "https://files.pythonhosted.org/packages/47/01/d2a137317c958b074d338807c1b6a383406cdf8b8e53b075d804cc3d211d/numpy-2.4.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:2e03c05abaee1f672e9d67bc858f300b5ccba1c21397211e8d77d98350972093", size = 6649461, upload-time = "2026-03-09T07:58:15.912Z" }, { url = "https://files.pythonhosted.org/packages/5c/34/812ce12bc0f00272a4b0ec0d713cd237cb390666eb6206323d1cc9cedbb2/numpy-2.4.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d1ce23cce91fcea443320a9d0ece9b9305d4368875bab09538f7a5b4131938a", size = 15725809, upload-time = "2026-03-09T07:58:17.787Z" }, { url = "https://files.pythonhosted.org/packages/25/c0/2aed473a4823e905e765fee3dc2cbf504bd3e68ccb1150fbdabd5c39f527/numpy-2.4.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c59020932feb24ed49ffd03704fbab89f22aa9c0d4b180ff45542fe8918f5611", size = 16655242, upload-time = "2026-03-09T07:58:20.476Z" }, { url = "https://files.pythonhosted.org/packages/f2/c8/7e052b2fc87aa0e86de23f20e2c42bd261c624748aa8efd2c78f7bb8d8c6/numpy-2.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9684823a78a6cd6ad7511fc5e25b07947d1d5b5e2812c93fe99d7d4195130720", size = 17080660, upload-time = "2026-03-09T07:58:23.067Z" }, { url = "https://files.pythonhosted.org/packages/f3/3d/0876746044db2adcb11549f214d104f2e1be00f07a67edbb4e2812094847/numpy-2.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0200b25c687033316fb39f0ff4e3e690e8957a2c3c8d22499891ec58c37a3eb5", size = 18380384, upload-time = "2026-03-09T07:58:25.839Z" }, { url = "https://files.pythonhosted.org/packages/07/12/8160bea39da3335737b10308df4f484235fd297f556745f13092aa039d3b/numpy-2.4.3-cp314-cp314t-win32.whl", hash = "sha256:5e10da9e93247e554bb1d22f8edc51847ddd7dde52d85ce31024c1b4312bfba0", size = 6154547, upload-time = "2026-03-09T07:58:28.289Z" }, { url = "https://files.pythonhosted.org/packages/42/f3/76534f61f80d74cc9cdf2e570d3d4eeb92c2280a27c39b0aaf471eda7b48/numpy-2.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:45f003dbdffb997a03da2d1d0cb41fbd24a87507fb41605c0420a3db5bd4667b", size = 12633645, upload-time = "2026-03-09T07:58:30.384Z" }, { url = "https://files.pythonhosted.org/packages/1f/b6/7c0d4334c15983cec7f92a69e8ce9b1e6f31857e5ee3a413ac424e6bd63d/numpy-2.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:4d382735cecd7bcf090172489a525cd7d4087bc331f7df9f60ddc9a296cf208e", size = 10565454, upload-time = "2026-03-09T07:58:33.031Z" }, { url = "https://files.pythonhosted.org/packages/64/e4/4dab9fb43c83719c29241c535d9e07be73bea4bc0c6686c5816d8e1b6689/numpy-2.4.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c6b124bfcafb9e8d3ed09130dbee44848c20b3e758b6bbf006e641778927c028", size = 16834892, upload-time = "2026-03-09T07:58:35.334Z" }, { url = "https://files.pythonhosted.org/packages/c9/29/f8b6d4af90fed3dfda84ebc0df06c9833d38880c79ce954e5b661758aa31/numpy-2.4.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:76dbb9d4e43c16cf9aa711fcd8de1e2eeb27539dcefb60a1d5e9f12fae1d1ed8", size = 14893070, upload-time = "2026-03-09T07:58:37.7Z" }, { url = "https://files.pythonhosted.org/packages/9a/04/a19b3c91dbec0a49269407f15d5753673a09832daed40c45e8150e6fa558/numpy-2.4.3-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:29363fbfa6f8ee855d7569c96ce524845e3d726d6c19b29eceec7dd555dab152", size = 5399609, upload-time = "2026-03-09T07:58:39.853Z" }, { url = "https://files.pythonhosted.org/packages/79/34/4d73603f5420eab89ea8a67097b31364bf7c30f811d4dd84b1659c7476d9/numpy-2.4.3-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:bc71942c789ef415a37f0d4eab90341425a00d538cd0642445d30b41023d3395", size = 6714355, upload-time = "2026-03-09T07:58:42.365Z" }, { url = "https://files.pythonhosted.org/packages/58/ad/1100d7229bb248394939a12a8074d485b655e8ed44207d328fdd7fcebc7b/numpy-2.4.3-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e58765ad74dcebd3ef0208a5078fba32dc8ec3578fe84a604432950cd043d79", size = 15800434, upload-time = "2026-03-09T07:58:44.837Z" }, { url = "https://files.pythonhosted.org/packages/0c/fd/16d710c085d28ba4feaf29ac60c936c9d662e390344f94a6beaa2ac9899b/numpy-2.4.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e236dbda4e1d319d681afcbb136c0c4a8e0f1a5c58ceec2adebb547357fe857", size = 16729409, upload-time = "2026-03-09T07:58:47.972Z" }, { url = "https://files.pythonhosted.org/packages/57/a7/b35835e278c18b85206834b3aa3abe68e77a98769c59233d1f6300284781/numpy-2.4.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4b42639cdde6d24e732ff823a3fa5b701d8acad89c4142bc1d0bd6dc85200ba5", size = 12504685, upload-time = "2026-03-09T07:58:50.525Z" }, ] [[package]] name = "orderly-set" version = "5.5.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/4a/88/39c83c35d5e97cc203e9e77a4f93bf87ec89cf6a22ac4818fdcc65d66584/orderly_set-5.5.0.tar.gz", hash = "sha256:e87185c8e4d8afa64e7f8160ee2c542a475b738bc891dc3f58102e654125e6ce", size = 27414, upload-time = "2025-07-10T20:10:55.885Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/12/27/fb8d7338b4d551900fa3e580acbe7a0cf655d940e164cb5c00ec31961094/orderly_set-5.5.0-py3-none-any.whl", hash = "sha256:46f0b801948e98f427b412fcabb831677194c05c3b699b80de260374baa0b1e7", size = 13068, upload-time = "2025-07-10T20:10:54.377Z" }, ] [[package]] name = "orjson" version = "3.11.7" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/53/45/b268004f745ede84e5798b48ee12b05129d19235d0e15267aa57dcdb400b/orjson-3.11.7.tar.gz", hash = "sha256:9b1a67243945819ce55d24a30b59d6a168e86220452d2c96f4d1f093e71c0c49", size = 6144992, upload-time = "2026-02-02T15:38:49.29Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/de/1a/a373746fa6d0e116dd9e54371a7b54622c44d12296d5d0f3ad5e3ff33490/orjson-3.11.7-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a02c833f38f36546ba65a452127633afce4cf0dd7296b753d3bb54e55e5c0174", size = 229140, upload-time = "2026-02-02T15:37:06.082Z" }, { url = "https://files.pythonhosted.org/packages/52/a2/fa129e749d500f9b183e8a3446a193818a25f60261e9ce143ad61e975208/orjson-3.11.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b63c6e6738d7c3470ad01601e23376aa511e50e1f3931395b9f9c722406d1a67", size = 128670, upload-time = "2026-02-02T15:37:08.002Z" }, { url = "https://files.pythonhosted.org/packages/08/93/1e82011cd1e0bd051ef9d35bed1aa7fb4ea1f0a055dc2c841b46b43a9ebd/orjson-3.11.7-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:043d3006b7d32c7e233b8cfb1f01c651013ea079e08dcef7189a29abd8befe11", size = 123832, upload-time = "2026-02-02T15:37:09.191Z" }, { url = "https://files.pythonhosted.org/packages/fe/d8/a26b431ef962c7d55736674dddade876822f3e33223c1f47a36879350d04/orjson-3.11.7-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57036b27ac8a25d81112eb0cc9835cd4833c5b16e1467816adc0015f59e870dc", size = 129171, upload-time = "2026-02-02T15:37:11.112Z" }, { url = "https://files.pythonhosted.org/packages/a7/19/f47819b84a580f490da260c3ee9ade214cf4cf78ac9ce8c1c758f80fdfc9/orjson-3.11.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:733ae23ada68b804b222c44affed76b39e30806d38660bf1eb200520d259cc16", size = 141967, upload-time = "2026-02-02T15:37:12.282Z" }, { url = "https://files.pythonhosted.org/packages/5b/cd/37ece39a0777ba077fdcdbe4cccae3be8ed00290c14bf8afdc548befc260/orjson-3.11.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5fdfad2093bdd08245f2e204d977facd5f871c88c4a71230d5bcbd0e43bf6222", size = 130991, upload-time = "2026-02-02T15:37:13.465Z" }, { url = "https://files.pythonhosted.org/packages/8f/ed/f2b5d66aa9b6b5c02ff5f120efc7b38c7c4962b21e6be0f00fd99a5c348e/orjson-3.11.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cededd6738e1c153530793998e31c05086582b08315db48ab66649768f326baa", size = 133674, upload-time = "2026-02-02T15:37:14.694Z" }, { url = "https://files.pythonhosted.org/packages/c4/6e/baa83e68d1aa09fa8c3e5b2c087d01d0a0bd45256de719ed7bc22c07052d/orjson-3.11.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:14f440c7268c8f8633d1b3d443a434bd70cb15686117ea6beff8fdc8f5917a1e", size = 138722, upload-time = "2026-02-02T15:37:16.501Z" }, { url = "https://files.pythonhosted.org/packages/0c/47/7f8ef4963b772cd56999b535e553f7eb5cd27e9dd6c049baee6f18bfa05d/orjson-3.11.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3a2479753bbb95b0ebcf7969f562cdb9668e6d12416a35b0dda79febf89cdea2", size = 409056, upload-time = "2026-02-02T15:37:17.895Z" }, { url = "https://files.pythonhosted.org/packages/38/eb/2df104dd2244b3618f25325a656f85cc3277f74bbd91224752410a78f3c7/orjson-3.11.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:71924496986275a737f38e3f22b4e0878882b3f7a310d2ff4dc96e812789120c", size = 144196, upload-time = "2026-02-02T15:37:19.349Z" }, { url = "https://files.pythonhosted.org/packages/b6/2a/ee41de0aa3a6686598661eae2b4ebdff1340c65bfb17fcff8b87138aab21/orjson-3.11.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4a9eefdc70bf8bf9857f0290f973dec534ac84c35cd6a7f4083be43e7170a8f", size = 134979, upload-time = "2026-02-02T15:37:20.906Z" }, { url = "https://files.pythonhosted.org/packages/4c/fa/92fc5d3d402b87a8b28277a9ed35386218a6a5287c7fe5ee9b9f02c53fb2/orjson-3.11.7-cp310-cp310-win32.whl", hash = "sha256:ae9e0b37a834cef7ce8f99de6498f8fad4a2c0bf6bfc3d02abd8ed56aa15b2de", size = 127968, upload-time = "2026-02-02T15:37:23.178Z" }, { url = "https://files.pythonhosted.org/packages/07/29/a576bf36d73d60df06904d3844a9df08e25d59eba64363aaf8ec2f9bff41/orjson-3.11.7-cp310-cp310-win_amd64.whl", hash = "sha256:d772afdb22555f0c58cfc741bdae44180122b3616faa1ecadb595cd526e4c993", size = 125128, upload-time = "2026-02-02T15:37:24.329Z" }, { url = "https://files.pythonhosted.org/packages/37/02/da6cb01fc6087048d7f61522c327edf4250f1683a58a839fdcc435746dd5/orjson-3.11.7-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9487abc2c2086e7c8eb9a211d2ce8855bae0e92586279d0d27b341d5ad76c85c", size = 228664, upload-time = "2026-02-02T15:37:25.542Z" }, { url = "https://files.pythonhosted.org/packages/c1/c2/5885e7a5881dba9a9af51bc564e8967225a642b3e03d089289a35054e749/orjson-3.11.7-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:79cacb0b52f6004caf92405a7e1f11e6e2de8bdf9019e4f76b44ba045125cd6b", size = 125344, upload-time = "2026-02-02T15:37:26.92Z" }, { url = "https://files.pythonhosted.org/packages/a4/1d/4e7688de0a92d1caf600dfd5fb70b4c5bfff51dfa61ac555072ef2d0d32a/orjson-3.11.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2e85fe4698b6a56d5e2ebf7ae87544d668eb6bde1ad1226c13f44663f20ec9e", size = 128404, upload-time = "2026-02-02T15:37:28.108Z" }, { url = "https://files.pythonhosted.org/packages/2f/b2/ec04b74ae03a125db7bd69cffd014b227b7f341e3261bf75b5eb88a1aa92/orjson-3.11.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b8d14b71c0b12963fe8a62aac87119f1afdf4cb88a400f61ca5ae581449efcb5", size = 123677, upload-time = "2026-02-02T15:37:30.287Z" }, { url = "https://files.pythonhosted.org/packages/4c/69/f95bdf960605f08f827f6e3291fe243d8aa9c5c9ff017a8d7232209184c3/orjson-3.11.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91c81ef070c8f3220054115e1ef468b1c9ce8497b4e526cb9f68ab4dc0a7ac62", size = 128950, upload-time = "2026-02-02T15:37:31.595Z" }, { url = "https://files.pythonhosted.org/packages/a4/1b/de59c57bae1d148ef298852abd31909ac3089cff370dfd4cd84cc99cbc42/orjson-3.11.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:411ebaf34d735e25e358a6d9e7978954a9c9d58cfb47bc6683cdc3964cd2f910", size = 141756, upload-time = "2026-02-02T15:37:32.985Z" }, { url = "https://files.pythonhosted.org/packages/ee/9e/9decc59f4499f695f65c650f6cfa6cd4c37a3fbe8fa235a0a3614cb54386/orjson-3.11.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a16bcd08ab0bcdfc7e8801d9c4a9cc17e58418e4d48ddc6ded4e9e4b1a94062b", size = 130812, upload-time = "2026-02-02T15:37:34.204Z" }, { url = "https://files.pythonhosted.org/packages/28/e6/59f932bcabd1eac44e334fe8e3281a92eacfcb450586e1f4bde0423728d8/orjson-3.11.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c0b51672e466fd7e56230ffbae7f1639e18d0ce023351fb75da21b71bc2c960", size = 133444, upload-time = "2026-02-02T15:37:35.446Z" }, { url = "https://files.pythonhosted.org/packages/f1/36/b0f05c0eaa7ca30bc965e37e6a2956b0d67adb87a9872942d3568da846ae/orjson-3.11.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:136dcd6a2e796dfd9ffca9fc027d778567b0b7c9968d092842d3c323cef88aa8", size = 138609, upload-time = "2026-02-02T15:37:36.657Z" }, { url = "https://files.pythonhosted.org/packages/b8/03/58ec7d302b8d86944c60c7b4b82975d5161fcce4c9bc8c6cb1d6741b6115/orjson-3.11.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:7ba61079379b0ae29e117db13bda5f28d939766e410d321ec1624afc6a0b0504", size = 408918, upload-time = "2026-02-02T15:37:38.076Z" }, { url = "https://files.pythonhosted.org/packages/06/3a/868d65ef9a8b99be723bd510de491349618abd9f62c826cf206d962db295/orjson-3.11.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0527a4510c300e3b406591b0ba69b5dc50031895b0a93743526a3fc45f59d26e", size = 143998, upload-time = "2026-02-02T15:37:39.706Z" }, { url = "https://files.pythonhosted.org/packages/5b/c7/1e18e1c83afe3349f4f6dc9e14910f0ae5f82eac756d1412ea4018938535/orjson-3.11.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a709e881723c9b18acddcfb8ba357322491ad553e277cf467e1e7e20e2d90561", size = 134802, upload-time = "2026-02-02T15:37:41.002Z" }, { url = "https://files.pythonhosted.org/packages/d4/0b/ccb7ee1a65b37e8eeb8b267dc953561d72370e85185e459616d4345bab34/orjson-3.11.7-cp311-cp311-win32.whl", hash = "sha256:c43b8b5bab288b6b90dac410cca7e986a4fa747a2e8f94615aea407da706980d", size = 127828, upload-time = "2026-02-02T15:37:42.241Z" }, { url = "https://files.pythonhosted.org/packages/af/9e/55c776dffda3f381e0f07d010a4f5f3902bf48eaba1bb7684d301acd4924/orjson-3.11.7-cp311-cp311-win_amd64.whl", hash = "sha256:6543001328aa857187f905308a028935864aefe9968af3848401b6fe80dbb471", size = 124941, upload-time = "2026-02-02T15:37:43.444Z" }, { url = "https://files.pythonhosted.org/packages/aa/8e/424a620fa7d263b880162505fb107ef5e0afaa765b5b06a88312ac291560/orjson-3.11.7-cp311-cp311-win_arm64.whl", hash = "sha256:1ee5cc7160a821dfe14f130bc8e63e7611051f964b463d9e2a3a573204446a4d", size = 126245, upload-time = "2026-02-02T15:37:45.18Z" }, { url = "https://files.pythonhosted.org/packages/80/bf/76f4f1665f6983385938f0e2a5d7efa12a58171b8456c252f3bae8a4cf75/orjson-3.11.7-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bd03ea7606833655048dab1a00734a2875e3e86c276e1d772b2a02556f0d895f", size = 228545, upload-time = "2026-02-02T15:37:46.376Z" }, { url = "https://files.pythonhosted.org/packages/79/53/6c72c002cb13b5a978a068add59b25a8bdf2800ac1c9c8ecdb26d6d97064/orjson-3.11.7-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:89e440ebc74ce8ab5c7bc4ce6757b4a6b1041becb127df818f6997b5c71aa60b", size = 125224, upload-time = "2026-02-02T15:37:47.697Z" }, { url = "https://files.pythonhosted.org/packages/2c/83/10e48852865e5dd151bdfe652c06f7da484578ed02c5fca938e3632cb0b8/orjson-3.11.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ede977b5fe5ac91b1dffc0a517ca4542d2ec8a6a4ff7b2652d94f640796342a", size = 128154, upload-time = "2026-02-02T15:37:48.954Z" }, { url = "https://files.pythonhosted.org/packages/6e/52/a66e22a2b9abaa374b4a081d410edab6d1e30024707b87eab7c734afe28d/orjson-3.11.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b7b1dae39230a393df353827c855a5f176271c23434cfd2db74e0e424e693e10", size = 123548, upload-time = "2026-02-02T15:37:50.187Z" }, { url = "https://files.pythonhosted.org/packages/de/38/605d371417021359f4910c496f764c48ceb8997605f8c25bf1dfe58c0ebe/orjson-3.11.7-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed46f17096e28fb28d2975834836a639af7278aa87c84f68ab08fbe5b8bd75fa", size = 129000, upload-time = "2026-02-02T15:37:51.426Z" }, { url = "https://files.pythonhosted.org/packages/44/98/af32e842b0ffd2335c89714d48ca4e3917b42f5d6ee5537832e069a4b3ac/orjson-3.11.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3726be79e36e526e3d9c1aceaadbfb4a04ee80a72ab47b3f3c17fefb9812e7b8", size = 141686, upload-time = "2026-02-02T15:37:52.607Z" }, { url = "https://files.pythonhosted.org/packages/96/0b/fc793858dfa54be6feee940c1463370ece34b3c39c1ca0aa3845f5ba9892/orjson-3.11.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0724e265bc548af1dedebd9cb3d24b4e1c1e685a343be43e87ba922a5c5fff2f", size = 130812, upload-time = "2026-02-02T15:37:53.944Z" }, { url = "https://files.pythonhosted.org/packages/dc/91/98a52415059db3f374757d0b7f0f16e3b5cd5976c90d1c2b56acaea039e6/orjson-3.11.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7745312efa9e11c17fbd3cb3097262d079da26930ae9ae7ba28fb738367cbad", size = 133440, upload-time = "2026-02-02T15:37:55.615Z" }, { url = "https://files.pythonhosted.org/packages/dc/b6/cb540117bda61791f46381f8c26c8f93e802892830a6055748d3bb1925ab/orjson-3.11.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f904c24bdeabd4298f7a977ef14ca2a022ca921ed670b92ecd16ab6f3d01f867", size = 138386, upload-time = "2026-02-02T15:37:56.814Z" }, { url = "https://files.pythonhosted.org/packages/63/1a/50a3201c334a7f17c231eee5f841342190723794e3b06293f26e7cf87d31/orjson-3.11.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b9fc4d0f81f394689e0814617aadc4f2ea0e8025f38c226cbf22d3b5ddbf025d", size = 408853, upload-time = "2026-02-02T15:37:58.291Z" }, { url = "https://files.pythonhosted.org/packages/87/cd/8de1c67d0be44fdc22701e5989c0d015a2adf391498ad42c4dc589cd3013/orjson-3.11.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:849e38203e5be40b776ed2718e587faf204d184fc9a008ae441f9442320c0cab", size = 144130, upload-time = "2026-02-02T15:38:00.163Z" }, { url = "https://files.pythonhosted.org/packages/0f/fe/d605d700c35dd55f51710d159fc54516a280923cd1b7e47508982fbb387d/orjson-3.11.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4682d1db3bcebd2b64757e0ddf9e87ae5f00d29d16c5cdf3a62f561d08cc3dd2", size = 134818, upload-time = "2026-02-02T15:38:01.507Z" }, { url = "https://files.pythonhosted.org/packages/e4/e4/15ecc67edb3ddb3e2f46ae04475f2d294e8b60c1825fbe28a428b93b3fbd/orjson-3.11.7-cp312-cp312-win32.whl", hash = "sha256:f4f7c956b5215d949a1f65334cf9d7612dde38f20a95f2315deef167def91a6f", size = 127923, upload-time = "2026-02-02T15:38:02.75Z" }, { url = "https://files.pythonhosted.org/packages/34/70/2e0855361f76198a3965273048c8e50a9695d88cd75811a5b46444895845/orjson-3.11.7-cp312-cp312-win_amd64.whl", hash = "sha256:bf742e149121dc5648ba0a08ea0871e87b660467ef168a3a5e53bc1fbd64bb74", size = 125007, upload-time = "2026-02-02T15:38:04.032Z" }, { url = "https://files.pythonhosted.org/packages/68/40/c2051bd19fc467610fed469dc29e43ac65891571138f476834ca192bc290/orjson-3.11.7-cp312-cp312-win_arm64.whl", hash = "sha256:26c3b9132f783b7d7903bf1efb095fed8d4a3a85ec0d334ee8beff3d7a4749d5", size = 126089, upload-time = "2026-02-02T15:38:05.297Z" }, { url = "https://files.pythonhosted.org/packages/89/25/6e0e52cac5aab51d7b6dcd257e855e1dec1c2060f6b28566c509b4665f62/orjson-3.11.7-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1d98b30cc1313d52d4af17d9c3d307b08389752ec5f2e5febdfada70b0f8c733", size = 228390, upload-time = "2026-02-02T15:38:06.8Z" }, { url = "https://files.pythonhosted.org/packages/a5/29/a77f48d2fc8a05bbc529e5ff481fb43d914f9e383ea2469d4f3d51df3d00/orjson-3.11.7-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:d897e81f8d0cbd2abb82226d1860ad2e1ab3ff16d7b08c96ca00df9d45409ef4", size = 125189, upload-time = "2026-02-02T15:38:08.181Z" }, { url = "https://files.pythonhosted.org/packages/89/25/0a16e0729a0e6a1504f9d1a13cdd365f030068aab64cec6958396b9969d7/orjson-3.11.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:814be4b49b228cfc0b3c565acf642dd7d13538f966e3ccde61f4f55be3e20785", size = 128106, upload-time = "2026-02-02T15:38:09.41Z" }, { url = "https://files.pythonhosted.org/packages/66/da/a2e505469d60666a05ab373f1a6322eb671cb2ba3a0ccfc7d4bc97196787/orjson-3.11.7-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d06e5c5fed5caedd2e540d62e5b1c25e8c82431b9e577c33537e5fa4aa909539", size = 123363, upload-time = "2026-02-02T15:38:10.73Z" }, { url = "https://files.pythonhosted.org/packages/23/bf/ed73f88396ea35c71b38961734ea4a4746f7ca0768bf28fd551d37e48dd0/orjson-3.11.7-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31c80ce534ac4ea3739c5ee751270646cbc46e45aea7576a38ffec040b4029a1", size = 129007, upload-time = "2026-02-02T15:38:12.138Z" }, { url = "https://files.pythonhosted.org/packages/73/3c/b05d80716f0225fc9008fbf8ab22841dcc268a626aa550561743714ce3bf/orjson-3.11.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f50979824bde13d32b4320eedd513431c921102796d86be3eee0b58e58a3ecd1", size = 141667, upload-time = "2026-02-02T15:38:13.398Z" }, { url = "https://files.pythonhosted.org/packages/61/e8/0be9b0addd9bf86abfc938e97441dcd0375d494594b1c8ad10fe57479617/orjson-3.11.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e54f3808e2b6b945078c41aa8d9b5834b28c50843846e97807e5adb75fa9705", size = 130832, upload-time = "2026-02-02T15:38:14.698Z" }, { url = "https://files.pythonhosted.org/packages/c9/ec/c68e3b9021a31d9ec15a94931db1410136af862955854ed5dd7e7e4f5bff/orjson-3.11.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12b80df61aab7b98b490fe9e4879925ba666fccdfcd175252ce4d9035865ace", size = 133373, upload-time = "2026-02-02T15:38:16.109Z" }, { url = "https://files.pythonhosted.org/packages/d2/45/f3466739aaafa570cc8e77c6dbb853c48bf56e3b43738020e2661e08b0ac/orjson-3.11.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:996b65230271f1a97026fd0e6a753f51fbc0c335d2ad0c6201f711b0da32693b", size = 138307, upload-time = "2026-02-02T15:38:17.453Z" }, { url = "https://files.pythonhosted.org/packages/e1/84/9f7f02288da1ffb31405c1be07657afd1eecbcb4b64ee2817b6fe0f785fa/orjson-3.11.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ab49d4b2a6a1d415ddb9f37a21e02e0d5dbfe10b7870b21bf779fc21e9156157", size = 408695, upload-time = "2026-02-02T15:38:18.831Z" }, { url = "https://files.pythonhosted.org/packages/18/07/9dd2f0c0104f1a0295ffbe912bc8d63307a539b900dd9e2c48ef7810d971/orjson-3.11.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:390a1dce0c055ddf8adb6aa94a73b45a4a7d7177b5c584b8d1c1947f2ba60fb3", size = 144099, upload-time = "2026-02-02T15:38:20.28Z" }, { url = "https://files.pythonhosted.org/packages/a5/66/857a8e4a3292e1f7b1b202883bcdeb43a91566cf59a93f97c53b44bd6801/orjson-3.11.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1eb80451a9c351a71dfaf5b7ccc13ad065405217726b59fdbeadbcc544f9d223", size = 134806, upload-time = "2026-02-02T15:38:22.186Z" }, { url = "https://files.pythonhosted.org/packages/0a/5b/6ebcf3defc1aab3a338ca777214966851e92efb1f30dc7fc8285216e6d1b/orjson-3.11.7-cp313-cp313-win32.whl", hash = "sha256:7477aa6a6ec6139c5cb1cc7b214643592169a5494d200397c7fc95d740d5fcf3", size = 127914, upload-time = "2026-02-02T15:38:23.511Z" }, { url = "https://files.pythonhosted.org/packages/00/04/c6f72daca5092e3117840a1b1e88dfc809cc1470cf0734890d0366b684a1/orjson-3.11.7-cp313-cp313-win_amd64.whl", hash = "sha256:b9f95dcdea9d4f805daa9ddf02617a89e484c6985fa03055459f90e87d7a0757", size = 124986, upload-time = "2026-02-02T15:38:24.836Z" }, { url = "https://files.pythonhosted.org/packages/03/ba/077a0f6f1085d6b806937246860fafbd5b17f3919c70ee3f3d8d9c713f38/orjson-3.11.7-cp313-cp313-win_arm64.whl", hash = "sha256:800988273a014a0541483dc81021247d7eacb0c845a9d1a34a422bc718f41539", size = 126045, upload-time = "2026-02-02T15:38:26.216Z" }, { url = "https://files.pythonhosted.org/packages/e9/1e/745565dca749813db9a093c5ebc4bac1a9475c64d54b95654336ac3ed961/orjson-3.11.7-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:de0a37f21d0d364954ad5de1970491d7fbd0fb1ef7417d4d56a36dc01ba0c0a0", size = 228391, upload-time = "2026-02-02T15:38:27.757Z" }, { url = "https://files.pythonhosted.org/packages/46/19/e40f6225da4d3aa0c8dc6e5219c5e87c2063a560fe0d72a88deb59776794/orjson-3.11.7-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c2428d358d85e8da9d37cba18b8c4047c55222007a84f97156a5b22028dfbfc0", size = 125188, upload-time = "2026-02-02T15:38:29.241Z" }, { url = "https://files.pythonhosted.org/packages/9d/7e/c4de2babef2c0817fd1f048fd176aa48c37bec8aef53d2fa932983032cce/orjson-3.11.7-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c4bc6c6ac52cdaa267552544c73e486fecbd710b7ac09bc024d5a78555a22f6", size = 128097, upload-time = "2026-02-02T15:38:30.618Z" }, { url = "https://files.pythonhosted.org/packages/eb/74/233d360632bafd2197f217eee7fb9c9d0229eac0c18128aee5b35b0014fe/orjson-3.11.7-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd0d68edd7dfca1b2eca9361a44ac9f24b078de3481003159929a0573f21a6bf", size = 123364, upload-time = "2026-02-02T15:38:32.363Z" }, { url = "https://files.pythonhosted.org/packages/79/51/af79504981dd31efe20a9e360eb49c15f06df2b40e7f25a0a52d9ae888e8/orjson-3.11.7-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:623ad1b9548ef63886319c16fa317848e465a21513b31a6ad7b57443c3e0dcf5", size = 129076, upload-time = "2026-02-02T15:38:33.68Z" }, { url = "https://files.pythonhosted.org/packages/67/e2/da898eb68b72304f8de05ca6715870d09d603ee98d30a27e8a9629abc64b/orjson-3.11.7-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6e776b998ac37c0396093d10290e60283f59cfe0fc3fccbd0ccc4bd04dd19892", size = 141705, upload-time = "2026-02-02T15:38:34.989Z" }, { url = "https://files.pythonhosted.org/packages/c5/89/15364d92acb3d903b029e28d834edb8780c2b97404cbf7929aa6b9abdb24/orjson-3.11.7-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:652c6c3af76716f4a9c290371ba2e390ede06f6603edb277b481daf37f6f464e", size = 130855, upload-time = "2026-02-02T15:38:36.379Z" }, { url = "https://files.pythonhosted.org/packages/c2/8b/ecdad52d0b38d4b8f514be603e69ccd5eacf4e7241f972e37e79792212ec/orjson-3.11.7-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a56df3239294ea5964adf074c54bcc4f0ccd21636049a2cf3ca9cf03b5d03cf1", size = 133386, upload-time = "2026-02-02T15:38:37.704Z" }, { url = "https://files.pythonhosted.org/packages/b9/0e/45e1dcf10e17d0924b7c9162f87ec7b4ca79e28a0548acf6a71788d3e108/orjson-3.11.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bda117c4148e81f746655d5a3239ae9bd00cb7bc3ca178b5fc5a5997e9744183", size = 138295, upload-time = "2026-02-02T15:38:39.096Z" }, { url = "https://files.pythonhosted.org/packages/63/d7/4d2e8b03561257af0450f2845b91fbd111d7e526ccdf737267108075e0ba/orjson-3.11.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:23d6c20517a97a9daf1d48b580fcdc6f0516c6f4b5038823426033690b4d2650", size = 408720, upload-time = "2026-02-02T15:38:40.634Z" }, { url = "https://files.pythonhosted.org/packages/78/cf/d45343518282108b29c12a65892445fc51f9319dc3c552ceb51bb5905ed2/orjson-3.11.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:8ff206156006da5b847c9304b6308a01e8cdbc8cce824e2779a5ba71c3def141", size = 144152, upload-time = "2026-02-02T15:38:42.262Z" }, { url = "https://files.pythonhosted.org/packages/a9/3a/d6001f51a7275aacd342e77b735c71fa04125a3f93c36fee4526bc8c654e/orjson-3.11.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:962d046ee1765f74a1da723f4b33e3b228fe3a48bd307acce5021dfefe0e29b2", size = 134814, upload-time = "2026-02-02T15:38:43.627Z" }, { url = "https://files.pythonhosted.org/packages/1d/d3/f19b47ce16820cc2c480f7f1723e17f6d411b3a295c60c8ad3aa9ff1c96a/orjson-3.11.7-cp314-cp314-win32.whl", hash = "sha256:89e13dd3f89f1c38a9c9eba5fbf7cdc2d1feca82f5f290864b4b7a6aac704576", size = 127997, upload-time = "2026-02-02T15:38:45.06Z" }, { url = "https://files.pythonhosted.org/packages/12/df/172771902943af54bf661a8d102bdf2e7f932127968080632bda6054b62c/orjson-3.11.7-cp314-cp314-win_amd64.whl", hash = "sha256:845c3e0d8ded9c9271cd79596b9b552448b885b97110f628fb687aee2eed11c1", size = 124985, upload-time = "2026-02-02T15:38:46.388Z" }, { url = "https://files.pythonhosted.org/packages/6f/1c/f2a8d8a1b17514660a614ce5f7aac74b934e69f5abc2700cc7ced882a009/orjson-3.11.7-cp314-cp314-win_arm64.whl", hash = "sha256:4a2e9c5be347b937a2e0203866f12bba36082e89b402ddb9e927d5822e43088d", size = 126038, upload-time = "2026-02-02T15:38:47.703Z" }, ] [[package]] name = "packaging" version = "26.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] [[package]] name = "pandas" version = "2.2.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "python-dateutil", marker = "python_full_version < '3.11'" }, { name = "pytz", marker = "python_full_version < '3.11'" }, { name = "tzdata", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213, upload-time = "2024-09-20T13:10:04.827Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/aa/70/c853aec59839bceed032d52010ff5f1b8d87dc3114b762e4ba2727661a3b/pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5", size = 12580827, upload-time = "2024-09-20T13:08:42.347Z" }, { url = "https://files.pythonhosted.org/packages/99/f2/c4527768739ffa4469b2b4fff05aa3768a478aed89a2f271a79a40eee984/pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348", size = 11303897, upload-time = "2024-09-20T13:08:45.807Z" }, { url = "https://files.pythonhosted.org/packages/ed/12/86c1747ea27989d7a4064f806ce2bae2c6d575b950be087837bdfcabacc9/pandas-2.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed", size = 66480908, upload-time = "2024-09-20T18:37:13.513Z" }, { url = "https://files.pythonhosted.org/packages/44/50/7db2cd5e6373ae796f0ddad3675268c8d59fb6076e66f0c339d61cea886b/pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57", size = 13064210, upload-time = "2024-09-20T13:08:48.325Z" }, { url = "https://files.pythonhosted.org/packages/61/61/a89015a6d5536cb0d6c3ba02cebed51a95538cf83472975275e28ebf7d0c/pandas-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42", size = 16754292, upload-time = "2024-09-20T19:01:54.443Z" }, { url = "https://files.pythonhosted.org/packages/ce/0d/4cc7b69ce37fac07645a94e1d4b0880b15999494372c1523508511b09e40/pandas-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f", size = 14416379, upload-time = "2024-09-20T13:08:50.882Z" }, { url = "https://files.pythonhosted.org/packages/31/9e/6ebb433de864a6cd45716af52a4d7a8c3c9aaf3a98368e61db9e69e69a9c/pandas-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645", size = 11598471, upload-time = "2024-09-20T13:08:53.332Z" }, { url = "https://files.pythonhosted.org/packages/a8/44/d9502bf0ed197ba9bf1103c9867d5904ddcaf869e52329787fc54ed70cc8/pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039", size = 12602222, upload-time = "2024-09-20T13:08:56.254Z" }, { url = "https://files.pythonhosted.org/packages/52/11/9eac327a38834f162b8250aab32a6781339c69afe7574368fffe46387edf/pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd", size = 11321274, upload-time = "2024-09-20T13:08:58.645Z" }, { url = "https://files.pythonhosted.org/packages/45/fb/c4beeb084718598ba19aa9f5abbc8aed8b42f90930da861fcb1acdb54c3a/pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698", size = 15579836, upload-time = "2024-09-20T19:01:57.571Z" }, { url = "https://files.pythonhosted.org/packages/cd/5f/4dba1d39bb9c38d574a9a22548c540177f78ea47b32f99c0ff2ec499fac5/pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc", size = 13058505, upload-time = "2024-09-20T13:09:01.501Z" }, { url = "https://files.pythonhosted.org/packages/b9/57/708135b90391995361636634df1f1130d03ba456e95bcf576fada459115a/pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3", size = 16744420, upload-time = "2024-09-20T19:02:00.678Z" }, { url = "https://files.pythonhosted.org/packages/86/4a/03ed6b7ee323cf30404265c284cee9c65c56a212e0a08d9ee06984ba2240/pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32", size = 14440457, upload-time = "2024-09-20T13:09:04.105Z" }, { url = "https://files.pythonhosted.org/packages/ed/8c/87ddf1fcb55d11f9f847e3c69bb1c6f8e46e2f40ab1a2d2abadb2401b007/pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5", size = 11617166, upload-time = "2024-09-20T13:09:06.917Z" }, { url = "https://files.pythonhosted.org/packages/17/a3/fb2734118db0af37ea7433f57f722c0a56687e14b14690edff0cdb4b7e58/pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9", size = 12529893, upload-time = "2024-09-20T13:09:09.655Z" }, { url = "https://files.pythonhosted.org/packages/e1/0c/ad295fd74bfac85358fd579e271cded3ac969de81f62dd0142c426b9da91/pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4", size = 11363475, upload-time = "2024-09-20T13:09:14.718Z" }, { url = "https://files.pythonhosted.org/packages/c6/2a/4bba3f03f7d07207481fed47f5b35f556c7441acddc368ec43d6643c5777/pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3", size = 15188645, upload-time = "2024-09-20T19:02:03.88Z" }, { url = "https://files.pythonhosted.org/packages/38/f8/d8fddee9ed0d0c0f4a2132c1dfcf0e3e53265055da8df952a53e7eaf178c/pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319", size = 12739445, upload-time = "2024-09-20T13:09:17.621Z" }, { url = "https://files.pythonhosted.org/packages/20/e8/45a05d9c39d2cea61ab175dbe6a2de1d05b679e8de2011da4ee190d7e748/pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8", size = 16359235, upload-time = "2024-09-20T19:02:07.094Z" }, { url = "https://files.pythonhosted.org/packages/1d/99/617d07a6a5e429ff90c90da64d428516605a1ec7d7bea494235e1c3882de/pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a", size = 14056756, upload-time = "2024-09-20T13:09:20.474Z" }, { url = "https://files.pythonhosted.org/packages/29/d4/1244ab8edf173a10fd601f7e13b9566c1b525c4f365d6bee918e68381889/pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13", size = 11504248, upload-time = "2024-09-20T13:09:23.137Z" }, { url = "https://files.pythonhosted.org/packages/64/22/3b8f4e0ed70644e85cfdcd57454686b9057c6c38d2f74fe4b8bc2527214a/pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015", size = 12477643, upload-time = "2024-09-20T13:09:25.522Z" }, { url = "https://files.pythonhosted.org/packages/e4/93/b3f5d1838500e22c8d793625da672f3eec046b1a99257666c94446969282/pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28", size = 11281573, upload-time = "2024-09-20T13:09:28.012Z" }, { url = "https://files.pythonhosted.org/packages/f5/94/6c79b07f0e5aab1dcfa35a75f4817f5c4f677931d4234afcd75f0e6a66ca/pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0", size = 15196085, upload-time = "2024-09-20T19:02:10.451Z" }, { url = "https://files.pythonhosted.org/packages/e8/31/aa8da88ca0eadbabd0a639788a6da13bb2ff6edbbb9f29aa786450a30a91/pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24", size = 12711809, upload-time = "2024-09-20T13:09:30.814Z" }, { url = "https://files.pythonhosted.org/packages/ee/7c/c6dbdb0cb2a4344cacfb8de1c5808ca885b2e4dcfde8008266608f9372af/pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659", size = 16356316, upload-time = "2024-09-20T19:02:13.825Z" }, { url = "https://files.pythonhosted.org/packages/57/b7/8b757e7d92023b832869fa8881a992696a0bfe2e26f72c9ae9f255988d42/pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb", size = 14022055, upload-time = "2024-09-20T13:09:33.462Z" }, { url = "https://files.pythonhosted.org/packages/3b/bc/4b18e2b8c002572c5a441a64826252ce5da2aa738855747247a971988043/pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d", size = 11481175, upload-time = "2024-09-20T13:09:35.871Z" }, { url = "https://files.pythonhosted.org/packages/76/a3/a5d88146815e972d40d19247b2c162e88213ef51c7c25993942c39dbf41d/pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468", size = 12615650, upload-time = "2024-09-20T13:09:38.685Z" }, { url = "https://files.pythonhosted.org/packages/9c/8c/f0fd18f6140ddafc0c24122c8a964e48294acc579d47def376fef12bcb4a/pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18", size = 11290177, upload-time = "2024-09-20T13:09:41.141Z" }, { url = "https://files.pythonhosted.org/packages/ed/f9/e995754eab9c0f14c6777401f7eece0943840b7a9fc932221c19d1abee9f/pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2", size = 14651526, upload-time = "2024-09-20T19:02:16.905Z" }, { url = "https://files.pythonhosted.org/packages/25/b0/98d6ae2e1abac4f35230aa756005e8654649d305df9a28b16b9ae4353bff/pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4", size = 11871013, upload-time = "2024-09-20T13:09:44.39Z" }, { url = "https://files.pythonhosted.org/packages/cc/57/0f72a10f9db6a4628744c8e8f0df4e6e21de01212c7c981d31e50ffc8328/pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d", size = 15711620, upload-time = "2024-09-20T19:02:20.639Z" }, { url = "https://files.pythonhosted.org/packages/ab/5f/b38085618b950b79d2d9164a711c52b10aefc0ae6833b96f626b7021b2ed/pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", size = 13098436, upload-time = "2024-09-20T13:09:48.112Z" }, ] [[package]] name = "pandas" version = "3.0.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.14'" }, { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, { name = "python-dateutil", marker = "python_full_version >= '3.11'" }, { name = "tzdata", marker = "(python_full_version >= '3.11' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2e/0c/b28ed414f080ee0ad153f848586d61d1878f91689950f037f976ce15f6c8/pandas-3.0.1.tar.gz", hash = "sha256:4186a699674af418f655dbd420ed87f50d56b4cd6603784279d9eef6627823c8", size = 4641901, upload-time = "2026-02-17T22:20:16.434Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ff/07/c7087e003ceee9b9a82539b40414ec557aa795b584a1a346e89180853d79/pandas-3.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de09668c1bf3b925c07e5762291602f0d789eca1b3a781f99c1c78f6cac0e7ea", size = 10323380, upload-time = "2026-02-17T22:18:16.133Z" }, { url = "https://files.pythonhosted.org/packages/c1/27/90683c7122febeefe84a56f2cde86a9f05f68d53885cebcc473298dfc33e/pandas-3.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:24ba315ba3d6e5806063ac6eb717504e499ce30bd8c236d8693a5fd3f084c796", size = 9923455, upload-time = "2026-02-17T22:18:19.13Z" }, { url = "https://files.pythonhosted.org/packages/0e/f1/ed17d927f9950643bc7631aa4c99ff0cc83a37864470bc419345b656a41f/pandas-3.0.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:406ce835c55bac912f2a0dcfaf27c06d73c6b04a5dde45f1fd3169ce31337389", size = 10753464, upload-time = "2026-02-17T22:18:21.134Z" }, { url = "https://files.pythonhosted.org/packages/2e/7c/870c7e7daec2a6c7ff2ac9e33b23317230d4e4e954b35112759ea4a924a7/pandas-3.0.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:830994d7e1f31dd7e790045235605ab61cff6c94defc774547e8b7fdfbff3dc7", size = 11255234, upload-time = "2026-02-17T22:18:24.175Z" }, { url = "https://files.pythonhosted.org/packages/5c/39/3653fe59af68606282b989c23d1a543ceba6e8099cbcc5f1d506a7bae2aa/pandas-3.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a64ce8b0f2de1d2efd2ae40b0abe7f8ae6b29fbfb3812098ed5a6f8e235ad9bf", size = 11767299, upload-time = "2026-02-17T22:18:26.824Z" }, { url = "https://files.pythonhosted.org/packages/9b/31/1daf3c0c94a849c7a8dab8a69697b36d313b229918002ba3e409265c7888/pandas-3.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9832c2c69da24b602c32e0c7b1b508a03949c18ba08d4d9f1c1033426685b447", size = 12333292, upload-time = "2026-02-17T22:18:28.996Z" }, { url = "https://files.pythonhosted.org/packages/1f/67/af63f83cd6ca603a00fe8530c10a60f0879265b8be00b5930e8e78c5b30b/pandas-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:84f0904a69e7365f79a0c77d3cdfccbfb05bf87847e3a51a41e1426b0edb9c79", size = 9892176, upload-time = "2026-02-17T22:18:31.79Z" }, { url = "https://files.pythonhosted.org/packages/79/ab/9c776b14ac4b7b4140788eca18468ea39894bc7340a408f1d1e379856a6b/pandas-3.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:4a68773d5a778afb31d12e34f7dd4612ab90de8c6fb1d8ffe5d4a03b955082a1", size = 9151328, upload-time = "2026-02-17T22:18:35.721Z" }, { url = "https://files.pythonhosted.org/packages/37/51/b467209c08dae2c624873d7491ea47d2b47336e5403309d433ea79c38571/pandas-3.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:476f84f8c20c9f5bc47252b66b4bb25e1a9fc2fa98cead96744d8116cb85771d", size = 10344357, upload-time = "2026-02-17T22:18:38.262Z" }, { url = "https://files.pythonhosted.org/packages/7c/f1/e2567ffc8951ab371db2e40b2fe068e36b81d8cf3260f06ae508700e5504/pandas-3.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0ab749dfba921edf641d4036c4c21c0b3ea70fea478165cb98a998fb2a261955", size = 9884543, upload-time = "2026-02-17T22:18:41.476Z" }, { url = "https://files.pythonhosted.org/packages/d7/39/327802e0b6d693182403c144edacbc27eb82907b57062f23ef5a4c4a5ea7/pandas-3.0.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8e36891080b87823aff3640c78649b91b8ff6eea3c0d70aeabd72ea43ab069b", size = 10396030, upload-time = "2026-02-17T22:18:43.822Z" }, { url = "https://files.pythonhosted.org/packages/3d/fe/89d77e424365280b79d99b3e1e7d606f5165af2f2ecfaf0c6d24c799d607/pandas-3.0.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:532527a701281b9dd371e2f582ed9094f4c12dd9ffb82c0c54ee28d8ac9520c4", size = 10876435, upload-time = "2026-02-17T22:18:45.954Z" }, { url = "https://files.pythonhosted.org/packages/b5/a6/2a75320849dd154a793f69c951db759aedb8d1dd3939eeacda9bdcfa1629/pandas-3.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:356e5c055ed9b0da1580d465657bc7d00635af4fd47f30afb23025352ba764d1", size = 11405133, upload-time = "2026-02-17T22:18:48.533Z" }, { url = "https://files.pythonhosted.org/packages/58/53/1d68fafb2e02d7881df66aa53be4cd748d25cbe311f3b3c85c93ea5d30ca/pandas-3.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9d810036895f9ad6345b8f2a338dd6998a74e8483847403582cab67745bff821", size = 11932065, upload-time = "2026-02-17T22:18:50.837Z" }, { url = "https://files.pythonhosted.org/packages/75/08/67cc404b3a966b6df27b38370ddd96b3b023030b572283d035181854aac5/pandas-3.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:536232a5fe26dd989bd633e7a0c450705fdc86a207fec7254a55e9a22950fe43", size = 9741627, upload-time = "2026-02-17T22:18:53.905Z" }, { url = "https://files.pythonhosted.org/packages/86/4f/caf9952948fb00d23795f09b893d11f1cacb384e666854d87249530f7cbe/pandas-3.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f463ebfd8de7f326d38037c7363c6dacb857c5881ab8961fb387804d6daf2f7", size = 9052483, upload-time = "2026-02-17T22:18:57.31Z" }, { url = "https://files.pythonhosted.org/packages/0b/48/aad6ec4f8d007534c091e9a7172b3ec1b1ee6d99a9cbb936b5eab6c6cf58/pandas-3.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5272627187b5d9c20e55d27caf5f2cd23e286aba25cadf73c8590e432e2b7262", size = 10317509, upload-time = "2026-02-17T22:18:59.498Z" }, { url = "https://files.pythonhosted.org/packages/a8/14/5990826f779f79148ae9d3a2c39593dc04d61d5d90541e71b5749f35af95/pandas-3.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:661e0f665932af88c7877f31da0dc743fe9c8f2524bdffe23d24fdcb67ef9d56", size = 9860561, upload-time = "2026-02-17T22:19:02.265Z" }, { url = "https://files.pythonhosted.org/packages/fa/80/f01ff54664b6d70fed71475543d108a9b7c888e923ad210795bef04ffb7d/pandas-3.0.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75e6e292ff898679e47a2199172593d9f6107fd2dd3617c22c2946e97d5df46e", size = 10365506, upload-time = "2026-02-17T22:19:05.017Z" }, { url = "https://files.pythonhosted.org/packages/f2/85/ab6d04733a7d6ff32bfc8382bf1b07078228f5d6ebec5266b91bfc5c4ff7/pandas-3.0.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1ff8cf1d2896e34343197685f432450ec99a85ba8d90cce2030c5eee2ef98791", size = 10873196, upload-time = "2026-02-17T22:19:07.204Z" }, { url = "https://files.pythonhosted.org/packages/48/a9/9301c83d0b47c23ac5deab91c6b39fd98d5b5db4d93b25df8d381451828f/pandas-3.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eca8b4510f6763f3d37359c2105df03a7a221a508f30e396a51d0713d462e68a", size = 11370859, upload-time = "2026-02-17T22:19:09.436Z" }, { url = "https://files.pythonhosted.org/packages/59/fe/0c1fc5bd2d29c7db2ab372330063ad555fb83e08422829c785f5ec2176ca/pandas-3.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:06aff2ad6f0b94a17822cf8b83bbb563b090ed82ff4fe7712db2ce57cd50d9b8", size = 11924584, upload-time = "2026-02-17T22:19:11.562Z" }, { url = "https://files.pythonhosted.org/packages/d6/7d/216a1588b65a7aa5f4535570418a599d943c85afb1d95b0876fc00aa1468/pandas-3.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:9fea306c783e28884c29057a1d9baa11a349bbf99538ec1da44c8476563d1b25", size = 9742769, upload-time = "2026-02-17T22:19:13.926Z" }, { url = "https://files.pythonhosted.org/packages/c4/cb/810a22a6af9a4e97c8ab1c946b47f3489c5bca5adc483ce0ffc84c9cc768/pandas-3.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:a8d37a43c52917427e897cb2e429f67a449327394396a81034a4449b99afda59", size = 9043855, upload-time = "2026-02-17T22:19:16.09Z" }, { url = "https://files.pythonhosted.org/packages/92/fa/423c89086cca1f039cf1253c3ff5b90f157b5b3757314aa635f6bf3e30aa/pandas-3.0.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d54855f04f8246ed7b6fc96b05d4871591143c46c0b6f4af874764ed0d2d6f06", size = 10752673, upload-time = "2026-02-17T22:19:18.304Z" }, { url = "https://files.pythonhosted.org/packages/22/23/b5a08ec1f40020397f0faba72f1e2c11f7596a6169c7b3e800abff0e433f/pandas-3.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e1b677accee34a09e0dc2ce5624e4a58a1870ffe56fc021e9caf7f23cd7668f", size = 10404967, upload-time = "2026-02-17T22:19:20.726Z" }, { url = "https://files.pythonhosted.org/packages/5c/81/94841f1bb4afdc2b52a99daa895ac2c61600bb72e26525ecc9543d453ebc/pandas-3.0.1-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a9cabbdcd03f1b6cd254d6dda8ae09b0252524be1592594c00b7895916cb1324", size = 10320575, upload-time = "2026-02-17T22:19:24.919Z" }, { url = "https://files.pythonhosted.org/packages/0a/8b/2ae37d66a5342a83adadfd0cb0b4bf9c3c7925424dd5f40d15d6cfaa35ee/pandas-3.0.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ae2ab1f166668b41e770650101e7090824fd34d17915dd9cd479f5c5e0065e9", size = 10710921, upload-time = "2026-02-17T22:19:27.181Z" }, { url = "https://files.pythonhosted.org/packages/a2/61/772b2e2757855e232b7ccf7cb8079a5711becb3a97f291c953def15a833f/pandas-3.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6bf0603c2e30e2cafac32807b06435f28741135cb8697eae8b28c7d492fc7d76", size = 11334191, upload-time = "2026-02-17T22:19:29.411Z" }, { url = "https://files.pythonhosted.org/packages/1b/08/b16c6df3ef555d8495d1d265a7963b65be166785d28f06a350913a4fac78/pandas-3.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6c426422973973cae1f4a23e51d4ae85974f44871b24844e4f7de752dd877098", size = 11782256, upload-time = "2026-02-17T22:19:32.34Z" }, { url = "https://files.pythonhosted.org/packages/55/80/178af0594890dee17e239fca96d3d8670ba0f5ff59b7d0439850924a9c09/pandas-3.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b03f91ae8c10a85c1613102c7bef5229b5379f343030a3ccefeca8a33414cf35", size = 10485047, upload-time = "2026-02-17T22:19:34.605Z" }, { url = "https://files.pythonhosted.org/packages/bb/8b/4bb774a998b97e6c2fd62a9e6cfdaae133b636fd1c468f92afb4ae9a447a/pandas-3.0.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:99d0f92ed92d3083d140bf6b97774f9f13863924cf3f52a70711f4e7588f9d0a", size = 10322465, upload-time = "2026-02-17T22:19:36.803Z" }, { url = "https://files.pythonhosted.org/packages/72/3a/5b39b51c64159f470f1ca3b1c2a87da290657ca022f7cd11442606f607d1/pandas-3.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3b66857e983208654294bb6477b8a63dee26b37bdd0eb34d010556e91261784f", size = 9910632, upload-time = "2026-02-17T22:19:39.001Z" }, { url = "https://files.pythonhosted.org/packages/4e/f7/b449ffb3f68c11da12fc06fbf6d2fa3a41c41e17d0284d23a79e1c13a7e4/pandas-3.0.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56cf59638bf24dc9bdf2154c81e248b3289f9a09a6d04e63608c159022352749", size = 10440535, upload-time = "2026-02-17T22:19:41.157Z" }, { url = "https://files.pythonhosted.org/packages/55/77/6ea82043db22cb0f2bbfe7198da3544000ddaadb12d26be36e19b03a2dc5/pandas-3.0.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1a9f55e0f46951874b863d1f3906dcb57df2d9be5c5847ba4dfb55b2c815249", size = 10893940, upload-time = "2026-02-17T22:19:43.493Z" }, { url = "https://files.pythonhosted.org/packages/03/30/f1b502a72468c89412c1b882a08f6eed8a4ee9dc033f35f65d0663df6081/pandas-3.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1849f0bba9c8a2fb0f691d492b834cc8dadf617e29015c66e989448d58d011ee", size = 11442711, upload-time = "2026-02-17T22:19:46.074Z" }, { url = "https://files.pythonhosted.org/packages/0d/f0/ebb6ddd8fc049e98cabac5c2924d14d1dda26a20adb70d41ea2e428d3ec4/pandas-3.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3d288439e11b5325b02ae6e9cc83e6805a62c40c5a6220bea9beb899c073b1c", size = 11963918, upload-time = "2026-02-17T22:19:48.838Z" }, { url = "https://files.pythonhosted.org/packages/09/f8/8ce132104074f977f907442790eaae24e27bce3b3b454e82faa3237ff098/pandas-3.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:93325b0fe372d192965f4cca88d97667f49557398bbf94abdda3bf1b591dbe66", size = 9862099, upload-time = "2026-02-17T22:19:51.081Z" }, { url = "https://files.pythonhosted.org/packages/e6/b7/6af9aac41ef2456b768ef0ae60acf8abcebb450a52043d030a65b4b7c9bd/pandas-3.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:97ca08674e3287c7148f4858b01136f8bdfe7202ad25ad04fec602dd1d29d132", size = 9185333, upload-time = "2026-02-17T22:19:53.266Z" }, { url = "https://files.pythonhosted.org/packages/66/fc/848bb6710bc6061cb0c5badd65b92ff75c81302e0e31e496d00029fe4953/pandas-3.0.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:58eeb1b2e0fb322befcf2bbc9ba0af41e616abadb3d3414a6bc7167f6cbfce32", size = 10772664, upload-time = "2026-02-17T22:19:55.806Z" }, { url = "https://files.pythonhosted.org/packages/69/5c/866a9bbd0f79263b4b0db6ec1a341be13a1473323f05c122388e0f15b21d/pandas-3.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cd9af1276b5ca9e298bd79a26bda32fa9cc87ed095b2a9a60978d2ca058eaf87", size = 10421286, upload-time = "2026-02-17T22:19:58.091Z" }, { url = "https://files.pythonhosted.org/packages/51/a4/2058fb84fb1cfbfb2d4a6d485e1940bb4ad5716e539d779852494479c580/pandas-3.0.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f87a04984d6b63788327cd9f79dda62b7f9043909d2440ceccf709249ca988", size = 10342050, upload-time = "2026-02-17T22:20:01.376Z" }, { url = "https://files.pythonhosted.org/packages/22/1b/674e89996cc4be74db3c4eb09240c4bb549865c9c3f5d9b086ff8fcfbf00/pandas-3.0.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85fe4c4df62e1e20f9db6ebfb88c844b092c22cd5324bdcf94bfa2fc1b391221", size = 10740055, upload-time = "2026-02-17T22:20:04.328Z" }, { url = "https://files.pythonhosted.org/packages/d0/f8/e954b750764298c22fa4614376531fe63c521ef517e7059a51f062b87dca/pandas-3.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:331ca75a2f8672c365ae25c0b29e46f5ac0c6551fdace8eec4cd65e4fac271ff", size = 11357632, upload-time = "2026-02-17T22:20:06.647Z" }, { url = "https://files.pythonhosted.org/packages/6d/02/c6e04b694ffd68568297abd03588b6d30295265176a5c01b7459d3bc35a3/pandas-3.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:15860b1fdb1973fffade772fdb931ccf9b2f400a3f5665aef94a00445d7d8dd5", size = 11810974, upload-time = "2026-02-17T22:20:08.946Z" }, { url = "https://files.pythonhosted.org/packages/89/41/d7dfb63d2407f12055215070c42fc6ac41b66e90a2946cdc5e759058398b/pandas-3.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:44f1364411d5670efa692b146c748f4ed013df91ee91e9bec5677fb1fd58b937", size = 10884622, upload-time = "2026-02-17T22:20:11.711Z" }, { url = "https://files.pythonhosted.org/packages/68/b0/34937815889fa982613775e4b97fddd13250f11012d769949c5465af2150/pandas-3.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:108dd1790337a494aa80e38def654ca3f0968cf4f362c85f44c15e471667102d", size = 9452085, upload-time = "2026-02-17T22:20:14.331Z" }, ] [[package]] name = "parso" version = "0.8.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/81/76/a1e769043c0c0c9fe391b702539d594731a4362334cdf4dc25d0c09761e7/parso-0.8.6.tar.gz", hash = "sha256:2b9a0332696df97d454fa67b81618fd69c35a7b90327cbe6ba5c92d2c68a7bfd", size = 401621, upload-time = "2026-02-09T15:45:24.425Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.py3-none-any.whl", hash = "sha256:2c549f800b70a5c4952197248825584cb00f033b29c692671d3bf08bf380baff", size = 106894, upload-time = "2026-02-09T15:45:21.391Z" }, ] [[package]] name = "pexpect" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ptyprocess", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, ] [[package]] name = "platformdirs" version = "4.9.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" }, ] [[package]] name = "pluggy" version = "1.6.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] [[package]] name = "polars" version = "1.39.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "polars-runtime-32" }, ] sdist = { url = "https://files.pythonhosted.org/packages/93/ab/f19e592fce9e000da49c96bf35e77cef67f9cb4b040bfa538a2764c0263e/polars-1.39.3.tar.gz", hash = "sha256:2e016c7f3e8d14fa777ef86fe0477cec6c67023a20ba4c94d6e8431eefe4a63c", size = 728987, upload-time = "2026-03-20T11:16:24.836Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b4/db/08f4ca10c5018813e7e0b59e4472302328b3d2ab1512f5a2157a814540e0/polars-1.39.3-py3-none-any.whl", hash = "sha256:c2b955ccc0a08a2bc9259785decf3d5c007b489b523bf2390cf21cec2bb82a56", size = 823985, upload-time = "2026-03-20T11:14:23.619Z" }, ] [[package]] name = "polars-runtime-32" version = "1.39.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/17/39/c8688696bc22b6c501e3b82ef3be10e543c07a785af5660f30997cd22dd2/polars_runtime_32-1.39.3.tar.gz", hash = "sha256:c728e4f469cafab501947585f36311b8fb222d3e934c6209e83791e0df20b29d", size = 2872335, upload-time = "2026-03-20T11:16:26.581Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3b/74/1b41205f7368c9375ab1dea91178eaa20435fe3eff036390a53a7660b416/polars_runtime_32-1.39.3-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:425c0b220b573fa097b4042edff73114cc6d23432a21dfd2dc41adf329d7d2e9", size = 45273243, upload-time = "2026-03-20T11:14:26.691Z" }, { url = "https://files.pythonhosted.org/packages/90/bf/297716b3095fe719be20fcf7af1d2b6ab069c38199bbace2469608a69b3a/polars_runtime_32-1.39.3-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:ef5884711e3c617d7dc93519a7d038e242f5741cfe5fe9afd32d58845d86c562", size = 40842924, upload-time = "2026-03-20T11:14:31.154Z" }, { url = "https://files.pythonhosted.org/packages/3d/3e/e65236d9d0d9babfa0ecba593413c06530fca60a8feb8f66243aa5dba92e/polars_runtime_32-1.39.3-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06b47f535eb1f97a9a1e5b0053ef50db3a4276e241178e37bbb1a38b1fa53b14", size = 43220650, upload-time = "2026-03-20T11:14:35.458Z" }, { url = "https://files.pythonhosted.org/packages/b0/15/fc3e43f3fdf3f20b7dfb5abe871ab6162cf8fb4aeabf4cfad822d5dc4c79/polars_runtime_32-1.39.3-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bc9e13dc1d2e828331f2fe8ccbc9757554dc4933a8d3e85e906b988178f95ed", size = 46877498, upload-time = "2026-03-20T11:14:40.14Z" }, { url = "https://files.pythonhosted.org/packages/3c/81/bd5f895919e32c6ab0a7786cd0c0ca961cb03152c47c3645808b54383f31/polars_runtime_32-1.39.3-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:363d49e3a3e638fc943e2b9887940300a7d06789930855a178a4727949259dc2", size = 43380176, upload-time = "2026-03-20T11:14:45.566Z" }, { url = "https://files.pythonhosted.org/packages/7a/3e/c86433c3b5ec0315bdfc7640d0c15d41f1216c0103a0eab9a9b5147d6c4c/polars_runtime_32-1.39.3-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7c206bdcc7bc62ea038d6adea8e44b02f0e675e0191a54c810703b4895208ea4", size = 46485933, upload-time = "2026-03-20T11:14:51.155Z" }, { url = "https://files.pythonhosted.org/packages/54/ce/200b310cf91f98e652eb6ea09fdb3a9718aa0293ebf113dce325797c8572/polars_runtime_32-1.39.3-cp310-abi3-win_amd64.whl", hash = "sha256:d66ca522517554a883446957539c40dc7b75eb0c2220357fb28bc8940d305339", size = 46995458, upload-time = "2026-03-20T11:14:56.074Z" }, { url = "https://files.pythonhosted.org/packages/da/76/2d48927e0aa2abbdde08cbf4a2536883b73277d47fbeca95e952de86df34/polars_runtime_32-1.39.3-cp310-abi3-win_arm64.whl", hash = "sha256:f49f51461de63f13e5dd4eb080421c8f23f856945f3f8bd5b2b1f59da52c2860", size = 41857648, upload-time = "2026-03-20T11:15:01.142Z" }, ] [[package]] name = "prompt-toolkit" version = "3.0.52" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wcwidth" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, ] [[package]] name = "ptyprocess" version = "0.7.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, ] [[package]] name = "pure-eval" version = "0.2.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, ] [[package]] name = "py-cpuinfo" version = "9.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/37/a8/d832f7293ebb21690860d2e01d8115e5ff6f2ae8bbdc953f0eb0fa4bd2c7/py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690", size = 104716, upload-time = "2022-10-25T20:38:06.303Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5", size = 22335, upload-time = "2022-10-25T20:38:27.636Z" }, ] [[package]] name = "pycodestyle" version = "2.14.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472, upload-time = "2025-06-20T18:49:48.75Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" }, ] [[package]] name = "pydantic" version = "2.12.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, { name = "pydantic-core" }, { name = "typing-extensions" }, { name = "typing-inspection" }, ] sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, ] [[package]] name = "pydantic-core" version = "2.41.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, ] [[package]] name = "pyflakes" version = "3.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/45/dc/fd034dc20b4b264b3d015808458391acbf9df40b1e54750ef175d39180b1/pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58", size = 64669, upload-time = "2025-06-20T18:45:27.834Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" }, ] [[package]] name = "pygments" version = "2.19.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] [[package]] name = "pytest" version = "9.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, ] [[package]] name = "pytest-benchmark" version = "5.2.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "py-cpuinfo" }, { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/24/34/9f732b76456d64faffbef6232f1f9dbec7a7c4999ff46282fa418bd1af66/pytest_benchmark-5.2.3.tar.gz", hash = "sha256:deb7317998a23c650fd4ff76e1230066a76cb45dcece0aca5607143c619e7779", size = 341340, upload-time = "2025-11-09T18:48:43.215Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/33/29/e756e715a48959f1c0045342088d7ca9762a2f509b945f362a316e9412b7/pytest_benchmark-5.2.3-py3-none-any.whl", hash = "sha256:bc839726ad20e99aaa0d11a127445457b4219bdb9e80a1afc4b51da7f96b0803", size = 45255, upload-time = "2025-11-09T18:48:39.765Z" }, ] [[package]] name = "pytest-cov" version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage", extra = ["toml"] }, { name = "pluggy" }, { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, ] [[package]] name = "python-dateutil" version = "2.9.0.post0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] [[package]] name = "python-discovery" version = "1.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, { name = "platformdirs" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9c/90/bcce6b46823c9bec1757c964dc37ed332579be512e17a30e9698095dcae4/python_discovery-1.2.0.tar.gz", hash = "sha256:7d33e350704818b09e3da2bd419d37e21e7c30db6e0977bb438916e06b41b5b1", size = 58055, upload-time = "2026-03-19T01:43:08.248Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c2/3c/2005227cb951df502412de2fa781f800663cccbef8d90ec6f1b371ac2c0d/python_discovery-1.2.0-py3-none-any.whl", hash = "sha256:1e108f1bbe2ed0ef089823d28805d5ad32be8e734b86a5f212bf89b71c266e4a", size = 31524, upload-time = "2026-03-19T01:43:07.045Z" }, ] [[package]] name = "python-dotenv" version = "1.2.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, ] [[package]] name = "pytz" version = "2026.1.post1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/56/db/b8721d71d945e6a8ac63c0fc900b2067181dbb50805958d4d4661cf7d277/pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", size = 321088, upload-time = "2026-03-03T07:47:50.683Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/10/99/781fe0c827be2742bcc775efefccb3b048a3a9c6ce9aec0cbf4a101677e5/pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a", size = 510489, upload-time = "2026-03-03T07:47:49.167Z" }, ] [[package]] name = "pyyaml" version = "6.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] [[package]] name = "requests" version = "2.33.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "charset-normalizer" }, { name = "idna" }, { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/34/64/8860370b167a9721e8956ae116825caff829224fbca0ca6e7bf8ddef8430/requests-2.33.0.tar.gz", hash = "sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652", size = 134232, upload-time = "2026-03-25T15:10:41.586Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl", hash = "sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b", size = 65017, upload-time = "2026-03-25T15:10:40.382Z" }, ] [[package]] name = "six" version = "1.17.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] [[package]] name = "snowballstemmer" version = "3.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, ] [[package]] name = "soupsieve" version = "2.8.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, ] [[package]] name = "sphinx" version = "8.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "alabaster" }, { name = "babel" }, { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "docutils" }, { name = "imagesize" }, { name = "jinja2" }, { name = "packaging" }, { name = "pygments" }, { name = "requests" }, { name = "snowballstemmer" }, { name = "sphinxcontrib-applehelp" }, { name = "sphinxcontrib-devhelp" }, { name = "sphinxcontrib-htmlhelp" }, { name = "sphinxcontrib-jsmath" }, { name = "sphinxcontrib-qthelp" }, { name = "sphinxcontrib-serializinghtml" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611, upload-time = "2024-10-13T20:27:13.93Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", size = 3487125, upload-time = "2024-10-13T20:27:10.448Z" }, ] [[package]] name = "sphinx-basic-ng" version = "1.0.0b2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "sphinx" }, ] sdist = { url = "https://files.pythonhosted.org/packages/98/0b/a866924ded68efec7a1759587a4e478aec7559d8165fac8b2ad1c0e774d6/sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9", size = 20736, upload-time = "2023-07-08T18:40:54.166Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3c/dd/018ce05c532a22007ac58d4f45232514cd9d6dd0ee1dc374e309db830983/sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b", size = 22496, upload-time = "2023-07-08T18:40:52.659Z" }, ] [[package]] name = "sphinx-last-updated-by-git" version = "0.3.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "sphinx" }, ] sdist = { url = "https://files.pythonhosted.org/packages/03/fd/de1685b6dab173dff31da24e0d3b29f02873fc24a1cdbb7678721ddc8581/sphinx_last_updated_by_git-0.3.8.tar.gz", hash = "sha256:c145011f4609d841805b69a9300099fc02fed8f5bb9e5bcef77d97aea97b7761", size = 10785, upload-time = "2024-08-11T07:15:54.601Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e1/fb/e496f16fa11fbe2dbdd0b5e306ede153dfed050aae4766fc89d500720dc7/sphinx_last_updated_by_git-0.3.8-py3-none-any.whl", hash = "sha256:6382c8285ac1f222483a58569b78c0371af5e55f7fbf9c01e5e8a72d6fdfa499", size = 8580, upload-time = "2024-08-11T07:15:53.244Z" }, ] [[package]] name = "sphinx-sitemap" version = "2.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "sphinx-last-updated-by-git" }, ] sdist = { url = "https://files.pythonhosted.org/packages/61/17/56fe0f65e3567f829b2b4153a622be1d4b222b781e0d90d7db5a7738f30f/sphinx_sitemap-2.9.0.tar.gz", hash = "sha256:70f97bcdf444e3d68e118355cf82a1f54c4d3c03d651cd17fe87398b26e25e21", size = 6978, upload-time = "2025-10-06T00:24:00.036Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e4/94/3c57e8b1985e755c48972e2ecd59526d4bf0b52a1fe805bc52a8e98cb92d/sphinx_sitemap-2.9.0-py3-none-any.whl", hash = "sha256:f1f1d3a9ad012ba17a7ef0b560d303bff2d0db26647567d6e810bcc754466664", size = 6218, upload-time = "2025-10-06T00:23:58.778Z" }, ] [[package]] name = "sphinxcontrib-applehelp" version = "2.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, ] [[package]] name = "sphinxcontrib-devhelp" version = "2.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, ] [[package]] name = "sphinxcontrib-htmlhelp" version = "2.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, ] [[package]] name = "sphinxcontrib-jsmath" version = "1.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, ] [[package]] name = "sphinxcontrib-qthelp" version = "2.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, ] [[package]] name = "sphinxcontrib-serializinghtml" version = "2.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, ] [[package]] name = "sphinxemoji" version = "0.3.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "sphinx" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ad/05/d531d8ce28eeb364e900dab98b8e236cf664a1b7f5fa93c212a5e87313aa/sphinxemoji-0.3.2.tar.gz", hash = "sha256:814da89a9a7603b7e4d85c487c7381656fa83632f20065e5ed782ab1dbbb5083", size = 45999, upload-time = "2025-12-15T12:01:56.885Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/32/12/ff5e52d884b218ee9807e25ebb779a544809d7f0da9e01326a6d273c0aa1/sphinxemoji-0.3.2-py3-none-any.whl", hash = "sha256:0fb07a95bca5960a3b312bc05b65b0c3040456414a076c363f4ecaf546c6e09e", size = 45454, upload-time = "2025-12-15T12:01:55.635Z" }, ] [[package]] name = "stack-data" version = "0.6.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asttokens" }, { name = "executing" }, { name = "pure-eval" }, ] sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, ] [[package]] name = "tomli" version = "2.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, ] [[package]] name = "tomli-w" version = "1.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, ] [[package]] name = "traitlets" version = "5.14.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, ] [[package]] name = "typing-extensions" version = "4.15.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] [[package]] name = "typing-inspection" version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] [[package]] name = "tzdata" version = "2025.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, ] [[package]] name = "urllib3" version = "2.6.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ] [[package]] name = "uuid6" version = "2025.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ca/b7/4c0f736ca824b3a25b15e8213d1bcfc15f8ac2ae48d1b445b310892dc4da/uuid6-2025.0.1.tar.gz", hash = "sha256:cd0af94fa428675a44e32c5319ec5a3485225ba2179eefcf4c3f205ae30a81bd", size = 13932, upload-time = "2025-07-04T18:30:35.186Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3d/b2/93faaab7962e2aa8d6e174afb6f76be2ca0ce89fde14d3af835acebcaa59/uuid6-2025.0.1-py3-none-any.whl", hash = "sha256:80530ce4d02a93cdf82e7122ca0da3ebbbc269790ec1cb902481fa3e9cc9ff99", size = 6979, upload-time = "2025-07-04T18:30:34.001Z" }, ] [[package]] name = "virtualenv" version = "21.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, { name = "filelock" }, { name = "platformdirs" }, { name = "python-discovery" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/aa/92/58199fe10049f9703c2666e809c4f686c54ef0a68b0f6afccf518c0b1eb9/virtualenv-21.2.0.tar.gz", hash = "sha256:1720dc3a62ef5b443092e3f499228599045d7fea4c79199770499df8becf9098", size = 5840618, upload-time = "2026-03-09T17:24:38.013Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c6/59/7d02447a55b2e55755011a647479041bc92a82e143f96a8195cb33bd0a1c/virtualenv-21.2.0-py3-none-any.whl", hash = "sha256:1bd755b504931164a5a496d217c014d098426cddc79363ad66ac78125f9d908f", size = 5825084, upload-time = "2026-03-09T17:24:35.378Z" }, ] [[package]] name = "wcwidth" version = "0.6.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, ]