python-jack-knife 0.6.11__tar.gz → 0.6.12__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 (87) hide show
  1. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/PKG-INFO +1 -1
  2. python_jack_knife-0.6.12/src/pjk/common.py +174 -0
  3. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/main.py +1 -2
  4. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/man_page.py +9 -11
  5. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/registry.py +94 -64
  6. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/sinks/graph.py +2 -2
  7. python_jack_knife-0.6.12/src/pjk/sinks/graph_bar_line.py +383 -0
  8. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/sources/factory.py +0 -2
  9. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/version.py +1 -1
  10. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/python_jack_knife.egg-info/PKG-INFO +1 -1
  11. python_jack_knife-0.6.11/src/pjk/common.py +0 -123
  12. python_jack_knife-0.6.11/src/pjk/sinks/graph_bar_line.py +0 -229
  13. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/LICENSE +0 -0
  14. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/README.md +0 -0
  15. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/pyproject.toml +0 -0
  16. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/setup.cfg +0 -0
  17. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/__init__.py +0 -0
  18. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/components.py +0 -0
  19. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/integrations/opensearch_client.py +0 -0
  20. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/integrations/opensearch_index_sink.py +0 -0
  21. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/integrations/opensearch_query_pipe.py +0 -0
  22. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/integrations/postgres_pipe.py +0 -0
  23. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/integrations/snowflake_pipe.py +0 -0
  24. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/log.py +0 -0
  25. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/parser.py +0 -0
  26. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/pipes/__init__.py +0 -0
  27. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/pipes/denorm.py +0 -0
  28. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/pipes/factory.py +0 -0
  29. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/pipes/filter.py +0 -0
  30. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/pipes/head.py +0 -0
  31. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/pipes/join.py +0 -0
  32. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/pipes/let_reduce.py +0 -0
  33. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/pipes/map.py +0 -0
  34. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/pipes/move_field.py +0 -0
  35. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/pipes/progress_pipe.py +0 -0
  36. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/pipes/query_pipe.py +0 -0
  37. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/pipes/remove_field.py +0 -0
  38. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/pipes/sample.py +0 -0
  39. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/pipes/select.py +0 -0
  40. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/pipes/sort.py +0 -0
  41. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/pipes/tail.py +0 -0
  42. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/pipes/user_pipe_factory.py +0 -0
  43. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/pipes/where.py +0 -0
  44. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/progress.py +0 -0
  45. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/sinks/__init__.py +0 -0
  46. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/sinks/create_sink.py +0 -0
  47. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/sinks/csv_sink.py +0 -0
  48. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/sinks/devnull.py +0 -0
  49. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/sinks/dir_sink.py +0 -0
  50. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/sinks/expect.py +0 -0
  51. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/sinks/factory.py +0 -0
  52. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/sinks/format_sink.py +0 -0
  53. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/sinks/graph_cumulative.py +0 -0
  54. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/sinks/graph_hist.py +0 -0
  55. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/sinks/graph_scatter.py +0 -0
  56. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/sinks/json_sink.py +0 -0
  57. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/sinks/s3_sink.py +0 -0
  58. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/sinks/s3_stream.py +0 -0
  59. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/sinks/sinks.py +0 -0
  60. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/sinks/stdout.py +0 -0
  61. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/sinks/tsv_sink.py +0 -0
  62. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/sinks/user_sink_factory.py +0 -0
  63. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/sources/__init__.py +0 -0
  64. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/sources/configs_source.py +0 -0
  65. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/sources/csv_source.py +0 -0
  66. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/sources/dir_source.py +0 -0
  67. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/sources/favorite_source.py +0 -0
  68. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/sources/format_source.py +0 -0
  69. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/sources/inline_source.py +0 -0
  70. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/sources/json_source.py +0 -0
  71. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/sources/lazy_file.py +0 -0
  72. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/sources/lazy_file_local.py +0 -0
  73. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/sources/lazy_file_s3.py +0 -0
  74. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/sources/macro_source.py +0 -0
  75. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/sources/npy_source.py +0 -0
  76. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/sources/parquet_source.py +0 -0
  77. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/sources/s3_source.py +0 -0
  78. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/sources/source_list.py +0 -0
  79. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/sources/sql_source.py +0 -0
  80. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/sources/tsv_source.py +0 -0
  81. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/sources/user_source_factory.py +0 -0
  82. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/pjk/usage.py +0 -0
  83. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/python_jack_knife.egg-info/SOURCES.txt +0 -0
  84. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/python_jack_knife.egg-info/dependency_links.txt +0 -0
  85. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/python_jack_knife.egg-info/entry_points.txt +0 -0
  86. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/src/python_jack_knife.egg-info/requires.txt +0 -0
  87. {python_jack_knife-0.6.11 → python_jack_knife-0.6.12}/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.11
