dataface 0.1.2__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.
- d3_format/__init__.py +14 -0
- d3_format/errors.py +19 -0
- d3_format/format.py +551 -0
- d3_format/spec.py +159 -0
- dataface/DATAFACE_SYNTAX.md +1135 -0
- dataface/__init__.py +93 -0
- dataface/_docs_site.py +20 -0
- dataface/_install_hint.py +26 -0
- dataface/agent_api/__init__.py +79 -0
- dataface/agent_api/_init_templates/__init__.py +0 -0
- dataface/agent_api/_init_templates/agents_dft_snippet.md +26 -0
- dataface/agent_api/_init_templates/dataface.yml +15 -0
- dataface/agent_api/_init_templates/faces-dataface.yml +144 -0
- dataface/agent_api/_init_templates/index.md +24 -0
- dataface/agent_api/_paths.py +118 -0
- dataface/agent_api/_project_agents_md.py +43 -0
- dataface/agent_api/_session_store.py +486 -0
- dataface/agent_api/_state.py +28 -0
- dataface/agent_api/chat.py +221 -0
- dataface/agent_api/dashboards.py +257 -0
- dataface/agent_api/describe.py +366 -0
- dataface/agent_api/describe_query.py +120 -0
- dataface/agent_api/docs/__init__.py +25 -0
- dataface/agent_api/docs/_loader.py +292 -0
- dataface/agent_api/docs/yaml-reference.md +2757 -0
- dataface/agent_api/file_refs.py +118 -0
- dataface/agent_api/init.py +126 -0
- dataface/agent_api/inspect.py +128 -0
- dataface/agent_api/mcp_install.py +170 -0
- dataface/agent_api/query.py +274 -0
- dataface/agent_api/schema.py +658 -0
- dataface/agent_api/schema_search.py +284 -0
- dataface/agent_api/search.py +270 -0
- dataface/agent_api/skill_install.py +141 -0
- dataface/agent_api/skill_render.py +90 -0
- dataface/agent_api/skills.py +293 -0
- dataface/agent_api/surface_aliases.yaml +128 -0
- dataface/agent_api/validate.py +175 -0
- dataface/agent_api/validate_query.py +84 -0
- dataface/ai/__init__.py +39 -0
- dataface/ai/agent.py +139 -0
- dataface/ai/context.py +45 -0
- dataface/ai/events.py +62 -0
- dataface/ai/external_mcp.py +610 -0
- dataface/ai/generate_sql.py +96 -0
- dataface/ai/llm.py +403 -0
- dataface/ai/mcp/__init__.py +51 -0
- dataface/ai/mcp/server.py +289 -0
- dataface/ai/memories.py +85 -0
- dataface/ai/prompts.py +177 -0
- dataface/ai/schema_context.py +138 -0
- dataface/ai/skills/before-after-comparison/SKILL.md +102 -0
- dataface/ai/skills/before-after-comparison/examples/before-after-comparison.yml +24 -0
- dataface/ai/skills/dashboard-build/SKILL.md +212 -0
- dataface/ai/skills/dashboard-build/examples/_smoke.yml +15 -0
- dataface/ai/skills/dashboard-design/SKILL.md +182 -0
- dataface/ai/skills/dashboard-review/SKILL.md +113 -0
- dataface/ai/skills/dashboard-structural-review/SKILL.md +173 -0
- dataface/ai/skills/dashboard-visual-review/SKILL.md +139 -0
- dataface/ai/skills/dataface-mcp-setup/SKILL.md +177 -0
- dataface/ai/skills/dataface-troubleshooting/SKILL.md +225 -0
- dataface/ai/skills/drill-down-link/SKILL.md +112 -0
- dataface/ai/skills/drill-down-link/examples/drill-down-link.yml +27 -0
- dataface/ai/skills/faceted-small-multiples/SKILL.md +116 -0
- dataface/ai/skills/faceted-small-multiples/examples/faceted-small-multiples.yml +33 -0
- dataface/ai/skills/filter-bar-with-variables/SKILL.md +105 -0
- dataface/ai/skills/filter-bar-with-variables/examples/filter-bar-with-variables.yml +49 -0
- dataface/ai/skills/kpi-row/SKILL.md +101 -0
- dataface/ai/skills/kpi-row/examples/kpi-row.yml +55 -0
- dataface/ai/skills/report-design/SKILL.md +184 -0
- dataface/ai/skills/single-metric-bignum/SKILL.md +90 -0
- dataface/ai/skills/single-metric-bignum/examples/single-metric-bignum.yml +27 -0
- dataface/ai/skills/table-heavy-ops-dashboard/SKILL.md +114 -0
- dataface/ai/skills/table-heavy-ops-dashboard/examples/table-heavy-ops-dashboard.yml +48 -0
- dataface/ai/skills/time-series-trend/SKILL.md +93 -0
- dataface/ai/skills/time-series-trend/examples/time-series-trend.yml +26 -0
- dataface/ai/skills/top-n-with-detail/SKILL.md +98 -0
- dataface/ai/skills/top-n-with-detail/examples/top-n-with-detail.yml +45 -0
- dataface/ai/skills/two-by-two-grid-overview/SKILL.md +78 -0
- dataface/ai/skills/two-by-two-grid-overview/examples/two-by-two-grid-overview.yml +64 -0
- dataface/ai/tool_schemas.py +132 -0
- dataface/ai/tools/__init__.py +312 -0
- dataface/ai/yaml_utils.py +57 -0
- dataface/cli/__init__.py +3 -0
- dataface/cli/_console.py +48 -0
- dataface/cli/_error_format.py +83 -0
- dataface/cli/_extras.py +190 -0
- dataface/cli/_json_output.py +8 -0
- dataface/cli/_parsing.py +17 -0
- dataface/cli/_version_info.py +56 -0
- dataface/cli/commands/__init__.py +3 -0
- dataface/cli/commands/_agent_input.py +205 -0
- dataface/cli/commands/_agent_server.py +115 -0
- dataface/cli/commands/chat.py +645 -0
- dataface/cli/commands/describe.py +107 -0
- dataface/cli/commands/docs.py +131 -0
- dataface/cli/commands/extension.py +179 -0
- dataface/cli/commands/init.py +240 -0
- dataface/cli/commands/inspect.py +94 -0
- dataface/cli/commands/mcp_init.py +167 -0
- dataface/cli/commands/query.py +386 -0
- dataface/cli/commands/render.py +291 -0
- dataface/cli/commands/schema.py +411 -0
- dataface/cli/commands/search.py +49 -0
- dataface/cli/commands/serve.py +114 -0
- dataface/cli/commands/skills.py +133 -0
- dataface/cli/commands/skills_init.py +161 -0
- dataface/cli/commands/validate.py +63 -0
- dataface/cli/main.py +1501 -0
- dataface/core/__init__.py +75 -0
- dataface/core/compile/__init__.py +244 -0
- dataface/core/compile/_jinja_helpers.py +78 -0
- dataface/core/compile/channel.py +222 -0
- dataface/core/compile/chart_focus.py +101 -0
- dataface/core/compile/chart_resolved.py +169 -0
- dataface/core/compile/chart_type_detection.py +489 -0
- dataface/core/compile/chart_update.py +261 -0
- dataface/core/compile/colors.py +64 -0
- dataface/core/compile/compiler.py +904 -0
- dataface/core/compile/config.py +823 -0
- dataface/core/compile/custom_chart_types.py +208 -0
- dataface/core/compile/data_table_attachment.py +1287 -0
- dataface/core/compile/detect.py +110 -0
- dataface/core/compile/errors.py +302 -0
- dataface/core/compile/filter_injection.py +319 -0
- dataface/core/compile/introspection.py +527 -0
- dataface/core/compile/jinja.py +511 -0
- dataface/core/compile/labels_env.py +52 -0
- dataface/core/compile/markdown.py +154 -0
- dataface/core/compile/meta.py +388 -0
- dataface/core/compile/models/__init__.py +0 -0
- dataface/core/compile/models/chart/__init__.py +0 -0
- dataface/core/compile/models/chart/authored.py +2137 -0
- dataface/core/compile/models/chart/compiled.py +398 -0
- dataface/core/compile/models/config.py +347 -0
- dataface/core/compile/models/face/__init__.py +0 -0
- dataface/core/compile/models/face/authored.py +659 -0
- dataface/core/compile/models/face/compiled.py +522 -0
- dataface/core/compile/models/factories.py +201 -0
- dataface/core/compile/models/markers.py +40 -0
- dataface/core/compile/models/palette.py +36 -0
- dataface/core/compile/models/primitives.py +415 -0
- dataface/core/compile/models/query/__init__.py +0 -0
- dataface/core/compile/models/query/authored.py +246 -0
- dataface/core/compile/models/query/compiled.py +710 -0
- dataface/core/compile/models/refs.py +137 -0
- dataface/core/compile/models/source.py +611 -0
- dataface/core/compile/models/style/__init__.py +0 -0
- dataface/core/compile/models/style/authored.py +481 -0
- dataface/core/compile/models/style/compiled.py +3399 -0
- dataface/core/compile/models/style/merged.py +1682 -0
- dataface/core/compile/models/theme.py +362 -0
- dataface/core/compile/models/variable/__init__.py +0 -0
- dataface/core/compile/models/variable/authored.py +254 -0
- dataface/core/compile/models/vega_lite/__init__.py +0 -0
- dataface/core/compile/models/vega_lite/config.py +510 -0
- dataface/core/compile/models/vega_lite/contracts.py +171 -0
- dataface/core/compile/normalize_charts.py +494 -0
- dataface/core/compile/normalize_layout.py +1000 -0
- dataface/core/compile/normalize_queries.py +297 -0
- dataface/core/compile/normalize_variables.py +489 -0
- dataface/core/compile/normalizer.py +543 -0
- dataface/core/compile/palette.py +1100 -0
- dataface/core/compile/parameterized.py +658 -0
- dataface/core/compile/parser.py +228 -0
- dataface/core/compile/schema.py +20 -0
- dataface/core/compile/schema_renderers/__init__.py +0 -0
- dataface/core/compile/schema_renderers/json_schema.py +163 -0
- dataface/core/compile/schema_renderers/prompt.py +152 -0
- dataface/core/compile/schema_renderers/vscode_schema.py +301 -0
- dataface/core/compile/sizing.py +2126 -0
- dataface/core/compile/sources.py +518 -0
- dataface/core/compile/sql_authoring_lint.py +56 -0
- dataface/core/compile/style_cascade.py +471 -0
- dataface/core/compile/typography.py +299 -0
- dataface/core/compile/validator.py +301 -0
- dataface/core/compile/variables.py +53 -0
- dataface/core/compile/vega_config.py +98 -0
- dataface/core/compile/vega_lite/__init__.py +6 -0
- dataface/core/compile/vega_lite/validation.py +95 -0
- dataface/core/compile/yaml_error_formatter.py +838 -0
- dataface/core/connections.py +38 -0
- dataface/core/dashboard.py +358 -0
- dataface/core/defaults/default_config.yml +101 -0
- dataface/core/defaults/palettes/categorical/category-10-dark.yml +32 -0
- dataface/core/defaults/palettes/categorical/category-10-light.yml +43 -0
- dataface/core/defaults/palettes/categorical/category-10.yml +31 -0
- dataface/core/defaults/palettes/categorical/category-6-tonal-blue.yml +22 -0
- dataface/core/defaults/palettes/categorical/category-6-tonal-brown.yml +29 -0
- dataface/core/defaults/palettes/categorical/category-6-tonal-green.yml +20 -0
- dataface/core/defaults/palettes/categorical/category-6-tonal-orange.yml +21 -0
- dataface/core/defaults/palettes/categorical/category-6-tonal-purple.yml +20 -0
- dataface/core/defaults/palettes/categorical/editorial-10-dark.yml +32 -0
- dataface/core/defaults/palettes/categorical/editorial-10.yml +40 -0
- dataface/core/defaults/palettes/categorical/hero-6.yml +17 -0
- dataface/core/defaults/palettes/categorical/single-blue.yml +11 -0
- dataface/core/defaults/palettes/categorical/tableau.yml +20 -0
- dataface/core/defaults/palettes/data/xkcd_colors.json +3803 -0
- dataface/core/defaults/palettes/diverging/blue-red.yml +25 -0
- dataface/core/defaults/palettes/diverging/coolwarm.yml +24 -0
- dataface/core/defaults/palettes/diverging/crimson-green.yml +23 -0
- dataface/core/defaults/palettes/diverging/orange-teal.yml +23 -0
- dataface/core/defaults/palettes/diverging/sunset.yml +24 -0
- dataface/core/defaults/palettes/scaffold/dft-creams.yml +38 -0
- dataface/core/defaults/palettes/scaffold/dft-grays.yml +53 -0
- dataface/core/defaults/palettes/sequential/amber.yml +22 -0
- dataface/core/defaults/palettes/sequential/blue.yml +22 -0
- dataface/core/defaults/palettes/sequential/brown.yml +22 -0
- dataface/core/defaults/palettes/sequential/gray.yml +22 -0
- dataface/core/defaults/palettes/sequential/green.yml +22 -0
- dataface/core/defaults/palettes/sequential/purple.yml +22 -0
- dataface/core/defaults/palettes/sequential/rust.yml +22 -0
- dataface/core/defaults/palettes/sequential/teal.yml +22 -0
- dataface/core/defaults/palettes/tone/negative.yml +32 -0
- dataface/core/defaults/palettes/tone/positive.yml +22 -0
- dataface/core/defaults/palettes/tone/warning.yml +22 -0
- dataface/core/defaults/themes/_base.yaml +786 -0
- dataface/core/defaults/themes/bi.yaml +16 -0
- dataface/core/defaults/themes/carbong100.yaml +41 -0
- dataface/core/defaults/themes/cream.yaml +122 -0
- dataface/core/defaults/themes/dark.yaml +40 -0
- dataface/core/defaults/themes/diagnostics-title-angle-extreme.yaml +9 -0
- dataface/core/defaults/themes/diagnostics-title-baseline-extreme.yaml +9 -0
- dataface/core/defaults/themes/diagnostics-title-baseline.yaml +24 -0
- dataface/core/defaults/themes/diagnostics-title-center.yaml +8 -0
- dataface/core/defaults/themes/diagnostics-title-color-extreme.yaml +24 -0
- dataface/core/defaults/themes/diagnostics-title-font-extreme.yaml +25 -0
- dataface/core/defaults/themes/diagnostics-title-left.yaml +8 -0
- dataface/core/defaults/themes/diagnostics-title-offset-extreme.yaml +9 -0
- dataface/core/defaults/themes/diagnostics-title-size-extreme.yaml +24 -0
- dataface/core/defaults/themes/diagnostics-title-weight-extreme.yaml +24 -0
- dataface/core/defaults/themes/editorial.yaml +147 -0
- dataface/core/defaults/themes/light.yaml +30 -0
- dataface/core/defaults/themes/looker.yaml +17 -0
- dataface/core/defaults/themes/stark.yaml +134 -0
- dataface/core/errors/__init__.py +67 -0
- dataface/core/errors/codes_compile.py +56 -0
- dataface/core/errors/codes_execute.py +177 -0
- dataface/core/errors/codes_render.py +106 -0
- dataface/core/errors/codes_unknown.py +15 -0
- dataface/core/errors/hints.py +74 -0
- dataface/core/errors/registry.py +42 -0
- dataface/core/errors/structured.py +92 -0
- dataface/core/execute/__init__.py +91 -0
- dataface/core/execute/adapters/__init__.py +49 -0
- dataface/core/execute/adapters/adapter_registry.py +400 -0
- dataface/core/execute/adapters/base.py +245 -0
- dataface/core/execute/adapters/csv_adapter.py +239 -0
- dataface/core/execute/adapters/dbt_adapter.py +283 -0
- dataface/core/execute/adapters/dbt_adapter_factory.py +212 -0
- dataface/core/execute/adapters/dbt_macro_loader.py +95 -0
- dataface/core/execute/adapters/dbt_utils.py +150 -0
- dataface/core/execute/adapters/http_adapter.py +224 -0
- dataface/core/execute/adapters/metricflow_adapter.py +94 -0
- dataface/core/execute/adapters/schema_resolver_adapter.py +144 -0
- dataface/core/execute/adapters/sql_adapter.py +710 -0
- dataface/core/execute/adapters/values_adapter.py +58 -0
- dataface/core/execute/batch.py +744 -0
- dataface/core/execute/cache_backend.py +135 -0
- dataface/core/execute/cache_keys.py +66 -0
- dataface/core/execute/dbt_jinja.py +21 -0
- dataface/core/execute/dialects/__init__.py +121 -0
- dataface/core/execute/dialects/athena.py +75 -0
- dataface/core/execute/dialects/base.py +302 -0
- dataface/core/execute/dialects/bigquery.py +38 -0
- dataface/core/execute/dialects/databricks.py +68 -0
- dataface/core/execute/dialects/duckdb.py +35 -0
- dataface/core/execute/dialects/mysql.py +68 -0
- dataface/core/execute/dialects/postgres.py +39 -0
- dataface/core/execute/dialects/redshift.py +12 -0
- dataface/core/execute/dialects/snowflake.py +51 -0
- dataface/core/execute/dialects/sqlserver.py +92 -0
- dataface/core/execute/duckdb_cache.py +712 -0
- dataface/core/execute/duckdb_config.py +26 -0
- dataface/core/execute/errors.py +213 -0
- dataface/core/execute/executor.py +1249 -0
- dataface/core/execute/parallel.py +162 -0
- dataface/core/execute/setup_sql.py +58 -0
- dataface/core/execute/source_registry.py +72 -0
- dataface/core/execute/source_resolver.py +255 -0
- dataface/core/execute/sql_guard.py +387 -0
- dataface/core/execute/sql_literals.py +199 -0
- dataface/core/fonts.py +52 -0
- dataface/core/inspect/__init__.py +32 -0
- dataface/core/inspect/cache_factory.py +98 -0
- dataface/core/inspect/db_types.py +162 -0
- dataface/core/inspect/dbt_schema.py +96 -0
- dataface/core/inspect/defaults.yml +37 -0
- dataface/core/inspect/fanout_risk.py +109 -0
- dataface/core/inspect/manifest_utils.py +77 -0
- dataface/core/inspect/partials/categorical.yml +40 -0
- dataface/core/inspect/partials/date.yml +40 -0
- dataface/core/inspect/partials/numeric.yml +55 -0
- dataface/core/inspect/partition_types.py +38 -0
- dataface/core/inspect/query_validator.py +975 -0
- dataface/core/inspect/renderer.py +354 -0
- dataface/core/inspect/resolver.py +808 -0
- dataface/core/inspect/search.py +461 -0
- dataface/core/inspect/sources/__init__.py +32 -0
- dataface/core/inspect/sources/dbt.py +738 -0
- dataface/core/inspect/sources/duckdb_utils.py +66 -0
- dataface/core/inspect/templates/__init__.py +1 -0
- dataface/core/inspect/templates/categorical_column.yml +196 -0
- dataface/core/inspect/templates/charts.yml +109 -0
- dataface/core/inspect/templates/date_column.yml +248 -0
- dataface/core/inspect/templates/model.yml +138 -0
- dataface/core/inspect/templates/numeric_column.yml +261 -0
- dataface/core/inspect/templates/quality.yml +80 -0
- dataface/core/inspect/templates/string_column.yml +263 -0
- dataface/core/project_roots.py +165 -0
- dataface/core/render/__init__.py +87 -0
- dataface/core/render/board_links.py +176 -0
- dataface/core/render/chart/__init__.py +27 -0
- dataface/core/render/chart/arc_attached_table.py +251 -0
- dataface/core/render/chart/artifacts.py +16 -0
- dataface/core/render/chart/callout.py +225 -0
- dataface/core/render/chart/decisions.py +358 -0
- dataface/core/render/chart/geo.py +700 -0
- dataface/core/render/chart/kpi.py +916 -0
- dataface/core/render/chart/labels.py +76 -0
- dataface/core/render/chart/pipeline.py +818 -0
- dataface/core/render/chart/presentation.py +36 -0
- dataface/core/render/chart/profile.py +3438 -0
- dataface/core/render/chart/render_single.py +347 -0
- dataface/core/render/chart/renderers.py +193 -0
- dataface/core/render/chart/rendering.py +565 -0
- dataface/core/render/chart/serialization.py +90 -0
- dataface/core/render/chart/spark.py +496 -0
- dataface/core/render/chart/spark_bar.py +370 -0
- dataface/core/render/chart/spec_builders.py +154 -0
- dataface/core/render/chart/standard_renderer.py +2645 -0
- dataface/core/render/chart/table.py +2957 -0
- dataface/core/render/chart/table_support.py +1452 -0
- dataface/core/render/chart/tick_values.py +66 -0
- dataface/core/render/chart/time_unit_detect.py +809 -0
- dataface/core/render/chart/title_overflow.py +157 -0
- dataface/core/render/chart/type_inference.py +122 -0
- dataface/core/render/chart/validation.py +99 -0
- dataface/core/render/chart/vega_lite.py +125 -0
- dataface/core/render/chart/vega_lite_types.py +268 -0
- dataface/core/render/chart/vl_field_maps.py +346 -0
- dataface/core/render/chart_interactivity.py +24 -0
- dataface/core/render/control_registry.py +287 -0
- dataface/core/render/converters/__init__.py +24 -0
- dataface/core/render/converters/chart.py +276 -0
- dataface/core/render/converters/html.py +98 -0
- dataface/core/render/converters/pdf.py +40 -0
- dataface/core/render/converters/png.py +41 -0
- dataface/core/render/errors.py +144 -0
- dataface/core/render/face_api.py +160 -0
- dataface/core/render/faces.py +1194 -0
- dataface/core/render/font_measurement.py +48 -0
- dataface/core/render/font_support.py +197 -0
- dataface/core/render/fonts/DFTSansTabular-Regular.ttf +0 -0
- dataface/core/render/fonts/DFTSansTabular-Regular.woff2 +0 -0
- dataface/core/render/fonts/DFTSerifOldstyleProportional-Regular.ttf +0 -0
- dataface/core/render/fonts/DFTSerifOldstyleTabular-Regular.ttf +0 -0
- dataface/core/render/fonts/InterVariable.ttf +0 -0
- dataface/core/render/fonts/InterVariable.woff2 +0 -0
- dataface/core/render/fonts/NOTO_COLOR_EMOJI_LICENSE.txt +93 -0
- dataface/core/render/fonts/NOTO_EMOJI_LICENSE.txt +93 -0
- dataface/core/render/fonts/NotoColorEmoji-Regular.ttf +0 -0
- dataface/core/render/fonts/NotoColorEmoji-Regular.woff2 +0 -0
- dataface/core/render/fonts/NotoEmoji-Regular.ttf +0 -0
- dataface/core/render/fonts/NotoEmoji-Regular.woff2 +0 -0
- dataface/core/render/fonts/SOURCE_CODE_PRO_LICENSE.txt +93 -0
- dataface/core/render/fonts/SOURCE_SERIF_4_LICENSE.txt +98 -0
- dataface/core/render/fonts/SourceCodePro-Regular.ttf +0 -0
- dataface/core/render/fonts/SourceSerif4-Regular.ttf +0 -0
- dataface/core/render/fonts/_emoji_font_face.css +43 -0
- dataface/core/render/fonts/source-serif-4-variable-latin.woff2 +0 -0
- dataface/core/render/format_utils.py +329 -0
- dataface/core/render/geo_defaults.yml +28 -0
- dataface/core/render/json_format.py +146 -0
- dataface/core/render/layout_sizing.py +865 -0
- dataface/core/render/layouts.py +541 -0
- dataface/core/render/markdown_defaults.yml +16 -0
- dataface/core/render/missing_vars_prompt.py +79 -0
- dataface/core/render/placeholder.py +389 -0
- dataface/core/render/render_result.py +14 -0
- dataface/core/render/renderer.py +467 -0
- dataface/core/render/script_embedding.py +16 -0
- dataface/core/render/svg_utils.py +212 -0
- dataface/core/render/template_loader.py +69 -0
- dataface/core/render/templates/controls/_styles.css +606 -0
- dataface/core/render/templates/controls/checkbox.html +16 -0
- dataface/core/render/templates/controls/date.html +16 -0
- dataface/core/render/templates/controls/number.html +19 -0
- dataface/core/render/templates/controls/readonly.html +9 -0
- dataface/core/render/templates/controls/select.html +21 -0
- dataface/core/render/templates/controls/slider.html +22 -0
- dataface/core/render/templates/controls/text.html +16 -0
- dataface/core/render/templates/scripts/chart_interactivity.js +191 -0
- dataface/core/render/templates/scripts/variables.js +976 -0
- dataface/core/render/templates/svg/grid_pattern.svg +3 -0
- dataface/core/render/templates/svg/styles.css +51 -0
- dataface/core/render/terminal.py +311 -0
- dataface/core/render/terminal_charts.py +563 -0
- dataface/core/render/terminal_defaults.yml +2 -0
- dataface/core/render/terminal_layouts.py +299 -0
- dataface/core/render/terminal_text.py +31 -0
- dataface/core/render/text/__init__.py +1 -0
- dataface/core/render/text/case.py +113 -0
- dataface/core/render/text_format.py +129 -0
- dataface/core/render/utils.py +106 -0
- dataface/core/render/variable_controls.py +946 -0
- dataface/core/render/variable_input_refinement.py +140 -0
- dataface/core/render/warnings/__init__.py +15 -0
- dataface/core/render/warnings/bar_color_1_to_1_with_x.py +80 -0
- dataface/core/render/warnings/base.py +44 -0
- dataface/core/render/warnings/fanout_risk.py +15 -0
- dataface/core/render/warnings/from_query_diagnostic.py +56 -0
- dataface/core/render/warnings/missing_join_predicate.py +13 -0
- dataface/core/render/warnings/query_parse_error.py +14 -0
- dataface/core/render/warnings/query_returned_zero_rows.py +42 -0
- dataface/core/render/warnings/reaggregation.py +14 -0
- dataface/core/render/warnings/registry.py +45 -0
- dataface/core/render/warnings/suppression.py +46 -0
- dataface/core/render/warnings/temporal_single_point.py +63 -0
- dataface/core/render/warnings/unreferenced_chart.py +15 -0
- dataface/core/render/warnings/y_encoding_mostly_null.py +76 -0
- dataface/core/render/yaml_format.py +167 -0
- dataface/core/resolve_face.py +195 -0
- dataface/core/schema/__init__.py +0 -0
- dataface/core/schema/guidance.py +151 -0
- dataface/core/scoped_paths.py +59 -0
- dataface/core/serve/__init__.py +14 -0
- dataface/core/serve/bootstrap.py +39 -0
- dataface/core/serve/embedded.py +57 -0
- dataface/core/serve/port.py +129 -0
- dataface/core/serve/server.py +938 -0
- dataface/core/serve/templates/__init__.py +0 -0
- dataface/core/serve/templates/directory.yml +6 -0
- dataface/core/serve/templates/error.html.j2 +217 -0
- dataface/core/utils.py +121 -0
- dataface/core/validate.py +64 -0
- dataface/integrations/__init__.py +0 -0
- dataface/integrations/highlighting.py +351 -0
- dataface/integrations/markdown.py +537 -0
- dataface/py.typed +0 -0
- dataface-0.1.2.dist-info/METADATA +375 -0
- dataface-0.1.2.dist-info/RECORD +455 -0
- dataface-0.1.2.dist-info/WHEEL +4 -0
- dataface-0.1.2.dist-info/entry_points.txt +2 -0
- dataface-0.1.2.dist-info/licenses/LICENSE +202 -0
- mdsvg/__init__.py +168 -0
- mdsvg/fonts.py +656 -0
- mdsvg/images.py +299 -0
- mdsvg/parser.py +629 -0
- mdsvg/playground.py +284 -0
- mdsvg/py.typed +2 -0
- mdsvg/renderer.py +1623 -0
- mdsvg/style.py +355 -0
- mdsvg/types.py +200 -0
- mdsvg/utils.py +86 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""Typed schema_search verb — full-text + filter over the metadata corpus.
|
|
2
|
+
|
|
3
|
+
Companion to ``dft schema``: the drill-down verb answers "I know what I'm
|
|
4
|
+
looking for" (navigation), this verb answers "I have a property, find what
|
|
5
|
+
matches" (find/filter). The two output shapes are intentionally different:
|
|
6
|
+
``schema`` returns a hierarchical named-dict tree wrapped in ``sources``;
|
|
7
|
+
``schema_search`` returns a flat list of fully-qualified hits.
|
|
8
|
+
|
|
9
|
+
Provenance flows through unchanged from the resolver: a hit on a name that
|
|
10
|
+
came from the adapter carries ``source='dbt_adapter'``; a hit on a
|
|
11
|
+
description that came from the cache carries ``source='super_schema'``;
|
|
12
|
+
a hit on a tag/test/owner from the manifest carries
|
|
13
|
+
``source='dbt_manifest'``.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from datetime import datetime, timezone
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr
|
|
23
|
+
|
|
24
|
+
from dataface.core.execute.adapters import AdapterRegistry
|
|
25
|
+
from dataface.core.inspect.cache_factory import build_resolver
|
|
26
|
+
from dataface.core.inspect.search import InvalidRegex, SchemaSearch, SchemaSearchHit
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SchemaSearchArgs(BaseModel):
|
|
30
|
+
"""Search the company's data vocabulary by keyword + structured filters.
|
|
31
|
+
|
|
32
|
+
Returns a flat list of fully-qualified hits across the metadata corpus:
|
|
33
|
+
schema/table/column names, descriptions (column + table), tags, listed
|
|
34
|
+
tests, and meta values. Use this when you want to find vocabulary or
|
|
35
|
+
cross-cutting matches ("which tables have a customer_id column?",
|
|
36
|
+
"find columns tagged pii", "find primary keys in analytics") — not for
|
|
37
|
+
drilling into a specific known table (use ``schema`` for that).
|
|
38
|
+
|
|
39
|
+
Keyword is a case-insensitive substring match by default; pass
|
|
40
|
+
``regex=True`` for a Python regex. Scope narrows which text-bearing
|
|
41
|
+
fields are searched (default: all of name, description, tag, tests,
|
|
42
|
+
meta, owner). Structured filters (``role``, ``tag``, ``has_test``,
|
|
43
|
+
``missing``, ``column_name``, ``table_name``, ``meta``, ``fk_to``) layer
|
|
44
|
+
as additional predicates.
|
|
45
|
+
|
|
46
|
+
Hits are returned in stable iteration order — no ranking, no top-N.
|
|
47
|
+
Each hit reports the path keys via ``location`` ("source.schema.table"
|
|
48
|
+
or "source.schema.table.column"), which text-bearing field matched
|
|
49
|
+
(``matched_field``), an excerpt (``snippet``), and the layer that
|
|
50
|
+
contributed the field (``source`` — "super_schema", "dbt_manifest",
|
|
51
|
+
or "dbt_adapter").
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
55
|
+
|
|
56
|
+
keyword: str = Field(
|
|
57
|
+
"",
|
|
58
|
+
description=(
|
|
59
|
+
"Substring to match (case-insensitive). Empty string disables "
|
|
60
|
+
"keyword matching — use with structured filters (role, tag, "
|
|
61
|
+
"has_test, missing, ...) to enumerate qualifying rows."
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
scope: list[str] | None = Field(
|
|
65
|
+
None,
|
|
66
|
+
description=(
|
|
67
|
+
"Limit which text-bearing fields are searched. "
|
|
68
|
+
"Choices: name, description, tag, tests, meta, owner. "
|
|
69
|
+
"Default: all."
|
|
70
|
+
),
|
|
71
|
+
)
|
|
72
|
+
regex: bool = Field(
|
|
73
|
+
False,
|
|
74
|
+
description="Treat keyword as a Python regex (case-insensitive).",
|
|
75
|
+
)
|
|
76
|
+
role: str | None = Field(
|
|
77
|
+
None,
|
|
78
|
+
description=(
|
|
79
|
+
"Filter columns by profiled role "
|
|
80
|
+
"(primary_key, foreign_key, dimension, measure, ...)."
|
|
81
|
+
),
|
|
82
|
+
)
|
|
83
|
+
tag: str | None = Field(
|
|
84
|
+
None,
|
|
85
|
+
description="Filter to rows with this tag in their tags array.",
|
|
86
|
+
)
|
|
87
|
+
has_test: str | None = Field(
|
|
88
|
+
None,
|
|
89
|
+
description=(
|
|
90
|
+
"Filter columns whose tests include this name "
|
|
91
|
+
"(unique, not_null, accepted_values, relationships, ...)."
|
|
92
|
+
),
|
|
93
|
+
)
|
|
94
|
+
missing: str | None = Field(
|
|
95
|
+
None,
|
|
96
|
+
description=(
|
|
97
|
+
"Emit synthetic hits for rows that LACK this field "
|
|
98
|
+
"(description, tag, owner, ...). matched_field on each hit is "
|
|
99
|
+
"'_missing:<field>'."
|
|
100
|
+
),
|
|
101
|
+
)
|
|
102
|
+
meta: dict[str, str] | None = Field(
|
|
103
|
+
None,
|
|
104
|
+
description="Filter by exact-match meta key=value pairs.",
|
|
105
|
+
)
|
|
106
|
+
column_name: str | None = Field(
|
|
107
|
+
None,
|
|
108
|
+
description=(
|
|
109
|
+
"Filter to columns whose name matches this fnmatch glob "
|
|
110
|
+
"(e.g. *_id, customer_*)."
|
|
111
|
+
),
|
|
112
|
+
)
|
|
113
|
+
table_name: str | None = Field(
|
|
114
|
+
None,
|
|
115
|
+
description=(
|
|
116
|
+
"Filter to tables whose name matches this fnmatch glob "
|
|
117
|
+
"(e.g. stg_*, fct_*)."
|
|
118
|
+
),
|
|
119
|
+
)
|
|
120
|
+
fk_to: str | None = Field(
|
|
121
|
+
None,
|
|
122
|
+
description=(
|
|
123
|
+
"Filter to columns with an explicit relationships: test "
|
|
124
|
+
"pointing at this table. No naming-heuristic inference."
|
|
125
|
+
),
|
|
126
|
+
)
|
|
127
|
+
fields: list[str] | None = Field(
|
|
128
|
+
None,
|
|
129
|
+
description=(
|
|
130
|
+
"Project each hit to a subset of fields "
|
|
131
|
+
"(location, matched_field, snippet, source, value, attrs)."
|
|
132
|
+
),
|
|
133
|
+
)
|
|
134
|
+
limit: int | None = Field(
|
|
135
|
+
None,
|
|
136
|
+
description=(
|
|
137
|
+
"Truncate results to N hits. ``total`` and ``truncated`` "
|
|
138
|
+
"report the pre-truncation count."
|
|
139
|
+
),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class _SearchMeta(BaseModel):
|
|
144
|
+
"""Single ``_meta`` footer per response."""
|
|
145
|
+
|
|
146
|
+
retrieved_at: str
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class SchemaSearchResult(BaseModel):
|
|
150
|
+
"""Flat list of hits across the metadata corpus."""
|
|
151
|
+
|
|
152
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
153
|
+
|
|
154
|
+
success: bool = True
|
|
155
|
+
hits: list[SchemaSearchHit] = Field(
|
|
156
|
+
default_factory=list,
|
|
157
|
+
description="Matching schema entries, ranked by relevance.",
|
|
158
|
+
)
|
|
159
|
+
total: int = 0
|
|
160
|
+
truncated: bool = False
|
|
161
|
+
# Per-source schema-walk failures (unreachable adapter, etc.). Surfaced
|
|
162
|
+
# so callers can tell "no hits" apart from "search couldn't reach a source"
|
|
163
|
+
# — the silent-fallback antipattern AGENTS.md flags.
|
|
164
|
+
warnings: list[str] = Field(
|
|
165
|
+
default_factory=list,
|
|
166
|
+
description="Per-source warnings (e.g. unreachable adapter).",
|
|
167
|
+
)
|
|
168
|
+
meta: _SearchMeta | None = Field(
|
|
169
|
+
default=None, alias="_meta", description="Pagination and timing metadata."
|
|
170
|
+
)
|
|
171
|
+
errors: list[str] = Field(
|
|
172
|
+
default_factory=list, description="Errors encountered during search."
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Projection is applied at dump time — not part of the schema. CLI/MCP
|
|
176
|
+
# callers that pass `fields=[...]` get a lean wire shape; in-process
|
|
177
|
+
# consumers iterate the typed `hits` list with full fields.
|
|
178
|
+
_project_fields: list[str] | None = PrivateAttr(default=None)
|
|
179
|
+
|
|
180
|
+
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
|
|
181
|
+
out = super().model_dump(**kwargs)
|
|
182
|
+
if self._project_fields is not None:
|
|
183
|
+
allowed = set(self._project_fields)
|
|
184
|
+
out["hits"] = [
|
|
185
|
+
{k: v for k, v in hit.items() if k in allowed} for hit in out["hits"]
|
|
186
|
+
]
|
|
187
|
+
return out
|
|
188
|
+
|
|
189
|
+
def model_dump_json(self, **kwargs: Any) -> str:
|
|
190
|
+
# model_dump_json bypasses model_dump — re-route through dump+json.dumps
|
|
191
|
+
# so projection is honored. Force ``mode='json'`` so any future field
|
|
192
|
+
# carrying a non-JSON-native value (datetime, Decimal, Path) is
|
|
193
|
+
# converted by Pydantic, not crashed by stdlib json.dumps. ``indent``
|
|
194
|
+
# is a json.dumps-only kwarg; pop it out of the model_dump call.
|
|
195
|
+
import json as _json
|
|
196
|
+
|
|
197
|
+
indent = kwargs.pop("indent", None)
|
|
198
|
+
kwargs.setdefault("mode", "json")
|
|
199
|
+
return _json.dumps(self.model_dump(**kwargs), indent=indent)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def schema_search(
|
|
203
|
+
keyword: str = "",
|
|
204
|
+
*,
|
|
205
|
+
scope: list[str] | None = None,
|
|
206
|
+
regex: bool = False,
|
|
207
|
+
role: str | None = None,
|
|
208
|
+
tag: str | None = None,
|
|
209
|
+
has_test: str | None = None,
|
|
210
|
+
missing: str | None = None,
|
|
211
|
+
meta: dict[str, str] | None = None,
|
|
212
|
+
column_name: str | None = None,
|
|
213
|
+
table_name: str | None = None,
|
|
214
|
+
fk_to: str | None = None,
|
|
215
|
+
fields: list[str] | None = None,
|
|
216
|
+
limit: int | None = None,
|
|
217
|
+
cache_path: Path | None = None,
|
|
218
|
+
adapter_registry: AdapterRegistry,
|
|
219
|
+
) -> SchemaSearchResult:
|
|
220
|
+
"""Full-text + filter search across the resolver's materialized schema search index."""
|
|
221
|
+
if (
|
|
222
|
+
not keyword
|
|
223
|
+
and missing is None
|
|
224
|
+
and not _any_filter(role, tag, has_test, meta, column_name, table_name, fk_to)
|
|
225
|
+
):
|
|
226
|
+
return SchemaSearchResult(
|
|
227
|
+
success=False,
|
|
228
|
+
errors=[
|
|
229
|
+
"schema_search requires a keyword or at least one filter "
|
|
230
|
+
"(role, tag, has_test, missing, meta, column_name, table_name, fk_to)"
|
|
231
|
+
],
|
|
232
|
+
)
|
|
233
|
+
try:
|
|
234
|
+
resolver = build_resolver(adapter_registry, cache_path)
|
|
235
|
+
searcher = SchemaSearch(resolver)
|
|
236
|
+
hits, warnings = searcher.search(
|
|
237
|
+
keyword,
|
|
238
|
+
scope=scope,
|
|
239
|
+
regex=regex,
|
|
240
|
+
role=role,
|
|
241
|
+
tag=tag,
|
|
242
|
+
has_test=has_test,
|
|
243
|
+
missing=missing,
|
|
244
|
+
meta=meta,
|
|
245
|
+
column_name=column_name,
|
|
246
|
+
table_name=table_name,
|
|
247
|
+
fk_to=fk_to,
|
|
248
|
+
)
|
|
249
|
+
except InvalidRegex as exc:
|
|
250
|
+
return SchemaSearchResult(success=False, errors=[str(exc)])
|
|
251
|
+
except Exception as exc: # noqa: BLE001 — verb returns errors via response envelope
|
|
252
|
+
return SchemaSearchResult(success=False, errors=[str(exc)])
|
|
253
|
+
|
|
254
|
+
total = len(hits)
|
|
255
|
+
truncated = False
|
|
256
|
+
if limit is not None and total > limit:
|
|
257
|
+
hits = hits[:limit]
|
|
258
|
+
truncated = True
|
|
259
|
+
|
|
260
|
+
result = SchemaSearchResult(
|
|
261
|
+
hits=hits,
|
|
262
|
+
total=total,
|
|
263
|
+
truncated=truncated,
|
|
264
|
+
warnings=warnings,
|
|
265
|
+
_meta=_SearchMeta(retrieved_at=datetime.now(timezone.utc).isoformat()),
|
|
266
|
+
)
|
|
267
|
+
if fields is not None:
|
|
268
|
+
result._project_fields = list(fields)
|
|
269
|
+
return result
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _any_filter(
|
|
273
|
+
role: str | None,
|
|
274
|
+
tag: str | None,
|
|
275
|
+
has_test: str | None,
|
|
276
|
+
meta: dict[str, str] | None,
|
|
277
|
+
column_name: str | None,
|
|
278
|
+
table_name: str | None,
|
|
279
|
+
fk_to: str | None,
|
|
280
|
+
) -> bool:
|
|
281
|
+
return any(
|
|
282
|
+
f is not None
|
|
283
|
+
for f in (role, tag, has_test, meta, column_name, table_name, fk_to)
|
|
284
|
+
)
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""Typed search verb for the agent API."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
9
|
+
|
|
10
|
+
MAX_SEARCH_LIMIT = 50
|
|
11
|
+
DEFAULT_SEARCH_LIMIT = 10
|
|
12
|
+
|
|
13
|
+
# Maintainer paths that must not appear in agent-facing search summaries.
|
|
14
|
+
_INTERNAL_REF_RE = re.compile(r"\s*;?\s*see\s+ai_notes/\S+", re.IGNORECASE)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _agent_safe_summary(description: str) -> str:
|
|
18
|
+
"""Strip maintainer-only references from face descriptions."""
|
|
19
|
+
return _INTERNAL_REF_RE.sub("", description).strip()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SearchDashboardsArgs(BaseModel):
|
|
23
|
+
"""Search existing dashboards by keyword. Returns ranked results with match scores and reasons. Use to discover relevant dashboards and reuse validated query patterns before creating new ones. The returned source_path can be passed to render_dashboard(path=source_path, ...) to inspect that dashboard. Results may also include sample SQL and literal file paths from matching queries; reuse those exact paths instead of inventing new file globs."""
|
|
24
|
+
|
|
25
|
+
model_config = ConfigDict(extra="forbid")
|
|
26
|
+
|
|
27
|
+
query: str = Field(
|
|
28
|
+
...,
|
|
29
|
+
description="Search query text (keywords to match against dashboard metadata)",
|
|
30
|
+
)
|
|
31
|
+
project_dir: Path | None = Field(
|
|
32
|
+
None,
|
|
33
|
+
description="Project directory to search (defaults to the dispatch context's dashboards directory, then CWD)",
|
|
34
|
+
)
|
|
35
|
+
tags: list[str] | None = Field(
|
|
36
|
+
None, description="Filter results to dashboards with ALL of these tags"
|
|
37
|
+
)
|
|
38
|
+
limit: int | None = Field(
|
|
39
|
+
None, description="Maximum results to return (default 10, max 50)"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
_index_cache: list[dict[str, Any]] | None = None
|
|
44
|
+
_index_cache_dir: Path | None = None
|
|
45
|
+
_index_cache_file_set: set[Path] | None = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class DashboardSearchHit(BaseModel):
|
|
49
|
+
title: str
|
|
50
|
+
summary: str
|
|
51
|
+
match_score: float
|
|
52
|
+
match_reasons: list[str]
|
|
53
|
+
source_path: Path
|
|
54
|
+
query_names: list[str]
|
|
55
|
+
chart_names: list[str]
|
|
56
|
+
sample_sql: str | None = None
|
|
57
|
+
file_paths: list[str]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class SearchResult(BaseModel):
|
|
61
|
+
success: bool
|
|
62
|
+
errors: list[str]
|
|
63
|
+
results: list[DashboardSearchHit]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def search_dashboards(
|
|
67
|
+
query: str,
|
|
68
|
+
project_dir: Path | None = None,
|
|
69
|
+
tags: list[str] | None = None,
|
|
70
|
+
limit: int = DEFAULT_SEARCH_LIMIT,
|
|
71
|
+
) -> SearchResult:
|
|
72
|
+
"""Search dashboards by keyword with ranked results."""
|
|
73
|
+
limit = max(0, min(limit, MAX_SEARCH_LIMIT))
|
|
74
|
+
query_stripped = query.strip()
|
|
75
|
+
|
|
76
|
+
if not query_stripped or limit == 0:
|
|
77
|
+
return SearchResult(success=True, errors=[], results=[])
|
|
78
|
+
|
|
79
|
+
query_tokens = list(_tokenize(query_stripped))
|
|
80
|
+
if not query_tokens:
|
|
81
|
+
return SearchResult(success=True, errors=[], results=[])
|
|
82
|
+
|
|
83
|
+
index = _get_index(project_dir or Path("."))
|
|
84
|
+
|
|
85
|
+
if tags:
|
|
86
|
+
required_tags = {t.lower() for t in tags}
|
|
87
|
+
index = [e for e in index if required_tags.issubset(set(e["tags"]))]
|
|
88
|
+
|
|
89
|
+
scored: list[tuple[float, Path, dict[str, Any], list[str]]] = []
|
|
90
|
+
for entry in index:
|
|
91
|
+
score, reasons = _score_entry(entry, query_tokens)
|
|
92
|
+
if score > 0:
|
|
93
|
+
scored.append((score, entry["source_path"], entry, reasons))
|
|
94
|
+
|
|
95
|
+
scored.sort(key=lambda x: (-x[0], str(x[1])))
|
|
96
|
+
|
|
97
|
+
results = []
|
|
98
|
+
for score, _path, entry, reasons in scored[:limit]:
|
|
99
|
+
results.append(
|
|
100
|
+
DashboardSearchHit(
|
|
101
|
+
title=entry["title"],
|
|
102
|
+
summary=entry["description"],
|
|
103
|
+
match_score=round(score, 2),
|
|
104
|
+
match_reasons=reasons,
|
|
105
|
+
source_path=entry["source_path"],
|
|
106
|
+
query_names=entry["query_names"],
|
|
107
|
+
chart_names=entry["chart_names"],
|
|
108
|
+
sample_sql=(
|
|
109
|
+
entry["sql_snippets"][0] if entry["sql_snippets"] else None
|
|
110
|
+
),
|
|
111
|
+
file_paths=_extract_file_paths(entry["sql_snippets"]),
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return SearchResult(success=True, errors=[], results=results)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _build_index(directory: Path) -> list[dict[str, Any]]:
|
|
119
|
+
from dataface.agent_api.dashboards import list_dashboards
|
|
120
|
+
|
|
121
|
+
listing = list_dashboards(directory, recursive=True)
|
|
122
|
+
entries: list[dict[str, Any]] = []
|
|
123
|
+
|
|
124
|
+
for dash in listing.dashboards:
|
|
125
|
+
abs_path = dash.absolute_path
|
|
126
|
+
try:
|
|
127
|
+
content = yaml.safe_load(abs_path.read_text())
|
|
128
|
+
except (yaml.YAMLError, OSError):
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
if not isinstance(content, dict):
|
|
132
|
+
continue
|
|
133
|
+
|
|
134
|
+
tags = content.get("tags", [])
|
|
135
|
+
queries = content.get("queries", {})
|
|
136
|
+
charts = content.get("charts", {})
|
|
137
|
+
|
|
138
|
+
sql_snippets: list[str] = []
|
|
139
|
+
query_names: list[str] = []
|
|
140
|
+
if isinstance(queries, dict):
|
|
141
|
+
for qname, qdef in queries.items():
|
|
142
|
+
query_names.append(qname)
|
|
143
|
+
if isinstance(qdef, dict) and qdef.get("sql"):
|
|
144
|
+
sql_snippets.append(qdef["sql"])
|
|
145
|
+
|
|
146
|
+
chart_names: list[str] = []
|
|
147
|
+
if isinstance(charts, dict):
|
|
148
|
+
chart_names = list(charts.keys())
|
|
149
|
+
|
|
150
|
+
entries.append(
|
|
151
|
+
{
|
|
152
|
+
"source_path": dash.path,
|
|
153
|
+
"title": dash.title,
|
|
154
|
+
"description": _agent_safe_summary(dash.description),
|
|
155
|
+
"tags": [t.lower() for t in tags] if isinstance(tags, list) else [],
|
|
156
|
+
"query_names": query_names,
|
|
157
|
+
"chart_names": chart_names,
|
|
158
|
+
"sql_snippets": sql_snippets,
|
|
159
|
+
"mtime": abs_path.stat().st_mtime,
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
return entries
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _get_index(directory: Path) -> list[dict[str, Any]]:
|
|
167
|
+
global _index_cache, _index_cache_dir, _index_cache_file_set
|
|
168
|
+
|
|
169
|
+
dir_path = directory.resolve()
|
|
170
|
+
|
|
171
|
+
if _index_cache is not None and _index_cache_dir == dir_path:
|
|
172
|
+
current_files = {
|
|
173
|
+
f.relative_to(dir_path)
|
|
174
|
+
for f in dir_path.glob("**/*.yml")
|
|
175
|
+
if not f.name.startswith("_")
|
|
176
|
+
}
|
|
177
|
+
current_files |= {
|
|
178
|
+
f.relative_to(dir_path)
|
|
179
|
+
for f in dir_path.glob("**/*.yaml")
|
|
180
|
+
if not f.name.startswith("_")
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if current_files == _index_cache_file_set:
|
|
184
|
+
still_valid = True
|
|
185
|
+
for entry in _index_cache:
|
|
186
|
+
fpath = dir_path / entry["source_path"]
|
|
187
|
+
try:
|
|
188
|
+
if fpath.stat().st_mtime != entry["mtime"]:
|
|
189
|
+
still_valid = False
|
|
190
|
+
break
|
|
191
|
+
except OSError:
|
|
192
|
+
still_valid = False
|
|
193
|
+
break
|
|
194
|
+
if still_valid:
|
|
195
|
+
return _index_cache
|
|
196
|
+
|
|
197
|
+
snapshot = {
|
|
198
|
+
f.relative_to(dir_path)
|
|
199
|
+
for f in dir_path.glob("**/*.yml")
|
|
200
|
+
if not f.name.startswith("_")
|
|
201
|
+
}
|
|
202
|
+
snapshot |= {
|
|
203
|
+
f.relative_to(dir_path)
|
|
204
|
+
for f in dir_path.glob("**/*.yaml")
|
|
205
|
+
if not f.name.startswith("_")
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
_index_cache = _build_index(directory)
|
|
209
|
+
_index_cache_dir = dir_path
|
|
210
|
+
_index_cache_file_set = snapshot
|
|
211
|
+
return _index_cache
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _tokenize(text: str) -> set[str]:
|
|
215
|
+
return set(re.findall(r"[a-z0-9]+", text.lower()))
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _extract_file_paths(sql_snippets: list[str]) -> list[str]:
|
|
219
|
+
paths: list[str] = []
|
|
220
|
+
pattern = re.compile(
|
|
221
|
+
r"""read_(?:csv|csv_auto|parquet|json(?:_auto)?)\(\s*['"]([^'"]+)['"]""",
|
|
222
|
+
re.IGNORECASE,
|
|
223
|
+
)
|
|
224
|
+
for snippet in sql_snippets:
|
|
225
|
+
for match in pattern.findall(snippet):
|
|
226
|
+
if match not in paths:
|
|
227
|
+
paths.append(match)
|
|
228
|
+
return paths
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _score_entry(
|
|
232
|
+
entry: dict[str, Any], query_tokens: list[str]
|
|
233
|
+
) -> tuple[float, list[str]]:
|
|
234
|
+
score = 0.0
|
|
235
|
+
reasons: list[str] = []
|
|
236
|
+
|
|
237
|
+
title_tokens = _tokenize(entry["title"])
|
|
238
|
+
desc_tokens = _tokenize(entry["description"])
|
|
239
|
+
tag_set = set(entry["tags"])
|
|
240
|
+
query_name_tokens = _tokenize(" ".join(entry["query_names"]))
|
|
241
|
+
chart_name_tokens = _tokenize(" ".join(entry["chart_names"]))
|
|
242
|
+
sql_tokens = _tokenize(" ".join(entry["sql_snippets"]))
|
|
243
|
+
|
|
244
|
+
for qt in query_tokens:
|
|
245
|
+
if qt in title_tokens:
|
|
246
|
+
score += 3.0
|
|
247
|
+
if "title_match" not in reasons:
|
|
248
|
+
reasons.append("title_match")
|
|
249
|
+
|
|
250
|
+
if qt in tag_set:
|
|
251
|
+
score += 2.5
|
|
252
|
+
if "tag_match" not in reasons:
|
|
253
|
+
reasons.append("tag_match")
|
|
254
|
+
|
|
255
|
+
if qt in desc_tokens:
|
|
256
|
+
score += 1.5
|
|
257
|
+
if "description_match" not in reasons:
|
|
258
|
+
reasons.append("description_match")
|
|
259
|
+
|
|
260
|
+
if qt in query_name_tokens or qt in chart_name_tokens:
|
|
261
|
+
score += 1.0
|
|
262
|
+
if "metric_overlap" not in reasons:
|
|
263
|
+
reasons.append("metric_overlap")
|
|
264
|
+
|
|
265
|
+
if qt in sql_tokens:
|
|
266
|
+
score += 0.5
|
|
267
|
+
if "sql_match" not in reasons:
|
|
268
|
+
reasons.append("sql_match")
|
|
269
|
+
|
|
270
|
+
return score, reasons
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""File-based install of wheel workflow skills into agent skill directories."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
from collections.abc import Set
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
from dataface.agent_api.skill_render import render_skill_body
|
|
12
|
+
from dataface.agent_api.skills import Skill, all_skill_names, list_skills
|
|
13
|
+
|
|
14
|
+
# Pattern skills are not file-installed; tombstone retired dirs on re-run.
|
|
15
|
+
RETIRED_SKILL_NAMES: tuple[str, ...] = (
|
|
16
|
+
"before-after-comparison",
|
|
17
|
+
"drill-down-link",
|
|
18
|
+
"faceted-small-multiples",
|
|
19
|
+
"filter-bar-with-variables",
|
|
20
|
+
"kpi-row",
|
|
21
|
+
"single-metric-bignum",
|
|
22
|
+
"table-heavy-ops-dashboard",
|
|
23
|
+
"time-series-trend",
|
|
24
|
+
"top-n-with-detail",
|
|
25
|
+
"two-by-two-grid-overview",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
_LEGACY_SKILL_ROOTS: tuple[Path, ...] = (
|
|
29
|
+
Path(".cursor/skills"),
|
|
30
|
+
Path(".codex/skills"),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
_SKILL_TARGET_DIRS: dict[str, Path] = {
|
|
34
|
+
"agents": Path(".agents/skills"),
|
|
35
|
+
"codex": Path(".agents/skills"),
|
|
36
|
+
"claude": Path(".claude/skills"),
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
SKILL_INSTALL_TARGETS: frozenset[str] = frozenset(_SKILL_TARGET_DIRS)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class InstallSkillsResult(BaseModel):
|
|
43
|
+
installed: list[str] = Field(default_factory=list)
|
|
44
|
+
retired_removed: list[str] = Field(default_factory=list)
|
|
45
|
+
skipped_existing: list[str] = Field(default_factory=list)
|
|
46
|
+
legacy_dirs_detected: list[Path] = Field(default_factory=list)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def skills_for_file_install() -> list[Skill]:
|
|
50
|
+
"""Workflow skills exposed on the CLI surface."""
|
|
51
|
+
return sorted(
|
|
52
|
+
(s for s in list_skills(surface="cli").skills if s.kind == "workflow"),
|
|
53
|
+
key=lambda s: s.name,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def target_dir_for(target: str, *, project_root: Path) -> Path:
|
|
58
|
+
rel = _SKILL_TARGET_DIRS[target]
|
|
59
|
+
return project_root / rel
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def detect_skill_targets(project_root: Path) -> list[str]:
|
|
63
|
+
"""Return install target keys detected under ``project_root``."""
|
|
64
|
+
targets: list[str] = []
|
|
65
|
+
if (project_root / ".cursor").is_dir() or (project_root / "AGENTS.md").is_file():
|
|
66
|
+
targets.append("agents")
|
|
67
|
+
if (project_root / "CLAUDE.md").is_file():
|
|
68
|
+
targets.append("claude")
|
|
69
|
+
return targets
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def detect_legacy_skill_dirs(
|
|
73
|
+
project_root: Path, wheel_skill_names: Set[str]
|
|
74
|
+
) -> list[Path]:
|
|
75
|
+
"""Legacy triplicate dirs that still contain a wheel skill name."""
|
|
76
|
+
found: list[Path] = []
|
|
77
|
+
for rel_root in _LEGACY_SKILL_ROOTS:
|
|
78
|
+
root = project_root / rel_root
|
|
79
|
+
if not root.is_dir():
|
|
80
|
+
continue
|
|
81
|
+
for child in root.iterdir():
|
|
82
|
+
if child.is_dir() and child.name in wheel_skill_names:
|
|
83
|
+
found.append(child)
|
|
84
|
+
return found
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _rendered_skill_md(skill: Skill) -> str:
|
|
88
|
+
raw = (skill.directory / "SKILL.md").read_text(encoding="utf-8")
|
|
89
|
+
if not raw.startswith("---"):
|
|
90
|
+
raise ValueError(f"{skill.directory / 'SKILL.md'}: missing frontmatter")
|
|
91
|
+
parts = raw.split("---", 2)
|
|
92
|
+
if len(parts) < 3:
|
|
93
|
+
raise ValueError(f"{skill.directory / 'SKILL.md'}: malformed frontmatter")
|
|
94
|
+
rendered_body = render_skill_body(skill.body, surface="cli")
|
|
95
|
+
return f"---{parts[1]}---\n{rendered_body}"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def install_skills(
|
|
99
|
+
*,
|
|
100
|
+
target_dir: Path,
|
|
101
|
+
project_root: Path,
|
|
102
|
+
force: bool = False,
|
|
103
|
+
check: bool = False,
|
|
104
|
+
) -> InstallSkillsResult:
|
|
105
|
+
"""Install CLI-rendered workflow skills into ``target_dir``."""
|
|
106
|
+
wheel_names = all_skill_names()
|
|
107
|
+
legacy = detect_legacy_skill_dirs(project_root, wheel_names)
|
|
108
|
+
retired_removed: list[str] = []
|
|
109
|
+
installed: list[str] = []
|
|
110
|
+
skipped_existing: list[str] = []
|
|
111
|
+
|
|
112
|
+
if not check:
|
|
113
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
114
|
+
for name in RETIRED_SKILL_NAMES:
|
|
115
|
+
retired_path = target_dir / name
|
|
116
|
+
if retired_path.is_dir():
|
|
117
|
+
shutil.rmtree(retired_path)
|
|
118
|
+
retired_removed.append(name)
|
|
119
|
+
|
|
120
|
+
for skill in skills_for_file_install():
|
|
121
|
+
dest_dir = target_dir / skill.name
|
|
122
|
+
dest_md = dest_dir / "SKILL.md"
|
|
123
|
+
content = _rendered_skill_md(skill)
|
|
124
|
+
if check:
|
|
125
|
+
installed.append(skill.name)
|
|
126
|
+
continue
|
|
127
|
+
if dest_md.is_file() and not force:
|
|
128
|
+
skipped_existing.append(skill.name)
|
|
129
|
+
continue
|
|
130
|
+
if dest_dir.is_dir():
|
|
131
|
+
shutil.rmtree(dest_dir)
|
|
132
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
133
|
+
dest_md.write_text(content, encoding="utf-8")
|
|
134
|
+
installed.append(skill.name)
|
|
135
|
+
|
|
136
|
+
return InstallSkillsResult(
|
|
137
|
+
installed=installed,
|
|
138
|
+
retired_removed=retired_removed,
|
|
139
|
+
skipped_existing=skipped_existing,
|
|
140
|
+
legacy_dirs_detected=legacy,
|
|
141
|
+
)
|