python-jack-knife 0.6.12__tar.gz → 0.6.15__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 (86) hide show
  1. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/PKG-INFO +1 -1
  2. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/common.py +1 -7
  3. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/integrations/postgres_pipe.py +42 -20
  4. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/main.py +5 -3
  5. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/pipes/let_reduce.py +1 -1
  6. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/pipes/map.py +1 -1
  7. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/pipes/query_pipe.py +9 -1
  8. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/pipes/remove_field.py +2 -2
  9. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/registry.py +24 -34
  10. python_jack_knife-0.6.15/src/pjk/sinks/stdout.py +78 -0
  11. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/version.py +1 -1
  12. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/python_jack_knife.egg-info/PKG-INFO +1 -1
  13. python_jack_knife-0.6.12/src/pjk/sinks/stdout.py +0 -46
  14. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/LICENSE +0 -0
  15. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/README.md +0 -0
  16. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/pyproject.toml +0 -0
  17. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/setup.cfg +0 -0
  18. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/__init__.py +0 -0
  19. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/components.py +0 -0
  20. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/integrations/opensearch_client.py +0 -0
  21. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/integrations/opensearch_index_sink.py +0 -0
  22. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/integrations/opensearch_query_pipe.py +0 -0
  23. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/integrations/snowflake_pipe.py +0 -0
  24. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/log.py +0 -0
  25. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/man_page.py +0 -0
  26. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/parser.py +0 -0
  27. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/pipes/__init__.py +0 -0
  28. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/pipes/denorm.py +0 -0
  29. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/pipes/factory.py +0 -0
  30. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/pipes/filter.py +0 -0
  31. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/pipes/head.py +0 -0
  32. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/pipes/join.py +0 -0
  33. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/pipes/move_field.py +0 -0
  34. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/pipes/progress_pipe.py +0 -0
  35. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/pipes/sample.py +0 -0
  36. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/pipes/select.py +0 -0
  37. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/pipes/sort.py +0 -0
  38. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/pipes/tail.py +0 -0
  39. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/pipes/user_pipe_factory.py +0 -0
  40. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/pipes/where.py +0 -0
  41. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/progress.py +0 -0
  42. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/sinks/__init__.py +0 -0
  43. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/sinks/create_sink.py +0 -0
  44. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/sinks/csv_sink.py +0 -0
  45. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/sinks/devnull.py +0 -0
  46. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/sinks/dir_sink.py +0 -0
  47. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/sinks/expect.py +0 -0
  48. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/sinks/factory.py +0 -0
  49. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/sinks/format_sink.py +0 -0
  50. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/sinks/graph.py +0 -0
  51. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/sinks/graph_bar_line.py +0 -0
  52. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/sinks/graph_cumulative.py +0 -0
  53. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/sinks/graph_hist.py +0 -0
  54. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/sinks/graph_scatter.py +0 -0
  55. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/sinks/json_sink.py +0 -0
  56. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/sinks/s3_sink.py +0 -0
  57. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/sinks/s3_stream.py +0 -0
  58. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/sinks/sinks.py +0 -0
  59. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/sinks/tsv_sink.py +0 -0
  60. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/sinks/user_sink_factory.py +0 -0
  61. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/sources/__init__.py +0 -0
  62. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/sources/configs_source.py +0 -0
  63. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/sources/csv_source.py +0 -0
  64. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/sources/dir_source.py +0 -0
  65. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/sources/factory.py +0 -0
  66. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/sources/favorite_source.py +0 -0
  67. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/sources/format_source.py +0 -0
  68. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/sources/inline_source.py +0 -0
  69. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/sources/json_source.py +0 -0
  70. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/sources/lazy_file.py +0 -0
  71. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/sources/lazy_file_local.py +0 -0
  72. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/sources/lazy_file_s3.py +0 -0
  73. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/sources/macro_source.py +0 -0
  74. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/sources/npy_source.py +0 -0
  75. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/sources/parquet_source.py +0 -0
  76. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/sources/s3_source.py +0 -0
  77. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/sources/source_list.py +0 -0
  78. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/sources/sql_source.py +0 -0
  79. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/sources/tsv_source.py +0 -0
  80. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/sources/user_source_factory.py +0 -0
  81. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/pjk/usage.py +0 -0
  82. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/python_jack_knife.egg-info/SOURCES.txt +0 -0
  83. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/python_jack_knife.egg-info/dependency_links.txt +0 -0
  84. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/python_jack_knife.egg-info/entry_points.txt +0 -0
  85. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/src/python_jack_knife.egg-info/requires.txt +0 -0
  86. {python_jack_knife-0.6.12 → python_jack_knife-0.6.15}/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.6.12
