rustytree-xarray 0.3.0__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. rustytree_xarray-0.4.0/.github/dependabot.yml +73 -0
  2. {rustytree_xarray-0.3.0 → rustytree_xarray-0.4.0}/.github/workflows/ci.yml +7 -7
  3. {rustytree_xarray-0.3.0 → rustytree_xarray-0.4.0}/.github/workflows/release.yml +8 -8
  4. {rustytree_xarray-0.3.0 → rustytree_xarray-0.4.0}/CHANGELOG.md +143 -1
  5. {rustytree_xarray-0.3.0 → rustytree_xarray-0.4.0}/Cargo.lock +187 -72
  6. {rustytree_xarray-0.3.0 → rustytree_xarray-0.4.0}/Cargo.toml +4 -4
  7. {rustytree_xarray-0.3.0 → rustytree_xarray-0.4.0}/PKG-INFO +2 -2
  8. {rustytree_xarray-0.3.0 → rustytree_xarray-0.4.0}/docs/architecture.md +13 -2
  9. {rustytree_xarray-0.3.0 → rustytree_xarray-0.4.0}/pyproject.toml +4 -2
  10. {rustytree_xarray-0.3.0 → rustytree_xarray-0.4.0}/python/rustytree/__init__.py +1 -1
  11. {rustytree_xarray-0.3.0 → rustytree_xarray-0.4.0}/python/rustytree/_array.py +20 -2
  12. rustytree_xarray-0.4.0/src/array.rs +740 -0
  13. rustytree_xarray-0.4.0/src/dtype_dispatch.rs +82 -0
  14. {rustytree_xarray-0.3.0 → rustytree_xarray-0.4.0}/src/node.rs +4 -1
  15. {rustytree_xarray-0.3.0 → rustytree_xarray-0.4.0}/src/walk.rs +8 -8
  16. {rustytree_xarray-0.3.0 → rustytree_xarray-0.4.0}/tests/conftest.py +39 -0
  17. {rustytree_xarray-0.3.0 → rustytree_xarray-0.4.0}/tests/test_backend_entrypoint.py +2 -7
  18. rustytree_xarray-0.4.0/tests/test_chunks.py +238 -0
  19. {rustytree_xarray-0.3.0 → rustytree_xarray-0.4.0}/tests/test_eager_fetch.py +6 -9
  20. {rustytree_xarray-0.3.0 → rustytree_xarray-0.4.0}/tests/test_icechunk.py +9 -7
  21. {rustytree_xarray-0.3.0 → rustytree_xarray-0.4.0}/tests/test_lazy.py +20 -15
  22. {rustytree_xarray-0.3.0 → rustytree_xarray-0.4.0}/tests/test_phase1_scaffold.py +1 -1
  23. rustytree_xarray-0.4.0/tests/test_string_arrays.py +335 -0
  24. {rustytree_xarray-0.3.0 → rustytree_xarray-0.4.0}/tests/test_walk.py +25 -1
  25. rustytree_xarray-0.3.0/src/array.rs +0 -384
  26. rustytree_xarray-0.3.0/src/dtype_dispatch.rs +0 -82
  27. rustytree_xarray-0.3.0/tests/test_chunks.py +0 -98
  28. {rustytree_xarray-0.3.0 → rustytree_xarray-0.4.0}/.gitignore +0 -0
  29. {rustytree_xarray-0.3.0 → rustytree_xarray-0.4.0}/LICENSE +0 -0
  30. {rustytree_xarray-0.3.0 → rustytree_xarray-0.4.0}/README.md +0 -0
  31. {rustytree_xarray-0.3.0 → rustytree_xarray-0.4.0}/assets/logo-banner-dark.png +0 -0
  32. {rustytree_xarray-0.3.0 → rustytree_xarray-0.4.0}/assets/logo-banner-dark.svg +0 -0
  33. {rustytree_xarray-0.3.0 → rustytree_xarray-0.4.0}/assets/logo-banner-light.png +0 -0
  34. {rustytree_xarray-0.3.0 → rustytree_xarray-0.4.0}/assets/logo-banner-light.svg +0 -0
  35. {rustytree_xarray-0.3.0 → rustytree_xarray-0.4.0}/assets/logo.png +0 -0
  36. {rustytree_xarray-0.3.0 → rustytree_xarray-0.4.0}/docs/contributing.md +0 -0
  37. {rustytree_xarray-0.3.0 → rustytree_xarray-0.4.0}/docs/release-process.md +0 -0
  38. {rustytree_xarray-0.3.0 → rustytree_xarray-0.4.0}/docs/usage.md +0 -0
  39. {rustytree_xarray-0.3.0 → rustytree_xarray-0.4.0}/notebooks/klot_demo.ipynb +0 -0
  40. {rustytree_xarray-0.3.0 → rustytree_xarray-0.4.0}/python/rustytree/backend.py +0 -0
  41. {rustytree_xarray-0.3.0 → rustytree_xarray-0.4.0}/src/error.rs +0 -0
  42. {rustytree_xarray-0.3.0 → rustytree_xarray-0.4.0}/src/glob.rs +0 -0
  43. {rustytree_xarray-0.3.0 → rustytree_xarray-0.4.0}/src/icechunk_store.rs +0 -0
  44. {rustytree_xarray-0.3.0 → rustytree_xarray-0.4.0}/src/lib.rs +0 -0
  45. {rustytree_xarray-0.3.0 → rustytree_xarray-0.4.0}/src/py_credentials.rs +0 -0
  46. {rustytree_xarray-0.3.0 → rustytree_xarray-0.4.0}/src/runtime.rs +0 -0
  47. {rustytree_xarray-0.3.0 → rustytree_xarray-0.4.0}/src/store.rs +0 -0
  48. {rustytree_xarray-0.3.0 → rustytree_xarray-0.4.0}/src/url.rs +0 -0
  49. {rustytree_xarray-0.3.0 → rustytree_xarray-0.4.0}/tests/test_codecs.py +0 -0
  50. {rustytree_xarray-0.3.0 → rustytree_xarray-0.4.0}/tests/test_pickle.py +0 -0
  51. {rustytree_xarray-0.3.0 → rustytree_xarray-0.4.0}/tests/test_to_rust_source.py +0 -0
