streamwright-spark 0.1.0__py3-none-any.whl

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 (67) hide show
  1. streamwright/spark/__init__.py +1 -0
  2. streamwright/spark/cli/__init__.py +14 -0
  3. streamwright/spark/cli/__main__.py +7 -0
  4. streamwright/spark/cli/aws_secrets.py +58 -0
  5. streamwright/spark/cli/common.py +73 -0
  6. streamwright/spark/cli/inputs.py +40 -0
  7. streamwright/spark/cli/lock.py +65 -0
  8. streamwright/spark/cli/main.py +94 -0
  9. streamwright/spark/cli/run.py +339 -0
  10. streamwright/spark/cli/state.py +133 -0
  11. streamwright/spark/cli/submit_format.py +43 -0
  12. streamwright/spark/cli/validate.py +60 -0
  13. streamwright/spark/contracts/__init__.py +36 -0
  14. streamwright/spark/contracts/adapter.py +7 -0
  15. streamwright/spark/contracts/events.py +5 -0
  16. streamwright/spark/contracts/launcher.py +71 -0
  17. streamwright/spark/contracts/state.py +5 -0
  18. streamwright/spark/contracts/types.py +29 -0
  19. streamwright/spark/docs.py +19 -0
  20. streamwright/spark/engine/__init__.py +1 -0
  21. streamwright/spark/engine/executor.py +276 -0
  22. streamwright/spark/engine/runtime.py +154 -0
  23. streamwright/spark/engine/session.py +95 -0
  24. streamwright/spark/entrypoint.py +33 -0
  25. streamwright/spark/guide.md +105 -0
  26. streamwright/spark/io/__init__.py +1 -0
  27. streamwright/spark/io/capabilities.py +13 -0
  28. streamwright/spark/io/database_rules.py +79 -0
  29. streamwright/spark/io/delta.py +314 -0
  30. streamwright/spark/io/file_rules.py +201 -0
  31. streamwright/spark/io/files.py +222 -0
  32. streamwright/spark/io/jdbc.py +320 -0
  33. streamwright/spark/io/messaging.py +40 -0
  34. streamwright/spark/io/sql.py +56 -0
  35. streamwright/spark/job/__init__.py +20 -0
  36. streamwright/spark/job/check.py +20 -0
  37. streamwright/spark/job/reader.py +10 -0
  38. streamwright/spark/job/rules.py +325 -0
  39. streamwright/spark/job/runtime_rules.py +99 -0
  40. streamwright/spark/job/validate.py +80 -0
  41. streamwright/spark/launchers/__init__.py +1 -0
  42. streamwright/spark/launchers/common.py +83 -0
  43. streamwright/spark/launchers/emr_ec2.py +200 -0
  44. streamwright/spark/launchers/local.py +22 -0
  45. streamwright/spark/launchers/s3_jobs.py +25 -0
  46. streamwright/spark/launchers/standalone.py +58 -0
  47. streamwright/spark/plan/__init__.py +5 -0
  48. streamwright/spark/plan/analysis.py +166 -0
  49. streamwright/spark/plugin.py +37 -0
  50. streamwright/spark/runtime/__init__.py +1 -0
  51. streamwright/spark/runtime/definition.py +104 -0
  52. streamwright/spark/runtime/errors.py +11 -0
  53. streamwright/spark/runtime/lock.py +126 -0
  54. streamwright/spark/runtime/registry.py +101 -0
  55. streamwright/spark/schema.py +20 -0
  56. streamwright/spark/sdk/__init__.py +55 -0
  57. streamwright/spark/sdk/py.typed +0 -0
  58. streamwright/spark/storage/__init__.py +1 -0
  59. streamwright/spark/storage/exports.py +155 -0
  60. streamwright/spark/storage/ingest.py +175 -0
  61. streamwright/spark/storage/paths.py +60 -0
  62. streamwright_spark-0.1.0.dist-info/METADATA +107 -0
  63. streamwright_spark-0.1.0.dist-info/RECORD +67 -0
  64. streamwright_spark-0.1.0.dist-info/WHEEL +5 -0
  65. streamwright_spark-0.1.0.dist-info/entry_points.txt +14 -0
  66. streamwright_spark-0.1.0.dist-info/licenses/LICENSE +201 -0
  67. streamwright_spark-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1 @@