3
+ Version: 0.6.12
4
4
  Summary: Python Jack Knife – a command line data processor
5
5
  Author-email: Mike Schultz <mike.schultz@gmail.com>
6
6
  License:
@@ -0,0 +1,174 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright 2024 Mike Schultz
3
+
4
+ import contextlib, io, os, subprocess, sys
5
+ import os
6
+ import re
7
+ from abc import ABC
8
+ from enum import Enum
9
+ from pjk.sources.format_source import FormatSource
10
+ from pjk.sinks.format_sink import FormatSink
11
+ from typing import List, Type
12
+
13
+ class SafeNamespace:
14
+ def __init__(self, obj):
15
+ for k, v in obj.items():
16
+ if isinstance(v, dict):
17
+ v = SafeNamespace(v)
18
+ elif isinstance(v, list):
19
+ v = [SafeNamespace(x) if isinstance(x, dict) else x for x in v]
20
+ setattr(self, k, v)
21
+
22
+ def __getattr__(self, key):
23
+ return None # gracefully handle missing keys
24
+
25
+ class ReducingNamespace:
26
+ def __init__(self, record):
27
+ self._record = record
28
+
29
+ def __getattr__(self, name):
30
+ value = self._record[name]
31
+ if isinstance(value, (list, tuple, set)):
32
+ return value
33
+ return [value] # promote scalars to singleton lists
34
+
35
+ # pjk/common.py
36
+ import contextlib, io, os, subprocess, sys
37
+
38
+ @contextlib.contextmanager
39
+ def pager_stdout(use_pager: bool = True):
40
+ """
41
+ Stream stdout into `less` via a pipe.
42
+ - If stdout is not a TTY or use_pager is False → write directly to sys.stdout.
43
+ - Otherwise spawn `less` and replace sys.stdout with less.stdin.
44
+ """
45
+ # If not a TTY, paging makes no sense
46
+ if not use_pager or not sys.stdout.isatty():
47
+ yield
48
+ return
49
+
50
+ env = os.environ.copy()
51
+ # -R: pass ANSI; -S: chop long lines; you can add -F/-X to taste
52
+ env.setdefault("LESS", "-R")
53
+ # Ensure UTF-8
54
+ env.setdefault("LESSCHARSET", "utf-8")
55
+
56
+ stdout_orig = sys.stdout
57
+ stderr_orig = sys.stderr
58
+
59
+ # Start less with a *pipe* for stdin and inherit the real terminal for out/err
60
+ pager = subprocess.Popen(
61
+ ["less"],
62
+ stdin=subprocess.PIPE,
63
+ stdout=stdout_orig, # keep interactivity
64
+ stderr=stderr_orig,
65
+ env=env,
66
+ close_fds=True,
67
+ bufsize=0, # unbuffered pipe
68
+ )
69
+
70
+ # Wrap less.stdin as a text writer and swap sys.stdout
71
+ assert pager.stdin is not None
72
+ pager_bin = pager.stdin
73
+ pager_txt = io.TextIOWrapper(pager_bin, encoding="utf-8", write_through=True)
74
+
75
+ sys.stdout = pager_txt
76
+ try:
77
+ yield
78
+ except BrokenPipeError:
79
+ pass
80
+ finally:
81
+ try:
82
+ sys.stdout.flush()
83
+ except Exception:
84
+ pass
85
+ # Restore first, then close pager stdin to send EOF
86
+ sys.stdout = stdout_orig
87
+ try:
88
+ pager_txt.flush()
89
+ except Exception:
90
+ pass
91
+ try:
92
+ pager_bin.close() # EOF → lets less exit
93
+ except Exception:
94
+ pass
95
+ try:
96
+ pager.wait()
97
+ except Exception:
98
+ pass
99
+
100
+ COLOR_CODES = {
101
+ 'bold': '\033[1m',
102
+ 'underline': '\033[4m',
103
+ 'red': '\033[31m',
104
+ 'green': '\033[32m',
105
+ 'yellow': '\033[33m',
106
+ 'blue': '\033[34m',
107
+ 'magenta': '\033[35m',
108
+ 'cyan': '\033[36m',
109
+ 'gray': '\033[90m',
110
+ }
111
+
112
+ RESET = '\033[0m'
113
+
114
+ def highlight(text: str, color: str = 'bold', value: str = None) -> str:
115
+ value = text if not value else value
116
+ style = COLOR_CODES.get(color.lower(), COLOR_CODES['bold'])
117
+ return text.replace(value, f"{style}{value}{RESET}")
118
+
119
+ # mixin
120
+ class Integration(ABC):
121
+ pass
122
+
123
+ class ComponentOrigin(Enum):
124
+ CORE = 0 # core components defined in python-jack-knife
125
+ EXTERNAL = 1 # component loaded via load_package_extras (displayed in either 'integrations' or 'applications')
126
+ USER = 2 # components loaded via load_user_components (always displayed in user_components)
127
+
128
+ class ComponentWrapper:
129
+ def __init__(self, name: str, comp_class, origin: ComponentOrigin):
130
+ self.name = name
131
+ self.comp_class = comp_class
132
+ self.origin = origin
133
+ self.is_integration = issubclass(comp_class, Integration)
134
+
135
+ class ComponentFactory:
136
+ def __init__(self, core_components: dict):
137
+ self.wrappers = {}
138
+ for k, v in core_components.items():
139
+ self.register(k, v, origin=ComponentOrigin.CORE)
140
+
141
+ def register(self, name, comp_class, origin: ComponentOrigin):
142
+ self.wrappers[name] = ComponentWrapper(name, comp_class=comp_class, origin=origin)
143
+
144
+ # is_integration True|False|None=don't care
145
+ def get_components(self, origin_list: List[ComponentOrigin], is_integration: bool) -> dict:
146
+ all = {}
147
+ for wrapper in self.wrappers.values():
148
+ if is_integration is not None:
149
+ if wrapper.is_integration != is_integration:
150
+ continue
151
+
152
+ for o in origin_list:
153
+ if wrapper.origin == o:
154
+ all[wrapper.name] = wrapper.comp_class
155
+
156
+ return all
157
+
158
+ def get_component_class(self, name: str):
159
+ wrapper = self.wrappers.get(name, None)
160
+ if not wrapper:
161
+ return None
162
+ return wrapper.comp_class
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
+ def create(self, token: str):
171
+ pass
172
+
173
+ def is_valid_field_name(name: str):
174
+ return re.fullmatch(r'^[A-Za-z_][A-Za-z0-9_]*$', name)
@@ -5,7 +5,6 @@
5
5
  import sys
