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,291 @@
|
|
|
1
|
+
"""Render command implementation."""
|
|
2
|
+
|
|
3
|
+
import json as _json
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from dataface.agent_api._paths import (
|
|
8
|
+
project_root_for,
|
|
9
|
+
setup_render_for_face,
|
|
10
|
+
setup_render_for_yaml,
|
|
11
|
+
)
|
|
12
|
+
from dataface.agent_api.dashboards import RenderedDashboard, render_dashboard
|
|
13
|
+
from dataface.cli._error_format import print_structured_errors
|
|
14
|
+
from dataface.core.errors import StructuredError
|
|
15
|
+
from dataface.core.render.warnings import (
|
|
16
|
+
registry as _warnings_registry,
|
|
17
|
+
unreferenced_chart,
|
|
18
|
+
)
|
|
19
|
+
from dataface.core.render.warnings.base import RenderWarning
|
|
20
|
+
from dataface.core.render.warnings.from_query_diagnostic import KNOWN_RENDER_CODES
|
|
21
|
+
|
|
22
|
+
# Compile-time warning producers don't live on detector modules in the registry,
|
|
23
|
+
# but their codes appear on the unified RenderedDashboard.warnings channel and
|
|
24
|
+
# must be recognized by --ignore-warning. Adapter map is the source of truth
|
|
25
|
+
# for query-validator codes; only orphan emitters (e.g. unreferenced_chart) add here.
|
|
26
|
+
_COMPILE_PRODUCER_CODES: frozenset[str] = KNOWN_RENDER_CODES | {unreferenced_chart.CODE}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _exit_with_errors(errs: list[StructuredError], json_errors: bool) -> None:
|
|
30
|
+
print_structured_errors(errs, json_output=json_errors)
|
|
31
|
+
sys.exit(1)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _emit_unknown_code_notices(ignore_codes: set[str]) -> None:
|
|
35
|
+
"""Print a one-line notice for each code not found in the registry.
|
|
36
|
+
|
|
37
|
+
Unknown codes are forward-compatible suppression stubs (a dataface.yml may
|
|
38
|
+
list codes that haven't shipped yet). They do NOT cause a non-zero exit.
|
|
39
|
+
Uses live module reference so tests can monkeypatch DETECTORS on the registry.
|
|
40
|
+
"""
|
|
41
|
+
known = {d.CODE for d in _warnings_registry.DETECTORS} | _COMPILE_PRODUCER_CODES
|
|
42
|
+
for code in sorted(ignore_codes):
|
|
43
|
+
if code not in known:
|
|
44
|
+
print(f"unknown warning code: {code}", file=sys.stderr)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _format_warnings_stderr(warnings: list[RenderWarning]) -> None:
|
|
48
|
+
"""Write the active warnings block to stderr in the canonical format.
|
|
49
|
+
|
|
50
|
+
Format:
|
|
51
|
+
⚠ N warning(s):
|
|
52
|
+
[CODE] chart-id: message
|
|
53
|
+
→ fix
|
|
54
|
+
"""
|
|
55
|
+
if not warnings:
|
|
56
|
+
return
|
|
57
|
+
n = len(warnings)
|
|
58
|
+
label = "warning" if n == 1 else "warnings"
|
|
59
|
+
print(f"⚠ {n} {label}:", file=sys.stderr)
|
|
60
|
+
for w in warnings:
|
|
61
|
+
chart_prefix = f"{w.chart}: " if w.chart else ""
|
|
62
|
+
print(f" [{w.code}] {chart_prefix}{w.message}", file=sys.stderr)
|
|
63
|
+
if w.fix:
|
|
64
|
+
print(f" → {w.fix}", file=sys.stderr)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _merge_warnings_into_json(
|
|
68
|
+
data: dict[str, object],
|
|
69
|
+
result: RenderedDashboard,
|
|
70
|
+
) -> str:
|
|
71
|
+
"""For JSON format, inject warnings and suppressed_warnings.
|
|
72
|
+
|
|
73
|
+
Agents and consumers reading --format json must see warnings regardless of
|
|
74
|
+
--no-warnings (that flag is a human-output convenience only).
|
|
75
|
+
data is always a dict here (dashboard.py JSON path returns json.loads output).
|
|
76
|
+
"""
|
|
77
|
+
base = dict(data)
|
|
78
|
+
base["warnings"] = [w.model_dump() for w in result.warnings]
|
|
79
|
+
base["suppressed_warnings"] = [w.model_dump() for w in result.suppressed_warnings]
|
|
80
|
+
return _json.dumps(base)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def render_command(
|
|
84
|
+
face_path: Path,
|
|
85
|
+
output: str | None = None,
|
|
86
|
+
format: str = "svg",
|
|
87
|
+
project_dir: Path | None = None,
|
|
88
|
+
variables: dict[str, str] | None = None,
|
|
89
|
+
use_cache: bool = True,
|
|
90
|
+
json_errors: bool = False,
|
|
91
|
+
no_warnings: bool = False,
|
|
92
|
+
ignore_codes: set[str] | None = None,
|
|
93
|
+
) -> str | None:
|
|
94
|
+
"""Render a dashboard to SVG, HTML, PNG, PDF, or terminal.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
face_path: Path to face YAML file
|
|
98
|
+
output: Output file path. For binary formats (png, pdf) and svg/html,
|
|
99
|
+
default is renders/ folder with face name. For json/text/yaml,
|
|
100
|
+
default is stdout. Use "-" to force stdout. Ignored for terminal
|
|
101
|
+
format (always prints to stdout).
|
|
102
|
+
format: Output format (svg, html, png, pdf, terminal, json, text, yaml)
|
|
103
|
+
project_dir: Project directory for resolving relative paths
|
|
104
|
+
variables: Variable values to pass to the render (key=value pairs)
|
|
105
|
+
use_cache: Whether to use cached query results
|
|
106
|
+
json_errors: Emit errors as JSON to stdout instead of Rich panels to stderr
|
|
107
|
+
no_warnings: Suppress stderr warning output (warnings still appear in JSON)
|
|
108
|
+
ignore_codes: Warning codes to suppress via the partition seam
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Output file path on success, None for terminal/stdout output or on error
|
|
112
|
+
"""
|
|
113
|
+
if ignore_codes:
|
|
114
|
+
_emit_unknown_code_notices(ignore_codes)
|
|
115
|
+
|
|
116
|
+
output_dir = project_root_for(project_dir)
|
|
117
|
+
setup, face_file = setup_render_for_face(face_path, project_dir)
|
|
118
|
+
result = render_dashboard(
|
|
119
|
+
path=setup.scoped_path,
|
|
120
|
+
project_dir=setup.scoped_base,
|
|
121
|
+
format=format,
|
|
122
|
+
variables=variables,
|
|
123
|
+
adapter_registry=setup.adapter_registry,
|
|
124
|
+
use_cache=use_cache,
|
|
125
|
+
# Match pre-PR behaviour: 2x retina PNG by default for the CLI.
|
|
126
|
+
scale=2.0,
|
|
127
|
+
ignore_codes=ignore_codes,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
if not result.success:
|
|
131
|
+
errors = result.validation_errors or (
|
|
132
|
+
[result.face_error] if result.face_error else []
|
|
133
|
+
)
|
|
134
|
+
_exit_with_errors(errors, json_errors=json_errors)
|
|
135
|
+
|
|
136
|
+
if not no_warnings:
|
|
137
|
+
_format_warnings_stderr(result.warnings)
|
|
138
|
+
|
|
139
|
+
if format == "json":
|
|
140
|
+
assert isinstance(result.data, dict)
|
|
141
|
+
rendered_content: str | bytes = _merge_warnings_into_json(result.data, result)
|
|
142
|
+
else:
|
|
143
|
+
rendered_content = (
|
|
144
|
+
result.data
|
|
145
|
+
if isinstance(result.data, (str, bytes))
|
|
146
|
+
else _json.dumps(result.data)
|
|
147
|
+
)
|
|
148
|
+
return _write_output(
|
|
149
|
+
rendered_content,
|
|
150
|
+
output=output,
|
|
151
|
+
format=format,
|
|
152
|
+
output_dir=output_dir,
|
|
153
|
+
default_output_stem=face_file.stem,
|
|
154
|
+
source_label=str(face_path),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def render_command_from_yaml(
|
|
159
|
+
yaml_content: str,
|
|
160
|
+
output: str | None = None,
|
|
161
|
+
format: str = "svg",
|
|
162
|
+
project_dir: Path | None = None,
|
|
163
|
+
variables: dict[str, str] | None = None,
|
|
164
|
+
use_cache: bool = True,
|
|
165
|
+
json_errors: bool = False,
|
|
166
|
+
no_warnings: bool = False,
|
|
167
|
+
ignore_codes: set[str] | None = None,
|
|
168
|
+
) -> str | None:
|
|
169
|
+
"""Render a dashboard from YAML content (e.g. stdin).
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
yaml_content: Raw YAML string
|
|
173
|
+
output: Output file path. Binary formats and svg/html default to a
|
|
174
|
+
renders/ file under project_dir; json/text/yaml default to stdout.
|
|
175
|
+
Use "-" to force stdout. Ignored for terminal format.
|
|
176
|
+
format: Output format (svg, html, png, pdf, terminal, json, text, yaml)
|
|
177
|
+
project_dir: Project directory for resolving relative paths
|
|
178
|
+
variables: Variable values to pass to the render
|
|
179
|
+
use_cache: Whether to use cached query results
|
|
180
|
+
json_errors: Emit errors as JSON to stdout instead of Rich panels to stderr
|
|
181
|
+
no_warnings: Suppress stderr warning output (warnings still appear in JSON)
|
|
182
|
+
ignore_codes: Warning codes to suppress via the partition seam
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Output file path on success, None for terminal/stdout output or on error
|
|
186
|
+
"""
|
|
187
|
+
if ignore_codes:
|
|
188
|
+
_emit_unknown_code_notices(ignore_codes)
|
|
189
|
+
|
|
190
|
+
output_dir = project_root_for(project_dir)
|
|
191
|
+
setup = setup_render_for_yaml(project_dir)
|
|
192
|
+
result = render_dashboard(
|
|
193
|
+
yaml_content=yaml_content,
|
|
194
|
+
project_dir=setup.scoped_base,
|
|
195
|
+
format=format,
|
|
196
|
+
variables=variables,
|
|
197
|
+
adapter_registry=setup.adapter_registry,
|
|
198
|
+
use_cache=use_cache,
|
|
199
|
+
scale=2.0,
|
|
200
|
+
ignore_codes=ignore_codes,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
if not result.success:
|
|
204
|
+
errors = result.validation_errors or (
|
|
205
|
+
[result.face_error] if result.face_error else []
|
|
206
|
+
)
|
|
207
|
+
_exit_with_errors(errors, json_errors=json_errors)
|
|
208
|
+
|
|
209
|
+
if not no_warnings:
|
|
210
|
+
_format_warnings_stderr(result.warnings)
|
|
211
|
+
|
|
212
|
+
if format == "json":
|
|
213
|
+
assert isinstance(result.data, dict)
|
|
214
|
+
rendered_content: str | bytes = _merge_warnings_into_json(result.data, result)
|
|
215
|
+
else:
|
|
216
|
+
rendered_content = (
|
|
217
|
+
result.data
|
|
218
|
+
if isinstance(result.data, (str, bytes))
|
|
219
|
+
else _json.dumps(result.data)
|
|
220
|
+
)
|
|
221
|
+
return _write_output(
|
|
222
|
+
rendered_content,
|
|
223
|
+
output=output,
|
|
224
|
+
format=format,
|
|
225
|
+
output_dir=output_dir,
|
|
226
|
+
default_output_stem="stdin",
|
|
227
|
+
source_label="<stdin>",
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _write_output(
|
|
232
|
+
rendered_content: str | bytes,
|
|
233
|
+
output: str | None,
|
|
234
|
+
format: str,
|
|
235
|
+
output_dir: Path,
|
|
236
|
+
default_output_stem: str,
|
|
237
|
+
source_label: str,
|
|
238
|
+
) -> str | None:
|
|
239
|
+
"""Write rendered content to the appropriate destination."""
|
|
240
|
+
# `terminal` is stdout-only by design — --output is meaningless for the
|
|
241
|
+
# ANSI-formatted preview. Every other text format honors --output when set
|
|
242
|
+
# and defaults to stdout otherwise (no implicit `renders/` write).
|
|
243
|
+
if format == "terminal" or (format in ("json", "text", "yaml") and output is None):
|
|
244
|
+
text_output = (
|
|
245
|
+
rendered_content.decode()
|
|
246
|
+
if isinstance(rendered_content, bytes)
|
|
247
|
+
else rendered_content
|
|
248
|
+
)
|
|
249
|
+
print(text_output)
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
if output == "-":
|
|
253
|
+
if isinstance(rendered_content, bytes):
|
|
254
|
+
sys.stdout.buffer.write(rendered_content)
|
|
255
|
+
else:
|
|
256
|
+
print(rendered_content)
|
|
257
|
+
return None
|
|
258
|
+
|
|
259
|
+
# Resolve output paths against output_dir (CWD / --project-dir), not project_root
|
|
260
|
+
is_binary = format in ("png", "pdf")
|
|
261
|
+
output_extension = f".{format}"
|
|
262
|
+
|
|
263
|
+
if output:
|
|
264
|
+
output_path = Path(output)
|
|
265
|
+
if not output_path.is_absolute():
|
|
266
|
+
output_path = output_dir / output_path
|
|
267
|
+
else:
|
|
268
|
+
renders_dir = output_dir / "renders"
|
|
269
|
+
renders_dir.mkdir(exist_ok=True)
|
|
270
|
+
output_path = renders_dir / f"{default_output_stem}{output_extension}"
|
|
271
|
+
|
|
272
|
+
if is_binary:
|
|
273
|
+
content_bytes = (
|
|
274
|
+
rendered_content.encode()
|
|
275
|
+
if isinstance(rendered_content, str)
|
|
276
|
+
else rendered_content
|
|
277
|
+
)
|
|
278
|
+
output_path.write_bytes(content_bytes)
|
|
279
|
+
else:
|
|
280
|
+
content_str = (
|
|
281
|
+
rendered_content.decode()
|
|
282
|
+
if isinstance(rendered_content, bytes)
|
|
283
|
+
else rendered_content
|
|
284
|
+
)
|
|
285
|
+
output_path.write_text(content_str, encoding="utf-8")
|
|
286
|
+
|
|
287
|
+
print(
|
|
288
|
+
f"Rendered {source_label} to {output_path} ({format} format)",
|
|
289
|
+
file=sys.stderr,
|
|
290
|
+
)
|
|
291
|
+
return str(output_path)
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
"""CLI command for `dft schema`.
|
|
2
|
+
|
|
3
|
+
Thin wrapper. All logic lives in ``dataface.agent_api.schema`` (drill-down)
|
|
4
|
+
and ``dataface.agent_api.schema_search`` (search mode); this file parses CLI
|
|
5
|
+
args, calls the appropriate verb, and renders for the terminal.
|
|
6
|
+
|
|
7
|
+
Modes
|
|
8
|
+
-----
|
|
9
|
+
- Drill-down (default): positional ``SOURCE``, ``SCHEMA``, ``TABLE``, ``COLUMN``
|
|
10
|
+
- Search mode (``-s`` / ``--search`` present): dispatches to schema_search;
|
|
11
|
+
all filter flags (``--role``, ``--tag``, ``--scope``, etc.) apply.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import warnings
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
import typer
|
|
21
|
+
|
|
22
|
+
warnings.filterwarnings(
|
|
23
|
+
"ignore",
|
|
24
|
+
message=r"Core Pydantic V1 functionality isn't compatible with Python 3\.14",
|
|
25
|
+
category=UserWarning,
|
|
26
|
+
module=r"dsi_pydantic_shim",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
from dataface.agent_api.schema import SchemaResponse, schema as _schema
|
|
30
|
+
from dataface.agent_api.schema_search import (
|
|
31
|
+
SchemaSearchResult,
|
|
32
|
+
schema_search as _schema_search,
|
|
33
|
+
)
|
|
34
|
+
from dataface.cli._error_format import print_structured_errors
|
|
35
|
+
from dataface.cli._parsing import parse_kv_pairs
|
|
36
|
+
from dataface.core.errors import DF_UNKNOWN_INTERNAL, StructuredError
|
|
37
|
+
from dataface.core.execute.adapters import build_adapter_registry
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def schema_command(
|
|
41
|
+
source: str | None = None,
|
|
42
|
+
schema: str | None = None,
|
|
43
|
+
table: str | None = None,
|
|
44
|
+
column: str | None = None,
|
|
45
|
+
json_output: bool = False,
|
|
46
|
+
project_dir: Path | None = None,
|
|
47
|
+
lineage_depth: int = 1,
|
|
48
|
+
# Search-mode args (only used when search is not None)
|
|
49
|
+
search: str | None = None,
|
|
50
|
+
scope: str | None = None,
|
|
51
|
+
regex: bool = False,
|
|
52
|
+
role: str | None = None,
|
|
53
|
+
tag: str | None = None,
|
|
54
|
+
has_test: str | None = None,
|
|
55
|
+
missing: str | None = None,
|
|
56
|
+
column_name: str | None = None,
|
|
57
|
+
table_name: str | None = None,
|
|
58
|
+
fk_to: str | None = None,
|
|
59
|
+
meta: list[str] | None = None,
|
|
60
|
+
fields: str | None = None,
|
|
61
|
+
limit: int | None = None,
|
|
62
|
+
) -> None:
|
|
63
|
+
"""Browse the data hierarchy or search the schema corpus.
|
|
64
|
+
|
|
65
|
+
``-s`` / ``--search`` has context-dependent semantics:
|
|
66
|
+
|
|
67
|
+
- With ``SOURCE`` + ``SCHEMA`` (no ``TABLE`` / ``COLUMN``): applies a
|
|
68
|
+
``re.search`` regexp filter on the level-3 table list (table name or cached
|
|
69
|
+
column name match). This calls ``agent_api.schema(table_search=...)``.
|
|
70
|
+
- Without drill-down args: full-text + predicate corpus search via
|
|
71
|
+
``agent_api.schema_search``. All ``--role``, ``--tag``, etc. flags apply here.
|
|
72
|
+
- With any other drill-down arg combination: raises ``BadParameter``.
|
|
73
|
+
|
|
74
|
+
MCP callers use ``table_search`` directly on the ``schema`` tool to get the
|
|
75
|
+
regexp filter without going through the ``-s`` routing.
|
|
76
|
+
"""
|
|
77
|
+
project_root = (project_dir or Path(".")).resolve()
|
|
78
|
+
adapter_registry = build_adapter_registry(project_root, read_only=True)
|
|
79
|
+
|
|
80
|
+
if search is not None:
|
|
81
|
+
# Level-3 drill-down + regexp filter: dft schema SOURCE SCHEMA -s PATTERN
|
|
82
|
+
# When source + schema are given but table/column are not, -s applies a
|
|
83
|
+
# regexp table/column-name filter on the drill-down result instead of
|
|
84
|
+
# dispatching to schema_search.
|
|
85
|
+
if (
|
|
86
|
+
source is not None
|
|
87
|
+
and schema is not None
|
|
88
|
+
and table is None
|
|
89
|
+
and column is None
|
|
90
|
+
):
|
|
91
|
+
# Reject search-mode-only flags — they have no meaning in filter mode
|
|
92
|
+
# and silently dropping them would produce a wrong-looking result.
|
|
93
|
+
search_only_flags = {
|
|
94
|
+
"--role": role,
|
|
95
|
+
"--tag": tag,
|
|
96
|
+
"--has-test": has_test,
|
|
97
|
+
"--missing": missing,
|
|
98
|
+
"--column-name": column_name,
|
|
99
|
+
"--table-name": table_name,
|
|
100
|
+
"--fk-to": fk_to,
|
|
101
|
+
"--meta": meta,
|
|
102
|
+
"--fields": fields,
|
|
103
|
+
"--limit": limit,
|
|
104
|
+
"--scope": scope,
|
|
105
|
+
"--regex": regex or None,
|
|
106
|
+
}
|
|
107
|
+
active_search_flags = [f for f, v in search_only_flags.items() if v]
|
|
108
|
+
if active_search_flags:
|
|
109
|
+
raise typer.BadParameter(
|
|
110
|
+
f"flags {active_search_flags} are search-mode-only and cannot "
|
|
111
|
+
"be combined with SOURCE + SCHEMA + -s (filter mode). "
|
|
112
|
+
"Drop the drill-down args to use search mode, or drop the "
|
|
113
|
+
"predicate flags to use filter mode."
|
|
114
|
+
)
|
|
115
|
+
drill_result = _schema(
|
|
116
|
+
source=source,
|
|
117
|
+
schema=schema,
|
|
118
|
+
lineage_depth=lineage_depth,
|
|
119
|
+
table_search=search,
|
|
120
|
+
surface="cli",
|
|
121
|
+
adapter_registry=adapter_registry,
|
|
122
|
+
)
|
|
123
|
+
if json_output:
|
|
124
|
+
typer.echo(
|
|
125
|
+
drill_result.model_dump_json(
|
|
126
|
+
by_alias=True, indent=2, exclude_none=True
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
if not drill_result.success:
|
|
130
|
+
raise typer.Exit(1)
|
|
131
|
+
return
|
|
132
|
+
_print_rich(
|
|
133
|
+
drill_result, source=source, schema=schema, table=None, column=None
|
|
134
|
+
)
|
|
135
|
+
if not drill_result.success:
|
|
136
|
+
raise typer.Exit(1)
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
drill_args = {
|
|
140
|
+
"SOURCE": source,
|
|
141
|
+
"SCHEMA": schema,
|
|
142
|
+
"TABLE": table,
|
|
143
|
+
"COLUMN": column,
|
|
144
|
+
}
|
|
145
|
+
conflicts = [arg for arg, val in drill_args.items() if val is not None]
|
|
146
|
+
if conflicts:
|
|
147
|
+
raise typer.BadParameter(
|
|
148
|
+
f"drill-down args {conflicts} cannot be combined with -s/--search. "
|
|
149
|
+
"Use -s for search mode or positional args for navigation, not both."
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
parsed_meta = parse_kv_pairs(meta, "--meta") if meta else None
|
|
153
|
+
|
|
154
|
+
result = _schema_search(
|
|
155
|
+
keyword=search,
|
|
156
|
+
scope=[s.strip() for s in scope.split(",") if s.strip()] if scope else None,
|
|
157
|
+
regex=regex,
|
|
158
|
+
role=role,
|
|
159
|
+
tag=tag,
|
|
160
|
+
has_test=has_test,
|
|
161
|
+
missing=missing,
|
|
162
|
+
meta=parsed_meta,
|
|
163
|
+
column_name=column_name,
|
|
164
|
+
table_name=table_name,
|
|
165
|
+
fk_to=fk_to,
|
|
166
|
+
fields=(
|
|
167
|
+
[f.strip() for f in fields.split(",") if f.strip()] if fields else None
|
|
168
|
+
),
|
|
169
|
+
limit=limit,
|
|
170
|
+
adapter_registry=adapter_registry,
|
|
171
|
+
)
|
|
172
|
+
if json_output:
|
|
173
|
+
typer.echo(result.model_dump_json(by_alias=True, exclude_none=True))
|
|
174
|
+
if not result.success:
|
|
175
|
+
raise typer.Exit(1)
|
|
176
|
+
return
|
|
177
|
+
_print_rich_search(result)
|
|
178
|
+
if not result.success:
|
|
179
|
+
raise typer.Exit(1)
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
drill_result = _schema(
|
|
183
|
+
source=source,
|
|
184
|
+
schema=schema,
|
|
185
|
+
table=table,
|
|
186
|
+
column=column,
|
|
187
|
+
lineage_depth=lineage_depth,
|
|
188
|
+
surface="cli",
|
|
189
|
+
adapter_registry=adapter_registry,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
if json_output:
|
|
193
|
+
typer.echo(
|
|
194
|
+
drill_result.model_dump_json(by_alias=True, indent=2, exclude_none=True)
|
|
195
|
+
)
|
|
196
|
+
if not drill_result.success:
|
|
197
|
+
raise typer.Exit(1)
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
_print_rich(drill_result, source=source, schema=schema, table=table, column=column)
|
|
201
|
+
if not drill_result.success:
|
|
202
|
+
raise typer.Exit(1)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _print_rich_search(result: SchemaSearchResult) -> None:
|
|
206
|
+
if not result.success:
|
|
207
|
+
# SchemaSearchResult carries only plain strings in errors; synthesize a
|
|
208
|
+
# DF-UNKNOWN-INTERNAL wrapper so print_structured_errors renders cleanly.
|
|
209
|
+
print_structured_errors(
|
|
210
|
+
[
|
|
211
|
+
StructuredError(
|
|
212
|
+
code=DF_UNKNOWN_INTERNAL.code,
|
|
213
|
+
domain=DF_UNKNOWN_INTERNAL.domain,
|
|
214
|
+
doc_url=DF_UNKNOWN_INTERNAL.doc_url,
|
|
215
|
+
docs_topic=DF_UNKNOWN_INTERNAL.docs_topic,
|
|
216
|
+
message="; ".join(result.errors),
|
|
217
|
+
)
|
|
218
|
+
]
|
|
219
|
+
)
|
|
220
|
+
return
|
|
221
|
+
if not result.hits:
|
|
222
|
+
typer.echo("No matches.")
|
|
223
|
+
return
|
|
224
|
+
for hit in result.hits:
|
|
225
|
+
typer.echo(f" [{hit.source}] {hit.location} ({hit.matched_field})")
|
|
226
|
+
if hit.snippet:
|
|
227
|
+
typer.echo(f" {hit.snippet}")
|
|
228
|
+
if result.truncated:
|
|
229
|
+
typer.echo(f"... truncated to {len(result.hits)} of {result.total} hits")
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _spec_is_exact(arg: str) -> bool:
|
|
233
|
+
"""Return True when the CLI arg is a literal name (no glob wildcards)."""
|
|
234
|
+
return "*" not in arg and "?" not in arg and "[" not in arg
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _iter_drill_tables(
|
|
238
|
+
sources: dict[str, Any],
|
|
239
|
+
source: str,
|
|
240
|
+
schema: str,
|
|
241
|
+
) -> list[tuple[str, str, dict[str, Any]]]:
|
|
242
|
+
"""Yield (schema_name, table_name, table_data) over the matching subtree.
|
|
243
|
+
|
|
244
|
+
When schema is an exact name, only that schema is visited. When it contains
|
|
245
|
+
a wildcard, every schema under the source is visited.
|
|
246
|
+
"""
|
|
247
|
+
schemas = sources.get(source, {}).get("schemas", {})
|
|
248
|
+
if _spec_is_exact(schema):
|
|
249
|
+
tables = schemas.get(schema, {}).get("tables") or {}
|
|
250
|
+
return [(schema, tname, tdata) for tname, tdata in tables.items()]
|
|
251
|
+
result = []
|
|
252
|
+
for sname, sdata in schemas.items():
|
|
253
|
+
for tname, tdata in (sdata.get("tables") or {}).items():
|
|
254
|
+
result.append((sname, tname, tdata))
|
|
255
|
+
return result
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _print_rich(
|
|
259
|
+
result: SchemaResponse,
|
|
260
|
+
source: str | None,
|
|
261
|
+
schema: str | None,
|
|
262
|
+
table: str | None,
|
|
263
|
+
column: str | None,
|
|
264
|
+
) -> None:
|
|
265
|
+
if not result.success:
|
|
266
|
+
print_structured_errors(
|
|
267
|
+
result.structured_errors
|
|
268
|
+
or [
|
|
269
|
+
StructuredError(
|
|
270
|
+
code=DF_UNKNOWN_INTERNAL.code,
|
|
271
|
+
domain=DF_UNKNOWN_INTERNAL.domain,
|
|
272
|
+
doc_url=DF_UNKNOWN_INTERNAL.doc_url,
|
|
273
|
+
docs_topic=DF_UNKNOWN_INTERNAL.docs_topic,
|
|
274
|
+
message="; ".join(result.errors),
|
|
275
|
+
)
|
|
276
|
+
]
|
|
277
|
+
)
|
|
278
|
+
return
|
|
279
|
+
|
|
280
|
+
sources = result.sources
|
|
281
|
+
if (
|
|
282
|
+
column is not None
|
|
283
|
+
and table is not None
|
|
284
|
+
and source is not None
|
|
285
|
+
and schema is not None
|
|
286
|
+
):
|
|
287
|
+
rows = _iter_drill_tables(sources, source, schema)
|
|
288
|
+
schema_is_wildcard = not _spec_is_exact(schema)
|
|
289
|
+
first = True
|
|
290
|
+
for sname, tname, tdata in rows:
|
|
291
|
+
label = f"{sname}.{tname}" if schema_is_wildcard else tname
|
|
292
|
+
for cname, cdata in (tdata.get("columns") or {}).items():
|
|
293
|
+
if not first:
|
|
294
|
+
typer.echo("")
|
|
295
|
+
_echo_column(f"{label}.{cname}", cdata)
|
|
296
|
+
first = False
|
|
297
|
+
return
|
|
298
|
+
|
|
299
|
+
if table is not None and source is not None and schema is not None:
|
|
300
|
+
rows = _iter_drill_tables(sources, source, schema)
|
|
301
|
+
schema_is_wildcard = not _spec_is_exact(schema)
|
|
302
|
+
first = True
|
|
303
|
+
for sname, tname, tdata in rows:
|
|
304
|
+
if not first:
|
|
305
|
+
typer.echo("")
|
|
306
|
+
label = f"{sname}.{tname}" if schema_is_wildcard else tname
|
|
307
|
+
_echo_table_profile(label, tdata)
|
|
308
|
+
first = False
|
|
309
|
+
return
|
|
310
|
+
|
|
311
|
+
if schema is not None and source is not None:
|
|
312
|
+
if result.message:
|
|
313
|
+
typer.echo(result.message)
|
|
314
|
+
for hint in result.hints or []:
|
|
315
|
+
typer.echo(f" → {hint}")
|
|
316
|
+
for warning in result.warnings or []:
|
|
317
|
+
typer.echo(f" ⚠ {warning}")
|
|
318
|
+
schemas_branch = sources.get(source, {}).get("schemas", {})
|
|
319
|
+
for sname, sdata in schemas_branch.items():
|
|
320
|
+
tables_branch = sdata.get("tables") or {}
|
|
321
|
+
if not tables_branch:
|
|
322
|
+
continue
|
|
323
|
+
typer.echo(f" [{sname}]")
|
|
324
|
+
for tname, summary in tables_branch.items():
|
|
325
|
+
kind = summary.get("kind", "table")
|
|
326
|
+
rc = summary.get("row_count")
|
|
327
|
+
tag = f" rows={rc}" if rc is not None else ""
|
|
328
|
+
typer.echo(f" {tname} ({kind}){tag}")
|
|
329
|
+
return
|
|
330
|
+
|
|
331
|
+
if source is not None:
|
|
332
|
+
schemas_branch = sources.get(source, {}).get("schemas", {})
|
|
333
|
+
for sname, sdata in schemas_branch.items():
|
|
334
|
+
count = sdata.get("table_count")
|
|
335
|
+
tag = f" ({count} tables)" if count is not None else ""
|
|
336
|
+
typer.echo(f" {sname}{tag}")
|
|
337
|
+
return
|
|
338
|
+
|
|
339
|
+
if not sources:
|
|
340
|
+
typer.echo("No data sources configured.")
|
|
341
|
+
return
|
|
342
|
+
for sname, sdata in sources.items():
|
|
343
|
+
stype = sdata.get("type", "unknown")
|
|
344
|
+
path = sdata.get("path")
|
|
345
|
+
line = f" {sname} ({stype})"
|
|
346
|
+
if path:
|
|
347
|
+
line += f" {path}"
|
|
348
|
+
typer.echo(line)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _echo_table_profile(name: str, data: dict[str, Any]) -> None:
|
|
352
|
+
typer.echo(f"Table: {name}")
|
|
353
|
+
if (rc := data.get("row_count")) is not None:
|
|
354
|
+
typer.echo(f" rows: {rc}")
|
|
355
|
+
if (desc := data.get("description")) is not None:
|
|
356
|
+
typer.echo(f" description: {desc}")
|
|
357
|
+
upstream = data.get("upstream") or []
|
|
358
|
+
downstream = data.get("downstream") or []
|
|
359
|
+
if upstream:
|
|
360
|
+
parts = [
|
|
361
|
+
f"{r['table']} ({'source' if r.get('kind') == 'source' else 'ref'})"
|
|
362
|
+
for r in upstream
|
|
363
|
+
]
|
|
364
|
+
typer.echo(f" ↑ upstream: {', '.join(parts)}")
|
|
365
|
+
if downstream:
|
|
366
|
+
typer.echo(f" ↓ downstream: {', '.join(r['table'] for r in downstream)}")
|
|
367
|
+
partitions = data.get("partitions")
|
|
368
|
+
if (
|
|
369
|
+
partitions
|
|
370
|
+
and partitions.get("supported", False)
|
|
371
|
+
and partitions.get("type") not in {"none", "unpartitioned"}
|
|
372
|
+
):
|
|
373
|
+
col = partitions.get("column")
|
|
374
|
+
n = len(partitions.get("entries") or [])
|
|
375
|
+
col_str = f" on {col}" if col else ""
|
|
376
|
+
noun = "entry" if n == 1 else "entries"
|
|
377
|
+
typer.echo(f" partitions: {partitions['type']}{col_str} ({n} {noun})")
|
|
378
|
+
if (lm := data.get("last_modified")) is not None:
|
|
379
|
+
typer.echo(f" last_modified: {lm}")
|
|
380
|
+
for cname, cdata in (data.get("columns") or {}).items():
|
|
381
|
+
ctype = cdata.get("type") or cdata.get("actual_type") or ""
|
|
382
|
+
sem = cdata.get("semantic_type") or ""
|
|
383
|
+
suffix = f" {sem}" if sem else ""
|
|
384
|
+
rel_suffix = ""
|
|
385
|
+
for rel in cdata.get("relationships") or []:
|
|
386
|
+
rel_suffix += f" → {rel['to_table']}.{rel['to_column']}"
|
|
387
|
+
typer.echo(f" {cname} {ctype}{suffix}{rel_suffix}")
|
|
388
|
+
for ref in data.get("referenced_by") or []:
|
|
389
|
+
typer.echo(f" ← {ref['from_table']}.{ref['from_column']}")
|
|
390
|
+
for hop in data.get("linked_via") or []:
|
|
391
|
+
typer.echo(
|
|
392
|
+
f" via {hop['through_column_a']} → {hop['hop_table_a']},"
|
|
393
|
+
f" via {hop['through_column_b']} → {hop['hop_table_b']}"
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _echo_column(label: str, data: dict[str, Any]) -> None:
|
|
398
|
+
typer.echo(f"Column: {label}")
|
|
399
|
+
ctype = data.get("type") or data.get("actual_type")
|
|
400
|
+
if ctype:
|
|
401
|
+
typer.echo(f" type: {ctype}")
|
|
402
|
+
if (sem := data.get("semantic_type")) is not None:
|
|
403
|
+
typer.echo(f" semantic_type: {sem}")
|
|
404
|
+
if (dist := data.get("distribution")) is not None:
|
|
405
|
+
typer.echo(f" distribution: {dist}")
|
|
406
|
+
if (dc := data.get("distinct_count")) is not None:
|
|
407
|
+
typer.echo(f" distinct_count: {dc}")
|
|
408
|
+
if (np := data.get("null_percentage")) is not None:
|
|
409
|
+
typer.echo(f" null_pct: {np:.1f}%")
|
|
410
|
+
for rel in data.get("relationships") or []:
|
|
411
|
+
typer.echo(f" → {rel['to_table']}.{rel['to_column']}")
|