dataface 0.1.6.dev345__py3-none-any.whl → 0.1.6.dev360__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dataface/agent_api/docs/yaml-reference.md +1 -0
- dataface/ai/prompts/sql-guidance.md +2 -2
- dataface/core/compile/models/face/authored.py +13 -0
- dataface/core/compile/models/face/normalized.py +9 -0
- dataface/core/compile/normalizer.py +1 -0
- dataface/core/registered_views/expander.py +0 -38
- dataface/core/registered_views/link_keys.py +42 -0
- dataface/core/registered_views/templates/data/table-index.yaml +4 -11
- dataface/core/render/board_links.py +35 -13
- dataface/core/render/chart/auto_link.py +187 -0
- dataface/core/render/chart/render_single.py +32 -0
- dataface/core/render/renderer.py +5 -0
- {dataface-0.1.6.dev345.dist-info → dataface-0.1.6.dev360.dist-info}/METADATA +1 -1
- {dataface-0.1.6.dev345.dist-info → dataface-0.1.6.dev360.dist-info}/RECORD +17 -15
- {dataface-0.1.6.dev345.dist-info → dataface-0.1.6.dev360.dist-info}/WHEEL +0 -0
- {dataface-0.1.6.dev345.dist-info → dataface-0.1.6.dev360.dist-info}/entry_points.txt +0 -0
- {dataface-0.1.6.dev345.dist-info → dataface-0.1.6.dev360.dist-info}/licenses/LICENSE +0 -0
|
@@ -31,6 +31,7 @@ AuthoredFace (dataface) definition from YAML.
|
|
|
31
31
|
| `height` | str \| int | ✓ | Height when nested (e.g., '300px' or an integer in pixels). |
|
|
32
32
|
| `visible` | bool \| str \| [SingleRowBoolProbe](#singlerowboolprobe) | ✓ | Controls whether this layout item is rendered. Accepts a bool, variable name, Jinja expression, or {query, column} probe. |
|
|
33
33
|
| `theme` | str | ✓ | Theme name (e.g., 'editorial', 'cream', 'stark'). |
|
|
34
|
+
| `auto_link` | bool | ✓ | When True, table charts with no explicit link: automatically link each row to its canonical /data/<source>/<schema>/<table>/detail/ page. Default off. Explicit link: always wins; set link: ~ to suppress per chart. |
|
|
34
35
|
|
|
35
36
|
<a id="sourcessection"></a>
|
|
36
37
|
## SourcesSection
|
|
@@ -4,11 +4,11 @@ Before writing SQL, write a short query plan:
|
|
|
4
4
|
|
|
5
5
|
- **OUTPUT**: the columns to return, with clear aliases that describe the value
|
|
6
6
|
- **FROM/JOIN**: the tables and the join keys between them
|
|
7
|
-
- **GRAIN**: what one row represents after the joins; guard against fan-out / double-counting on the non-unique side of a join — if a join multiplies rows, aggregate before joining or add a GROUP BY
|
|
7
|
+
- **GRAIN**: what one row represents after the joins; guard against fan-out / double-counting on the non-unique side of a join — if a join multiplies rows, aggregate before joining or add a GROUP BY. If the schema context already states a join's multiplicity (e.g. one-to-many) or a column's uniqueness, use that to decide grain — don't spend a query probing for what the schema already tells you
|
|
8
8
|
- **FILTERS**: each WHERE condition; verify the exact stored value before filtering (stored values may differ from display labels — e.g. `'LA'` not `'Los Angeles'`)
|
|
9
9
|
- **SORT/LIMIT**: include if the question asks to rank, sort, or return a top-N
|
|
10
10
|
|
|
11
|
-
Then translate the plan faithfully into SQL. Verify by executing the query and checking that shape and values match the plan.
|
|
11
|
+
Then translate the plan faithfully into SQL. Verify by executing the query and checking that shape and values match the plan. When you run a query, heed any diagnostics it returns — a `fanout_risk`, `missing_join_predicate`, or `reaggregation` warning means the join or aggregate is probably double-counting even though the query ran; fix the grain before trusting the numbers.
|
|
12
12
|
|
|
13
13
|
Additional rules:
|
|
14
14
|
|
|
@@ -609,6 +609,19 @@ class AuthoredFace(BaseModel):
|
|
|
609
609
|
description="Theme name (e.g., 'editorial', 'cream', 'stark').",
|
|
610
610
|
)
|
|
611
611
|
|
|
612
|
+
# Auto-link: synthesize a detail-page link for table charts when no explicit
|
|
613
|
+
# link: is set. Default off — opt in at the face level (per-face override).
|
|
614
|
+
# Explicit link: always wins; link: none suppresses per chart.
|
|
615
|
+
# dft serve-scoped for Phase 1; project-level opt-in is deferred.
|
|
616
|
+
auto_link: bool = Field(
|
|
617
|
+
default=False,
|
|
618
|
+
description=(
|
|
619
|
+
"When True, table charts with no explicit link: automatically link each "
|
|
620
|
+
"row to its canonical /data/<source>/<schema>/<table>/detail/ page. "
|
|
621
|
+
"Default off. Explicit link: always wins; set link: ~ to suppress per chart."
|
|
622
|
+
),
|
|
623
|
+
)
|
|
624
|
+
|
|
612
625
|
@model_validator(mode="before")
|
|
613
626
|
@classmethod
|
|
614
627
|
def reject_face_key(cls, data: Any) -> Any:
|
|
@@ -381,6 +381,15 @@ class Face(BaseModel):
|
|
|
381
381
|
description="When True, adds gap between cards and adjusts page margin.",
|
|
382
382
|
)
|
|
383
383
|
|
|
384
|
+
# Auto-link: synthesize detail-page links for table charts with no explicit link:
|
|
385
|
+
auto_link: bool = Field(
|
|
386
|
+
default=False,
|
|
387
|
+
description=(
|
|
388
|
+
"When True, table charts with no explicit link: automatically link each "
|
|
389
|
+
"row to its canonical /data/<source>/<schema>/<table>/detail/ page."
|
|
390
|
+
),
|
|
391
|
+
)
|
|
392
|
+
|
|
384
393
|
# Semantic heading level: count of titled ancestors (not structural nesting depth).
|
|
385
394
|
# A titled root face is level=1; a titled face nested under a bare wrapper is still
|
|
386
395
|
# level=2 (only one titled ancestor). Bare wrappers without a title do not advance
|
|
@@ -36,10 +36,6 @@ Template globals:
|
|
|
36
36
|
param (integers, exact decimals, strings). The detail view and the
|
|
37
37
|
index→detail link use this so a numeric ``id`` primary key actually selects one
|
|
38
38
|
row, where the browse-filter set would have dropped it.
|
|
39
|
-
- ``plan_link_keys(rows)`` — the minimal, URL-safe id-like key for the index→detail
|
|
40
|
-
*link* (a single ``id``, else ``*_id`` columns; integers/exact-decimals only).
|
|
41
|
-
Narrower than ``plan_key_variables`` so the link can't drop on a NULL non-key
|
|
42
|
-
column or be corrupted by an unencoded string value.
|
|
43
39
|
- ``sql_identifier(name)`` — returns a safely ANSI-double-quoted SQL identifier
|
|
44
40
|
(``"`` chars inside ``name`` are escaped as ``""``). Use for any path param that
|
|
45
41
|
appears inside a SQL ``FROM`` or column reference to prevent SQL injection.
|
|
@@ -170,40 +166,6 @@ def _plan_key_variables(
|
|
|
170
166
|
_TEMPLATE_ENV.globals["plan_key_variables"] = _plan_key_variables
|
|
171
167
|
|
|
172
168
|
|
|
173
|
-
def _plan_link_keys(
|
|
174
|
-
rows: list[dict[str, Any]],
|
|
175
|
-
) -> list[dict[str, str]]:
|
|
176
|
-
"""Pick the minimal, URL-safe row-identity key for the index→detail link.
|
|
177
|
-
|
|
178
|
-
The cell-link renderer drops the WHOLE link if any referenced column is NULL
|
|
179
|
-
and does no percent-encoding, so the link key must be both non-null-by-
|
|
180
|
-
convention and URL-safe. We use the id naming convention as the proxy for a
|
|
181
|
-
primary key: a single exact ``id`` column when present, else every ``*_id``
|
|
182
|
-
column (composite foreign-key identity). Restricted to URL-safe numeric types
|
|
183
|
-
(``is_url_safe_key``) — a string/UUID id would need encoding the renderer does
|
|
184
|
-
not do, so such tables get no auto-link rather than a corruptible one.
|
|
185
|
-
|
|
186
|
-
Returns a list of ``{"name": col}`` (the link only needs names); empty when no
|
|
187
|
-
id-like URL-safe column exists.
|
|
188
|
-
"""
|
|
189
|
-
from dataface.core.registered_views.variable_planner import PlannerColumn
|
|
190
|
-
|
|
191
|
-
cols = [PlannerColumn(name=r["name"], actual_type=r["actual_type"]) for r in rows]
|
|
192
|
-
id_like = [
|
|
193
|
-
c
|
|
194
|
-
for c in cols
|
|
195
|
-
if c.is_valid_variable_id
|
|
196
|
-
and c.is_url_safe_key
|
|
197
|
-
and (c.name == "id" or c.name.endswith("_id"))
|
|
198
|
-
]
|
|
199
|
-
exact = [c for c in id_like if c.name == "id"]
|
|
200
|
-
chosen = exact if exact else id_like
|
|
201
|
-
return [{"name": c.name} for c in chosen]
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
_TEMPLATE_ENV.globals["plan_link_keys"] = _plan_link_keys
|
|
205
|
-
|
|
206
|
-
|
|
207
169
|
def _sql_identifier(name: str) -> str:
|
|
208
170
|
"""Return a safely double-quoted SQL identifier.
|
|
209
171
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Shared key-selection helper for auto-link and registered-view templates.
|
|
2
|
+
|
|
3
|
+
A single copy lives here; both the Jinja template global (expander.py) and the
|
|
4
|
+
render-time auto-link resolver (render/chart/auto_link.py) call this function
|
|
5
|
+
so the selection logic cannot drift between the two.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def plan_link_keys(rows: list[dict[str, str]]) -> list[dict[str, str]]:
|
|
12
|
+
"""Pick the minimal, URL-safe row-identity key for the index→detail link.
|
|
13
|
+
|
|
14
|
+
The cell-link renderer drops the WHOLE link if any referenced column is NULL
|
|
15
|
+
and does no percent-encoding, so the link key must be both non-null-by-
|
|
16
|
+
convention and URL-safe. Uses the id naming convention as the proxy for a
|
|
17
|
+
primary key: a single exact ``id`` column when present, else every ``*_id``
|
|
18
|
+
column (composite foreign-key identity). Restricted to URL-safe numeric types
|
|
19
|
+
(``is_url_safe_key``) — a string/UUID id would need encoding the renderer does
|
|
20
|
+
not do, so such tables get no auto-link rather than a corruptible one.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
rows: Column metadata rows, each with ``name`` and ``actual_type`` keys.
|
|
24
|
+
Matches the format returned by the schema profiler / ViewQueryResult.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
A list of ``{"name": col}`` dicts (the link only needs names); empty when no
|
|
28
|
+
id-like URL-safe column exists.
|
|
29
|
+
"""
|
|
30
|
+
from dataface.core.registered_views.variable_planner import PlannerColumn
|
|
31
|
+
|
|
32
|
+
cols = [PlannerColumn(name=r["name"], actual_type=r["actual_type"]) for r in rows]
|
|
33
|
+
id_like = [
|
|
34
|
+
c
|
|
35
|
+
for c in cols
|
|
36
|
+
if c.is_valid_variable_id
|
|
37
|
+
and c.is_url_safe_key
|
|
38
|
+
and (c.name == "id" or c.name.endswith("_id"))
|
|
39
|
+
]
|
|
40
|
+
exact = [c for c in id_like if c.name == "id"]
|
|
41
|
+
chosen = exact if exact else id_like
|
|
42
|
+
return [{"name": c.name} for c in chosen]
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
title: [[ (path.source ~ "/" ~ path.schema ~ "/" ~ path.table) | tojson ]]
|
|
2
2
|
|
|
3
|
+
# The auto_link resolver fires at render time and synthesizes the detail-page
|
|
4
|
+
# link for each row using plan_link_keys — no hand-rolled link: needed here.
|
|
5
|
+
auto_link: true
|
|
6
|
+
|
|
3
7
|
[% set vars = plan_variables(queries.columns.rows) %]
|
|
4
8
|
[% if vars %]
|
|
5
9
|
variables:
|
|
@@ -10,15 +14,6 @@ variables:
|
|
|
10
14
|
[% endfor %]
|
|
11
15
|
[% endif %]
|
|
12
16
|
|
|
13
|
-
# The detail link carries only the minimal, URL-safe id-like key (see
|
|
14
|
-
# plan_link_keys): never a nullable non-key column (the renderer drops the whole
|
|
15
|
-
# link if any referenced column is NULL) and never an unencoded string value.
|
|
16
|
-
[% set link_keys = plan_link_keys(queries.columns.rows) %]
|
|
17
|
-
[% set ns = namespace(qs='') %]
|
|
18
|
-
[% for k in link_keys %]
|
|
19
|
-
[% set ns.qs = ns.qs ~ ('?' if loop.first else '&') ~ k.name ~ '={{ ' ~ k.name ~ ' }}' %]
|
|
20
|
-
[% endfor %]
|
|
21
|
-
|
|
22
17
|
queries:
|
|
23
18
|
rows:
|
|
24
19
|
type: sql
|
|
@@ -34,8 +29,6 @@ charts:
|
|
|
34
29
|
title: [[ path.table | tojson ]]
|
|
35
30
|
type: table
|
|
36
31
|
query: rows
|
|
37
|
-
[% if link_keys %] link: [[ ("/data/" ~ path.source ~ "/" ~ path.schema ~ "/" ~ path.table ~ "/detail/" ~ ns.qs) | tojson ]]
|
|
38
|
-
[% endif %]
|
|
39
32
|
|
|
40
33
|
rows:
|
|
41
34
|
- row_data
|
|
@@ -44,20 +44,25 @@ class LinkContext:
|
|
|
44
44
|
"""Runtime context for resolving board links.
|
|
45
45
|
|
|
46
46
|
Attributes:
|
|
47
|
-
runtime: ``"serve"``
|
|
47
|
+
runtime: ``"serve"`` for local dft serve; ``"cloud"`` for Cloud surfaces.
|
|
48
|
+
Only the ``"serve"`` runtime triggers the ``file://`` fallback for
|
|
49
|
+
relative links that leave the served faces tree (requires
|
|
50
|
+
``faces_root`` + ``board_file_path``).
|
|
48
51
|
current_board_slug: Author-space slug of the board being rendered
|
|
49
52
|
(e.g. ``"zendesk/tickets/list"``).
|
|
50
53
|
storage_prefix: On-disk directory prefix for serve mode (default ``"faces"``).
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
url_prefix: When non-empty, root-relative slug paths are prefixed with
|
|
55
|
+
this string instead of ``storage_prefix``. Cloud callers set this to
|
|
56
|
+
``/{org}/{project}``. Dashboard slugs get a ``/d/`` infix and trailing
|
|
57
|
+
slash (``/{org}/{project}/d/zendesk/overview/``); system-view slugs
|
|
58
|
+
(``data/``) are emitted bare (``/{org}/{project}/data/src/schema/``).
|
|
59
|
+
branch: Current branch name to merge into outbound links.
|
|
54
60
|
"""
|
|
55
61
|
|
|
56
62
|
runtime: str # "serve" | "cloud"
|
|
57
63
|
current_board_slug: str = ""
|
|
58
64
|
storage_prefix: str = "faces"
|
|
59
|
-
|
|
60
|
-
project_slug: str = ""
|
|
65
|
+
url_prefix: str = ""
|
|
61
66
|
branch: str = ""
|
|
62
67
|
# Serve-only: the on-disk root that slugs resolve against (the served faces
|
|
63
68
|
# dir) and the current board's file path. Together they let a relative link
|
|
@@ -75,7 +80,8 @@ def resolve_href(href: str, ctx: LinkContext) -> str:
|
|
|
75
80
|
- Root-relative (``/path``) maps from author namespace.
|
|
76
81
|
- Relative (``../``, ``./``, bare) resolves against current board directory.
|
|
77
82
|
- ``.md`` / ``.yml`` / ``.yaml`` suffixes are stripped.
|
|
78
|
-
-
|
|
83
|
+
- When ``url_prefix`` is set, URLs are prefixed with it (e.g. Cloud sets
|
|
84
|
+
``/{org}/{project}``).
|
|
79
85
|
- Serve URLs get ``/{storage_prefix}/{slug}`` shape.
|
|
80
86
|
"""
|
|
81
87
|
for prefix in _PASSTHROUGH_PREFIXES:
|
|
@@ -113,8 +119,8 @@ def resolve_href(href: str, ctx: LinkContext) -> str:
|
|
|
113
119
|
):
|
|
114
120
|
return _build_file_url(ctx.board_file_path, path, query, fragment)
|
|
115
121
|
|
|
116
|
-
if ctx.
|
|
117
|
-
url =
|
|
122
|
+
if ctx.url_prefix:
|
|
123
|
+
url = _build_prefixed_url(resolved, query, ctx)
|
|
118
124
|
else:
|
|
119
125
|
url = _build_serve_url(resolved, query, ctx)
|
|
120
126
|
return f"{url}#{fragment}" if fragment else url
|
|
@@ -171,8 +177,24 @@ def _build_serve_url(slug: str, query: str, ctx: LinkContext) -> str:
|
|
|
171
177
|
return url
|
|
172
178
|
|
|
173
179
|
|
|
174
|
-
|
|
175
|
-
|
|
180
|
+
# System-view route prefixes that Cloud serves outside the /d/<slug>/ dashboard
|
|
181
|
+
# namespace. Slugs starting with these prefixes are emitted as bare project-
|
|
182
|
+
# relative paths (e.g. /org/proj/data/src/schema/); all other slugs are wrapped
|
|
183
|
+
# in the Cloud dashboard infix /d/<slug>/.
|
|
184
|
+
# Only include prefixes with a wired Cloud route — inspector/ is excluded until
|
|
185
|
+
# its Cloud route is added.
|
|
186
|
+
_SYSTEM_VIEW_PREFIXES = ("data/",)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _build_prefixed_url(slug: str, query: str, ctx: LinkContext) -> str:
|
|
190
|
+
# System-view slugs (data/, inspector/) live outside the /d/ dashboard
|
|
191
|
+
# namespace and are served directly under the project prefix.
|
|
192
|
+
if any(slug.startswith(p) for p in _SYSTEM_VIEW_PREFIXES):
|
|
193
|
+
# Preserve the trailing slash — the registered-view router requires it.
|
|
194
|
+
url = f"{ctx.url_prefix}/{slug}"
|
|
195
|
+
else:
|
|
196
|
+
# Dashboard slugs use the Cloud routing infix: /d/<slug>/
|
|
197
|
+
url = f"{ctx.url_prefix}/d/{slug}/"
|
|
176
198
|
|
|
177
199
|
# Merge branch if present in context and not already in query
|
|
178
200
|
if ctx.branch and not _query_has_key(query, "branch"):
|
|
@@ -180,8 +202,8 @@ def _build_cloud_url(slug: str, query: str, ctx: LinkContext) -> str:
|
|
|
180
202
|
query = f"{query}{separator}branch={ctx.branch}"
|
|
181
203
|
|
|
182
204
|
if query:
|
|
183
|
-
|
|
184
|
-
return
|
|
205
|
+
url = f"{url}?{query}"
|
|
206
|
+
return url
|
|
185
207
|
|
|
186
208
|
|
|
187
209
|
def _query_has_key(query: str, key: str) -> bool:
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Auto-link resolver for row-grain table charts (Phase 1).
|
|
2
|
+
|
|
3
|
+
Purpose: When a table chart has no explicit ``link:`` and the face has
|
|
4
|
+
``auto_link: true``, synthesize the canonical
|
|
5
|
+
``/data/<source>/<schema>/<table>/detail/?<key>={{ key }}`` link template so
|
|
6
|
+
the existing cell-link renderer can carry it without any new rendering path.
|
|
7
|
+
|
|
8
|
+
This is render/serve-time (not compile-time) because it needs schema metadata
|
|
9
|
+
(column types from the query result) to select URL-safe keys.
|
|
10
|
+
|
|
11
|
+
Entry points:
|
|
12
|
+
- ``resolve_auto_link(chart, query, column_rows, auto_link=False)`` — pure
|
|
13
|
+
resolver logic, callable from tests without the context machinery.
|
|
14
|
+
- ``set_auto_link_context(enabled)`` / ``get_auto_link_context()`` — set/get
|
|
15
|
+
the per-render-pass flag via a ContextVar so the renderer can flip it on
|
|
16
|
+
once per face without threading the bool through every call-site signature.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import re
|
|
22
|
+
from contextvars import ContextVar
|
|
23
|
+
from typing import TYPE_CHECKING
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from dataface.core.compile.models.chart.normalized import Chart
|
|
27
|
+
from dataface.core.compile.models.query.normalized import AnyQuery
|
|
28
|
+
|
|
29
|
+
# Per-render-pass flag: True when the current face has auto_link=True.
|
|
30
|
+
# Set by the renderer at the start of a render pass, cleared in finally.
|
|
31
|
+
_auto_link_ctx: ContextVar[bool] = ContextVar("_auto_link_ctx", default=False)
|
|
32
|
+
|
|
33
|
+
# Sentinel used by private helpers to signal "bail — no link possible".
|
|
34
|
+
# Callers check `if not result` to detect it.
|
|
35
|
+
_BAIL: tuple[()] = ()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def set_auto_link_context(enabled: bool) -> None:
|
|
39
|
+
"""Set the auto_link flag for the current render pass."""
|
|
40
|
+
_auto_link_ctx.set(enabled)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_auto_link_context() -> bool:
|
|
44
|
+
"""Return the auto_link flag for the current render pass."""
|
|
45
|
+
return _auto_link_ctx.get()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _extract_single_base_table(sql: str) -> tuple[str, str] | tuple[()]:
|
|
49
|
+
"""Extract (schema, table) from SQL with exactly one base table, or _BAIL.
|
|
50
|
+
|
|
51
|
+
Bails on joins, CTEs, subqueries, aggregation (GROUP BY / DISTINCT /
|
|
52
|
+
aggregate functions), catalog-qualified 3-part names, missing schema
|
|
53
|
+
prefix, or parse error.
|
|
54
|
+
|
|
55
|
+
Aggregation is rejected because the resulting rows are not row-grain: a
|
|
56
|
+
link to a per-row detail page would be semantically wrong (the "row-grain"
|
|
57
|
+
contract in the module docstring). Catalog-qualified names are rejected
|
|
58
|
+
because the catalog component would be silently dropped from the URL.
|
|
59
|
+
"""
|
|
60
|
+
import sqlglot
|
|
61
|
+
import sqlglot.errors
|
|
62
|
+
from sqlglot import exp
|
|
63
|
+
|
|
64
|
+
# Strip Jinja tokens so sqlglot can parse the structural skeleton.
|
|
65
|
+
stripped = re.sub(r"\{\{.*?\}\}", "1", sql, flags=re.DOTALL)
|
|
66
|
+
stripped = re.sub(r"\{%.*?%\}", "", stripped, flags=re.DOTALL)
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
tree = sqlglot.parse_one(stripped)
|
|
70
|
+
except sqlglot.errors.ParseError:
|
|
71
|
+
return _BAIL
|
|
72
|
+
|
|
73
|
+
if list(tree.find_all(exp.CTE)):
|
|
74
|
+
return _BAIL
|
|
75
|
+
if list(tree.find_all(exp.Join)):
|
|
76
|
+
return _BAIL
|
|
77
|
+
if list(tree.find_all(exp.Subquery)):
|
|
78
|
+
return _BAIL
|
|
79
|
+
# Aggregation collapses grain — link would point at a wrong detail page.
|
|
80
|
+
if list(tree.find_all(exp.Group)):
|
|
81
|
+
return _BAIL
|
|
82
|
+
if list(tree.find_all(exp.Distinct)):
|
|
83
|
+
return _BAIL
|
|
84
|
+
if list(tree.find_all(exp.AggFunc)):
|
|
85
|
+
return _BAIL
|
|
86
|
+
|
|
87
|
+
tables = list(tree.find_all(exp.Table))
|
|
88
|
+
if len(tables) != 1:
|
|
89
|
+
return _BAIL
|
|
90
|
+
|
|
91
|
+
t = tables[0]
|
|
92
|
+
table_name = t.name
|
|
93
|
+
schema_name = t.db # sqlglot calls the schema qualifier `.db`
|
|
94
|
+
catalog_name = t.catalog # non-empty for mydb.schema.table 3-part names
|
|
95
|
+
|
|
96
|
+
if not table_name or not schema_name:
|
|
97
|
+
return _BAIL
|
|
98
|
+
# Bail on catalog-qualified names — the catalog would be silently dropped
|
|
99
|
+
# from the URL, producing an ambiguous path.
|
|
100
|
+
if catalog_name:
|
|
101
|
+
return _BAIL
|
|
102
|
+
|
|
103
|
+
return schema_name, table_name
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _source_schema_table(query: AnyQuery) -> tuple[str, str, str] | tuple[()]:
|
|
107
|
+
"""Extract (source, schema, table) from a query, or _BAIL."""
|
|
108
|
+
from dataface.core.compile.models.query.normalized import (
|
|
109
|
+
is_schema_query,
|
|
110
|
+
is_sql_query,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if is_schema_query(query):
|
|
114
|
+
if not query.source or not query.schema_name or not query.table:
|
|
115
|
+
return _BAIL
|
|
116
|
+
return query.source, query.schema_name, query.table
|
|
117
|
+
|
|
118
|
+
if is_sql_query(query):
|
|
119
|
+
raw_source = query.source
|
|
120
|
+
if not isinstance(raw_source, str) or not raw_source:
|
|
121
|
+
return _BAIL
|
|
122
|
+
result = _extract_single_base_table(query.sql)
|
|
123
|
+
if not result:
|
|
124
|
+
return _BAIL
|
|
125
|
+
schema, table = result
|
|
126
|
+
return raw_source, schema, table
|
|
127
|
+
|
|
128
|
+
return _BAIL
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def resolve_auto_link(
|
|
132
|
+
chart: Chart,
|
|
133
|
+
query: AnyQuery,
|
|
134
|
+
column_rows: list[dict[str, str]],
|
|
135
|
+
auto_link: bool = False,
|
|
136
|
+
) -> str:
|
|
137
|
+
"""Synthesize the detail-page link template for a row-grain table chart.
|
|
138
|
+
|
|
139
|
+
Called at render time after query execution when the chart has no explicit
|
|
140
|
+
``link:`` and ``auto_link`` is enabled. Returns the synthesized link string
|
|
141
|
+
(e.g. ``/data/dw/analytics/orders/detail/?id={{ id }}``) or an empty string
|
|
142
|
+
to signal "emit no link" (caller checks ``if result:``).
|
|
143
|
+
|
|
144
|
+
Bails (returns ``""``) on:
|
|
145
|
+
- ``auto_link=False``
|
|
146
|
+
- Chart already has an explicit ``link:`` (caller must not override it)
|
|
147
|
+
- Chart type is not ``table``
|
|
148
|
+
- Query is not a ``SchemaQuery`` or ``SqlQuery``
|
|
149
|
+
- SQL query has joins, CTEs, subqueries, or no schema prefix
|
|
150
|
+
- No URL-safe id-like key columns in ``column_rows``
|
|
151
|
+
- Any ambiguity — a wrong link is worse than no link
|
|
152
|
+
|
|
153
|
+
Callers must guard ``query is not None`` before calling — ``query`` must be
|
|
154
|
+
a resolved query (``SchemaQuery`` or ``SqlQuery``). The function does not
|
|
155
|
+
accept ``None`` to avoid ``| None`` in the signature (type-state ratchet).
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
chart: Normalized chart (must be ``type=table`` with ``link=None``).
|
|
159
|
+
query: The chart's resolved query. Only ``SchemaQuery`` and ``SqlQuery``
|
|
160
|
+
are handled; all other query types bail silently.
|
|
161
|
+
column_rows: Column metadata from the query result, each with ``name``
|
|
162
|
+
and ``actual_type`` keys (same format as the schema profiler).
|
|
163
|
+
auto_link: Whether auto-linking is enabled. Default ``False`` (opt-in).
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
A Jinja link template string, or ``""`` when no link should be emitted.
|
|
167
|
+
"""
|
|
168
|
+
if not auto_link:
|
|
169
|
+
return ""
|
|
170
|
+
if chart.link is not None:
|
|
171
|
+
return ""
|
|
172
|
+
if chart.type != "table":
|
|
173
|
+
return ""
|
|
174
|
+
|
|
175
|
+
loc = _source_schema_table(query)
|
|
176
|
+
if not loc:
|
|
177
|
+
return ""
|
|
178
|
+
source, schema, table = loc
|
|
179
|
+
|
|
180
|
+
from dataface.core.registered_views.link_keys import plan_link_keys
|
|
181
|
+
|
|
182
|
+
keys = plan_link_keys(column_rows)
|
|
183
|
+
if not keys:
|
|
184
|
+
return ""
|
|
185
|
+
|
|
186
|
+
qs = "&".join(f"{k['name']}={{{{ {k['name']} }}}}" for k in keys)
|
|
187
|
+
return f"/data/{source}/{schema}/{table}/detail/?{qs}"
|
|
@@ -89,6 +89,38 @@ def _resolve_chart_for_render(
|
|
|
89
89
|
resolved_chart = chart.model_copy(update=updates) if updates else chart
|
|
90
90
|
resolved_chart = resolve_field_names(resolved_chart, data)
|
|
91
91
|
|
|
92
|
+
# Auto-link: synthesize a detail-page link for row-grain table charts when
|
|
93
|
+
# auto_link is enabled for this render pass and no explicit link: is set.
|
|
94
|
+
from dataface.core.render.chart.auto_link import (
|
|
95
|
+
get_auto_link_context,
|
|
96
|
+
resolve_auto_link,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if (
|
|
100
|
+
get_auto_link_context()
|
|
101
|
+
and resolved_chart.link is None
|
|
102
|
+
and chart.query_name
|
|
103
|
+
and resolved_chart.query is not None
|
|
104
|
+
):
|
|
105
|
+
col_descs_for_link = executor.get_column_descriptions(chart.query_name)
|
|
106
|
+
if col_descs_for_link:
|
|
107
|
+
# str(desc[1]) is the PEP 249 type_code. DuckDB returns string type
|
|
108
|
+
# names ("INTEGER", "BIGINT", etc.), which plan_link_keys understands.
|
|
109
|
+
# Other drivers (Postgres, Snowflake) return integer OIDs — auto-link
|
|
110
|
+
# degrades to no-link for those, which is the safe failure mode for Phase 1.
|
|
111
|
+
column_rows = [
|
|
112
|
+
{"name": name, "actual_type": str(desc[1])}
|
|
113
|
+
for name, desc in col_descs_for_link.items()
|
|
114
|
+
]
|
|
115
|
+
synthesized = resolve_auto_link(
|
|
116
|
+
resolved_chart,
|
|
117
|
+
resolved_chart.query,
|
|
118
|
+
column_rows,
|
|
119
|
+
auto_link=True,
|
|
120
|
+
)
|
|
121
|
+
if synthesized:
|
|
122
|
+
resolved_chart = resolved_chart.model_copy(update={"link": synthesized})
|
|
123
|
+
|
|
92
124
|
detect_data = data
|
|
93
125
|
if use_placeholder and not data:
|
|
94
126
|
from dataface.core.render.placeholder import generate_placeholder_data
|
dataface/core/render/renderer.py
CHANGED
|
@@ -382,9 +382,11 @@ def render(
|
|
|
382
382
|
|
|
383
383
|
# Board link rewriting: set context for the duration of this render pass
|
|
384
384
|
from dataface.core.render.board_links import set_link_context
|
|
385
|
+
from dataface.core.render.chart.auto_link import set_auto_link_context
|
|
385
386
|
|
|
386
387
|
link_context = options.get("link_context")
|
|
387
388
|
set_link_context(link_context)
|
|
389
|
+
set_auto_link_context(face.auto_link)
|
|
388
390
|
|
|
389
391
|
try:
|
|
390
392
|
grid_enabled = options.get("grid", False)
|
|
@@ -405,6 +407,7 @@ def render(
|
|
|
405
407
|
# (e.g. DF_RENDER_NO_LAYOUT, MissingRequiredVariablesError). Surface as
|
|
406
408
|
# face_error so callers see the structured code unwrapped.
|
|
407
409
|
set_link_context(None)
|
|
410
|
+
set_auto_link_context(False)
|
|
408
411
|
return RenderResult(
|
|
409
412
|
output=None,
|
|
410
413
|
chart_errors=error_collector,
|
|
@@ -416,6 +419,7 @@ def render(
|
|
|
416
419
|
from dataface.core.errors import DF_RENDER_INTERNAL
|
|
417
420
|
|
|
418
421
|
set_link_context(None)
|
|
422
|
+
set_auto_link_context(False)
|
|
419
423
|
wrapped = RenderError.from_code(DF_RENDER_INTERNAL, inner_message=str(e))
|
|
420
424
|
return RenderResult(
|
|
421
425
|
output=None,
|
|
@@ -426,6 +430,7 @@ def render(
|
|
|
426
430
|
)
|
|
427
431
|
finally:
|
|
428
432
|
set_link_context(None)
|
|
433
|
+
set_auto_link_context(False)
|
|
429
434
|
|
|
430
435
|
# Convert to requested format
|
|
431
436
|
if format == "svg":
|
|
@@ -112,7 +112,7 @@ dataface/core/compile/normalize_charts.py,sha256=KW4dsW1_0SRS3q6cS6upI68-bgm60Gx
|
|
|
112
112
|
dataface/core/compile/normalize_layout.py,sha256=Qv-wtc0c6O0EMnSW6CAuXTwH5j-wzzthqDl2rA7CUdY,34952
|
|
113
113
|
dataface/core/compile/normalize_queries.py,sha256=VZ2yszJU_lTicT_X_p8AalRbXJQ7pUjgUDrwQ8BUE1Q,11951
|
|
114
114
|
dataface/core/compile/normalize_variables.py,sha256=uz5XK7Gc-4omz5gJU1sgvUI32Ph55HhEWwqa17v_Obc,21458
|
|
115
|
-
dataface/core/compile/normalizer.py,sha256=
|
|
115
|
+
dataface/core/compile/normalizer.py,sha256=He1aSaut8CN5-JyLZS98dXif14rcjayP7ObgjoA8mRI,25285
|
|
116
116
|
dataface/core/compile/palette.py,sha256=5yl3fzQPCdYdkgxqf2-D4Sh72M-mh5W_9JiGA5T3hGY,41567
|
|
117
117
|
dataface/core/compile/parameterized.py,sha256=-wNzLLdZ2Nj6DGHW0TShNDBNuRTwxEpukbdQMesAYro,21996
|
|
118
118
|
dataface/core/compile/parser.py,sha256=Itr_OfS3BNK29TCkoNnnoFevvaPUsu7mxAekk_bveQw,7850
|
|
@@ -140,8 +140,8 @@ dataface/core/compile/models/chart/authored.py,sha256=FxYrI8MUMp6fiLH677t7xbLt7z
|
|
|
140
140
|
dataface/core/compile/models/chart/normalized.py,sha256=aJa-C2jcXg03bRUmod-JuW5TJMPWn3swjauiEtvVI7w,15373
|
|
141
141
|
dataface/core/compile/models/chart/resolved.py,sha256=JQz38CKC15SSXueOi98tiFZp_GFsdZKGeMzn-4pqyWA,6336
|
|
142
142
|
dataface/core/compile/models/face/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
143
|
-
dataface/core/compile/models/face/authored.py,sha256=
|
|
144
|
-
dataface/core/compile/models/face/normalized.py,sha256=
|
|
143
|
+
dataface/core/compile/models/face/authored.py,sha256=rjfB2vQuz06oXtZkP4JNxe3SzutblSOych2JckmyATY,23213
|
|
144
|
+
dataface/core/compile/models/face/normalized.py,sha256=rMqvyYRO-ol9_VLM4TC1ciQY5a62kTpdIU39WlrpIOs,17329
|
|
145
145
|
dataface/core/compile/models/face/resolved.py,sha256=btD3f2p_-3IXQOu0tc9CmCaSrAbxOsCG18MLhWdYvLg,3852
|
|
146
146
|
dataface/core/compile/models/query/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
147
147
|
dataface/core/compile/models/query/authored.py,sha256=tfe7KMc22VzxaxSDoQAKjk2Wn5GSn-wnf-x22hH7gG4,8522
|
|
@@ -302,7 +302,8 @@ dataface/core/pack/planner.py,sha256=GwN5bkPK5Rm-rx52SwaOb45KvDCeY_cvwmYWCv3D_ls
|
|
|
302
302
|
dataface/core/pack/proposal_store.py,sha256=k2nY5TmdFaNeYYVVLA238oD4SJFwW8-LH2wwUUzGWsU,2943
|
|
303
303
|
dataface/core/registered_views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
304
304
|
dataface/core/registered_views/data_urls.py,sha256=5vxlpwpiT0VlzChI_Clzj1H8QOfCzDmoN7rofSzaKTg,1292
|
|
305
|
-
dataface/core/registered_views/expander.py,sha256=
|
|
305
|
+
dataface/core/registered_views/expander.py,sha256=fQ6BG5cmRo09B-UOdtMrjvmaK7K1o1MgrwHHaivaSFE,14068
|
|
306
|
+
dataface/core/registered_views/link_keys.py,sha256=PwiA4CpbN5l0Qis_wHBuzBQBzWhGWARi5rCyAsUrg0Q,1839
|
|
306
307
|
dataface/core/registered_views/loader.py,sha256=eA6P34JIEdzmF_k82wnll2IylDupLc0IMGM59Wg7VDo,3425
|
|
307
308
|
dataface/core/registered_views/models.py,sha256=MhbdWIa649t5Ps4HUekz15qW-zFIVYI2Z5A3HCNaDm0,1724
|
|
308
309
|
dataface/core/registered_views/query_runner.py,sha256=3popMvvaVD0zDQCnlYXVlCMKGuHFDa_9WD8fVI-9XRA,10657
|
|
@@ -314,13 +315,13 @@ dataface/core/registered_views/templates/data/root.yaml,sha256=Xo9-P_PkOqM0z-Cd-
|
|
|
314
315
|
dataface/core/registered_views/templates/data/schema-index.yaml,sha256=W7v6nw7XHrNX7_bPpKXOY-FjgQyUJooOd-Ue9zLnVi0,418
|
|
315
316
|
dataface/core/registered_views/templates/data/source-index.yaml,sha256=QIJJ4eJq5EjvDZOOwLFvIN-4x0TSvfg45PSpXZdFX3Q,343
|
|
316
317
|
dataface/core/registered_views/templates/data/table-detail.yaml,sha256=3lWn9lxfvvjoFCUF-2mnQKnxluJilQF9zEELtEbduO8,691
|
|
317
|
-
dataface/core/registered_views/templates/data/table-index.yaml,sha256=
|
|
318
|
+
dataface/core/registered_views/templates/data/table-index.yaml,sha256=qWeSNXI6ksFU9Vd3d5vyPFlyceRTFBqHFrwuQVyBFTs,847
|
|
318
319
|
dataface/core/registered_views/templates/inspector/column.yaml,sha256=Pnb14siQxQsPqKhpk1hPY50HNLruUh9cyoyu5Nar2xw,517
|
|
319
320
|
dataface/core/registered_views/templates/inspector/schema.yaml,sha256=Y6_OGWHxfEEsznXbnQkx37vjilSdzFE3y5yPVK8noOE,347
|
|
320
321
|
dataface/core/registered_views/templates/inspector/source.yaml,sha256=13kVlM_HKbEdkZcox3vQ51qTYNvE_naDD5MbNkffkmA,294
|
|
321
322
|
dataface/core/registered_views/templates/inspector/table.yaml,sha256=nbOqjuLKZMV6miQbOi6U9TjDCnK3CkX3GEwvWL67Dsk,445
|
|
322
323
|
dataface/core/render/__init__.py,sha256=0aVzC2Maa1Hn68Qj7lpLLSzhgR5TNhlF-I8ZlKH3Y2U,2255
|
|
323
|
-
dataface/core/render/board_links.py,sha256=
|
|
324
|
+
dataface/core/render/board_links.py,sha256=kku2qAcicpss9FSl4zTVslulC-sHJUlgdv4hFP41Mq0,9413
|
|
324
325
|
dataface/core/render/chart_interactivity.py,sha256=tcnTZ1c1RHFkxCZUvRzDm5cNYbEuSfGmwpc2B6y648k,1993
|
|
325
326
|
dataface/core/render/dir_context.py,sha256=mZ-wD1rqbD5glACZglh7fncc8V04TucSv-08Z0lcRDQ,12587
|
|
326
327
|
dataface/core/render/errors.py,sha256=WS1fnxlNGW2V9w6cKuPPLuuiX5fXar95xRdNziHGgmM,4716
|
|
@@ -337,7 +338,7 @@ dataface/core/render/layouts.py,sha256=kcwIi57N6rJ4cG7eqqCVG_RsMC0P4xr2dxFfqLjLv
|
|
|
337
338
|
dataface/core/render/nav.py,sha256=l8VnDuwgJwftMY6Lw6dUK0yN44Cs-pxXcVIe5TpjK1o,3307
|
|
338
339
|
dataface/core/render/placeholder.py,sha256=M1JncsWVGYJUzu3cEnC8-8TAnRtQWuBRpr6GtCBJupw,13517
|
|
339
340
|
dataface/core/render/render_result.py,sha256=Rfc2-rlGpS98cHRxjJNy7KXoZOLNbxM6bzgggB8aReg,453
|
|
340
|
-
dataface/core/render/renderer.py,sha256=
|
|
341
|
+
dataface/core/render/renderer.py,sha256=ZpqjIr2dbQLdQBh_GtXDcscEAo7isapITRkmJ_g--E0,21481
|
|
341
342
|
dataface/core/render/script_embedding.py,sha256=OePzZdk-7jR-WrF-Jnfq9SriGv8Cs2kqKfrZiZMiHuw,442
|
|
342
343
|
dataface/core/render/svg_utils.py,sha256=q1ZS-r3jrSOXM2AO5HXrTtfiVwZEvEdgBcPw5pfRTUQ,6892
|
|
343
344
|
dataface/core/render/template_loader.py,sha256=xa6jkqEfHxFbBg5sCH1IX4c2LJSD80yNFBzz09tn42w,2296
|
|
@@ -354,6 +355,7 @@ dataface/core/render/yaml_format.py,sha256=TfYQPEB7qGKwLLRZ3bzAvh9L0XnPilQsstf6G
|
|
|
354
355
|
dataface/core/render/chart/__init__.py,sha256=nHyhv9WW7ySP2EooQ9MUDjnJSIEcAUTBHnAFGd4Z60I,961
|
|
355
356
|
dataface/core/render/chart/arc_attached_table.py,sha256=A5E3Oi5QbjOXq38Sogf740BTymzY2Ks0p4GLEaQXC9s,10204
|
|
356
357
|
dataface/core/render/chart/artifacts.py,sha256=0GnDrr35cJtX5REAgWMYVlEvcTdatm-flRlxy1T8KNM,384
|
|
358
|
+
dataface/core/render/chart/auto_link.py,sha256=Z2011cDCHTjfQ2EGTjEkfeatRQ35p_I-vZFbqCclJj8,6886
|
|
357
359
|
dataface/core/render/chart/callout.py,sha256=XD1NNtSWZ-25lDsgHPgBPMjRW_pDPO2x4rLU-DPPNrQ,7830
|
|
358
360
|
dataface/core/render/chart/decisions.py,sha256=4yNsUqhv0QiQ4O__eOU1CQybop8FdaStsY1C_1IFOoo,16218
|
|
359
361
|
dataface/core/render/chart/geo.py,sha256=dIX4oMJb9yAboszznWYShsN-IL-7BZjVqb_bLcXvl4k,25014
|
|
@@ -362,7 +364,7 @@ dataface/core/render/chart/labels.py,sha256=VAjMiHbqqy8qPlbfHhJhcPPeZTF3N36NI15m
|
|
|
362
364
|
dataface/core/render/chart/pipeline.py,sha256=QYwkybtLJ-74WIrcFCWtAiosZ1QwO-0n5McX3Wor-ts,27347
|
|
363
365
|
dataface/core/render/chart/presentation.py,sha256=rDqP3g0XJAo1U2zJYU5PCH_c3vkDjo0V4eT4fbV32C4,1336
|
|
364
366
|
dataface/core/render/chart/profile.py,sha256=rEAK9206LWgeCray8agMGYRD6jne1Wiglbxi__xppdo,162232
|
|
365
|
-
dataface/core/render/chart/render_single.py,sha256=
|
|
367
|
+
dataface/core/render/chart/render_single.py,sha256=5jPiHZyCjaPPoUTyLUIqp9sW71y6LtKrSFl9WIEd4Qc,17723
|
|
366
368
|
dataface/core/render/chart/renderers.py,sha256=bRc27MFf42ni9JxP8iPekSSPVNz7vjLarq2Ve_MItbg,4646
|
|
367
369
|
dataface/core/render/chart/rendering.py,sha256=YkcglFVrox2ByG8qGiBwGa-ZQUnrG71UTIQc1xWfBDg,20320
|
|
368
370
|
dataface/core/render/chart/serialization.py,sha256=fQFJkB8FMbmEx2Q8b8UdSe2yMl2XBWwPOPuobJyLnUA,2513
|
|
@@ -465,7 +467,7 @@ dataface/agent_api/_init_templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQe
|
|
|
465
467
|
dataface/agent_api/_init_templates/dataface.yml,sha256=RiPfK_iwARqva3cao9oBf5b2lGTuut-adaabKFfv7xU,542
|
|
466
468
|
dataface/agent_api/_init_templates/guide.yaml,sha256=WgGCIcIfJIOHvz33GsOm-kpSeJ6ikWsOH4_Alj4bC8A,4782
|
|
467
469
|
dataface/agent_api/_init_templates/meta.yaml,sha256=9I8jwiVKGCu-g3vmMtwNo6ZKBJLtKYSVfugDCPFii6E,818
|
|
468
|
-
dataface/ai/prompts/sql-guidance.md,sha256=
|
|
470
|
+
dataface/ai/prompts/sql-guidance.md,sha256=CJKSA18Pw2RjBq181I1Ktc8X0FnQtX8pYAhuWT6O6Bs,1622
|
|
469
471
|
dataface/ai/prompts/sql-system.md,sha256=L1F_Mh2BewpZU9x3UryUVHdWg1kZDC80Su_iD0x2xXk,299
|
|
470
472
|
dataface/ai/prompts/system.md,sha256=ruqScPJ2UgYtnAviM6FFbeqaIQmogCbGCjl7gW48Wok,294
|
|
471
473
|
dataface/ai/skills/before-after-comparison/SKILL.md,sha256=sVUPTTlU1WdDStBELhing_XQcQpzvKFco29DceFvmPU,3276
|
|
@@ -499,7 +501,7 @@ dataface/ai/skills/top-n-with-detail/SKILL.md,sha256=lW25eQCTZPzj46EHNvZTGZm4lAC
|
|
|
499
501
|
dataface/ai/skills/top-n-with-detail/examples/top-n-with-detail.yml,sha256=UOhvxkZncXLsj6D1P0g0O1TZ5sEs0gOCWce2gc9xgPQ,1076
|
|
500
502
|
dataface/ai/skills/two-by-two-grid-overview/SKILL.md,sha256=RRDXPrdbwCRpIhlRCzNZNp4pBRXmdgeu5rGjktlM4S0,2919
|
|
501
503
|
dataface/ai/skills/two-by-two-grid-overview/examples/two-by-two-grid-overview.yml,sha256=rquYNR89pxWannuca_k2bQfTGB8T1PtffxytJ0nxK0c,1385
|
|
502
|
-
dataface/agent_api/docs/yaml-reference.md,sha256=
|
|
504
|
+
dataface/agent_api/docs/yaml-reference.md,sha256=lglcFlYJ8JcKeWzyXkGgbDk6M0WI2duG9pRHc5bDFTM,190275
|
|
503
505
|
d3_format/__init__.py,sha256=FkU-W58LUkTf-aA0Tv2FVRTZ-PL0cznXVmdW9viHJ14,407
|
|
504
506
|
d3_format/errors.py,sha256=yJXRnnfpVtsrWjZYbGRFV_BR62ZaRNVkfDiuAPwiqX4,674
|
|
505
507
|
d3_format/format.py,sha256=MgcqKSUsa-qYxzeT3RCGYcxY8WaF9I545ymTv5SIMQo,19367
|
|
@@ -515,8 +517,8 @@ mdsvg/renderer.py,sha256=3aHnwxlJxmI2X6kuI5GFBcOnXeP9T8HC8qNY0m7InsQ,66026
|
|
|
515
517
|
mdsvg/style.py,sha256=AOpUrMseyjeIVMzgpEdYmNO66OdiOQIESWG0Fv9jUfg,17058
|
|
516
518
|
mdsvg/types.py,sha256=MQlqsUP5y78D8kX0brD-82DkBvaUYdbmkq1U95TnyWA,5093
|
|
517
519
|
mdsvg/utils.py,sha256=CIqFACFaJiM0A_dl-hJXYhRqH-T7ayazP62wgfWGMjw,1866
|
|
518
|
-
dataface-0.1.6.
|
|
519
|
-
dataface-0.1.6.
|
|
520
|
-
dataface-0.1.6.
|
|
521
|
-
dataface-0.1.6.
|
|
522
|
-
dataface-0.1.6.
|
|
520
|
+
dataface-0.1.6.dev360.dist-info/METADATA,sha256=oa3W02_3Wh6Y_FUyF8uJnm8HEkLRyNkAHfCqkghuau0,11455
|
|
521
|
+
dataface-0.1.6.dev360.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
522
|
+
dataface-0.1.6.dev360.dist-info/entry_points.txt,sha256=imfZRsAzmIbDRHW7jqhn0nT6EbYWAyOiuHisxmfVgXs,46
|
|
523
|
+
dataface-0.1.6.dev360.dist-info/licenses/LICENSE,sha256=Iy9gBB2gC8WGQEwHxJd4-huwUvxB6OMOoT3no6emeaQ,11345
|
|
524
|
+
dataface-0.1.6.dev360.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|