6
6
  import os
7
7
  import shlex
8
- import shutil
9
8
  from typing import List
10
9
  from pjk.parser import ExpressionParser
11
10
  from pjk.usage import UsageError
@@ -80,7 +79,7 @@ def execute(command: str):
80
79
 
81
80
  def execute_tokens(tokens: List[str]):
82
81
  initialize()
83
-
82
+
84
83
  if '--version' in tokens:
85
84
  print(f"pjk version {__version__}")
86
85
  sys.exit(0)
@@ -8,7 +8,7 @@ from pjk.parser import ExpressionParser
8
8
  from pjk.components import Source, Pipe, Sink
9
9
  from pjk.usage import Usage, ParsedToken
10
10
  from pjk.registry import ComponentRegistry
11
- from pjk.common import pager_stdout, highlight
11
+ from pjk.common import pager_stdout, highlight, ComponentOrigin
12
12
  from contextlib import nullcontext
13
13
 
14
14
  def get_base_class(usage: Usage, as_string: bool = False):
@@ -49,9 +49,9 @@ def do_man(name: str, registry: ComponentRegistry):
49
49
  # source and sinks have common names so go through multiple times
50
50
  printed = False
51
51
  for factory in registry.get_factories():
52
- usage = factory.get_usage(name)
53
- if usage:
54
- print_man(registry, name, usage)
52
+ comp_class = factory.get_component_class(name)
53
+ if comp_class:
54
+ print_man(registry, name, comp_class.usage())
55
55
  printed = True
