polymo 1.4.1__tar.gz → 1.4.2__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.4.1 → polymo-1.4.2}/PKG-INFO +1 -1
- {polymo-1.4.1 → polymo-1.4.2}/pyproject.toml +1 -1
- {polymo-1.4.1 → polymo-1.4.2}/pyproject.toml.orig +1 -1
- {polymo-1.4.1 → polymo-1.4.2}/src/polymo/builder/app.py +61 -4
- {polymo-1.4.1 → polymo-1.4.2}/src/polymo/builder/preview.py +58 -0
- {polymo-1.4.1 → polymo-1.4.2}/README.md +0 -0
- {polymo-1.4.1 → polymo-1.4.2}/src/polymo/__init__.py +0 -0
- {polymo-1.4.1 → polymo-1.4.2}/src/polymo/builder/__init__.py +0 -0
- {polymo-1.4.1 → polymo-1.4.2}/src/polymo/builder/databricks.py +0 -0
- {polymo-1.4.1 → polymo-1.4.2}/src/polymo/builder/static/examples/countries.polymo.json +0 -0
- {polymo-1.4.1 → polymo-1.4.2}/src/polymo/builder/static/examples/githubrepos.polymo.json +0 -0
- {polymo-1.4.1 → polymo-1.4.2}/src/polymo/builder/static/examples/jsonplaceholder.polymo.json +0 -0
- {polymo-1.4.1 → polymo-1.4.2}/src/polymo/builder/static/examples/jsonplaceholder_endpoints.polymo.json +0 -0
- {polymo-1.4.1 → polymo-1.4.2}/src/polymo/builder/static/examples/weather.polymo.json +0 -0
- {polymo-1.4.1 → polymo-1.4.2}/src/polymo/builder/static/favicon.ico +0 -0
- {polymo-1.4.1 → polymo-1.4.2}/src/polymo/builder/static/favicon.png +0 -0
- {polymo-1.4.1 → polymo-1.4.2}/src/polymo/builder/static/index.html +0 -0
- {polymo-1.4.1 → polymo-1.4.2}/src/polymo/builder/static/logo.png +0 -0
- {polymo-1.4.1 → polymo-1.4.2}/src/polymo/builder/static/logo192.png +0 -0
- {polymo-1.4.1 → polymo-1.4.2}/src/polymo/builder/static/main.css +0 -0
- {polymo-1.4.1 → polymo-1.4.2}/src/polymo/builder/static/main.js +0 -0
- {polymo-1.4.1 → polymo-1.4.2}/src/polymo/builder/static/mockServiceWorker.js +0 -0
- {polymo-1.4.1 → polymo-1.4.2}/src/polymo/builder/templates/index.html +0 -0
- {polymo-1.4.1 → polymo-1.4.2}/src/polymo/cli.py +0 -0
- {polymo-1.4.1 → polymo-1.4.2}/src/polymo/codegen/__init__.py +0 -0
- {polymo-1.4.1 → polymo-1.4.2}/src/polymo/codegen/bundle.py +0 -0
- {polymo-1.4.1 → polymo-1.4.2}/src/polymo/codegen/generator.py +0 -0
- {polymo-1.4.1 → polymo-1.4.2}/src/polymo/codegen/templates/bundle/databricks.yml.jinja +0 -0
- {polymo-1.4.1 → polymo-1.4.2}/src/polymo/codegen/templates/bundle/pipeline.py.jinja +0 -0
- {polymo-1.4.1 → polymo-1.4.2}/src/polymo/codegen/templates/bundle/readme.md.jinja +0 -0
- {polymo-1.4.1 → polymo-1.4.2}/src/polymo/codegen/templates/core.py.jinja +0 -0
- {polymo-1.4.1 → polymo-1.4.2}/src/polymo/codegen/templates/dp.py.jinja +0 -0
- {polymo-1.4.1 → polymo-1.4.2}/src/polymo/codegen/templating.py +0 -0
- {polymo-1.4.1 → polymo-1.4.2}/src/polymo/config.py +0 -0
- {polymo-1.4.1 → polymo-1.4.2}/src/polymo/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: polymo
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.2
|
|
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
|
|
@@ -20,7 +20,7 @@ from ..codegen import CodegenError, generate, generate_bundle
|
|
|
20
20
|
from ..codegen.generator import _identifier
|
|
21
21
|
from ..config import ConfigError, RestSourceConfig, config_to_dict, parse_config
|
|
22
22
|
from . import databricks
|
|
23
|
-
from .preview import run_preview
|
|
23
|
+
from .preview import _infer_field_types, infer_ddl_from_records, run_preview
|
|
24
24
|
|
|
25
25
|
if TYPE_CHECKING: # pragma: no cover - typing only
|
|
26
26
|
from pyspark.sql import SparkSession
|
|
@@ -215,11 +215,25 @@ def create_app() -> FastAPI:
|
|
|
215
215
|
records, dtypes = await run_in_threadpool(
|
|
216
216
|
partial(_collect_records, config, payload.token, payload.limit)
|
|
217
217
|
)
|
|
218
|
-
except Exception as exc:
|
|
218
|
+
except Exception as exc:
|
|
219
|
+
# The fetch itself already succeeded (raw_pages was collected
|
|
220
|
+
# above, before this records/dtypes step ever ran) — only the
|
|
221
|
+
# local dtypes-inference step failed. Surface that as a normal
|
|
222
|
+
# 200 with `rest_error` set rather than a 502, so the already
|
|
223
|
+
# -collected `raw_pages` aren't discarded: the UI's Raw API tab
|
|
224
|
+
# renders `raw_pages`/`rest_error` the same way it does for an
|
|
225
|
+
# actual fetch failure, and the prefix below makes clear to the
|
|
226
|
+
# user that the request itself worked.
|
|
219
227
|
detail = str(exc)
|
|
220
228
|
if needles:
|
|
221
229
|
detail = _redact_secret(detail, needles)
|
|
222
|
-
|
|
230
|
+
return SampleResponse(
|
|
231
|
+
stream=stream_config.name,
|
|
232
|
+
records=[],
|
|
233
|
+
dtypes=[],
|
|
234
|
+
raw_pages=raw_pages,
|
|
235
|
+
rest_error=f"records/schema step failed: {detail}",
|
|
236
|
+
)
|
|
223
237
|
|
|
224
238
|
if needles:
|
|
225
239
|
records = _redact_secret(records, needles)
|
|
@@ -634,7 +648,50 @@ def _get_preview_df(
|
|
|
634
648
|
"""
|
|
635
649
|
if schema_ddl:
|
|
636
650
|
return spark.createDataFrame(records, schema=schema_ddl)
|
|
637
|
-
|
|
651
|
+
|
|
652
|
+
# No explicit schema: don't hand `records` to Spark's own type
|
|
653
|
+
# inference. It can't cope with a column that is `None` in every
|
|
654
|
+
# sampled record (`[CANNOT_DETERMINE_TYPE]`) — common for XML APIs,
|
|
655
|
+
# where an always-empty element decodes to `None`. Instead derive a DDL
|
|
656
|
+
# ourselves with the same voting rules the generated script's
|
|
657
|
+
# `_infer_schema` uses (see `infer_ddl_from_records`), which always
|
|
658
|
+
# produces a type for every column seen (defaulting to STRING).
|
|
659
|
+
#
|
|
660
|
+
# A DDL-typed `createDataFrame` also requires every value to match its
|
|
661
|
+
# declared column type, so a dict/list value under an inferred STRING
|
|
662
|
+
# column would crash Spark's row conversion. Mirror the generated batch
|
|
663
|
+
# reader's `_cell` helper (`codegen/templates/dp.py.jinja`) and
|
|
664
|
+
# JSON-encode nested values first — only in this inferred-schema path;
|
|
665
|
+
# `records` itself (returned to the API response) is left untouched.
|
|
666
|
+
field_types = _infer_field_types(records)
|
|
667
|
+
if not field_types:
|
|
668
|
+
# Records exist but carry no fields at all (e.g. all `{}`) — no
|
|
669
|
+
# columns to declare a DDL for; fall back to letting Spark infer.
|
|
670
|
+
return spark.createDataFrame(records)
|
|
671
|
+
|
|
672
|
+
# Unlike Spark's own type inference, `createDataFrame(data, schema=...)`
|
|
673
|
+
# verifies each value strictly against its declared column type instead
|
|
674
|
+
# of upcasting — a plain Python `int` under a merged DOUBLE column (int
|
|
675
|
+
# in one sampled record, float in another) raises
|
|
676
|
+
# `[FIELD_DATA_TYPE_UNACCEPTABLE_WITH_NAME]` unless it's coerced to
|
|
677
|
+
# `float` first.
|
|
678
|
+
double_columns = {name for name, type_ in field_types.items() if type_ == "DOUBLE"}
|
|
679
|
+
safe_records = [
|
|
680
|
+
{
|
|
681
|
+
key: (
|
|
682
|
+
json.dumps(value)
|
|
683
|
+
if isinstance(value, (dict, list))
|
|
684
|
+
else float(value)
|
|
685
|
+
if key in double_columns
|
|
686
|
+
and isinstance(value, int)
|
|
687
|
+
and not isinstance(value, bool)
|
|
688
|
+
else value
|
|
689
|
+
)
|
|
690
|
+
for key, value in record.items()
|
|
691
|
+
}
|
|
692
|
+
for record in records
|
|
693
|
+
]
|
|
694
|
+
return spark.createDataFrame(safe_records, schema=infer_ddl_from_records(records))
|
|
638
695
|
|
|
639
696
|
|
|
640
697
|
def _get_or_create_spark() -> Any:
|
|
@@ -116,3 +116,61 @@ def run_preview(
|
|
|
116
116
|
error = str(exc)
|
|
117
117
|
|
|
118
118
|
return records, raw_pages, error
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _infer_field_types(records: List[Dict[str, Any]]) -> Dict[str, str]:
|
|
122
|
+
"""Vote a Spark type per column across `records`; the shared core of
|
|
123
|
+
`infer_ddl_from_records` (also used by `_get_preview_df` in `app.py` to
|
|
124
|
+
know which columns need numeric coercion before `createDataFrame`).
|
|
125
|
+
|
|
126
|
+
Mirrors `_infer_schema` in `codegen/templates/core.py.jinja` — keep the
|
|
127
|
+
two in sync. That template function is baked into every *generated*
|
|
128
|
+
script (so it can re-sample the live API at run time), while this one
|
|
129
|
+
runs against the preview records the builder UI already fetched.
|
|
130
|
+
|
|
131
|
+
One deliberate difference from the template: a column whose value is
|
|
132
|
+
`None` in every sampled record casts no type vote there, so the
|
|
133
|
+
template's `_infer_schema` just omits it from the DDL entirely (dropping
|
|
134
|
+
the column). That's fine for a generated script, which raises loudly if
|
|
135
|
+
sampling ever comes up completely empty. For the builder preview it
|
|
136
|
+
means the column silently vanishes from `dtypes` instead of showing up
|
|
137
|
+
as unknown/empty — worse, unqualified `spark.createDataFrame(records)`
|
|
138
|
+
(no schema at all) actually *crashes* with `[CANNOT_DETERMINE_TYPE]` in
|
|
139
|
+
this situation, which is the bug this function exists to fix (typical
|
|
140
|
+
for XML APIs, where an always-empty element decodes to `None` on every
|
|
141
|
+
record). So here, a no-vote column defaults to STRING instead of being
|
|
142
|
+
dropped.
|
|
143
|
+
"""
|
|
144
|
+
columns: Dict[str, Optional[str]] = {}
|
|
145
|
+
for record in records:
|
|
146
|
+
for key, value in record.items():
|
|
147
|
+
columns.setdefault(key, None)
|
|
148
|
+
if value is None:
|
|
149
|
+
continue
|
|
150
|
+
if isinstance(value, bool):
|
|
151
|
+
candidate = "BOOLEAN"
|
|
152
|
+
elif isinstance(value, int):
|
|
153
|
+
candidate = "BIGINT"
|
|
154
|
+
elif isinstance(value, float):
|
|
155
|
+
candidate = "DOUBLE"
|
|
156
|
+
else:
|
|
157
|
+
candidate = "STRING"
|
|
158
|
+
current = columns[key]
|
|
159
|
+
if current is None:
|
|
160
|
+
columns[key] = candidate
|
|
161
|
+
elif current == candidate:
|
|
162
|
+
pass
|
|
163
|
+
elif {current, candidate} == {"BIGINT", "DOUBLE"}:
|
|
164
|
+
columns[key] = "DOUBLE"
|
|
165
|
+
else:
|
|
166
|
+
columns[key] = "STRING"
|
|
167
|
+
return {name: type_ or "STRING" for name, type_ in columns.items()}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def infer_ddl_from_records(records: List[Dict[str, Any]]) -> str:
|
|
171
|
+
"""Infer a Spark DDL schema string from already-collected preview records.
|
|
172
|
+
|
|
173
|
+
See `_infer_field_types` for the voting rules this applies.
|
|
174
|
+
"""
|
|
175
|
+
fields = _infer_field_types(records)
|
|
176
|
+
return ", ".join(f"`{name}` {type_}" for name, type_ in fields.items())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{polymo-1.4.1 → polymo-1.4.2}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|