streamwright 0.1.3__tar.gz → 0.1.4__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 (54) hide show
  1. {streamwright-0.1.3/src/streamwright.egg-info → streamwright-0.1.4}/PKG-INFO +2 -1
  2. {streamwright-0.1.3 → streamwright-0.1.4}/README.md +1 -0
  3. {streamwright-0.1.3 → streamwright-0.1.4}/pyproject.toml +1 -1
  4. {streamwright-0.1.3 → streamwright-0.1.4}/src/streamwright/core/catalog.py +50 -27
  5. {streamwright-0.1.3 → streamwright-0.1.4}/src/streamwright/core/cli.py +10 -7
  6. {streamwright-0.1.3 → streamwright-0.1.4/src/streamwright.egg-info}/PKG-INFO +2 -1
  7. {streamwright-0.1.3 → streamwright-0.1.4}/src/streamwright.egg-info/SOURCES.txt +1 -0
  8. streamwright-0.1.4/tests/test_catalog.py +127 -0
  9. {streamwright-0.1.3 → streamwright-0.1.4}/LICENSE +0 -0
  10. {streamwright-0.1.3 → streamwright-0.1.4}/setup.cfg +0 -0
  11. {streamwright-0.1.3 → streamwright-0.1.4}/setup.py +0 -0
  12. {streamwright-0.1.3 → streamwright-0.1.4}/src/streamwright/core/__init__.py +0 -0
  13. {streamwright-0.1.3 → streamwright-0.1.4}/src/streamwright/core/config/__init__.py +0 -0
  14. {streamwright-0.1.3 → streamwright-0.1.4}/src/streamwright/core/config/inputs.py +0 -0
  15. {streamwright-0.1.3 → streamwright-0.1.4}/src/streamwright/core/config/loader.py +0 -0
  16. {streamwright-0.1.3 → streamwright-0.1.4}/src/streamwright/core/config/reader.py +0 -0
  17. {streamwright-0.1.3 → streamwright-0.1.4}/src/streamwright/core/connectors.json +0 -0
  18. {streamwright-0.1.3 → streamwright-0.1.4}/src/streamwright/core/engine/__init__.py +0 -0
  19. {streamwright-0.1.3 → streamwright-0.1.4}/src/streamwright/core/engine/queries.py +0 -0
  20. {streamwright-0.1.3 → streamwright-0.1.4}/src/streamwright/core/engine/runner.py +0 -0
  21. {streamwright-0.1.3 → streamwright-0.1.4}/src/streamwright/core/engine/sql.py +0 -0
  22. {streamwright-0.1.3 → streamwright-0.1.4}/src/streamwright/core/net/__init__.py +0 -0
  23. {streamwright-0.1.3 → streamwright-0.1.4}/src/streamwright/core/net/downloads.py +0 -0
  24. {streamwright-0.1.3 → streamwright-0.1.4}/src/streamwright/core/net/http.py +0 -0
  25. {streamwright-0.1.3 → streamwright-0.1.4}/src/streamwright/core/outputs/__init__.py +0 -0
  26. {streamwright-0.1.3 → streamwright-0.1.4}/src/streamwright/core/outputs/dlt_output.py +0 -0
  27. {streamwright-0.1.3 → streamwright-0.1.4}/src/streamwright/core/outputs/exporter.py +0 -0
  28. {streamwright-0.1.3 → streamwright-0.1.4}/src/streamwright/core/outputs/output.py +0 -0
  29. {streamwright-0.1.3 → streamwright-0.1.4}/src/streamwright/core/outputs/staging.py +0 -0
  30. {streamwright-0.1.3 → streamwright-0.1.4}/src/streamwright/core/outputs/warehouse.py +0 -0
  31. {streamwright-0.1.3 → streamwright-0.1.4}/src/streamwright/core/runtime/__init__.py +0 -0
  32. {streamwright-0.1.3 → streamwright-0.1.4}/src/streamwright/core/runtime/components.py +0 -0
  33. {streamwright-0.1.3 → streamwright-0.1.4}/src/streamwright/core/runtime/logs.py +0 -0
  34. {streamwright-0.1.3 → streamwright-0.1.4}/src/streamwright/core/runtime/templates.py +0 -0
  35. {streamwright-0.1.3 → streamwright-0.1.4}/src/streamwright/core/runtime/testing.py +0 -0
  36. {streamwright-0.1.3 → streamwright-0.1.4}/src/streamwright/core/validation/__init__.py +0 -0
  37. {streamwright-0.1.3 → streamwright-0.1.4}/src/streamwright/core/validation/engine.py +0 -0
  38. {streamwright-0.1.3 → streamwright-0.1.4}/src/streamwright/core/validation/schema.py +0 -0
  39. {streamwright-0.1.3 → streamwright-0.1.4}/src/streamwright/core/validation/source.py +0 -0
  40. {streamwright-0.1.3 → streamwright-0.1.4}/src/streamwright.egg-info/dependency_links.txt +0 -0
  41. {streamwright-0.1.3 → streamwright-0.1.4}/src/streamwright.egg-info/entry_points.txt +0 -0
  42. {streamwright-0.1.3 → streamwright-0.1.4}/src/streamwright.egg-info/requires.txt +0 -0
  43. {streamwright-0.1.3 → streamwright-0.1.4}/src/streamwright.egg-info/top_level.txt +0 -0
  44. {streamwright-0.1.3 → streamwright-0.1.4}/tests/test_connectors.py +0 -0
  45. {streamwright-0.1.3 → streamwright-0.1.4}/tests/test_dlt_output.py +0 -0
  46. {streamwright-0.1.3 → streamwright-0.1.4}/tests/test_exact_decimals.py +0 -0
  47. {streamwright-0.1.3 → streamwright-0.1.4}/tests/test_exporter.py +0 -0
  48. {streamwright-0.1.3 → streamwright-0.1.4}/tests/test_logging.py +0 -0
  49. {streamwright-0.1.3 → streamwright-0.1.4}/tests/test_output_formats.py +0 -0
  50. {streamwright-0.1.3 → streamwright-0.1.4}/tests/test_runner.py +0 -0
  51. {streamwright-0.1.3 → streamwright-0.1.4}/tests/test_sql.py +0 -0
  52. {streamwright-0.1.3 → streamwright-0.1.4}/tests/test_transform.py +0 -0
  53. {streamwright-0.1.3 → streamwright-0.1.4}/tests/test_validation.py +0 -0
  54. {streamwright-0.1.3 → streamwright-0.1.4}/tests/test_warehouse_output.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: streamwright
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: Declarative data connectors — pull from APIs, files and databases with YAML, shape with SQL, write anywhere.
5
5
  Author: Karthick Jaganathan
