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,312 @@
|
|
|
1
|
+
"""OpenAI function-calling adapters and MCP dispatch shim.
|
|
2
|
+
|
|
3
|
+
Thin wrappers over dataface.agent_api. No business logic here.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
|
+
|
|
13
|
+
import dataface.agent_api.describe_query as _describe_query
|
|
14
|
+
from dataface.agent_api import (
|
|
15
|
+
dashboards as _dash,
|
|
16
|
+
query as _query,
|
|
17
|
+
schema as _schema_mod,
|
|
18
|
+
schema_search as _schema_search_mod,
|
|
19
|
+
search as _search,
|
|
20
|
+
skills as _skills,
|
|
21
|
+
)
|
|
22
|
+
from dataface.agent_api.validate import (
|
|
23
|
+
ValidateDashboardArgs as _ValidateDashboardArgs,
|
|
24
|
+
validate as _validate_func,
|
|
25
|
+
)
|
|
26
|
+
from dataface.ai.context import DatafaceAIContext
|
|
27
|
+
from dataface.ai.tool_schemas import (
|
|
28
|
+
DESCRIBE_QUERY,
|
|
29
|
+
DOCS,
|
|
30
|
+
EXECUTE_QUERY,
|
|
31
|
+
QUERY_FACE,
|
|
32
|
+
RENDER_DASHBOARD,
|
|
33
|
+
SCHEMA,
|
|
34
|
+
SEARCH_DASHBOARDS,
|
|
35
|
+
to_openai_tool,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
if TYPE_CHECKING:
|
|
39
|
+
from dataface.ai.external_mcp import ExternalMCPManager
|
|
40
|
+
|
|
41
|
+
ToolHandler = Callable[[dict[str, Any], DatafaceAIContext], dict[str, Any]]
|
|
42
|
+
|
|
43
|
+
TOOL_RENDER_DASHBOARD: dict[str, Any] = to_openai_tool(
|
|
44
|
+
RENDER_DASHBOARD,
|
|
45
|
+
property_subset=[
|
|
46
|
+
"path",
|
|
47
|
+
"yaml_content",
|
|
48
|
+
"project_dir",
|
|
49
|
+
"variables",
|
|
50
|
+
"format",
|
|
51
|
+
"as_link",
|
|
52
|
+
],
|
|
53
|
+
)
|
|
54
|
+
TOOL_EXECUTE_QUERY: dict[str, Any] = to_openai_tool(
|
|
55
|
+
EXECUTE_QUERY, property_subset=["sql", "variables", "limit"]
|
|
56
|
+
)
|
|
57
|
+
TOOL_DESCRIBE_QUERY: dict[str, Any] = to_openai_tool(
|
|
58
|
+
DESCRIBE_QUERY, property_subset=["sql", "source", "dialect"]
|
|
59
|
+
)
|
|
60
|
+
TOOL_SCHEMA: dict[str, Any] = to_openai_tool(
|
|
61
|
+
SCHEMA, property_subset=["source", "schema", "table", "column", "table_search"]
|
|
62
|
+
)
|
|
63
|
+
TOOL_SEARCH_DASHBOARDS: dict[str, Any] = to_openai_tool(SEARCH_DASHBOARDS)
|
|
64
|
+
TOOL_QUERY_FACE: dict[str, Any] = to_openai_tool(QUERY_FACE)
|
|
65
|
+
TOOL_DOCS: dict[str, Any] = to_openai_tool(DOCS)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def get_tools(include_data_tools: bool = True) -> list[dict[str, Any]]:
|
|
69
|
+
tools: list[dict[str, Any]] = [TOOL_RENDER_DASHBOARD, TOOL_DOCS]
|
|
70
|
+
if include_data_tools:
|
|
71
|
+
tools.extend(
|
|
72
|
+
[
|
|
73
|
+
TOOL_EXECUTE_QUERY,
|
|
74
|
+
TOOL_DESCRIBE_QUERY,
|
|
75
|
+
TOOL_QUERY_FACE,
|
|
76
|
+
TOOL_SCHEMA,
|
|
77
|
+
TOOL_SEARCH_DASHBOARDS,
|
|
78
|
+
]
|
|
79
|
+
)
|
|
80
|
+
return tools
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _render_project_dir(args: dict[str, Any], ctx: DatafaceAIContext) -> Path | None:
|
|
84
|
+
project_dir = args.get("project_dir")
|
|
85
|
+
if project_dir is not None:
|
|
86
|
+
return Path(project_dir)
|
|
87
|
+
if ctx.default_project_dir is not None:
|
|
88
|
+
return ctx.default_project_dir
|
|
89
|
+
return ctx.adapter_registry.project_root
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _render_path(path: str | None, project_dir: Path | None) -> Path | None:
|
|
93
|
+
if not path:
|
|
94
|
+
return None
|
|
95
|
+
raw_path = Path(path)
|
|
96
|
+
if project_dir is None or not raw_path.is_absolute():
|
|
97
|
+
return raw_path
|
|
98
|
+
try:
|
|
99
|
+
return raw_path.resolve().relative_to(project_dir.resolve())
|
|
100
|
+
except ValueError:
|
|
101
|
+
return raw_path
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _handle_render(args: dict[str, Any], ctx: DatafaceAIContext) -> dict[str, Any]:
|
|
105
|
+
parsed = _dash.RenderDashboardArgs.model_validate(args)
|
|
106
|
+
project_dir = _render_project_dir(args, ctx)
|
|
107
|
+
return _dash.render_dashboard(
|
|
108
|
+
path=_render_path(str(parsed.path) if parsed.path else None, project_dir),
|
|
109
|
+
yaml_content=parsed.yaml_content,
|
|
110
|
+
project_dir=project_dir,
|
|
111
|
+
variables=parsed.variables,
|
|
112
|
+
format=parsed.format or "json",
|
|
113
|
+
as_link=parsed.as_link,
|
|
114
|
+
adapter_registry=ctx.adapter_registry,
|
|
115
|
+
server_port=ctx.server_port,
|
|
116
|
+
).model_dump(mode="json", exclude_none=True)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _handle_query(args: dict[str, Any], ctx: DatafaceAIContext) -> dict[str, Any]:
|
|
120
|
+
return _query.execute_query(
|
|
121
|
+
sql=args.get("sql", ""),
|
|
122
|
+
variables=args.get("variables"),
|
|
123
|
+
source=args.get("source"),
|
|
124
|
+
limit=args.get("limit", 50),
|
|
125
|
+
adapter_registry=ctx.adapter_registry,
|
|
126
|
+
).model_dump(mode="json", exclude_none=True)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _handle_describe_query(
|
|
130
|
+
args: dict[str, Any], ctx: DatafaceAIContext
|
|
131
|
+
) -> dict[str, Any]:
|
|
132
|
+
return _describe_query.describe_query(
|
|
133
|
+
sql=args.get("sql", ""),
|
|
134
|
+
source=args.get("source"),
|
|
135
|
+
dialect=args.get("dialect"),
|
|
136
|
+
adapter_registry=ctx.adapter_registry,
|
|
137
|
+
).model_dump(mode="json", exclude_none=True)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _handle_query_face(args: dict[str, Any], ctx: DatafaceAIContext) -> dict[str, Any]:
|
|
141
|
+
parsed = _query.QueryFaceArgs.model_validate(args)
|
|
142
|
+
return _query.query_face(
|
|
143
|
+
name=parsed.name,
|
|
144
|
+
path=parsed.path,
|
|
145
|
+
project_dir=_render_project_dir(args, ctx),
|
|
146
|
+
vars=parsed.vars,
|
|
147
|
+
limit=parsed.limit,
|
|
148
|
+
adapter_registry=ctx.adapter_registry,
|
|
149
|
+
).model_dump(mode="json", exclude_none=True)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _force_refresh_arg(args: dict[str, Any]) -> bool:
|
|
153
|
+
raw = args.get("force_refresh", False)
|
|
154
|
+
if raw is None:
|
|
155
|
+
return False
|
|
156
|
+
if isinstance(raw, bool):
|
|
157
|
+
return raw
|
|
158
|
+
raise ValueError("force_refresh must be a JSON boolean")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _handle_schema(args: dict[str, Any], ctx: DatafaceAIContext) -> dict[str, Any]:
|
|
162
|
+
try:
|
|
163
|
+
force_refresh = _force_refresh_arg(args)
|
|
164
|
+
except ValueError as exc:
|
|
165
|
+
return {"success": False, "sources": {}, "errors": [str(exc)]}
|
|
166
|
+
|
|
167
|
+
return _schema_mod.schema(
|
|
168
|
+
source=args.get("source"),
|
|
169
|
+
schema=args.get("schema"),
|
|
170
|
+
table=args.get("table"),
|
|
171
|
+
column=args.get("column"),
|
|
172
|
+
force_refresh=force_refresh,
|
|
173
|
+
table_search=args.get("table_search"),
|
|
174
|
+
adapter_registry=ctx.adapter_registry,
|
|
175
|
+
).model_dump(mode="json", by_alias=True, exclude_none=True)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _handle_schema_search(
|
|
179
|
+
args: dict[str, Any], ctx: DatafaceAIContext
|
|
180
|
+
) -> dict[str, Any]:
|
|
181
|
+
parsed = _schema_search_mod.SchemaSearchArgs.model_validate(args)
|
|
182
|
+
return _schema_search_mod.schema_search(
|
|
183
|
+
**parsed.model_dump(),
|
|
184
|
+
adapter_registry=ctx.adapter_registry,
|
|
185
|
+
).model_dump(mode="json", by_alias=True, exclude_none=True)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _handle_search(args: dict[str, Any], ctx: DatafaceAIContext) -> dict[str, Any]:
|
|
189
|
+
parsed = _search.SearchDashboardsArgs.model_validate(args)
|
|
190
|
+
kwargs: dict[str, Any] = {
|
|
191
|
+
"query": parsed.query,
|
|
192
|
+
"project_dir": parsed.project_dir or ctx.dashboards_directory,
|
|
193
|
+
"tags": parsed.tags,
|
|
194
|
+
}
|
|
195
|
+
if parsed.limit is not None:
|
|
196
|
+
kwargs["limit"] = parsed.limit
|
|
197
|
+
return _search.search_dashboards(**kwargs).model_dump(
|
|
198
|
+
mode="json", exclude_none=True
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _handle_docs(args: dict[str, Any], ctx: DatafaceAIContext) -> dict[str, Any]:
|
|
203
|
+
from dataface.agent_api.docs import DocsArgs, docs as _docs
|
|
204
|
+
|
|
205
|
+
parsed = DocsArgs.model_validate(args)
|
|
206
|
+
return _docs(
|
|
207
|
+
topic=parsed.topic,
|
|
208
|
+
search=parsed.search,
|
|
209
|
+
limit=parsed.limit,
|
|
210
|
+
).model_dump(mode="json")
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _handle_describe_dashboard(
|
|
214
|
+
args: dict[str, Any], ctx: DatafaceAIContext
|
|
215
|
+
) -> dict[str, Any]:
|
|
216
|
+
from dataface.agent_api.describe import DescribeFaceArgs, describe_face
|
|
217
|
+
|
|
218
|
+
parsed = DescribeFaceArgs.model_validate(args)
|
|
219
|
+
return describe_face(
|
|
220
|
+
path=parsed.path,
|
|
221
|
+
project_dir=_render_project_dir(args, ctx),
|
|
222
|
+
).model_dump(mode="json", exclude_none=True)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# Wheel-internal bookkeeping fields that MCP clients don't need on the wire.
|
|
226
|
+
# `surfaces` is the per-skill visibility filter; `rendered_for` is the marker
|
|
227
|
+
# the registry stamps on after macro expansion. Both are useful for in-process
|
|
228
|
+
# callers (tests, the CLI) but only add noise to MCP tool responses.
|
|
229
|
+
_SKILL_FIELDS_INTERNAL = {"surfaces", "rendered_for"}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _handle_list_skills(args: dict[str, Any], ctx: DatafaceAIContext) -> dict[str, Any]:
|
|
233
|
+
return _skills.list_skills(surface="mcp").model_dump(
|
|
234
|
+
mode="json",
|
|
235
|
+
exclude_none=True,
|
|
236
|
+
exclude={"skills": {"__all__": _SKILL_FIELDS_INTERNAL}},
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _handle_get_skill(args: dict[str, Any], ctx: DatafaceAIContext) -> dict[str, Any]:
|
|
241
|
+
parsed = _skills.GetSkillArgs.model_validate(args)
|
|
242
|
+
try:
|
|
243
|
+
skill = _skills.get_skill(parsed.name, surface="mcp")
|
|
244
|
+
except _skills.SkillNotFound as exc:
|
|
245
|
+
return {"success": False, "errors": [str(exc)]}
|
|
246
|
+
return skill.model_dump(
|
|
247
|
+
mode="json", exclude_none=True, exclude=_SKILL_FIELDS_INTERNAL
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _handle_search_skills(
|
|
252
|
+
args: dict[str, Any], ctx: DatafaceAIContext
|
|
253
|
+
) -> dict[str, Any]:
|
|
254
|
+
parsed = _skills.SearchSkillsArgs.model_validate(args)
|
|
255
|
+
return _skills.search_skills(
|
|
256
|
+
parsed.query, limit=parsed.limit, surface="mcp"
|
|
257
|
+
).model_dump(mode="json", exclude_none=True)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _handle_validate(args: dict[str, Any], ctx: DatafaceAIContext) -> dict[str, Any]:
|
|
261
|
+
parsed = _ValidateDashboardArgs.model_validate(args)
|
|
262
|
+
return _validate_func(
|
|
263
|
+
path=parsed.path,
|
|
264
|
+
project_dir=_render_project_dir(args, ctx),
|
|
265
|
+
).model_dump(mode="json", exclude_none=True)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
TOOL_HANDLERS: dict[str, ToolHandler] = {
|
|
269
|
+
"validate_dashboard": _handle_validate,
|
|
270
|
+
"render_dashboard": _handle_render,
|
|
271
|
+
"execute_query": _handle_query,
|
|
272
|
+
"describe_query": _handle_describe_query,
|
|
273
|
+
"query_face": _handle_query_face,
|
|
274
|
+
"schema": _handle_schema,
|
|
275
|
+
"schema_search": _handle_schema_search,
|
|
276
|
+
"search_dashboards": _handle_search,
|
|
277
|
+
"docs": _handle_docs,
|
|
278
|
+
"describe_dashboard": _handle_describe_dashboard,
|
|
279
|
+
"list_skills": _handle_list_skills,
|
|
280
|
+
"get_skill": _handle_get_skill,
|
|
281
|
+
"search_skills": _handle_search_skills,
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def handle_tool_call(
|
|
286
|
+
function_name: str, function_args: dict[str, Any], *, context: DatafaceAIContext
|
|
287
|
+
) -> str:
|
|
288
|
+
return json.dumps(
|
|
289
|
+
dispatch_tool_call(function_name, function_args, context=context), default=str
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def dispatch_tool_call(
|
|
294
|
+
function_name: str,
|
|
295
|
+
function_args: dict[str, Any],
|
|
296
|
+
*,
|
|
297
|
+
context: DatafaceAIContext,
|
|
298
|
+
external_manager: ExternalMCPManager | None = None,
|
|
299
|
+
) -> dict[str, Any]:
|
|
300
|
+
if "__" in function_name:
|
|
301
|
+
if external_manager is None:
|
|
302
|
+
return {
|
|
303
|
+
"error": f"External tool '{function_name}' called but no MCP manager available"
|
|
304
|
+
}
|
|
305
|
+
return external_manager.call_tool(function_name, function_args)
|
|
306
|
+
handler = TOOL_HANDLERS.get(function_name)
|
|
307
|
+
if handler is None:
|
|
308
|
+
return {"error": f"Unknown tool: {function_name}"}
|
|
309
|
+
try:
|
|
310
|
+
return handler(dict(function_args), context)
|
|
311
|
+
except Exception as exc: # noqa: BLE001 — last-resort catch at dispatch boundary
|
|
312
|
+
return {"success": False, "errors": [str(exc)]}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""YAML extraction utilities for AI responses.
|
|
2
|
+
|
|
3
|
+
This module provides utilities for extracting YAML content from AI-generated
|
|
4
|
+
text responses.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def extract_yaml(text: str) -> str | None:
|
|
13
|
+
"""Extract YAML code block from text.
|
|
14
|
+
|
|
15
|
+
Looks for YAML content in markdown code blocks. Supports both
|
|
16
|
+
explicit ```yaml blocks and generic ``` blocks that contain
|
|
17
|
+
YAML-like content.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
text: Text that may contain YAML code blocks
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Extracted YAML content or None if not found
|
|
24
|
+
|
|
25
|
+
Example:
|
|
26
|
+
>>> text = '''Here's the dashboard:
|
|
27
|
+
... ```yaml
|
|
28
|
+
... title: "My Dashboard"
|
|
29
|
+
... queries:
|
|
30
|
+
... sales: { sql: "SELECT * FROM sales" }
|
|
31
|
+
... ```
|
|
32
|
+
... '''
|
|
33
|
+
>>> extract_yaml(text)
|
|
34
|
+
'title: "My Dashboard"\\nqueries:\\n sales: { sql: "SELECT * FROM sales" }'
|
|
35
|
+
"""
|
|
36
|
+
# Look for ```yaml ... ``` blocks (most specific)
|
|
37
|
+
# Allow optional whitespace before closing backticks (handles indented code blocks)
|
|
38
|
+
pattern = r"```yaml\s*\n(.*?)\n\s*```"
|
|
39
|
+
matches = re.findall(pattern, text, re.DOTALL)
|
|
40
|
+
if matches:
|
|
41
|
+
return matches[0].strip()
|
|
42
|
+
|
|
43
|
+
# Also try ``` ... ``` (without yaml tag) - check if it looks like YAML
|
|
44
|
+
pattern = r"```\s*\n(.*?)\n\s*```"
|
|
45
|
+
matches = re.findall(pattern, text, re.DOTALL)
|
|
46
|
+
if matches:
|
|
47
|
+
content = matches[0].strip()
|
|
48
|
+
# Check if it looks like YAML (starts with common YAML keys)
|
|
49
|
+
if content.startswith(("title:", "queries:", "charts:", "rows:", "variables:")):
|
|
50
|
+
return content
|
|
51
|
+
# Also check if it contains YAML-like structure
|
|
52
|
+
if ":" in content and (
|
|
53
|
+
"queries:" in content or "charts:" in content or "rows:" in content
|
|
54
|
+
):
|
|
55
|
+
return content
|
|
56
|
+
|
|
57
|
+
return None
|
dataface/cli/__init__.py
ADDED
dataface/cli/_console.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Console factory and agent/pipe context detection for the dft CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
# The list of env vars to check is fixed at module load; values are looked
|
|
11
|
+
# up per call via os.environ.get.
|
|
12
|
+
AGENT_ENV_VARS = (
|
|
13
|
+
"CLAUDECODE", # Claude Code (official)
|
|
14
|
+
"GEMINI_CLI", # Gemini CLI (official)
|
|
15
|
+
"CLINE_ACTIVE", # Cline v3.24+ (official)
|
|
16
|
+
"CURSOR_AGENT", # Cursor (acknowledged, undocumented)
|
|
17
|
+
"COPILOT_CLI", # GitHub Copilot CLI (community-observed)
|
|
18
|
+
"GOOSE_TERMINAL", # Goose (official)
|
|
19
|
+
"AGENT", # Goose + Amp + emerging cross-agent convention
|
|
20
|
+
"AI_AGENT", # Vercel-proposed universal fallback
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def is_plain_output() -> bool:
|
|
25
|
+
"""Return True when output should be plain (no Rich chrome).
|
|
26
|
+
|
|
27
|
+
True when running inside a known AI agent or when stdout is not a TTY
|
|
28
|
+
(e.g. piped to cat, redirected to a file).
|
|
29
|
+
"""
|
|
30
|
+
return any(os.environ.get(v) for v in AGENT_ENV_VARS) or not sys.stdout.isatty()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def dft_console(*, stderr: bool = False) -> Console:
|
|
34
|
+
"""Build a Rich Console that honors is_plain_output() at call time.
|
|
35
|
+
|
|
36
|
+
Use this in CLI command modules instead of bare Console(...) so that
|
|
37
|
+
agent-PTY and piped contexts (CLAUDECODE=1, stdout-to-file, ...) get
|
|
38
|
+
plain output uniformly.
|
|
39
|
+
"""
|
|
40
|
+
plain = is_plain_output()
|
|
41
|
+
return Console(
|
|
42
|
+
stderr=stderr,
|
|
43
|
+
force_terminal=not plain,
|
|
44
|
+
no_color=plain,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
__all__ = ["AGENT_ENV_VARS", "dft_console", "is_plain_output"]
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Rich-formatted output for StructuredError lists.
|
|
2
|
+
|
|
3
|
+
In plain mode (agent or pipe context) we construct the Console with
|
|
4
|
+
``force_terminal=False, no_color=True`` and skip the surrounding
|
|
5
|
+
``Panel`` wrapper. Inline Rich markup like ``[bold]``, ``[dim]`` is
|
|
6
|
+
consumed by Rich's parser either way — when the Console isn't emitting
|
|
7
|
+
ANSI, the markup leaves no trace in the output. So a single body
|
|
8
|
+
composition handles both surfaces; only the Panel wrapper needs gating.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Literal
|
|
16
|
+
|
|
17
|
+
from rich.panel import Panel
|
|
18
|
+
|
|
19
|
+
from dataface.cli._console import dft_console
|
|
20
|
+
from dataface.core.errors import StructuredError
|
|
21
|
+
from dataface.core.render.warnings.base import RenderWarning
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def print_structured_errors(
|
|
25
|
+
errs: list[StructuredError],
|
|
26
|
+
*,
|
|
27
|
+
json_output: bool = False,
|
|
28
|
+
severity: Literal["error", "warning"] = "error",
|
|
29
|
+
) -> None:
|
|
30
|
+
"""Print structured errors as Rich panels (stderr) or JSON (stdout)."""
|
|
31
|
+
if json_output:
|
|
32
|
+
print(
|
|
33
|
+
json.dumps(
|
|
34
|
+
{"errors": [e.model_dump(exclude_none=True) for e in errs]}, indent=2
|
|
35
|
+
)
|
|
36
|
+
)
|
|
37
|
+
return
|
|
38
|
+
style = "red" if severity == "error" else "yellow"
|
|
39
|
+
console = dft_console(stderr=True)
|
|
40
|
+
for e in errs:
|
|
41
|
+
body_parts = [f"[bold {style}]{e.code}[/] {e.message}"]
|
|
42
|
+
if e.hint:
|
|
43
|
+
body_parts.append(f"[dim]Hint:[/] {e.hint}")
|
|
44
|
+
if e.file:
|
|
45
|
+
loc = e.file + (f":{e.line}" if e.line else "")
|
|
46
|
+
body_parts.append(f"[dim]At:[/] {loc}")
|
|
47
|
+
if e.docs_topic:
|
|
48
|
+
body_parts.append(f"[dim]Docs:[/] dft docs {e.docs_topic}")
|
|
49
|
+
elif e.doc_url:
|
|
50
|
+
body_parts.append(f"[dim]Docs:[/] {e.doc_url}")
|
|
51
|
+
for cmd in e.next_commands:
|
|
52
|
+
body_parts.append(f"[dim]→[/] {cmd.label}: [bold]$ {cmd.command}[/]")
|
|
53
|
+
body = "\n".join(body_parts)
|
|
54
|
+
if console.no_color:
|
|
55
|
+
console.print(body)
|
|
56
|
+
else:
|
|
57
|
+
console.print(Panel(body, border_style=style, expand=False))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def print_render_warnings(
|
|
61
|
+
warnings: list[RenderWarning],
|
|
62
|
+
*,
|
|
63
|
+
path: Path | None = None,
|
|
64
|
+
) -> None:
|
|
65
|
+
"""Print RenderWarning objects as yellow Rich panels (stderr).
|
|
66
|
+
|
|
67
|
+
Mirrors print_structured_errors' panel style at severity=warning so the
|
|
68
|
+
visual treatment matches across error and warning surfaces.
|
|
69
|
+
"""
|
|
70
|
+
style = "yellow"
|
|
71
|
+
console = dft_console(stderr=True)
|
|
72
|
+
for w in warnings:
|
|
73
|
+
chart_prefix = f"{w.chart}: " if w.chart else ""
|
|
74
|
+
body_parts = [f"[bold {style}]{w.code}[/] {chart_prefix}{w.message}"]
|
|
75
|
+
if w.fix:
|
|
76
|
+
body_parts.append(f"[dim]Fix:[/] {w.fix}")
|
|
77
|
+
if path is not None:
|
|
78
|
+
body_parts.append(f"[dim]At:[/] {path}")
|
|
79
|
+
body = "\n".join(body_parts)
|
|
80
|
+
if console.no_color:
|
|
81
|
+
console.print(body)
|
|
82
|
+
else:
|
|
83
|
+
console.print(Panel(body, border_style=style, expand=False))
|
dataface/cli/_extras.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""Optional-dependency gate for CLI commands that need heavyweight extras."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.metadata
|
|
6
|
+
import importlib.util
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import shlex
|
|
10
|
+
import shutil
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
from typing import Literal
|
|
14
|
+
|
|
15
|
+
import typer
|
|
16
|
+
from rich.markup import escape
|
|
17
|
+
from rich.panel import Panel
|
|
18
|
+
|
|
19
|
+
from dataface._install_hint import install_hint
|
|
20
|
+
from dataface.cli._console import dft_console
|
|
21
|
+
|
|
22
|
+
# dist-name → import-name overrides for packages where they differ
|
|
23
|
+
_DIST_TO_IMPORT: dict[str, str] = {
|
|
24
|
+
"prompt-toolkit": "prompt_toolkit",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
_console = dft_console(stderr=True)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _dist_to_import_name(dist_name: str) -> str:
|
|
31
|
+
"""Convert a distribution name to its top-level import name."""
|
|
32
|
+
if dist_name in _DIST_TO_IMPORT:
|
|
33
|
+
return _DIST_TO_IMPORT[dist_name]
|
|
34
|
+
return dist_name.replace("-", "_")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _agent_extra_packages(extra: str) -> list[str]:
|
|
38
|
+
"""Return bare dist names declared under [project.optional-dependencies.<extra>]."""
|
|
39
|
+
try:
|
|
40
|
+
reqs = importlib.metadata.requires("dataface") or []
|
|
41
|
+
except importlib.metadata.PackageNotFoundError as exc:
|
|
42
|
+
_console.print(
|
|
43
|
+
f"[red]Cannot read package metadata for 'dataface' in {sys.executable}. "
|
|
44
|
+
"The package may not be installed correctly.[/red]"
|
|
45
|
+
)
|
|
46
|
+
raise typer.Exit(1) from exc
|
|
47
|
+
|
|
48
|
+
pattern = re.compile(rf"""extra\s*==\s*['"]({re.escape(extra)})['"]""")
|
|
49
|
+
result = []
|
|
50
|
+
for req in reqs:
|
|
51
|
+
if pattern.search(req):
|
|
52
|
+
# strip version specifier — take only the dist name before any space/>;
|
|
53
|
+
dist = re.split(r"[><=!;\s]", req)[0]
|
|
54
|
+
result.append(dist)
|
|
55
|
+
return result
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _missing_packages(extra: str) -> list[str]:
|
|
59
|
+
"""Return dist names from <extra> whose top-level module cannot be imported."""
|
|
60
|
+
missing = []
|
|
61
|
+
for dist in _agent_extra_packages(extra):
|
|
62
|
+
import_name = _dist_to_import_name(dist)
|
|
63
|
+
if importlib.util.find_spec(import_name) is None:
|
|
64
|
+
missing.append(dist)
|
|
65
|
+
return missing
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _resolve_installer() -> Literal["pip", "uv"] | None:
|
|
69
|
+
"""Detect the active installer. Returns 'pip', 'uv', or None if neither available.
|
|
70
|
+
|
|
71
|
+
Priority: UV env var (uv set this when it spawned us) → pip importable → uv on PATH.
|
|
72
|
+
"""
|
|
73
|
+
if os.environ.get("UV"):
|
|
74
|
+
return "uv"
|
|
75
|
+
if importlib.util.find_spec("pip") is not None:
|
|
76
|
+
return "pip"
|
|
77
|
+
if shutil.which("uv"):
|
|
78
|
+
return "uv"
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _installer_command(
|
|
83
|
+
installer: Literal["pip", "uv"], missing: list[str]
|
|
84
|
+
) -> list[str]:
|
|
85
|
+
"""Return the subprocess argv to install missing packages."""
|
|
86
|
+
if installer == "pip":
|
|
87
|
+
return [sys.executable, "-m", "pip", "install", "--no-input", *missing]
|
|
88
|
+
return ["uv", "pip", "install", "--python", sys.executable, *missing]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _installer_prefix(installer: Literal["pip", "uv"]) -> str:
|
|
92
|
+
"""Return the human-readable installer prefix for copy-paste commands."""
|
|
93
|
+
if installer == "pip":
|
|
94
|
+
return "pip install"
|
|
95
|
+
return f"uv pip install --python {shlex.quote(sys.executable)}"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _installer_display_command(
|
|
99
|
+
installer: Literal["pip", "uv"], missing: list[str]
|
|
100
|
+
) -> str:
|
|
101
|
+
"""Return the human-readable install command for the missing packages."""
|
|
102
|
+
return f"{_installer_prefix(installer)} {' '.join(missing)}"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _build_extras_panel(
|
|
106
|
+
extra: str,
|
|
107
|
+
missing: list[str],
|
|
108
|
+
installer: Literal["pip", "uv"] | None,
|
|
109
|
+
) -> Panel:
|
|
110
|
+
if installer is None:
|
|
111
|
+
return Panel(
|
|
112
|
+
"[bold]dft chat[/bold] requires optional packages that are not installed:\n\n"
|
|
113
|
+
+ "\n".join(f" • [cyan]{escape(p)}[/cyan]" for p in missing)
|
|
114
|
+
+ "\n\nNo installer found in this environment. "
|
|
115
|
+
"Install uv (https://docs.astral.sh/uv/getting-started/installation/) "
|
|
116
|
+
"or [dim]pip[/dim] into this interpreter, then re-run — "
|
|
117
|
+
"or install these packages by whatever mechanism you used to install "
|
|
118
|
+
"[dim]dataface[/dim] itself.",
|
|
119
|
+
title="[yellow]Optional dependencies required for `dft chat`[/yellow]",
|
|
120
|
+
expand=False,
|
|
121
|
+
)
|
|
122
|
+
pip_cmd = _installer_display_command(installer, missing)
|
|
123
|
+
canonical_hint = install_hint(extra)
|
|
124
|
+
return Panel(
|
|
125
|
+
"[bold]dft chat[/bold] requires optional packages that are not installed:\n\n"
|
|
126
|
+
+ "\n".join(f" • [cyan]{escape(p)}[/cyan]" for p in missing)
|
|
127
|
+
+ f"\n\nTo install manually:\n [dim]{escape(pip_cmd)}[/dim]\n\n"
|
|
128
|
+
+ f"Or reinstall Dataface with the {escape('[' + extra + ']')} extra:\n"
|
|
129
|
+
+ f" [dim]{escape(canonical_hint)}[/dim]",
|
|
130
|
+
title="[yellow]Optional dependencies required for `dft chat`[/yellow]",
|
|
131
|
+
expand=False,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def install_extras(extra: str, *, interactive: bool) -> None:
|
|
136
|
+
"""Install missing packages for <extra>.
|
|
137
|
+
|
|
138
|
+
interactive=False: silently install without prompting (caller already confirmed).
|
|
139
|
+
interactive=True: show info panel and prompt before installing.
|
|
140
|
+
Raises typer.Exit(1) on install failure or (when interactive) user decline.
|
|
141
|
+
"""
|
|
142
|
+
missing = _missing_packages(extra)
|
|
143
|
+
if not missing:
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
installer = _resolve_installer()
|
|
147
|
+
if installer is None:
|
|
148
|
+
_console.print(_build_extras_panel(extra, missing, installer=None))
|
|
149
|
+
raise typer.Exit(1)
|
|
150
|
+
|
|
151
|
+
if interactive:
|
|
152
|
+
_console.print(_build_extras_panel(extra, missing, installer=installer))
|
|
153
|
+
answer = _console.input("Install now? [Y/n] ").strip().lower()
|
|
154
|
+
if answer not in ("", "y", "yes"):
|
|
155
|
+
raise typer.Exit(1)
|
|
156
|
+
|
|
157
|
+
display_cmd = _installer_display_command(installer, missing)
|
|
158
|
+
try:
|
|
159
|
+
subprocess.check_call(_installer_command(installer, missing))
|
|
160
|
+
except subprocess.CalledProcessError as exc:
|
|
161
|
+
_console.print(f"[red]Install failed. Try manually:\n {display_cmd}[/red]")
|
|
162
|
+
raise typer.Exit(1) from exc
|
|
163
|
+
importlib.invalidate_caches()
|
|
164
|
+
still_missing = _missing_packages(extra)
|
|
165
|
+
if still_missing:
|
|
166
|
+
_console.print(
|
|
167
|
+
f"[red]Install reported success but packages are still missing "
|
|
168
|
+
f"in {sys.executable}. Try:\n {display_cmd}[/red]"
|
|
169
|
+
)
|
|
170
|
+
raise typer.Exit(1)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def require_extras(extra: str) -> None:
|
|
174
|
+
"""Raise typer.Exit(1) (or offer to install) if <extra> packages are missing.
|
|
175
|
+
|
|
176
|
+
- On a TTY without DFT_NO_AUTO_INSTALL=1: prompt the user; install on yes.
|
|
177
|
+
- Otherwise: print the install command and raise Exit(1).
|
|
178
|
+
"""
|
|
179
|
+
missing = _missing_packages(extra)
|
|
180
|
+
if not missing:
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
interactive = sys.stdin.isatty() and os.environ.get("DFT_NO_AUTO_INSTALL") != "1"
|
|
184
|
+
|
|
185
|
+
if interactive:
|
|
186
|
+
install_extras(extra, interactive=True)
|
|
187
|
+
else:
|
|
188
|
+
installer = _resolve_installer()
|
|
189
|
+
_console.print(_build_extras_panel(extra, missing, installer=installer))
|
|
190
|
+
raise typer.Exit(1)
|