3
+ Version: 0.6.15
4
4
  Summary: Python Jack Knife – a command line data processor
5
5
  Author-email: Mike Schultz <mike.schultz@gmail.com>
6
6
  License:
@@ -49,7 +49,7 @@ def pager_stdout(use_pager: bool = True):
49
49
 
50
50
  env = os.environ.copy()
51
51
  # -R: pass ANSI; -S: chop long lines; you can add -F/-X to taste
52
- env.setdefault("LESS", "-R")
52
+ env.setdefault("LESS", "-RFX")
53
53
  # Ensure UTF-8
54
54
  env.setdefault("LESSCHARSET", "utf-8")
55
55
 
@@ -161,12 +161,6 @@ class ComponentFactory:
161
161
  return None
162
162
  return wrapper.comp_class
163
163
 
164
- #def get_usage(self, name: str):
165
- # comp_class = self.get_component_class(name)
166
- # if not comp_class:
167
- # return None
168
- # return comp_class.usage()
169
-
170
164
  def create(self, token: str):
171
165
  pass
172
166
 
@@ -97,7 +97,8 @@ class PostgresPipe(QueryPipe,Integration):
97
97
  examples = [
98
98
  ['myquery.sql', 'postgres:mydb', '-'],
99
99
  ["{'query': 'SELECT * from MY_TABLE;'}", 'postgres:mydb', '-'],
100
- ["{'query': 'SELECT * FROM pg_catalog.pg_tables;'}", 'postgres:mydb']
100
+ ["{'query': 'SELECT * FROM pg_catalog.pg_tables;'}", 'postgres:mydb'],
101
+ ["{'query': 'SELECT stored_procedure(%s, ...), batch_params:{...}"]
101
102
  ]
102
103
 
103
104
  # name, type, default
@@ -165,14 +166,32 @@ class PostgresPipe(QueryPipe,Integration):
165
166
  try:
166
167
  query = record.get(self.query_field)
167
168
  if not query:
168
- record['_error'] = 'missing query'
169
- yield record
170
- else:
171
- params = record.get(self.params_field)
172
-
173
- cur = client.conn.cursor()
174
- try:
175
- # execute
169
+ record['_error'] = 'missing query'
170
+ yield record
171
+ return
172
+
173
+ params = record.get(self.params_field) # single-exec params
174
+ batch = record.get("batch_params", None) # list[tuple|dict] for batching
175
+
176
+ cur = client.conn.cursor()
177
+ try:
178
+ did_executemany = False
179
+
180
+ # ---------- execute ----------
181
+ if batch is not None:
182
+ # Handle batch sizes explicitly to preserve single-SELECT streaming semantics
183
+ if len(batch) == 0:
184
+ # No-op batch; execute a lightweight statement so we can still emit a header
185
+ cur.execute("SELECT 1")
186
+ header_params = {"batch_size": 0}
187
+ elif len(batch) == 1:
188
+ cur.execute(query, batch[0])
189
+ header_params = batch[0]
190
+ else:
191
+ cur.executemany(query, batch)
192
+ did_executemany = True
193
+ header_params = {"batch_size": len(batch)}
194
+ else:
176
195
  if params is None:
177
196
  cur.execute(query)
178
197
  else:
@@ -180,17 +199,20 @@ class PostgresPipe(QueryPipe,Integration):
180
199
  cur.execute(query, params)
181
200
  else:
182
201
  cur.execute(query, (params,))
202
+ header_params = params
183
203
 
184
- # yield header first
185
- yield self._make_header(cur, query, params)
186
-
187
- # then stream rows if it was a real SELECT with results
188
- if cur.description:
189
- cols = [d[0] for d in cur.description]
190
- if not (len(cols) == 1 and cols[0] == "ingest_event"):
191
- for row in cur:
192
- yield _row_to_dict(cur, row)
193
- finally:
194
- cur.close()
204
+ # ---------- header ----------
205
+ yield self._make_header(cur, query, header_params)
206
+
207
+ # ---------- stream rows (only meaningful for single execute that returns rows) ----------
208
+ # Note: executemany() typically doesn't expose per-execution result sets.
209
+ if not did_executemany and cur.description:
210
+ cols = [d[0] for d in cur.description]
211
+ if not (len(cols) == 1 and cols[0] == "ingest_event"):
212
+ for row in cur:
213
+ yield _row_to_dict(cur, row)
214
+
215
+ finally:
216
+ cur.close()
195
217
  finally:
196
218
  client.close()
@@ -10,7 +10,6 @@ from pjk.parser import ExpressionParser
10
10
  from pjk.usage import UsageError
11
11
  from pjk.log import init as init_logging
12
12
  from datetime import datetime
13
- from pathlib import Path
14
13
  import traceback
15
14
  import concurrent.futures
16
15
  from pjk.registry import ComponentRegistry
@@ -26,6 +25,10 @@ def write_history(tokens):
26
25
 
27
26
  log_path = ".pjk-history.txt"
28
27
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M")
28
+
29
+ if len(tokens) < 2:
30
+ return
31
+
29
32
  command = " ".join(tokens)
30
33
 
31
34
  try:
@@ -67,6 +70,7 @@ def execute_threaded(sinks, stop_progress=None):
67
70
 
68
71
  def initialize():
69
72
  init_logging()
73
+ write_history(sys.argv[1:])
70
74
 
71
75
  #src = Path("src/pjk/resources/configs.tmpl")
72
76
  #dst_dir = Path.home() / ".pjk"
@@ -121,8 +125,6 @@ def execute_tokens(tokens: List[str]):
121
125
  else:
122
126
  sink.drain()
123
127
 
124
- write_history(sys.argv[1:])
125
-
126
128
  except UsageError as e:
127
129
  print(e, file=sys.stderr)
128
130
  sys.exit(2)
@@ -24,7 +24,7 @@ def do_eval(expr, env):
24
24
  safe_env['json'] = json
25
25
  return eval(expr, {}, safe_env)
26
26
  except Exception:
27
- raise UsageError(f"UsageError in expression: {expr}")
27
+ raise Exception(f"Error in expression: {expr}")
28
28
 
29
29
  def eval_regular(expr: str, record: dict):
30
30
  env = {'f': SafeNamespace(record)}
@@ -51,7 +51,7 @@ class MapByPipe(Pipe, KeyedSource):
51
51
  key_rec = {}
52
52
  for field in self.fields:
53
53
  key_val = record.pop(field, None) if self.is_group else record.get(field)
54
- if not key_val:
54
+ if key_val is None: # not only false-ish but NONE
55
55
  return None
56
56
 
57
57
  key_rec[field] = key_val
@@ -2,7 +2,7 @@ from pjk.components import Pipe
2
2
  from pjk.usage import ParsedToken, Usage, CONFIG_FILE
3
3
  from typing import Any, Dict, Iterable, Optional
4
4
  from abc import abstractmethod
5
-
5
+ from pjk.progress import papi
6
6
 
7
7
  class QueryPipe(Pipe):
8
8
  name: str = None