6
6
  License-Expression: Apache-2.0
@@ -54,6 +54,7 @@ connectors yet. Add the ones you need on demand:
54
54
  streamwright connectors list # browse the hub
55
55
  streamwright connectors install files # a reader (files, s3, gcs, postgres)
56
56
  streamwright connectors install google_ads # an API connector (pulls in its SDK)
57
+ streamwright connectors install google_ads meta_ads microsoft_ads # several at once (one pip install)
57
58
  ```
58
59
 
59
60
  ## Quickstart
@@ -26,6 +26,7 @@ connectors yet. Add the ones you need on demand:
26
26
  streamwright connectors list # browse the hub
27
27
  streamwright connectors install files # a reader (files, s3, gcs, postgres)
28
28
  streamwright connectors install google_ads # an API connector (pulls in its SDK)
29
+ streamwright connectors install google_ads meta_ads microsoft_ads # several at once (one pip install)
29
30
  ```
30
31
 
31
32
  ## Quickstart
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "streamwright"
7
- version = "0.1.3"
7
+ version = "0.1.4"
8
8
  description = "Declarative data connectors — pull from APIs, files and databases with YAML, shape with SQL, write anywhere."
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -74,12 +74,13 @@ def installed():
74
74
 
75
75
 
76
76
  def search(text, hub_url=None):
