deepdiff-rust 0.1.0__cp39-abi3-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,93 @@
1
+ """deepdiff 9.1.0's value-normalization options, as the native diff takes them."""
2
+
3
+ import datetime
4
+ import decimal
5
+ import fractions
6
+ import uuid
7
+
8
+ # deepdiff's `numbers` and `strings`, plus numpy scalars when numpy is importable.
9
+ _NUMBERS = [
10
+ int, float, complex, decimal.Decimal, fractions.Fraction,
11
+ datetime.datetime, datetime.date, datetime.timedelta, datetime.time,
12
+ ]
13
+ try:
14
+ import numpy as _np
15
+
16
+ _NUMBERS.extend(
17
+ t for t in (
18
+ getattr(_np, "integer", None),
19
+ getattr(_np, "floating", None),
20
+ getattr(_np, "complexfloating", None),
21
+ )
22
+ if t is not None
23
+ )
24
+ except ImportError:
25
+ pass
26
+ _NUMBERS = tuple(_NUMBERS)
27
+ _STRINGS = (str, bytes, memoryview)
28
+ _UUID_STR = (uuid.UUID, str)
29
+ _ZEROED = ("hour", "minute", "second", "microsecond")
30
+ _TRUNCATE_FROM = {"day": 0, "hour": 1, "minute": 2, "second": 3}
31
+
32
+
33
+ def _type_groups(groups, ignore_string_type_changes, ignore_numeric_type_changes, ignore_uuid_types):
34
+ """`get_ignore_types_in_groups`: each group a tuple of types."""
35
+ if groups and isinstance(groups[0], type):
36
+ groups = [groups]
37
+ result = [
38
+ tuple(type(item) if item is None or not isinstance(item, type) else item for item in group)
39
+ for group in groups or []
40
+ ]
41
+ if ignore_string_type_changes and _STRINGS not in result:
42
+ result.append(_STRINGS)
43
+ if ignore_numeric_type_changes and _NUMBERS not in result:
44
+ result.append(_NUMBERS)
45
+ if ignore_uuid_types and _UUID_STR not in result:
46
+ result.append(_UUID_STR)
47
+ return result
48
+
49
+
50
+ def native_options(
51
+ builtin_types, *, significant_digits, number_format_notation, math_epsilon, use_log_scale,
52
+ log_scale_similarity_threshold, ignore_nan_inequality, ignore_string_case,
53
+ ignore_numeric_type_changes, ignore_string_type_changes, ignore_type_in_groups,
54
+ ignore_type_subclasses, truncate_datetime, ignore_uuid_types=False,
55
+ ):
56
+ """The native `DeepDiff` keyword arguments for these options."""
57
+ if significant_digits is not None and significant_digits < 0:
58
+ raise ValueError("significant_digits must be None or a non-negative integer")
59
+ if significant_digits is None and ignore_numeric_type_changes:
60
+ significant_digits = 12
61
+ if significant_digits is not None and number_format_notation not in ("f", "e"):
62
+ raise ValueError(
63
+ f"number_format_notation got invalid value of {number_format_notation}. "
64
+ "The valid values are 'f' and 'e'"
65
+ )
66
+ if truncate_datetime not in {None, *_TRUNCATE_FROM}:
67
+ raise ValueError("truncate_datetime must be second, minute, hour or day")
68
+ options = {
69
+ "truncate_datetime": truncate_datetime
70
+ and dict.fromkeys(_ZEROED[_TRUNCATE_FROM[truncate_datetime]:], 0),
71
+ "significant_digits": significant_digits,
72
+ "exponent_notation": number_format_notation == "e",
73
+ "math_epsilon": math_epsilon,
74
+ "log_scale_threshold": log_scale_similarity_threshold if use_log_scale else None,
75
+ "ignore_nan_inequality": ignore_nan_inequality,
76
+ "ignore_string_case": ignore_string_case,
77
+ "ignore_numeric_type_changes": ignore_numeric_type_changes,
78
+ }
79
+ groups = _type_groups(
80
+ ignore_type_in_groups, ignore_string_type_changes, ignore_numeric_type_changes,
81
+ ignore_uuid_types,
82
+ )
83
+ if groups:
84
+ # ponytail: a u64 mask, so at most 64 groups.
85
+ def group_mask(cls):
86
+ return sum(
87
+ 1 << i for i, group in enumerate(groups)
88
+ if (cls in group if ignore_type_subclasses else issubclass(cls, group))
89
+ )
90
+
91
+ options["type_groups"] = {name: group_mask(cls) for name, cls in builtin_types.items()}
92
+ options["group_mask"] = group_mask
93
+ return options
@@ -0,0 +1,152 @@
1
+ """Path filters: deepdiff 9.1.0's `_skip_this` over rendered path strings."""
2
+
3
+ import ast
4
+ import re
5
+ from functools import cache
6
+
7
+ _WILDCARD_RE = re.compile(r"\[\*\*?\]|\.\*\*?(?=[.\[]|$)")
8
+ _ONE, _ANY = object(), object()
9
+
10
+
11
+ def _element(text, literal):
12
+ if text in ("*", "**"):
13
+ return _ONE if text == "*" else _ANY
14
+ if not literal:
15
+ return text
16
+ try:
17
+ return ast.literal_eval(text)
18
+ except (ValueError, SyntaxError):
19
+ return text
20
+
21
+
22
+ @cache
23
+ def _elements(path):
24
+ """`root['a'][0].b` -> ('a', 0, 'b'); `*`/`**` become wildcard markers."""
25
+ out, i, n = [], 4, len(path)
26
+ while i < n:
27
+ j = i + 1
28
+ if path[i] == ".":
29
+ while j < n and path[j] not in ".[":
30
+ j += 1
31
+ out.append(_element(path[i + 1 : j], literal=False))
32
+ i = j
33
+ continue
34
+ quote = None
35
+ while j < n and (quote or path[j] != "]"):
36
+ if quote and path[j] == "\\":
37
+ j += 1
38
+ elif path[j] == quote:
39
+ quote = None
40
+ elif not quote and path[j] in "'\"":
41
+ quote = path[j]
42
+ j += 1
43
+ out.append(_element(path[i + 1 : j], literal=True))
44
+ i = j + 1
45
+ return tuple(out)
46
+
47
+
48
+ def _matches(pattern, target, prefix):
49
+ """`pattern` matches `target`, or (`prefix`) some prefix of it."""
50
+
51
+ @cache
52
+ def go(p, t):
53
+ if p == len(pattern):
54
+ return prefix or t == len(target)
55
+ if pattern[p] is _ANY:
56
+ return any(go(p + 1, k) for k in range(t, len(target) + 1))
57
+ if t == len(target):
58
+ return False
59
+ return (pattern[p] is _ONE or pattern[p] == target[t]) and go(p + 1, t + 1)
60
+
61
+ return go(0, 0)
62
+
63
+
64
+ def _could_reach(pattern, target):
65
+ """`target` is an ancestor of some path `pattern` matches."""
66
+
67
+ @cache
68
+ def go(p, t):
69
+ if t == len(target):
70
+ return p < len(pattern)
71
+ if p == len(pattern):
72
+ return False
73
+ if pattern[p] is _ANY:
74
+ return go(p + 1, t) or go(p, t + 1)
75
+ return (pattern[p] is _ONE or pattern[p] == target[t]) and go(p + 1, t + 1)
76
+
77
+ return go(0, 0)
78
+
79
+
80
+ def _root_paths(paths):
81
+ """deepdiff's `add_root_to_paths`: a bare key also means `root[key]`."""
82
+ result = set()
83
+ for path in paths:
84
+ if path.startswith("root"):
85
+ result.add(path)
86
+ elif path.isdigit():
87
+ result |= {f"root['{path}']", f"root[{path}]"}
88
+ elif path[0].isdigit():
89
+ result.add(f"root['{path}']")
90
+ else:
91
+ result |= {f"root.{path}", f"root['{path}']"}
92
+ return result
93
+
94
+
95
+ def _split(paths):
96
+ """(exact paths, glob patterns as element tuples), as deepdiff separates them."""
97
+ if not paths:
98
+ return set(), []
99
+ paths = {paths} if isinstance(paths, str) else set(paths)
100
+ globs = []
101
+ for path in paths:
102
+ if _WILDCARD_RE.search(path):
103
+ if not path.startswith("root"):
104
+ raise ValueError(f"Wildcard paths must start with 'root'. Got: {path}")
105
+ globs.append(_elements(path))
106
+ exact = {path for path in paths if not _WILDCARD_RE.search(path)}
107
+ return _root_paths(exact), globs
108
+
109
+
110
+ def _regexes(items):
111
+ if not items:
112
+ return []
113
+ items = [items] if isinstance(items, (str, re.Pattern)) else list(items)
114
+ return [item if isinstance(item, re.Pattern) else re.compile(item) for item in items]
115
+
116
+
117
+ def skip_filter(exclude_paths, include_paths, exclude_regex_paths, exclude_types):
118
+ """`skip(prefixes, typed)` for a finding: `prefixes` are its path's rendered
119
+ prefixes from `root`, `typed[k]` whether `prefixes[k]` holds an
120
+ `exclude_types` instance. `None` when nothing filters."""
121
+ ex_exact, ex_globs = _split(exclude_paths)
122
+ in_exact, in_globs = _split(include_paths)
123
+ regexes = _regexes(exclude_regex_paths)
124
+ if not (ex_exact or ex_globs or in_exact or in_globs or regexes or exclude_types):
125
+ return None
126
+
127
+ def included(path):
128
+ return path in in_exact or any(p in path or path in p for p in in_exact)
129
+
130
+ @cache
131
+ def skip_level(path, typed):
132
+ if path in ex_exact or any(_matches(g, _elements(path), False) for g in ex_globs):
133
+ return True
134
+ if (in_exact or in_globs) and path != "root":
135
+ if included(path):
136
+ return False
137
+ target = _elements(path)
138
+ return not any(_matches(g, target, False) or _could_reach(g, target) for g in in_globs)
139
+ if any(regex.search(path) for regex in regexes):
140
+ return True
141
+ return typed
142
+
143
+ def skip_report(path):
144
+ # `_skip_report_for_include_glob`: a glob ancestor is walked, not reported.
145
+ if not in_globs or path in in_exact or any(p in path for p in in_exact):
146
+ return False
147
+ return not any(_matches(g, _elements(path), True) for g in in_globs)
148
+
149
+ def skip(prefixes, typed):
150
+ return any(map(skip_level, prefixes, typed)) or skip_report(prefixes[-1])
151
+
152
+ return skip
deepdiff_rust/_view.py ADDED
@@ -0,0 +1,131 @@
1
+ """pretty() formatting and a minimal view='tree' DiffLevel."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class DiffLevel:
7
+ """Minimal DeepDiff tree node: path, t1/t2, report_type."""
8
+
9
+ __slots__ = ("t1", "t2", "report_type", "_path", "additional")
10
+
11
+ def __init__(self, path, t1, t2, report_type):
12
+ self._path = path
13
+ self.t1 = t1
14
+ self.t2 = t2
15
+ self.report_type = report_type
16
+ self.additional = {}
17
+
18
+ def path(self, root="root", **_kwargs):
19
+ if root == "root":
20
+ return self._path
21
+ if self._path == "root":
22
+ return root
23
+ return root + self._path[4:]
24
+
25
+ def __repr__(self):
26
+ return f"<{self._path} t1:{self.t1!r}, t2:{self.t2!r}>"
27
+
28
+
29
+ _PRETTY = {
30
+ "type_changes": (
31
+ "Type of {diff_path} changed from {type_t1} to {type_t2} "
32
+ "and value changed from {val_t1} to {val_t2}."
33
+ ),
34
+ "values_changed": "Value of {diff_path} changed from {val_t1} to {val_t2}.",
35
+ "dictionary_item_added": "Item {diff_path} added to dictionary.",
36
+ "dictionary_item_removed": "Item {diff_path} removed from dictionary.",
37
+ "iterable_item_added": "Item {diff_path} added to iterable.",
38
+ "iterable_item_removed": "Item {diff_path} removed from iterable.",
39
+ "attribute_added": "Attribute {diff_path} added.",
40
+ "attribute_removed": "Attribute {diff_path} removed.",
41
+ "set_item_added": "Item root[{val_t2}] added to set.",
42
+ "set_item_removed": "Item root[{val_t1}] removed from set.",
43
+ "repetition_change": "Repetition change for item {diff_path}.",
44
+ "iterable_item_moved": "Item {diff_path} moved to {val_t2}.",
45
+ }
46
+
47
+ _PRETTY_V2 = {
48
+ **_PRETTY,
49
+ "dictionary_item_added": "Item {diff_path} ({val_t2}) added to dictionary.",
50
+ "dictionary_item_removed": "Item {diff_path} ({val_t1}) removed from dictionary.",
51
+ "iterable_item_added": "Item {diff_path} ({val_t2}) added to iterable.",
52
+ "iterable_item_removed": "Item {diff_path} ({val_t1}) removed from iterable.",
53
+ "attribute_added": "Attribute {diff_path} ({val_t2}) added.",
54
+ "attribute_removed": "Attribute {diff_path} ({val_t1}) removed.",
55
+ }
56
+
57
+
58
+ def _fmt_val(value):
59
+ if isinstance(value, str):
60
+ return f'"{value}"'
61
+ return str(value)
62
+
63
+
64
+ def _iter_findings(findings):
65
+ if isinstance(findings, dict):
66
+ yield from findings.items()
67
+ else:
68
+ for path in findings:
69
+ yield path, None
70
+
71
+
72
+ def _level_from_finding(report_type, path, payload):
73
+ if report_type == "values_changed" and isinstance(payload, dict):
74
+ return DiffLevel(path, payload.get("old_value"), payload.get("new_value"), report_type)
75
+ if report_type == "type_changes" and isinstance(payload, dict):
76
+ return DiffLevel(path, payload.get("old_value"), payload.get("new_value"), report_type)
77
+ if report_type == "iterable_item_moved" and isinstance(payload, dict):
78
+ return DiffLevel(path, payload.get("value"), payload.get("new_path"), report_type)
79
+ if report_type == "repetition_change" and isinstance(payload, dict):
80
+ return DiffLevel(path, payload.get("value"), payload.get("value"), report_type)
81
+ if report_type.endswith("_added"):
82
+ return DiffLevel(path, None, payload, report_type)
83
+ if report_type.endswith("_removed"):
84
+ return DiffLevel(path, payload, None, report_type)
85
+ return DiffLevel(path, payload, payload, report_type)
86
+
87
+
88
+ def build_tree(report: dict, verbose_level: int = 1) -> dict:
89
+ """text-report dict → {category: [DiffLevel, ...]} like DeepDiff.tree."""
90
+ tree = {}
91
+ for report_type, findings in report.items():
92
+ if report_type == "deep_distance":
93
+ continue
94
+ levels = [
95
+ _level_from_finding(report_type, path, payload)
96
+ for path, payload in _iter_findings(findings)
97
+ ]
98
+ if levels:
99
+ tree[report_type] = levels
100
+ return tree
101
+
102
+
103
+ def tree_as_result(tree: dict) -> dict:
104
+ """DeepDiff view='tree' shape: category → list of DiffLevel."""
105
+ return {key: list(levels) for key, levels in tree.items()}
106
+
107
+
108
+ def pretty_from_tree(tree: dict, verbose_level: int = 1, prefix=None) -> str:
109
+ forms = _PRETTY_V2 if verbose_level == 2 else _PRETTY
110
+ if prefix is None:
111
+ prefix = ""
112
+ lines = []
113
+ for report_type in sorted(tree):
114
+ for level in tree[report_type]:
115
+ template = forms.get(level.report_type, "")
116
+ if not template:
117
+ continue
118
+ type_t1 = type(level.t1).__name__
119
+ type_t2 = type(level.t2).__name__
120
+ line = template.format(
121
+ diff_path=level.path(),
122
+ type_t1=type_t1,
123
+ type_t2=type_t2,
124
+ val_t1=_fmt_val(level.t1),
125
+ val_t2=_fmt_val(level.t2),
126
+ )
127
+ if callable(prefix):
128
+ lines.append(f"{prefix(diff=level)}{line}")
129
+ else:
130
+ lines.append(f"{prefix}{line}")
131
+ return "\n".join(lines)
deepdiff_rust/py.typed ADDED
File without changes
@@ -0,0 +1,191 @@
1
+ Metadata-Version: 2.4
2
+ Name: deepdiff-rust
3
+ Version: 0.1.0
4
+ Classifier: Development Status :: 3 - Alpha
5
+ Classifier: Intended Audience :: Developers
6
+ Classifier: License :: OSI Approved :: MIT License
7
+ Classifier: Operating System :: MacOS
8
+ Classifier: Operating System :: Microsoft :: Windows
9
+ Classifier: Operating System :: POSIX :: Linux
10
+ Classifier: Programming Language :: Rust
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Programming Language :: Python :: Implementation :: CPython
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Requires-Dist: pytest>=8 ; extra == 'test'
21
+ Requires-Dist: deepdiff==9.1.0 ; extra == 'test'
22
+ Provides-Extra: test
23
+ License-File: LICENSE
24
+ License-File: LICENSE-ONIX
25
+ Summary: Rust-backed DeepDiff-compatible comparison for Python
26
+ Keywords: deepdiff,diff,rust,performance,comparison
27
+ Author-email: viktormokhov <110980778+viktormokhov@users.noreply.github.com>
28
+ License: MIT
29
+ Requires-Python: >=3.10
30
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
31
+ Project-URL: Homepage, https://github.com/viktormokhov/deepdiff-rust
32
+ Project-URL: Issues, https://github.com/viktormokhov/deepdiff-rust/issues
33
+ Project-URL: Repository, https://github.com/viktormokhov/deepdiff-rust
34
+
35
+ # deepdiff-rust
36
+
37
+ Rust-backed [DeepDiff](https://github.com/seperman/deepdiff) for Python.
38
+ Forked from [Onix](https://github.com/ksco92/onix) (`deepdiff-rs`) and extended
39
+ toward full `deepdiff==9.1.0` parity for the comparison API.
40
+
41
+ - **Distribution:** `deepdiff-rust`
42
+ - **Import:** `deepdiff_rust` (does not shadow PyPI `deepdiff` or `deepdiff-rs`)
43
+ - **Runtime deps:** none (CPython 3.10+)
44
+ - **Branch:** `fork-onix` is the active development line
45
+
46
+ ```python
47
+ from deepdiff_rust import DeepDiff
48
+
49
+ print(DeepDiff({"a": 1, "b": [1, 2]}, {"a": 2, "b": [1, 3]}))
50
+ # {'values_changed': {"root['a']": {...}, "root['b'][1]": {...}}}
51
+ ```
52
+
53
+ ## Install
54
+
55
+ From a local wheel (Windows / manylinux x86_64 abi3):
56
+
57
+ ```bash
58
+ pip install --force-reinstall --no-deps path/to/deepdiff_rust-0.1.0-*.whl
59
+ ```
60
+
61
+ From source (Rust toolchain + Maturin):
62
+
63
+ ```bash
64
+ git clone https://github.com/viktormokhov/deepdiff-rust.git
65
+ cd deepdiff-rust
66
+ git checkout fork-onix
67
+ python -m venv .venv
68
+ source .venv/bin/activate # Windows: .venv\Scripts\activate
69
+ pip install maturin
70
+ maturin develop --release
71
+ ```
72
+
73
+ ## Status
74
+
75
+ **Core `DeepDiff(...)` comparison is largely done** and differentially tested
76
+ against live `deepdiff==9.1.0`. Not a complete drop-in for the whole DeepDiff
77
+ *package* yet (no DeepHash / DeepSearch / Delta).
78
+
79
+ ### Supported (tested vs DeepDiff 9.1.0)
80
+
81
+ | Area | Options / behavior |
82
+ |---|---|
83
+ | Result API | `dict`-like result, `to_dict` / `to_json`, `affected_paths`, `affected_root_keys`, `t1` / `t2`, `verbose_level` 0/1/2 |
84
+ | Order | `ignore_order`, `zip_ordered_iterables`, pairing cutoffs, `threshold_to_diff_deeper`, `report_repetition`, `group_by` |
85
+ | Limits | `max_diffs`, `max_passes`, `max_depth` (extension beyond DeepDiff) |
86
+ | Numbers / strings | `significant_digits`, `math_epsilon`, `use_log_scale`, `ignore_nan_inequality`, `ignore_string_case`, type-ignore groups |
87
+ | Filters | `exclude_paths` / `include_paths` (incl. `*`/`**`), `exclude_regex_paths`, `exclude_types`, `ignore_private_variables` |
88
+ | Callbacks | `exclude_obj_callback`(+`_strict`), `include_obj_callback`(+`_strict`), `iterable_compare_func`, `custom_operators` |
89
+ | Types | `None`, bool, int, float, str, bytes, list/tuple/set/frozenset, dict, Decimal, UUID, complex, datetime/date/time/timedelta, Enum / IntEnum (`use_enum_value`), numpy int/float scalars + `ndarray`, many custom objects by attributes |
90
+ | Other | `get_deep_distance`, `view='tree'`, `pretty()`, `diff_json` (JSON fast path in Rust) |
91
+
92
+ Details and known divergences: [docs/compatibility.md](docs/compatibility.md).
93
+
94
+ ### Not yet
95
+
96
+ - `DeepHash` / `hasher`, `DeepSearch`, `Delta` / patch
97
+ - numpy `bool_` / `datetime64` / complex scalars
98
+ - Onix extras we did not port: `diff_tables` (Arrow), `onix` CLI
99
+ - Some exotic report shapes for failed/opaque objects may still differ from DeepDiff
100
+
101
+ ## vs `deepdiff-rs` (Onix) and `deepdiff`
102
+
103
+ | | `deepdiff` | `deepdiff-rs` (Onix) | **this package** |
104
+ |---|---|---|---|
105
+ | Engine | pure Python | Rust | Rust (Onix fork) |
106
+ | Import | `deepdiff` | `deepdiff_rs` | `deepdiff_rust` |
107
+ | Pip runtime deps | several | none (+ optional pyarrow) | none |
108
+ | Full DeepDiff option set | yes | core only | most comparison options |
109
+ | DeepHash / Delta / Search | yes | no | no (planned) |
110
+ | Arrow `diff_tables` | no | yes | no |
111
+
112
+ Keep stock `deepdiff` installed only if you want differential tests or a reference oracle.
113
+
114
+ ## Architecture
115
+
116
+ - `crates/deepdiff-core` — comparison engine, paths, reports
117
+ - `crates/deepdiff-python` — PyO3 extension (`deepdiff_rust._native`)
118
+ - `python/deepdiff_rust` — public Python API
119
+ - `tests` — API + differential tests against `deepdiff==9.1.0`
120
+ - `docs/` — compatibility matrix and design notes
121
+
122
+ Live Python objects are converted into a native value model, diffed in Rust,
123
+ then rendered as DeepDiff-compatible reports. JSON strings can skip the
124
+ Python object round-trip via `diff_json`.
125
+
126
+ ## Development
127
+
128
+ ```bash
129
+ pip install -e ".[test]" # pytest + deepdiff==9.1.0
130
+ cargo test --workspace
131
+ cargo fmt --all --check
132
+ cargo clippy --workspace --all-targets --all-features -- -D warnings
133
+ maturin develop --release
134
+ pytest -q
135
+ ```
136
+
137
+ ## Publishing to PyPI
138
+
139
+ Workflow [`.github/workflows/publish.yml`](.github/workflows/publish.yml) builds
140
+ abi3 wheels (Linux x86_64/aarch64, Windows x64, macOS Intel/ARM) plus an sdist.
141
+
142
+ ### Secrets (GitHub → Settings → Secrets and variables → Actions)
143
+
144
+ | Secret | Purpose |
145
+ |---|---|
146
+ | `TEST_PYPI_API_TOKEN` | Upload to https://test.pypi.org |
147
+ | `PYPI_API_TOKEN` | Upload to https://pypi.org |
148
+
149
+ Create tokens on TestPyPI / PyPI (Account → API tokens). Store the raw token
150
+ value; maturin authenticates as user `__token__`.
151
+
152
+ ### Dry run on TestPyPI
153
+
154
+ 1. Register on https://test.pypi.org and create a token → secret
155
+ `TEST_PYPI_API_TOKEN`.
156
+ 2. **Actions → Publish → Run workflow** → target `testpypi`.
157
+ 3. Install and smoke-test:
158
+
159
+ ```bash
160
+ pip install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ deepdiff-rust==0.1.0
161
+ python -c "from deepdiff_rust import DeepDiff; print(DeepDiff({'a':1},{'a':2}))"
162
+ ```
163
+
164
+ (`--extra-index-url` is only needed if a future release gains PyPI dependencies.)
165
+
166
+ ### Production release
167
+
168
+ 1. Version in `pyproject.toml` and `[workspace.package]` must match the tag
169
+ without the leading `v` (e.g. `0.1.0` ↔ `v0.1.0`).
170
+ 2. Push the tag (or run the workflow with target `pypi`):
171
+
172
+ ```bash
173
+ git tag v0.1.0
174
+ git push origin v0.1.0
175
+ ```
176
+
177
+ 3. After the job finishes: `pip install deepdiff-rust`.
178
+
179
+ A version string can be uploaded to PyPI only once — fix mistakes with a new
180
+ version number.
181
+
182
+ ## Third-party code
183
+
184
+ Comparison engine and Python conversion modules are forked from Onix,
185
+ Copyright (c) 2026 Rodrigo Carvajal, MIT. See
186
+ [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) and [LICENSE-ONIX](LICENSE-ONIX).
187
+
188
+ ## License
189
+
190
+ MIT. Upstream licenses in third-party notices still apply.
191
+
@@ -0,0 +1,17 @@
1
+ deepdiff_rust/__init__.py,sha256=q3PDPu7ZJBqo9_T-birDkHjvY-p91ULOd3punAxKvi8,260
2
+ deepdiff_rust/__init__.pyi,sha256=NaPyHP4kLZINdwsa-gAKVYeLAFGP1wEqdZ1hQmdRHDo,3326
3
+ deepdiff_rust/_callbacks.py,sha256=61TpMIQdYQmMoJRw49pHCHIbAbdWtp_fc6e_wNfyTKA,3008
4
+ deepdiff_rust/_diff.py,sha256=p872e2pnYoFyA8kL6RnRTRnBGxQ14tuAGNaP4uTNUZs,14170
5
+ deepdiff_rust/_dual.py,sha256=56m4Vfx0BE1MCkOCbg5fGTBpjNmR7x_tElVbg7-0QRY,4697
6
+ deepdiff_rust/_group_by.py,sha256=tjSQah_qNcCtkUzlnUTe49WVbeCpCparw55g1mWIGp8,1993
7
+ deepdiff_rust/_native.pyd,sha256=E_gn4hQiNANnYK4CgW2dHylKXtUWwXZDMzMOyq9EJfU,1552384
8
+ deepdiff_rust/_options.py,sha256=VReNz-JZjHfBh8kxHYYAeXE-V2Ixfz1rQgb3TwAFaqY,3868
9
+ deepdiff_rust/_paths.py,sha256=aXa90axI6yVv0mhI3dbc32LGP6gWvOBD-l_7XJsqLtA,5162
10
+ deepdiff_rust/_view.py,sha256=icf6kZoThV9Tya55JUulxqce6ThRoJ-3CzPg045dg1s,5070
11
+ deepdiff_rust/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ deepdiff_rust-0.1.0.dist-info/METADATA,sha256=tEe1Q2FOc6xXP-8E9YVTfhIrzOyTNScqeatzG9hWOgU,7558
13
+ deepdiff_rust-0.1.0.dist-info/WHEEL,sha256=xw8F9xBWUOgm5ym2S9tihj_bLJOb723bjhA0kdANo2I,95
14
+ deepdiff_rust-0.1.0.dist-info/licenses/LICENSE,sha256=bBESMrPM6AtsqS38rSgpBMXoBLhBRMDcE_bHw_Ivie0,1091
15
+ deepdiff_rust-0.1.0.dist-info/licenses/LICENSE-ONIX,sha256=-9RD2QdrTE077HE1bP2OPj29mVd9n63ywdnqxzsN5yg,1094
16
+ deepdiff_rust-0.1.0.dist-info/sboms/deepdiff-python.cyclonedx.json,sha256=nI7vnQda7asAVzuRkwxQfgCmbQm8AE87Jdcacr2NFWU,32576
17
+ deepdiff_rust-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: maturin (1.16.0)
3
+ Root-Is-Purelib: false
4
+ Tag: cp39-abi3-win_amd64
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Viktor Mokhov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rodrigo Carvajal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.