dataface 0.1.6.dev82__py3-none-any.whl → 0.1.6.dev183__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 (112) hide show
  1. dataface/agent_api/_paths.py +7 -18
  2. dataface/agent_api/docs/yaml-reference.md +14 -13
  3. dataface/agent_api/project_session.py +5 -44
  4. dataface/agent_api/query.py +6 -0
  5. dataface/agent_api/render_face.py +1 -14
  6. dataface/ai/context.py +1 -0
  7. dataface/ai/mcp/server.py +5 -5
  8. dataface/ai/skills/dashboard-build/SKILL.md +7 -0
  9. dataface/cli/commands/_agent_input.py +3 -3
  10. dataface/cli/commands/render.py +2 -10
  11. dataface/core/compile/__init__.py +0 -2
  12. dataface/core/compile/channel.py +1 -6
  13. dataface/core/compile/colors.py +1 -1
  14. dataface/core/compile/compiler.py +15 -13
  15. dataface/core/compile/config.py +13 -8
  16. dataface/core/compile/dbt_jinja.py +66 -0
  17. dataface/core/compile/detect.py +32 -6
  18. dataface/core/compile/inherit_graph.py +31 -4
  19. dataface/core/compile/inherit_resolver.py +13 -4
  20. dataface/core/compile/jinja.py +2 -2
  21. dataface/core/compile/markdown.py +109 -18
  22. dataface/core/compile/models/chart/authored.py +33 -39
  23. dataface/core/compile/models/config.py +4 -0
  24. dataface/core/compile/models/face/resolved.py +3 -3
  25. dataface/core/compile/models/markers.py +14 -3
  26. dataface/core/compile/models/query/normalized.py +4 -0
  27. dataface/core/compile/models/source.py +4 -86
  28. dataface/core/compile/models/style/authored.py +2 -2
  29. dataface/core/compile/models/style/resolved.py +15 -518
  30. dataface/core/compile/models/style/theme.py +64 -58
  31. dataface/core/compile/normalize_charts.py +3 -3
  32. dataface/core/compile/normalize_layout.py +6 -33
  33. dataface/core/compile/normalize_variables.py +10 -10
  34. dataface/core/compile/normalizer.py +24 -24
  35. dataface/core/compile/parameterized.py +1 -6
  36. dataface/core/compile/sizing.py +30 -8
  37. dataface/core/compile/sources.py +19 -6
  38. dataface/core/compile/style_cascade.py +202 -60
  39. dataface/core/compile/typography.py +92 -89
  40. dataface/core/compile/yaml_error_formatter.py +102 -33
  41. dataface/core/defaults/default_config.yml +1 -0
  42. dataface/core/defaults/themes/_base.yaml +2 -2
  43. dataface/core/defaults/themes/stark.yaml +4 -0
  44. dataface/core/execute/adapters/adapter_registry.py +3 -8
  45. dataface/core/execute/adapters/dbt_adapter_factory.py +7 -7
  46. dataface/core/execute/adapters/sql_adapter.py +10 -6
  47. dataface/core/execute/batch.py +4 -2
  48. dataface/core/execute/duckdb_cache.py +4 -3
  49. dataface/core/execute/executor.py +3 -6
  50. dataface/core/inspect/query_validator.py +1 -1
  51. dataface/core/inspect/renderer.py +0 -1
  52. dataface/core/project_roots.py +15 -35
  53. dataface/core/render/__init__.py +0 -3
  54. dataface/core/render/board_links.py +71 -18
  55. dataface/core/render/chart/callout.py +5 -9
  56. dataface/core/render/chart/geo.py +71 -29
  57. dataface/core/render/chart/kpi.py +18 -11
  58. dataface/core/render/chart/pipeline.py +15 -98
  59. dataface/core/render/chart/profile.py +89 -77
  60. dataface/core/render/chart/render_single.py +116 -67
  61. dataface/core/render/chart/renderers.py +45 -95
  62. dataface/core/render/chart/rendering.py +67 -64
  63. dataface/core/render/chart/spark.py +1 -1
  64. dataface/core/render/chart/spark_bar.py +34 -20
  65. dataface/core/render/chart/standard_renderer.py +102 -228
  66. dataface/core/render/chart/table.py +58 -82
  67. dataface/core/render/chart/table_support.py +2 -5
  68. dataface/core/render/chart/time_unit_detect.py +28 -19
  69. dataface/core/render/chart/vega_lite.py +13 -23
  70. dataface/core/render/chart/vega_lite_types.py +5 -5
  71. dataface/core/render/converters/chart.py +5 -6
  72. dataface/core/render/dir_context.py +42 -38
  73. dataface/core/render/faces.py +35 -27
  74. dataface/core/render/font_support.py +8 -8
  75. dataface/core/render/layout_sizing.py +44 -53
  76. dataface/core/render/layouts.py +9 -17
  77. dataface/core/render/renderer.py +21 -18
  78. dataface/core/render/svg_utils.py +11 -8
  79. dataface/core/render/templates/nav/nav-fragment.html +1 -1
  80. dataface/core/render/templates/nav/nav.js +3 -3
  81. dataface/core/render/terminal_charts.py +2 -2
  82. dataface/core/render/utils.py +1 -1
  83. dataface/core/render/variable_controls.py +20 -22
  84. dataface/core/render/warnings/bar_color_1_to_1_with_x.py +4 -3
  85. dataface/core/render/warnings/base.py +2 -2
  86. dataface/core/render/warnings/layered_chart_shared_y_axis_scale_mismatch.py +2 -2
  87. dataface/core/render/warnings/likely_currency_or_percent_missing_formatter.py +2 -2
  88. dataface/core/render/warnings/redundant_encoding.py +10 -6
  89. dataface/core/render/warnings/temporal_single_point.py +3 -3
  90. dataface/core/resolve_face.py +21 -6
  91. dataface/core/scoped_paths.py +30 -9
  92. dataface/core/serve/alias_index.py +9 -17
  93. dataface/core/serve/bootstrap.py +2 -2
  94. dataface/core/serve/server.py +91 -46
  95. dataface/core/validate.py +2 -2
  96. dataface/integrations/highlighting.py +4 -3
  97. dataface/integrations/markdown.py +2 -2
  98. {dataface-0.1.6.dev82.dist-info → dataface-0.1.6.dev183.dist-info}/METADATA +1 -1
  99. {dataface-0.1.6.dev82.dist-info → dataface-0.1.6.dev183.dist-info}/RECORD +104 -111
  100. mdsvg/renderer.py +76 -16
  101. mdsvg/style.py +16 -4
  102. dataface/core/render/control_registry.py +0 -287
  103. dataface/core/render/templates/controls/checkbox.html +0 -16
  104. dataface/core/render/templates/controls/date.html +0 -16
  105. dataface/core/render/templates/controls/number.html +0 -19
  106. dataface/core/render/templates/controls/readonly.html +0 -9
  107. dataface/core/render/templates/controls/select.html +0 -20
  108. dataface/core/render/templates/controls/slider.html +0 -22
  109. dataface/core/render/templates/controls/text.html +0 -16
  110. {dataface-0.1.6.dev82.dist-info → dataface-0.1.6.dev183.dist-info}/WHEEL +0 -0
  111. {dataface-0.1.6.dev82.dist-info → dataface-0.1.6.dev183.dist-info}/entry_points.txt +0 -0
  112. {dataface-0.1.6.dev82.dist-info → dataface-0.1.6.dev183.dist-info}/licenses/LICENSE +0 -0
