jsonatapy 2.2.2__tar.gz → 2.2.3__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 (55) hide show
  1. {jsonatapy-2.2.2 → jsonatapy-2.2.3}/CHANGELOG.md +31 -0
  2. {jsonatapy-2.2.2 → jsonatapy-2.2.3}/Cargo.lock +167 -1
  3. {jsonatapy-2.2.2 → jsonatapy-2.2.3}/Cargo.toml +10 -1
  4. {jsonatapy-2.2.2 → jsonatapy-2.2.3}/PKG-INFO +21 -2
  5. {jsonatapy-2.2.2 → jsonatapy-2.2.3}/README.md +16 -1
  6. {jsonatapy-2.2.2 → jsonatapy-2.2.3}/pyproject.toml +12 -1
  7. {jsonatapy-2.2.2 → jsonatapy-2.2.3}/python/jsonatapy/__init__.py +51 -0
  8. jsonatapy-2.2.3/python/jsonatapy/__main__.py +46 -0
  9. jsonatapy-2.2.3/python/jsonatapy/_cli/__init__.py +1 -0
  10. jsonatapy-2.2.3/python/jsonatapy/_cli/bindings.py +51 -0
  11. jsonatapy-2.2.3/python/jsonatapy/_cli/error_format.py +25 -0
  12. jsonatapy-2.2.3/python/jsonatapy/_cli/mcp_server.py +192 -0
  13. jsonatapy-2.2.3/python/jsonatapy/_cli/resolve.py +85 -0
  14. jsonatapy-2.2.3/python/jsonatapy/_cli/run.py +204 -0
  15. jsonatapy-2.2.3/src/bin/jsonata/bindings.rs +62 -0
  16. jsonatapy-2.2.3/src/bin/jsonata/error_format.rs +47 -0
  17. jsonatapy-2.2.3/src/bin/jsonata/main.rs +182 -0
  18. jsonatapy-2.2.3/src/bin/jsonata/resolve.rs +148 -0
  19. {jsonatapy-2.2.2 → jsonatapy-2.2.3}/src/evaluator.rs +51 -0
  20. {jsonatapy-2.2.2 → jsonatapy-2.2.3}/src/lib.rs +53 -30
  21. {jsonatapy-2.2.2 → jsonatapy-2.2.3}/src/parser.rs +39 -0
  22. jsonatapy-2.2.3/study/cli_fixtures.json +138 -0
  23. jsonatapy-2.2.3/study/cli_fixtures_testdata.json +1 -0
  24. jsonatapy-2.2.3/study/cli_spec.md +99 -0
  25. jsonatapy-2.2.3/tests/cli_fixtures_test.rs +69 -0
  26. jsonatapy-2.2.3/tests/cli_test.rs +352 -0
  27. {jsonatapy-2.2.2 → jsonatapy-2.2.3}/.gitignore +0 -0
  28. {jsonatapy-2.2.2 → jsonatapy-2.2.3}/.gitmodules +0 -0
  29. {jsonatapy-2.2.2 → jsonatapy-2.2.3}/.serena/.gitignore +0 -0
  30. {jsonatapy-2.2.2 → jsonatapy-2.2.3}/.serena/memories/architecture.md +0 -0
  31. {jsonatapy-2.2.2 → jsonatapy-2.2.3}/.serena/memories/conventions.md +0 -0
  32. {jsonatapy-2.2.2 → jsonatapy-2.2.3}/.serena/memories/core.md +0 -0
  33. {jsonatapy-2.2.2 → jsonatapy-2.2.3}/.serena/memories/memory_maintenance.md +0 -0
  34. {jsonatapy-2.2.2 → jsonatapy-2.2.3}/.serena/memories/suggested_commands.md +0 -0
  35. {jsonatapy-2.2.2 → jsonatapy-2.2.3}/.serena/memories/task_completion.md +0 -0
  36. {jsonatapy-2.2.2 → jsonatapy-2.2.3}/.serena/memories/tech_stack.md +0 -0
  37. {jsonatapy-2.2.2 → jsonatapy-2.2.3}/.serena/project.yml +0 -0
  38. {jsonatapy-2.2.2 → jsonatapy-2.2.3}/LICENSE +0 -0
  39. {jsonatapy-2.2.2 → jsonatapy-2.2.3}/benches/evaluator_bench.rs +0 -0
  40. {jsonatapy-2.2.2 → jsonatapy-2.2.3}/examples/evaluator_demo.rs +0 -0
  41. {jsonatapy-2.2.2 → jsonatapy-2.2.3}/examples/parser_demo.rs +0 -0
  42. {jsonatapy-2.2.2 → jsonatapy-2.2.3}/examples/simd_json_bench.rs +0 -0
  43. {jsonatapy-2.2.2 → jsonatapy-2.2.3}/python/jsonatapy/py.typed +0 -0
  44. {jsonatapy-2.2.2 → jsonatapy-2.2.3}/src/ast.rs +0 -0
  45. {jsonatapy-2.2.2 → jsonatapy-2.2.3}/src/ast_transform.rs +0 -0
  46. {jsonatapy-2.2.2 → jsonatapy-2.2.3}/src/compiler.rs +0 -0
  47. {jsonatapy-2.2.2 → jsonatapy-2.2.3}/src/datetime.rs +0 -0
  48. {jsonatapy-2.2.2 → jsonatapy-2.2.3}/src/functions.rs +0 -0
  49. {jsonatapy-2.2.2 → jsonatapy-2.2.3}/src/parser/README.md +0 -0
  50. {jsonatapy-2.2.2 → jsonatapy-2.2.3}/src/signature.rs +0 -0
  51. {jsonatapy-2.2.2 → jsonatapy-2.2.3}/src/value.rs +0 -0
  52. {jsonatapy-2.2.2 → jsonatapy-2.2.3}/src/vm.rs +0 -0
  53. {jsonatapy-2.2.2 → jsonatapy-2.2.3}/tests/datetime_picture_suite.rs +0 -0
  54. {jsonatapy-2.2.2 → jsonatapy-2.2.3}/tests/integration_test.rs +0 -0
  55. {jsonatapy-2.2.2 → jsonatapy-2.2.3}/tests/parent_and_focus_binding_suite.rs +0 -0