77
- text = (text or "").lower()
77
+ """The catalog connectors whose key, title, summary or family contains every word of `text`."""
78
+ words = (text or "").lower().split()
78
79
  hits = {}
79
80
  for key, entry in connectors(hub_url).items():
80
81
  haystack = " ".join([key, entry.get("title", ""), entry.get("summary", ""),
81
82
  entry.get("family", "")]).lower()
82
- if text in haystack:
83
+ if all(word in haystack for word in words):
83
84
  hits[key] = entry
84
85
  return hits
85
86
 
@@ -111,40 +112,62 @@ def listing(hub_url=None):
111
112
 
112
113
 
113
114
  def install(key, assume_yes=False, hub_url=None, pip_args=()):
114
- """Resolve `key` in the catalog and install it (pip for pip/git; images are run, not installed)."""
115
- entry = connectors(hub_url).get(key)
116
- if not entry:
117
- sys.stderr.write("streamwright: unknown connector %r (try: streamwright connectors list)\n" % key)
115
+ """Resolve one connector `key` in the catalog and install it (see install_many)."""
116
+ return install_many([key], assume_yes=assume_yes, hub_url=hub_url, pip_args=pip_args)
117
+
118
+
119
+ def install_many(keys, assume_yes=False, hub_url=None, pip_args=()):
120
+ """
121
+ Resolve connector `keys` in the catalog and install them with ONE pip command, so pip resolves their
122
+ dependencies together. Every key is checked first: an unknown key, or a connector without an installable
123
+ source, installs nothing. Image-distributed connectors are reported, not installed. Non-"official"
124
+ connectors need one confirmation for all of them (or `assume_yes`), and are refused non-interactively.
125
+ """
126
+ keys = list(dict.fromkeys(keys)) # in order, without repeats
127
+ if not keys:
128
+ sys.stderr.write("streamwright: 'connectors install' needs at least one connector KEY\n")
129
+ return 2
130
+ catalog = connectors(hub_url)
131
+ unknown = [key for key in keys if key not in catalog]
132
+ if unknown:
133
+ sys.stderr.write("streamwright: unknown connector%s %s (try: streamwright connectors list)\n" % (
134
+ "s" if len(unknown) > 1 else "", ", ".join(repr(key) for key in unknown)))
118
135
  return 2
119
- inst = entry.get("install", {})
120
- trust = entry.get("trust", "community")
121
136
 
122
- if "image" in inst:
123
- print("%r is distributed as an image, not a Python package:" % key)
124
- print(" %s" % inst["image"])
125
- print("Run it with the docker or k8s execution mode (it is not pip-installed).")
137
+ specs, untrusted = [], []
138
+ for key in keys:
139
+ entry = catalog[key]
140
+ inst = entry.get("install", {})
141
+ if "image" in inst:
142
+ print("%r is distributed as an image, not a Python package:" % key)
143
+ print(" %s" % inst["image"])
144
+ print("Run it with the docker or k8s execution mode (it is not pip-installed).")
145
+ continue
146
+ spec = inst.get("pip") or inst.get("git")
147
+ if not spec:
148
+ sys.stderr.write("streamwright: connector %r has no installable source\n" % key)
149
+ return 2
150
+ specs.append(spec)
151
+ trust = entry.get("trust", "community")
152
+ if trust != "official":
153
+ untrusted.append((key, trust, _source_of(inst)))
154
+ if not specs:
126
155
  return 0
127
156
 
128
- spec = inst.get("pip") or inst.get("git")
129
- if not spec:
130
- sys.stderr.write("streamwright: connector %r has no installable source\n" % key)
131
- return 2
132
-
133
- if trust != "official" and not assume_yes:
134
- source = _source_of(inst)
157
+ if untrusted and not assume_yes:
158
+ names = ", ".join("%s connector %r" % (trust, key) for key, trust, _ in untrusted)
135
159
  if not sys.stdin.isatty():