@@ -0,0 +1,73 @@
1
+ # Dependabot version updates. Opens PRs on a schedule; nothing auto-merges —
2
+ # every PR is reviewed and merged by hand (including the grouped github-actions
3
+ # one). Security updates (CVE-triggered PRs) are enabled separately at the repo
4
+ # level, independent of this schedule.
5
+ #
6
+ # Deliberately NOT managed here (bumped by hand — coordinated, cross-manifest):
7
+ # - icechunk / typetag: FFI lockstep. The Python `icechunk` (pyproject) must
8
+ # share a minor with the Rust `icechunk` (`=2.1.0`) and `typetag`
9
+ # (`=0.2.22`) the crate links — the msgpack `Session` bytes format is
10
+ # coupled across the boundary, so a solo bump breaks `Session::from_bytes`
11
+ # (the 2.0.5→2.0.6 skew behind #46). Bump all three together manually.
12
+ # - ruff: pinned in pyproject AND hardcoded in ci.yml (`uvx ruff@0.15.8`),
13
+ # which Dependabot can't edit; a solo bump desyncs the lint gate and can
14
+ # churn formatting. Bump pyproject + both ci.yml literals together.
15
+ # - dtolnay/rust-toolchain@stable, pypa/gh-action-pypi-publish@release/v1:
16
+ # branch refs, not semver tags — Dependabot leaves them alone (no rule
17
+ # needed).
18
+ version: 2
19
+
20
+ updates:
21
+ # GitHub Actions — the `uses:` version pins in .github/workflows/* (exactly
22
+ # the manual toil of #54). One grouped PR per run.
23
+ - package-ecosystem: github-actions
24
+ directory: "/"
25
+ schedule:
26
+ interval: weekly
27
+ labels: ["dependencies"]
28
+ commit-message:
29
+ prefix: ci
30
+ groups:
31
+ actions:
32
+ patterns: ["*"]
33
+
34
+ # Rust crates (Cargo.toml / Cargo.lock).
35
+ - package-ecosystem: cargo
36
+ directory: "/"
37
+ schedule:
38
+ interval: weekly
39
+ labels: ["dependencies"]
40
+ commit-message:
41
+ prefix: deps
42
+ open-pull-requests-limit: 5
43
+ groups:
44
+ # Keep the zarrs family (zarrs, zarrs_storage, zarrs_object_store,
45
+ # zarrs_icechunk) moving together — they share a release cadence.
46
+ zarrs:
47
+ patterns: ["zarrs*"]
48
+ # pyo3 and numpy track matched 0.x minors (pyo3 0.28 ↔ numpy 0.28).
49
+ pyo3:
50
+ patterns: ["pyo3", "numpy"]
51
+ # Everything else: fold minor/patch into one PR; majors get their own.
52
+ cargo-minor:
53
+ update-types: ["minor", "patch"]
54
+ ignore:
55
+ - dependency-name: "icechunk" # FFI lockstep — bump by hand (see header)
56
+ - dependency-name: "typetag" # FFI lockstep — bump by hand (see header)
57
+
58
+ # Python deps (pyproject.toml: project deps + the `dev` extra).
59
+ - package-ecosystem: pip
60
+ directory: "/"
61
+ schedule:
62
+ interval: weekly
63
+ labels: ["dependencies"]
64
+ commit-message:
65
+ prefix: deps
66
+ open-pull-requests-limit: 5
67
+ groups:
68
+ # Fold minor/patch into one PR; majors get their own.
69
+ python:
70
+ update-types: ["minor", "patch"]
71
+ ignore:
72
+ - dependency-name: "icechunk" # FFI lockstep — bump by hand (see header)
73
+ - dependency-name: "ruff" # also hardcoded in ci.yml — bump by hand
@@ -24,8 +24,8 @@ jobs:
24
24
  name: ruff (lint + format)