@@ -19,6 +19,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
19
19
 
20
20
  ### Security
21
21
 
22
+ ## [2.2.3] - 2026-07-12
23
+
24
+ ### Added
25
+ - `evaluate_json_or_none()`'s `json_str` parameter now accepts `None` (in addition to a JSON
26
+ string), binding the top-level context (`$`) to a true JSONata `Undefined` rather than an
27
+ explicit `null`. (#68)
28
+ - `docs/cli.md`: a full command-line reference for both `jsonata` (Rust) and `jsonatapy`
29
+ (Python), covering flags, input resolution, output/exit-code semantics, and the MCP
30
+ subcommand — previously undocumented outside internal spec/plan files. (#68)
31
+
32
+ ### Changed
33
+ - Release CLI binary archives are now named `jsonata-v<version>-<target>.tar.gz`/`.zip`
34
+ (previously `jsonata-<target>.tar.gz`/`.zip` with no version embedded).
35
+
36
+ ### Deprecated
37
+
38
+ ### Removed
39
+
40
+ ### Fixed
41
+ - The Python CLI's `-n`/`--null-input` now binds `$` to a true `Undefined`, matching the Rust
42
+ CLI exactly (`jsonatapy -n '$'` now prints nothing, as `jsonata -n '$'` already did). It
43
+ previously passed an explicit JSON `null` context instead, observable only for expressions
44
+ referencing `$` directly. (#68)
45
+ - Release workflow: `publish-pypi` and `publish-crates` now require `build-cli-binaries` to
46
+ succeed first. Previously the two registry publishes were independent of the CLI binary
47
+ build, so a CLI build failure (untested in a real release prior to this) would have shipped
48
+ a version to PyPI/crates.io with no CLI binaries attached and no way to reuse that version
49
+ number.
50
+
51
+ ### Security
52
+
22
53
  ## [2.2.2] - 2026-07-09
23
54
 
24
55
  ### Added
@@ -41,12 +41,56 @@ version = "0.1.6"
41
41
  source = "registry+https://github.com/rust-lang/crates.io-index"
42
42
  checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
43
43
 
44
+ [[package]]
45
+ name = "anstream"
46
+ version = "1.0.0"
47
+ source = "registry+https://github.com/rust-lang/crates.io-index"
48
+ checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
49
+ dependencies = [
50
+ "anstyle",
51
+ "anstyle-parse",
52
+ "anstyle-query",
53
+ "anstyle-wincon",
54
+ "colorchoice",
55
+ "is_terminal_polyfill",
56
+ "utf8parse",
57
+ ]
58
+
44
59
  [[package]]
45
60
  name = "anstyle"
46
61
  version = "1.0.14"
47
62
  source = "registry+https://github.com/rust-lang/crates.io-index"
48
63
  checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
49
64
 
65
+ [[package]]
66
+ name = "anstyle-parse"
67
+ version = "1.0.0"
68
+ source = "registry+https://github.com/rust-lang/crates.io-index"
69
+ checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
70
+ dependencies = [
71
+ "utf8parse",
72
+ ]
73
+
74
+ [[package]]
75
+ name = "anstyle-query"
76
+ version = "1.1.5"
77
+ source = "registry+https://github.com/rust-lang/crates.io-index"
78
+ checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
79
+ dependencies = [
80
+ "windows-sys",
81
+ ]
82
+
83
+ [[package]]
84
+ name = "anstyle-wincon"
85
+ version = "3.0.11"
86
+ source = "registry+https://github.com/rust-lang/crates.io-index"
87
+ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
88
+ dependencies = [
89
+ "anstyle",
90
+ "once_cell_polyfill",
91
+ "windows-sys",
92
+ ]
93
+
50
94
  [[package]]
51
95
  name = "ar_archive_writer"
52
96
  version = "0.5.2"
@@ -56,6 +100,21 @@ dependencies = [
56
100
  "object",
57
101
  ]
58
102
 
103
+ [[package]]
104
+ name = "assert_cmd"
105
+ version = "2.2.2"
106
+ source = "registry+https://github.com/rust-lang/crates.io-index"
107
+ checksum = "2aa3a22042e45de04255c7bf3626e239f450200fd0493c1e382263544b20aea6"
108
+ dependencies = [
109
+ "anstyle",
110
+ "bstr",
111
+ "libc",
112
+ "predicates",
113
+ "predicates-core",
114
+ "predicates-tree",
115
+ "wait-timeout",
116
+ ]
117
+
59
118
  [[package]]
60
119
  name = "autocfg"
61
120
  version = "1.5.1"
@@ -89,6 +148,17 @@ version = "2.13.0"
89
148
  source = "registry+https://github.com/rust-lang/crates.io-index"
90
149
  checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
91
150
 
151
+ [[package]]
152
+ name = "bstr"
153
+ version = "1.12.3"
154
+ source = "registry+https://github.com/rust-lang/crates.io-index"
155
+ checksum = "5cee35f73844aa3014bb606320a6c1f010249dbdf43342fe54b5a4f6a8ed4b79"
156
+ dependencies = [
157
+ "memchr",
158
+ "regex-automata",
159
+ "serde_core",
160
+ ]
161
+
92
162
  [[package]]
93
163
  name = "bumpalo"
94
164
  version = "3.20.3"
@@ -175,6 +245,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
175
245
  checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
176
246
  dependencies = [
177
247
  "clap_builder",
248
+ "clap_derive",
178
249
  ]
179
250
 
180
251
  [[package]]
@@ -183,8 +254,22 @@ version = "4.6.0"
183
254
  source = "registry+https://github.com/rust-lang/crates.io-index"
184
255
  checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
185
256
  dependencies = [
257
+ "anstream",
186
258
  "anstyle",
187
259
  "clap_lex",
260
+ "strsim",
261
+ ]
262
+
263
+ [[package]]
264
+ name = "clap_derive"
265
+ version = "4.6.1"
266
+ source = "registry+https://github.com/rust-lang/crates.io-index"
267
+ checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
268
+ dependencies = [
269
+ "heck",
270
+ "proc-macro2",
271
+ "quote",
272
+ "syn",
188
273
  ]
189
274
 
190
275
  [[package]]
@@ -193,6 +278,12 @@ version = "1.1.0"
193
278
  source = "registry+https://github.com/rust-lang/crates.io-index"
194
279
  checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
195
280
 
281
+ [[package]]
282
+ name = "colorchoice"
283
+ version = "1.0.5"
284
+ source = "registry+https://github.com/rust-lang/crates.io-index"
285
+ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
286
+
196
287
  [[package]]
197
288
  name = "core-foundation-sys"
198
289
  version = "0.8.7"
@@ -274,6 +365,12 @@ version = "0.2.4"
274
365
  source = "registry+https://github.com/rust-lang/crates.io-index"
275
366
  checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
276
367
 
368
+ [[package]]
369
+ name = "difflib"
370
+ version = "0.4.0"
371
+ source = "registry+https://github.com/rust-lang/crates.io-index"
372
+ checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
373
+
277
374
  [[package]]
278
375
  name = "either"
279
376
  version = "1.16.0"
@@ -455,6 +552,12 @@ dependencies = [
455
552
  "hashbrown 0.17.1",
456
553
  ]
457
554
 
555
+ [[package]]
556
+ name = "is_terminal_polyfill"
557
+ version = "1.70.2"
558
+ source = "registry+https://github.com/rust-lang/crates.io-index"
559
+ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
560
+
458
561
  [[package]]
459
562
  name = "itertools"
460
563
  version = "0.13.0"
@@ -483,14 +586,17 @@ dependencies = [
483
586
 
484
587
  [[package]]
485
588
  name = "jsonata-core"
486
- version = "2.2.2"
589
+ version = "2.2.3"
487
590
  dependencies = [
591
+ "assert_cmd",
488
592
  "base64",
489
593
  "chrono",
594
+ "clap",
490
595
  "criterion",
491
596
  "indexmap",
492
597
  "num-traits",
493
598
  "percent-encoding",
599
+ "predicates",
494
600
  "proptest",
495
601
  "pyo3",
496
602
  "rand 0.10.2",
@@ -526,6 +632,12 @@ version = "2.8.3"
526
632
  source = "registry+https://github.com/rust-lang/crates.io-index"
527
633
  checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
528
634
 
635
+ [[package]]
636
+ name = "normalize-line-endings"
637
+ version = "0.3.0"
638
+ source = "registry+https://github.com/rust-lang/crates.io-index"
639
+ checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
640
+
529
641
  [[package]]
530
642
  name = "num-traits"
531
643
  version = "0.2.19"
@@ -550,6 +662,12 @@ version = "1.21.4"
550
662
  source = "registry+https://github.com/rust-lang/crates.io-index"
551
663
  checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
552
664
 
665
+ [[package]]
666
+ name = "once_cell_polyfill"
667
+ version = "1.70.2"
668
+ source = "registry+https://github.com/rust-lang/crates.io-index"
669
+ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
670
+
553
671
  [[package]]
554
672
  name = "oorandom"
555
673
  version = "11.1.5"
@@ -621,6 +739,36 @@ dependencies = [
621
739
  "zerocopy",
622
740
  ]
623
741
 
742
+ [[package]]
743
+ name = "predicates"
744
+ version = "3.1.4"
745
+ source = "registry+https://github.com/rust-lang/crates.io-index"
746
+ checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe"
747
+ dependencies = [
748
+ "anstyle",
749
+ "difflib",
750
+ "float-cmp",
751
+ "normalize-line-endings",
752
+ "predicates-core",
753
+ "regex",
754
+ ]
755
+
756
+ [[package]]
757
+ name = "predicates-core"
758
+ version = "1.0.10"
759
+ source = "registry+https://github.com/rust-lang/crates.io-index"
760
+ checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144"
761
+
762
+ [[package]]
763
+ name = "predicates-tree"
764
+ version = "1.0.13"
765
+ source = "registry+https://github.com/rust-lang/crates.io-index"
766
+ checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2"
767
+ dependencies = [
768
+ "predicates-core",
769
+ "termtree",
770
+ ]
771
+
624
772
  [[package]]
625
773
  name = "proc-macro2"
626
774
  version = "1.0.106"
@@ -1002,6 +1150,12 @@ dependencies = [
1002
1150
  "windows-sys",
1003
1151
  ]
1004
1152
 
1153
+ [[package]]
1154
+ name = "strsim"
1155
+ version = "0.11.1"
1156
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1157
+ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
1158
+
1005
1159
  [[package]]
1006
1160
  name = "syn"
1007
1161
  version = "2.0.118"
@@ -1032,6 +1186,12 @@ dependencies = [
1032
1186
  "windows-sys",
1033
1187
  ]
1034
1188
 
1189
+ [[package]]
1190
+ name = "termtree"
1191
+ version = "0.5.1"
1192
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1193
+ checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
1194
+
1035
1195
  [[package]]
1036
1196
  name = "thiserror"
1037
1197
  version = "2.0.18"
@@ -1074,6 +1234,12 @@ version = "1.0.24"
1074
1234
  source = "registry+https://github.com/rust-lang/crates.io-index"
1075
1235
  checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
1076
1236
 
1237
+ [[package]]
1238
+ name = "utf8parse"
1239
+ version = "0.2.2"
1240
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1241
+ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
1242
+
1077
1243
  [[package]]
1078
1244
  name = "value-trait"
1079
1245
  version = "0.12.1"
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "jsonata-core"
3
- version = "2.2.2"
3
+ version = "2.2.3"
4
4
  edition = "2021"
5
5
  authors = ["txjmb <txjmb@users.noreply.github.com>"]
6
6
  description = "High-performance Rust implementation of JSONata query and transformation language"
@@ -36,6 +36,11 @@ exclude = [
36
36
  name = "jsonata_core"
37
37
  crate-type = ["cdylib", "rlib"]
38
38
 
39
+ [[bin]]
40
+ name = "jsonata"
41
+ path = "src/bin/jsonata/main.rs"
42
+ required-features = ["cli"]
43
+
39
44
  [dependencies]
40
45
  pyo3 = { version = "0.29", optional = true }
41
46
  serde = { version = "1.0", features = ["derive"] }
@@ -50,16 +55,20 @@ percent-encoding = "2.3"
50
55
  rand = "0.10"
51
56
  stacker = "0.1"
52
57
  simd-json = { version = "0.17", optional = true }
58
+ clap = { version = "4.6", features = ["derive"], optional = true }
53
59
 
54
60
  [features]
55
61
  default = ["simd"]
56
62
  simd = ["dep:simd-json"]
57
63
  python = ["dep:pyo3"]
64
+ cli = ["dep:clap"]
58
65
  bench = [] # exposes _bench facade for Criterion benchmarks
59
66
 
60
67
  [dev-dependencies]
61
68
  criterion = "0.8"
62
69
  proptest = "1.10"
70
+ assert_cmd = "2.0"
71
+ predicates = "3.1"
63
72
 
64
73
  [[bench]]
65
74
  name = "evaluator_bench"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jsonatapy
3
- Version: 2.2.2
3
+ Version: 2.2.3
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -20,16 +20,20 @@ Requires-Dist: matplotlib>=3.5 ; extra == 'bench'
20
20
  Requires-Dist: pytest>=7.0 ; extra == 'dev'
21
21
  Requires-Dist: pytest-cov>=4.0 ; extra == 'dev'
22
22
  Requires-Dist: pytest-xdist>=3.0 ; extra == 'dev'
23
+ Requires-Dist: pytest-asyncio>=0.24 ; extra == 'dev'
23
24
  Requires-Dist: ruff>=0.3.0 ; extra == 'dev'
24
25
  Requires-Dist: mypy>=1.0 ; extra == 'dev'
25
26
  Requires-Dist: maturin>=1.0 ; extra == 'dev'
27
+ Requires-Dist: fastmcp>=3.0 ; extra == 'dev'
26
28
  Requires-Dist: mkdocs>=1.5 ; extra == 'docs'
27
29
  Requires-Dist: mkdocs-material>=9.0 ; extra == 'docs'
28
30
  Requires-Dist: mkdocstrings[python]>=0.24 ; extra == 'docs'
29
31
  Requires-Dist: pymdown-extensions>=10.0 ; extra == 'docs'
32
+ Requires-Dist: fastmcp>=3.0 ; extra == 'mcp'
30
33
  Provides-Extra: bench
31
34
  Provides-Extra: dev
32
35
  Provides-Extra: docs
36
+ Provides-Extra: mcp
33
37
  License-File: LICENSE
34
38
  Summary: High-performance Python/Rust implementation of JSONata query and transformation language
35
39
  Keywords: jsonata,json,query,transform
@@ -50,7 +54,7 @@ High-performance [JSONata](https://jsonata.org/) implementation in Rust, with Py
50
54
  Much of this project was built using Claude Code with significant human oversight. There was no performant
51
55
  JSONata implementation in Python, so the goal was to port JSONata to Rust (with a PyO3 wrapper
52
56
  for Python) and see how fast it could go. The answer: faster than V8 for most expression
53
- workloads, and faster than the next pure-Rust implementation.
57
+ workloads, and faster than the next pure-Rust implementation. The rust versions are published on crates.io, and the python wheels on pypi.
54
58
 
55
59
  Many, many thanks to the incredible work of all the maintainers of the [JSONata](https://github.com/jsonata-js/jsonata) reference library. JSONata is a very powerful, well-designed, and useful language that has made an impact on many projects. This project leverages their outstanding work to extend that capability to Python and Rust and would not be possible without that project. The implementation in Rust was strongly influenced by their implementation. The 1600+ tests they created provided the scaffolding and validation for all of this project. This project will continue to follow and be a derivative of the reference project as the JSONata reference library evolves.
56
60
 
@@ -135,6 +139,20 @@ Supports Python 3.10, 3.11, 3.12, 3.13, 3.14 on Linux, macOS (Intel & ARM), and
135
139
 
136
140
  ---
137
141
 
142
+ ## Command-line quick start
143
+
144
+ Both packages also ship a CLI, `jq`-shaped, with an identical contract:
145
+
146
+ ```bash
147
+ pip install jsonatapy
148
+ echo '{"orders":[{"product":"Laptop","price":1200}]}' | jsonatapy 'orders[price > 100].product'
149
+ # "Laptop"
150
+ ```
151
+
152
+ See [CLI Reference](docs/cli.md) for the full flag/exit-code contract.
153
+
154
+ ---
155
+
138
156
  ## What is JSONata?
139
157
 
140
158
  JSONata is a query and transformation language for JSON data:
@@ -189,6 +207,7 @@ benchmark methodology.
189
207
  - [Installation](docs/installation.md)
190
208
  - [API Reference](docs/api.md)
191
209
  - [Usage Guide](docs/usage.md)
210
+ - [CLI Reference](docs/cli.md)
192
211
  - [Performance](docs/performance.md)
193
212
  - [Optimization Tips](docs/optimization-tips.md)
194
213
  - [Building from Source](docs/development/building.md)
@@ -5,7 +5,7 @@ High-performance [JSONata](https://jsonata.org/) implementation in Rust, with Py
5
5
  Much of this project was built using Claude Code with significant human oversight. There was no performant
6
6
  JSONata implementation in Python, so the goal was to port JSONata to Rust (with a PyO3 wrapper
7
7
  for Python) and see how fast it could go. The answer: faster than V8 for most expression
8
- workloads, and faster than the next pure-Rust implementation.
8
+ workloads, and faster than the next pure-Rust implementation. The rust versions are published on crates.io, and the python wheels on pypi.
9
9
 
10
10
  Many, many thanks to the incredible work of all the maintainers of the [JSONata](https://github.com/jsonata-js/jsonata) reference library. JSONata is a very powerful, well-designed, and useful language that has made an impact on many projects. This project leverages their outstanding work to extend that capability to Python and Rust and would not be possible without that project. The implementation in Rust was strongly influenced by their implementation. The 1600+ tests they created provided the scaffolding and validation for all of this project. This project will continue to follow and be a derivative of the reference project as the JSONata reference library evolves.
11
11
 
@@ -90,6 +90,20 @@ Supports Python 3.10, 3.11, 3.12, 3.13, 3.14 on Linux, macOS (Intel & ARM), and
90
90
 
91
91
  ---
92
92
 
93
+ ## Command-line quick start
94
+
95
+ Both packages also ship a CLI, `jq`-shaped, with an identical contract:
96
+
97
+ ```bash
98
+ pip install jsonatapy
99
+ echo '{"orders":[{"product":"Laptop","price":1200}]}' | jsonatapy 'orders[price > 100].product'
100
+ # "Laptop"
101
+ ```
102
+
103
+ See [CLI Reference](docs/cli.md) for the full flag/exit-code contract.
104
+
105
+ ---
106
+
93
107
  ## What is JSONata?
94
108
 
95
109
  JSONata is a query and transformation language for JSON data:
@@ -144,6 +158,7 @@ benchmark methodology.
144
158
  - [Installation](docs/installation.md)
145
159
  - [API Reference](docs/api.md)
146
160
  - [Usage Guide](docs/usage.md)
161
+ - [CLI Reference](docs/cli.md)
147
162
  - [Performance](docs/performance.md)
148
163
  - [Optimization Tips](docs/optimization-tips.md)
149
164
  - [Building from Source](docs/development/building.md)
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "jsonatapy"
7
- version = "2.2.2"
7
+ version = "2.2.3"
8
8
  description = "High-performance Python/Rust implementation of JSONata query and transformation language"
9
9
  authors = [
10
10
  {name = "txjmb", email = "txjmb@users.noreply.github.com"}
@@ -30,14 +30,22 @@ classifiers = [
30
30
  ]
31
31
  dependencies = []
32
32
 
33
+ [project.scripts]
34
+ jsonatapy = "jsonatapy.__main__:main"
35
+
33
36
  [project.optional-dependencies]
34
37
  dev = [
35
38
  "pytest>=7.0",
36
39
  "pytest-cov>=4.0",
37
40
  "pytest-xdist>=3.0", # Parallel test execution
41
+ "pytest-asyncio>=0.24", # Async tests for the FastMCP server (Task 9)
38
42
  "ruff>=0.3.0", # Linting and formatting
39
43
  "mypy>=1.0",
40
44
  "maturin>=1.0",
45
+ "fastmcp>=3.0", # So `dev` installs can run the full test suite (Task 9)
46
+ ]
47
+ mcp = [
48
+ "fastmcp>=3.0",
41
49
  ]
42
50
  bench = [
43
51
  "jsonata>=0.2.0", # jsonata-python for benchmarking
@@ -67,9 +75,11 @@ dev = [
67
75
  "pytest>=7.0",
68
76
  "pytest-cov>=4.0",
69
77
  "pytest-xdist>=3.0",
78
+ "pytest-asyncio>=0.24",
70
79
  "ruff>=0.3.0",
71
80
  "mypy>=1.0",
72
81
  "maturin>=1.0",
82
+ "fastmcp>=3.0",
73
83
  ]
74
84
 
75
85
  [tool.ruff]
@@ -112,6 +122,7 @@ python_files = "test_*.py"
112
122
  python_classes = "Test*"
113
123
  python_functions = "test_*"
114
124
  addopts = "-v --strict-markers"
125
+ asyncio_mode = "auto"
115
126
  markers = [
116
127
  "slow: marks tests as slow (deselect with '-m \"not slow\"')",
117
128
  "compatibility: marks tests that verify JS compatibility",
@@ -230,6 +230,57 @@ class JsonataExpression:
230
230
  json_str, bindings, timeout, max_stack_depth, max_sequence_length
231
231
  )
232
232
 
233
+ def evaluate_json_or_none(
234
+ self,
235
+ json_str: str | None,
236
+ bindings: dict[str, Any] | None = None,
237
+ *,
238
+ timeout: int | None = None,
239
+ max_stack_depth: int | None = None,
240
+ max_sequence_length: int | None = None,
241
+ ) -> str | None:
242
+ """
243
+ Evaluate with JSON string input, distinguishing Undefined from null.
244
+
245
+ Unlike evaluate_json(), which serializes both a JSONata Undefined
246
+ result and an explicit JSON null result to the same text "null",
247
+ this method returns None (the Python value) for Undefined and the
248
+ string "null" for an explicit null result.
249
+
250
+ Args:
251
+ json_str: Input data as a JSON string, or None for no input
252
+ document at all -- the top-level context (`$`) is then a
253
+ true JSONata Undefined, distinct from passing "null" (an
254
+ explicit JSON null context).
255
+ bindings: Optional additional variable bindings
256
+ timeout: Maximum evaluation time in milliseconds (raises ValueError
257
+ with a D1012 code on timeout). Overrides any default set via
258
+ `compile(timeout=...)` for this call only.
259
+ max_stack_depth: Maximum recursion stack depth (raises ValueError
260
+ with a D1011 code when exceeded). Overrides any compile-time default.
261
+ max_sequence_length: Maximum length of a query-result sequence
262
+ (map/filter/wildcard/descendants/etc; raises ValueError with a
263
+ D2015 code when exceeded). Overrides any compile-time default.
264
+
265
+ Returns:
266
+ None if the result is JSONata Undefined, otherwise the result
267
+ as a JSON string (e.g. "null" for an explicit null result).
268
+
269
+ Raises:
270
+ ValueError: If JSON parsing or evaluation fails, or a guardrail is exceeded
271
+
272
+ Example:
273
+ >>> expr = compile("nonexistent")
274
+ >>> expr.evaluate_json_or_none('{"a": 1}') is None
275
+ True
276
+ >>> expr2 = compile("a")
277
+ >>> expr2.evaluate_json_or_none('{"a": null}')
278
+ 'null'
279
+ """
280
+ return self._expr.evaluate_json_or_none(
281
+ json_str, bindings, timeout, max_stack_depth, max_sequence_length
282
+ )
283
+
233
284
  def evaluate_with_data(
234
285
  self,
235
286
  data: "JsonataData",
@@ -0,0 +1,46 @@
1
+ """Console-script entry point for the `jsonatapy` CLI.
2
+
3
+ Dispatches to the MCP server subcommand (`jsonatapy mcp ...`) or evaluate
4
+ mode (everything else). See study/cli_spec.md for the full contract.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import sys
11
+
12
+ from ._cli.run import run
13
+
14
+
15
+ def _run_mcp(argv: list[str]) -> int:
16
+ parser = argparse.ArgumentParser(prog="jsonatapy mcp", description="Run the JSONata MCP server")
17
+ parser.add_argument("--http", action="store_true", help="Serve over HTTP instead of stdio")
18
+ parser.add_argument(
19
+ "--port", type=int, default=None, metavar="N", help="HTTP port (default 8000)"
20
+ )
21
+ args = parser.parse_args(argv)
22
+
23
+ try:
24
+ from ._cli.mcp_server import serve
25
+ except ImportError:
26
+ print(
27
+ "error: the 'mcp' extra is not installed. Run:\n"
28
+ ' uvx --from "jsonatapy[mcp]" jsonatapy mcp\n'
29
+ "or: pip install jsonatapy[mcp]",
30
+ file=sys.stderr,
31
+ )
32
+ return 2
33
+
34
+ serve(args.http, args.port)
35
+ return 0
36
+
37
+
38
+ def main() -> int:
39
+ argv = sys.argv[1:]
40
+ if argv[:1] == ["mcp"]:
41
+ return _run_mcp(argv[1:])
42
+ return run(argv)
43
+
44
+
45
+ if __name__ == "__main__":
46
+ sys.exit(main())
@@ -0,0 +1 @@
1
+ """Internal CLI implementation for the `jsonatapy` console script. Not public API."""
@@ -0,0 +1,51 @@
1
+ """Parses --arg/--argjson CLI specs into a name -> value map for
2
+ evaluate_json_or_none() bindings."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import math
8
+ from typing import Any
9
+
10
+
11
+ class BindingError(Exception):
12
+ """Raised when an --arg/--argjson spec is malformed."""
13
+
14
+
15
+ def _reject_constant(token: str) -> float:
16
+ raise ValueError(f"{token} is not valid JSON")
17
+
18
+
19
+ def _finite_float(text: str) -> float:
20
+ value = float(text)
21
+ if not math.isfinite(value):
22
+ raise ValueError(f"{text} is not valid JSON")
23
+ return value
24
+
25
+
26
+ def parse_bindings(arg: list[str], argjson: list[str]) -> dict[str, Any]:
27
+ """Parses --arg NAME=VALUE (string) and --argjson NAME=JSON (parsed) specs
28
+ into a single name -> value dict. --argjson wins on name collision
29
+ (applied second, matching src/bin/jsonata/bindings.rs's iteration order)."""
30
+ bindings: dict[str, Any] = {}
31
+ for spec in arg:
32
+ name, value = _split_name_value(spec, "--arg")
33
+ bindings[name] = value
34
+ for spec in argjson:
35
+ name, value = _split_name_value(spec, "--argjson")
36
+ try:
37
+ bindings[name] = json.loads(
38
+ value, parse_constant=_reject_constant, parse_float=_finite_float
39
+ )
40
+ except ValueError as e:
41
+ raise BindingError(f"--argjson {name}: invalid JSON value: {e}") from e
42
+ return bindings
43
+
44
+
45
+ def _split_name_value(spec: str, flag: str) -> tuple[str, str]:
46
+ if "=" not in spec:
47
+ raise BindingError(f"{flag} expects NAME=VALUE, got: {spec}")
48
+ name, _, value = spec.partition("=")
49
+ if not name:
50
+ raise BindingError(f"{flag} expects NAME=VALUE, got: {spec}")
51
+ return name, value