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.
Files changed (73) hide show
  1. dataface/DATAFACE_SYNTAX.md +15 -4
  2. dataface/agent_api/docs/yaml-reference.md +33 -133
  3. dataface/agent_api/serve.py +0 -2
  4. dataface/agent_api/validate.py +8 -0
  5. dataface/cli/commands/render.py +6 -0
  6. dataface/cli/commands/serve.py +0 -6
  7. dataface/cli/main.py +20 -14
  8. dataface/core/aliases.py +27 -0
  9. dataface/core/compile/__init__.py +0 -8
  10. dataface/core/compile/channel.py +0 -13
  11. dataface/core/compile/compiler.py +73 -71
  12. dataface/core/compile/introspection.py +2 -13
  13. dataface/core/compile/models/chart/authored/_base.py +1 -1
  14. dataface/core/compile/models/chart/normalized/__init__.py +2 -0
  15. dataface/core/compile/models/chart/normalized/_base.py +26 -0
  16. dataface/core/compile/models/config.py +5 -0
  17. dataface/core/compile/models/face/authored.py +24 -89
  18. dataface/core/compile/models/query/authored.py +1 -119
  19. dataface/core/compile/models/refs.py +0 -2
  20. dataface/core/compile/models/source.py +73 -159
  21. dataface/core/compile/models/style/theme/variables.py +0 -3
  22. dataface/core/compile/normalize_charts.py +4 -152
  23. dataface/core/compile/normalize_layout.py +20 -284
  24. dataface/core/compile/normalize_queries.py +3 -506
  25. dataface/core/compile/normalizer.py +4 -10
  26. dataface/core/compile/palette.py +19 -7
  27. dataface/core/compile/validator.py +121 -44
  28. dataface/core/compile/yaml_error_formatter.py +107 -2
  29. dataface/core/dashboard.py +3 -1
  30. dataface/core/defaults/default_config.yml +4 -0
  31. dataface/core/defaults/themes/_base.yaml +0 -1
  32. dataface/core/defaults/themes/editorial.yaml +3 -0
  33. dataface/core/defaults/themes/stark.yaml +8 -0
  34. dataface/core/diagnostics/__init__.py +10 -0
  35. dataface/core/diagnostics/codes_compile.py +4 -2
  36. dataface/core/diagnostics/codes_execute.py +70 -0
  37. dataface/core/diagnostics/codes_render.py +58 -4
  38. dataface/core/execute/adapters/__init__.py +1 -1
  39. dataface/core/execute/cache_keys.py +4 -9
  40. dataface/core/execute/executor.py +25 -3
  41. dataface/core/execute/file_source_materializer.py +185 -32
  42. dataface/core/execute/trivial_local_cache.py +5 -2
  43. dataface/core/inspect/__init__.py +0 -1
  44. dataface/core/project.py +6 -2
  45. dataface/core/render/chart/mark_extents.py +339 -0
  46. dataface/core/render/chart/rendering.py +97 -34
  47. dataface/core/render/conditions.py +122 -0
  48. dataface/core/render/controls.py +60 -187
  49. dataface/core/render/data_format.py +1 -1
  50. dataface/core/render/face_resolve.py +7 -0
  51. dataface/core/render/faces.py +94 -30
  52. dataface/core/render/layout_sizing.py +46 -0
  53. dataface/core/render/renderer.py +14 -1
  54. dataface/core/render/sizing.py +42 -105
  55. dataface/core/render/svg_utils.py +60 -14
  56. dataface/core/render/terminal.py +1 -1
  57. dataface/core/render/variables_layout.py +211 -0
  58. dataface/core/render/variables_resolve.py +376 -0
  59. dataface/core/render/variables_strip.py +291 -368
  60. dataface/core/render/warnings/base.py +18 -0
  61. dataface/core/render/warnings/layout_min_exceeds_height.py +68 -0
  62. dataface/core/render/warnings/registry.py +2 -0
  63. dataface/core/serve/alias_index.py +16 -22
  64. dataface/data/schemas/board-resolved/board-resolved.schema.json +1 -499
  65. {dataface-0.3.2.dev111.dist-info → dataface-0.3.2.dev150.dist-info}/METADATA +1 -3
  66. {dataface-0.3.2.dev111.dist-info → dataface-0.3.2.dev150.dist-info}/RECORD +69 -67
  67. dataface/core/inspect/partials/categorical.yml +0 -38
  68. dataface/core/inspect/partials/date.yml +0 -38
  69. dataface/core/inspect/partials/numeric.yml +0 -53
  70. dataface/core/inspect/templates/charts.yml +0 -94
  71. {dataface-0.3.2.dev111.dist-info → dataface-0.3.2.dev150.dist-info}/WHEEL +0 -0
  72. {dataface-0.3.2.dev111.dist-info → dataface-0.3.2.dev150.dist-info}/entry_points.txt +0 -0
  73. {dataface-0.3.2.dev111.dist-info → dataface-0.3.2.dev150.dist-info}/licenses/LICENSE +0 -0