25
25
  runs-on: ubuntu-latest
26
26
  steps:
27
- - uses: actions/checkout@v4
28
- - uses: astral-sh/setup-uv@v4
27
+ - uses: actions/checkout@v7
28
+ - uses: astral-sh/setup-uv@v7
29
29
  - run: uvx 'ruff@0.15.8' check .
30
30
  - run: uvx 'ruff@0.15.8' format --check .
31
31
 
@@ -35,11 +35,11 @@ jobs:
35
35
  name: cargo (fmt, clippy, test)
36
36
  runs-on: ubuntu-latest
37
37
  steps:
38
- - uses: actions/checkout@v4
38
+ - uses: actions/checkout@v7
39
39
  - uses: dtolnay/rust-toolchain@stable
40
40
  with:
41
41
  components: clippy, rustfmt
42
- - uses: actions/setup-python@v5
42
+ - uses: actions/setup-python@v6
43
43
  with:
44
44
  python-version: "3.12"
45
45
  - uses: Swatinem/rust-cache@v2
@@ -67,16 +67,16 @@ jobs:
67
67
  matrix:
68
68
  python-version: ["3.12"]
69
69
  steps:
70
- - uses: actions/checkout@v4
70
+ - uses: actions/checkout@v7
71
71
  - uses: dtolnay/rust-toolchain@stable
72
72
  - uses: Swatinem/rust-cache@v2
73
73
  with:
74
74
  # Keyed by Python version so each matrix cell gets its own cache.
75
75
  key: py-${{ matrix.python-version }}
76
- - uses: actions/setup-python@v5
76
+ - uses: actions/setup-python@v6
77
77
  with:
78
78
  python-version: ${{ matrix.python-version }}
79
- - uses: astral-sh/setup-uv@v4
79
+ - uses: astral-sh/setup-uv@v7
80
80
  - name: Set up venv and install maturin
81
81
  run: |
82
82
  uv venv --python "${{ matrix.python-version }}"
@@ -47,8 +47,8 @@ jobs:
47
47
  python-version: ["3.12", "3.13"]
48
48
  runs-on: ${{ matrix.platform.os }}
49
49
  steps:
50
- - uses: actions/checkout@v4
51
- - uses: actions/setup-python@v5
50
+ - uses: actions/checkout@v7
51
+ - uses: actions/setup-python@v6
52
52
  with:
53
53
  python-version: ${{ matrix.python-version }}
54
54
  - uses: dtolnay/rust-toolchain@stable
@@ -61,7 +61,7 @@ jobs:
61
61
  target: ${{ matrix.platform.target }}
62
62
  manylinux: ${{ matrix.platform.manylinux }}
63
63
  args: --release --strip --out dist -i python${{ matrix.python-version }}
64
- - uses: actions/upload-artifact@v4
64
+ - uses: actions/upload-artifact@v7
65
65
  with:
66
66
  name: wheel-${{ matrix.platform.target }}-py${{ matrix.python-version }}
