polymo 1.0.1__tar.gz → 1.2.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.
- {polymo-1.0.1 → polymo-1.2.0}/PKG-INFO +27 -25
- {polymo-1.0.1 → polymo-1.2.0}/README.md +21 -17
- {polymo-1.0.1 → polymo-1.2.0}/pyproject.toml +1 -6
- polymo-1.2.0/src/polymo/cli.py +70 -0
- {polymo-1.0.1 → polymo-1.2.0}/src/polymo/codegen/templates/core.py.jinja +48 -0
- polymo-1.2.0/src/polymo/codegen/templates/dp.py.jinja +131 -0
- polymo-1.0.1/src/polymo/cli.py +0 -70
- polymo-1.0.1/src/polymo/codegen/templates/dp.py.jinja +0 -97
- {polymo-1.0.1 → polymo-1.2.0}/src/polymo/__init__.py +0 -0
- {polymo-1.0.1 → polymo-1.2.0}/src/polymo/builder/__init__.py +0 -0
- {polymo-1.0.1 → polymo-1.2.0}/src/polymo/builder/app.py +0 -0
- {polymo-1.0.1 → polymo-1.2.0}/src/polymo/builder/preview.py +0 -0
- {polymo-1.0.1 → polymo-1.2.0}/src/polymo/builder/static/examples/countries.polymo.json +0 -0
- {polymo-1.0.1 → polymo-1.2.0}/src/polymo/builder/static/examples/githubrepos.polymo.json +0 -0
- {polymo-1.0.1 → polymo-1.2.0}/src/polymo/builder/static/examples/jsonplaceholder.polymo.json +0 -0
- {polymo-1.0.1 → polymo-1.2.0}/src/polymo/builder/static/examples/jsonplaceholder_endpoints.polymo.json +0 -0
- {polymo-1.0.1 → polymo-1.2.0}/src/polymo/builder/static/examples/weather.polymo.json +0 -0
- {polymo-1.0.1 → polymo-1.2.0}/src/polymo/builder/static/favicon.ico +0 -0
- {polymo-1.0.1 → polymo-1.2.0}/src/polymo/builder/static/favicon.png +0 -0
- {polymo-1.0.1 → polymo-1.2.0}/src/polymo/builder/static/index.html +0 -0
- {polymo-1.0.1 → polymo-1.2.0}/src/polymo/builder/static/logo.png +0 -0
- {polymo-1.0.1 → polymo-1.2.0}/src/polymo/builder/static/logo192.png +0 -0
- {polymo-1.0.1 → polymo-1.2.0}/src/polymo/builder/static/main.css +0 -0
- {polymo-1.0.1 → polymo-1.2.0}/src/polymo/builder/static/main.js +0 -0
- {polymo-1.0.1 → polymo-1.2.0}/src/polymo/builder/static/mockServiceWorker.js +0 -0
- {polymo-1.0.1 → polymo-1.2.0}/src/polymo/builder/templates/index.html +0 -0
- {polymo-1.0.1 → polymo-1.2.0}/src/polymo/codegen/__init__.py +0 -0
- {polymo-1.0.1 → polymo-1.2.0}/src/polymo/codegen/generator.py +0 -0
- {polymo-1.0.1 → polymo-1.2.0}/src/polymo/codegen/templating.py +0 -0
- {polymo-1.0.1 → polymo-1.2.0}/src/polymo/config.py +0 -0
- {polymo-1.0.1 → polymo-1.2.0}/src/polymo/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: polymo
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Generate standalone Lakeflow Declarative Pipelines scripts from a point-and-click REST connector builder
|
|
5
5
|
Keywords: spark,pyspark,rest,api,ingestion,data-engineering,etl,http
|
|
6
6
|
Author: Daniel Tom
|
|
@@ -21,18 +21,16 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
21
21
|
Classifier: Operating System :: OS Independent
|
|
22
22
|
Classifier: Typing :: Typed
|
|
23
23
|
Requires-Dist: jinja2>=3.1.6
|
|
24
|
-
Requires-Dist: fastapi>=0.110
|
|
25
|
-
Requires-Dist: uvicorn>=0.24
|
|
26
|
-
Requires-Dist:
|
|
27
|
-
Requires-Dist:
|
|
28
|
-
Requires-Dist:
|
|
29
|
-
Requires-Dist: requests>=2.31 ; extra == 'builder'
|
|
24
|
+
Requires-Dist: fastapi>=0.110
|
|
25
|
+
Requires-Dist: uvicorn>=0.24
|
|
26
|
+
Requires-Dist: pyspark>=4
|
|
27
|
+
Requires-Dist: pyarrow>=13
|
|
28
|
+
Requires-Dist: requests>=2.31
|
|
30
29
|
Requires-Python: >=3.10
|
|
31
30
|
Project-URL: Documentation, https://dan1elt0m.github.io/polymo/
|
|
32
31
|
Project-URL: Homepage, https://github.com/dan1elt0m/polymo
|
|
33
32
|
Project-URL: Issues, https://github.com/dan1elt0m/polymo/issues
|
|
34
33
|
Project-URL: Repository, https://github.com/dan1elt0m/polymo.git
|
|
35
|
-
Provides-Extra: builder
|
|
36
34
|
Description-Content-Type: text/markdown
|
|
37
35
|
|
|
38
36
|
<p align="center">
|
|
@@ -71,10 +69,16 @@ PARAMS: dict = {"_limit": 20}
|
|
|
71
69
|
|
|
72
70
|
# … fetch/retry helpers elided …
|
|
73
71
|
|
|
72
|
+
# … inline Data Source (RestSource/_Reader) elided; the batch @dp.table
|
|
73
|
+
# below ingests through it, same as Lakeflow requires for the streaming
|
|
74
|
+
# variant …
|
|
75
|
+
|
|
76
|
+
spark.dataSource.register(RestSource)
|
|
77
|
+
|
|
78
|
+
|
|
74
79
|
@dp.table(name="posts")
|
|
75
80
|
def posts():
|
|
76
|
-
|
|
77
|
-
return spark.createDataFrame(rows)
|
|
81
|
+
return spark.read.format("posts_source").load()
|
|
78
82
|
```
|
|
79
83
|
|
|
80
84
|
See [docs/superpowers/specs/2026-08-28-codegen-pivot-design.md](docs/superpowers/specs/2026-08-28-codegen-pivot-design.md) for the full design behind this pivot.
|
|
@@ -108,30 +112,28 @@ The YAML runtime (`spark.read.format("polymo")`, `PolymoConfig`,
|
|
|
108
112
|
ready to move yet.
|
|
109
113
|
|
|
110
114
|
## How to start?
|
|
111
|
-
Locally you probably want to install polymo along with the Builder UI:
|
|
112
115
|
|
|
113
116
|
```bash
|
|
114
|
-
pip install
|
|
117
|
+
pip install polymo
|
|
115
118
|
```
|
|
116
119
|
|
|
117
|
-
This
|
|
120
|
+
This installs everything you need, Builder UI included — FastAPI, Uvicorn,
|
|
121
|
+
PySpark, PyArrow, and requests all come along, so there's nothing extra to
|
|
122
|
+
opt into.
|
|
123
|
+
|
|
124
|
+
Note that `polymo` is never a dependency of the scripts it generates —
|
|
125
|
+
nothing you export needs polymo installed to run.
|
|
126
|
+
|
|
127
|
+
## Launch the builder UI
|
|
118
128
|
|
|
119
|
-
Scripting connector generation directly in Python (calling `generate()` /
|
|
120
|
-
`parse_config()` without the UI — see the [API reference](docs/api.md))
|
|
121
|
-
doesn't need pyspark at all. In that case, just install the bare minimum
|
|
122
|
-
deps with:
|
|
123
129
|
```bash
|
|
124
|
-
|
|
130
|
+
polymo
|
|
125
131
|
```
|
|
126
132
|
|
|
127
|
-
|
|
128
|
-
`polymo` is never a dependency of the scripts it generates — nothing you
|
|
129
|
-
export needs polymo installed to run.
|
|
133
|
+
Or without installing it first:
|
|
130
134
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
```bash
|
|
134
|
-
polymo builder
|
|
135
|
+
```bash
|
|
136
|
+
uvx polymo
|
|
135
137
|
```
|
|
136
138
|
|
|
137
139
|
#### (Optional) Run the Builder in Docker
|
|
@@ -34,10 +34,16 @@ PARAMS: dict = {"_limit": 20}
|
|
|
34
34
|
|
|
35
35
|
# … fetch/retry helpers elided …
|
|
36
36
|
|
|
37
|
+
# … inline Data Source (RestSource/_Reader) elided; the batch @dp.table
|
|
38
|
+
# below ingests through it, same as Lakeflow requires for the streaming
|
|
39
|
+
# variant …
|
|
40
|
+
|
|
41
|
+
spark.dataSource.register(RestSource)
|
|
42
|
+
|
|
43
|
+
|
|
37
44
|
@dp.table(name="posts")
|
|
38
45
|
def posts():
|
|
39
|
-
|
|
40
|
-
return spark.createDataFrame(rows)
|
|
46
|
+
return spark.read.format("posts_source").load()
|
|
41
47
|
```
|
|
42
48
|
|
|
43
49
|
See [docs/superpowers/specs/2026-08-28-codegen-pivot-design.md](docs/superpowers/specs/2026-08-28-codegen-pivot-design.md) for the full design behind this pivot.
|
|
@@ -71,30 +77,28 @@ The YAML runtime (`spark.read.format("polymo")`, `PolymoConfig`,
|
|
|
71
77
|
ready to move yet.
|
|
72
78
|
|
|
73
79
|
## How to start?
|
|
74
|
-
Locally you probably want to install polymo along with the Builder UI:
|
|
75
80
|
|
|
76
81
|
```bash
|
|
77
|
-
pip install
|
|
82
|
+
pip install polymo
|
|
78
83
|
```
|
|
79
84
|
|
|
80
|
-
This
|
|
85
|
+
This installs everything you need, Builder UI included — FastAPI, Uvicorn,
|
|
86
|
+
PySpark, PyArrow, and requests all come along, so there's nothing extra to
|
|
87
|
+
opt into.
|
|
88
|
+
|
|
89
|
+
Note that `polymo` is never a dependency of the scripts it generates —
|
|
90
|
+
nothing you export needs polymo installed to run.
|
|
91
|
+
|
|
92
|
+
## Launch the builder UI
|
|
81
93
|
|
|
82
|
-
Scripting connector generation directly in Python (calling `generate()` /
|
|
83
|
-
`parse_config()` without the UI — see the [API reference](docs/api.md))
|
|
84
|
-
doesn't need pyspark at all. In that case, just install the bare minimum
|
|
85
|
-
deps with:
|
|
86
94
|
```bash
|
|
87
|
-
|
|
95
|
+
polymo
|
|
88
96
|
```
|
|
89
97
|
|
|
90
|
-
|
|
91
|
-
`polymo` is never a dependency of the scripts it generates — nothing you
|
|
92
|
-
export needs polymo installed to run.
|
|
98
|
+
Or without installing it first:
|
|
93
99
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
```bash
|
|
97
|
-
polymo builder
|
|
100
|
+
```bash
|
|
101
|
+
uvx polymo
|
|
98
102
|
```
|
|
99
103
|
|
|
100
104
|
#### (Optional) Run the Builder in Docker
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "polymo"
|
|
3
|
-
version = "1.0
|
|
3
|
+
version = "1.2.0"
|
|
4
4
|
description = "Generate standalone Lakeflow Declarative Pipelines scripts from a point-and-click REST connector builder"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -27,13 +27,8 @@ classifiers = [
|
|
|
27
27
|
]
|
|
28
28
|
dependencies = [
|
|
29
29
|
"jinja2>=3.1.6",
|
|
30
|
-
]
|
|
31
|
-
|
|
32
|
-
[project.optional-dependencies]
|
|
33
|
-
builder = [
|
|
34
30
|
"fastapi>=0.110",
|
|
35
31
|
"uvicorn>=0.24",
|
|
36
|
-
"jinja2>=3.1.6",
|
|
37
32
|
"pyspark>=4",
|
|
38
33
|
"pyarrow>=13",
|
|
39
34
|
"requests>=2.31",
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Command line entry points for polymo."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
from typing import Sequence
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
10
|
+
parser = argparse.ArgumentParser(
|
|
11
|
+
prog="polymo", description="Launch the local polymo Builder UI"
|
|
12
|
+
)
|
|
13
|
+
parser.add_argument(
|
|
14
|
+
"--host", default="127.0.0.1", help="Host to bind (default: %(default)s)"
|
|
15
|
+
)
|
|
16
|
+
parser.add_argument(
|
|
17
|
+
"--port", type=int, default=8000, help="Port to bind (default: %(default)s)"
|
|
18
|
+
)
|
|
19
|
+
parser.add_argument(
|
|
20
|
+
"--reload",
|
|
21
|
+
action="store_true",
|
|
22
|
+
help="Enable auto-reload (useful during development)",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
args = parser.parse_args(argv)
|
|
26
|
+
|
|
27
|
+
if not _require_builder_deps():
|
|
28
|
+
return 1
|
|
29
|
+
|
|
30
|
+
import uvicorn
|
|
31
|
+
|
|
32
|
+
uvicorn.run(
|
|
33
|
+
"polymo.builder.app:create_app",
|
|
34
|
+
host=args.host,
|
|
35
|
+
port=args.port,
|
|
36
|
+
reload=args.reload,
|
|
37
|
+
factory=True,
|
|
38
|
+
)
|
|
39
|
+
return 0
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _require_builder_deps() -> bool:
|
|
43
|
+
"""Verify the install is intact (pyspark present and compatible).
|
|
44
|
+
|
|
45
|
+
polymo now depends on pyspark unconditionally, so a missing or
|
|
46
|
+
incompatible pyspark means the install itself is broken. Returns True
|
|
47
|
+
when the environment is ready to launch the builder. On a missing
|
|
48
|
+
pyspark install, prints a friendly hint and returns False so the caller
|
|
49
|
+
can exit cleanly instead of tracebacking.
|
|
50
|
+
"""
|
|
51
|
+
try:
|
|
52
|
+
import pyspark
|
|
53
|
+
except ModuleNotFoundError:
|
|
54
|
+
print(
|
|
55
|
+
"polymo's dependencies are incomplete — reinstall with: "
|
|
56
|
+
"pip install --force-reinstall polymo"
|
|
57
|
+
)
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
if not pyspark.__version__.startswith("4."):
|
|
61
|
+
raise ImportError(
|
|
62
|
+
"pyspark>=4.0.0 is required: reinstall with "
|
|
63
|
+
"pip install --force-reinstall polymo"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
return True
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__": # pragma: no cover
|
|
70
|
+
raise SystemExit(main())
|
|
@@ -45,6 +45,7 @@ WINDOWS = {{ windows_repr }}
|
|
|
45
45
|
# durable path (e.g. a Databricks Volume: /Volumes/cat/schema/vol/{{ stream_name_comment }}.json).
|
|
46
46
|
STATE_PATH = {{ state_path_repr }}
|
|
47
47
|
{% if not has_windows %}
|
|
48
|
+
# used by the builder preview; the dp table below tracks its own cursor
|
|
48
49
|
LAST_CURSOR = {"value": None}
|
|
49
50
|
{% endif %}
|
|
50
51
|
|
|
@@ -448,6 +449,53 @@ def fetch_records(extra_params=None, path=None):
|
|
|
448
449
|
LAST_CURSOR["value"] = value
|
|
449
450
|
{% endif %}
|
|
450
451
|
{% endif %}
|
|
452
|
+
{% if not schema_ddl %}
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _infer_schema():
|
|
456
|
+
"""Sample records to derive a DDL schema (explicit SCHEMA recommended)."""
|
|
457
|
+
from itertools import islice
|
|
458
|
+
|
|
459
|
+
{% if has_windows %}
|
|
460
|
+
sample = []
|
|
461
|
+
for window in WINDOWS:
|
|
462
|
+
sample.extend(islice(fetch_records(**window), max(1, 50 // len(WINDOWS))))
|
|
463
|
+
{% else %}
|
|
464
|
+
sample = list(islice(fetch_records(), 50))
|
|
465
|
+
{% endif %}
|
|
466
|
+
if not sample:
|
|
467
|
+
raise RuntimeError(
|
|
468
|
+
"cannot infer a schema from an empty response; set an explicit schema"
|
|
469
|
+
)
|
|
470
|
+
fields = {}
|
|
471
|
+
for record in sample:
|
|
472
|
+
for key, value in record.items():
|
|
473
|
+
if value is None:
|
|
474
|
+
continue
|
|
475
|
+
if isinstance(value, bool):
|
|
476
|
+
candidate = "BOOLEAN"
|
|
477
|
+
elif isinstance(value, int):
|
|
478
|
+
candidate = "BIGINT"
|
|
479
|
+
elif isinstance(value, float):
|
|
480
|
+
candidate = "DOUBLE"
|
|
481
|
+
else:
|
|
482
|
+
candidate = "STRING"
|
|
483
|
+
current = fields.get(key)
|
|
484
|
+
if current is None:
|
|
485
|
+
fields[key] = candidate
|
|
486
|
+
elif current == candidate:
|
|
487
|
+
pass
|
|
488
|
+
elif {current, candidate} == {"BIGINT", "DOUBLE"}:
|
|
489
|
+
fields[key] = "DOUBLE"
|
|
490
|
+
else:
|
|
491
|
+
fields[key] = "STRING"
|
|
492
|
+
if not fields:
|
|
493
|
+
raise RuntimeError(
|
|
494
|
+
"cannot infer a schema: sampled records have no fields; set an explicit"
|
|
495
|
+
" schema"
|
|
496
|
+
)
|
|
497
|
+
return ", ".join(f"`{name}` {type_}" for name, type_ in fields.items())
|
|
498
|
+
{% endif %}
|
|
451
499
|
{% if streaming %}
|
|
452
500
|
|
|
453
501
|
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
from pyspark import pipelines as dp # noqa: E402
|
|
2
|
+
from pyspark.sql import SparkSession # noqa: E402
|
|
3
|
+
|
|
4
|
+
spark = SparkSession.getActiveSession()
|
|
5
|
+
|
|
6
|
+
{% if schema_ddl %}
|
|
7
|
+
SCHEMA = {{ schema_ddl | tojson }}
|
|
8
|
+
|
|
9
|
+
{% endif %}
|
|
10
|
+
|
|
11
|
+
{% if streaming %}
|
|
12
|
+
from pyspark.sql.datasource import DataSource, SimpleDataSourceStreamReader # noqa: E402
|
|
13
|
+
|
|
14
|
+
# Assumes SCHEMA is a comma-separated "name TYPE" DDL string (as emitted
|
|
15
|
+
# above); column names are taken as the first whitespace-separated token.
|
|
16
|
+
COLUMNS = [f.split()[0] for f in SCHEMA.split(",")]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _Reader(SimpleDataSourceStreamReader):
|
|
20
|
+
def initialOffset(self):
|
|
21
|
+
return {"page": 0}
|
|
22
|
+
|
|
23
|
+
def read(self, start):
|
|
24
|
+
page = start["page"]
|
|
25
|
+
records = fetch_page(page)
|
|
26
|
+
rows = [tuple(r.get(c) for c in COLUMNS) for r in records]
|
|
27
|
+
next_page = page + 1 if records else page
|
|
28
|
+
return iter(rows), {"page": next_page}
|
|
29
|
+
|
|
30
|
+
def readBetweenOffsets(self, start, end):
|
|
31
|
+
rows = []
|
|
32
|
+
for page in range(start["page"], end["page"]):
|
|
33
|
+
for r in fetch_page(page):
|
|
34
|
+
rows.append(tuple(r.get(c) for c in COLUMNS))
|
|
35
|
+
return iter(rows)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class RestStreamSource(DataSource):
|
|
39
|
+
@classmethod
|
|
40
|
+
def name(cls):
|
|
41
|
+
return "{{ func_name }}_stream"
|
|
42
|
+
|
|
43
|
+
def schema(self):
|
|
44
|
+
return SCHEMA
|
|
45
|
+
|
|
46
|
+
def simpleStreamReader(self, schema):
|
|
47
|
+
return _Reader()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
spark.dataSource.register(RestStreamSource)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dp.table(name={{ table_name_repr }})
|
|
54
|
+
def {{ func_name }}():
|
|
55
|
+
return spark.readStream.format("{{ func_name }}_stream").load()
|
|
56
|
+
{% else %}
|
|
57
|
+
{% if has_windows %}
|
|
58
|
+
from pyspark.sql.datasource import DataSource, DataSourceReader, InputPartition # noqa: E402
|
|
59
|
+
{% else %}
|
|
60
|
+
from pyspark.sql.datasource import DataSource, DataSourceReader # noqa: E402
|
|
61
|
+
{% endif %}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class RestSource(DataSource):
|
|
65
|
+
@classmethod
|
|
66
|
+
def name(cls):
|
|
67
|
+
return "{{ func_name }}_source"
|
|
68
|
+
|
|
69
|
+
def schema(self):
|
|
70
|
+
return {% if schema_ddl %}SCHEMA{% else %}_infer_schema(){% endif %}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def reader(self, schema):
|
|
74
|
+
return _Reader(schema)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class _Reader(DataSourceReader):
|
|
78
|
+
def __init__(self, schema):
|
|
79
|
+
self._columns = [field.name for field in schema.fields]
|
|
80
|
+
|
|
81
|
+
{% if has_windows %}
|
|
82
|
+
def partitions(self):
|
|
83
|
+
return [InputPartition(index) for index in range(len(WINDOWS))]
|
|
84
|
+
|
|
85
|
+
def read(self, partition):
|
|
86
|
+
records = fetch_records(**WINDOWS[partition.value])
|
|
87
|
+
{% else %}
|
|
88
|
+
def read(self, partition):
|
|
89
|
+
records = fetch_records()
|
|
90
|
+
{% endif %}
|
|
91
|
+
{% if incremental_mode %}
|
|
92
|
+
cursor = None
|
|
93
|
+
{% endif %}
|
|
94
|
+
for record in records:
|
|
95
|
+
{% if incremental_mode %}
|
|
96
|
+
value = record.get({{ cursor_field_repr }})
|
|
97
|
+
if value is not None and (cursor is None or value > cursor):
|
|
98
|
+
cursor = value
|
|
99
|
+
{% endif %}
|
|
100
|
+
yield tuple(_cell(record.get(column)) for column in self._columns)
|
|
101
|
+
{% if incremental_mode %}
|
|
102
|
+
# read() runs per partition on the executor that owns it, so each
|
|
103
|
+
# partition writes its own max cursor independently; whichever
|
|
104
|
+
# partition finishes last wins. Batch tables re-fetch fully each
|
|
105
|
+
# run, so a stale cursor only costs redundant fetching, not missed
|
|
106
|
+
# data. STATE_PATH must point somewhere every executor can reach
|
|
107
|
+
# (e.g. a Databricks Volume, per the comment on it above) for this
|
|
108
|
+
# to work at all.
|
|
109
|
+
_write_state(cursor)
|
|
110
|
+
{% endif %}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _cell(value):
|
|
114
|
+
"""Nested structures become JSON strings; scalars pass through."""
|
|
115
|
+
if isinstance(value, (dict, list)):
|
|
116
|
+
import json
|
|
117
|
+
|
|
118
|
+
return json.dumps(value)
|
|
119
|
+
return value
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
spark.dataSource.register(RestSource)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
{% if partition_strategy == "pagination" %}
|
|
126
|
+
# partition strategy "pagination" runs sequentially in generated scripts
|
|
127
|
+
{% endif %}
|
|
128
|
+
@dp.table(name={{ table_name_repr }})
|
|
129
|
+
def {{ func_name }}():
|
|
130
|
+
return spark.read.format("{{ func_name }}_source").load()
|
|
131
|
+
{% endif %}
|
polymo-1.0.1/src/polymo/cli.py
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
"""Command line entry points for polymo."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import argparse
|
|
6
|
-
from typing import Sequence
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def main(argv: Sequence[str] | None = None) -> int:
|
|
10
|
-
parser = argparse.ArgumentParser(
|
|
11
|
-
prog="polymo", description="Utilities for the polymo toolkit"
|
|
12
|
-
)
|
|
13
|
-
subparsers = parser.add_subparsers(dest="command")
|
|
14
|
-
bld_parser = subparsers.add_parser("builder", help="Launch the local builder UI")
|
|
15
|
-
bld_parser.add_argument(
|
|
16
|
-
"--host", default="127.0.0.1", help="Host to bind (default: %(default)s)"
|
|
17
|
-
)
|
|
18
|
-
bld_parser.add_argument(
|
|
19
|
-
"--port", type=int, default=8000, help="Port to bind (default: %(default)s)"
|
|
20
|
-
)
|
|
21
|
-
bld_parser.add_argument(
|
|
22
|
-
"--reload",
|
|
23
|
-
action="store_true",
|
|
24
|
-
help="Enable auto-reload (useful during development)",
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
args = parser.parse_args(argv)
|
|
28
|
-
|
|
29
|
-
if args.command == "builder":
|
|
30
|
-
if not _require_builder_deps():
|
|
31
|
-
return 1
|
|
32
|
-
|
|
33
|
-
import uvicorn
|
|
34
|
-
|
|
35
|
-
uvicorn.run(
|
|
36
|
-
"polymo.builder.app:create_app",
|
|
37
|
-
host=args.host,
|
|
38
|
-
port=args.port,
|
|
39
|
-
reload=args.reload,
|
|
40
|
-
factory=True,
|
|
41
|
-
)
|
|
42
|
-
return 0
|
|
43
|
-
|
|
44
|
-
parser.print_help()
|
|
45
|
-
return 1
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def _require_builder_deps() -> bool:
|
|
49
|
-
"""Verify the ``builder`` extra (pyspark) is installed and compatible.
|
|
50
|
-
|
|
51
|
-
Returns True when the environment is ready to launch the builder.
|
|
52
|
-
On a missing pyspark install, prints a friendly hint and returns False
|
|
53
|
-
so the caller can exit cleanly instead of tracebacking.
|
|
54
|
-
"""
|
|
55
|
-
try:
|
|
56
|
-
import pyspark
|
|
57
|
-
except ModuleNotFoundError:
|
|
58
|
-
print("The builder needs extras: pip install 'polymo[builder]'")
|
|
59
|
-
return False
|
|
60
|
-
|
|
61
|
-
if not pyspark.__version__.startswith("4."):
|
|
62
|
-
raise ImportError(
|
|
63
|
-
"pyspark>=4.0.0 is required: run pip install 'polymo[builder]'"
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
return True
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if __name__ == "__main__": # pragma: no cover
|
|
70
|
-
raise SystemExit(main())
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
from pyspark import pipelines as dp # noqa: E402
|
|
2
|
-
from pyspark.sql import SparkSession # noqa: E402
|
|
3
|
-
|
|
4
|
-
spark = SparkSession.getActiveSession()
|
|
5
|
-
|
|
6
|
-
{% if schema_ddl %}
|
|
7
|
-
SCHEMA = {{ schema_ddl | tojson }}
|
|
8
|
-
|
|
9
|
-
{% endif %}
|
|
10
|
-
|
|
11
|
-
{% if streaming %}
|
|
12
|
-
from pyspark.sql.datasource import DataSource, SimpleDataSourceStreamReader # noqa: E402
|
|
13
|
-
|
|
14
|
-
# Assumes SCHEMA is a comma-separated "name TYPE" DDL string (as emitted
|
|
15
|
-
# above); column names are taken as the first whitespace-separated token.
|
|
16
|
-
COLUMNS = [f.split()[0] for f in SCHEMA.split(",")]
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class _Reader(SimpleDataSourceStreamReader):
|
|
20
|
-
def initialOffset(self):
|
|
21
|
-
return {"page": 0}
|
|
22
|
-
|
|
23
|
-
def read(self, start):
|
|
24
|
-
page = start["page"]
|
|
25
|
-
records = fetch_page(page)
|
|
26
|
-
rows = [tuple(r.get(c) for c in COLUMNS) for r in records]
|
|
27
|
-
next_page = page + 1 if records else page
|
|
28
|
-
return iter(rows), {"page": next_page}
|
|
29
|
-
|
|
30
|
-
def readBetweenOffsets(self, start, end):
|
|
31
|
-
rows = []
|
|
32
|
-
for page in range(start["page"], end["page"]):
|
|
33
|
-
for r in fetch_page(page):
|
|
34
|
-
rows.append(tuple(r.get(c) for c in COLUMNS))
|
|
35
|
-
return iter(rows)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
class RestStreamSource(DataSource):
|
|
39
|
-
@classmethod
|
|
40
|
-
def name(cls):
|
|
41
|
-
return "{{ func_name }}_stream"
|
|
42
|
-
|
|
43
|
-
def schema(self):
|
|
44
|
-
return SCHEMA
|
|
45
|
-
|
|
46
|
-
def simpleStreamReader(self, schema):
|
|
47
|
-
return _Reader()
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
spark.dataSource.register(RestStreamSource)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
@dp.table(name={{ table_name_repr }})
|
|
54
|
-
def {{ func_name }}():
|
|
55
|
-
return spark.readStream.format("{{ func_name }}_stream").load()
|
|
56
|
-
{% else %}
|
|
57
|
-
{% if windows_repr %}
|
|
58
|
-
@dp.table(name={{ table_name_repr }})
|
|
59
|
-
def {{ func_name }}():
|
|
60
|
-
sc = spark.sparkContext
|
|
61
|
-
rows = sc.parallelize(WINDOWS, len(WINDOWS)).flatMap(
|
|
62
|
-
lambda window: list(fetch_records(**window))
|
|
63
|
-
).collect()
|
|
64
|
-
{% if incremental_mode %}
|
|
65
|
-
# fetch_records runs on executors under parallelize/flatMap, which don't
|
|
66
|
-
# share memory with the driver, so the cursor can't be tracked as
|
|
67
|
-
# in-process state; compute it here instead, from the rows collected
|
|
68
|
-
# back on the driver.
|
|
69
|
-
cursor_values = [
|
|
70
|
-
r.get({{ cursor_field_repr }})
|
|
71
|
-
for r in rows
|
|
72
|
-
if isinstance(r, dict) and r.get({{ cursor_field_repr }}) is not None
|
|
73
|
-
]
|
|
74
|
-
_write_state(max(cursor_values) if cursor_values else None)
|
|
75
|
-
{% endif %}
|
|
76
|
-
{% if schema_ddl %}
|
|
77
|
-
return spark.createDataFrame(rows, schema=SCHEMA)
|
|
78
|
-
{% else %}
|
|
79
|
-
return spark.createDataFrame(rows)
|
|
80
|
-
{% endif %}
|
|
81
|
-
{% else %}
|
|
82
|
-
{% if partition_strategy == "pagination" %}
|
|
83
|
-
# partition strategy "pagination" runs sequentially in generated scripts
|
|
84
|
-
{% endif %}
|
|
85
|
-
@dp.table(name={{ table_name_repr }})
|
|
86
|
-
def {{ func_name }}():
|
|
87
|
-
rows = list(fetch_records())
|
|
88
|
-
{% if incremental_mode %}
|
|
89
|
-
_write_state(LAST_CURSOR["value"])
|
|
90
|
-
{% endif %}
|
|
91
|
-
{% if schema_ddl %}
|
|
92
|
-
return spark.createDataFrame(rows, schema=SCHEMA)
|
|
93
|
-
{% else %}
|
|
94
|
-
return spark.createDataFrame(rows)
|
|
95
|
-
{% endif %}
|
|
96
|
-
{% endif %}
|
|
97
|
-
{% endif %}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{polymo-1.0.1 → polymo-1.2.0}/src/polymo/builder/static/examples/jsonplaceholder.polymo.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|