@@ -40,6 +40,8 @@ class QueryPipe(Pipe):
40
40
  self.output_shape = usage.get_param('shape')
41
41
  self.count = usage.get_param('count')
42
42
  self.query_field = 'query' # for all subclasses
43
+ self.inrecs = papi.get_counter(self, var_label=None) # don't display progress
44
+ self.outrecs = papi.get_percentage_counter(self, var_label='recs_out', denom_counter=self.inrecs)
43
45
 
44
46
  @abstractmethod
45
47
  def execute_query_returning_S_xO_iterable(self, record) -> Iterable[Dict[str, Any]]:
@@ -53,6 +55,7 @@ class QueryPipe(Pipe):
53
55
 
54
56
  def __iter__(self):
55
57
  for in_rec in self.left:
58
+ self.inrecs.increment()
56
59
  iter = self.execute_query_returning_S_xO_iterable(in_rec)
57
60
 
58
61
  if self.output_shape == 'S_xO':
@@ -60,8 +63,11 @@ class QueryPipe(Pipe):
60
63
  for out_rec in iter:
61
64
  if not q_done:
62
65
  q_done = True
66
+ self.outrecs.increment()
63
67
  yield self._make_q_object(in_rec, out_rec)
64
68
  continue
69
+
70
+ self.outrecs.increment()
65
71
  yield out_rec
66
72
 
67
73
  elif self.output_shape == 'xO':
@@ -70,6 +76,7 @@ class QueryPipe(Pipe):
70
76
  if not q_done:
71
77
  q_done = True
72
78
  continue
79
+ self.outrecs.increment()
73
80
  yield out_rec
74
81
 
75
82
  elif self.output_shape == 'Sxo':
@@ -84,6 +91,7 @@ class QueryPipe(Pipe):
84
91
  continue
85
92
  r_list.append(out_rec)
86
93
  q_out['child'] = r_list
94
+ self.outrecs.increment()
87
95
  yield q_out
88
96
 
89
97
 
@@ -3,10 +3,10 @@
3
3
 
4
4
  # djk/pipes/remove_field.py
5
5
 
6
- from pjk.components import Pipe
6
+ from pjk.components import DeepCopyPipe
7
7
  from pjk.usage import ParsedToken, Usage, UsageError
8
8
 
9
- class RemoveField(Pipe):
9
+ class RemoveField(DeepCopyPipe):
10
10
  @classmethod
11
11
  def usage(cls):