136
- sys.stderr.write(
137
- "streamwright: refusing to install %s connector %r non-interactively; pass --yes to confirm (%s)\n"
138
- % (trust, key, source))
160
+ sys.stderr.write("streamwright: refusing to install %s non-interactively; pass --yes to confirm (%s)\n"
161
+ % (names, "; ".join(source for _, _, source in untrusted)))
139
162
  return 2
140
- sys.stderr.write(
141
- "\n WARNING: %r is a %s connector. Installing runs third-party code from:\n %s\n"
142
- % (key, trust, source))
163
+ sys.stderr.write("\n WARNING: installing runs third-party code from:\n")
164
+ for key, trust, source in untrusted:
165
+ sys.stderr.write(" %s (%s): %s\n" % (key, trust, source))
143
166
  answer = input(" Continue? [y/N] ").strip().lower()
144
167
  if answer not in ("y", "yes"):
145
168
  print("aborted")
146
169
  return 1
147
170
 
148
- cmd = [sys.executable, "-m", "pip", "install", *pip_args, spec]
149
- print("+ " + " ".join(cmd))
171
+ cmd = [sys.executable, "-m", "pip", "install", *pip_args, *specs]
172
+ print("+ " + " ".join(cmd), flush=True)
150
173
  return subprocess.call(cmd)
@@ -22,6 +22,8 @@ The `streamwright` command.
22
22
  [--file-name TEMPLATE] [--allow-connector NAME] [--summary FILE] [logging options]
23
23
  (SOURCE: a source folder - source.yaml and streams/ - or a source file)
24
24
  streamwright connectors (installed connectors, with the names of their SDK loggers)
25
+ streamwright connectors list | search WORDS | install KEY [KEY ...] [--yes] [--hub-url URL]
26
+ (the catalog; install resolves every KEY, then runs one pip install)
25
27
  streamwright validate PATH... (streamwright-validate, plus the checks of the installed connectors and query builders)
26
28
 
27
29
  Logging options (streamwright run and streamwright validate): --log-level LEVEL, --log NAME=LEVEL, --log-format text|json,
@@ -240,9 +242,9 @@ def _parser():
240
242
  help="list, search or install connectors (bare: the installed connectors and their "
241
243
  "SDK loggers)")
242
244
  conn.add_argument("action", nargs="?", choices=["list", "search", "install"],
243
- help="list (catalog + installed), search TEXT, or install KEY")
244
- conn.add_argument("query", nargs="?", metavar="KEY|TEXT",
245
- help="a connector key (install) or text (search)")
245
+ help="list (catalog + installed), search TEXT, or install KEY [KEY ...]")
246
+ conn.add_argument("query", nargs="*", metavar="KEY|TEXT",
247
+ help="connector keys (install: one or more) or words (search)")
246
248
  conn.add_argument("--yes", action="store_true", help="skip the confirmation prompt when installing")
247
249
  conn.add_argument("--hub-url", metavar="URL",
248
250
  help="catalog URL to use (default: the bundled catalog, or $STREAMWRIGHT_HUB_URL)")
@@ -544,18 +546,19 @@ def _connectors_command(args):
544
546
  print("\n".join(lines) if lines else "the catalog is empty")
545
547
  return 0
546
548
  if action == "search":
547
- hits = catalog.search(args.query or "", args.hub_url)
549
+ text = " ".join(args.query)
550
+ hits = catalog.search(text, args.hub_url)
548
551
  if not hits:
549
- print("no connectors match %r" % (args.query or ""))
552
+ print("no connectors match %r" % text)
550
553
  return 0
551
554
  for key, entry in sorted(hits.items()):
552
555
  print(" %-16s %-10s %s" % (key, entry.get("trust", "community"), entry.get("summary", "")))
553
556
  return 0
554
557
  if action == "install":
555
558
  if not args.query:
556
- sys.stderr.write("streamwright: 'connectors install' needs a connector KEY\n")
559
+ sys.stderr.write("streamwright: 'connectors install' needs at least one connector KEY\n")
557
560
  return 2
558
- return catalog.install(args.query, assume_yes=args.yes, hub_url=args.hub_url)
561
+ return catalog.install_many(args.query, assume_yes=args.yes, hub_url=args.hub_url)
559
562
  return 2