56
56
 
57
57
  if not printed:
@@ -61,11 +61,9 @@ def do_all_man(registry: ComponentRegistry, no_pager: bool = True):
61
61
  cm = nullcontext() if no_pager else pager_stdout()
62
62
  with cm:
63
63
  for factory in registry.get_factories():
64
- #comp_type = factory.get_comp_type_name()
65
- component_tuples = factory.get_component_name_class_tuples() # all of them
66
- for name, comp_class in component_tuples:
67
- usage = factory.get_usage(name)
68
- print_man(registry, name, usage)
64
+ component_dict = factory.get_components([ComponentOrigin.CORE, ComponentOrigin.EXTERNAL, ComponentOrigin.USER], is_integration=None)
65
+ for name, comp_class in component_dict.items():
66
+ print_man(registry, name, comp_class.usage())
69
67
  print()
70
68
 
71
69
  def print_man(registry: ComponentRegistry, name: str, usage: Usage):
@@ -94,8 +92,8 @@ def do_examples(token:str, registry: ComponentRegistry):
94
92
  cm = nullcontext() if no_pager else pager_stdout()
95
93
  with cm:
96
94
  for factory in registry.get_factories():
97
- comp_type = factory.get_comp_type_name()
98
- for name, comp_class in factory.get_component_name_class_tuples():
95
+ component_dict = factory.get_components([ComponentOrigin.CORE, ComponentOrigin.EXTERNAL, ComponentOrigin.USER], is_integration=None)
96
+ for name, comp_class in component_dict.items():
99
97
  usage = comp_class.usage()
100
98
 
101
99
  comp_type = get_base_class(usage, as_string=True)
@@ -3,6 +3,7 @@
3
3
 
4
4
  import os
5
5
  import sys
6
+ import time
6
7
  from pjk.sinks.factory import SinkFactory
7
8
  from pjk.pipes.factory import PipeFactory
8
9
  from pjk.sources.factory import SourceFactory
@@ -11,24 +12,33 @@ from pjk.sources.format_source import FormatSource
11
12
  import importlib.util
12
13
  import importlib
13
14
  import importlib.metadata
15
+ import sysconfig, pathlib, importlib
14
16
  from pjk.components import Pipe, Source, Sink
15
- from pjk.common import ComponentFactory, highlight
16
- from typing import List
17
+ from pjk.common import ComponentFactory, highlight, ComponentOrigin
18
+ from typing import List, Type
17
19
 
18
- class DisplayHolder:
19
- def __init__(self, factories: List[ComponentFactory]):
20
- for factory in factories:
21
- pass
22
-
20
+ class ExternalRegistrar:
21
+ def __init__(self, sources: SourceFactory, pipes: PipeFactory, sinks: SinkFactory) -> None:
22
+ self._sources: SourceFactory = sources
23
+ self._pipes: PipeFactory = pipes
24
+ self._sinks: SinkFactory = sinks
25
+
26
+ def source(self, name: str, cls: Type[Source]) -> None:
27
+ self._sources.register(name, cls, origin=ComponentOrigin.EXTERNAL)
28
+
29
+ def pipe(self, name: str, cls: Type[Pipe]) -> None:
30
+ self._pipes.register(name, cls, origin=ComponentOrigin.EXTERNAL)
31
+
32
+ def sink(self, name: str, cls: Type[Sink]) -> None:
33
+ self._sinks.register(name, cls, origin=ComponentOrigin.EXTERNAL)
23
34
 