dataface/cli/main.py CHANGED
@@ -796,6 +796,16 @@ def render(
796
796
  ),
797
797
  ),
798
798
  ] = False,
799
+ chart: Annotated[
800
+ str | None,
801
+ typer.Option(
802
+ "--chart",
803
+ help=(
804
+ "Render only this chart (by chart id). "
805
+ "Valid only with a single face argument or stdin."
806
+ ),
807
+ ),
808
+ ] = None,
799
809
  ) -> None:
800
810
  """Render one or more dashboards to SVG, HTML, PNG, PDF, or terminal.
801
811
 
@@ -859,6 +869,14 @@ def render(
859
869
  param_hint="--output",
860
870
  )
861
871
 
872
+ # --chart is ambiguous with multiple faces: no mapping syntax, no silent fanout.
873
+ if chart is not None and len(faces) > 1:
874
+ raise typer.BadParameter(
875
+ f"--chart is only valid with a single face argument; "
876
+ f"got {len(faces)} face arguments",
877
+ param_hint="--chart",
878
+ )
879
+
862
880
  # Stdin path (single face "-")
863
881
  if has_stdin:
864
882
  yaml_content = sys.stdin.read()
@@ -879,6 +897,7 @@ def render(
879
897
  fail_on_chart_errors=fail_on_chart_errors,
880
898
  max_workers=max_workers,
881
899
  print0=print0,
900
+ chart=chart,
882
901
  )
883
902
  return
884
903
 
@@ -897,6 +916,7 @@ def render(
897
916
  max_workers=max_workers,
898
917
  print0=print0,
899
918
  fail_fast=fail_fast,
919
+ chart=chart,
900
920
  )
901
921
 
902
922
 
@@ -1087,19 +1107,6 @@ def serve(
1087
1107
  ),
1088
1108
  ),
1089
1109
  ] = None,
1090
- allow_external_access: Annotated[
1091
- bool,
1092
- typer.Option(
1093
- "--allow-external-access",
1094
- help=(
1095
- "Allow author SQL to read local files / URLs through DuckDB "
1096
- "(read_csv, read_json_auto, httpfs). Off by default — serve runs "
1097
- "strict read-only. Opt in only for local dashboards that must read "
1098
- "files directly in SQL (e.g. glob analytics not expressible as file "
1099
- "sources); prefer type: csv/json/parquet sources otherwise."
1100
- ),
1101
- ),
1102
- ] = False,
1103
1110
  ) -> None:
1104
1111
  """Start the dashboard server.
1105
1112
 
@@ -1149,7 +1156,6 @@ def serve(
1149
1156
  max_workers=max_workers,
1150
1157
  no_cache=no_cache,
1151
1158
  cache_path=cache,
1152
- allow_external_access=allow_external_access,
1153
1159
  )
1154
1160
 
1155
1161
 
