lambdaflow 0.1.0__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.
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.4
2
+ Name: lambdaflow
3
+ Version: 0.1.0
4
+ Summary: A truly serverless Airflow on your own AWS account: Airflow 3 DAG files on Lambda, Step Functions, EventBridge Scheduler and DynamoDB, ≈ $0 when idle. Python client for the deployment.
5
+ Author: Frank Zhang
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/cloudformer/lambdaflow
8
+ Project-URL: Repository, https://github.com/cloudformer/lambdaflow
9
+ Project-URL: Deploy, https://github.com/cloudformer/lambdaflow#quick-start
10
+ Project-URL: Compatibility, https://github.com/cloudformer/lambdaflow/blob/main/tests/compat/REPORT.md
11
+ Project-URL: Changelog, https://github.com/cloudformer/lambdaflow/blob/main/CHANGELOG.md
12
+ Project-URL: Issues, https://github.com/cloudformer/lambdaflow/issues
13
+ Keywords: serverless,aws,lambda,airflow,mwaa,step-functions
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Requires-Python: >=3.9
19
+ Description-Content-Type: text/markdown
20
+
21
+ # lambdaflow
22
+
23
+ **Python client for [lambdaflow](https://github.com/cloudformer/lambdaflow) — a truly serverless Airflow:
24
+ Airflow 3 DAG files on Lambda, Step Functions, EventBridge Scheduler, DynamoDB and S3, ≈ $0 when idle.**
25
+ Part of [Redefine AWS Serverless](https://github.com/topics/redefine-aws-serverless).
26
+
27
+ ![lambdaflow architecture](https://raw.githubusercontent.com/cloudformer/lambdaflow/main/docs/architecture.svg)
28
+
29
+ ```bash
30
+ pip install lambdaflow
31
+ ```
32
+
33
+ ```python
34
+ from lambdaflow import client
35
+
36
+ lf = client.connect("https://xxxx.lambda-url.us-east-2.on.aws", "admin", "secret") # terraform output console_url
37
+ lf.upload("my_dag.py") # parsed at once; import errors come back
38
+ run_id = lf.trigger("my_dag", conf={"x": 1}, unpause=True)
39
+ print(lf.wait("my_dag", run_id)) # "success" | "failed"
40
+ print(lf.log("my_dag", run_id, "extract"))
41
+ print(lf.cli("dags list-runs my_dag")) # the airflow commands of the console's command box
42
+ ```
43
+
44
+ Standard library only. The deployment comes from the [main repository](https://github.com/cloudformer/lambdaflow)
45
+ (Terraform). Compatibility with Airflow 3.3:
46
+ [REPORT.md](https://github.com/cloudformer/lambdaflow/blob/main/tests/compat/REPORT.md).
@@ -0,0 +1,26 @@
1
+ # lambdaflow
2
+
3
+ **Python client for [lambdaflow](https://github.com/cloudformer/lambdaflow) — a truly serverless Airflow:
4
+ Airflow 3 DAG files on Lambda, Step Functions, EventBridge Scheduler, DynamoDB and S3, ≈ $0 when idle.**
5
+ Part of [Redefine AWS Serverless](https://github.com/topics/redefine-aws-serverless).
6
+
7
+ ![lambdaflow architecture](https://raw.githubusercontent.com/cloudformer/lambdaflow/main/docs/architecture.svg)
8
+
9
+ ```bash
10
+ pip install lambdaflow
11
+ ```
12
+
13
+ ```python
14
+ from lambdaflow import client
15
+
16
+ lf = client.connect("https://xxxx.lambda-url.us-east-2.on.aws", "admin", "secret") # terraform output console_url
17
+ lf.upload("my_dag.py") # parsed at once; import errors come back
18
+ run_id = lf.trigger("my_dag", conf={"x": 1}, unpause=True)
19
+ print(lf.wait("my_dag", run_id)) # "success" | "failed"
20
+ print(lf.log("my_dag", run_id, "extract"))
21
+ print(lf.cli("dags list-runs my_dag")) # the airflow commands of the console's command box
22
+ ```
23
+
24
+ Standard library only. The deployment comes from the [main repository](https://github.com/cloudformer/lambdaflow)
25
+ (Terraform). Compatibility with Airflow 3.3:
26
+ [REPORT.md](https://github.com/cloudformer/lambdaflow/blob/main/tests/compat/REPORT.md).
@@ -0,0 +1,4 @@
1
+ """lambdaflow — a truly serverless Airflow. `from lambdaflow import client` talks to your deployment."""
2
+ from . import client # noqa: F401
3
+
4
+ __version__ = "0.1.0"
@@ -0,0 +1,166 @@
1
+ """lambdaflow.client — your lambdaflow deployment from Python: upload DAG files, trigger runs, wait, read logs.
2
+
3
+ Standard library only. The transport below (keep-alive connection, strict retry policy: re-send only
4
+ when the request provably never reached the server) is shared across the series; the methods on top
5
+ are the console's JSON API, plus cli() for the airflow commands the console's command box understands.
6
+
7
+ from lambdaflow import client
8
+ lf = client.connect("https://xxxx.lambda-url.us-east-2.on.aws", "admin", "secret")
9
+ lf.upload("my_dag.py")
10
+ run_id = lf.trigger("my_dag", conf={"x": 1}, unpause=True)
11
+ print(lf.wait("my_dag", run_id)) # "success" | "failed"
12
+ """
13
+ import base64
14
+ import http.client
15
+ import json
16
+ import os
17
+ import socket
18
+ import time
19
+ import urllib.parse
20
+
21
+ __version__ = "0.1.0"
22
+
23
+
24
+ class Error(Exception):
25
+ def __init__(self, status, message):
26
+ super().__init__(status, message)
27
+ self.status = status
28
+ self.message = message
29
+
30
+
31
+ class NotSupportedError(Error):
32
+ """The feature needs Airflow / MWAA; refused explicitly, never silently different."""
33
+
34
+
35
+ class _NotDelivered(Exception):
36
+ def __init__(self, cause, reused):
37
+ super().__init__(cause)
38
+ self.reused = reused
39
+
40
+
41
+ class Connection:
42
+ def __init__(self, url, user="", password="", timeout=30):
43
+ p = urllib.parse.urlparse(url.rstrip("/"))
44
+ self._host, self._base_path = p.netloc, p.path
45
+ self._auth = base64.b64encode(f"{user}:{password}".encode()).decode()
46
+ self.timeout = timeout
47
+ self._conn = None
48
+ self.last = {} # server-side metrics of the last call
49
+
50
+ # ---- transport (shared) ----
51
+
52
+ def close(self):
53
+ try:
54
+ if self._conn:
55
+ self._conn.close()
56
+ finally:
57
+ self._conn = None
58
+
59
+ def _send(self, method, path, body):
60
+ reused = self._conn is not None
61
+ if self._conn is None:
62
+ self._conn = http.client.HTTPSConnection(self._host, timeout=self.timeout)
63
+ headers = {"content-type": "application/json", "authorization": f"Basic {self._auth}"}
64
+ try:
65
+ self._conn.request(method, self._base_path + path, body, headers)
66
+ except (socket.timeout, TimeoutError):
67
+ raise
68
+ except (OSError, http.client.HTTPException) as e:
69
+ raise _NotDelivered(e, reused) from None
70
+ try:
71
+ resp = self._conn.getresponse()
72
+ except (http.client.RemoteDisconnected, http.client.BadStatusLine) as e:
73
+ raise _NotDelivered(e, reused) from None
74
+ raw = resp.read()
75
+ try:
76
+ data = json.loads(raw)
77
+ except ValueError:
78
+ data = {"error": raw[:200].decode(errors="replace")}
79
+ if resp.status >= 400:
80
+ raise Error(resp.status, data.get("error", ""))
81
+ return data
82
+
83
+ def _call(self, method, path, payload=None):
84
+ body = json.dumps(payload) if payload is not None else None
85
+ try:
86
+ data = self._send(method, path, body)
87
+ except _NotDelivered as e:
88
+ self.close()
89
+ if not e.reused:
90
+ raise Error(0, f"request not delivered: {e}") from None
91
+ data = self._send(method, path, body) # an idle keep-alive connection was closed: safe to re-send once
92
+ except (socket.timeout, TimeoutError) as e:
93
+ self.close()
94
+ raise Error(0, f"timeout: result unknown ({e})") from None
95
+ self.last = {"ms": data.get("ms")}
96
+ return data
97
+
98
+ # ---- the API: the console's routes ----
99
+
100
+ def ping(self):
101
+ return self._call("GET", "/api/ping").get("ok", False)
102
+
103
+ def dags(self):
104
+ """Every DAG: dag_id, paused, schedule, next_run, recent runs."""
105
+ return self._call("GET", "/api/dags")["dags"]
106
+
107
+ def import_errors(self):
108
+ return self._call("GET", "/api/import-errors")["import_errors"]
109
+
110
+ def upload(self, path, name=None):
111
+ """Upload a DAG file; parsed at once. Returns {"dags": [...], "removed": [...], "error": None | {...}}."""
112
+ with open(path, encoding="utf-8") as f:
113
+ source = f.read()
114
+ return self._call("PUT", "/api/files/" + _q(name or os.path.basename(path)), {"source": source})
115
+
116
+ def pause(self, dag_id, paused=True):
117
+ return self._call("POST", f"/api/dags/{_q(dag_id)}/pause", {"paused": paused})
118
+
119
+ def unpause(self, dag_id):
120
+ return self.pause(dag_id, False)
121
+
122
+ def trigger(self, dag_id, conf=None, logical_date=None, unpause=False):
123
+ """Start a run (a paused DAG needs unpause=True); returns its run_id."""
124
+ return self._call("POST", f"/api/dags/{_q(dag_id)}/runs",
125
+ {"conf": conf or {}, "logical_date": logical_date, "unpause": unpause})["run_id"]
126
+
127
+ def runs(self, dag_id, limit=25):
128
+ """The DAG's runs, oldest first, each with its task instances ("tis")."""
129
+ return self._call("GET", f"/api/dags/{_q(dag_id)}?runs={int(limit)}")["runs"]
130
+
131
+ def run(self, dag_id, run_id):
132
+ """{"run": {...}, "tis": {task_id: {...}}, "xcom": {"task.key": value}}"""
133
+ return self._call("GET", f"/api/dags/{_q(dag_id)}/runs/{_q(run_id)}")
134
+
135
+ def wait(self, dag_id, run_id, timeout=900, every=3):
136
+ """Poll until the run is done; returns "success" or "failed"."""
137
+ end = time.time() + timeout
138
+ while True:
139
+ state = self.run(dag_id, run_id)["run"]["state"]
140
+ if state in ("success", "failed"):
141
+ return state
142
+ if time.time() > end:
143
+ raise Error(0, f"run {run_id} still {state} after {timeout}s")
144
+ time.sleep(every)
145
+
146
+ def log(self, dag_id, run_id, task_id, try_number=None):
147
+ q = f"?try={int(try_number)}" if try_number else ""
148
+ return self._call("GET", f"/api/dags/{_q(dag_id)}/runs/{_q(run_id)}/tasks/{_q(task_id)}/log{q}")["log"]
149
+
150
+ def clear(self, dag_id, run_id, task_ids=None, downstream=True, only_failed=False):
151
+ return self._call("POST", f"/api/dags/{_q(dag_id)}/runs/{_q(run_id)}/clear",
152
+ {"task_ids": task_ids, "downstream": downstream, "only_failed": only_failed})["cleared"]
153
+
154
+ def cli(self, command):
155
+ """An airflow command, as typed in the console's command box: cli("dags list-runs my_dag")."""
156
+ return self._call("POST", "/api/cli", {"command": command})["output"]
157
+
158
+
159
+ def _q(s):
160
+ return urllib.parse.quote(str(s), safe="")
161
+
162
+
163
+ def connect(host, user="", password="", timeout=30, **_ignored):
164
+ """host: the console_url terraform printed (with or without https://)."""
165
+ url = host if host.startswith("http") else "https://" + host
166
+ return Connection(url, user, password, timeout)
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.4
2
+ Name: lambdaflow
3
+ Version: 0.1.0
4
+ Summary: A truly serverless Airflow on your own AWS account: Airflow 3 DAG files on Lambda, Step Functions, EventBridge Scheduler and DynamoDB, ≈ $0 when idle. Python client for the deployment.
5
+ Author: Frank Zhang
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/cloudformer/lambdaflow
8
+ Project-URL: Repository, https://github.com/cloudformer/lambdaflow
9
+ Project-URL: Deploy, https://github.com/cloudformer/lambdaflow#quick-start
10
+ Project-URL: Compatibility, https://github.com/cloudformer/lambdaflow/blob/main/tests/compat/REPORT.md
11
+ Project-URL: Changelog, https://github.com/cloudformer/lambdaflow/blob/main/CHANGELOG.md
12
+ Project-URL: Issues, https://github.com/cloudformer/lambdaflow/issues
13
+ Keywords: serverless,aws,lambda,airflow,mwaa,step-functions
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Requires-Python: >=3.9
19
+ Description-Content-Type: text/markdown
20
+
21
+ # lambdaflow
22
+
23
+ **Python client for [lambdaflow](https://github.com/cloudformer/lambdaflow) — a truly serverless Airflow:
24
+ Airflow 3 DAG files on Lambda, Step Functions, EventBridge Scheduler, DynamoDB and S3, ≈ $0 when idle.**
25
+ Part of [Redefine AWS Serverless](https://github.com/topics/redefine-aws-serverless).
26
+
27
+ ![lambdaflow architecture](https://raw.githubusercontent.com/cloudformer/lambdaflow/main/docs/architecture.svg)
28
+
29
+ ```bash
30
+ pip install lambdaflow
31
+ ```
32
+
33
+ ```python
34
+ from lambdaflow import client
35
+
36
+ lf = client.connect("https://xxxx.lambda-url.us-east-2.on.aws", "admin", "secret") # terraform output console_url
37
+ lf.upload("my_dag.py") # parsed at once; import errors come back
38
+ run_id = lf.trigger("my_dag", conf={"x": 1}, unpause=True)
39
+ print(lf.wait("my_dag", run_id)) # "success" | "failed"
40
+ print(lf.log("my_dag", run_id, "extract"))
41
+ print(lf.cli("dags list-runs my_dag")) # the airflow commands of the console's command box
42
+ ```
43
+
44
+ Standard library only. The deployment comes from the [main repository](https://github.com/cloudformer/lambdaflow)
45
+ (Terraform). Compatibility with Airflow 3.3:
46
+ [REPORT.md](https://github.com/cloudformer/lambdaflow/blob/main/tests/compat/REPORT.md).
@@ -0,0 +1,8 @@
1
+ README.md
2
+ pyproject.toml
3
+ lambdaflow/__init__.py
4
+ lambdaflow/client.py
5
+ lambdaflow.egg-info/PKG-INFO
6
+ lambdaflow.egg-info/SOURCES.txt
7
+ lambdaflow.egg-info/dependency_links.txt
8
+ lambdaflow.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ lambdaflow
@@ -0,0 +1,31 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "lambdaflow"
7
+ version = "0.1.0"
8
+ description = "A truly serverless Airflow on your own AWS account: Airflow 3 DAG files on Lambda, Step Functions, EventBridge Scheduler and DynamoDB, ≈ $0 when idle. Python client for the deployment."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Frank Zhang" }]
13
+ keywords = ["serverless", "aws", "lambda", "airflow", "mwaa", "step-functions"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ ]
20
+ dependencies = [] # standard library only
21
+
22
+ [project.urls]
23
+ Homepage = "https://github.com/cloudformer/lambdaflow"
24
+ Repository = "https://github.com/cloudformer/lambdaflow"
25
+ Deploy = "https://github.com/cloudformer/lambdaflow#quick-start"
26
+ Compatibility = "https://github.com/cloudformer/lambdaflow/blob/main/tests/compat/REPORT.md"
27
+ Changelog = "https://github.com/cloudformer/lambdaflow/blob/main/CHANGELOG.md"
28
+ Issues = "https://github.com/cloudformer/lambdaflow/issues"
29
+
30
+ [tool.setuptools]
31
+ packages = ["lambdaflow"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+