560
563
 
561
564
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: streamwright
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: Declarative data connectors — pull from APIs, files and databases with YAML, shape with SQL, write anywhere.
5
5
  Author: Karthick Jaganathan
6
6
  License-Expression: Apache-2.0
@@ -54,6 +54,7 @@ connectors yet. Add the ones you need on demand:
54
54
  streamwright connectors list # browse the hub
55
55
  streamwright connectors install files # a reader (files, s3, gcs, postgres)
56
56
  streamwright connectors install google_ads # an API connector (pulls in its SDK)
57
+ streamwright connectors install google_ads meta_ads microsoft_ads # several at once (one pip install)
57
58
  ```
58
59
 
59
60
  ## Quickstart
@@ -38,6 +38,7 @@ src/streamwright/core/validation/__init__.py
38
38
  src/streamwright/core/validation/engine.py
39
39
  src/streamwright/core/validation/schema.py
40
40
  src/streamwright/core/validation/source.py
41
+ tests/test_catalog.py
41
42
  tests/test_connectors.py
42
43
  tests/test_dlt_output.py
43
44
  tests/test_exact_decimals.py
@@ -0,0 +1,127 @@
1
+ """`streamwright connectors install KEY [KEY ...]` and search: resolution, consent and the single pip command."""
2
+
3
+ import io
4
+ import sys
5
+
6
+ import pytest
7
+
8
+ from streamwright.core import catalog, cli
9
+
10
+ CATALOG = {
11
+ "files": {"title": "Files", "family": "readers", "summary": "Local files", "trust": "official",
12
+ "install": {"git": "git+https://example.test/sw.git@abc#subdirectory=connectors/readers/files"}},
13
+ "google_ads": {"title": "Google Ads", "family": "ads", "summary": "Google Ads reports", "trust": "official",
14
+ "install": {"git": "git+https://example.test/sw.git@abc#subdirectory=connectors/ads/google_ads"}},
15
+ "meta_ads": {"title": "Meta Ads", "family": "ads", "summary": "Meta Marketing insights", "trust": "official",
16
+ "install": {"pip": "streamwright-meta-ads>=0.1,<0.2"}},
17
+ "acme": {"title": "Acme", "family": "ads", "summary": "Acme reports", "trust": "community",
18
+ "install": {"git": "git+https://example.test/acme.git@def"}},
19
+ "imaged": {"title": "Imaged", "family": "readers", "summary": "An image", "trust": "official",
20
+ "install": {"image": "ghcr.io/org/imaged@sha256:" + "0" * 64}},
21
+ "empty": {"title": "Empty", "family": "readers", "summary": "No source", "trust": "official", "install": {}},
22
+ }
23
+
24
+
25
+ class TTY(io.StringIO):
26
+ def __init__(self, tty):
27
+ super().__init__()
28
+ self.tty = tty
29
+
30
+ def isatty(self):
31
+ return self.tty
32
+
33
+
34
+ @pytest.fixture
35
+ def pip(monkeypatch):
36
+ """The catalog above; records each pip command instead of running it."""
37
+ calls = []
38
+ monkeypatch.setattr(catalog, "connectors", lambda hub_url=None: CATALOG)
39
+ monkeypatch.setattr(catalog.subprocess, "call", lambda cmd: calls.append(cmd) or 0)
40
+ monkeypatch.setattr(sys, "stdin", TTY(False))
41
+ return calls
42
+
43
+
44
+ def specs(cmd):
45
+ return cmd[cmd.index("install") + 1:]
46
+
47
+
48
+ def test_one_key_installs_its_source(pip):
49
+ assert catalog.install("files") == 0
50
+ assert [specs(cmd) for cmd in pip] == [[CATALOG["files"]["install"]["git"]]]
51
+ assert pip[0][:4] == [sys.executable, "-m", "pip", "install"]
52
+
53
+
54
+ def test_several_keys_install_with_one_pip_command_in_order_without_repeats(pip):
55
+ assert catalog.install_many(["google_ads", "meta_ads", "files", "google_ads"]) == 0
56
+ assert len(pip) == 1
57
+ assert specs(pip[0]) == [CATALOG["google_ads"]["install"]["git"], "streamwright-meta-ads>=0.1,<0.2",
58
+ CATALOG["files"]["install"]["git"]]
59
+
60
+
61
+ def test_an_unknown_key_installs_nothing(pip, capsys):
62
+ assert catalog.install_many(["google_ads", "nope", "nada"]) == 2
63
+ assert pip == []
64
+ assert "unknown connectors 'nope', 'nada'" in capsys.readouterr().err
65
+
66
+
67
+ def test_a_connector_without_a_source_installs_nothing(pip, capsys):
68
+ assert catalog.install_many(["files", "empty"]) == 2
69
+ assert pip == []
70
+ assert "'empty' has no installable source" in capsys.readouterr().err
71
+
72
+
73
+ def test_no_keys_is_an_error(pip, capsys):
74
+ assert catalog.install_many([]) == 2
75
+ assert pip == []
76
+ assert "at least one connector KEY" in capsys.readouterr().err
77
+
78
+
79
+ def test_a_community_connector_is_refused_non_interactively_without_yes(pip, capsys):
80
+ assert catalog.install_many(["files", "acme"]) == 2
81
+ assert pip == []
82
+ assert "refusing to install community connector 'acme' non-interactively" in capsys.readouterr().err
83
+ assert catalog.install_many(["files", "acme"], assume_yes=True) == 0
84
+ assert specs(pip[0]) == [CATALOG["files"]["install"]["git"], CATALOG["acme"]["install"]["git"]]
85
+
86
+
87
+ def test_one_confirmation_covers_every_untrusted_connector(pip, monkeypatch, capsys):
88
+ monkeypatch.setattr(sys, "stdin", TTY(True))
89
+ answers = []
90
+ monkeypatch.setattr("builtins.input", lambda prompt: answers.append(prompt) or "n")
91
+ assert catalog.install_many(["acme", "files"]) == 1
92
+ assert pip == [] and len(answers) == 1
93
+ monkeypatch.setattr("builtins.input", lambda prompt: "y")
94
+ assert catalog.install_many(["acme", "files"]) == 0
95
+ assert len(pip) == 1
96
+ assert "acme (community): git: git+https://example.test/acme.git@def" in capsys.readouterr().err
97
+
98
+
99
+ def test_images_are_reported_not_installed(pip, capsys):
100
+ assert catalog.install_many(["imaged"]) == 0
101
+ assert pip == []
102
+ assert catalog.install_many(["imaged", "files"]) == 0
103
+ assert specs(pip[0]) == [CATALOG["files"]["install"]["git"]]
104
+ assert "'imaged' is distributed as an image" in capsys.readouterr().out
105
+
106
+
107
+ def test_search_matches_every_word(pip):
108
+ assert set(catalog.search("ads")) == {"google_ads", "meta_ads", "acme"}
109
+ assert set(catalog.search("google ads")) == {"google_ads"}
110
+ assert set(catalog.search("")) == set(CATALOG)
111
+
112
+
113
+ def test_cli_installs_several_connectors(pip):
114
+ assert cli.main(["connectors", "install", "google_ads", "meta_ads", "--yes"]) == 0
115
+ assert specs(pip[0]) == [CATALOG["google_ads"]["install"]["git"], "streamwright-meta-ads>=0.1,<0.2"]
116
+
117
+
118
+ def test_cli_install_needs_a_key(pip, capsys):
119
+ assert cli.main(["connectors", "install"]) == 2
120
+ assert pip == []
121
+ assert "at least one connector KEY" in capsys.readouterr().err
122
+
123
+
124
+ def test_cli_search_joins_its_words(pip, capsys):
125
+ assert cli.main(["connectors", "search", "meta", "insights"]) == 0
126
+ out = capsys.readouterr().out
127
+ assert "meta_ads" in out and "google_ads" not in out
File without changes
File without changes
File without changes