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.
Files changed (35) hide show
  1. {polymo-1.4.1 → polymo-1.4.2}/PKG-INFO +1 -1
  2. {polymo-1.4.1 → polymo-1.4.2}/pyproject.toml +1 -1
  3. {polymo-1.4.1 → polymo-1.4.2}/pyproject.toml.orig +1 -1
  4. {polymo-1.4.1 → polymo-1.4.2}/src/polymo/builder/app.py +61 -4
  5. {polymo-1.4.1 → polymo-1.4.2}/src/polymo/builder/preview.py +58 -0
  6. {polymo-1.4.1 → polymo-1.4.2}/README.md +0 -0
  7. {polymo-1.4.1 → polymo-1.4.2}/src/polymo/__init__.py +0 -0
  8. {polymo-1.4.1 → polymo-1.4.2}/src/polymo/builder/__init__.py +0 -0
  9. {polymo-1.4.1 → polymo-1.4.2}/src/polymo/builder/databricks.py +0 -0
  10. {polymo-1.4.1 → polymo-1.4.2}/src/polymo/builder/static/examples/countries.polymo.json +0 -0
  11. {polymo-1.4.1 → polymo-1.4.2}/src/polymo/builder/static/examples/githubrepos.polymo.json +0 -0
  12. {polymo-1.4.1 → polymo-1.4.2}/src/polymo/builder/static/examples/jsonplaceholder.polymo.json +0 -0
  13. {polymo-1.4.1 → polymo-1.4.2}/src/polymo/builder/static/examples/jsonplaceholder_endpoints.polymo.json +0 -0
  14. {polymo-1.4.1 → polymo-1.4.2}/src/polymo/builder/static/examples/weather.polymo.json +0 -0
  15. {polymo-1.4.1 → polymo-1.4.2}/src/polymo/builder/static/favicon.ico +0 -0
  16. {polymo-1.4.1 → polymo-1.4.2}/src/polymo/builder/static/favicon.png +0 -0
  17. {polymo-1.4.1 → polymo-1.4.2}/src/polymo/builder/static/index.html +0 -0
  18. {polymo-1.4.1 → polymo-1.4.2}/src/polymo/builder/static/logo.png +0 -0
  19. {polymo-1.4.1 → polymo-1.4.2}/src/polymo/builder/static/logo192.png +0 -0
  20. {polymo-1.4.1 → polymo-1.4.2}/src/polymo/builder/static/main.css +0 -0
  21. {polymo-1.4.1 → polymo-1.4.2}/src/polymo/builder/static/main.js +0 -0
  22. {polymo-1.4.1 → polymo-1.4.2}/src/polymo/builder/static/mockServiceWorker.js +0 -0
  23. {polymo-1.4.1 → polymo-1.4.2}/src/polymo/builder/templates/index.html +0 -0
  24. {polymo-1.4.1 → polymo-1.4.2}/src/polymo/cli.py +0 -0
  25. {polymo-1.4.1 → polymo-1.4.2}/src/polymo/codegen/__init__.py +0 -0
  26. {polymo-1.4.1 → polymo-1.4.2}/src/polymo/codegen/bundle.py +0 -0
  27. {polymo-1.4.1 → polymo-1.4.2}/src/polymo/codegen/generator.py +0 -0
  28. {polymo-1.4.1 → polymo-1.4.2}/src/polymo/codegen/templates/bundle/databricks.yml.jinja +0 -0
  29. {polymo-1.4.1 → polymo-1.4.2}/src/polymo/codegen/templates/bundle/pipeline.py.jinja +0 -0
  30. {polymo-1.4.1 → polymo-1.4.2}/src/polymo/codegen/templates/bundle/readme.md.jinja +0 -0
  31. {polymo-1.4.1 → polymo-1.4.2}/src/polymo/codegen/templates/core.py.jinja +0 -0
  32. {polymo-1.4.1 → polymo-1.4.2}/src/polymo/codegen/templates/dp.py.jinja +0 -0
  33. {polymo-1.4.1 → polymo-1.4.2}/src/polymo/codegen/templating.py +0 -0
  34. {polymo-1.4.1 → polymo-1.4.2}/src/polymo/config.py +0 -0
  35. {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.1
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "polymo"
3
- version = "1.4.1"
3
+ version = "1.4.2"
4
4
  description = "Generate standalone Lakeflow Declarative Pipelines scripts from a point-and-click REST connector builder"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "polymo"
3
- version = "1.4.1"
3
+ version = "1.4.2"
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 = [
@@ -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: # pragma: no cover - surfaced to UI
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
- raise HTTPException(status_code=502, detail=detail) from exc
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
- return spark.createDataFrame(records)
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