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.
- lambdaflow-0.1.0/PKG-INFO +46 -0
- lambdaflow-0.1.0/README.md +26 -0
- lambdaflow-0.1.0/lambdaflow/__init__.py +4 -0
- lambdaflow-0.1.0/lambdaflow/client.py +166 -0
- lambdaflow-0.1.0/lambdaflow.egg-info/PKG-INFO +46 -0
- lambdaflow-0.1.0/lambdaflow.egg-info/SOURCES.txt +8 -0
- lambdaflow-0.1.0/lambdaflow.egg-info/dependency_links.txt +1 -0
- lambdaflow-0.1.0/lambdaflow.egg-info/top_level.txt +1 -0
- lambdaflow-0.1.0/pyproject.toml +31 -0
- lambdaflow-0.1.0/setup.cfg +4 -0
|
@@ -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
|
+

|
|
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
|
+

|
|
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,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
|
+

|
|
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 @@
|
|
|
1
|
+
|
|
@@ -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"]
|