24
35
  class ComponentRegistry:
25
36
  def __init__(self):
26
37
  self.source_factory = SourceFactory()
27
38
  self.pipe_factory = PipeFactory()
28
39
  self.sink_factory = SinkFactory()
29
-
30
40
  self.load_user_components()
31
- load_package_extras()
41
+ self.load_package_extras()
32
42
 
33
43
  def create_source(self, token: str):
34
44
  return self.source_factory.create(token)
@@ -56,9 +66,31 @@ class ComponentRegistry:
56
66
  print()
57
67
  print_factory_core(self.sink_factory, header='sinks')
58
68
 
59
- self.print_origin_components('integration', 'integrations')
60
- self.print_origin_components('user', 'user components (~/.pjk/plugins)')
69
+ self.print_non_core([ComponentOrigin.CORE,ComponentOrigin.EXTERNAL], is_integration=True, header='integrations')
70
+ self.print_non_core([ComponentOrigin.EXTERNAL], is_integration=False, header='apps')
71
+ self.print_non_core([ComponentOrigin.USER], is_integration=None, header='user components (~/.pjk/plugins)')
72
+
73
+ # is_integration = True|False|None None=don't care
74
+ def print_non_core(self, origin_list: List[ComponentOrigin], is_integration: bool, header:str):
75
+ all = {}
76
+ for factory in [self.source_factory, self.pipe_factory, self.sink_factory]:
77
+ component_dict = factory.get_components(origin_list=origin_list, is_integration=is_integration)
78
+ all.update(component_dict)
79
+
80
+ if not all:
81
+ return
61
82
 
83
+ print()
84
+ print(highlight(header))
85
+
86
+ for name, comp_class in all.items():
87
+ usage = comp_class.usage()
88
+ comp_class_type_str = get_component_type(comp_class)
89
+ lines = usage.desc.split('\n')
90
+ temp = highlight(comp_class_type_str)
91
+ line = f' {name:<17} {temp:<15} {lines[0]}'
92
+ print(line)
93
+
62
94
  def load_user_components(self, path=os.path.expanduser("~/.pjk/plugins")):
63
95
  if not os.path.isdir(path):
64
96
  return
@@ -87,36 +119,55 @@ class ComponentRegistry:
87
119
  name = usage.name
88
120
 
89
121
  if is_sink(obj, module):
90
- self.sink_factory.register(name, obj, 'user')
122
+ self.sink_factory.register(name, obj, ComponentOrigin.USER)
91
123
  elif is_pipe(obj, module):
92
- self.pipe_factory.register(name, obj, 'user')
124
+ self.pipe_factory.register(name, obj, ComponentOrigin.USER)
93
125
  elif is_source(obj, module):
94
- self.source_factory.register(name, obj, 'user')
126
+ self.source_factory.register(name, obj, ComponentOrigin.USER)
95
127
 
96
- def print_origin_components(self, origin: str, header:str):
97
- component_tuples = []
98
- for factory in [self.source_factory, self.pipe_factory, self.sink_factory]:
99
- component_tuples.extend(factory.get_component_name_class_tuples(origin))
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, [])
100
134
 
101
- if not component_tuples:
102
- return
103
- print()
104
- print(highlight(header))
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.
105
138
 
106
- for name, comp_class in component_tuples:
107
- usage = comp_class.usage()
108
- comp_class_type_str = get_component_type(comp_class)
109
- lines = usage.desc.split('\n')
110
- temp = highlight(comp_class_type_str)
111
- line = f' {name:<17} {temp:<15} {lines[0]}'
112
- print(line)
139
+ Preferred contract:
140
+ entry point -> callable register(registrar)
141
+
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)
146
+
147
+ for ep in self.iter_entry_points(group):
148
+ 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}")
162
+ except Exception as e:
163
+ print(f"[pjk] failed to load extra {ep.name}: {e}")
113
164
 