12
12
  usage = Usage(
@@ -3,7 +3,6 @@
3
3
 
4
4
  import os
5
5
  import sys
6
- import time
7
6
  from pjk.sinks.factory import SinkFactory
8
7
  from pjk.pipes.factory import PipeFactory
9
8
  from pjk.sources.factory import SourceFactory
@@ -11,8 +10,6 @@ from pjk.sinks.format_sink import FormatSink
11
10
  from pjk.sources.format_source import FormatSource
12
11
  import importlib.util
13
12
  import importlib
14
- import importlib.metadata
15
- import sysconfig, pathlib, importlib
16
13
  from pjk.components import Pipe, Source, Sink
17
14
  from pjk.common import ComponentFactory, highlight, ComponentOrigin
18
15
  from typing import List, Type
@@ -38,7 +35,7 @@ class ComponentRegistry:
38
35
  self.pipe_factory = PipeFactory()
39
36
  self.sink_factory = SinkFactory()
40
37
  self.load_user_components()
41
- self.load_package_extras()
38
+ self.load_namespace_extras()
42
39
 
43
40
  def create_source(self, token: str):
44
41
  return self.source_factory.create(token)
@@ -125,42 +122,35 @@ class ComponentRegistry:
125
122
  elif is_source(obj, module):
126
123
  self.source_factory.register(name, obj, ComponentOrigin.USER)
127
124
 
128
- def iter_entry_points(self, group: str):
129
- """
130
- Return entry points in the given group across Python 3.9–3.12.
131
- """
132
- eps = importlib.metadata.entry_points()
133
- return eps.select(group=group) if hasattr(eps, "select") else eps.get(group, [])
125
+ def load_namespace_extras(self, package: str = "pjk_extras") -> None:
126
+ registrar = ExternalRegistrar(self.source_factory, self.pipe_factory, self.sink_factory)
127
+ import importlib, importlib.metadata as im
134
128
 
135
- def load_package_extras(self, group: str = "pjk.package_extras") -> None:
136
- """
137
- Load pip-installed extras and register their components into THIS registry.
129
+ for dist in im.distributions():
130
+ name = (dist.metadata.get("Name") or "")
131
+ if not name.startswith("pjk-"):
132
+ continue
138
133
 
139
- Preferred contract:
140
- entry point -> callable register(registrar)
134
+ modname = f"{package}.{name[4:].replace('-', '_')}" # pjk-foo-bar -> pjk_extras.foo_bar
141
135
 
142
- Fallback (legacy):
143
- entry point -> module path; we import the MODULE PART ONLY for side-effects.
144
- """
145
- registrar = ExternalRegistrar(self.source_factory, self.pipe_factory, self.sink_factory)
136
+ # Import the extra; if it fails, continue to the next
137
+ try:
138
+ mod = importlib.import_module(modname)
139
+ except Exception as e:
140
+ print(f"[pjk] import failed for {modname}: {e}")
141
+ continue
142
+
143
+ reg = getattr(mod, "register", None)
144
+ if not callable(reg):
145
+ print(f"[pjk] extra '{modname}' has no register(registrar)")
146
+ continue
146
147
 
147
- for ep in self.iter_entry_points(group):
148
+ # Run its register; if it fails, continue to the next
148
149
  try:
149
- # Try the modern, explicit path first.
150
- loader = getattr(ep, "load", None)
151
- if callable(loader):
152
- target = ep.load() # resolves "module:object" to the actual object
153
- if callable(target):
154
- target(registrar) # plugin registers into your live factories
155
- #print(f"[pjk] loaded extra (callable): {ep.name} -> {ep.value}")
156
- continue
157
- # Not callable -> fall through to legacy import
158
- # Legacy path: import ONLY the module portion before ':' for side-effects
159
- mod = ep.value.split(":", 1)[0]
160
- importlib.import_module(mod)
161
- #print(f"[pjk] loaded extra (import): {ep.name} -> {ep.value}")
150
+ reg(registrar) # registers class TYPES, same contract as before
162
151
  except Exception as e:
163
- print(f"[pjk] failed to load extra {ep.name}: {e}")
152
+ print(f"[pjk] register() failed in {modname}: {e}")
153
+ continue
164
154
 
165
155
  def print_core_formats(factories: List[ComponentFactory]):
166
156
  print(highlight('formats'))
@@ -0,0 +1,78 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright 2024 Mike Schultz
3
+
4
+ import sys
5
+ import yaml
6
+ from yaml.representer import SafeRepresenter # kept for compatibility
7
+ from pjk.components import Sink, Source
8
+ from pjk.usage import ParsedToken, Usage
9
+ from pjk.common import pager_stdout
10
+
11
+
12
+ class StdoutSink(Sink):
13
+ @classmethod
14
+ def usage(cls):
15
+ usage = Usage(
16
+ name='-',
17
+ desc='display records in yaml or txt format to stdout through less',
18
+ component_class=cls
19
+ )
20
+ usage.def_param('less', usage='use less to display', valid_values=['true', 'false'], default='true')
21
+ usage.def_param('format', usage='output format', valid_values=['yaml', 'txt'], default='yaml')
22
+ usage.def_example(["{hello:'world!'}"], "{hello:'world!'}")
23
+ return usage
24
+
25
+ def __init__(self, ptok: ParsedToken, usage: Usage):
26
+ super().__init__(ptok, usage)
27
+ self.use_pager = True if usage.get_param('less') is None else usage.get_param('less') == 'true'
28
+ self.output_format = usage.get_param('format') or 'yaml'
29
+
30
+ def _sanitize_scalar(self, v) -> str:
31
+ if v is None:
32
+ s = ''
33
+ elif isinstance(v, (list, dict)):
34
+ # single-line YAML to preserve structure compactly
35
+ s = yaml.safe_dump(v, sort_keys=False, allow_unicode=True).strip()
36
+ else:
37
+ s = str(v)
38
+ return s.replace('\r', ' ').replace('\n', ' ')
39
+
40
+ def _process_yaml(self):
41
+ for record in self.input:
42
+ try:
43
+ yaml.dump(
44
+ record,
45
+ sys.stdout,
46
+ sort_keys=False,
47
+ explicit_start=True, # '---' before each record
48
+ allow_unicode=True,
49
+ width=10**9,
50
+ )
51
+ except BrokenPipeError:
52
+ break
53
+
54
+ def _process_txt(self):
55
+ for record in self.input:
56
+ try:
57
+ # record delimiter
58
+ sys.stdout.write('---\n')
59
+ if isinstance(record, dict):
60
+ for k, v in record.items():
61
+ key = str(k)
62
+ val = self._sanitize_scalar(v)
63
+ sys.stdout.write(f'{key}: {val}\n')
64
+ else:
65
+ val = self._sanitize_scalar(record)
66
+ sys.stdout.write(f'value: {val}\n')
67
+ except BrokenPipeError:
68
+ break
69
+
70
+ def process(self) -> None:
71
+ try:
72
+ with pager_stdout(self.use_pager):
73
+ if self.output_format == 'txt':
74
+ self._process_txt()
75
+ else:
76
+ self._process_yaml()
77
+ except BrokenPipeError:
78
+ pass
@@ -1,4 +1,4 @@
1
1
  # SPDX-License-Identifier: Apache-2.0
2
2
  # Copyright 2024 Mike Schultz
3
3
 
4
- __version__ = "0.6.12"
4
+ __version__ = "0.6.15"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-jack-knife
3
- Version: 0.6.12
3
+ Version: 0.6.15
4
4
  Summary: Python Jack Knife – a command line data processor
5
5
  Author-email: Mike Schultz <mike.schultz@gmail.com>
6
6
  License:
@@ -1,46 +0,0 @@
1
- # SPDX-License-Identifier: Apache-2.0
2
- # Copyright 2024 Mike Schultz
3
-
4
- import sys
5
- import yaml
6
- from yaml.representer import SafeRepresenter
7
- from pjk.components import Sink, Source
8
- from pjk.usage import ParsedToken, Usage
9
- from pjk.common import pager_stdout
10
-
11
- class StdoutSink(Sink):
12
- @classmethod
13
- def usage(cls):
14
- usage = Usage(
15
- name='-',
16
- desc='display records in yaml format to stdout through less',
17
- component_class=cls
18
- )
19
- usage.def_param('less', usage='use less to display', valid_values=['true', 'false'], default='true')
20
- usage.def_example(["{hello:'world!'}"], "{hello:'world!'}")
21
- return usage
22
-
23
- def __init__(self, ptok: ParsedToken, usage: Usage):
24
- super().__init__(ptok, usage)
25
- self.use_pager = True if usage.get_param('less') == None else usage.get_param('less') == 'true'
26
-
27
- def process(self) -> None:
28
- try:
29
- with pager_stdout(self.use_pager):
30
- for record in self.input:
31
- try:
32
- # everything else -> normal YAML
33
- yaml.dump(
34
- record,
35
- sys.stdout,
36
- sort_keys=False,
37
- explicit_start=True,
38
- allow_unicode=True,
39
- width=10**9,
40
- )
41
- except BrokenPipeError:
42
- break
43
- except BrokenPipeError:
44
- pass
45
-
46
-