1
+ """SparkJob engine for StreamWright."""
@@ -0,0 +1,14 @@
1
+ """The `sw spark` command line."""
2
+
3
+ import argparse
4
+
5
+ from streamwright.spark.cli.main import main
6
+
7
+ __all__ = ["main", "register"]
8
+
9
+
10
+ def register(subparsers):
11
+ parser = subparsers.add_parser("spark", help="validate, run and inspect SparkJob pipelines", add_help=False)
12
+ parser.add_argument("-h", "--help", action="store_true")
13
+ parser.add_argument("arguments", nargs=argparse.REMAINDER)
14
+ parser.set_defaults(command_handler=lambda args: main(["--help"] if args.help else args.arguments))
@@ -0,0 +1,7 @@
1
+ """python -m streamwright.spark.cli"""
2
+
3
+ import sys
4
+
5
+ from streamwright.spark.cli.main import main
6
+
7
+ sys.exit(main())
@@ -0,0 +1,58 @@
1
+ """AWS Secrets Manager access, imported only by the driver when configured."""
2
+
3
+ import json
4
+ import re
5
+
6
+ import boto3
7
+
8
+
9
+ def read_secrets(source, job):
10
+ region = ((job.get("runtime") or {}).get("platform") or {}).get("region")
11
+ client = boto3.client("secretsmanager", region_name=region)
12
+ data = {}
13
+
14
+ def fetch_value(secret_id):
15
+ reply = client.get_secret_value(SecretId=secret_id)
16
+ raw = reply.get("SecretString")
17
+ if raw is None and "SecretBinary" in reply:
18
+ raw = reply["SecretBinary"].decode("utf-8")
19
+ return raw or ""
20
+
21
+ mapping = source.get("mapping")
22
+ if isinstance(mapping, dict):
23
+ for variable, secret_id in mapping.items():
24
+ raw = fetch_value(secret_id)
25
+ try:
26
+ parsed = json.loads(raw)
27
+ if isinstance(parsed, dict):
28
+ if variable in parsed:
29
+ data[variable] = parsed[variable]
30
+ elif len(parsed) == 1:
31
+ data[variable] = next(iter(parsed.values()))
32
+ else:
33
+ data.update(parsed)
34
+ else:
35
+ data[variable] = raw
36
+ except (ValueError, TypeError):
37
+ data[variable] = raw
38
+
39
+ secret_ids = source.get("ids") or source.get("id")
40
+ if isinstance(secret_ids, str):
41
+ secret_ids = [secret_ids]
42
+ elif not isinstance(secret_ids, (list, tuple)):
43
+ secret_ids = []
44
+ for secret_id in secret_ids:
45
+ raw = fetch_value(secret_id)
46
+ try:
47
+ parsed = json.loads(raw)
48
+ if isinstance(parsed, dict):
49
+ data.update(parsed)
50
+ else:
51
+ leaf = secret_id.split("/")[-1].split(":")[-1]
52
+ leaf = re.sub(r"-[A-Za-z0-9]{6}$", "", leaf)
53
+ data[leaf] = raw
54
+ except (ValueError, TypeError):
55
+ leaf = secret_id.split("/")[-1].split(":")[-1]
56
+ leaf = re.sub(r"-[A-Za-z0-9]{6}$", "", leaf)
57
+ data[leaf] = raw
58
+ return data
@@ -0,0 +1,73 @@
1
+ """Pieces shared by the sw spark commands."""
2
+
3
+ import sys
4
+
5
+ from streamwright.core.jobs.inputs import InputError as InputsError
6
+ from streamwright.core.jobs.inputs import parse_set
7
+ from streamwright.core.sdk.contract import resolve_connection
8
+ from streamwright.spark.contracts import Connection
9
+ from streamwright.spark.runtime import registry
10
+
11
+ __all__ = ["InputsError", "add_job_arguments", "connections_of", "err", "load_plugins", "parse_set"]
12
+
13
+
14
+ def err(text):
15
+ sys.stderr.write(text + "\n")
16
+
17
+
18
+ def add_job_arguments(parser, many=False):
19
+ if many:
20
+ parser.add_argument("job", nargs="*", metavar="PATH", help="path to the job definition YAML file(s)")
21
+ parser.add_argument(
22
+ "--job",
23
+ "-j",
24
+ action="append",
25
+ default=[],
26
+ dest="flag_jobs",
27
+ help="path to the job definition YAML file (can be specified multiple times)",
28
+ )
29
+ parser.add_argument("--file", "-f", action="append", default=[], dest="flag_files", help="alias for --job")
30
+ parser.add_argument("--pipeline", action="append", default=[], dest="flag_pipelines", help="alias for --job")
31
+ else:
32
+ parser.add_argument("job", nargs="?", metavar="JOB", help="path to the job definition YAML file or directory")
33
+ parser.add_argument("--job", "-j", dest="flag_job", help="path to the job definition YAML file or directory")
34
+ parser.add_argument("--file", "-f", dest="flag_file", help="alias for --job")
35
+ parser.add_argument("--pipeline", dest="flag_pipeline", help="alias for --job")
36
+ parser.add_argument("--config", help="a YAML file with a `config:` mapping (values for spec.config)")
37
+ parser.add_argument(
38
+ "--set",
39
+ action="append",
40
+ default=[],
41
+ metavar="NAME=VALUE",
42
+ help="override a spec.config value (e.g. --set key=value)",
43
+ )
44
+ parser.add_argument("--overlay", action="append", default=[], help="YAML merged over the job; a null deletes a key")
45
+ parser.add_argument(
46
+ "--allow-connector", action="append", default=None, metavar="PROVIDER", help="only load these providers"
47
+ )
48
+
49
+
50
+ def load_plugins(allow=None):
51
+ """({provider: adapter}, {platform: launcher}, problems)."""
52
+ adapters, problems = registry.load_all("adapter", allow)
53
+ launchers, more = registry.load_all("launcher")
54
+ return adapters, launchers, problems + more
55
+
56
+
57
+ def connections_of(job, *, adapters=None, declared=None, config=None, secrets=None):
58
+ """
59
+ {name: Connection} of the connections the Spark engine runs natively (a Spark adapter): `provider` apart, the rest
60
+ is the auth block. A provider without a Spark adapter is reached only through call steps, which resolve its
61
+ connection in the source engine.
62
+ """
63
+ connections = {}
64
+ for name, connection in (job.get("connections") or {}).items():
65
+ provider = connection["provider"]
66
+ adapter = (adapters or {}).get(provider)
67
+ if adapter is None:
68
+ continue
69
+ original = (declared or job)["connections"][name]
70
+ auth = {key: value for key, value in original.items() if key != "provider"}
71
+ values = resolve_connection(adapter.definition, auth, config=config or {}, secrets=secrets or {})
72
+ connections[name] = Connection(name, provider, values)
73
+ return connections
@@ -0,0 +1,40 @@
1
+ """Config and secret values for a run, through core input handling."""
2
+
3
+ from importlib import import_module
4
+
5
+ from streamwright.core.config import inputs as core_inputs
6
+ from streamwright.core.jobs import inputs as job_inputs
7
+ from streamwright.spark.cli.common import err, parse_set
8
+
9
+ __all__ = ["InputError", "config_values", "secret_values"]
10
+
11
+ InputError = core_inputs.InputError
12
+
13
+
14
+ def config_values(job, args):
15
+ provided = {}
16
+ if args.config:
17
+ provided.update(job_inputs.read_config_values(args.config))
18
+ provided.update(parse_set(args.set))
19
+ return job_inputs.effective_config(job, provided)
20
+
21
+
22
+ def secret_values(job, args):
23
+ """STREAMWRIGHT_SECRET_<NAME> variables, --secrets FILE, or runtime.secrets (AWS Secrets Manager); never printed."""
24
+ declared = ((job.get("spec") or {}).get("secrets")) or {}
25
+ provided = {}
26
+ if getattr(args, "secrets", None):
27
+ provided.update(core_inputs.read_values_file(args.secrets, err))
28
+ for name, value in core_inputs.secrets_from_env(list(declared)).items():
29
+ provided.setdefault(name, value)
30
+ source = (job.get("runtime") or {}).get("secrets") or {}
31
+ if source.get("source") == "aws_secretsmanager" and len(set(provided) & set(declared)) < len(declared):
32
+ try:
33
+ aws_secrets = import_module("streamwright.spark.cli.aws_secrets")
34
+ except ImportError as exc:
35
+ if exc.name != "boto3":
36
+ raise
37
+ raise InputError('boto3 is not installed; pip install "streamwright-spark[emr]"') from exc
38
+ for name, value in aws_secrets.read_secrets(source, job).items():
39
+ provided.setdefault(name, value)
40
+ return job_inputs.secret_values(job, provided)
@@ -0,0 +1,65 @@
1
+ """sw spark lock: write or check the version lock."""
2
+
3
+ import os
4
+
5
+ from streamwright.spark.cli.common import err, load_plugins
6
+ from streamwright.spark.job import check_job, load_job
7
+ from streamwright.spark.launchers.common import sha256_file
8
+ from streamwright.spark.runtime import lock as lockmod
9
+
10
+ __all__ = ["add_arguments", "run"]
11
+
12
+
13
+ def add_arguments(parser):
14
+ parser.add_argument("--spark", required=True, help="the Spark version the job runs on, e.g. 3.5.3")
15
+ parser.add_argument(
16
+ "--out", help="output path for the generated lock file (defaults to runtime.artifacts.lock or <name>.lock.json)"
17
+ )
18
+ parser.add_argument("--emr-release", help="EMR release label (e.g. emr-7.2.0) to record in the lock file")
19
+ parser.add_argument(
20
+ "--env-archive", help="path to packaged virtual environment archive (.tar.gz) for remote executors"
21
+ )
22
+ parser.add_argument(
23
+ "--jar",
24
+ action="append",
25
+ default=[],
26
+ metavar="GROUP:ARTIFACT=VERSION",
27
+ help="JAR dependency override in GROUP:ARTIFACT=VERSION format",
28
+ )
29
+ parser.add_argument("--check", action="store_true", help="verify an existing lock instead of writing one")
30
+
31
+
32
+ def run(args):
33
+ adapters, launchers, _ = load_plugins(args.allow_connector)
34
+ job = load_job(args.job, args.overlay)
35
+ problems, _ = check_job(job, adapters, launchers)
36
+ if problems:
37
+ for text in problems:
38
+ err("error: " + text)
39
+ return 2
40
+ used = sorted({c["provider"] for c in job["connections"].values()})
41
+ jars = dict(item.split("=", 1) for item in args.jar if "=" in item)
42
+ try:
43
+ python_env = None
44
+ if args.env_archive:
45
+ python_env = {"archive": os.path.basename(args.env_archive), "sha256": sha256_file(args.env_archive)}
46
+ lock = lockmod.build_lock(
47
+ job["name"],
48
+ [adapters[p] for p in used],
49
+ args.spark,
50
+ extra_jars=jars,
51
+ emr_release=args.emr_release,
52
+ python_env=python_env,
53
+ )
54
+ except lockmod.LockError as exc:
55
+ err("error: %s" % exc)
56
+ return 2
57
+ out = args.out or ((job.get("runtime") or {}).get("artifacts") or {}).get("lock") or "%s.lock.json" % job["name"]
58
+ if args.check:
59
+ problems = lockmod.check_lock(lockmod.load(out), args.spark)
60
+ for text in problems:
61
+ err("lock: " + text)
62
+ return 2 if problems else 0
63
+ lockmod.dump(lock, out)
64
+ print("wrote %s (fingerprint %s)" % (out, lockmod.fingerprint(lock)[:12]))
65
+ return 0
@@ -0,0 +1,94 @@
1
+ """
2
+ `sw spark ...`. Entry points `streamwright-spark` / `sw-spark`; attaching it to `streamwright` core is a lazy
3
+ dispatch like `streamwright mcp` (see README). Exit codes: 0 ok, 1 failed, 2 usage/validation, 130 interrupted.
4
+
5
+ sw spark validate PATH... [--config F] [--set K=V] [--overlay F] [--analyze] [--graph] [--strict] [--lock F]
6
+ sw spark run JOB [--config F] [--set K=V] [--secrets F] [--overlay F] [--once|--drain|--follow [--interval 10s]]
7
+ [--max-messages N] [--run-key K] [--conf K=V] [--lock F] [--lock-required] [--dry-run] [--detach]
8
+ sw spark lock JOB --spark V [--out F] [--emr-release L] [--env-archive F] [--jar G:A=V] [--check]
9
+ sw spark state init|status|batches|show|retry|abandon|purge|lease JOB [ARG]
10
+ """
11
+
12
+ import argparse
13
+ import sys
14
+
15
+ from streamwright.spark.cli import lock, run, state, validate
16
+ from streamwright.spark.cli.common import InputsError, add_job_arguments, err
17
+ from streamwright.spark.cli.inputs import InputError
18
+ from streamwright.spark.job import JobError
19
+ from streamwright.spark.runtime.lock import LockError
20
+
21
+ __all__ = ["main"]
22
+
23
+ _COMMANDS = (
24
+ ("validate", validate, "check jobs without Spark, brokers or credentials", True),
25
+ ("plan", validate, "show Spark actions and step dependencies without running the job", True),
26
+ ("run", run, "run a job", False),
27
+ ("lock", lock, "write or check the version lock", False),
28
+ ("state", state, "inspect and repair bookkeeping", None),
29
+ )
30
+
31
+
32
+ def _parser():
33
+ parser = argparse.ArgumentParser(prog="sw spark", description="Spark jobs for StreamWright.")
34
+ sub = parser.add_subparsers(dest="command")
35
+ for name, module, help_text, many in _COMMANDS:
36
+ command = sub.add_parser(name, help=help_text)
37
+ if many is not None:
38
+ add_job_arguments(command, many)
39
+ module.add_arguments(command)
40
+ command.set_defaults(handler=_run_plan if name == "plan" else module.run)
41
+ return parser
42
+
43
+
44
+ def _run_plan(args):
45
+ args.analyze = True
46
+ args.graph = True
47
+ return validate.run(args)
48
+
49
+
50
+ def _normalize_job_args(args):
51
+ if hasattr(args, "flag_job"):
52
+ flag_val = args.flag_job or getattr(args, "flag_file", None) or getattr(args, "flag_pipeline", None)
53
+ if flag_val:
54
+ if hasattr(args, "arg") and args.job and args.arg is None:
55
+ args.arg = args.job
56
+ args.job = flag_val
57
+ elif not args.job:
58
+ err("error: job is required (pass JOB positional argument or --job PATH)")
59
+ return False
60
+
61
+ if hasattr(args, "flag_jobs"):
62
+ jobs = (
63
+ list(args.job or [])
64
+ + list(args.flag_jobs or [])
65
+ + list(getattr(args, "flag_files", None) or [])
66
+ + list(getattr(args, "flag_pipelines", None) or [])
67
+ )
68
+ if not jobs:
69
+ err("error: at least one job path is required (pass PATH positional argument or --job PATH)")
70
+ return False
71
+ args.job = jobs
72
+
73
+ return True
74
+
75
+
76
+ def main(argv=None):
77
+ parser = _parser()
78
+ args = parser.parse_args(argv)
79
+ if not getattr(args, "handler", None):
80
+ parser.print_help()
81
+ return 2
82
+ if not _normalize_job_args(args):
83
+ return 2
84
+ try:
85
+ return args.handler(args)
86
+ except (JobError, InputError, InputsError, LockError) as exc:
87
+ err("error: %s" % exc)
88
+ return 2
89
+ except KeyboardInterrupt:
90
+ return 130
91
+
92
+
93
+ if __name__ == "__main__":
94
+ sys.exit(main())