python-jack-knife 0.7.1__tar.gz → 0.7.5__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.
- {python_jack_knife-0.7.1/src/python_jack_knife.egg-info → python_jack_knife-0.7.5}/PKG-INFO +2 -1
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/pyproject.toml +1 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/__init__.py +2 -1
- python_jack_knife-0.7.5/src/pjk/engine.py +51 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/history.py +1 -1
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/integrations/opensearch_query_pipe.py +1 -2
- python_jack_knife-0.7.5/src/pjk/integrations/postgres_pipe.py +285 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/main.py +4 -2
- python_jack_knife-0.7.5/src/pjk/parse_pjk_file.py +66 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/parser.py +108 -33
- python_jack_knife-0.7.5/src/pjk/pipes/ddiff.py +144 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/pipes/factory.py +44 -1
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/pipes/let_reduce.py +137 -4
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/pipes/query_pipe.py +31 -21
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/registry.py +7 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/sinks/format_sink.py +2 -2
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/sinks/graph.py +14 -0
- python_jack_knife-0.7.5/src/pjk/sinks/graph_axis.py +9 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/sinks/graph_bar_line.py +15 -2
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/sinks/graph_cumulative.py +4 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/sinks/graph_hist.py +3 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/sinks/graph_scatter.py +4 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/sources/csv_source.py +0 -2
- python_jack_knife-0.7.5/src/pjk/sources/dict_list_source.py +15 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/sources/factory.py +18 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/sources/format_source.py +17 -7
- python_jack_knife-0.7.5/src/pjk/sources/http_source.py +98 -0
- python_jack_knife-0.7.5/src/pjk/sources/s3_select_source.py +394 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/sources/sql_source.py +13 -4
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/usage.py +24 -2
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/version.py +1 -1
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5/src/python_jack_knife.egg-info}/PKG-INFO +2 -1
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/python_jack_knife.egg-info/SOURCES.txt +7 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/python_jack_knife.egg-info/requires.txt +1 -0
- python_jack_knife-0.7.1/src/pjk/integrations/postgres_pipe.py +0 -227
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/LICENSE +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/README.md +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/setup.cfg +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/common.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/components.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/integrations/opensearch_client.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/integrations/opensearch_index_sink.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/integrations/snowflake_pipe.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/log.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/man_page.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/pipes/__init__.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/pipes/denorm.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/pipes/filter.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/pipes/head.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/pipes/join.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/pipes/map.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/pipes/move_field.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/pipes/progress_pipe.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/pipes/remove_field.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/pipes/sample.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/pipes/select.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/pipes/sort.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/pipes/tail.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/pipes/user_pipe_factory.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/pipes/where.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/progress.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/sinks/__init__.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/sinks/create_sink.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/sinks/csv_sink.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/sinks/devnull.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/sinks/dir_sink.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/sinks/expect.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/sinks/factory.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/sinks/json_sink.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/sinks/s3_sink.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/sinks/s3_stream.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/sinks/sinks.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/sinks/stdout.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/sinks/tsv_sink.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/sinks/user_sink_factory.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/sources/__init__.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/sources/dir_source.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/sources/favorite_source.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/sources/inline_source.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/sources/json_source.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/sources/lazy_file.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/sources/lazy_file_local.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/sources/lazy_file_s3.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/sources/npy_source.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/sources/parquet_source.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/sources/s3_source.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/sources/source_list.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/sources/tsv_source.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/pjk/sources/user_source_factory.py +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/python_jack_knife.egg-info/dependency_links.txt +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/python_jack_knife.egg-info/entry_points.txt +0 -0
- {python_jack_knife-0.7.1 → python_jack_knife-0.7.5}/src/python_jack_knife.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-jack-knife
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.5
|
|
4
4
|
Summary: Python Jack Knife – a command line data processor
|
|
5
5
|
Author-email: Mike Schultz <mike.schultz@gmail.com>
|
|
6
6
|
License:
|
|
@@ -212,6 +212,7 @@ License-File: LICENSE
|
|
|
212
212
|
Requires-Dist: hjson>=3.1.0
|
|
213
213
|
Requires-Dist: pyyaml>=6.0
|
|
214
214
|
Requires-Dist: requests>=2.32.0
|
|
215
|
+
Requires-Dist: deepdiff<9,>=8.0.0
|
|
215
216
|
Provides-Extra: aws
|
|
216
217
|
Requires-Dist: boto3>=1.34; extra == "aws"
|
|
217
218
|
Provides-Extra: postgres
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright 2024 Mike Schultz
|
|
3
|
+
|
|
4
|
+
from typing import Iterator, List, Optional
|
|
5
|
+
|
|
6
|
+
from pjk.parser import ExpressionParser, expand_macros
|
|
7
|
+
from pjk.registry import ComponentRegistry
|
|
8
|
+
from pjk.sources.dict_list_source import DictListSource
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PjkEngine:
|
|
12
|
+
"""
|
|
13
|
+
Run a pjk pipeline from a .pjk file, optionally with supplied input records.
|
|
14
|
+
|
|
15
|
+
- inrecs supplied: the source in the .pjk file is replaced with inrecs.
|
|
16
|
+
Expression may be full (source + pipes + sink) or pipes-only.
|
|
17
|
+
- inrecs=None: expression.pjk is fully self-contained (source, pipes, sink)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, inrecs: Optional[List[dict]] = None, pjk_file: str = ""):
|
|
21
|
+
self.inrecs = inrecs
|
|
22
|
+
self.pjk_file = pjk_file
|
|
23
|
+
|
|
24
|
+
def __iter__(self) -> Iterator[dict]:
|
|
25
|
+
registry = ComponentRegistry()
|
|
26
|
+
parser = ExpressionParser(registry)
|
|
27
|
+
expanded = expand_macros([self.pjk_file])
|
|
28
|
+
|
|
29
|
+
if self.inrecs is not None:
|
|
30
|
+
source_override = DictListSource(self.inrecs)
|
|
31
|
+
try:
|
|
32
|
+
first_is_source = registry.create_source(expanded[0]) is not None
|
|
33
|
+
except Exception:
|
|
34
|
+
first_is_source = False
|
|
35
|
+
if first_is_source:
|
|
36
|
+
expanded = ["{to_override: 'true'}"] + expanded[1:]
|
|
37
|
+
else:
|
|
38
|
+
expanded = ["{to_override: 'true'}"] + expanded
|
|
39
|
+
else:
|
|
40
|
+
source_override = None
|
|
41
|
+
|
|
42
|
+
sink = parser.parse(expanded, source_override=source_override)
|
|
43
|
+
|
|
44
|
+
inputs = [sink.input]
|
|
45
|
+
sink.input._get_sources(inputs)
|
|
46
|
+
try:
|
|
47
|
+
for record in sink.input:
|
|
48
|
+
yield record
|
|
49
|
+
finally:
|
|
50
|
+
for inp in inputs:
|
|
51
|
+
inp.close()
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright 2024 Mike Schultz
|
|
3
|
+
#
|
|
4
|
+
# djk/pipes/postgres_pipe.py
|
|
5
|
+
|
|
6
|
+
import base64
|
|
7
|
+
import datetime as _dt
|
|
8
|
+
import socket
|
|
9
|
+
import sys
|
|
10
|
+
import uuid
|
|
11
|
+
import time
|
|
12
|
+
from decimal import Decimal
|
|
13
|
+
from typing import Any, Dict, Optional
|
|
14
|
+
|
|
15
|
+
from pjk.usage import ParsedToken, Usage
|
|
16
|
+
from pjk.common import Integration
|
|
17
|
+
from pjk.pipes.query_pipe import QueryPipe
|
|
18
|
+
|
|
19
|
+
MAX_RETRIES = 3
|
|
20
|
+
BASE_DELAY = 0.1 # seconds
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _print_db_connect_failure(host: str, port: int, exc: BaseException) -> None:
|
|
24
|
+
print("Failed to connect to DB", file=sys.stderr)
|
|
25
|
+
cur: Optional[BaseException] = exc
|
|
26
|
+
while cur is not None:
|
|
27
|
+
if isinstance(cur, socket.gaierror):
|
|
28
|
+
print(
|
|
29
|
+
f" Could not resolve hostname {host!r} (port {port}). "
|
|
30
|
+
"Private or corporate DB hosts usually require VPN or split-DNS.",
|
|
31
|
+
file=sys.stderr,
|
|
32
|
+
)
|
|
33
|
+
return
|
|
34
|
+
cur = cur.__cause__
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class DBClient:
|
|
38
|
+
"""Per-instance pg8000 connection wrapper. No shared state."""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
host: str,
|
|
43
|
+
username: str,
|
|
44
|
+
password: Optional[str],
|
|
45
|
+
db_name: str,
|
|
46
|
+
port: int = 5432,
|
|
47
|
+
ssl: bool = False,
|
|
48
|
+
):
|
|
49
|
+
import pg8000 # lazy import
|
|
50
|
+
|
|
51
|
+
kwargs = dict(
|
|
52
|
+
user=username,
|
|
53
|
+
password=password,
|
|
54
|
+
host=host,
|
|
55
|
+
database=db_name,
|
|
56
|
+
port=port,
|
|
57
|
+
)
|
|
58
|
+
if ssl:
|
|
59
|
+
import ssl as _ssl
|
|
60
|
+
|
|
61
|
+
kwargs["ssl_context"] = _ssl.create_default_context()
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
self.conn = pg8000.connect(**kwargs)
|
|
65
|
+
self.conn.autocommit = True
|
|
66
|
+
except Exception as e:
|
|
67
|
+
_print_db_connect_failure(host, port, e)
|
|
68
|
+
raise
|
|
69
|
+
|
|
70
|
+
def close(self):
|
|
71
|
+
if getattr(self, "conn", None) is None:
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
import pg8000 # lazy
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
self.conn.close()
|
|
78
|
+
except pg8000.exceptions.InterfaceError:
|
|
79
|
+
# Already closed / broken; ignore.
|
|
80
|
+
pass
|
|
81
|
+
finally:
|
|
82
|
+
self.conn = None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _iso_dt(x: _dt.datetime) -> str:
|
|
86
|
+
"""ISO 8601; normalize UTC offset to 'Z'."""
|
|
87
|
+
s = x.isoformat()
|
|
88
|
+
return s.replace("+00:00", "Z")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def normalize(obj: Any) -> Any:
|
|
92
|
+
"""
|
|
93
|
+
Make values JSON/YAML-safe and portable (schema-agnostic):
|
|
94
|
+
- Decimal -> exact string (no sci-notation)
|
|
95
|
+
- date/datetime/time -> ISO-8601 string (datetime keeps offset; UTC -> 'Z')
|
|
96
|
+
- UUID -> string
|
|
97
|
+
- bytes -> base64 string
|
|
98
|
+
- lists/tuples/sets, dicts -> normalized recursively
|
|
99
|
+
- leaves int/float/str/bool/None as-is
|
|
100
|
+
"""
|
|
101
|
+
if obj is None:
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
if isinstance(obj, Decimal):
|
|
105
|
+
return format(obj, "f") # exact value as string
|
|
106
|
+
|
|
107
|
+
if isinstance(obj, _dt.datetime):
|
|
108
|
+
return _iso_dt(obj)
|
|
109
|
+
|
|
110
|
+
if isinstance(obj, (_dt.date, _dt.time)):
|
|
111
|
+
return obj.isoformat()
|
|
112
|
+
|
|
113
|
+
if isinstance(obj, uuid.UUID):
|
|
114
|
+
return str(obj)
|
|
115
|
+
|
|
116
|
+
if isinstance(obj, (bytes, bytearray, memoryview)):
|
|
117
|
+
return base64.b64encode(bytes(obj)).decode("ascii")
|
|
118
|
+
|
|
119
|
+
if isinstance(obj, dict):
|
|
120
|
+
return {k: normalize(v) for k, v in obj.items()}
|
|
121
|
+
|
|
122
|
+
if isinstance(obj, (list, tuple, set)):
|
|
123
|
+
return [normalize(v) for v in obj]
|
|
124
|
+
|
|
125
|
+
return obj
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _row_to_dict(cursor, row) -> Dict[str, Any]:
|
|
129
|
+
cols = [d[0] for d in cursor.description]
|
|
130
|
+
return {col: normalize(val) for col, val in zip(cols, row)}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class PostgresPipe(QueryPipe, Integration):
|
|
134
|
+
name = "postgres"
|
|
135
|
+
desc = "Postgres query pipe; executes SQL over input record['query']."
|
|
136
|
+
arg0 = ("instance", "instance of database.")
|
|
137
|
+
examples = [
|
|
138
|
+
["myquery.sql", "postgres:mydb", "-"],
|
|
139
|
+
["{'query': 'SELECT * from MY_TABLE;'}", "postgres:mydb", "-"],
|
|
140
|
+
["{'query': 'SELECT * FROM pg_catalog.pg_tables;'}", "postgres:mydb"],
|
|
141
|
+
["{'query': 'SELECT procedure_batch(%s, ...), batch_params:{...}"],
|
|
142
|
+
["{'query': 'SELECT procedure_jsonb(%s, ...), json_params:json_string"],
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
# name, type, default
|
|
146
|
+
config_tuples = [
|
|
147
|
+
("db_name", str, None),
|
|
148
|
+
("host", str, None),
|
|
149
|
+
("user", str, None),
|
|
150
|
+
("password", str, None),
|
|
151
|
+
("port", int, 5432),
|
|
152
|
+
("ssl", bool, False),
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
def __init__(self, ptok: ParsedToken, u: Usage, root=None):
|
|
156
|
+
super().__init__(ptok, u, root=root)
|
|
157
|
+
|
|
158
|
+
self.db_name = u.get_config("db_name")
|
|
159
|
+
self.db_host = u.get_config("host")
|
|
160
|
+
self.db_user = u.get_config("user")
|
|
161
|
+
self.db_pass = u.get_config("password")
|
|
162
|
+
self.db_port = u.get_config("port")
|
|
163
|
+
self.db_ssl = u.get_config("ssl")
|
|
164
|
+
|
|
165
|
+
# Standard params field: single-exec params (list/tuple/dict/single value)
|
|
166
|
+
self.params_field = "params"
|
|
167
|
+
|
|
168
|
+
# Legacy batch path: list[tuple|list|dict] → executemany
|
|
169
|
+
self.batch_field = "batch_params"
|
|
170
|
+
|
|
171
|
+
# Explicit JSON payload field (no query sniffing).
|
|
172
|
+
# If present, this value is passed to cur.execute(query, json_params).
|
|
173
|
+
self.json_params_field = "json_params"
|
|
174
|
+
|
|
175
|
+
# One DB client (and thus one connection) per PostgresPipe instance.
|
|
176
|
+
# Under your invariant (one thread per pipe), this is thread-safe.
|
|
177
|
+
self.client = DBClient(
|
|
178
|
+
host=self.db_host,
|
|
179
|
+
username=self.db_user,
|
|
180
|
+
password=self.db_pass,
|
|
181
|
+
db_name=self.db_name,
|
|
182
|
+
port=self.db_port,
|
|
183
|
+
ssl=self.db_ssl,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
def reset(self):
|
|
187
|
+
# stateless across reset
|
|
188
|
+
pass
|
|
189
|
+
|
|
190
|
+
def close(self):
|
|
191
|
+
if self.client is not None:
|
|
192
|
+
self.client.close()
|
|
193
|
+
|
|
194
|
+
def _make_header(self, cur, query: str, params=None) -> Dict[str, Any]:
|
|
195
|
+
"""
|
|
196
|
+
Inspect the cursor and build a full header record.
|
|
197
|
+
Figures out result, rowcount, function automatically.
|
|
198
|
+
"""
|
|
199
|
+
h = {
|
|
200
|
+
"db": self.db_name,
|
|
201
|
+
"dbhost": self.db_host,
|
|
202
|
+
}
|
|
203
|
+
if params is not None:
|
|
204
|
+
h["params"] = params
|
|
205
|
+
|
|
206
|
+
if cur.description:
|
|
207
|
+
cols = [d[0] for d in cur.description]
|
|
208
|
+
if len(cols) == 1 and cols[0] == "ingest_event":
|
|
209
|
+
_ = cur.fetchone() # consume void row
|
|
210
|
+
h["result"] = "ok"
|
|
211
|
+
h["function"] = "ingest_event"
|
|
212
|
+
else:
|
|
213
|
+
h["result"] = "ok"
|
|
214
|
+
h["rowcount"] = cur.rowcount if cur.rowcount != -1 else None
|
|
215
|
+
else:
|
|
216
|
+
h["result"] = "ok"
|
|
217
|
+
h["rowcount"] = cur.rowcount
|
|
218
|
+
|
|
219
|
+
return h
|
|
220
|
+
|
|
221
|
+
def execute_query_returning_S_xO_iterable(self, record):
|
|
222
|
+
query = record.get(self.query_field)
|
|
223
|
+
if not query:
|
|
224
|
+
record["_error"] = "missing query"
|
|
225
|
+
yield record
|
|
226
|
+
return
|
|
227
|
+
|
|
228
|
+
# Priority: json_params > batch_params > params
|
|
229
|
+
json_params = record.get(self.json_params_field, None)
|
|
230
|
+
batch = record.get(self.batch_field, None)
|
|
231
|
+
params = record.get(self.params_field, None)
|
|
232
|
+
|
|
233
|
+
cur = self.client.conn.cursor()
|
|
234
|
+
try:
|
|
235
|
+
did_executemany = False
|
|
236
|
+
header_params = None
|
|
237
|
+
|
|
238
|
+
# ---------- execute ----------
|
|
239
|
+
if json_params is not None:
|
|
240
|
+
# Explicit JSON payload; caller controls shape.
|
|
241
|
+
# We don't inspect query or payload.
|
|
242
|
+
if isinstance(json_params, (list, tuple, dict)):
|
|
243
|
+
cur.execute(query, json_params)
|
|
244
|
+
else:
|
|
245
|
+
cur.execute(query, (json_params,))
|
|
246
|
+
header_params = {self.json_params_field: json_params}
|
|
247
|
+
|
|
248
|
+
elif batch is not None:
|
|
249
|
+
# Legacy executemany path; no magic.
|
|
250
|
+
if len(batch) == 0:
|
|
251
|
+
cur.execute("SELECT 1")
|
|
252
|
+
header_params = {"batch_size": 0}
|
|
253
|
+
elif len(batch) == 1:
|
|
254
|
+
cur.execute(query, batch[0])
|
|
255
|
+
header_params = {"batch_size": 1, "params": batch[0]}
|
|
256
|
+
else:
|
|
257
|
+
cur.executemany(query, batch)
|
|
258
|
+
did_executemany = True
|
|
259
|
+
header_params = {"batch_size": len(batch)}
|
|
260
|
+
|
|
261
|
+
else:
|
|
262
|
+
# Single-statement path.
|
|
263
|
+
if params is None:
|
|
264
|
+
cur.execute(query)
|
|
265
|
+
header_params = None
|
|
266
|
+
else:
|
|
267
|
+
if isinstance(params, (list, tuple, dict)):
|
|
268
|
+
cur.execute(query, params)
|
|
269
|
+
else:
|
|
270
|
+
cur.execute(query, (params,))
|
|
271
|
+
header_params = params
|
|
272
|
+
|
|
273
|
+
# ---------- header ----------
|
|
274
|
+
yield self._make_header(cur, query, header_params)
|
|
275
|
+
|
|
276
|
+
# ---------- stream rows (only meaningful for single execute that returns rows) ----------
|
|
277
|
+
if not did_executemany and cur.description:
|
|
278
|
+
cols = [d[0] for d in cur.description]
|
|
279
|
+
if not (len(cols) == 1 and cols[0] == "ingest_event"):
|
|
280
|
+
for row in cur:
|
|
281
|
+
yield _row_to_dict(cur, row)
|
|
282
|
+
|
|
283
|
+
finally:
|
|
284
|
+
cur.close()
|
|
285
|
+
# connection stays open for this pipe; closed in .close()
|
|
@@ -14,7 +14,7 @@ import concurrent.futures
|
|
|
14
14
|
from pjk.registry import ComponentRegistry
|
|
15
15
|
from pjk.sinks.stdout import StdoutSink
|
|
16
16
|
from pjk.man_page import do_man, do_examples, display_configs, display_macros
|
|
17
|
-
from pjk.history import write_history, display_history, get_history_tokens
|
|
17
|
+
from pjk.history import write_history, display_history, get_history_tokens, printable_command
|
|
18
18
|
from pjk.sinks.expect import ExpectSink
|
|
19
19
|
from pjk.progress import ProgressDisplay
|
|
20
20
|
from pjk.version import __version__
|
|
@@ -52,7 +52,6 @@ def execute_threaded(sinks, stop_progress=None):
|
|
|
52
52
|
|
|
53
53
|
def initialize():
|
|
54
54
|
init_logging()
|
|
55
|
-
write_history(sys.argv[1:])
|
|
56
55
|
|
|
57
56
|
#src = Path("src/pjk/resources/configs.tmpl")
|
|
58
57
|
#dst_dir = Path.home() / ".pjk"
|
|
@@ -111,12 +110,15 @@ def execute_tokens(tokens: List[str]):
|
|
|
111
110
|
if not tokens:
|
|
112
111
|
print('No such history')
|
|
113
112
|
return
|
|
113
|
+
cmd = printable_command(tokens)
|
|
114
|
+
print(f"pjk {cmd}")
|
|
114
115
|
|
|
115
116
|
parser = ExpressionParser(registry)
|
|
116
117
|
|
|
117
118
|
display = None
|
|
118
119
|
try:
|
|
119
120
|
sink = parser.parse(tokens)
|
|
121
|
+
write_history(sys.argv[1:]) # now that it's parsed sucessfully
|
|
120
122
|
if not isinstance(sink, (StdoutSink | ExpectSink)):
|
|
121
123
|
display = ProgressDisplay(interval=3.0)
|
|
122
124
|
display.start()
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright 2024 Mike Schultz
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import shlex
|
|
7
|
+
from typing import Dict, List
|
|
8
|
+
from pjk.usage import TokenError, UsageError
|
|
9
|
+
|
|
10
|
+
PJK_END_TOKEN = 'END'
|
|
11
|
+
PJK_SET_TOKEN = 'SET'
|
|
12
|
+
|
|
13
|
+
# ${VAR} or $VAR - match anywhere in token (${VAR} first to avoid partial match)
|
|
14
|
+
VAR_REF_PATTERN = re.compile(r'\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}|\$([a-zA-Z_][a-zA-Z0-9_]*)')
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _expand_token(t: str, env: Dict[str, str]) -> str:
|
|
18
|
+
"""Expand $VAR or ${VAR} anywhere in token; raise if undefined."""
|
|
19
|
+
|
|
20
|
+
def repl(m):
|
|
21
|
+
name = m.group(1) or m.group(2)
|
|
22
|
+
if name not in env:
|
|
23
|
+
raise TokenError(f"Undefined variable: ${name}")
|
|
24
|
+
return env[name]
|
|
25
|
+
|
|
26
|
+
return VAR_REF_PATTERN.sub(repl, t)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def handle_pjk_file(token: str, expanded: List[str]):
|
|
30
|
+
if not token.endswith(".pjk"):
|
|
31
|
+
return False
|
|
32
|
+
|
|
33
|
+
if not os.path.isfile(token):
|
|
34
|
+
raise TokenError(f"pjk file not found: {token}")
|
|
35
|
+
|
|
36
|
+
with open(token, "r") as f:
|
|
37
|
+
lines = f.readlines()
|
|
38
|
+
|
|
39
|
+
env: Dict[str, str] = {}
|
|
40
|
+
|
|
41
|
+
for line in lines:
|
|
42
|
+
try:
|
|
43
|
+
parts = shlex.split(line, comments=True, posix=True)
|
|
44
|
+
except ValueError as e:
|
|
45
|
+
raise UsageError(f"Error parsing {token}: {e}")
|
|
46
|
+
|
|
47
|
+
if not parts:
|
|
48
|
+
continue
|
|
49
|
+
if parts[0] == PJK_END_TOKEN:
|
|
50
|
+
break
|
|
51
|
+
|
|
52
|
+
if parts[0] == PJK_SET_TOKEN:
|
|
53
|
+
for p in parts[1:]:
|
|
54
|
+
if '=' in p:
|
|
55
|
+
k, v = p.split('=', 1)
|
|
56
|
+
env[k.strip()] = v.strip()
|
|
57
|
+
continue
|
|
58
|
+
|
|
59
|
+
for p in parts:
|
|
60
|
+
if p == PJK_END_TOKEN:
|
|
61
|
+
break
|
|
62
|
+
expanded.append(_expand_token(p, env))
|
|
63
|
+
else:
|
|
64
|
+
continue
|
|
65
|
+
break
|
|
66
|
+
return True
|