dataface 0.3.2.dev111__py3-none-any.whl → 0.3.2.dev150__py3-none-any.whl
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.
- dataface/DATAFACE_SYNTAX.md +15 -4
- dataface/agent_api/docs/yaml-reference.md +33 -133
- dataface/agent_api/serve.py +0 -2
- dataface/agent_api/validate.py +8 -0
- dataface/cli/commands/render.py +6 -0
- dataface/cli/commands/serve.py +0 -6
- dataface/cli/main.py +20 -14
- dataface/core/aliases.py +27 -0
- dataface/core/compile/__init__.py +0 -8
- dataface/core/compile/channel.py +0 -13
- dataface/core/compile/compiler.py +73 -71
- dataface/core/compile/introspection.py +2 -13
- dataface/core/compile/models/chart/authored/_base.py +1 -1
- dataface/core/compile/models/chart/normalized/__init__.py +2 -0
- dataface/core/compile/models/chart/normalized/_base.py +26 -0
- dataface/core/compile/models/config.py +5 -0
- dataface/core/compile/models/face/authored.py +24 -89
- dataface/core/compile/models/query/authored.py +1 -119
- dataface/core/compile/models/refs.py +0 -2
- dataface/core/compile/models/source.py +73 -159
- dataface/core/compile/models/style/theme/variables.py +0 -3
- dataface/core/compile/normalize_charts.py +4 -152
- dataface/core/compile/normalize_layout.py +20 -284
- dataface/core/compile/normalize_queries.py +3 -506
- dataface/core/compile/normalizer.py +4 -10
- dataface/core/compile/palette.py +19 -7
- dataface/core/compile/validator.py +121 -44
- dataface/core/compile/yaml_error_formatter.py +107 -2
- dataface/core/dashboard.py +3 -1
- dataface/core/defaults/default_config.yml +4 -0
- dataface/core/defaults/themes/_base.yaml +0 -1
- dataface/core/defaults/themes/editorial.yaml +3 -0
- dataface/core/defaults/themes/stark.yaml +8 -0
- dataface/core/diagnostics/__init__.py +10 -0
- dataface/core/diagnostics/codes_compile.py +4 -2
- dataface/core/diagnostics/codes_execute.py +70 -0
- dataface/core/diagnostics/codes_render.py +58 -4
- dataface/core/execute/adapters/__init__.py +1 -1
- dataface/core/execute/cache_keys.py +4 -9
- dataface/core/execute/executor.py +25 -3
- dataface/core/execute/file_source_materializer.py +185 -32
- dataface/core/execute/trivial_local_cache.py +5 -2
- dataface/core/inspect/__init__.py +0 -1
- dataface/core/project.py +6 -2
- dataface/core/render/chart/mark_extents.py +339 -0
- dataface/core/render/chart/rendering.py +97 -34
- dataface/core/render/conditions.py +122 -0
- dataface/core/render/controls.py +60 -187
- dataface/core/render/data_format.py +1 -1
- dataface/core/render/face_resolve.py +7 -0
- dataface/core/render/faces.py +94 -30
- dataface/core/render/layout_sizing.py +46 -0
- dataface/core/render/renderer.py +14 -1
- dataface/core/render/sizing.py +42 -105
- dataface/core/render/svg_utils.py +60 -14
- dataface/core/render/terminal.py +1 -1
- dataface/core/render/variables_layout.py +211 -0
- dataface/core/render/variables_resolve.py +376 -0
- dataface/core/render/variables_strip.py +291 -368
- dataface/core/render/warnings/base.py +18 -0
- dataface/core/render/warnings/layout_min_exceeds_height.py +68 -0
- dataface/core/render/warnings/registry.py +2 -0
- dataface/core/serve/alias_index.py +16 -22
- dataface/data/schemas/board-resolved/board-resolved.schema.json +1 -499
- {dataface-0.3.2.dev111.dist-info → dataface-0.3.2.dev150.dist-info}/METADATA +1 -3
- {dataface-0.3.2.dev111.dist-info → dataface-0.3.2.dev150.dist-info}/RECORD +69 -67
- dataface/core/inspect/partials/categorical.yml +0 -38
- dataface/core/inspect/partials/date.yml +0 -38
- dataface/core/inspect/partials/numeric.yml +0 -53
- dataface/core/inspect/templates/charts.yml +0 -94
- {dataface-0.3.2.dev111.dist-info → dataface-0.3.2.dev150.dist-info}/WHEEL +0 -0
- {dataface-0.3.2.dev111.dist-info → dataface-0.3.2.dev150.dist-info}/entry_points.txt +0 -0
- {dataface-0.3.2.dev111.dist-info → dataface-0.3.2.dev150.dist-info}/licenses/LICENSE +0 -0
|
@@ -111,29 +111,14 @@ def reject_credential_literals(source_name: str, raw_config: Mapping[str, Any])
|
|
|
111
111
|
)
|
|
112
112
|
|
|
113
113
|
|
|
114
|
-
def _validate_relative_path(class_name: str, field_label: str, value: str) -> str:
|
|
115
|
-
"""Reject an empty, absolute, or root-escaping project-relative path.
|
|
116
|
-
|
|
117
|
-
Delegates the escape rule to ``assert_relpath`` — the single project-relative
|
|
118
|
-
path guard — and re-raises with the class/field context. Shared by every field
|
|
119
|
-
that stores a project-relative filesystem path (LookML model/lookml_path fields
|
|
120
|
-
today).
|
|
121
|
-
"""
|
|
122
|
-
try:
|
|
123
|
-
assert_relpath(value)
|
|
124
|
-
except ValueError as e:
|
|
125
|
-
raise ValueError(f"{class_name}: {field_label} {e}") from e
|
|
126
|
-
return value
|
|
127
|
-
|
|
128
|
-
|
|
129
114
|
def _validate_files_mapping(
|
|
130
|
-
class_name: str, files: dict[str, str], expected_extension: str
|
|
115
|
+
class_name: str, files: dict[str, str], expected_extension: str | tuple[str, ...]
|
|
131
116
|
) -> None:
|
|
132
117
|
"""Validate all keys and values in a file source's ``files:`` mapping.
|
|
133
118
|
|
|
134
119
|
Keys must be valid SQL identifiers (letters, digits, underscores; no
|
|
135
120
|
leading digit). Values must be non-empty relative POSIX paths that do not
|
|
136
|
-
escape above the project root after normalization, and must end in
|
|
121
|
+
escape above the project root after normalization, and must end in one of
|
|
137
122
|
``expected_extension`` (case-insensitive) — parsing dispatches on the
|
|
138
123
|
declared source ``type``, not the file extension, so this is what
|
|
139
124
|
guarantees every servable data file's extension matches its type (and
|
|
@@ -143,12 +128,18 @@ def _validate_files_mapping(
|
|
|
143
128
|
Args:
|
|
144
129
|
class_name: Source config class name (for error messages).
|
|
145
130
|
files: The ``files:`` mapping from the authored YAML.
|
|
146
|
-
expected_extension: The canonical extension
|
|
147
|
-
(e.g. ``".csv"``
|
|
131
|
+
expected_extension: The canonical extension or tuple of extensions for
|
|
132
|
+
this source type (e.g. ``".csv"`` or ``(".json", ".jsonl")``),
|
|
133
|
+
including the leading dot.
|
|
148
134
|
|
|
149
135
|
Raises:
|
|
150
136
|
ValueError: On an invalid key, path value, or extension mismatch.
|
|
151
137
|
"""
|
|
138
|
+
exts = (
|
|
139
|
+
(expected_extension,)
|
|
140
|
+
if isinstance(expected_extension, str)
|
|
141
|
+
else expected_extension
|
|
142
|
+
)
|
|
152
143
|
for table_name, file_path in files.items():
|
|
153
144
|
if not _SQL_IDENTIFIER_RE.match(table_name):
|
|
154
145
|
raise ValueError(
|
|
@@ -160,10 +151,11 @@ def _validate_files_mapping(
|
|
|
160
151
|
assert_relpath(file_path)
|
|
161
152
|
except ValueError as e:
|
|
162
153
|
raise ValueError(f"{class_name}: files[{table_name!r}] {e}") from e
|
|
163
|
-
if not file_path.lower().endswith(
|
|
154
|
+
if not any(file_path.lower().endswith(ext) for ext in exts):
|
|
155
|
+
ext_msg = repr(exts[0]) if len(exts) == 1 else f"one of {list(exts)}"
|
|
164
156
|
raise ValueError(
|
|
165
157
|
f"{class_name}: files[{table_name!r}] path {file_path!r} must end in "
|
|
166
|
-
f"{
|
|
158
|
+
f"{ext_msg} to match source type."
|
|
167
159
|
)
|
|
168
160
|
|
|
169
161
|
|
|
@@ -373,125 +365,12 @@ class DuckDBRetriesConfig(BaseModel):
|
|
|
373
365
|
] = Field(default_factory=_default_retryable_exceptions)
|
|
374
366
|
|
|
375
367
|
|
|
376
|
-
# ============================================================================
|
|
377
|
-
# LOOKML MODELS (nested under warehouse sources)
|
|
378
|
-
# ============================================================================
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
class LookMLModelConfig(BaseModel):
|
|
382
|
-
"""One LookML model: a named collection of explores under a warehouse source.
|
|
383
|
-
|
|
384
|
-
Exactly one selector chooses which explores belong to the collection:
|
|
385
|
-
|
|
386
|
-
- ``lkml``: a specific ``.lkml`` model file. Validated as a relative path
|
|
387
|
-
that does not escape the project root. File-scoping within likeml is not
|
|
388
|
-
yet implemented, so using this selector raises a clear error at compile
|
|
389
|
-
time directing authors to use an explicit ``explores:`` list instead.
|
|
390
|
-
Disk existence is not checked at parse time.
|
|
391
|
-
- ``group_label``: explores tagged with this label in LookML. Accepted at
|
|
392
|
-
parse time but likeml carries no group_label metadata yet, so using it
|
|
393
|
-
raises a clear error at compile time.
|
|
394
|
-
- ``explores``: an explicit list of explore names. The only selector
|
|
395
|
-
enforced at compile time: an explore not in the list is rejected.
|
|
396
|
-
"""
|
|
397
|
-
|
|
398
|
-
model_config = ConfigDict(extra="forbid")
|
|
399
|
-
|
|
400
|
-
lkml: str | None = Field(
|
|
401
|
-
default=None,
|
|
402
|
-
description="Path to a specific .lkml model file, relative to the project root.",
|
|
403
|
-
)
|
|
404
|
-
group_label: str | None = Field(
|
|
405
|
-
default=None,
|
|
406
|
-
description="LookML group_label selecting this model's explores. Not yet enforced at compile time.",
|
|
407
|
-
)
|
|
408
|
-
explores: list[str] | None = Field(
|
|
409
|
-
default=None, description="Explicit list of explore names in this model."
|
|
410
|
-
)
|
|
411
|
-
|
|
412
|
-
@field_validator("lkml")
|
|
413
|
-
@classmethod
|
|
414
|
-
def _validate_lkml(cls, v: str | None) -> str | None:
|
|
415
|
-
if v is None:
|
|
416
|
-
return v
|
|
417
|
-
return _validate_relative_path("LookMLModelConfig", "lkml", v)
|
|
418
|
-
|
|
419
|
-
@model_validator(mode="after")
|
|
420
|
-
def _require_one_selector(self) -> LookMLModelConfig:
|
|
421
|
-
selectors = [
|
|
422
|
-
name
|
|
423
|
-
for name, value in (
|
|
424
|
-
("lkml", self.lkml),
|
|
425
|
-
("group_label", self.group_label),
|
|
426
|
-
("explores", self.explores),
|
|
427
|
-
)
|
|
428
|
-
if value
|
|
429
|
-
]
|
|
430
|
-
if len(selectors) != 1:
|
|
431
|
-
got = ", ".join(selectors) if selectors else "none"
|
|
432
|
-
raise ValueError(
|
|
433
|
-
"LookMLModelConfig must set exactly one of 'lkml', 'group_label', "
|
|
434
|
-
f"'explores' (got {got})"
|
|
435
|
-
)
|
|
436
|
-
return self
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
class LookMLModelsMixin(BaseModel):
|
|
440
|
-
"""Shared nested ``models:`` field for warehouse sources with LookML explores.
|
|
441
|
-
|
|
442
|
-
A warehouse source with ``models:`` also declares ``lookml_path`` — the
|
|
443
|
-
directory of ``.lkml`` files the models' explores are parsed from. Each
|
|
444
|
-
warehouse source owns its own nested ``models:`` map; a model name must be
|
|
445
|
-
unique across the project (one model = one warehouse connection).
|
|
446
|
-
|
|
447
|
-
Example:
|
|
448
|
-
sources:
|
|
449
|
-
bigquery_prod:
|
|
450
|
-
type: bigquery
|
|
451
|
-
project: digital-arbor-400
|
|
452
|
-
dataset: transforms_bi
|
|
453
|
-
lookml_path: lkml
|
|
454
|
-
models:
|
|
455
|
-
finance: {lkml: fivetran.model.lkml}
|
|
456
|
-
core_kpis: {explores: [revenue, goals]}
|
|
457
|
-
"""
|
|
458
|
-
|
|
459
|
-
model_config = ConfigDict(extra="forbid")
|
|
460
|
-
|
|
461
|
-
lookml_path: str | None = Field(
|
|
462
|
-
default=None,
|
|
463
|
-
description="Relative path to the directory of .lkml files backing this source's models.",
|
|
464
|
-
)
|
|
465
|
-
models: dict[str, LookMLModelConfig] = Field(
|
|
466
|
-
default_factory=dict,
|
|
467
|
-
description="Named LookML model collections nested under this source.",
|
|
468
|
-
)
|
|
469
|
-
|
|
470
|
-
@field_validator("lookml_path")
|
|
471
|
-
@classmethod
|
|
472
|
-
def _validate_lookml_path(cls, v: str | None) -> str | None:
|
|
473
|
-
if v is None:
|
|
474
|
-
return v
|
|
475
|
-
return _validate_relative_path("LookMLModelsMixin", "lookml_path", v)
|
|
476
|
-
|
|
477
|
-
@model_validator(mode="after")
|
|
478
|
-
def _require_lookml_path_with_models(self) -> LookMLModelsMixin:
|
|
479
|
-
if self.models and not self.lookml_path:
|
|
480
|
-
raise ValueError(
|
|
481
|
-
"'lookml_path' is required when 'models' is set — it names the "
|
|
482
|
-
"directory of .lkml files the models' explores are parsed from."
|
|
483
|
-
)
|
|
484
|
-
return self
|
|
485
|
-
|
|
486
|
-
|
|
487
368
|
# ============================================================================
|
|
488
369
|
# DATABASE SOURCE CONFIGS
|
|
489
370
|
# ============================================================================
|
|
490
371
|
|
|
491
372
|
|
|
492
|
-
class PostgresSourceConfig(
|
|
493
|
-
AttributedSourceConfig, LookMLModelsMixin, DatabaseSourceConfig
|
|
494
|
-
):
|
|
373
|
+
class PostgresSourceConfig(AttributedSourceConfig, DatabaseSourceConfig):
|
|
495
374
|
"""Postgres source configuration.
|
|
496
375
|
|
|
497
376
|
Field names match dbt profiles.yml exactly.
|
|
@@ -567,9 +446,7 @@ class PostgresSourceConfig(
|
|
|
567
446
|
] = 1
|
|
568
447
|
|
|
569
448
|
|
|
570
|
-
class SnowflakeSourceConfig(
|
|
571
|
-
AttributedSourceConfig, LookMLModelsMixin, DatabaseSourceConfig
|
|
572
|
-
):
|
|
449
|
+
class SnowflakeSourceConfig(AttributedSourceConfig, DatabaseSourceConfig):
|
|
573
450
|
"""Snowflake source configuration.
|
|
574
451
|
|
|
575
452
|
Field names match dbt profiles.yml exactly.
|
|
@@ -702,9 +579,7 @@ class SnowflakeSourceConfig(
|
|
|
702
579
|
] = 0.0
|
|
703
580
|
|
|
704
581
|
|
|
705
|
-
class BigQuerySourceConfig(
|
|
706
|
-
AttributedSourceConfig, LookMLModelsMixin, DatabaseSourceConfig
|
|
707
|
-
):
|
|
582
|
+
class BigQuerySourceConfig(AttributedSourceConfig, DatabaseSourceConfig):
|
|
708
583
|
"""BigQuery source configuration.
|
|
709
584
|
|
|
710
585
|
Field names match dbt profiles.yml exactly.
|
|
@@ -870,9 +745,7 @@ class BigQuerySourceConfig(
|
|
|
870
745
|
return self
|
|
871
746
|
|
|
872
747
|
|
|
873
|
-
class RedshiftSourceConfig(
|
|
874
|
-
AttributedSourceConfig, LookMLModelsMixin, DatabaseSourceConfig
|
|
875
|
-
):
|
|
748
|
+
class RedshiftSourceConfig(AttributedSourceConfig, DatabaseSourceConfig):
|
|
876
749
|
"""Redshift source configuration.
|
|
877
750
|
|
|
878
751
|
Field names match dbt profiles.yml exactly, except `autocommit`: Dataface
|
|
@@ -1025,9 +898,7 @@ class RedshiftSourceConfig(
|
|
|
1025
898
|
] = None
|
|
1026
899
|
|
|
1027
900
|
|
|
1028
|
-
class MySQLSourceConfig(
|
|
1029
|
-
AttributedSourceConfig, LookMLModelsMixin, DatabaseSourceConfig
|
|
1030
|
-
):
|
|
901
|
+
class MySQLSourceConfig(AttributedSourceConfig, DatabaseSourceConfig):
|
|
1031
902
|
"""MySQL source configuration.
|
|
1032
903
|
|
|
1033
904
|
Field names match dbt profiles.yml exactly.
|
|
@@ -1048,9 +919,7 @@ class MySQLSourceConfig(
|
|
|
1048
919
|
password: str = Field(description="MySQL password.")
|
|
1049
920
|
|
|
1050
921
|
|
|
1051
|
-
class TrinoSourceConfig(
|
|
1052
|
-
AttributedSourceConfig, LookMLModelsMixin, DatabaseSourceConfig
|
|
1053
|
-
):
|
|
922
|
+
class TrinoSourceConfig(AttributedSourceConfig, DatabaseSourceConfig):
|
|
1054
923
|
"""Trino / Presto source configuration.
|
|
1055
924
|
|
|
1056
925
|
`database` names the Trino catalog; dbt-trino aliases `catalog` → `database`,
|
|
@@ -1195,7 +1064,7 @@ class TrinoSourceConfig(
|
|
|
1195
1064
|
] = None
|
|
1196
1065
|
|
|
1197
1066
|
|
|
1198
|
-
class DuckDBSourceConfig(
|
|
1067
|
+
class DuckDBSourceConfig(DatabaseSourceConfig):
|
|
1199
1068
|
"""DuckDB source configuration.
|
|
1200
1069
|
|
|
1201
1070
|
Read-only posture: handled in-process. DuckDBAdapter opens file-based DuckDB
|
|
@@ -1326,7 +1195,7 @@ class DuckDBSourceConfig(LookMLModelsMixin, DatabaseSourceConfig):
|
|
|
1326
1195
|
return data
|
|
1327
1196
|
|
|
1328
1197
|
|
|
1329
|
-
class SQLiteSourceConfig(BaseSourceConfig
|
|
1198
|
+
class SQLiteSourceConfig(BaseSourceConfig):
|
|
1330
1199
|
"""SQLite source configuration.
|
|
1331
1200
|
|
|
1332
1201
|
Read-only posture: handled in-process. SqliteAdapter opens SQLite connections
|
|
@@ -1354,6 +1223,9 @@ class CsvSourceConfig(BaseSourceConfig):
|
|
|
1354
1223
|
|
|
1355
1224
|
A file source is a namespace of relations: each key in `files` is a table
|
|
1356
1225
|
name; the value is the path to the CSV file (relative to the project root).
|
|
1226
|
+
Glob patterns (``*`` and ``?``) are supported: every matched file is loaded
|
|
1227
|
+
and its rows are concatenated into one table. All matched files must share
|
|
1228
|
+
the same column schema. A glob that matches no files is an error.
|
|
1357
1229
|
|
|
1358
1230
|
Example:
|
|
1359
1231
|
sources:
|
|
@@ -1362,14 +1234,22 @@ class CsvSourceConfig(BaseSourceConfig):
|
|
|
1362
1234
|
files:
|
|
1363
1235
|
sales: assets/data/sales.csv
|
|
1364
1236
|
returns: assets/data/returns.csv
|
|
1237
|
+
monthly: assets/data/monthly_*.csv
|
|
1365
1238
|
"""
|
|
1366
1239
|
|
|
1367
1240
|
source_category: ClassVar[str] = "file"
|
|
1368
1241
|
type: Literal["csv"] = Field(description="Source type identifier.")
|
|
1369
1242
|
files: dict[str, str] = Field(
|
|
1370
1243
|
description=(
|
|
1371
|
-
"Mapping of table_name →
|
|
1372
|
-
"Each key is the SQL table name
|
|
1244
|
+
"Mapping of table_name → relative path or glob pattern. "
|
|
1245
|
+
"Each key is the SQL table name. "
|
|
1246
|
+
"The value is a path relative to the project root, or a glob "
|
|
1247
|
+
"pattern using ``*`` (matches within a directory) and ``?`` "
|
|
1248
|
+
"(matches one character). "
|
|
1249
|
+
"A glob must match at least one file; all matched files must share "
|
|
1250
|
+
"the same column schema and are concatenated into one table. "
|
|
1251
|
+
"The fan-out limit is ``execution.max_glob_file_count`` in "
|
|
1252
|
+
"``dataface.yml`` (default 1000). "
|
|
1373
1253
|
"Required and must not be empty."
|
|
1374
1254
|
)
|
|
1375
1255
|
)
|
|
@@ -1392,6 +1272,9 @@ class ParquetSourceConfig(BaseSourceConfig):
|
|
|
1392
1272
|
|
|
1393
1273
|
A file source is a namespace of relations: each key in `files` is a table
|
|
1394
1274
|
name; the value is the path to the Parquet file (relative to the project root).
|
|
1275
|
+
Glob patterns (``*`` and ``?``) are supported: every matched file is loaded
|
|
1276
|
+
and its rows are concatenated into one table. All matched files must share
|
|
1277
|
+
the same column schema. A glob that matches no files is an error.
|
|
1395
1278
|
|
|
1396
1279
|
Example:
|
|
1397
1280
|
sources:
|
|
@@ -1399,14 +1282,22 @@ class ParquetSourceConfig(BaseSourceConfig):
|
|
|
1399
1282
|
type: parquet
|
|
1400
1283
|
files:
|
|
1401
1284
|
events: assets/data/events.parquet
|
|
1285
|
+
archive: assets/data/archive_*.parquet
|
|
1402
1286
|
"""
|
|
1403
1287
|
|
|
1404
1288
|
source_category: ClassVar[str] = "file"
|
|
1405
1289
|
type: Literal["parquet"] = Field(description="Source type identifier.")
|
|
1406
1290
|
files: dict[str, str] = Field(
|
|
1407
1291
|
description=(
|
|
1408
|
-
"Mapping of table_name →
|
|
1409
|
-
"Each key is the SQL table name
|
|
1292
|
+
"Mapping of table_name → relative path or glob pattern. "
|
|
1293
|
+
"Each key is the SQL table name. "
|
|
1294
|
+
"The value is a path relative to the project root, or a glob "
|
|
1295
|
+
"pattern using ``*`` (matches within a directory) and ``?`` "
|
|
1296
|
+
"(matches one character). "
|
|
1297
|
+
"A glob must match at least one file; all matched files must share "
|
|
1298
|
+
"the same column schema and are concatenated into one table. "
|
|
1299
|
+
"The fan-out limit is ``execution.max_glob_file_count`` in "
|
|
1300
|
+
"``dataface.yml`` (default 1000). "
|
|
1410
1301
|
"Required and must not be empty."
|
|
1411
1302
|
)
|
|
1412
1303
|
)
|
|
@@ -1427,6 +1318,12 @@ class JsonSourceConfig(BaseSourceConfig):
|
|
|
1427
1318
|
|
|
1428
1319
|
A file source is a namespace of relations: each key in `files` is a table
|
|
1429
1320
|
name; the value is the path to the JSON file (relative to the project root).
|
|
1321
|
+
Both ``.json`` and ``.jsonl`` (newline-delimited JSON) extensions are accepted;
|
|
1322
|
+
the parser is chosen by extension: ``.json`` → standard JSON array/object,
|
|
1323
|
+
``.jsonl`` → newline-delimited JSON (one object per line).
|
|
1324
|
+
Glob patterns (``*`` and ``?``) are supported: every matched file is loaded
|
|
1325
|
+
and its rows are concatenated into one table. A glob that matches no files
|
|
1326
|
+
is an error.
|
|
1430
1327
|
|
|
1431
1328
|
Example:
|
|
1432
1329
|
sources:
|
|
@@ -1434,17 +1331,34 @@ class JsonSourceConfig(BaseSourceConfig):
|
|
|
1434
1331
|
type: json
|
|
1435
1332
|
files:
|
|
1436
1333
|
products: assets/data/products.json
|
|
1334
|
+
events: assets/data/events_*.jsonl
|
|
1437
1335
|
"""
|
|
1438
1336
|
|
|
1439
1337
|
source_category: ClassVar[str] = "file"
|
|
1440
1338
|
type: Literal["json"] = Field(description="Source type identifier.")
|
|
1441
1339
|
files: dict[str, str] = Field(
|
|
1442
1340
|
description=(
|
|
1443
|
-
"Mapping of table_name →
|
|
1444
|
-
"Each key is the SQL table name
|
|
1341
|
+
"Mapping of table_name → relative path or glob pattern. "
|
|
1342
|
+
"Each key is the SQL table name. "
|
|
1343
|
+
"The value is a path relative to the project root, or a glob "
|
|
1344
|
+
"pattern using ``*`` (matches within a directory) and ``?`` "
|
|
1345
|
+
"(matches one character). "
|
|
1346
|
+
"A glob must match at least one file and all matched files are "
|
|
1347
|
+
"concatenated into one table. "
|
|
1348
|
+
"The fan-out limit is ``execution.max_glob_file_count`` in "
|
|
1349
|
+
"``dataface.yml`` (default 1000). "
|
|
1445
1350
|
"Required and must not be empty."
|
|
1446
1351
|
)
|
|
1447
1352
|
)
|
|
1353
|
+
union_by_name: bool = Field(
|
|
1354
|
+
default=False,
|
|
1355
|
+
description=(
|
|
1356
|
+
"When True, rows from different files in a glob set are unified by "
|
|
1357
|
+
"column name: columns absent in a particular file are filled with "
|
|
1358
|
+
"NULL. When False (default), all files in a glob set must share the "
|
|
1359
|
+
"same column schema — a mismatch raises an error."
|
|
1360
|
+
),
|
|
1361
|
+
)
|
|
1448
1362
|
|
|
1449
1363
|
@model_validator(mode="after")
|
|
1450
1364
|
def _require_nonempty_files(self) -> JsonSourceConfig:
|
|
@@ -1453,7 +1367,7 @@ class JsonSourceConfig(BaseSourceConfig):
|
|
|
1453
1367
|
"JsonSourceConfig: files must not be empty — "
|
|
1454
1368
|
"provide at least one table_name → path entry."
|
|
1455
1369
|
)
|
|
1456
|
-
_validate_files_mapping("JsonSourceConfig", self.files, ".json")
|
|
1370
|
+
_validate_files_mapping("JsonSourceConfig", self.files, (".json", ".jsonl"))
|
|
1457
1371
|
return self
|
|
1458
1372
|
|
|
1459
1373
|
|
|
@@ -107,9 +107,6 @@ class VariablesStyle(BaseModel):
|
|
|
107
107
|
placeholder: VariablesPlaceholderStyle = Field(
|
|
108
108
|
description="Style for unselected/hint text in variable inputs."
|
|
109
109
|
)
|
|
110
|
-
line_height: float = Field(
|
|
111
|
-
description="Line height for variable control text in pixels."
|
|
112
|
-
)
|
|
113
110
|
padding: float = Field(
|
|
114
111
|
description="Outer padding around the variables panel in pixels."
|
|
115
112
|
)
|
|
@@ -14,11 +14,9 @@ from typing import TYPE_CHECKING, Any, get_args
|
|
|
14
14
|
if TYPE_CHECKING:
|
|
15
15
|
from dataface.core.project import ProjectDirectory
|
|
16
16
|
|
|
17
|
-
from collections.abc import Callable
|
|
18
17
|
|
|
19
18
|
from pydantic import BaseModel, TypeAdapter, ValidationError
|
|
20
19
|
|
|
21
|
-
from dataface.core.compile.channel import is_dotted_lookml_ref
|
|
22
20
|
from dataface.core.compile.errors import CompilationError, ReferenceError
|
|
23
21
|
from dataface.core.compile.jinja import extract_variable_dependencies
|
|
24
22
|
from dataface.core.compile.models.cache import INHERIT_CACHE, CachePatch
|
|
@@ -46,8 +44,6 @@ from dataface.core.compile.models.face.authored import QueryOrRef
|
|
|
46
44
|
from dataface.core.compile.models.face.normalized import Layout
|
|
47
45
|
from dataface.core.compile.models.primitives import FormatConfig
|
|
48
46
|
from dataface.core.compile.models.query.authored import (
|
|
49
|
-
AuthoredLookmlMergeQuery,
|
|
50
|
-
AuthoredLookmlQuery,
|
|
51
47
|
AuthoredMetricflowQuery,
|
|
52
48
|
TimeGrain,
|
|
53
49
|
_BaseQueryFields,
|
|
@@ -661,8 +657,8 @@ def _collect_charts_from_layout(
|
|
|
661
657
|
|
|
662
658
|
# Chart keys that carry data-binding field references (axis, encoding, geo, etc.).
|
|
663
659
|
# Presentation-only keys (title, subtitle, description, link, format, ...) are
|
|
664
|
-
# excluded — a dotted string in those positions is not a
|
|
665
|
-
|
|
660
|
+
# excluded — a dotted string in those positions is not a field ref.
|
|
661
|
+
_FIELD_CHANNEL_KEYS: frozenset[str] = frozenset(
|
|
666
662
|
{
|
|
667
663
|
"x",
|
|
668
664
|
"y",
|
|
@@ -681,150 +677,6 @@ _LOOKML_CHANNEL_KEYS: frozenset[str] = frozenset(
|
|
|
681
677
|
)
|
|
682
678
|
|
|
683
679
|
|
|
684
|
-
def _rewrite_channel_value(
|
|
685
|
-
value: object, collected: set[str], channel_alias: Callable[[str, str], str]
|
|
686
|
-
) -> object:
|
|
687
|
-
"""Rewrite dotted `view.field` refs in a channel value to their SQL alias.
|
|
688
|
-
|
|
689
|
-
A channel is a scalar ref OR a list of refs (multi-series `y: [a, b]`).
|
|
690
|
-
Every dotted ref in either shape is collected+rewritten — a list-valued
|
|
691
|
-
channel whose refs went uncollected would silently drop those measures
|
|
692
|
-
from the query while reporting success.
|
|
693
|
-
|
|
694
|
-
Returns the rewritten value, or None if `value` had no dotted ref to
|
|
695
|
-
rewrite (including when `value` itself is None).
|
|
696
|
-
"""
|
|
697
|
-
if value is None:
|
|
698
|
-
return None
|
|
699
|
-
items = value if isinstance(value, list) else [value]
|
|
700
|
-
rewritten: list[object] = []
|
|
701
|
-
changed = False
|
|
702
|
-
for item in items:
|
|
703
|
-
if isinstance(item, str) and is_dotted_lookml_ref(item):
|
|
704
|
-
collected.add(item)
|
|
705
|
-
view, field = item.split(".", 1)
|
|
706
|
-
rewritten.append(channel_alias(view, field))
|
|
707
|
-
changed = True
|
|
708
|
-
else:
|
|
709
|
-
rewritten.append(item)
|
|
710
|
-
if not changed:
|
|
711
|
-
return None
|
|
712
|
-
return rewritten if isinstance(value, list) else rewritten[0]
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
def collect_lookml_channel_refs(
|
|
716
|
-
charts: dict[str, Any],
|
|
717
|
-
query_registry: dict[str, QueryOrRef],
|
|
718
|
-
lookml_model_names: set[str],
|
|
719
|
-
) -> None:
|
|
720
|
-
"""Collect dotted view.field chart-channel refs into fields-less lookml queries.
|
|
721
|
-
|
|
722
|
-
A lookml query may omit `fields:`; authors instead name fields by dotted
|
|
723
|
-
`view.field` ref directly in chart channel positions (x, y, color, theta,
|
|
724
|
-
value, ...). This pass finds every named lookml query with no `fields`,
|
|
725
|
-
gathers the dotted refs from charts bound to it, injects them as the
|
|
726
|
-
query's `fields`, and rewrites each chart's channel value to the
|
|
727
|
-
resulting `view_field` alias so it matches the lowered SQL's output
|
|
728
|
-
column (the standard T1.2 lowering pass runs after this, unaware that
|
|
729
|
-
`fields` was collected rather than authored).
|
|
730
|
-
|
|
731
|
-
Must run before query normalization (which lowers lookml queries to
|
|
732
|
-
SqlQuery) and only sees named queries — inline per-chart queries own
|
|
733
|
-
their own `fields` and are not eligible for collection.
|
|
734
|
-
|
|
735
|
-
Raises:
|
|
736
|
-
CompilationError: A lookml query has no `fields` and no chart bound
|
|
737
|
-
to it (by name) uses a dotted-ref channel.
|
|
738
|
-
"""
|
|
739
|
-
try:
|
|
740
|
-
# Lazy import: likeml is optional (pip install dataface[lookml]).
|
|
741
|
-
# It is an internal workspace package and not published to PyPI.
|
|
742
|
-
from likeml import channel_alias # noqa: PLC0415
|
|
743
|
-
except ImportError as e:
|
|
744
|
-
raise CompilationError(
|
|
745
|
-
"likeml is not installed. Install with: pip install dataface[lookml]"
|
|
746
|
-
) from e
|
|
747
|
-
|
|
748
|
-
for query_name, query_def in query_registry.items():
|
|
749
|
-
if not isinstance(query_def, (AuthoredLookmlQuery, AuthoredLookmlMergeQuery)):
|
|
750
|
-
continue
|
|
751
|
-
# source may be an inline dict (unhashable) — a `dict not in set` membership
|
|
752
|
-
# test would raise TypeError, so gate on str first. A non-str source is never
|
|
753
|
-
# a lookml source ref, so skip it.
|
|
754
|
-
if not isinstance(query_def.source, str):
|
|
755
|
-
continue
|
|
756
|
-
if query_def.source not in lookml_model_names:
|
|
757
|
-
continue
|
|
758
|
-
if query_def.fields:
|
|
759
|
-
continue
|
|
760
|
-
|
|
761
|
-
collected: set[str] = set()
|
|
762
|
-
for chart_def in charts.values():
|
|
763
|
-
is_model = isinstance(chart_def, BaseModel)
|
|
764
|
-
chart_dict = (
|
|
765
|
-
chart_def.model_dump(exclude_none=True) if is_model else chart_def
|
|
766
|
-
)
|
|
767
|
-
bound_to_query = chart_dict.get("query") == query_name
|
|
768
|
-
if bound_to_query:
|
|
769
|
-
for field_name, value in chart_dict.items():
|
|
770
|
-
if field_name not in _LOOKML_CHANNEL_KEYS:
|
|
771
|
-
continue
|
|
772
|
-
new_value = _rewrite_channel_value(value, collected, channel_alias)
|
|
773
|
-
if new_value is None:
|
|
774
|
-
continue
|
|
775
|
-
if is_model:
|
|
776
|
-
setattr(chart_def, field_name, new_value)
|
|
777
|
-
else:
|
|
778
|
-
chart_dict[field_name] = new_value
|
|
779
|
-
|
|
780
|
-
# Layered charts carry their own channels inside `layers[]`, one query
|
|
781
|
-
# binding per layer (falls back to the chart-level query when a layer
|
|
782
|
-
# omits its own). Each layer is walked independently, mutating the
|
|
783
|
-
# actual Layer object (model or dict) in place, not the model_dump()
|
|
784
|
-
# copy — model_dump() flattens `layers` into plain dicts, so writes must
|
|
785
|
-
# go through the real chart_def.layers list to reach the pipeline.
|
|
786
|
-
layers = chart_dict.get("layers")
|
|
787
|
-
if not layers:
|
|
788
|
-
continue
|
|
789
|
-
# Use the live typed layer objects from the model so setattr writes
|
|
790
|
-
# propagate (model_dump() produces plain dicts — mutations to those are lost).
|
|
791
|
-
# `layers` is not on all chart types, so getattr is appropriate.
|
|
792
|
-
model_layers: list[CartesianLayer] | None = (
|
|
793
|
-
getattr(chart_def, "layers", None) if is_model else None
|
|
794
|
-
)
|
|
795
|
-
real_layers: list[CartesianLayer] | list[dict[str, Any]] = (
|
|
796
|
-
model_layers if model_layers else layers
|
|
797
|
-
)
|
|
798
|
-
for idx, layer_dict in enumerate(layers):
|
|
799
|
-
layer_query_name = layer_dict.get("query")
|
|
800
|
-
if layer_query_name is None:
|
|
801
|
-
if not bound_to_query:
|
|
802
|
-
continue
|
|
803
|
-
elif layer_query_name != query_name:
|
|
804
|
-
continue
|
|
805
|
-
real_layer = real_layers[idx]
|
|
806
|
-
for field_name in ("x", "y", "color"):
|
|
807
|
-
new_value = _rewrite_channel_value(
|
|
808
|
-
layer_dict.get(field_name), collected, channel_alias
|
|
809
|
-
)
|
|
810
|
-
if new_value is None:
|
|
811
|
-
continue
|
|
812
|
-
if isinstance(real_layer, dict):
|
|
813
|
-
real_layer[field_name] = new_value
|
|
814
|
-
else:
|
|
815
|
-
setattr(real_layer, field_name, new_value)
|
|
816
|
-
|
|
817
|
-
if not collected:
|
|
818
|
-
raise CompilationError(
|
|
819
|
-
f"lookml query '{query_name}' has no fields and no charts bind "
|
|
820
|
-
"dotted-ref channels to it. Either add `fields: [view.field, ...]` "
|
|
821
|
-
"to the query, or bind a chart channel (x, y, color, ...) to a "
|
|
822
|
-
"dotted `view.field` ref."
|
|
823
|
-
)
|
|
824
|
-
|
|
825
|
-
query_def.fields = sorted(collected)
|
|
826
|
-
|
|
827
|
-
|
|
828
680
|
# Maps a raw grain token ("month") to the typed TimeGrain literal, so the
|
|
829
681
|
# classifier returns a real TimeGrain without a cast.
|
|
830
682
|
_GRAIN_BY_NAME: dict[str, TimeGrain] = {name: name for name in get_args(TimeGrain)}
|
|
@@ -1002,7 +854,7 @@ def collect_metricflow_channel_refs(
|
|
|
1002
854
|
) -> None:
|
|
1003
855
|
"""Populate a `model:`-synthesized metricflow query from its charts' channels.
|
|
1004
856
|
|
|
1005
|
-
|
|
857
|
+
`model_by_query`
|
|
1006
858
|
maps each synthesized query name to the semantic model named in the chart's
|
|
1007
859
|
`model: <source>.<semantic_model>`. For every chart bound to that query, each
|
|
1008
860
|
bare channel field is classified against the manifest into a metric, an
|
|
@@ -1040,7 +892,7 @@ def collect_metricflow_channel_refs(
|
|
|
1040
892
|
if chart_dict.get("query") != query_name:
|
|
1041
893
|
continue
|
|
1042
894
|
for field_name, value in chart_dict.items():
|
|
1043
|
-
if field_name not in
|
|
895
|
+
if field_name not in _FIELD_CHANNEL_KEYS:
|
|
1044
896
|
continue
|
|
1045
897
|
if not isinstance(value, (str, list)):
|
|
1046
898
|
continue
|