dataface 0.1.5.dev322__py3-none-any.whl → 0.1.5.dev384__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 (95) hide show
  1. d3_format/format.py +9 -0
  2. dataface/DATAFACE_SYNTAX.md +3 -0
  3. dataface/agent_api/docs/_loader.py +1 -1
  4. dataface/agent_api/docs/yaml-reference.md +16 -16
  5. dataface/agent_api/mcp_install.py +2 -3
  6. dataface/ai/mcp/server.py +5 -5
  7. dataface/ai/tool_schemas.py +2 -28
  8. dataface/ai/tools/__init__.py +6 -4
  9. dataface/cli/commands/serve.py +7 -4
  10. dataface/cli/main.py +13 -1
  11. dataface/core/compile/colors.py +39 -23
  12. dataface/core/compile/compiler.py +2 -4
  13. dataface/core/compile/config.py +71 -8
  14. dataface/core/compile/data_table_attachment.py +26 -24
  15. dataface/core/compile/inherit_graph.py +47 -21
  16. dataface/core/compile/inherit_resolver.py +86 -0
  17. dataface/core/compile/introspection.py +1 -1
  18. dataface/core/compile/jinja.py +8 -50
  19. dataface/core/compile/models/chart/authored.py +24 -24
  20. dataface/core/compile/models/chart/normalized.py +14 -6
  21. dataface/core/compile/models/config.py +12 -7
  22. dataface/core/compile/models/markers.py +7 -7
  23. dataface/core/compile/models/query/normalized.py +19 -12
  24. dataface/core/compile/models/source.py +33 -81
  25. dataface/core/compile/models/style/resolved.py +22 -18
  26. dataface/core/compile/models/style/theme.py +52 -38
  27. dataface/core/compile/normalize_charts.py +6 -3
  28. dataface/core/compile/normalize_queries.py +3 -0
  29. dataface/core/compile/normalize_variables.py +102 -14
  30. dataface/core/compile/normalizer.py +15 -0
  31. dataface/core/compile/schema_renderers/prompt.py +4 -14
  32. dataface/core/compile/schema_renderers/vscode_schema.py +4 -15
  33. dataface/core/compile/sizing.py +21 -14
  34. dataface/core/compile/style_cascade.py +9 -14
  35. dataface/core/compile/yaml_error_formatter.py +46 -11
  36. dataface/core/dashboard.py +19 -6
  37. dataface/core/defaults/default_config.yml +12 -0
  38. dataface/core/defaults/themes/_base.yaml +5 -0
  39. dataface/core/defaults/themes/editorial.yaml +3 -0
  40. dataface/core/errors/__init__.py +2 -0
  41. dataface/core/errors/codes_render.py +16 -0
  42. dataface/core/errors/codes_serve.py +2 -3
  43. dataface/core/errors/structured.py +2 -2
  44. dataface/core/execute/adapters/adapter_registry.py +6 -0
  45. dataface/core/execute/adapters/dbt_adapter_factory.py +14 -2
  46. dataface/core/execute/adapters/http_adapter.py +12 -15
  47. dataface/core/execute/adapters/sql_adapter.py +175 -17
  48. dataface/core/execute/batch.py +45 -0
  49. dataface/core/execute/duckdb_cache.py +84 -8
  50. dataface/core/execute/executor.py +132 -133
  51. dataface/core/inspect/query_validator.py +1 -0
  52. dataface/core/inspect/templates/categorical_column.yml +0 -2
  53. dataface/core/inspect/templates/charts.yml +0 -2
  54. dataface/core/inspect/templates/date_column.yml +0 -2
  55. dataface/core/inspect/templates/model.yml +0 -2
  56. dataface/core/inspect/templates/numeric_column.yml +0 -2
  57. dataface/core/inspect/templates/quality.yml +0 -2
  58. dataface/core/inspect/templates/string_column.yml +0 -2
  59. dataface/core/project_roots.py +1 -1
  60. dataface/core/registered_views/query_runner.py +29 -1
  61. dataface/core/registered_views/render_pipeline.py +1 -1
  62. dataface/core/render/chart/geo.py +91 -17
  63. dataface/core/render/chart/pipeline.py +3 -0
  64. dataface/core/render/chart/profile.py +47 -55
  65. dataface/core/render/chart/serialization.py +2 -1
  66. dataface/core/render/chart/standard_renderer.py +48 -0
  67. dataface/core/render/chart/table.py +46 -42
  68. dataface/core/render/chart/table_support.py +5 -7
  69. dataface/core/render/chart/vl_field_maps.py +11 -0
  70. dataface/core/render/control_registry.py +1 -3
  71. dataface/core/render/errors.py +11 -5
  72. dataface/core/render/faces.py +4 -4
  73. dataface/core/render/geo_defaults.yml +16 -0
  74. dataface/core/render/nav.py +14 -32
  75. dataface/core/render/renderer.py +23 -13
  76. dataface/core/render/templates/controls/_styles.css +15 -12
  77. dataface/core/render/templates/controls/select.html +0 -1
  78. dataface/core/render/templates/nav/nav.css +11 -11
  79. dataface/core/render/templates/nav/nav.html +56 -1
  80. dataface/core/render/terminal.py +7 -3
  81. dataface/core/render/utils.py +19 -12
  82. dataface/core/render/variable_controls.py +19 -71
  83. dataface/core/serve/bootstrap.py +37 -22
  84. dataface/core/serve/port.py +16 -6
  85. dataface/core/serve/server.py +295 -218
  86. dataface/core/serve/templates/error.html.j2 +8 -7
  87. {dataface-0.1.5.dev322.dist-info → dataface-0.1.5.dev384.dist-info}/METADATA +1 -1
  88. {dataface-0.1.5.dev322.dist-info → dataface-0.1.5.dev384.dist-info}/RECORD +93 -94
  89. mdsvg/fonts.py +4 -1
  90. mdsvg/renderer.py +13 -6
  91. dataface/core/render/markdown_defaults.yml +0 -16
  92. dataface/core/render/missing_vars_prompt.py +0 -79
  93. {dataface-0.1.5.dev322.dist-info → dataface-0.1.5.dev384.dist-info}/WHEEL +0 -0
  94. {dataface-0.1.5.dev322.dist-info → dataface-0.1.5.dev384.dist-info}/entry_points.txt +0 -0
  95. {dataface-0.1.5.dev322.dist-info → dataface-0.1.5.dev384.dist-info}/licenses/LICENSE +0 -0