114
165
  def print_core_formats(factories: List[ComponentFactory]):
115
166
  print(highlight('formats'))
116
167
  formats = set()
117
168
  for factory in factories:
118
- tuples = factory.get_component_name_class_tuples('core')
119
- for name, comp_class in tuples:
169
+ component_dict = factory.get_components([ComponentOrigin.CORE], is_integration=False)
170
+ for name, comp_class in component_dict.items():
120
171
  if issubclass(comp_class, FormatSink|FormatSource):
121
172
  formats.add(name)
122
173
 
@@ -124,23 +175,21 @@ def print_core_formats(factories: List[ComponentFactory]):
124
175
  lst = ', '.join(list(formats))
125
176
  print(f'{space:<15}{lst}. (sources/sinks in local files, dirs and s3)')
126
177
 
127
- def print_factory_core(factory: ComponentFactory, header: str, include_formats: bool=False, include_integrations=False):
128
- components:list = factory.get_component_name_class_tuples('core')
129
- header = highlight(header)
130
- print(header)
178
+ def print_factory_core(factory: ComponentFactory, header: str):
179
+ component_dict = factory.get_components([ComponentOrigin.CORE], is_integration=False)
180
+ header = highlight(header)
181
+ print(header)
131
182
 
132
- i = 0
133
- # user and outside package components are also here, but printed from registry class
134
- for name, comp_class in components:
135
- if issubclass(comp_class, FormatSink|FormatSource) and not include_formats:
136
- continue
183
+ # user and outside package components are also here, but printed from registry class
184
+ for name, comp_class in component_dict.items():
185
+ if issubclass(comp_class, FormatSink|FormatSource):
186
+ continue
137
187
 
138
- usage = comp_class.usage()
139
- lines = usage.desc.split('\n')
188
+ usage = comp_class.usage()
189
+ lines = usage.desc.split('\n')
140
190
 
141
- line = f' {name:<12} {lines[0]}'
142
- print(line)
143
- i += 1
191
+ line = f' {name:<12} {lines[0]}'
192
+ print(line)
144
193
 
145
194
  def get_component_type(component_class) -> str:
146
195
  if issubclass(component_class, Sink):
@@ -178,22 +227,3 @@ def is_sink(obj, module):
178
227
  and obj.__module__ == module.__name__
179
228
  )
180
229
 
181
-
182
- def iter_entry_points(group: str):
183
- eps = importlib.metadata.entry_points()
184
- if hasattr(eps, "select"):
185
- # Python 3.10+ (importlib.metadata.EntryPoints)
186
- return eps.select(group=group)
187
- # Python 3.9 and older
188
- return eps.get(group, [])
189
-
190
- def load_package_extras():
191
- """
192
- Discover and import all installed pjk extras (via entry points).
193
- """
194
- for ep in iter_entry_points("pjk.package_extras"):
195
- try:
196
- importlib.import_module(ep.value)
197
- print(f"[pjk] loaded package extra: {ep.name} -> {ep.value}")
198
- except Exception as e:
199
- print(f"[pjk] failed to load extra {ep.name}: {e}")
@@ -18,8 +18,8 @@ class GraphSink(Sink):
18
18
  component_class=cls
19
19
  )
20
20
  usage.def_arg(name='kind', usage='hist|scatter|bar|line|cumulative')
21
- usage.def_param(name='x', usage='Name of x-axis field', default='x')
22
- usage.def_param(name='y', usage='Name of y-axis field', default='y')
21
+ usage.def_param(name='x', usage='x-axis field', default='x')
22
+ usage.def_param(name='y', usage='comma separated list of y-axis fields', default='y')
23
23
  usage.def_param(name='pause', usage='Seconds to show graph', is_num=True, default='-1')
24
24
  return usage
25
25