@@ -6,9 +6,8 @@ from dataclasses import dataclass
6
6
  from pathlib import Path
7
7
 
8
8
  from dataface.core.project_roots import (
9
- discover_render_context as discover_render_context,
10
- discovery_boundary_for_face as discovery_boundary_for_face,
11
9
  find_dft_root as find_dft_root,
10
+ find_project_root as find_project_root,
12
11
  )
13
12
  from dataface.core.scoped_paths import (
14
13
  resolve_scoped_path as resolve_scoped_path,
@@ -60,8 +59,8 @@ class FaceRenderContext:
60
59
  """Path resolution result from a face path + project root.
61
60
 
62
61
  Adapter registry is built by `ProjectSession.open`, not by the context — call sites
63
- open a `ProjectSession` with `project_root` + `dbt_project_path` and read the
64
- registry off `project.adapter_registry`.
62
+ open a `ProjectSession` with `project_root` and read the registry off
63
+ `project.adapter_registry`.
65
64
  """
66
65
 
67
66
  face_file: Path
@@ -69,18 +68,16 @@ class FaceRenderContext:
69
68
  scoped_base: Path
70
69
  project_root: Path
71
70
  output_dir: Path
72
- dbt_project_path: Path | None = None
73
71
 
74
72
 
75
73
  def build_face_render_context(
76
74
  face_path: Path,
77
75
  project_dir: Path,
78
76
  ) -> FaceRenderContext:
79
- """Resolve a face path and walk for dbt context.
77
+ """Resolve a face path against the given project root.
80
78
 
81
- ``project_dir`` is authoritative; the walk only contributes the dbt project
82
- path. Callers must resolve their project dir first (e.g. via
83
- ``resolve_project_dir(raw_dir)`` at the CLI boundary).
79
+ ``project_dir`` is authoritative. Callers must resolve their project dir first
80
+ (e.g. via ``resolve_project_dir(raw_dir)`` at the CLI boundary).
84
81
  """
85
82
  if face_path.is_absolute():
86
83
  face_file = face_path.resolve()
@@ -89,10 +86,6 @@ def build_face_render_context(
89
86
  else:
90
87
  face_file = (project_dir / face_path).resolve()
91
88
 
92
- _, dbt_project_path = discover_render_context(
93
- face_file.parent,
94
- discovery_boundary_for_face(face_file.parent, project_dir),
95
- )
96
89
  project_root = project_dir
97
90
 
98
91
  try:
@@ -109,7 +102,6 @@ def build_face_render_context(
109
102
  scoped_base=project_root,
110
103
  project_root=project_root,
111
104
  output_dir=project_root,
112
- dbt_project_path=dbt_project_path,
113
105
  )
114
106
 
115
107
 
@@ -122,21 +114,18 @@ class YamlRenderContext:
122
114
 
123
115
  project_root: Path
124
116
  output_dir: Path
125
- dbt_project_path: Path | None = None
126
117
 
127
118
 
128
119
  def build_yaml_render_context(
129
120
  project_dir: Path,
130
121
  ) -> YamlRenderContext:
131
- """Walk for dbt context anchored at the given project root.
122
+ """Resolve the project root for rendering inline YAML.
132
123
 
133
124
  ``project_dir`` is authoritative. Callers must resolve their project dir
134
125
  first (e.g. via ``resolve_project_dir(raw_dir)`` at the CLI boundary).
135
126
  """
136
127
  project_root = project_dir.resolve()
137
- _, dbt_project_path = discover_render_context(project_root, None)
138
128
  return YamlRenderContext(
139
129
  project_root=project_root,
140
130
  output_dir=project_root,
141
- dbt_project_path=dbt_project_path,
142
131
  )
@@ -128,8 +128,8 @@ Authored patch for bar and histogram charts.
128
128
  | `sort` | [ChartSort](#chartsort) | ✓ | Sort configuration: field to sort by and direction (asc/desc). |
129
129
  | `labels` | [ChartLabels](#chartlabels) | ✓ | Per-row text annotations near each data anchor. |
130
130
  | `data_table` | [ChartDataTable](#chartdatatable) | ✓ | Optional mini data-grid attached below/above the chart. |
131
- | `height` | int \| float | ✓ | Explicit chart height in pixels. Positive number only. When set, overrides aspect_ratio and theme cascade. Not valid on kpi, table, callout, or spark_bar those renderers own their own sizing contracts. |
132
- | `width` | int \| float | ✓ | Chart width hint in pixels. Positive number only. Used by the label-overlap heuristic to determine whether x-axis labels need tilting. Does not override the layout slot width — the chart still fills its allocated container. Not valid on kpi, table, callout, or spark_bar those renderers own their own sizing contracts. |
131
+ | `height` | int \| float | ✓ | Explicit chart height in pixels. Positive number only. When set, overrides aspect_ratio and theme cascade. Only valid on cartesian chart families: area, bar, heatmap, histogram, line, and scatter. Other chart families use renderer-owned or layout-owned sizing contracts. |
132
+ | `width` | int \| float | ✓ | Chart width hint in pixels. Positive number only. Used by the label-overlap heuristic to determine whether x-axis labels need tilting. Does not override the layout slot width — the chart still fills its allocated container. Only valid on cartesian chart families: area, bar, heatmap, histogram, line, and scatter. Other chart families use renderer-owned or layout-owned sizing contracts. |
133
133
  | `style` | [BarChartStyle](#barchartstyle) | ✓ | Chart-local style overrides. |
134
134
 
135
135
  <a id="linechart"></a>
@@ -156,8 +156,8 @@ Authored patch for line charts.
156
156
  | `sort` | [ChartSort](#chartsort) | ✓ | Sort configuration: field to sort by and direction (asc/desc). |
157
157
  | `labels` | [ChartLabels](#chartlabels) | ✓ | Per-row text annotations near each data anchor. |
158
158
  | `data_table` | [ChartDataTable](#chartdatatable) | ✓ | Optional mini data-grid attached below/above the chart. |
159
- | `height` | int \| float | ✓ | Explicit chart height in pixels. Positive number only. When set, overrides aspect_ratio and theme cascade. Not valid on kpi, table, callout, or spark_bar those renderers own their own sizing contracts. |
160
- | `width` | int \| float | ✓ | Chart width hint in pixels. Positive number only. Used by the label-overlap heuristic to determine whether x-axis labels need tilting. Does not override the layout slot width — the chart still fills its allocated container. Not valid on kpi, table, callout, or spark_bar those renderers own their own sizing contracts. |
159
+ | `height` | int \| float | ✓ | Explicit chart height in pixels. Positive number only. When set, overrides aspect_ratio and theme cascade. Only valid on cartesian chart families: area, bar, heatmap, histogram, line, and scatter. Other chart families use renderer-owned or layout-owned sizing contracts. |
160
+ | `width` | int \| float | ✓ | Chart width hint in pixels. Positive number only. Used by the label-overlap heuristic to determine whether x-axis labels need tilting. Does not override the layout slot width — the chart still fills its allocated container. Only valid on cartesian chart families: area, bar, heatmap, histogram, line, and scatter. Other chart families use renderer-owned or layout-owned sizing contracts. |
161
161
  | `style` | [LineChartStyle](#linechartstyle) | ✓ | Chart-local style overrides. |
162
162
 
163
163
  <a id="areachart"></a>
@@ -184,8 +184,8 @@ Authored patch for area charts.
184
184
  | `sort` | [ChartSort](#chartsort) | ✓ | Sort configuration: field to sort by and direction (asc/desc). |
185
185
  | `labels` | [ChartLabels](#chartlabels) | ✓ | Per-row text annotations near each data anchor. |
186
186
  | `data_table` | [ChartDataTable](#chartdatatable) | ✓ | Optional mini data-grid attached below/above the chart. |
187
- | `height` | int \| float | ✓ | Explicit chart height in pixels. Positive number only. When set, overrides aspect_ratio and theme cascade. Not valid on kpi, table, callout, or spark_bar those renderers own their own sizing contracts. |
188
- | `width` | int \| float | ✓ | Chart width hint in pixels. Positive number only. Used by the label-overlap heuristic to determine whether x-axis labels need tilting. Does not override the layout slot width — the chart still fills its allocated container. Not valid on kpi, table, callout, or spark_bar those renderers own their own sizing contracts. |
187
+ | `height` | int \| float | ✓ | Explicit chart height in pixels. Positive number only. When set, overrides aspect_ratio and theme cascade. Only valid on cartesian chart families: area, bar, heatmap, histogram, line, and scatter. Other chart families use renderer-owned or layout-owned sizing contracts. |
188
+ | `width` | int \| float | ✓ | Chart width hint in pixels. Positive number only. Used by the label-overlap heuristic to determine whether x-axis labels need tilting. Does not override the layout slot width — the chart still fills its allocated container. Only valid on cartesian chart families: area, bar, heatmap, histogram, line, and scatter. Other chart families use renderer-owned or layout-owned sizing contracts. |
189
189
  | `style` | [AreaChartStyle](#areachartstyle) | ✓ | Chart-local style overrides. |
190
190
 
191
191
  <a id="scatterchart"></a>
@@ -212,8 +212,8 @@ Authored patch for scatter charts.
212
212
  | `sort` | [ChartSort](#chartsort) | ✓ | Sort configuration: field to sort by and direction (asc/desc). |
213
213
  | `labels` | [ChartLabels](#chartlabels) | ✓ | Per-row text annotations near each data anchor. |
214
214
  | `data_table` | [ChartDataTable](#chartdatatable) | ✓ | Optional mini data-grid attached below/above the chart. |
215
- | `height` | int \| float | ✓ | Explicit chart height in pixels. Positive number only. When set, overrides aspect_ratio and theme cascade. Not valid on kpi, table, callout, or spark_bar those renderers own their own sizing contracts. |
216
- | `width` | int \| float | ✓ | Chart width hint in pixels. Positive number only. Used by the label-overlap heuristic to determine whether x-axis labels need tilting. Does not override the layout slot width — the chart still fills its allocated container. Not valid on kpi, table, callout, or spark_bar those renderers own their own sizing contracts. |
215
+ | `height` | int \| float | ✓ | Explicit chart height in pixels. Positive number only. When set, overrides aspect_ratio and theme cascade. Only valid on cartesian chart families: area, bar, heatmap, histogram, line, and scatter. Other chart families use renderer-owned or layout-owned sizing contracts. |
216
+ | `width` | int \| float | ✓ | Chart width hint in pixels. Positive number only. Used by the label-overlap heuristic to determine whether x-axis labels need tilting. Does not override the layout slot width — the chart still fills its allocated container. Only valid on cartesian chart families: area, bar, heatmap, histogram, line, and scatter. Other chart families use renderer-owned or layout-owned sizing contracts. |
217
217
  | `size` | str | ✓ | Field used to size-encode data points (quantitative). |
218
218
  | `shape` | str | ✓ | Field used to shape-encode data points (categorical). |
219
219
  | `style` | [ScatterChartStyle](#scatterchartstyle) | ✓ | Chart-local style overrides. |
@@ -242,8 +242,8 @@ Authored patch for heatmap charts.
242
242
  | `sort` | [ChartSort](#chartsort) | ✓ | Sort configuration: field to sort by and direction (asc/desc). |
243
243
  | `labels` | [ChartLabels](#chartlabels) | ✓ | Per-row text annotations near each data anchor. |
244
244
  | `data_table` | [ChartDataTable](#chartdatatable) | ✓ | Optional mini data-grid attached below/above the chart. |
245
- | `height` | int \| float | ✓ | Explicit chart height in pixels. Positive number only. When set, overrides aspect_ratio and theme cascade. Not valid on kpi, table, callout, or spark_bar those renderers own their own sizing contracts. |
246
- | `width` | int \| float | ✓ | Chart width hint in pixels. Positive number only. Used by the label-overlap heuristic to determine whether x-axis labels need tilting. Does not override the layout slot width — the chart still fills its allocated container. Not valid on kpi, table, callout, or spark_bar those renderers own their own sizing contracts. |
245
+ | `height` | int \| float | ✓ | Explicit chart height in pixels. Positive number only. When set, overrides aspect_ratio and theme cascade. Only valid on cartesian chart families: area, bar, heatmap, histogram, line, and scatter. Other chart families use renderer-owned or layout-owned sizing contracts. |
246
+ | `width` | int \| float | ✓ | Chart width hint in pixels. Positive number only. Used by the label-overlap heuristic to determine whether x-axis labels need tilting. Does not override the layout slot width — the chart still fills its allocated container. Only valid on cartesian chart families: area, bar, heatmap, histogram, line, and scatter. Other chart families use renderer-owned or layout-owned sizing contracts. |
247
247
  | `style` | [HeatmapChartStyle](#heatmapchartstyle) | ✓ | Chart-local style overrides. |
248
248
 
249
249
  <a id="piechart"></a>
@@ -1082,8 +1082,8 @@ Authored overlay for TitleStyle. Board and face titles.
1082
1082
  |-------|------|:--------:|-------------|
1083
1083
  | `font` | [FontStyle](#fontstyle) | ✓ | Title font style overrides. |
1084
1084
  | `line_height` | float | ✓ | Line height multiplier for titles and markdown headings. Headings typically want a tighter multiplier than body prose. |
1085
- | `sizes` | list[float] | ✓ | Font sizes for the H1–H6 heading ramp, indexed by ``face.level - 1``. Combined with ``width_offsets`` at render time to size titles responsively by card width. |
1086
- | `width_offsets` | [TitleWidthOffsetsStyle](#titlewidthoffsetsstyle) | ✓ | Additive level offsets by card width (tiny/narrow/medium/wide). Added to the title's base level before indexing ``sizes``. Consumed by chart_title_spec / face_title_spec. |
1085
+ | `sizes` | list[float] | ✓ | Font sizes for the H1–H6 heading ramp, indexed by ``face.level - 1``. Face/prose titles index this ramp directly (no width input). Object titles (chart/table/spark) combine ``width_offsets`` with a fixed object-title anchor to pick a slot. |
1086
+ | `width_offsets` | [TitleWidthOffsetsStyle](#titlewidthoffsetsstyle) | ✓ | Additive level offsets by card width (tiny/narrow/medium/wide). Consumed by ``chart_title_spec`` only — object titles add the tier offset to a fixed anchor to pick an H slot from ``sizes``. Face/prose titles are level-only and do not consult these. |
1087
1087
  | `min_height` | float | ✓ | Minimum title row height in pixels. |
1088
1088
  | `overflow` | str | ✓ | Text overflow mode (clip, truncate, wrap-two, wrap). |
1089
1089
  | `position` | [TitlePositionStyle](#titlepositionstyle) | ✓ | Vega-Lite title positioning: anchor, angle, offset, baseline. |
@@ -2372,10 +2372,11 @@ Authored overlay for SparkAreaStyle.
2372
2372
 
2373
2373
  <a id="geoshapemarkstyle"></a>
2374
2374
  ## GeoshapeMarkStyle
2375
- Authored overlay for GeoshapeMarkStyle. Geoshape (choropleth) mark boundary stroke.
2375
+ Authored overlay for GeoshapeMarkStyle. Geoshape (choropleth) mark fill and boundary stroke.
2376
2376
 
2377
2377
  | Field | Type | Optional | Description |
2378
2378
  |-------|------|:--------:|-------------|
2379
+ | `fill` | str | ✓ | Neutral geoshape fill color. |
2379
2380
  | `stroke` | [StrokeStyle](#strokestyle) | ✓ | Geoshape boundary stroke style. |
2380
2381
 
2381
2382
  <a id="circlemarkstyle"></a>
@@ -17,7 +17,6 @@ else:
17
17
  # Verb-forwarder imports use SUBMODULES so test monkeypatches on the
18
18
  # module attribute affect the call site.
19
19
  from dataface.agent_api import (
20
- dashboards as _dashboards,
21
20
  describe as _describe,
22
21
  describe_query as _describe_query,
23
22
  query as _query,
@@ -42,7 +41,7 @@ from dataface.core.inspect.query_validator import (
42
41
  validate_query as _core_validate_query,
43
42
  )
44
43
  from dataface.core.project import Project
45
- from dataface.core.project_roots import discover_render_context
44
+ from dataface.core.project_roots import find_project_root
46
45
 
47
46
  if TYPE_CHECKING:
48
47
  from dataface.core.dashboard import RenderedDashboard, RenderFormat
@@ -71,7 +70,6 @@ class ProjectSession:
71
70
  cache: DuckDBCache | None
72
71
  _owns_registry: bool
73
72
  _read_only: bool
74
- _dbt_project_path: Path | None
75
73
  _connection_string: str | None
76
74
  _dialect: str
77
75
  _target: str
@@ -96,7 +94,6 @@ class ProjectSession:
96
94
  # short-circuit the build on first access.
97
95
  self.__dict__["adapter_registry"] = adapter_registry
98
96
  self._read_only = read_only
99
- self._dbt_project_path = None
100
97
  self._connection_string = None
101
98
  self._dialect = "duckdb"
102
99
  self._target = "dev"
@@ -111,7 +108,6 @@ class ProjectSession:
111
108
  *,
112
109
  cache: DuckDBCache | None = None,
113
110
  read_only: bool = True,
114
- dbt_project_path: Path | None = None,
115
111
  connection_string: str | None = None,
116
112
  dialect: str = "duckdb",
117
113
  target: str = "dev",
@@ -135,7 +131,6 @@ class ProjectSession:
135
131
  cache=cache,
136
132
  read_only=read_only,
137
133
  )
138
- session._dbt_project_path = dbt_project_path
139
134
  session._connection_string = connection_string
140
135
  session._dialect = dialect
141
136
  session._target = target
@@ -151,7 +146,6 @@ class ProjectSession:
151
146
  *,
152
147
  cache: DuckDBCache | None = None,
153
148
  read_only: bool = True,
154
- dbt_project_path: Path | None = None,
155
149
  connection_string: str | None = None,
156
150
  dialect: str = "duckdb",
157
151
  target: str = "dev",
@@ -166,7 +160,6 @@ class ProjectSession:
166
160
  this method stores it directly without re-wrapping.
167
161
  """
168
162
  session = cls(project=project, cache=cache, read_only=read_only)
169
- session._dbt_project_path = dbt_project_path
170
163
  session._connection_string = connection_string
171
164
  session._dialect = dialect
172
165
  session._target = target
@@ -185,13 +178,8 @@ class ProjectSession:
185
178
  ) -> Self:
186
179
  """Open a ProjectSession rooted at the project directory discovered upward from face_file."""
187
180
  resolved = Path(face_file).resolve()
188
- project_root, dbt_project_path = discover_render_context(resolved.parent, None)
189
- return cls.open(
190
- project_root,
191
- read_only=read_only,
192
- cache=cache,
193
- dbt_project_path=dbt_project_path,
194
- )
181
+ project_root = find_project_root(resolved.parent, boundary=None)
182
+ return cls.open(project_root, read_only=read_only, cache=cache)
195
183
 
196
184
  def __enter__(self) -> Self:
197
185
  return self
@@ -210,7 +198,6 @@ class ProjectSession:
210
198
  return build_adapter_registry(
211
199
  self.project,
212
200
  read_only=self._read_only,
213
- dbt_project_path=self._dbt_project_path,
214
201
  connection_string=self._connection_string,
215
202
  profile_type=self._dialect,
216
203
  target=self._target,
@@ -279,11 +266,6 @@ class ProjectSession:
279
266
 
280
267
  # ── Verb forwarders ──────────────────────────────────────────────────────
281
268
 
282
- def validate(self, face_path: Path) -> ValidateResult:
283
- result = _validate.validate(face_path, project=self.project)
284
- annotated = _validate.annotate_with_data_lint([result], project_session=self)
285
- return annotated[0]
286
-
287
269
  def validate_paths(self, paths: list[Path] | None) -> list[ValidateResult]:
288
270
  results = _validate.validate_paths(paths, project=self.project)
289
271
  return _validate.annotate_with_data_lint(results, project_session=self)
@@ -346,32 +328,9 @@ class ProjectSession:
346
328
  relationship_context=self._relationship_context,
347
329
  )
348
330
 
349
- def describe_face(self, path: Path) -> _describe.DescribeFaceResult:
350
- return _describe.describe_face(path, project=self.project)
351
-
352
331
  def describe_paths(self, paths: list[Path]) -> list[_describe.DescribeFaceResult]:
353
332
  return _describe.describe_paths(paths, project=self.project)
354
333
 
355
- def list_dashboards(
356
- self,
357
- directory: Path | None = None,
358
- recursive: bool = True,
359
- ) -> _dashboards.ListDashboardsResult:
360
- """When ``directory`` is None, defaults to ``self.project.root``. When provided, callers are responsible for passing an absolute path; relative paths resolve against the process working directory."""
361
- return _dashboards.list_dashboards(
362
- directory=directory if directory is not None else self.project.root,
363
- recursive=recursive,
364
- )
365
-
366
- def get_dashboard(
367
- self,
368
- path: Path,
369
- include_raw: bool = False,
370
- ) -> _dashboards.CompiledDashboard:
371
- return _dashboards.get_dashboard(
372
- path, include_raw=include_raw, project=self.project
373
- )
374
-
375
334
  def lookup_face_query_sql(
376
335
  self,
377
336
  name: str,
@@ -401,6 +360,7 @@ class ProjectSession:
401
360
  variables: dict[str, Any] | None = None,
402
361
  source: str | None = None,
403
362
  limit: int = 50,
363
+ lenient_variables: bool = False,
404
364
  ) -> _query.ExecuteQueryResult:
405
365
  return _query.execute_query(
406
366
  sql,
@@ -408,6 +368,7 @@ class ProjectSession:
408
368
  source=source,
409
369
  limit=limit,
410
370
  adapter_registry=self.adapter_registry,
371
+ lenient_variables=lenient_variables,
411
372
  )
412
373
 
413
374
  def describe_query(
@@ -88,6 +88,10 @@ class ExecuteQueryArgs(BaseModel):
88
88
  )
89
89
  source: str | None = Field(None, description="Data source name to execute against")
90
90
  limit: int | None = Field(None, description="Maximum rows to return (default 50)")
91
+ lenient_variables: bool = Field(
92
+ False,
93
+ description="When True, undefined Jinja variables degrade gracefully (useful for iterative chart editing where not all variables are set).",
94
+ )
91
95
 
92
96
 
93
97
  class ExecuteQueryResult(BaseModel):
@@ -135,6 +139,7 @@ def execute_query(
135
139
  limit: int = 50,
136
140
  *,
137
141
  adapter_registry: AdapterRegistry,
142
+ lenient_variables: bool = False,
138
143
  ) -> ExecuteQueryResult:
139
144
  """Execute a SQL query and return the results.
140
145
 
@@ -155,6 +160,7 @@ def execute_query(
155
160
  sql=sql,
156
161
  source=source,
157
162
  limit=fetch_limit,
163
+ lenient_variables=lenient_variables,
158
164
  )
159
165
 
160
166
  result = adapter_registry.execute(query, variables=variables)
@@ -15,10 +15,6 @@ from dataface.core.compile.markdown import (
15
15
  )
16
16
  from dataface.core.execute.duckdb_cache import DuckDBCache
17
17
  from dataface.core.project import Project
18
- from dataface.core.project_roots import (
19
- discover_render_context,
20
- discovery_boundary_for_face,
21
- )
22
18
  from dataface.core.render.face_api import compile_and_render
23
19
 
24
20
 
@@ -72,16 +68,7 @@ def render_face(
72
68
  if project is not None:
73
69
  if warnings_ignore is None:
74
70
  warnings_ignore = project.warnings_ignore
75
- # Discover dbt_project.yml between the face and the project root.
76
- # build_adapter_registry's own fallback would only walk upward from
77
- # project.root, missing nested dbt projects under it.
78
- _, dbt_project_path = discover_render_context(
79
- face_file.parent,
80
- discovery_boundary_for_face(face_file.parent, project.root),
81
- )
82
- project_session = ProjectSession.open(
83
- project.root, cache=cache, dbt_project_path=dbt_project_path
84
- )
71
+ project_session = ProjectSession.open(project.root, cache=cache)
85
72
  else:
86
73
  if warnings_ignore is None:
87
74
  warnings_ignore = frozenset()
dataface/ai/context.py CHANGED
@@ -21,6 +21,7 @@ class DatafaceAIContext:
21
21
  # `dft chat` after binding; consumed by `_view_url` to build clickable
22
22
  # localhost URLs in `render_dashboard` responses (including as_link=True).
23
23
  server_port: int | None = None
24
+ prompt_context: dict[str, str] | None = None
24
25
 
25
26
  def resolve_dashboard_path(self, path: Path) -> Path:
26
27
  """Resolve a dashboard path inside the scoped dashboards directory.
dataface/ai/mcp/server.py CHANGED
@@ -180,14 +180,14 @@ def create_server(context: DatafaceAIContext) -> Any:
180
180
  return TypeAdapter(AnyUrl).validate_python(value)
181
181
 
182
182
  @server.list_resources() # type: ignore[no-untyped-call]
183
- async def handle_list_resources() -> list[Resource]:
183
+ async def handle_list_resources() -> list[Resource]: # pyright: ignore[reportUnusedFunction] # decorator-registered — pyright cannot model runtime registration # fmt: skip
184
184
  return [
185
185
  Resource(uri=_uri(u), mimeType=m, name=n, description=d)
186
186
  for u, m, n, d in (*_BASE_RESOURCES, *_docs_topic_resources())
187
187
  ]
188
188
 
189
189
  @server.list_resource_templates() # type: ignore[no-untyped-call]
190
- async def handle_list_resource_templates() -> list[ResourceTemplate]:
190
+ async def handle_list_resource_templates() -> list[ResourceTemplate]: # pyright: ignore[reportUnusedFunction] # decorator-registered — pyright cannot model runtime registration # fmt: skip
191
191
  return [
192
192
  ResourceTemplate(
193
193
  uriTemplate="dataface://dashboard/{path}",
@@ -204,11 +204,11 @@ def create_server(context: DatafaceAIContext) -> Any:
204
204
  ]
205
205
 
206
206
  @server.read_resource() # type: ignore[no-untyped-call]
207
- async def handle_read_resource(uri: AnyUrl) -> str:
207
+ async def handle_read_resource(uri: AnyUrl) -> str: # pyright: ignore[reportUnusedFunction] # decorator-registered — pyright cannot model runtime registration # fmt: skip
208
208
  return _read_resource_content(str(uri), context=context)
209
209
 
210
210
  @server.list_tools() # type: ignore[no-untyped-call]
211
- async def handle_list_tools() -> list[Tool]:
211
+ async def handle_list_tools() -> list[Tool]: # pyright: ignore[reportUnusedFunction] # decorator-registered — pyright cannot model runtime registration # fmt: skip
212
212
  return [
213
213
  Tool(
214
214
  name=cast(str, t["name"]),
@@ -234,7 +234,7 @@ def create_server(context: DatafaceAIContext) -> Any:
234
234
  ]
235
235
 
236
236
  @server.call_tool()
237
- async def handle_call_tool(
237
+ async def handle_call_tool( # pyright: ignore[reportUnusedFunction] # decorator-registered — pyright cannot model runtime registration
238
238
  name: str, arguments: dict[str, Any] | None
239
239
  ) -> CallToolResult:
240
240
  arguments = arguments or {}
@@ -19,6 +19,13 @@ metadata:
19
19
 
20
20
  **`{{ s_render_dashboard }}` is how you deliver a dashboard** — call it and return its output. Do not skip it.
21
21
 
22
+ ## Delivery discipline
23
+
24
+ - `{{ s_render_dashboard }}` delivers the dashboard — call it and let the result stand. Do not also paste the rendered output or the full YAML back as prose; that just duplicates the deliverable as an unreadable dump.
25
+ - Act, don't ask. Apply edits and render directly — do not ask for permission or offer optional follow-ups ("if you want, I can save this", "would you like me to…").
26
+ - Fail loud. If a tool returns an error, report it and either fix the input and call the tool again, or stop and say what's blocking. Never route around a failed tool by fabricating output, dumping query rows as a text table in place of a chart, or improvising a different format. A failed render is a failure to report, not to paper over.
27
+ - Never claim a render, query, or save succeeded unless the tool returned success. Never invent columns, tables, or data — if a schema or query call fails, fix it against the real schema.
28
+
22
29
  Build dashboards and reports **incrementally** — one chart at a time, validating at every step. Never one-shot an entire dashboard.
23
30
 
24
31
  ## Companion Skills
@@ -140,11 +140,11 @@ class PromptToolkitInput:
140
140
  kb = KeyBindings()
141
141
 
142
142
  @kb.add("enter")
143
- def _submit(event: Any) -> None:
143
+ def _submit(event: Any) -> None: # pyright: ignore[reportUnusedFunction] # decorator-registered — pyright cannot model runtime registration # fmt: skip
144
144
  event.current_buffer.validate_and_handle()
145
145
 
146
146
  @kb.add("escape", "enter")
147
- def _newline_meta(event: Any) -> None:
147
+ def _newline_meta(event: Any) -> None: # pyright: ignore[reportUnusedFunction] # decorator-registered — pyright cannot model runtime registration # fmt: skip
148
148
  event.current_buffer.insert_text("\n")
149
149
 
150
150
  self._session: PromptSession = PromptSession(
@@ -205,7 +205,7 @@ def select_input_layer(
205
205
  return StdlibInput(hist_path)
206
206
 
207
207
  try:
208
- import prompt_toolkit # noqa: PLC0415, F401
208
+ import prompt_toolkit # noqa: PLC0415, F401 # pyright: ignore[reportUnusedImport] — availability probe; ImportError selects StdlibInput
209
209
  except ImportError:
210
210
  return StdlibInput(hist_path)
211
211
 
@@ -129,11 +129,7 @@ def render_command(
129
129
 
130
130
  with (
131
131
  cache_from_env() as cache,
132
- ProjectSession.open(
133
- ctx.project_root,
134
- cache=cache,
135
- dbt_project_path=ctx.dbt_project_path,
136
- ) as project_session,
132
+ ProjectSession.open(ctx.project_root, cache=cache) as project_session,
137
133
  ):
138
134
  result = project_session.render_dashboard(
139
135
  path=ctx.scoped_path,
@@ -219,11 +215,7 @@ def render_command_from_yaml(
219
215
 
220
216
  with (
221
217
  cache_from_env() as cache,
222
- ProjectSession.open(
223
- ctx.project_root,
224
- cache=cache,
225
- dbt_project_path=ctx.dbt_project_path,
226
- ) as project_session,
218
+ ProjectSession.open(ctx.project_root, cache=cache) as project_session,
227
219
  ):
228
220
  result = project_session.render_dashboard(
229
221
  yaml_content=yaml_content,
@@ -120,7 +120,6 @@ from dataface.core.compile.models.source import (
120
120
  is_database_source,
121
121
  is_file_source,
122
122
  parse_source_config,
123
- resolve_env_var,
124
123
  )
125
124
  from dataface.core.compile.models.variable.authored import (
126
125
  Variable,
@@ -201,7 +200,6 @@ __all__ = [
201
200
  "is_database_source",
202
201
  "is_file_source",
203
202
  "is_api_source",
204
- "resolve_env_var",
205
203
  # Config
206
204
  "ProjectSourcesConfig",
207
205
  "get_config",
@@ -52,11 +52,6 @@ def parse_style_channel(
52
52
  if isinstance(raw, str):
53
53
  return ResolvedStyleChannel(channel=channel_name, mode="series", data_field=raw)
54
54
 
55
- if not isinstance(raw, dict):
56
- raise ValueError(
57
- f"Channel '{channel_name}' must be a string or dict, got {type(raw).__name__}"
58
- )
59
-
60
55
  has_column = "column" in raw
61
56
  has_value = "value" in raw
62
57
  has_scale = "scale" in raw
@@ -188,7 +183,7 @@ def normalize_chart_channels(
188
183
  channels["color"] = parse_style_channel(color_raw, "color")
189
184
 
190
185
  # Upgrade series channel to gradient when style.color carries a scale.
191
- if isinstance(style_color, StyleColorConfig) and style_color.scale is not None:
186
+ if isinstance(style_color, StyleColorConfig):
192
187
  if "color" not in channels:
193
188
  raise ValueError(
194
189
  "style.color: {scale: ...} requires chart.color to name a data field.\n"
@@ -43,7 +43,7 @@ def sanitize_color(color: str | None, fallback: str | None = None) -> str | None
43
43
  immediately — authored colors that are not valid are a configuration error,
44
44
  not a case for silent defaulting.
45
45
  """
46
- if not color or not isinstance(color, str):
46
+ if not color:
47
47
  return fallback
48
48
  if _CSS_HEX_COLOR_PATTERN.match(color):
49
49
  return color
@@ -271,9 +271,7 @@ def compile(
271
271
  yaml_content: YAML string to compile
272
272
  options: Optional compilation options
273
273
  base_dir: Base directory for resolving file references
274
- project_sources: Project-level sources, ready to use. Callers that
275
- hold a ``ProjectSession`` thread ``project_session.sources``;
276
- one-shot callers pre-load via ``load_project_sources(Project(project_dir))``.
274
+ project_sources: Project-level sources.
277
275
 
278
276
  Returns:
279
277
  CompileResult with compiled face or errors
@@ -440,6 +438,7 @@ def compile_file(
440
438
  apply_meta: bool = True,
441
439
  *,
442
440
  project: Project,
441
+ markdown_metadata_table: bool = False,
443
442
  ) -> CompileResult:
444
443
  """Compile a YAML file to a Face.
445
444
 
@@ -451,6 +450,8 @@ def compile_file(
451
450
  options: Optional compilation options
452
451
  apply_meta: If True, resolve and apply meta.yaml chain (default: True)
453
452
  project: Project for meta resolution and source loading
453
+ markdown_metadata_table: When True and file_path is a .md, prepend
454
+ non-face frontmatter keys as a metadata table before the body.
454
455
 
455
456
  Returns:
456
457
  CompileResult with compiled face or errors
@@ -471,13 +472,14 @@ def compile_file(
471
472
  )
472
473
 
473
474
  # Markdown report files: translate to YAML before compiling
474
- if file_path.suffix.lower() == ".md":
475
- from dataface.core.compile.markdown import (
476
- MARKDOWN_NOT_FACE_MESSAGE,
477
- is_markdown_face,
478
- markdown_to_yaml,
479
- )
475
+ from dataface.core.compile.markdown import (
476
+ MARKDOWN_NOT_FACE_MESSAGE,
477
+ MARKDOWN_SUFFIXES,
478
+ is_markdown_face,
479
+ markdown_to_yaml,
480
+ )
480
481
 
482
+ if file_path.suffix.lower() in MARKDOWN_SUFFIXES:
481
483
  if not is_markdown_face(file_path):
482
484
  return CompileResult(
483
485
  errors=[CompilationError(MARKDOWN_NOT_FACE_MESSAGE).to_structured()]
@@ -485,7 +487,9 @@ def compile_file(
485
487
 
486
488
  try:
487
489
  raw_text = file_path.read_text(encoding="utf-8")
488
- yaml_content = markdown_to_yaml(raw_text)
490
+ yaml_content = markdown_to_yaml(
491
+ raw_text, metadata_table=markdown_metadata_table
492
+ )
489
493
  except (OSError, ValueError) as e:
490
494
  return CompileResult(
491
495
  errors=[CompilationError(f"Markdown parse error: {e}").to_structured()]
@@ -820,10 +824,8 @@ def load_from_reference(
820
824
  section_name = "variables"
821
825
  elif isinstance(reference, QueryRef):
822
826
  section_name = "queries"
823
- elif isinstance(reference, ChartRef):
824
- section_name = "charts"
825
827
  else:
826
- raise CompilationError(f"Unexpected reference type: {type(reference)!r}")
828
+ section_name = "charts"
827
829
 
828
830
  # Grammar already validated by the typed model; split is deterministic.
829
831
  file_path_str, item_name = reference.ref.rsplit(f".{section_name}.", 1)