tq-query 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.
- tq_query-0.1.0/.github/workflows/ci.yml +29 -0
- tq_query-0.1.0/.github/workflows/publish.yml +57 -0
- tq_query-0.1.0/.gitignore +8 -0
- tq_query-0.1.0/PKG-INFO +119 -0
- tq_query-0.1.0/PUBLISHING.md +27 -0
- tq_query-0.1.0/README.md +108 -0
- tq_query-0.1.0/pyproject.toml +35 -0
- tq_query-0.1.0/src/tq/__init__.py +5 -0
- tq_query-0.1.0/src/tq/__main__.py +5 -0
- tq_query-0.1.0/src/tq/cli.py +135 -0
- tq_query-0.1.0/src/tq/query.py +528 -0
- tq_query-0.1.0/tests/test_cli.py +84 -0
- tq_query-0.1.0/tests/test_query.py +161 -0
- tq_query-0.1.0/uv.lock +149 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
test:
|
|
14
|
+
name: Python ${{ matrix.python-version }}
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
strategy:
|
|
17
|
+
fail-fast: false
|
|
18
|
+
matrix:
|
|
19
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
- uses: astral-sh/setup-uv@v5
|
|
23
|
+
with:
|
|
24
|
+
python-version: ${{ matrix.python-version }}
|
|
25
|
+
enable-cache: true
|
|
26
|
+
- run: uv sync --locked --extra cli
|
|
27
|
+
- run: uv run ruff check .
|
|
28
|
+
- run: uv run ruff format --check .
|
|
29
|
+
- run: uv run pytest -q
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: astral-sh/setup-uv@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: "3.12"
|
|
19
|
+
enable-cache: true
|
|
20
|
+
- run: uv sync --locked --extra cli
|
|
21
|
+
- name: Check tag matches package version
|
|
22
|
+
run: |
|
|
23
|
+
uv run python - <<'PY'
|
|
24
|
+
import os
|
|
25
|
+
import tomllib
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
version = tomllib.loads(Path("pyproject.toml").read_text())["project"]["version"]
|
|
29
|
+
expected = f"v{version}"
|
|
30
|
+
actual = os.environ["GITHUB_REF_NAME"]
|
|
31
|
+
if actual != expected:
|
|
32
|
+
raise SystemExit(f"tag {actual!r} does not match package version {expected!r}")
|
|
33
|
+
PY
|
|
34
|
+
- run: uv run ruff check .
|
|
35
|
+
- run: uv run ruff format --check .
|
|
36
|
+
- run: uv run pytest -q
|
|
37
|
+
- run: uv build
|
|
38
|
+
- uses: actions/upload-artifact@v4
|
|
39
|
+
with:
|
|
40
|
+
name: python-package-distributions
|
|
41
|
+
path: dist/
|
|
42
|
+
if-no-files-found: error
|
|
43
|
+
|
|
44
|
+
publish:
|
|
45
|
+
needs: build
|
|
46
|
+
runs-on: ubuntu-latest
|
|
47
|
+
environment:
|
|
48
|
+
name: pypi
|
|
49
|
+
url: https://pypi.org/p/tq-query
|
|
50
|
+
permissions:
|
|
51
|
+
id-token: write
|
|
52
|
+
steps:
|
|
53
|
+
- uses: actions/download-artifact@v4
|
|
54
|
+
with:
|
|
55
|
+
name: python-package-distributions
|
|
56
|
+
path: dist/
|
|
57
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
tq_query-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: tq-query
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Query arrays of structured data
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Keywords: arrays,json,query
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Provides-Extra: cli
|
|
9
|
+
Requires-Dist: rich<15,>=13.7; extra == 'cli'
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# tq
|
|
13
|
+
|
|
14
|
+
`tq` is a small Python package and CLI for querying JSON records.
|
|
15
|
+
|
|
16
|
+
It reads JSON Lines, JSON arrays, multiline objects, and consecutive JSON objects from a file or stdin, and writes matching records as JSON.
|
|
17
|
+
|
|
18
|
+
## Get started
|
|
19
|
+
|
|
20
|
+
Install the Python library only:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
pip install tq-query
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Install the CLI and its optional dependencies:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
pip install "tq-query[cli]"
|
|
30
|
+
# or: uv tool install "tq-query[cli]"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The `tq` command requires the `cli` extra; without it, the command prints an install hint.
|
|
34
|
+
|
|
35
|
+
To try `tq` with OpenRouter model data from models.dev, download the catalog and convert it to `models.jsonl`:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
curl -fsSL https://models.dev/catalog.json \
|
|
39
|
+
| jq '.providers.openrouter.models[] | {id, modalities, limit, reasoning, tool_call}' -c \
|
|
40
|
+
> models.jsonl
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Each line is one model record. Query by identity or fields:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
tq 'openai/*[reasoning;tool_call]' models.jsonl
|
|
47
|
+
tq '[limit.context>=200000]' models.jsonl
|
|
48
|
+
tq '[modalities.input has image]' models.jsonl
|
|
49
|
+
tq '[modalities.input has all (text,image)]' models.jsonl
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## CLI
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
usage: tq [OPTIONS] QUERY [FILE]
|
|
56
|
+
|
|
57
|
+
Arguments:
|
|
58
|
+
query match expression
|
|
59
|
+
file JSON input file; defaults to stdin
|
|
60
|
+
|
|
61
|
+
Options:
|
|
62
|
+
-h, --help show this help message and exit
|
|
63
|
+
-k KEY identity field (default: ref, then id)
|
|
64
|
+
-r, --reorder order matches by query branch, then input position
|
|
65
|
+
-C, --color force colored output
|
|
66
|
+
-M, --no-color force uncolored output
|
|
67
|
+
-c, --compact compact JSON output, one record per line
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Color is enabled for terminals and disabled for pipes. `-c` controls formatting independently.
|
|
71
|
+
|
|
72
|
+
## Query syntax
|
|
73
|
+
|
|
74
|
+
A query is a comma-separated list of branches. Each branch may combine an identity pattern with predicates:
|
|
75
|
+
|
|
76
|
+
```text
|
|
77
|
+
IDENTITY[PREDICATE;PREDICATE],IDENTITY[PREDICATE]
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Omit the identity to match any record. Branches are OR-ed; predicates within a branch are AND-ed and may be separated by commas or semicolons.
|
|
81
|
+
|
|
82
|
+
| Syntax | Meaning |
|
|
83
|
+
| --- | --- |
|
|
84
|
+
| `foo*` / `foo?` | Identity glob: `*` matches any sequence; `?` matches one character |
|
|
85
|
+
| `"foo*"` | Exact identity; wildcards are literal |
|
|
86
|
+
| `f` / `!f` | `f = true` / `f = false` |
|
|
87
|
+
| `f = v` / `f != v` | Equal / not equal |
|
|
88
|
+
| `f < v`, `f <= v`, `f > v`, `f >= v` | Ordered comparison |
|
|
89
|
+
| `f has v` / `f has no v` | Array contains / does not contain `v` |
|
|
90
|
+
| `f has any (a,b)` | Array contains at least one candidate |
|
|
91
|
+
| `f has all (a,b)` | Array contains every candidate |
|
|
92
|
+
| `f has none (a,b)` | Array contains no candidate |
|
|
93
|
+
| `v in f` / `v not in f` | Aliases for `f has v` / `f has no v` |
|
|
94
|
+
| `a.b.c` | Access a nested field |
|
|
95
|
+
|
|
96
|
+
`has any`, `has all`, and `has none` require at least two candidates.
|
|
97
|
+
|
|
98
|
+
Quote values when needed; for example, `"true"` is the text value `true`. Missing fields never match a predicate, including negative predicates. Explicit `null` is distinct from a missing field.
|
|
99
|
+
|
|
100
|
+
## Python API
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from tq import Query
|
|
104
|
+
|
|
105
|
+
schema = {"key": "id", "filter": ["reasoning", "tool_call"]}
|
|
106
|
+
query = Query.parse("openai/*[reasoning;tool_call]").validate(schema)
|
|
107
|
+
|
|
108
|
+
record = {
|
|
109
|
+
"id": "openai/gpt-5",
|
|
110
|
+
"reasoning": True,
|
|
111
|
+
"tool_call": True,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
branch = query.match(record)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Publishing
|
|
118
|
+
|
|
119
|
+
See [PUBLISHING.md](PUBLISHING.md) for PyPI Trusted Publishing setup and release instructions.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Publishing
|
|
2
|
+
|
|
3
|
+
The `publish.yml` workflow publishes `tq-query` to PyPI when a `v*` version tag is pushed. It verifies that the tag matches the version in `pyproject.toml`, runs checks and tests, builds the wheel and source distribution, then publishes them using PyPI Trusted Publishing (OIDC). No PyPI token is stored in GitHub.
|
|
4
|
+
|
|
5
|
+
## One-time PyPI setup
|
|
6
|
+
|
|
7
|
+
Configure a Trusted Publisher for `tq-query` with:
|
|
8
|
+
|
|
9
|
+
- Owner: `briceyan`
|
|
10
|
+
- Repository: `tq`
|
|
11
|
+
- Workflow: `publish.yml`
|
|
12
|
+
- Environment: `pypi`
|
|
13
|
+
|
|
14
|
+
If the project has not been published before, configure it as a pending publisher when creating the PyPI project.
|
|
15
|
+
|
|
16
|
+
## Release a version
|
|
17
|
+
|
|
18
|
+
1. Update `project.version` in `pyproject.toml` and refresh `uv.lock` with `uv lock`.
|
|
19
|
+
2. Run the CI checks and commit the changes to `main`.
|
|
20
|
+
3. Create and push a matching version tag, for example:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
git tag v0.1.1
|
|
24
|
+
git push origin v0.1.1
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The tag push starts the publish workflow. A GitHub Release is not required.
|
tq_query-0.1.0/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# tq
|
|
2
|
+
|
|
3
|
+
`tq` is a small Python package and CLI for querying JSON records.
|
|
4
|
+
|
|
5
|
+
It reads JSON Lines, JSON arrays, multiline objects, and consecutive JSON objects from a file or stdin, and writes matching records as JSON.
|
|
6
|
+
|
|
7
|
+
## Get started
|
|
8
|
+
|
|
9
|
+
Install the Python library only:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
pip install tq-query
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Install the CLI and its optional dependencies:
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
pip install "tq-query[cli]"
|
|
19
|
+
# or: uv tool install "tq-query[cli]"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The `tq` command requires the `cli` extra; without it, the command prints an install hint.
|
|
23
|
+
|
|
24
|
+
To try `tq` with OpenRouter model data from models.dev, download the catalog and convert it to `models.jsonl`:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
curl -fsSL https://models.dev/catalog.json \
|
|
28
|
+
| jq '.providers.openrouter.models[] | {id, modalities, limit, reasoning, tool_call}' -c \
|
|
29
|
+
> models.jsonl
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Each line is one model record. Query by identity or fields:
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
tq 'openai/*[reasoning;tool_call]' models.jsonl
|
|
36
|
+
tq '[limit.context>=200000]' models.jsonl
|
|
37
|
+
tq '[modalities.input has image]' models.jsonl
|
|
38
|
+
tq '[modalities.input has all (text,image)]' models.jsonl
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## CLI
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
usage: tq [OPTIONS] QUERY [FILE]
|
|
45
|
+
|
|
46
|
+
Arguments:
|
|
47
|
+
query match expression
|
|
48
|
+
file JSON input file; defaults to stdin
|
|
49
|
+
|
|
50
|
+
Options:
|
|
51
|
+
-h, --help show this help message and exit
|
|
52
|
+
-k KEY identity field (default: ref, then id)
|
|
53
|
+
-r, --reorder order matches by query branch, then input position
|
|
54
|
+
-C, --color force colored output
|
|
55
|
+
-M, --no-color force uncolored output
|
|
56
|
+
-c, --compact compact JSON output, one record per line
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Color is enabled for terminals and disabled for pipes. `-c` controls formatting independently.
|
|
60
|
+
|
|
61
|
+
## Query syntax
|
|
62
|
+
|
|
63
|
+
A query is a comma-separated list of branches. Each branch may combine an identity pattern with predicates:
|
|
64
|
+
|
|
65
|
+
```text
|
|
66
|
+
IDENTITY[PREDICATE;PREDICATE],IDENTITY[PREDICATE]
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Omit the identity to match any record. Branches are OR-ed; predicates within a branch are AND-ed and may be separated by commas or semicolons.
|
|
70
|
+
|
|
71
|
+
| Syntax | Meaning |
|
|
72
|
+
| --- | --- |
|
|
73
|
+
| `foo*` / `foo?` | Identity glob: `*` matches any sequence; `?` matches one character |
|
|
74
|
+
| `"foo*"` | Exact identity; wildcards are literal |
|
|
75
|
+
| `f` / `!f` | `f = true` / `f = false` |
|
|
76
|
+
| `f = v` / `f != v` | Equal / not equal |
|
|
77
|
+
| `f < v`, `f <= v`, `f > v`, `f >= v` | Ordered comparison |
|
|
78
|
+
| `f has v` / `f has no v` | Array contains / does not contain `v` |
|
|
79
|
+
| `f has any (a,b)` | Array contains at least one candidate |
|
|
80
|
+
| `f has all (a,b)` | Array contains every candidate |
|
|
81
|
+
| `f has none (a,b)` | Array contains no candidate |
|
|
82
|
+
| `v in f` / `v not in f` | Aliases for `f has v` / `f has no v` |
|
|
83
|
+
| `a.b.c` | Access a nested field |
|
|
84
|
+
|
|
85
|
+
`has any`, `has all`, and `has none` require at least two candidates.
|
|
86
|
+
|
|
87
|
+
Quote values when needed; for example, `"true"` is the text value `true`. Missing fields never match a predicate, including negative predicates. Explicit `null` is distinct from a missing field.
|
|
88
|
+
|
|
89
|
+
## Python API
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from tq import Query
|
|
93
|
+
|
|
94
|
+
schema = {"key": "id", "filter": ["reasoning", "tool_call"]}
|
|
95
|
+
query = Query.parse("openai/*[reasoning;tool_call]").validate(schema)
|
|
96
|
+
|
|
97
|
+
record = {
|
|
98
|
+
"id": "openai/gpt-5",
|
|
99
|
+
"reasoning": True,
|
|
100
|
+
"tool_call": True,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
branch = query.match(record)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Publishing
|
|
107
|
+
|
|
108
|
+
See [PUBLISHING.md](PUBLISHING.md) for PyPI Trusted Publishing setup and release instructions.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tq-query"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Query arrays of structured data"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = { text = "Apache-2.0" }
|
|
12
|
+
keywords = ["query", "json", "arrays"]
|
|
13
|
+
dependencies = []
|
|
14
|
+
|
|
15
|
+
[project.optional-dependencies]
|
|
16
|
+
cli = ["rich>=13.7,<15"]
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
tq = "tq.cli:main"
|
|
20
|
+
|
|
21
|
+
[tool.hatch.build.targets.wheel]
|
|
22
|
+
packages = ["src/tq"]
|
|
23
|
+
|
|
24
|
+
[tool.pytest.ini_options]
|
|
25
|
+
pythonpath = ["src"]
|
|
26
|
+
testpaths = ["tests"]
|
|
27
|
+
|
|
28
|
+
[tool.ruff]
|
|
29
|
+
target-version = "py311"
|
|
30
|
+
|
|
31
|
+
[dependency-groups]
|
|
32
|
+
dev = [
|
|
33
|
+
"pytest>=9.1.1",
|
|
34
|
+
"ruff>=0.16.9",
|
|
35
|
+
]
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""CLI for matching JSON records and rendering results."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from collections.abc import Mapping, Sequence
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from .query import Query, QueryError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _read_records(source: str) -> list[Mapping[str, Any]]:
|
|
17
|
+
decoder = json.JSONDecoder()
|
|
18
|
+
records: list[Mapping[str, Any]] = []
|
|
19
|
+
position = 0
|
|
20
|
+
while position < len(source):
|
|
21
|
+
while position < len(source) and source[position].isspace():
|
|
22
|
+
position += 1
|
|
23
|
+
if position == len(source):
|
|
24
|
+
break
|
|
25
|
+
try:
|
|
26
|
+
value, position = decoder.raw_decode(source, position)
|
|
27
|
+
except json.JSONDecodeError as error:
|
|
28
|
+
line = source.count("\n", 0, error.pos) + 1
|
|
29
|
+
raise QueryError(f"invalid JSON near line {line}: {error.msg}") from error
|
|
30
|
+
for record in value if isinstance(value, list) else (value,):
|
|
31
|
+
if not isinstance(record, Mapping):
|
|
32
|
+
raise QueryError("input records must be JSON objects")
|
|
33
|
+
records.append(record)
|
|
34
|
+
return records
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _infer_key(records: Sequence[Mapping[str, Any]], explicit: str | None) -> str:
|
|
38
|
+
if explicit:
|
|
39
|
+
return explicit
|
|
40
|
+
for candidate in ("ref", "id"):
|
|
41
|
+
if any(candidate in record for record in records):
|
|
42
|
+
return candidate
|
|
43
|
+
raise QueryError("cannot infer key field; provide -k KEY")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
47
|
+
parser = argparse.ArgumentParser(
|
|
48
|
+
description="Filter JSON records with tq match union syntax",
|
|
49
|
+
usage="tq [-h] [-k KEY] [-r] [-C | -M] [-c] query [file]",
|
|
50
|
+
)
|
|
51
|
+
parser.add_argument("query", metavar="query", help="match union expression")
|
|
52
|
+
parser.add_argument(
|
|
53
|
+
"file", nargs="?", metavar="file", help="JSON input file; defaults to stdin"
|
|
54
|
+
)
|
|
55
|
+
parser.add_argument(
|
|
56
|
+
"-k",
|
|
57
|
+
"--key",
|
|
58
|
+
metavar="KEY",
|
|
59
|
+
dest="key",
|
|
60
|
+
help="record key field (default: ref, then id)",
|
|
61
|
+
)
|
|
62
|
+
parser.add_argument(
|
|
63
|
+
"-r",
|
|
64
|
+
"--reorder",
|
|
65
|
+
action="store_true",
|
|
66
|
+
help="order matches by query branch, then input position",
|
|
67
|
+
)
|
|
68
|
+
parser.add_argument(
|
|
69
|
+
"-c",
|
|
70
|
+
"--compact",
|
|
71
|
+
action="store_true",
|
|
72
|
+
help="compact output to one line per record",
|
|
73
|
+
)
|
|
74
|
+
colors = parser.add_mutually_exclusive_group()
|
|
75
|
+
colors.add_argument(
|
|
76
|
+
"-C",
|
|
77
|
+
"--color",
|
|
78
|
+
action="store_true",
|
|
79
|
+
help="force color even when output is piped",
|
|
80
|
+
)
|
|
81
|
+
colors.add_argument(
|
|
82
|
+
"-M", "--no-color", action="store_true", help="force plain output"
|
|
83
|
+
)
|
|
84
|
+
return parser
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
88
|
+
"""Filter records and render each result as JSON."""
|
|
89
|
+
parser = _build_parser()
|
|
90
|
+
args = parser.parse_args(argv)
|
|
91
|
+
try:
|
|
92
|
+
from rich.console import Console
|
|
93
|
+
from rich.json import JSON
|
|
94
|
+
except ModuleNotFoundError:
|
|
95
|
+
parser.error("the CLI requires the optional extra; install `tq-query[cli]`")
|
|
96
|
+
try:
|
|
97
|
+
source = (
|
|
98
|
+
Path(args.file).read_text(encoding="utf-8")
|
|
99
|
+
if args.file
|
|
100
|
+
else sys.stdin.read()
|
|
101
|
+
)
|
|
102
|
+
records = _read_records(source)
|
|
103
|
+
query = Query.parse(args.query)
|
|
104
|
+
key = _infer_key(records, args.key)
|
|
105
|
+
matched = [
|
|
106
|
+
(index, branch)
|
|
107
|
+
for index, item in enumerate(records)
|
|
108
|
+
if (branch := query.match(item, key=key)) is not None
|
|
109
|
+
]
|
|
110
|
+
if args.reorder:
|
|
111
|
+
matched.sort(key=lambda result: (result[1], result[0]))
|
|
112
|
+
|
|
113
|
+
is_terminal = bool(getattr(sys.stdout, "isatty", lambda: False)())
|
|
114
|
+
color_enabled = args.color or (
|
|
115
|
+
is_terminal and not args.no_color and "NO_COLOR" not in os.environ
|
|
116
|
+
)
|
|
117
|
+
console = Console(
|
|
118
|
+
file=sys.stdout,
|
|
119
|
+
force_terminal=color_enabled,
|
|
120
|
+
no_color=not color_enabled,
|
|
121
|
+
color_system="standard" if color_enabled else None,
|
|
122
|
+
width=100_000,
|
|
123
|
+
)
|
|
124
|
+
for item_index, _branch in matched:
|
|
125
|
+
record = records[item_index]
|
|
126
|
+
if args.compact:
|
|
127
|
+
compact = json.dumps(record, ensure_ascii=False, separators=(",", ":"))
|
|
128
|
+
console.print(
|
|
129
|
+
JSON(compact, indent=None, ensure_ascii=False), soft_wrap=True
|
|
130
|
+
)
|
|
131
|
+
else:
|
|
132
|
+
console.print(JSON.from_data(record, indent=2, ensure_ascii=False))
|
|
133
|
+
except (OSError, QueryError, TypeError, ValueError) as error:
|
|
134
|
+
parser.error(str(error))
|
|
135
|
+
return 0
|