jqon 0.1.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.
jqon-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Martin Strohalm
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.
jqon-0.1.0/MANIFEST.in ADDED
@@ -0,0 +1,2 @@
1
+ include README.md
2
+ include LICENSE
jqon-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,51 @@
1
+ Metadata-Version: 2.4
2
+ Name: jqon
3
+ Version: 0.1.0
4
+ Summary: JSON expression language for querying Python objects.
5
+ Home-page: https://github.com/xxao/jqon
6
+ Author: Martin Strohalm
7
+ Author-email:
8
+ License: MIT
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Programming Language :: Python :: 3 :: Only
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Topic :: Utilities
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Dynamic: author
16
+ Dynamic: classifier
17
+ Dynamic: description
18
+ Dynamic: description-content-type
19
+ Dynamic: home-page
20
+ Dynamic: license
21
+ Dynamic: license-file
22
+ Dynamic: summary
23
+
24
+ # JqON
25
+
26
+ The *jqon* package was developed to provide a simple way to define queries in JSON format, which can later be run
27
+ in Python. It can be utilized in testing automations, where expected output of a system can be submitted a
28
+ predefined set of tests.
29
+
30
+
31
+ ## Installation
32
+
33
+ This package is fully implemented in Python. No additional compiler is necessary. After downloading the source
34
+ code just run the following command from the *jexon* folder:
35
+
36
+ `$ python setup.py install`
37
+
38
+ or simply by using pip
39
+
40
+ `$ pip install jqson`
41
+
42
+
43
+ ## Requirements
44
+
45
+ - [Python 3.11+](https://www.python.org)
46
+
47
+
48
+ ## Disclaimer
49
+
50
+ This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
51
+ warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
jqon-0.1.0/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # JqON
2
+
3
+ The *jqon* package was developed to provide a simple way to define queries in JSON format, which can later be run
4
+ in Python. It can be utilized in testing automations, where expected output of a system can be submitted a
5
+ predefined set of tests.
6
+
7
+
8
+ ## Installation
9
+
10
+ This package is fully implemented in Python. No additional compiler is necessary. After downloading the source
11
+ code just run the following command from the *jexon* folder:
12
+
13
+ `$ python setup.py install`
14
+
15
+ or simply by using pip
16
+
17
+ `$ pip install jqson`
18
+
19
+
20
+ ## Requirements
21
+
22
+ - [Python 3.11+](https://www.python.org)
23
+
24
+
25
+ ## Disclaimer
26
+
27
+ This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
28
+ warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
@@ -0,0 +1,32 @@
1
+ # Created byMartin.cz
2
+ # Copyright (c) Martin Strohalm. All rights reserved.
3
+
4
+ version = (0, 1, 0)
5
+
6
+ from .errors import *
7
+ from .register import register
8
+ from .query import Query
9
+ from .value import Value
10
+ from .variable import Variable
11
+ from .path import Path
12
+ from .getters import Attr, Item
13
+ from .unary import Unary
14
+ from .binary import Binary
15
+ from .logical import AND, OR, NOT
16
+ from .funcs import Bool, Len, Any, All, Min, Max, Sum, Avg
17
+ from .enumerable import Take, Skip, Slice
18
+ from .enumerable import Select, Many, Where, Distinct
19
+ from .enumerable import Single, First, Last, Count
20
+
21
+
22
+ # init shortcuts
23
+ def from_text(text):
24
+ """Initialize query from text."""
25
+
26
+ return Query.from_text(text)
27
+
28
+
29
+ def from_json(data):
30
+ """Initialize query from JSON."""
31
+
32
+ return Query.from_json(data)
@@ -0,0 +1,111 @@
1
+ # Created byMartin.cz
2
+ # Copyright (c) Martin Strohalm. All rights reserved.
3
+
4
+ from dataclasses import dataclass
5
+ from .errors import *
6
+ from .register import register
7
+ from .query import Query
8
+ from .path import Path
9
+
10
+
11
+ @dataclass
12
+ @register("=", "==", "!=", ">", "<", ">=", "<=")
13
+ @register("eq", "ne", "gt", "lt", "ge", "gte", "le", "lte")
14
+ @register("in", "not_in", "contains")
15
+ class Binary(Query):
16
+ """Evaluates binary condition to boolean."""
17
+
18
+ operand: str
19
+ left: Path | None = None
20
+ right: Path | None = None
21
+
22
+
23
+ def apply(self, data, *args, **kwargs):
24
+ """Applies query to data."""
25
+
26
+ # get left value
27
+ left = data
28
+ if isinstance(self.left, Query):
29
+ left = self.left(data, *args, **kwargs)
30
+
31
+ # get right value
32
+ right = data
33
+ if isinstance(self.right, Query):
34
+ right = self.right(data, *args, **kwargs)
35
+
36
+ # equals
37
+ if self.operand in ("=", "==", "eq"):
38
+ return left == right
39
+
40
+ # not equals
41
+ if self.operand in ("!=", "ne"):
42
+ return left != right
43
+
44
+ # greater than
45
+ if self.operand in (">", "gt"):
46
+ return left > right
47
+
48
+ # less than
49
+ if self.operand in ("<", "lt"):
50
+ return left < right
51
+
52
+ # greater than or equal
53
+ if self.operand in (">=", "ge", "gte"):
54
+ return left >= right
55
+
56
+ # less than or equal
57
+ if self.operand in ("<=", "le", "lte"):
58
+ return left <= right
59
+
60
+ # in
61
+ if self.operand == "in":
62
+ return left in right
63
+
64
+ # not in
65
+ if self.operand == "not_in":
66
+ return left not in right
67
+
68
+ # contains
69
+ if self.operand == "contains":
70
+ return right in left
71
+
72
+ # unknown operand
73
+ raise QSyntaxError(f"Unrecognized unary condition operand '{self.operand}'.")
74
+
75
+
76
+ @classmethod
77
+ def from_json(cls, data):
78
+ """Initialize instance from JSON."""
79
+
80
+ # get values
81
+ operand = next(iter(data))
82
+ values = next(iter(data.values()))
83
+
84
+ # convert single value
85
+ if not isinstance(values, list):
86
+ values = [values]
87
+
88
+ # check values
89
+ if not isinstance(values, list) or len(values) not in (1, 2):
90
+ raise QSyntaxError("Binary condition requires one or two values.")
91
+
92
+ # get values
93
+ if len(values) == 1:
94
+ left = None
95
+ right = values[0]
96
+ else:
97
+ left = values[0]
98
+ right = values[1]
99
+
100
+ # convert paths
101
+ if left is not None:
102
+ left = Path.from_json(left)
103
+ if right is not None:
104
+ right = Path.from_json(right)
105
+
106
+ # init instance
107
+ return cls(
108
+ operand = operand,
109
+ left = left,
110
+ right = right
111
+ )
@@ -0,0 +1,416 @@
1
+ # Created byMartin.cz
2
+ # Copyright (c) Martin Strohalm. All rights reserved.
3
+
4
+ from dataclasses import dataclass
5
+ from .errors import *
6
+ from .register import register
7
+ from .query import Query
8
+ from .path import Path
9
+
10
+
11
+ @dataclass
12
+ @register("take")
13
+ class Take(Query):
14
+ """Takes number of items from sequence."""
15
+
16
+ count: int | Path | None = None
17
+
18
+
19
+ def apply(self, data, *args, **kwargs):
20
+ """Applies query to data."""
21
+
22
+ # get count
23
+ count = self.count
24
+ if isinstance(self.count, Query):
25
+ count = self.count(data, *args, **kwargs)
26
+
27
+ # get slice
28
+ return data[:count]
29
+
30
+
31
+ @classmethod
32
+ def from_json(cls, data):
33
+ """Initialize instance from JSON."""
34
+
35
+ # get value
36
+ count = next(iter(data.values()))
37
+
38
+ # convert to path
39
+ if not isinstance(count, (int, type(None))):
40
+ count = Path.from_json(count)
41
+
42
+ # init instance
43
+ return cls(
44
+ count = count
45
+ )
46
+
47
+
48
+ @dataclass
49
+ @register("skip")
50
+ class Skip(Query):
51
+ """Skips number of items from sequence."""
52
+
53
+ count: int | Path | None = None
54
+
55
+
56
+ def apply(self, data, *args, **kwargs):
57
+ """Applies query to data."""
58
+
59
+ # get count
60
+ count = self.count
61
+ if isinstance(self.count, Query):
62
+ count = self.count(data, *args, **kwargs)
63
+
64
+ # get slice
65
+ return data[count:]
66
+
67
+
68
+ @classmethod
69
+ def from_json(cls, data):
70
+ """Initialize instance from JSON."""
71
+
72
+ # get value
73
+ count = next(iter(data.values()))
74
+
75
+ # convert to path
76
+ if not isinstance(count, (int, type(None))):
77
+ count = Path.from_json(count)
78
+
79
+ # init instance
80
+ return cls(
81
+ count = count
82
+ )
83
+
84
+
85
+ @dataclass
86
+ @register("slice")
87
+ class Slice(Query):
88
+ """Takes slice of items from sequence."""
89
+
90
+ start: int | Path | None = None
91
+ end: int | Path | None = None
92
+ step: int | Path | None = None
93
+
94
+
95
+ def apply(self, data, *args, **kwargs):
96
+ """Applies query to data."""
97
+
98
+ # get start
99
+ start = self.start
100
+ if isinstance(self.start, Query):
101
+ start = self.start(data, *args, **kwargs)
102
+
103
+ # get end
104
+ end = self.end
105
+ if isinstance(self.end, Query):
106
+ end = self.end(data, *args, **kwargs)
107
+
108
+ # get step
109
+ step = self.step
110
+ if isinstance(self.step, Query):
111
+ step = self.step(data, *args, **kwargs)
112
+
113
+ # get slice
114
+ return data[start:end:step]
115
+
116
+
117
+ @classmethod
118
+ def from_json(cls, data):
119
+ """Initialize instance from JSON."""
120
+
121
+ # get values
122
+ values = next(iter(data.values()))
123
+
124
+ # check empty
125
+ if values is None:
126
+ return cls()
127
+
128
+ # check values
129
+ if not isinstance(values, list) or len(values) > 3:
130
+ raise QSyntaxError("Slice definition must be a list of one, two or three values.")
131
+
132
+ # get values
133
+ start = values[0]
134
+ end = values[1] if len(values) > 1 else None
135
+ step = values[2] if len(values) > 2 else None
136
+
137
+ # convert to paths
138
+ if not isinstance(start, (int, type(None))):
139
+ start = Path.from_json(start)
140
+ if not isinstance(end, (int, type(None))):
141
+ end = Path.from_json(end)
142
+ if not isinstance(step, (int, type(None))):
143
+ step = Path.from_json(step)
144
+
145
+ # init instance
146
+ return cls(
147
+ start = start,
148
+ end = end,
149
+ step = step
150
+ )
151
+
152
+
153
+ @dataclass
154
+ @register("select")
155
+ class Select(Query):
156
+ """Selects value from every item in sequence."""
157
+
158
+ path: Path
159
+
160
+
161
+ def apply(self, data, *args, **kwargs):
162
+ """Applies query to data."""
163
+
164
+ # select values
165
+ return [self.path(item, *args, **kwargs) for item in data]
166
+
167
+
168
+ @classmethod
169
+ def from_json(cls, data):
170
+ """Initialize instance from JSON."""
171
+
172
+ # get values
173
+ value = next(iter(data.values()))
174
+
175
+ # convert to path
176
+ if value is not None:
177
+ value = Path.from_json(value)
178
+
179
+ # init instance
180
+ return cls(
181
+ path = value
182
+ )
183
+
184
+
185
+ @dataclass
186
+ @register("many")
187
+ class Many(Query):
188
+ """Flattens value from every item in sequence."""
189
+
190
+ path: Path | None = None
191
+
192
+
193
+ def apply(self, data, *args, **kwargs):
194
+ """Applies query to data."""
195
+
196
+ # get values
197
+ if isinstance(self.path, Query):
198
+ data = (self.path(item, *args, **kwargs) for item in data)
199
+
200
+ # flatten lists
201
+ return [item for chain in data for item in chain]
202
+
203
+
204
+ @classmethod
205
+ def from_json(cls, data):
206
+ """Initialize instance from JSON."""
207
+
208
+ # get values
209
+ value = next(iter(data.values()))
210
+
211
+ # convert to path
212
+ if value is not None:
213
+ value = Path.from_json(value)
214
+
215
+ # init instance
216
+ return cls(
217
+ path = value
218
+ )
219
+
220
+
221
+ @dataclass
222
+ @register("where")
223
+ class Where(Query):
224
+ """Gets items from sequence where condition met."""
225
+
226
+ path: Path
227
+
228
+
229
+ def apply(self, data, *args, **kwargs):
230
+ """Applies query to data."""
231
+
232
+ # filter items
233
+ return [item for item in data if self.path(item, *args, **kwargs)]
234
+
235
+
236
+ @classmethod
237
+ def from_json(cls, data):
238
+ """Initialize instance from JSON."""
239
+
240
+ # get value
241
+ value = next(iter(data.values()))
242
+
243
+ # init instance
244
+ return cls(
245
+ path = Path.from_json(value)
246
+ )
247
+
248
+
249
+ @dataclass
250
+ @register("distinct")
251
+ class Distinct(Query):
252
+ """Gets distinct items from sequence."""
253
+
254
+ path: Path | None = None
255
+
256
+
257
+ def apply(self, data, *args, **kwargs):
258
+ """Applies query to data."""
259
+
260
+ # check selector
261
+ if not isinstance(self.path, Query):
262
+ return list(set(data))
263
+
264
+ # find distinct
265
+ seen = set()
266
+ items = []
267
+ for item in data:
268
+ key = self.path(item, *args, **kwargs)
269
+ if key not in seen:
270
+ seen.add(key)
271
+ items.append(item)
272
+
273
+ return items
274
+
275
+
276
+ @classmethod
277
+ def from_json(cls, data):
278
+ """Initialize instance from JSON."""
279
+
280
+ # get value
281
+ value = next(iter(data.values()))
282
+
283
+ # convert to path
284
+ if value is not None:
285
+ value = Path.from_json(value)
286
+
287
+ # init instance
288
+ return cls(
289
+ path = value
290
+ )
291
+
292
+
293
+ @dataclass
294
+ @register("single")
295
+ class Single(Query):
296
+ """Gets the single item from sequence where condition met."""
297
+
298
+ path: Path | None = None
299
+
300
+
301
+ def apply(self, data, *args, **kwargs):
302
+ """Applies query to data."""
303
+
304
+ # get data
305
+ if isinstance(self.path, Query):
306
+ data = (item for item in data if self.path(item, *args, **kwargs))
307
+
308
+ # use single
309
+ i = -1
310
+ item = None
311
+ for i, item in enumerate(data):
312
+ if i > 0:
313
+ raise QIterError("More then one item found.")
314
+
315
+ # found
316
+ if i == 0:
317
+ return item
318
+
319
+ # no item found
320
+ raise QIterError("No item found.")
321
+
322
+
323
+ @classmethod
324
+ def from_json(cls, data):
325
+ """Initialize instance from JSON."""
326
+
327
+ # get value
328
+ value = next(iter(data.values()))
329
+
330
+ # convert to path
331
+ if value is not None:
332
+ value = Path.from_json(value)
333
+
334
+ # init instance
335
+ return cls(
336
+ path = value
337
+ )
338
+
339
+
340
+ @dataclass
341
+ @register("first")
342
+ class First(Single):
343
+ """Gets the first item from sequence where condition met."""
344
+
345
+
346
+ def apply(self, data, *args, **kwargs):
347
+ """Applies query to data."""
348
+
349
+ # get data
350
+ if isinstance(self.path, Query):
351
+ data = (item for item in data if self.path(item, *args, **kwargs))
352
+
353
+ # use first
354
+ for item in data:
355
+ return item
356
+
357
+ # no item found
358
+ raise QIterError("No item found.")
359
+
360
+
361
+ @dataclass
362
+ @register("last")
363
+ class Last(Single):
364
+ """Gets the last item from sequence where condition met."""
365
+
366
+
367
+ def apply(self, data, *args, **kwargs):
368
+ """Applies query to data."""
369
+
370
+ # get data
371
+ data = reversed(data)
372
+ if isinstance(self.path, Query):
373
+ data = (item for item in data if self.path(item, *args, **kwargs))
374
+
375
+ # use first
376
+ for item in data:
377
+ return item
378
+
379
+ # no item found
380
+ raise QIterError("No item found.")
381
+
382
+
383
+ @dataclass
384
+ @register("count")
385
+ class Count(Query):
386
+ """Gets count of items in sequence where condition met."""
387
+
388
+ path: Path | None = None
389
+
390
+
391
+ def apply(self, data, *args, **kwargs):
392
+ """Applies query to data."""
393
+
394
+ # check selector
395
+ if not isinstance(self.path, Query):
396
+ return len(data)
397
+
398
+ # count true
399
+ return sum(1 for item in data if self.path(item, *args, **kwargs))
400
+
401
+
402
+ @classmethod
403
+ def from_json(cls, data):
404
+ """Initialize instance from JSON."""
405
+
406
+ # get value
407
+ value = next(iter(data.values()))
408
+
409
+ # convert to path
410
+ if value is not None:
411
+ value = Path.from_json(value)
412
+
413
+ # init instance
414
+ return cls(
415
+ path = value
416
+ )
@@ -0,0 +1,22 @@
1
+ # Created byMartin.cz
2
+ # Copyright (c) Martin Strohalm. All rights reserved.
3
+
4
+
5
+ class QError(Exception):
6
+ """Represents a jqson exception base."""
7
+ pass
8
+
9
+
10
+ class QSyntaxError(QError):
11
+ """Represents an incorrect syntax exception."""
12
+ pass
13
+
14
+
15
+ class QUndefError(QError):
16
+ """Represents a missing definition exception."""
17
+ pass
18
+
19
+
20
+ class QIterError(QError):
21
+ """Represents an iteration exception."""
22
+ pass