67
67
  path: dist/*.whl
@@ -71,12 +71,12 @@ jobs:
71
71
  name: sdist
72
72
  runs-on: ubuntu-latest
73
73
  steps:
74
- - uses: actions/checkout@v4
74
+ - uses: actions/checkout@v7
75
75
  - uses: PyO3/maturin-action@v1
76
76
  with:
77
77
  command: sdist
78
78
  args: --out dist
79
- - uses: actions/upload-artifact@v4
79
+ - uses: actions/upload-artifact@v7
80
80
  with:
81
81
  name: sdist
82
82
  path: dist/*.tar.gz
@@ -97,8 +97,8 @@ jobs:
97
97
  group: release-${{ github.ref }}
98
98
  cancel-in-progress: false
99
99
  steps:
100
- - uses: actions/checkout@v4
101
- - uses: actions/download-artifact@v4
100
+ - uses: actions/checkout@v7
101
+ - uses: actions/download-artifact@v8
102
102
  with:
103
103
  path: dist
104
104
  merge-multiple: true
@@ -128,7 +128,7 @@ jobs:
128
128
  permissions:
129
129
  id-token: write # mint short-lived OIDC tokens for trusted publishing
130
130
  steps:
131
- - uses: actions/download-artifact@v4
131
+ - uses: actions/download-artifact@v8
132
132
  with:
133
133
  path: dist
134
134
  merge-multiple: true
@@ -11,6 +11,137 @@ release, that section is renamed to `[x.y.z] - YYYY-MM-DD` and a fresh
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [0.4.0] - 2026-07-24
15
+
16
+ ### Added
17
+
18
+ - Dependabot dependency automation ([#56]). A `.github/dependabot.yml` opens
19
+ weekly grouped version-update PRs for three ecosystems — `cargo`, `pip`
20
+ (pyproject), and `github-actions` — plus repo-level vulnerability alerts and
21
+ Dependabot security updates for CVE-triggered PRs. Nothing auto-merges; every
22
+ PR is reviewed by hand. `icechunk`/`typetag` (FFI lockstep) and `ruff` (also
23
+ pinned in `ci.yml`) are intentionally ignored and stay manual, coordinated
24
+ bumps.
25
+
26
+ ### Changed
27
+
28
+ - Bump `zarrs` 0.22 → 0.23 ([#60]). A breaking upstream release: `DataType`
29
+ became a newtype over `Arc<dyn DataTypeTraits>` rather than an enum, so the
30
+ shared `for_each_supported_dtype!` dispatch is now an
31
+ `is::<Float64DataType>()` chain against the marker types in
32
+ `zarrs::array::data_type`. `ArraySubset` moved from the removed
33
+ `zarrs::array_subset` module into `zarrs::array`, and
34
+ `async_retrieve_array_subset_elements::<T>` (deprecated in 0.23, already
35
+ deleted upstream for 0.24) became `async_retrieve_array_subset::<Vec<T>>`.
36
+ The supported-dtype set, the read path, and the storage stack are otherwise
37
+ unchanged — `zarrs_storage` still resolves to a single 0.4.x, so
38
+ `zarrs_object_store` and `zarrs_icechunk` needed no bump and the icechunk
39
+ FFI lockstep is untouched.
40
+ - Fixes a dtype string that 0.23 would have silently corrupted: the walk
41
+ built `VarMeta::dtype` via `format!("{}", array.data_type())`, and 0.23's
42
+ `Display` renders *both* Zarr spellings when they differ (`float64` →
43
+ `"float64 / <f8"`), which `numpy.dtype()` rejects. Both walk sites now
44
+ route through `zarrs_dtype_to_numpy_str`, the same canonical mapper the
45
+ array handle already used.
46
+ - `zarrs_dtype_to_numpy_str` now reads the dtype's Zarr V3 name (identical
47
+ to the NumPy name for all thirteen dtypes we can name; pinned by a new
48
+ unit test) instead of maintaining a parallel hand-written table. The one
49
+ exception is `bytes`, reported as `object` to match zarr-python —
50
+ `np.dtype("bytes")` is `|S0`, which would mislabel a variable-length
51
+ binary array rather than fail. Dtypes we can neither read nor safely
52
+ rename (`string`, `numpy.datetime64`, `r32`, …) still surface as a
53
+ `TypeError` from `np.dtype()` at open, as before.
54
+
55
+ - CI: bump GitHub Actions off the deprecated Node 20 runtime ([#55], fixes #54).
56
+ `actions/checkout` v4 → v5, `actions/setup-python` v5 → v6,
57
+ `actions/upload-artifact` v4 → v6, `actions/download-artifact` v4 → v7 — the
58
+ lowest major of each that runs on Node 24 (verified via each tag's
59
+ `runs.using`; the artifact actions only moved to Node 24 at v6/v7, so a naïve
60
+ bump to v5 would still have been Node 20). No workflow behaviour change.
61
+
62
+ - Raise the NumPy floor `numpy>=1.24` → `numpy>=2.0` ([#71]). Variable-length
63
+ `string` arrays are surfaced as numpy 2's variable-width `StringDType`
64
+ (`np.dtype("T")`), which did not exist before 2.0. rustytree's other pins
65
+ (`zarr>=3`, recent `xarray`) already track numpy 2, so this only makes an
66
+ existing expectation explicit.
67
+
68
+ - Routine dependency and CI-action bumps via Dependabot ([#57], [#58], [#59],
69
+ [#64]) — the first batch after the automation ([#56]) landed. Cargo: `tokio`
70
+ 1.52.3 → 1.53.0, `futures` 0.3.32 → 0.3.33, `zarrs_storage` 0.4.3 → 0.4.4
71
+ ([#64]); `chrono` 0.4.44 → 0.4.45, `zarrs_icechunk` 0.5.0 → 0.5.1 ([#59]);
72
+ `cmov` 0.5.3 → 0.5.4 ([#57]). CI actions ([#58]): `actions/checkout` 5 → 7,
73
+ `astral-sh/setup-uv` 4 → 7, `actions/upload-artifact` 6 → 7, and
74
+ `actions/download-artifact` 7 → 8 — superseding the v5/v6/v7 pins the [#55]
75
+ entry above describes.
76
+
77
+ ### Fixed
78
+
79
+ - Zarr v3 string arrays now open and read instead of crashing ([#71], fixes
80
+ #70). Both v3 string flavours failed before: a variable-length `string` array
81
+ raised `TypeError: data type 'string' not understood` (its name reached
82
+ `np.dtype()` unmapped), and a `fixed_length_utf32` array (numpy `<U…`,
83
+ xarray's default `str` encoding) raised `NotImplementedError` at read. This
84
+ became a production regression when raw2zarr's FM301 retrofit added
85
+ `sweep_mode`/`prt_mode`/`follow_mode` as vlen-`string` scalars to the KLOT
86
+ eager store and the Serbian OSN stores, forcing consumers to fall back to the
87
+ slower `engine="zarr"`. rustytree now mirrors `engine="zarr"` for both
88
+ flavours — `xr.testing.assert_identical` holds against it for all
89
+ valid-Unicode content (2-D chunking, interior NULs, non-BMP code points,
90
+ empty strings, and unwritten-chunk fill values all included):
91
+ - **vlen `string`** is *declared* `object` but its values *materialise* as
92
+ numpy-2 `StringDType`. That declared/read split is exactly what
93
+ `engine="zarr"` produces: xarray's CF-decode flattens a
94
+ `StringDType`-declared variable's values back to `object`, so declaring
95
+ `object` up-front is the only way to keep `StringDType` values. The `numpy`
96
+ crate can only build `object` arrays across the FFI boundary, so the read
97
+ returns `object` and `RustyBackendArray` casts to the dtype the new
98
+ `ZarrsArrayHandle.read_dtype` reports.
99
+ - **`fixed_length_utf32`** → fixed-width `<U{n}` (`n` code points). zarrs 0.23
100
+ opens this dtype (0.22 could not); rustytree decodes the native-endian
101
+ UTF-32 to `str` and builds the `<U` array. One documented divergence: lone
102
+ surrogate code points (U+D800–U+DFFF, malformed Unicode) are dropped rather
103
+ than preserved, because the decode goes through Rust `char`.
104
+ - `slice_nd`'s bound relaxed from `T: Copy` to `T: Clone` so the same slicer
105
+ serves the `String` buffers; zero-cost for the numeric arms.
106
+
107
+ - An empty selection no longer returns a stray element or panics ([#68], fixes
108
+ #65). `isel(time=slice(1, 1))` and friends returned one bogus element whenever
109
+ the offset was not a multiple of the chunk size, and raised
110
+ `pyo3_runtime.PanicException` when the selection sat at the end of a ragged
111
+ final chunk (e.g. `slice(13, 13)` on a shape-13 array with chunks of 4).
112
+ Raggedness was needed only for the panic — the wrong length happened on any
113
+ non-chunk-aligned offset, including on arrays whose shape divides evenly.
114
+ `slice_nd`'s walk was a do-while that pushed an element before it could test
115
+ the bounds; it is now driven by the output element count, so an empty request
116
+ runs zero iterations and totality is structural rather than a guard. The panic
117
+ mattered beyond the wrong length: `PanicException` derives from
118
+ `BaseException`, so it passed straight through ordinary `except Exception:`
119
+ handlers. Empty selections also short-circuit before the chunk-alignment step
120
+ now, so they no longer pull a chunk off the network to return nothing.
121
+
122
+ - A reversed slice is empty rather than an error ([#68]). `v.isel(n=slice(5, 3))`
123
+ and `v[5:3]` raised `ValueError: start 5 > stop 3`, where numpy, pandas and
124
+ `engine="zarr"` all return an empty array. `slice.indices()` reports a reversed
125
+ slice faithfully as `(5, 3, 1)`, and that pair reached `read_subset` unchanged;
126
+ the Python adapter now normalises it. `read_subset` keeps its stricter
127
+ `start <= stop` contract, so a genuinely malformed range still raises.
128
+
129
+ - Tests: the two opt-in KTWX smoke tests now skip when the directory exists but
130
+ holds no icechunk repository ([#60]). The guard tested `KTWX_PATH.exists()`,
131
+ so an empty leftover directory failed the guard's purpose and the tests
132
+ errored with `KeyError: group / not found in store` instead of skipping. The
133
+ shared `ktwx_repo_available()` helper in `tests/conftest.py` now mirrors
134
+ rustytree's own detector (`looks_like_icechunk_repo`): a `repo` manifest file
135
+ plus a `snapshots/` directory.
136
+
137
+ ### Security
138
+
139
+ - Bump `pyo3` and `numpy` 0.28 → 0.29 ([#61]) to fix a **high**-severity
140
+ out-of-bounds read in PyO3's `PyIterator` `nth`/`nth_back` (RUSTSEC/GHSA)
141
+ plus a medium-severity missing `Sync` bound on `PyCFunction::new_closure`.
142
+ A clean bump — no source changes; the pyo3/numpy majors move in lockstep.
143
+ (Surfaced by the newly-enabled Dependabot security alerts.)
144
+
14
145
  ## [0.3.0] - 2026-07-04
15
146
 
16
147
  ### Added
@@ -704,7 +835,8 @@ below.
704
835
  intentionally not normalized — relative-vs-absolute glob
705
836
  semantics differ in `PurePosixPath.match`.
706
837
 
707
- [Unreleased]: https://github.com/aladinor/rustytree/compare/v0.3.0...HEAD
838
+ [Unreleased]: https://github.com/aladinor/rustytree/compare/v0.4.0...HEAD
839
+ [0.4.0]: https://github.com/aladinor/rustytree/compare/v0.3.0...v0.4.0
708
840
  [0.3.0]: https://github.com/aladinor/rustytree/compare/v0.2.1...v0.3.0
709
841
  [0.2.1]: https://github.com/aladinor/rustytree/compare/v0.2.0...v0.2.1
710
842
  [0.2.0]: https://github.com/aladinor/rustytree/compare/v0.1.0...v0.2.0
@@ -742,3 +874,13 @@ below.
742
874
  [#49]: https://github.com/aladinor/rustytree/pull/49
743
875
  [#50]: https://github.com/aladinor/rustytree/pull/50
744
876
  [#52]: https://github.com/aladinor/rustytree/pull/52
877
+ [#55]: https://github.com/aladinor/rustytree/pull/55
878
+ [#56]: https://github.com/aladinor/rustytree/pull/56
879
+ [#57]: https://github.com/aladinor/rustytree/pull/57
880
+ [#58]: https://github.com/aladinor/rustytree/pull/58
881
+ [#59]: https://github.com/aladinor/rustytree/pull/59
882
+ [#60]: https://github.com/aladinor/rustytree/pull/60
883
+ [#61]: https://github.com/aladinor/rustytree/pull/61
884
+ [#64]: https://github.com/aladinor/rustytree/pull/64
885
+ [#68]: https://github.com/aladinor/rustytree/pull/68
886
+ [#71]: https://github.com/aladinor/rustytree/pull/71