d3_format/format.py CHANGED
@@ -14,6 +14,7 @@ from __future__ import annotations
14
14
  import math
15
15
  from collections.abc import Callable
16
16
  from decimal import ROUND_HALF_UP, Decimal
17
+ from typing import overload
17
18
 
18
19
  from d3_format.spec import FormatSpec, parse
19
20
 
@@ -520,6 +521,14 @@ def _apply_spec(spec: FormatSpec, v: float | int | Decimal) -> str:
520
521
  return _assemble(spec, sign_char, body, suffix)
521
522
 
522
523
 
524
+ @overload
525
+ def format(spec_str: str) -> Callable[[float | int | Decimal], str]: ... # noqa: A001
526
+
527
+
528
+ @overload
529
+ def format(spec_str: str, value: float | int | Decimal) -> str: ... # noqa: A001
530
+
531
+
523
532
  def format( # noqa: A001
524
533
  spec_str: str,
525
534
  value: float | int | Decimal | None = None,
@@ -427,6 +427,8 @@ WHERE {{ filter('plan', plans) }}
427
427
 
428
428
  The `filter()` macro handles `select` (single value → `=`) and `multiselect` (list → `IN (...)`) automatically and quotes all string literals correctly.
429
429
 
430
+ Select and multiselect controls render only the options the face author provides. Dataface does not add an "All" option; author a real sentinel option and matching SQL/Jinja explicitly if a dashboard needs one.
431
+
430
432
  ### Inline query in a chart
431
433
 
432
434
  A chart can carry its own one-off query instead of referencing a named one. Three equivalent forms:
@@ -672,6 +674,7 @@ lookup: state
672
674
  value: revenue
673
675
 
674
676
  # map — generic choropleth (alias for geoshape with named source)
677
+ # world-countries/world-50m join on numeric TopoJSON ids such as 840, not ISO alpha codes.
675
678
  type: map
676
679
  geo_source: us-states
677
680
  lookup: state
@@ -58,7 +58,7 @@ class DocsSearchHit(BaseModel):
58
58
 
59
59
 
60
60
  class DocsArgs(BaseModel):
61
- """Input model for agent_api.docs drives MCP inputSchema."""
61
+ """Browse the Dataface YAML reference offline. Modes: no args = topic index (slug + one-line description per H2), topic='<slug>' = one section, topic='all' = whole reference unsliced, search='<query>' = substring search across topics. Use this before writing YAML to learn field names, valid values, and examples. Call with no args first to see the available topics."""
62
62
 
63
63
  topic: str | None = Field(
64
64
  None,
@@ -1457,8 +1457,8 @@ Authored overlay for BarChartMarksStyle. Bar-family mark overrides. Only bar and
1457
1457
 
1458
1458
  | Field | Type | Optional | Description |
1459
1459
  |-------|------|:--------:|-------------|
1460
- | `bar` | [BarMarkStyle](#barmarkstyle) | ✓ | Bar mark overrides for bar charts; None inherits global. |
1461
- | `text` | [TextMarkStyle](#textmarkstyle) | ✓ | Text mark overrides for bar endpoint labels; None inherits global. |
1460
+ | `bar` | [BarMarkStyle](#barmarkstyle) | ✓ | Bar mark overrides for bar charts; inherits from global. |
1461
+ | `text` | [TextMarkStyle](#textmarkstyle) | ✓ | Text mark overrides for bar endpoint labels; inherits from global. |
1462
1462
 
1463
1463
  <a id="linechartmarksstyle"></a>
1464
1464
  ## LineChartMarksStyle
@@ -1466,9 +1466,9 @@ Authored overlay for LineChartMarksStyle. Line-family mark overrides.
1466
1466
 
1467
1467
  | Field | Type | Optional | Description |
1468
1468
  |-------|------|:--------:|-------------|
1469
- | `line` | [LineMarkStyle](#linemarkstyle) | ✓ | Line mark overrides; None inherits global. |
1470
- | `point` | [PointMarkStyle](#pointmarkstyle) | ✓ | Point mark overrides; None inherits global. |
1471
- | `text` | [TextMarkStyle](#textmarkstyle) | ✓ | Text mark overrides; None inherits global. |
1469
+ | `line` | [LineMarkStyle](#linemarkstyle) | ✓ | Line mark overrides; inherits from global. |
1470
+ | `point` | [PointMarkStyle](#pointmarkstyle) | ✓ | Point mark overrides; inherits from global. |
1471
+ | `text` | [TextMarkStyle](#textmarkstyle) | ✓ | Text mark overrides; inherits from global. |
1472
1472
  | `rule` | [RuleMarkStyle](#rulemarkstyle) | ✓ | Rule mark overrides; None inherits global. |
1473
1473
 
1474
1474
  <a id="areachartmarksstyle"></a>
@@ -1477,10 +1477,10 @@ Authored overlay for AreaChartMarksStyle. Area-family mark overrides.
1477
1477
 
1478
1478
  | Field | Type | Optional | Description |
1479
1479
  |-------|------|:--------:|-------------|
1480
- | `area` | [AreaMarkStyle](#areamarkstyle) | ✓ | Area mark overrides; None inherits global. |
1481
- | `line` | [LineMarkStyle](#linemarkstyle) | ✓ | Top-edge line mark overrides; None inherits global. |
1482
- | `point` | [PointMarkStyle](#pointmarkstyle) | ✓ | Point mark overrides; None inherits global. |
1483
- | `text` | [TextMarkStyle](#textmarkstyle) | ✓ | Text mark overrides; None inherits global. |
1480
+ | `area` | [AreaMarkStyle](#areamarkstyle) | ✓ | Area mark overrides; inherits from global. |
1481
+ | `line` | [LineMarkStyle](#linemarkstyle) | ✓ | Top-edge line mark overrides; inherits from global. |
1482
+ | `point` | [PointMarkStyle](#pointmarkstyle) | ✓ | Point mark overrides; inherits from global. |
1483
+ | `text` | [TextMarkStyle](#textmarkstyle) | ✓ | Text mark overrides; inherits from global. |
1484
1484
 
1485
1485
  <a id="scatterchartmarksstyle"></a>
1486
1486
  ## ScatterChartMarksStyle
@@ -1488,8 +1488,8 @@ Authored overlay for ScatterChartMarksStyle. Scatter-family mark overrides.
1488
1488
 
1489
1489
  | Field | Type | Optional | Description |
1490
1490
  |-------|------|:--------:|-------------|
1491
- | `point` | [PointMarkStyle](#pointmarkstyle) | ✓ | Point mark overrides; None inherits global. |
1492
- | `text` | [TextMarkStyle](#textmarkstyle) | ✓ | Text mark overrides; None inherits global. |
1491
+ | `point` | [PointMarkStyle](#pointmarkstyle) | ✓ | Point mark overrides; inherits from global. |
1492
+ | `text` | [TextMarkStyle](#textmarkstyle) | ✓ | Text mark overrides; inherits from global. |
1493
1493
 
1494
1494
  <a id="heatmapchartmarksstyle"></a>
1495
1495
  ## HeatmapChartMarksStyle
@@ -1497,7 +1497,7 @@ Authored overlay for HeatmapChartMarksStyle. Heatmap-family mark overrides.
1497
1497
 
1498
1498
  | Field | Type | Optional | Description |
1499
1499
  |-------|------|:--------:|-------------|
1500
- | `rect` | [RectMarkStyle](#rectmarkstyle) | ✓ | Rect mark overrides; None inherits global. |
1500
+ | `rect` | [RectMarkStyle](#rectmarkstyle) | ✓ | Rect mark overrides; inherits from global. |
1501
1501
  | `text` | [TextMarkStyle](#textmarkstyle) | ✓ | Text mark overrides; None inherits global. |
1502
1502
 
1503
1503
  <a id="totalstyle"></a>
@@ -1515,7 +1515,7 @@ Authored overlay for PieChartMarksStyle. Pie/donut-family mark overrides.
1515
1515
 
1516
1516
  | Field | Type | Optional | Description |
1517
1517
  |-------|------|:--------:|-------------|
1518
- | `slice` | [SliceMarkStyle](#slicemarkstyle) | ✓ | Slice mark overrides; None inherits global. |
1518
+ | `slice` | [SliceMarkStyle](#slicemarkstyle) | ✓ | Slice mark overrides; inherits from global. |
1519
1519
  | `text` | [TextMarkStyle](#textmarkstyle) | ✓ | Text mark overrides; None inherits global. |
1520
1520
 
1521
1521
  <a id="kpivaluestyle"></a>
@@ -1720,7 +1720,7 @@ Authored overlay for PointMapChartMarksStyle. Point map-family mark overrides.
1720
1720
 
1721
1721
  | Field | Type | Optional | Description |
1722
1722
  |-------|------|:--------:|-------------|
1723
- | `point` | [PointMarkStyle](#pointmarkstyle) | ✓ | Point mark overrides; None inherits global. |
1723
+ | `point` | [PointMarkStyle](#pointmarkstyle) | ✓ | Point mark overrides; inherits from global. |
1724
1724
 
1725
1725
  <a id="geoshapechartmarksstyle"></a>
1726
1726
  ## GeoshapeChartMarksStyle
@@ -1728,7 +1728,7 @@ Authored overlay for GeoshapeChartMarksStyle. Geoshape-family mark overrides.
1728
1728
 
1729
1729
  | Field | Type | Optional | Description |
1730
1730
  |-------|------|:--------:|-------------|
1731
- | `geoshape` | [GeoshapeMarkStyle](#geoshapemarkstyle) | ✓ | Geoshape mark overrides; None inherits global. |
1731
+ | `geoshape` | [GeoshapeMarkStyle](#geoshapemarkstyle) | ✓ | Geoshape mark overrides; inherits from global. |
1732
1732
 
1733
1733
  <a id="layeraxisystyle"></a>
1734
1734
  ## LayerAxisYStyle
@@ -2441,7 +2441,7 @@ Authored overlay for HistogramChartMarksStyle. Histogram-family mark overrides.
2441
2441
 
2442
2442
  | Field | Type | Optional | Description |
2443
2443
  |-------|------|:--------:|-------------|
2444
- | `bar` | [BarMarkStyle](#barmarkstyle) | ✓ | Bar mark overrides; None inherits global. |
2444
+ | `bar` | [BarMarkStyle](#barmarkstyle) | ✓ | Bar mark overrides; inherits from global. |
2445
2445
  | `rule` | [RuleMarkStyle](#rulemarkstyle) | ✓ | Rule mark overrides; None inherits global. |
2446
2446
 
2447
2447
  <a id="detailsarrowstyle"></a>
@@ -126,9 +126,8 @@ def _upsert_mcp_config(
126
126
  existing: dict[str, Any] = {}
127
127
  if config_path.exists():
128
128
  try:
129
- existing = json.loads(config_path.read_text(encoding="utf-8"))
130
- if not isinstance(existing, dict):
131
- existing = {}
129
+ loaded = json.loads(config_path.read_text(encoding="utf-8"))
130
+ existing = loaded if isinstance(loaded, dict) else {}
132
131
  except (json.JSONDecodeError, OSError):
133
132
  existing = {}
134
133
 
dataface/ai/mcp/server.py CHANGED
@@ -178,14 +178,14 @@ def create_server(context: DatafaceAIContext) -> Any:
178
178
  def _uri(value: str) -> AnyUrl:
179
179
  return TypeAdapter(AnyUrl).validate_python(value)
180
180
 
181
- @server.list_resources()
181
+ @server.list_resources() # type: ignore[no-untyped-call]
182
182
  async def handle_list_resources() -> list[Resource]:
183
183
  return [
184
184
  Resource(uri=_uri(u), mimeType=m, name=n, description=d)
185
185
  for u, m, n, d in (*_BASE_RESOURCES, *_docs_topic_resources())
186
186
  ]
187
187
 
188
- @server.list_resource_templates()
188
+ @server.list_resource_templates() # type: ignore[no-untyped-call]
189
189
  async def handle_list_resource_templates() -> list[ResourceTemplate]:
190
190
  return [
191
191
  ResourceTemplate(
@@ -202,11 +202,11 @@ def create_server(context: DatafaceAIContext) -> Any:
202
202
  ),
203
203
  ]
204
204
 
205
- @server.read_resource()
206
- async def handle_read_resource(uri: str) -> str:
205
+ @server.read_resource() # type: ignore[no-untyped-call]
206
+ async def handle_read_resource(uri: AnyUrl) -> str:
207
207
  return _read_resource_content(str(uri), context=context)
208
208
 
209
- @server.list_tools()
209
+ @server.list_tools() # type: ignore[no-untyped-call]
210
210
  async def handle_list_tools() -> list[Tool]:
211
211
  return [
212
212
  Tool(
@@ -14,6 +14,7 @@ from pydantic import BaseModel
14
14
  from dataface.agent_api.dashboards import RenderDashboardArgs
15
15
  from dataface.agent_api.describe import DescribeFaceArgs
16
16
  from dataface.agent_api.describe_query import DescribeQueryArgs
17
+ from dataface.agent_api.docs import DocsArgs
17
18
  from dataface.agent_api.docs.warnings import GetWarningCodeArgs
18
19
  from dataface.agent_api.files import (
19
20
  EditFileArgs,
@@ -47,34 +48,7 @@ SCHEMA = _ai_tool("schema", SchemaArgs)
47
48
  SCHEMA_SEARCH = _ai_tool("schema_search", SchemaSearchArgs)
48
49
  SEARCH_DASHBOARDS = _ai_tool("search_dashboards", SearchDashboardsArgs)
49
50
 
50
- DOCS = {
51
- "name": "docs",
52
- "description": (
53
- "Browse the Dataface YAML reference offline. "
54
- "Modes: no args = topic index (slug + one-line description per H2), "
55
- "topic='<slug>' = one section, topic='all' = whole reference unsliced, "
56
- "search='<query>' = substring search across topics. "
57
- "Use this before writing YAML to learn field names, valid values, "
58
- "and examples. Call with no args first to see the available topics."
59
- ),
60
- "input_schema": {
61
- "type": "object",
62
- "properties": {
63
- "topic": {
64
- "type": "string",
65
- "description": "Topic slug (e.g. 'face', 'charts', 'cheatsheet') or 'all' for the whole file. Omit for the topic index.",
66
- },
67
- "search": {
68
- "type": "string",
69
- "description": "Substring query — scans all H2 sections and returns ranked hits",
70
- },
71
- "limit": {
72
- "type": "integer",
73
- "description": "Max search hits to return (default 5, max 50)",
74
- },
75
- },
76
- },
77
- }
51
+ DOCS = _ai_tool("docs", DocsArgs)
78
52
 
79
53
  LIST_WARNING_CODES = {
80
54
  "name": "list_warning_codes",
@@ -20,6 +20,7 @@ from dataface.agent_api import (
20
20
  search as _search,
21
21
  skills as _skills,
22
22
  )
23
+ from dataface.agent_api.query import ExecuteQueryArgs as _ExecuteQueryArgs
23
24
  from dataface.agent_api.validate import (
24
25
  ValidateDashboardArgs as _ValidateDashboardArgs,
25
26
  annotate_with_data_lint as _annotate_with_data_lint,
@@ -120,11 +121,12 @@ def _handle_render(args: dict[str, Any], ctx: DatafaceAIContext) -> dict[str, An
120
121
 
121
122
 
122
123
  def _handle_query(args: dict[str, Any], ctx: DatafaceAIContext) -> dict[str, Any]:
124
+ parsed = _ExecuteQueryArgs.model_validate(args)
123
125
  return _query.execute_query(
124
- sql=args.get("sql", ""),
125
- variables=args.get("variables"),
126
- source=args.get("source"),
127
- limit=args.get("limit", 50),
126
+ sql=parsed.sql,
127
+ variables=parsed.variables,
128
+ source=parsed.source,
129
+ limit=parsed.limit or 50,
128
130
  adapter_registry=ctx.project.adapter_registry,
129
131
  ).model_dump(mode="json", exclude_none=True)
130
132
 
@@ -20,6 +20,7 @@ def serve_command(
20
20
  dialect: str | None = None,
21
21
  target: str | None = None,
22
22
  max_workers: int | None = None,
23
+ no_cache: bool = False,
23
24
  ) -> None:
24
25
  """Start unified Dataface server.
25
26
 
@@ -46,21 +47,22 @@ def serve_command(
46
47
  None (exits with code 0 on success, 1 on errors)
47
48
  """
48
49
  from dataface.core.project_roots import find_dft_root, infer_dialect_from_dbt
49
- from dataface.core.serve.bootstrap import apply_default_theme_from_env
50
+ from dataface.core.serve.bootstrap import apply_default_theme
50
51
  from dataface.core.serve.port import resolve_port
51
52
  from dataface.core.serve.server import create_server
52
53
 
53
54
  # Resolve the project root: --project-dir if given, else discover from cwd.
54
55
  project_dir = project_dir or find_dft_root() or Path.cwd()
55
56
 
56
- # Apply DFT_DEFAULT_THEME before anything else fails fast on invalid names.
57
+ # Resolve default theme (env var > dataface.yml theme: > built-in default).
58
+ # Fails fast on invalid values.
57
59
  try:
58
- apply_default_theme_from_env()
60
+ apply_default_theme(project_dir)
59
61
  except DatafaceError as e:
60
62
  print_structured_errors([e.to_structured()])
61
63
  raise typer.Exit(1) from None
62
64
 
63
- # Resolve port: --port > DFT_PORT > dataface.yml > hash(project_dir)
65
+ # Resolve port: --port > DFT_PORT > dataface.yml server.port > hash(project_dir)
64
66
  port = resolve_port(
65
67
  explicit_port=port,
66
68
  project_dir=project_dir,
@@ -85,6 +87,7 @@ def serve_command(
85
87
  dialect=effective_dialect,
86
88
  target=effective_target,
87
89
  max_workers=max_workers,
90
+ no_cache=no_cache,
88
91
  )
89
92
 
90
93
  # Start server
dataface/cli/main.py CHANGED
@@ -911,6 +911,17 @@ def serve(
911
911
  ),
912
912
  ),
913
913
  ] = None,
914
+ no_cache: Annotated[
915
+ bool,
916
+ typer.Option(
917
+ "--no-cache",
918
+ help=(
919
+ "Disable the persistent query cache. By default dft serve caches "
920
+ "results in <project>/.dft/cache.duckdb across page loads. "
921
+ "Use --no-cache to disable (matches today's behavior)."
922
+ ),
923
+ ),
924
+ ] = False,
914
925
  ) -> None:
915
926
  """Start the dashboard server.
916
927
 
@@ -929,7 +940,7 @@ def serve(
929
940
  4. ~/.dbt/profiles.yml
930
941
 
931
942
  Port is auto-resolved: --port flag > DFT_PORT env var > dataface.yml
932
- port field > deterministic hash of project directory. If the chosen port
943
+ server.port > deterministic hash of project directory. If the chosen port
933
944
  is occupied, the next available port is used automatically.
934
945
 
935
946
  \b
@@ -948,6 +959,7 @@ def serve(
948
959
  dialect=dialect,
949
960
  target=target,
950
961
  max_workers=max_workers,
962
+ no_cache=no_cache,
951
963
  )
952
964
 
953
965
 
@@ -1,7 +1,6 @@
1
1
  """Color utility functions for Dataface renderers.
2
2
 
3
- Provides is_dark_color, is_sanitizable_color, and sanitize_color for use across
4
- compile and render layers.
3
+ Provides color helpers for use across compile and render layers.
5
4
  """
6
5
 
7
6
  from __future__ import annotations
@@ -9,9 +8,16 @@ from __future__ import annotations
9
8
  import re
10
9
  from typing import overload
11
10
 
11
+ from PIL import ImageColor
12
+
12
13
  from dataface.core.compile.errors import CompilationError
13
14
 
14
15
  _CSS_HEX_COLOR_PATTERN = re.compile(r"^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$")
16
+ _CSS_RGBA_COLOR_PATTERN = re.compile(
17
+ r"^rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*"
18
+ r"((?:0(?:\.\d+)?)|(?:1(?:\.0+)?)|(?:\.\d+))\s*\)$",
19
+ re.IGNORECASE,
20
+ )
15
21
 
16
22
 
17
23
  def is_sanitizable_color(color: str) -> bool:
@@ -22,31 +28,41 @@ def is_sanitizable_color(color: str) -> bool:
22
28
  }
23
29
 
24
30
 
25
- def is_dark_color(color: str) -> bool:
26
- """Check if a color is dark based on luminance.
31
+ def _color_channels(color: str) -> tuple[int, int, int]:
32
+ rgba_match = _CSS_RGBA_COLOR_PATTERN.match(color)
33
+ if rgba_match:
34
+ r, g, b = (int(value) for value in rgba_match.groups()[:3])
35
+ if all(0 <= value <= 255 for value in (r, g, b)):
36
+ return (r, g, b)
37
+ raise CompilationError(f"mix_colors expects CSS colors, got {color!r}")
27
38
 
28
- Uses the relative luminance formula to determine if a color is
29
- dark (luminance < 0.5) or light (luminance >= 0.5).
30
- """
31
- if not color or not color.startswith("#"):
32
- return False
39
+ try:
40
+ parsed = ImageColor.getrgb(color)
41
+ except ValueError as exc:
42
+ raise CompilationError(f"mix_colors expects CSS colors, got {color!r}") from exc
33
43
 
34
- hex_color = color.lstrip("#")
35
- if len(hex_color) == 3:
36
- hex_color = "".join(c * 2 for c in hex_color)
44
+ r, g, b = int(parsed[0]), int(parsed[1]), int(parsed[2])
45
+ if not all(0 <= value <= 255 for value in (r, g, b)):
46
+ raise CompilationError(f"mix_colors expects CSS colors, got {color!r}")
47
+ return (r, g, b)
37
48
 
38
- if len(hex_color) != 6:
39
- return False
40
49
 
41
- try:
42
- r = int(hex_color[0:2], 16) / 255.0
43
- g = int(hex_color[2:4], 16) / 255.0
44
- b = int(hex_color[4:6], 16) / 255.0
45
- except ValueError:
46
- return False
47
-
48
- luminance = 0.299 * r + 0.587 * g + 0.114 * b
49
- return luminance < 0.5
50
+ def mix_colors(base: str, overlay: str, fraction: float) -> str:
51
+ """Blend ``fraction`` of ``overlay`` into ``base``. Returns 6-digit hex.
52
+
53
+ Used to derive subtle surface/secondary tiers from theme tokens — e.g. a code
54
+ background that is the page background nudged toward the text color — so the
55
+ result tracks the theme on both light and dark backgrounds instead of pinning a
56
+ constant. ``fraction`` is clamped to [0, 1]; 0 returns ``base``, 1 returns ``overlay``.
57
+ """
58
+
59
+ t = min(1.0, max(0.0, fraction))
60
+ br, bg, bb = _color_channels(base)
61
+ o_r, o_g, o_b = _color_channels(overlay)
62
+ r = round(br + (o_r - br) * t)
63
+ g = round(bg + (o_g - bg) * t)
64
+ b = round(bb + (o_b - bb) * t)
65
+ return f"#{r:02x}{g:02x}{b:02x}"
50
66
 
51
67
 
52
68
  @overload
@@ -437,7 +437,7 @@ def validate_compiled_queries(
437
437
 
438
438
 
439
439
  def compile_file(
440
- file_path: Path,
440
+ file_path: str | Path,
441
441
  options: dict[str, Any] | None = None,
442
442
  apply_meta: bool = True,
443
443
  root_path: Path | None = None,
@@ -468,9 +468,7 @@ def compile_file(
468
468
  >>> if result.success:
469
469
  ... print(result.face.title)
470
470
  """
471
-
472
- if isinstance(file_path, str):
473
- file_path = Path(file_path)
471
+ file_path = Path(file_path)
474
472
 
475
473
  if not file_path.exists():
476
474
  return CompileResult(
@@ -23,6 +23,7 @@ YAML is the single source of truth - no Python dataclass defaults.
23
23
 
24
24
  from __future__ import annotations
25
25
 
26
+ import os
26
27
  from collections.abc import Mapping
27
28
  from copy import deepcopy
28
29
  from functools import cache
@@ -36,8 +37,8 @@ from dataface.core.compile.models.config import (
36
37
  ConfigNode,
37
38
  ConfigPatch,
38
39
  ExecutionConfig,
39
- MarkdownConfig,
40
40
  RenderingConfig,
41
+ ServerConfig,
41
42
  VegaRuntimeConfig,
42
43
  as_plain_mapping,
43
44
  is_mapping_like,
@@ -61,7 +62,6 @@ _core_dir: Path = Path(__file__).parent.parent
61
62
  _MODULE_DEFAULT_PATHS: list[Path] = [
62
63
  _core_dir / "inspect" / "defaults.yml",
63
64
  _core_dir / "render" / "geo_defaults.yml",
64
- _core_dir / "render" / "markdown_defaults.yml",
65
65
  _core_dir / "render" / "terminal_defaults.yml",
66
66
  ]
67
67
 
@@ -122,11 +122,6 @@ def get_chart_rendering() -> ConfigNode:
122
122
  return get_config().chart_rendering
123
123
 
124
124
 
125
- def get_markdown_config() -> MarkdownConfig:
126
- """Narrow getter — markdown rendering color config."""
127
- return get_config().markdown
128
-
129
-
130
125
  def get_terminal_config() -> ConfigNode:
131
126
  """Narrow getter — terminal rendering defaults."""
132
127
  return get_config().terminal
@@ -147,6 +142,42 @@ def get_execution_config() -> ExecutionConfig:
147
142
  return get_config().execution
148
143
 
149
144
 
145
+ def resolve_max_workers(explicit: int | None) -> int:
146
+ """Resolve the query-parallelism width: explicit arg → DFT_MAX_WORKERS → config.
147
+
148
+ Single source of truth for both the render-time worker pool and the
149
+ per-source connection pool, so the two never disagree on width.
150
+ """
151
+ if explicit is not None:
152
+ return explicit
153
+ env_val = os.getenv("DFT_MAX_WORKERS") # noqa: TID251 — DFT_MAX_WORKERS knob
154
+ if env_val is not None:
155
+ return int(env_val)
156
+ return get_execution_config().max_workers
157
+
158
+
159
+ def get_project_server_config(project_dir: Path) -> ServerConfig:
160
+ """Read project-level ``server:`` settings from dataface.yml/yaml.
161
+
162
+ Only the ``server:`` section is validated here. dataface.yml is also used
163
+ as a lightweight project marker in tests and examples, so unrelated top-level
164
+ keys must not make serve startup fail.
165
+ """
166
+ base = get_config().server.to_plain_dict(exclude_none=False)
167
+ resolved_dir = project_dir.resolve()
168
+ for candidate in (resolved_dir / "dataface.yml", resolved_dir / "dataface.yaml"):
169
+ if candidate.exists():
170
+ file_data = _as_mapping(_load_yaml_data(candidate), candidate.name)
171
+ server_section = file_data.get("server")
172
+ if server_section is None:
173
+ break
174
+ server_data = _as_mapping(
175
+ server_section, f"{candidate.name} server section"
176
+ )
177
+ return ServerConfig.model_validate(_deep_merge(base, server_data))
178
+ return ServerConfig.model_validate(base)
179
+
180
+
150
181
  def load_config(path: Path) -> Config:
151
182
  """Load configuration from a custom YAML file.
152
183
 
@@ -444,7 +475,7 @@ def set_default_theme_name(theme_name: str) -> None:
444
475
  """Override the in-process default theme name.
445
476
 
446
477
  Validates the theme exists before patching. Raises ValueError on unknown names.
447
- Called at serve startup by apply_default_theme_from_env(); not for general use.
478
+ Called at serve startup by apply_default_theme(); not for general use.
448
479
 
449
480
  Args:
450
481
  theme_name: A built-in theme stem (e.g. "carbong100", "light").
@@ -628,6 +659,38 @@ def get_project_warnings_ignore(project_dir: Path) -> frozenset[str]:
628
659
  return frozenset()
629
660
 
630
661
 
662
+ # ============================================================================
663
+ # PROJECT DEFAULT THEME
664
+ # ============================================================================
665
+
666
+
667
+ def get_project_default_theme(project_dir: Path) -> str | None:
668
+ """Return the project-declared default theme from ``dataface.yml``.
669
+
670
+ Reads the top-level ``theme:`` key. Returns ``None`` when the file is
671
+ absent or the key is unauthored. Raises ``TypeError`` for any non-string
672
+ value so misauthored configs fail loud at startup.
673
+
674
+ Validation of the theme name (must be a built-in theme) lives in
675
+ ``apply_default_theme`` so that env-var and config-file inputs share one
676
+ structured-error path.
677
+ """
678
+ resolved_dir = project_dir.resolve()
679
+ for candidate in (resolved_dir / "dataface.yml", resolved_dir / "dataface.yaml"):
680
+ if candidate.exists():
681
+ file_data = _as_mapping(_load_yaml_data(candidate), candidate.name)
682
+ if "theme" not in file_data:
683
+ return None
684
+ value = file_data["theme"]
685
+ if not isinstance(value, str):
686
+ raise TypeError(
687
+ f"{candidate.name}: theme must be a string, "
688
+ f"got {type(value).__name__}: {value!r}"
689
+ )
690
+ return value
691
+ return None
692
+
693
+
631
694
  # ============================================================================
632
695
  # META CONFIG CLASSES
633
696
  # ============================================================================