@@ -0,0 +1,27 @@
1
+ """Alias-URL semantics, shared by every host that resolves a face's ``aliases:``.
2
+
3
+ A leaf module on purpose. An alias is claimed in a face and matched against an
4
+ incoming request path as a *string*, so the two sides must normalize it
5
+ identically or a claim resolves on one host and silently does nothing on the
6
+ other. Hosts that keep their own board index (rather than building an
7
+ ``AliasIndex``) need that rule without importing the ``dft serve`` HTTP
8
+ application to get it — ``core/serve/`` sits at the top of the dependency stack.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from urllib.parse import unquote
14
+
15
+
16
+ def normalize_alias_url(url: str) -> str:
17
+ """Normalize a URL key: percent-decode, ensure leading /, ensure trailing /."""
18
+ decoded = unquote(url)
19
+ if not decoded.startswith("/"):
20
+ raise ValueError(
21
+ f"Alias {url!r} is not absolute: aliases must start with a leading /. "
22
+ "Example: /old-reports/"
23
+ )
24
+ # Trailing-slash canonical: add if missing
25
+ if not decoded.endswith("/"):
26
+ decoded = decoded + "/"
27
+ return decoded
@@ -80,8 +80,6 @@ from dataface.core.compile.models.face.normalized import (
80
80
  from dataface.core.compile.models.query.authored import (
81
81
  AuthoredCompactValuesQuery,
82
82
  AuthoredHttpQuery,
83
- AuthoredLookmlMergeQuery,
84
- AuthoredLookmlQuery,
85
83
  AuthoredMetricflowQuery,
86
84
  AuthoredQuery,
87
85
  AuthoredSchemaQuery,
@@ -104,8 +102,6 @@ from dataface.core.compile.models.source import (
104
102
  DuckDBSourceConfig,
105
103
  HttpSourceConfig,
106
104
  JsonSourceConfig,
107
- LookMLModelConfig,
108
- LookMLModelsMixin,
109
105
  MySQLSourceConfig,
110
106
  ParquetSourceConfig,
111
107
  PostgresSourceConfig,
@@ -162,8 +158,6 @@ __all__ = [
162
158
  "SparkBarChart",
163
159
  "AuthoredCompactValuesQuery",
164
160
  "AuthoredHttpQuery",
165
- "AuthoredLookmlQuery",
166
- "AuthoredLookmlMergeQuery",
167
161
  "AuthoredMetricflowQuery",
168
162
  "AuthoredQuery",
169
163
  "AuthoredSchemaQuery",
@@ -196,8 +190,6 @@ __all__ = [
196
190
  "JsonSourceConfig",
197
191
  "HttpSourceConfig",
198
192
  "DbtProfileSourceConfig",
199
- "LookMLModelConfig",
200
- "LookMLModelsMixin",
201
193
  "VALID_SOURCE_TYPES",
202
194
  "parse_source_config",
203
195
  "is_database_source",
@@ -10,7 +10,6 @@ block (indexed by column name), not per-channel. See ``types.py`` for
10
10
 
11
11
  from __future__ import annotations
12
12
 
13
- import re
14
13
  from collections.abc import Callable, Iterable
15
14
  from dataclasses import dataclass
16
15
  from typing import Any, Protocol, runtime_checkable
@@ -72,8 +71,6 @@ class ConditionalFormattingChart(Protocol):
72
71
  ...
73
72
 
74
73
 
75
- _DOTTED_LOOKML_REF = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*$")
76
-
77
74
  # Chart types whose color channel is a paintable mark fill — conditional_
78
75
  # formatting rules targeting `background` lower into their `color` channel.
79
76
  _MARK_FILL_CHART_TYPES: frozenset[str] = frozenset(
@@ -91,16 +88,6 @@ _MARK_FILL_CHART_TYPES: frozenset[str] = frozenset(
91
88
  )
92
89
 
93
90
 
94
- def is_dotted_lookml_ref(value: str) -> bool:
95
- """True if `value` has the shape of a LookML `view.field` ref.
96
-
97
- Exactly one dot, both sides valid identifiers, no spaces. Used by the
98
- Surface B collect pass to find channel values that name LookML fields
99
- rather than already-resolved column names.
100
- """
101
- return bool(_DOTTED_LOOKML_REF.match(value))
102
-
103
-
104
91
  def parse_style_channel(
105
92
  raw: _RawStyleChannel,
106
93
  channel_name: str,
@@ -66,12 +66,10 @@ from dataface.core.compile.models.chart.authored import _BaseChartFields
66
66
  from dataface.core.compile.models.config import ProjectCacheConfig
67
67
  from dataface.core.compile.models.face.authored import (
68
68
  AuthoredFace,
69
- ForeachItem,
70
69
  QueryOrRef,
71
70
  )
72
71
  from dataface.core.compile.models.face.normalized import Face
73
72
  from dataface.core.compile.models.query.authored import (
74
- AuthoredLookmlQuery,
75
73
  AuthoredMetricflowQuery,
76
74
  )
77
75
  from dataface.core.compile.models.query.normalized import AnyQuery, is_sql_query
@@ -83,13 +81,9 @@ from dataface.core.compile.models.refs import (
83
81
  )
84
82
  from dataface.core.compile.models.variable.authored import Variable
85
83
  from dataface.core.compile.normalize_charts import (
86
- collect_lookml_channel_refs,
87
84
  collect_metricflow_channel_refs,
88
85
  )
89
- from dataface.core.compile.normalize_queries import (
90
- find_lookml_model_source,
91
- split_external_query_ref,
92
- )
86
+ from dataface.core.compile.normalize_queries import split_external_query_ref
93
87
  from dataface.core.compile.normalize_variables import (
94
88
  VariableReferenceErrors,
95
89
  synthetic_query_name,
@@ -328,7 +322,7 @@ def compile_authored_face(
328
322
  # ════════════════════════════════════════════════════════════════════
329
323
  # STEP 3b: Build Chart Registry (moved ahead of queries — Surface B's
330
324
  # collect pass below needs charts to find dotted-ref channel bindings
331
- # before lookml queries lower to SQL).
325
+ # before semantic queries lower to SQL).
332
326
  # ════════════════════════════════════════════════════════════════════
333
327
  # Pool that imported charts' own query chains resolve into, lexically
334
328
  # scoped to each chart's source face (compile/AGENTS.md's "Cross-face
@@ -355,11 +349,10 @@ def compile_authored_face(
355
349
  # ════════════════════════════════════════════════════════════════════
356
350
  # STEP 3b.5: Desugar chart-level `model:` sugar into a synthesized named
357
351
  # query, before Surface B channel collection (below) so the synthesized
358
- # query is visible to that pass. `model: model.explore` on a LookML source
359
- # becomes a lookml query; `model: source.semantic_model` on a dbt_profile
360
- # source becomes a metricflow query (roles inferred from the manifest in
361
- # STEP 3c). Runs unconditionally so an unknown/ambiguous `model:` name still
362
- # raises a clear error.
352
+ # query is visible to that pass. `model: source.semantic_model` on a
353
+ # dbt_profile source becomes a metricflow query (roles inferred from the
354
+ # manifest in STEP 3c). Runs unconditionally so an unknown/ambiguous
355
+ # `model:` name still raises a clear error.
363
356
  # ════════════════════════════════════════════════════════════════════
364
357
  metricflow_model_by_query: dict[str, str] = {}
365
358
  try:
@@ -388,33 +381,22 @@ def compile_authored_face(
388
381
  if len(_parts) != 2 or not all(_parts):
389
382
  raise CompilationError(
390
383
  f"chart '{_chart_key}' has `model: {_model_value!r}` — expected "
391
- "`model.explore` (a LookML model name and explore name "
392
- "joined by a dot)."
384
+ "`source.semantic_model` (a dbt_profile source name and "
385
+ "semantic model name joined by a dot)."
393
386
  )
394
- _model_name, _explore_name = _parts
395
- _lookml_source = find_lookml_model_source(_model_name, sources_registry)
396
- _source_cfg = sources_registry.get(_model_name)
397
- _is_metricflow = (
398
- _lookml_source is None
399
- and isinstance(_source_cfg, dict)
387
+ _source_name, _semantic_model_name = _parts
388
+ _source_cfg = sources_registry.get(_source_name)
389
+ if not (
390
+ isinstance(_source_cfg, dict)
400
391
  and _source_cfg.get("type") == "dbt_profile"
401
- )
402
- if _lookml_source is None and not _is_metricflow:
392
+ ):
403
393
  raise CompilationError(
404
394
  f"chart '{_chart_key}' has `model: {_model_value}` naming "
405
- f"unknown model '{_model_name}' — no source declares it in its "
406
- "'models:' map, and it is not a dbt_profile source."
395
+ f"unknown source '{_source_name}' — it is not a dbt_profile source."
407
396
  )
408
397
  _synth_name = synthetic_query_name("model_query", _chart_key)
409
- if _is_metricflow:
410
- authored.queries[_synth_name] = AuthoredMetricflowQuery(
411
- source=_model_name
412
- )
413
- metricflow_model_by_query[_synth_name] = _explore_name
414
- else:
415
- authored.queries[_synth_name] = AuthoredLookmlQuery(
416
- source=_model_name, explore=_explore_name
417
- )
398
+ authored.queries[_synth_name] = AuthoredMetricflowQuery(source=_source_name)
399
+ metricflow_model_by_query[_synth_name] = _semantic_model_name
418
400
  # Mutate in place: _BaseChartFields declares query + model; plain
419
401
  # dicts use key assignment. Non-_BaseChartFields BaseModels (e.g.
420
402
  # CalloutChart) have no `model:` field and are skipped above.
@@ -428,31 +410,21 @@ def compile_authored_face(
428
410
  return CompileResult(errors=[e.to_diagnostic(file=file)])
429
411
 
430
412
  # ════════════════════════════════════════════════════════════════════
431
- # STEP 3c: Collect dotted-ref (lookml) / bare-field (metricflow) chart
432
- # channels into their synthesized queries, before query normalization
433
- # lowers them. LookML collects `view.field` refs into a fields-less query;
434
- # MetricFlow classifies bare fields against the manifest into
413
+ # STEP 3c: Collect bare-field (metricflow) chart channels into their
414
+ # synthesized queries, before query normalization lowers them. MetricFlow
415
+ # classifies bare fields against the manifest into
435
416
  # metrics/dimensions/time_grain.
436
417
  # ════════════════════════════════════════════════════════════════════
437
- lookml_model_names: set[str] = set()
438
- for _cfg in sources_registry.values():
439
- if isinstance(_cfg, dict) and isinstance(_cfg.get("models"), dict):
440
- lookml_model_names.update(_cfg["models"].keys())
441
- if lookml_model_names or metricflow_model_by_query:
418
+ if metricflow_model_by_query:
442
419
  try:
443
420
  raw_queries: dict[str, QueryOrRef] = {}
444
421
  _collect_raw_queries(authored, raw_queries)
445
- if lookml_model_names:
446
- collect_lookml_channel_refs(
447
- collectible_charts, raw_queries, lookml_model_names
448
- )
449
- if metricflow_model_by_query:
450
- collect_metricflow_channel_refs(
451
- collectible_charts,
452
- raw_queries,
453
- metricflow_model_by_query,
454
- base_dir,
455
- )
422
+ collect_metricflow_channel_refs(
423
+ collectible_charts,
424
+ raw_queries,
425
+ metricflow_model_by_query,
426
+ base_dir,
427
+ )
456
428
  except CompilationError as e:
457
429
  return CompileResult(errors=[e.to_diagnostic(file=file)])
458
430
 
@@ -493,7 +465,7 @@ def compile_authored_face(
493
465
  # host-supplied fallback) so inline chart queries synthesized during
494
466
  # normalization inherit it too — not just the named queries built above.
495
467
  # `sources` (board-global named source configs) rides alongside so an
496
- # inline lookml/metricflow query on a chart resolves its model just
468
+ # inline metricflow query on a chart resolves its model just
497
469
  # like an up-top query.
498
470
  parent_context={
499
471
  "default_source": default_source,
@@ -996,11 +968,6 @@ def _extract_nested_faces(face: AuthoredFace) -> list[AuthoredFace]:
996
968
  tabs=item.tabs,
997
969
  )
998
970
  nested_faces.append(tab_face)
999
- elif isinstance(item, ForeachItem):
1000
- # Recurse into foreach items to find any nested faces
1001
- for foreach_subitem in item.foreach.items:
1002
- if isinstance(foreach_subitem, AuthoredFace):
1003
- nested_faces.append(foreach_subitem)
1004
971
 
1005
972
  return nested_faces
1006
973
 
@@ -1070,7 +1037,7 @@ def _collect_raw_queries(face: AuthoredFace, registry: dict[str, QueryOrRef]) ->
1070
1037
 
1071
1038
  Unlike `build_query_registry`, entries stay as authored AuthoredQuery/
1072
1039
  QueryRef instances — used by the Surface B collect pass, which must
1073
- mutate a lookml query's `fields` before `normalize_query` lowers it.
1040
+ mutate a semantic query's fields before `normalize_query` lowers it.
1074
1041
  Cross-file QueryRef entries are included by reference but not resolved
1075
1042
  (a ref's own file already ran its own collect pass at its own compile).
1076
1043
  """
@@ -1088,7 +1055,7 @@ def _collect_inline_layout_charts(face: AuthoredFace, registry: dict[str, Any])
1088
1055
  `build_chart_registry` only sees top-level named `charts:` — a chart authored
1089
1056
  directly inline in `rows:`/`cols:`/`grid:`/`tabs:` (a bare AuthoredChart patch,
1090
1057
  or `dict[str, AuthoredChart]`) never gets a registry entry. The Surface B
1091
- collect pass (`collect_lookml_channel_refs`) needs to see and mutate those
1058
+ collect pass (`collect_metricflow_channel_refs`) needs to see and mutate those
1092
1059
  authored chart objects in place before normalization re-serializes them.
1093
1060
 
1094
1061
  Keys are Python object ids (`id(chart_obj)`) — guaranteed unique per live
@@ -1099,7 +1066,7 @@ def _collect_inline_layout_charts(face: AuthoredFace, registry: dict[str, Any])
1099
1066
  def register(item: object) -> None:
1100
1067
  # Handles both the bare-patch form (`- type: bar, ...`) and the named-dict
1101
1068
  # form (`- my_chart: {type: bar, ...}`). Anything else (chart-name string,
1102
- # nested face, foreach) is not an inline chart and is skipped here.
1069
+ # nested face, swatch strip) is not an inline chart and is skipped here.
1103
1070
  # str(id(item)) is unique per live Python object — safer key than a
1104
1071
  # position-only path, which collides when sibling nested faces share indices.
1105
1072
  if isinstance(item, _BaseChartFields):
@@ -1148,7 +1115,7 @@ def _process_queries_for_registry(
1148
1115
  registry: Registry to add queries to
1149
1116
  base_dir: ProjectDirectory anchor for the face file (used to resolve sub-file refs).
1150
1117
  default_source: Default source to apply to queries without explicit source
1151
- sources: Named source config dicts. Consulted by lookml queries at normalize time.
1118
+ sources: Named source config dicts. Consulted by metricflow queries at normalize time.
1152
1119
  face_cache: The face-scope cache layer already folded down the nesting
1153
1120
  chain by the caller.
1154
1121
 
@@ -1265,7 +1232,7 @@ def build_query_registry(
1265
1232
  registry: Existing registry to add to
1266
1233
  default_source: Default source to apply to queries without explicit source
1267
1234
  sources: Named source config dicts (project + face level). Consulted by
1268
- lookml queries, which resolve their source at normalize time.
1235
+ metricflow queries, which resolve their source at normalize time.
1269
1236
  face_cache: The cache layer this face inherits from the faces it is
1270
1237
  nested in; its own `cache:` folds over it inside `_build_registry`.
1271
1238
 
@@ -1410,6 +1377,26 @@ def _resolve_ref_item(
1410
1377
  return section[item_name], item_name, resolved_path, content
1411
1378
 
1412
1379
 
1380
+ def _face_level_default_source(
1381
+ content: dict[str, YamlValue], file_label: str
1382
+ ) -> str | None:
1383
+ """Validated face-level `source:` of a raw source-file mapping.
1384
+
1385
+ Cross-face-pulled queries inherit the declaring file's default source. The
1386
+ source file's content is a raw mapping here (never parsed into
1387
+ AuthoredFace), so the string-ness the model would enforce is checked here.
1388
+ """
1389
+ source = content.get("source")
1390
+ if source is None:
1391
+ return None
1392
+ if not isinstance(source, str):
1393
+ raise CompilationError(
1394
+ f"Invalid face-level `source:` in {file_label}: "
1395
+ f"expected a source name string, got {type(source).__name__}"
1396
+ )
1397
+ return source
1398
+
1399
+
1413
1400
  def load_from_reference(
1414
1401
  reference: VariableRef | QueryRef | ChartRef,
1415
1402
  base_dir: ProjectDirectory | None = None,
@@ -1464,7 +1451,7 @@ def load_from_reference(
1464
1451
  else:
1465
1452
  section_name = "charts"
1466
1453
 
1467
- item_def, item_name, resolved_path, _content = _resolve_ref_item(
1454
+ item_def, item_name, resolved_path, source_file_content = _resolve_ref_item(
1468
1455
  reference.ref, section_name, base_dir, yaml_path
1469
1456
  )
1470
1457
 
@@ -1472,6 +1459,7 @@ def load_from_reference(
1472
1459
  return normalize_query(
1473
1460
  item_name,
1474
1461
  item_def,
1462
+ _face_level_default_source(source_file_content, str(resolved_path.relpath)),
1475
1463
  base_dir=base_dir,
1476
1464
  sources=sources,
1477
1465
  cache_root=cache_root,
@@ -1550,10 +1538,13 @@ def _is_lexically_scoped_query_name(value: str) -> bool:
1550
1538
 
1551
1539
  def _read_queries_section(
1552
1540
  file_dir: ProjectDirectory, target_file_str: str, ref: str, chart_ref: str
1553
- ) -> tuple[ProjectPath, dict[str, YamlValue]]:
1541
+ ) -> tuple[ProjectPath, dict[str, YamlValue], dict[str, YamlValue]]:
1554
1542
  """The `queries:` section of `target_file_str`, resolved relative to
1555
1543
  `file_dir` — the *declaring* file's own directory, never the importer's.
1556
1544
 
1545
+ Also returns the file's full parsed mapping, so callers can read its
1546
+ face-level `source:` default for the queries declared in it.
1547
+
1557
1548
  `ref` is the authored ref string this file part came from and `chart_ref`
1558
1549
  the chart import that pulled it in; both are named in every failure, since
1559
1550
  the author is looking at neither file when the resolution breaks.
@@ -1577,12 +1568,16 @@ def _read_queries_section(
1577
1568
  # compile() rather than a diagnostic.
1578
1569
  if target_content is not None and not isinstance(target_content, dict):
1579
1570
  raise CompilationError(f"{where}, but {target_file_str} is not a YAML mapping.")
1580
- target_queries = target_content.get("queries") if target_content else None
1571
+ if target_content is None:
1572
+ raise CompilationError(
1573
+ f"{where}, but {target_file_str} has no queries: section."
1574
+ )
1575
+ target_queries = target_content.get("queries")
1581
1576
  if not isinstance(target_queries, dict):
1582
1577
  raise CompilationError(
1583
1578
  f"{where}, but {target_file_str} has no queries: section."
1584
1579
  )
1585
- return target_path, target_queries
1580
+ return target_path, target_queries, target_content
1586
1581
 
1587
1582
 
1588
1583
  def _resolve_cross_face_query(
@@ -1595,6 +1590,7 @@ def _resolve_cross_face_query(
1595
1590
  resolving: set[tuple[str, str]],
1596
1591
  chart_ref: str,
1597
1592
  *,
1593
+ default_source: str | None,
1598
1594
  sources: dict[str, Any],
1599
1595
  cache_root: CachePatch | None,
1600
1596
  face_cache: CachePatch,
@@ -1639,7 +1635,7 @@ def _resolve_cross_face_query(
1639
1635
  target_file_str, target_name = raw_def["ref"].rsplit(".queries.", 1)
1640
1636
  if not (target_file_str.endswith(".yml") or target_file_str.endswith(".yaml")):
1641
1637
  target_file_str += ".yml"
1642
- target_path, target_queries = _read_queries_section(
1638
+ target_path, target_queries, target_content = _read_queries_section(
1643
1639
  file_dir, target_file_str, raw_def["ref"], chart_ref
1644
1640
  )
1645
1641
  target_key = _resolve_cross_face_query(
@@ -1651,6 +1647,7 @@ def _resolve_cross_face_query(
1651
1647
  key_cache,
1652
1648
  resolving,
1653
1649
  chart_ref,
1650
+ default_source=_face_level_default_source(target_content, target_file_str),
1654
1651
  sources=sources,
1655
1652
  cache_root=cache_root,
1656
1653
  face_cache=face_cache,
@@ -1676,6 +1673,7 @@ def _resolve_cross_face_query(
1676
1673
  key_cache,
1677
1674
  resolving,
1678
1675
  chart_ref,
1676
+ default_source=default_source,
1679
1677
  sources=sources,
1680
1678
  cache_root=cache_root,
1681
1679
  face_cache=face_cache,
@@ -1697,6 +1695,7 @@ def _resolve_cross_face_query(
1697
1695
  pool[qualified_key] = normalize_query(
1698
1696
  qualified_key,
1699
1697
  raw_def,
1698
+ default_source,
1700
1699
  base_dir=file_dir,
1701
1700
  sources=sources,
1702
1701
  cache_root=cache_root,
@@ -1746,6 +1745,7 @@ def _load_chart_ref_with_lexical_queries(
1746
1745
  queries_section = {}
1747
1746
  file_dir = resolved_path.parent
1748
1747
  file_relpath = resolved_path.relpath
1748
+ source_default = _face_level_default_source(content, str(file_relpath))
1749
1749
  key_cache: dict[tuple[str, str], str] = {}
1750
1750
  resolving: set[tuple[str, str]] = set()
1751
1751
 
@@ -1769,12 +1769,13 @@ def _load_chart_ref_with_lexical_queries(
1769
1769
  key_cache,
1770
1770
  resolving,
1771
1771
  chart_ref.ref,
1772
+ default_source=source_default,
1772
1773
  sources=sources,
1773
1774
  cache_root=cache_root,
1774
1775
  face_cache=face_cache,
1775
1776
  )
1776
1777
  target_file_str, target_name = split_external_query_ref(stripped)
1777
- target_path, target_queries = _read_queries_section(
1778
+ target_path, target_queries, target_content = _read_queries_section(
1778
1779
  file_dir, target_file_str, stripped, chart_ref.ref
1779
1780
  )
1780
1781
  return _resolve_cross_face_query(
@@ -1786,6 +1787,7 @@ def _load_chart_ref_with_lexical_queries(
1786
1787
  key_cache,
1787
1788
  resolving,
1788
1789
  chart_ref.ref,
1790
+ default_source=_face_level_default_source(target_content, target_file_str),
1789
1791
  sources=sources,
1790
1792
  cache_root=cache_root,
1791
1793
  face_cache=face_cache,
@@ -27,8 +27,6 @@ from dataface.core.compile.models.factories import _PatchBase
27
27
  from dataface.core.compile.models.query.authored import (
28
28
  AuthoredCompactValuesQuery,
29
29
  AuthoredHttpQuery,
30
- AuthoredLookmlMergeQuery,
31
- AuthoredLookmlQuery,
32
30
  AuthoredMetricflowQuery,
33
31
  AuthoredQuery,
34
32
  AuthoredSchemaQuery,
@@ -42,7 +40,6 @@ from dataface.core.compile.models.source import (
42
40
  DuckDBSourceConfig,
43
41
  HttpSourceConfig,
44
42
  JsonSourceConfig,
45
- LookMLModelConfig,
46
43
  MySQLSourceConfig,
47
44
  ParquetSourceConfig,
48
45
  PostgresSourceConfig,
@@ -133,9 +130,7 @@ def _is_authored_model(cls: Any) -> bool:
133
130
  """Return True if cls should be included in the IR.
134
131
 
135
132
  Includes authored.* models, primitives, _PatchBase subclasses, compiled
136
- style models from style/theme/, LookMLModelConfig so
137
- dict[str, LookMLModelConfig] fields emit additionalProperties refs in JSON
138
- Schema, and *Patch models from models.cache (CachePatch is the authored
133
+ style models from style/theme/, and *Patch models from models.cache (CachePatch is the authored
139
134
  overlay for the cache block — same role as *StylePatch, different module).
140
135
  """
141
136
  if not (isinstance(cls, type) and issubclass(cls, BaseModel)):
@@ -148,7 +143,6 @@ def _is_authored_model(cls: Any) -> bool:
148
143
  or module.endswith("primitives")
149
144
  or module.endswith("style.theme")
150
145
  or ".style.theme." in module
151
- or issubclass(cls, LookMLModelConfig)
152
146
  or (module.endswith(".models.cache") and cls.__name__.endswith("Patch"))
153
147
  )
154
148
 
@@ -576,8 +570,6 @@ def introspect() -> AuthorableSchema:
576
570
  AuthoredValuesQuery,
577
571
  AuthoredCompactValuesQuery,
578
572
  AuthoredSchemaQuery,
579
- AuthoredLookmlQuery,
580
- AuthoredLookmlMergeQuery,
581
573
  PaginationConfig,
582
574
  PostgresSourceConfig,
583
575
  SnowflakeSourceConfig,
@@ -591,7 +583,6 @@ def introspect() -> AuthorableSchema:
591
583
  JsonSourceConfig,
592
584
  HttpSourceConfig,
593
585
  DbtProfileSourceConfig,
594
- LookMLModelConfig,
595
586
  # Compiled style root — walks style/theme/ types so inherit-chain metadata
596
587
  # (Inherit/InheritSlot markers) is visible in the schema IR.
597
588
  Style,
@@ -623,8 +614,6 @@ def introspect() -> AuthorableSchema:
623
614
  "AuthoredValuesQuery",
624
615
  "AuthoredCompactValuesQuery",
625
616
  "AuthoredSchemaQuery",
626
- "AuthoredLookmlQuery",
627
- "AuthoredLookmlMergeQuery",
628
617
  )
629
618
  if all(name in collected for name in query_variants):
630
619
  collected["AuthoredQuery"] = AuthorableModel(
@@ -632,7 +621,7 @@ def introspect() -> AuthorableSchema:
632
621
  doc=(
633
622
  "Union of per-type authored query shapes. "
634
623
  "type: is inferred from key presence when omitted (bare SQL string, "
635
- "sql: -> sql; metrics: -> metricflow; explore:/merge: -> lookml; "
624
+ "sql: -> sql; metrics: -> metricflow; "
636
625
  "url: -> http; rows:/values: -> values)."
637
626
  ),
638
627
  fields=[],
@@ -168,7 +168,7 @@ class _BaseChartFields(BaseModel):
168
168
  str | None,
169
169
  Field(
170
170
  default=None,
171
- description="Semantic-layer entry point, two dot-joined names: 'model.explore' for a LookML source, or 'source.semantic_model' for a dbt_profile (MetricFlow) source. Channel fields name the model's fields directly; roles are inferred from the LookML catalog / dbt semantic manifest. Mutually exclusive with `query:`.",
171
+ description="Semantic-layer entry point, two dot-joined names: 'source.semantic_model' for a dbt_profile (MetricFlow) source. Channel fields name the model's fields directly; roles are inferred from the dbt semantic manifest. Mutually exclusive with `query:`.",
172
172
  ),
173
173
  ]
174
174
 
@@ -30,6 +30,7 @@ from pydantic import Discriminator
30
30
 
31
31
  from ._base import (
32
32
  NON_ASPECT_RATIO_TYPES,
33
+ PAINTS_MARKS,
33
34
  SVG_LAYOUT_PADDED_TYPES,
34
35
  ChartDependencies,
35
36
  _BaseChartFields,
@@ -75,6 +76,7 @@ model. Each variant's ``type:`` Literal is the discriminator key.
75
76
  __all__ = [
76
77
  # constants and helpers
77
78
  "NON_ASPECT_RATIO_TYPES",
79
+ "PAINTS_MARKS",
78
80
  "SVG_LAYOUT_PADDED_TYPES",
79
81
  "ChartDependencies",
80
82
  # discriminated union
@@ -189,6 +189,32 @@ __all__ = [
189
189
  NON_ASPECT_RATIO_TYPES = frozenset({"table", "kpi", "spark_bar", "callout"})
190
190
  SVG_LAYOUT_PADDED_TYPES = frozenset({"table", "kpi", "spark_bar"})
191
191
 
192
+ # Chart-type strings dispatched through get_emitter() (render/chart/emitters/__init__.py)
193
+ # — the families that emit Vega-Lite marks. The complement (table, kpi, spark_bar,
194
+ # callout) never paints a mark; it is drawn by a hand-crafted SVG renderer instead.
195
+ # This is a different fact from SVG_LAYOUT_PADDED_TYPES above (that one is about
196
+ # layout padding, not mark-painting) even though their complements happen to
197
+ # coincide today — don't fold them into one set.
198
+ #
199
+ # These are RESOLVED-stage chart_type strings (ResolvedChart.chart_type), not
200
+ # normalized-stage ones. The normalized `.type` vocabulary also contains
201
+ # `donut`, `map`, and `bubble_map` — they collapse to `pie`/`geoshape`/
202
+ # `point_map` only at resolve. Applying this set to a normalized `.type` would
203
+ # silently misclassify those three as non-painting.
204
+ PAINTS_MARKS = frozenset(
205
+ {
206
+ "bar",
207
+ "histogram",
208
+ "line",
209
+ "area",
210
+ "scatter",
211
+ "heatmap",
212
+ "pie",
213
+ "geoshape",
214
+ "point_map",
215
+ }
216
+ )
217
+
192
218
 
193
219
  @dataclass(frozen=True)
194
220
  class ChartDependencies:
@@ -217,6 +217,11 @@ class ExecutionConfig(ConfigNode):
217
217
  max_query_duration_seconds: int = Field(
218
218
  gt=0, description="Maximum seconds a single query may run (must be > 0)."
219
219
  )
220
+ # Hard cap on files matched per glob pattern in file sources. Error is raised
221
+ # before any file is read so a runaway glob fails fast. Override in dataface.yml.
222
+ max_glob_file_count: int = Field(
223
+ gt=0, description="Maximum files a single glob may match (must be > 0)."
224
+ )
220
225
  # sqlglot uses different dialect names than Dataface's public-facing dialect strings.
221
226
  # This mapping normalizes Dataface names to sqlglot equivalents before parsing.
222
227
  dialect_aliases: dict[str, str]