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,118 @@
|
|
|
1
|
+
"""File-ref expansion for Dataface agent surfaces.
|
|
2
|
+
|
|
3
|
+
Scans a prompt for @<path> tokens and inlines matching file contents.
|
|
4
|
+
Reusable from any surface: CLI, Cloud chat, MCP tools, IDE clients.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel
|
|
13
|
+
|
|
14
|
+
_AT_REF_RE = re.compile(r"(?<![\w.])@(?P<path>[\w./-]+)")
|
|
15
|
+
_MAX_INLINE_BYTES = 200 * 1024
|
|
16
|
+
_STUB_LINES = 50
|
|
17
|
+
_STUB_HEAD_BYTES = 64 * 1024
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FileRef(BaseModel):
|
|
21
|
+
token: str # original @token (e.g. "@faces/foo.yml")
|
|
22
|
+
path: Path # resolved absolute path
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ExpandedPrompt(BaseModel):
|
|
26
|
+
text: str
|
|
27
|
+
references: list[FileRef]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _has_dotdot(path_str: str) -> bool:
|
|
31
|
+
return any(seg == ".." for seg in path_str.split("/"))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _fenced(text: str) -> str:
|
|
35
|
+
longest = run = 0
|
|
36
|
+
for ch in text:
|
|
37
|
+
if ch == "`":
|
|
38
|
+
run += 1
|
|
39
|
+
longest = max(longest, run)
|
|
40
|
+
else:
|
|
41
|
+
run = 0
|
|
42
|
+
fence = "`" * max(3, longest + 1)
|
|
43
|
+
return f"{fence}\n{text}\n{fence}"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _inline(path_str: str, project_dir: Path) -> tuple[str, Path] | None:
|
|
47
|
+
"""Return (replacement_text, resolved_path) or None if token is unresolvable."""
|
|
48
|
+
if _has_dotdot(path_str):
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
resolved = (project_dir / path_str).resolve()
|
|
52
|
+
try:
|
|
53
|
+
resolved.relative_to(project_dir.resolve())
|
|
54
|
+
except ValueError:
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
if not resolved.exists() or not resolved.is_file():
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
size = resolved.stat().st_size
|
|
62
|
+
except OSError:
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
if size > _MAX_INLINE_BYTES:
|
|
66
|
+
try:
|
|
67
|
+
with resolved.open("rb") as f:
|
|
68
|
+
head_bytes = f.read(_STUB_HEAD_BYTES)
|
|
69
|
+
except OSError:
|
|
70
|
+
return None
|
|
71
|
+
try:
|
|
72
|
+
head_text = head_bytes.decode("utf-8")
|
|
73
|
+
except UnicodeDecodeError:
|
|
74
|
+
return None
|
|
75
|
+
head_lines = head_text.splitlines()
|
|
76
|
+
truncated = "\n".join(head_lines[:_STUB_LINES])
|
|
77
|
+
if size <= len(head_bytes):
|
|
78
|
+
line_count = len(head_lines)
|
|
79
|
+
else:
|
|
80
|
+
avg_line_bytes = max(1, len(head_bytes) // max(1, len(head_lines)))
|
|
81
|
+
line_count = size // avg_line_bytes
|
|
82
|
+
header = (
|
|
83
|
+
f"(@{path_str}: {size // 1024} KB, ~{line_count} lines,"
|
|
84
|
+
f" first {_STUB_LINES} lines below)"
|
|
85
|
+
)
|
|
86
|
+
return f"{header}\n{_fenced(truncated)}", resolved
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
text = resolved.read_text(encoding="utf-8")
|
|
90
|
+
except (OSError, UnicodeDecodeError):
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
return _fenced(text), resolved
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def expand_file_refs(message: str, *, project_dir: Path) -> ExpandedPrompt:
|
|
97
|
+
"""Replace @<path> tokens in *message* with file contents.
|
|
98
|
+
|
|
99
|
+
Tokens that cannot be resolved (out of project_dir, non-existent, binary,
|
|
100
|
+
dotdot traversal) are left as-is and not recorded in references.
|
|
101
|
+
"""
|
|
102
|
+
references: list[FileRef] = []
|
|
103
|
+
|
|
104
|
+
def replace(match: re.Match[str]) -> str:
|
|
105
|
+
full_match = match.group(0)
|
|
106
|
+
path_str = match.group("path").rstrip(".")
|
|
107
|
+
if not path_str:
|
|
108
|
+
return full_match
|
|
109
|
+
suffix = full_match[1 + len(path_str) :]
|
|
110
|
+
result = _inline(path_str, project_dir)
|
|
111
|
+
if result is None:
|
|
112
|
+
return full_match
|
|
113
|
+
replacement, resolved_path = result
|
|
114
|
+
references.append(FileRef(token=f"@{path_str}", path=resolved_path))
|
|
115
|
+
return replacement + suffix
|
|
116
|
+
|
|
117
|
+
text = _AT_REF_RE.sub(replace, message)
|
|
118
|
+
return ExpandedPrompt(text=text, references=references)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Bootstrap (or refresh) a Dataface project layout."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.resources
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from dataface.agent_api._project_agents_md import (
|
|
11
|
+
load_agents_snippet,
|
|
12
|
+
merge_agents_snippet,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
GITIGNORE_ENTRIES = ("renders/", ".venv/", "__pycache__/", "*.duckdb")
|
|
16
|
+
|
|
17
|
+
_TEMPLATES = importlib.resources.files("dataface.agent_api._init_templates")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class InitResult(BaseModel):
|
|
21
|
+
project_dir: Path
|
|
22
|
+
dbt_detected: bool
|
|
23
|
+
created_files: list[Path] = []
|
|
24
|
+
skipped_files: list[Path] = []
|
|
25
|
+
refreshed_files: list[Path] = []
|
|
26
|
+
appended_files: list[Path] = []
|
|
27
|
+
hints: list[str] = []
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def init_project(
|
|
31
|
+
project_dir: Path | None = None,
|
|
32
|
+
*,
|
|
33
|
+
force: bool = False,
|
|
34
|
+
write_agents_md: bool = True,
|
|
35
|
+
write_claude_md: bool = True,
|
|
36
|
+
eject_inspect: bool = False,
|
|
37
|
+
) -> InitResult:
|
|
38
|
+
"""Bootstrap (or refresh) a Dataface project layout.
|
|
39
|
+
|
|
40
|
+
Safe to re-run: scaffold files are skipped unless *force* is set.
|
|
41
|
+
AGENTS.md: appends a short Dataface blurb when the file already exists;
|
|
42
|
+
creates the blurb alone when missing. Re-run refreshes only the marked
|
|
43
|
+
``<!-- dft-dataface:* -->`` section. CLAUDE.md is create-only.
|
|
44
|
+
"""
|
|
45
|
+
root = (project_dir or Path(".")).resolve()
|
|
46
|
+
dbt_detected = (root / "dbt_project.yml").exists()
|
|
47
|
+
|
|
48
|
+
result = InitResult(project_dir=root, dbt_detected=dbt_detected)
|
|
49
|
+
|
|
50
|
+
(root / "faces").mkdir(exist_ok=True)
|
|
51
|
+
(root / "faces" / "partials").mkdir(exist_ok=True)
|
|
52
|
+
|
|
53
|
+
scaffolds: list[tuple[str, str]] = [
|
|
54
|
+
("dataface.yml", _TEMPLATES.joinpath("dataface.yml").read_text()),
|
|
55
|
+
("faces/index.md", _TEMPLATES.joinpath("index.md").read_text()),
|
|
56
|
+
("faces/dataface.yml", _TEMPLATES.joinpath("faces-dataface.yml").read_text()),
|
|
57
|
+
("faces/partials/.gitkeep", ""),
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
for rel, content in scaffolds:
|
|
61
|
+
target = root / rel
|
|
62
|
+
if target.exists() and not force:
|
|
63
|
+
result.skipped_files.append(Path(rel))
|
|
64
|
+
elif target.exists():
|
|
65
|
+
target.write_text(content)
|
|
66
|
+
result.refreshed_files.append(Path(rel))
|
|
67
|
+
else:
|
|
68
|
+
target.write_text(content)
|
|
69
|
+
result.created_files.append(Path(rel))
|
|
70
|
+
|
|
71
|
+
_ensure_gitignore_entries(root, result)
|
|
72
|
+
|
|
73
|
+
if write_agents_md:
|
|
74
|
+
snippet = load_agents_snippet()
|
|
75
|
+
agents_md_path = root / "AGENTS.md"
|
|
76
|
+
existing: str | None = (
|
|
77
|
+
agents_md_path.read_text() if agents_md_path.exists() else None
|
|
78
|
+
)
|
|
79
|
+
final_text, action = merge_agents_snippet(existing, snippet)
|
|
80
|
+
agents_md_path.write_text(final_text)
|
|
81
|
+
if action == "created":
|
|
82
|
+
result.created_files.append(Path("AGENTS.md"))
|
|
83
|
+
elif action == "refreshed":
|
|
84
|
+
result.refreshed_files.append(Path("AGENTS.md"))
|
|
85
|
+
else:
|
|
86
|
+
result.appended_files.append(Path("AGENTS.md"))
|
|
87
|
+
|
|
88
|
+
if write_claude_md:
|
|
89
|
+
claude_md_path = root / "CLAUDE.md"
|
|
90
|
+
if not claude_md_path.exists():
|
|
91
|
+
claude_md_path.write_text("@AGENTS.md\n")
|
|
92
|
+
result.created_files.append(Path("CLAUDE.md"))
|
|
93
|
+
|
|
94
|
+
if eject_inspect:
|
|
95
|
+
from dataface.agent_api import inspect as _api_inspect
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
ejected = _api_inspect.eject_templates(
|
|
99
|
+
root / "faces" / "inspect", templates=None, force=False
|
|
100
|
+
)
|
|
101
|
+
result.created_files.extend(p.relative_to(root) for p in ejected)
|
|
102
|
+
except (FileNotFoundError, ModuleNotFoundError) as exc:
|
|
103
|
+
result.hints.append(
|
|
104
|
+
f"warning: inspect templates could not be ejected ({exc!r}); "
|
|
105
|
+
"your install may be partial. Run 'pip show dataface' to verify."
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return result
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _ensure_gitignore_entries(root: Path, result: InitResult) -> None:
|
|
112
|
+
gitignore = root / ".gitignore"
|
|
113
|
+
if not gitignore.exists():
|
|
114
|
+
gitignore.write_text("\n".join(GITIGNORE_ENTRIES) + "\n")
|
|
115
|
+
result.created_files.append(Path(".gitignore"))
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
text = gitignore.read_text()
|
|
119
|
+
existing = set(text.splitlines())
|
|
120
|
+
missing = [e for e in GITIGNORE_ENTRIES if e not in existing]
|
|
121
|
+
if not missing:
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
separator = "" if not text or text.endswith("\n") else "\n"
|
|
125
|
+
gitignore.write_text(f"{text}{separator}" + "\n".join(missing) + "\n")
|
|
126
|
+
result.refreshed_files.append(Path(".gitignore"))
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Inspect template-management verbs (OSS).
|
|
2
|
+
|
|
3
|
+
Profiler verbs (inspect_table, inspect_all, audit_tables, bake_schema_metadata)
|
|
4
|
+
live in the private ``dataface-super-schema`` package and are exposed via the
|
|
5
|
+
CLI entry-point plugin. This module ships the template-management verbs that
|
|
6
|
+
belong in the OSS wheel: list, eject, validate.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from hashlib import sha256
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from pydantic import BaseModel
|
|
16
|
+
|
|
17
|
+
from dataface import __version__
|
|
18
|
+
from dataface.core.inspect import INSPECT_TEMPLATES
|
|
19
|
+
from dataface.core.inspect.manifest_utils import (
|
|
20
|
+
INSPECT_TEMPLATE_MANIFEST,
|
|
21
|
+
MANIFEST_SCHEMA_VERSION,
|
|
22
|
+
compare_templates,
|
|
23
|
+
load_manifest,
|
|
24
|
+
save_manifest,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class InspectTemplate(BaseModel):
|
|
29
|
+
name: str
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ValidateTemplatesResult(BaseModel):
|
|
33
|
+
success: bool
|
|
34
|
+
missing: list[str] = []
|
|
35
|
+
upstream_changed: list[str] = []
|
|
36
|
+
custom_safe: list[str] = []
|
|
37
|
+
unchanged: list[str] = []
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def list_templates() -> list[InspectTemplate]:
|
|
41
|
+
"""List all built-in inspect templates."""
|
|
42
|
+
return [InspectTemplate(name=name) for name in INSPECT_TEMPLATES]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def eject_templates(
|
|
46
|
+
target_dir: Path,
|
|
47
|
+
*,
|
|
48
|
+
templates: list[str] | None = None,
|
|
49
|
+
force: bool = False,
|
|
50
|
+
) -> list[Path]:
|
|
51
|
+
"""Write built-in inspect templates to target_dir.
|
|
52
|
+
|
|
53
|
+
Returns paths of files actually written (skipped files are omitted).
|
|
54
|
+
Raises ValueError for unknown template names.
|
|
55
|
+
"""
|
|
56
|
+
from importlib.resources import files as _pkg_files
|
|
57
|
+
|
|
58
|
+
to_eject = templates if templates is not None else list(INSPECT_TEMPLATES)
|
|
59
|
+
|
|
60
|
+
invalid = [t for t in to_eject if t not in INSPECT_TEMPLATES]
|
|
61
|
+
if invalid:
|
|
62
|
+
raise ValueError(f"Unknown templates: {', '.join(invalid)}")
|
|
63
|
+
|
|
64
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
65
|
+
templates_pkg = _pkg_files("dataface.core.inspect.templates")
|
|
66
|
+
manifest = load_manifest(target_dir, dataface_version=__version__)
|
|
67
|
+
manifest["schema_version"] = MANIFEST_SCHEMA_VERSION
|
|
68
|
+
manifest["dataface_version"] = __version__
|
|
69
|
+
manifest["generated_at"] = datetime.now(timezone.utc).isoformat()
|
|
70
|
+
manifest_templates = manifest.setdefault("templates", {})
|
|
71
|
+
|
|
72
|
+
ejected: list[Path] = []
|
|
73
|
+
|
|
74
|
+
for name in to_eject:
|
|
75
|
+
source_file = templates_pkg.joinpath(f"{name}.yml")
|
|
76
|
+
target_file = target_dir / f"{name}.yml"
|
|
77
|
+
|
|
78
|
+
if target_file.exists() and not force:
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
content = source_file.read_text()
|
|
82
|
+
source_hash = sha256(content.encode("utf-8")).hexdigest()
|
|
83
|
+
target_file.write_text(content)
|
|
84
|
+
|
|
85
|
+
manifest_templates[name] = {
|
|
86
|
+
"filename": f"{name}.yml",
|
|
87
|
+
"ejected_at": datetime.now(timezone.utc).isoformat(),
|
|
88
|
+
"source_version": __version__,
|
|
89
|
+
"source_sha256": source_hash,
|
|
90
|
+
}
|
|
91
|
+
ejected.append(target_file)
|
|
92
|
+
|
|
93
|
+
save_manifest(target_dir, manifest)
|
|
94
|
+
return ejected
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def validate_ejected_templates(
|
|
98
|
+
target_dir: Path | None = None,
|
|
99
|
+
) -> ValidateTemplatesResult:
|
|
100
|
+
"""Compare ejected templates against built-in versions using the manifest.
|
|
101
|
+
|
|
102
|
+
Returns a ValidateTemplatesResult. Call .success to check overall status.
|
|
103
|
+
Raises FileNotFoundError if the manifest is missing.
|
|
104
|
+
"""
|
|
105
|
+
from importlib.resources import files as _pkg_files
|
|
106
|
+
|
|
107
|
+
resolved = target_dir or (Path.cwd() / "faces" / "inspect")
|
|
108
|
+
manifest_path = resolved / INSPECT_TEMPLATE_MANIFEST
|
|
109
|
+
if not manifest_path.exists():
|
|
110
|
+
raise FileNotFoundError(
|
|
111
|
+
f"Missing manifest at {manifest_path}. Run eject_templates() first."
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
manifest = load_manifest(resolved, dataface_version=__version__)
|
|
115
|
+
manifest_templates: dict[str, dict] = manifest.get("templates", {})
|
|
116
|
+
templates_pkg = _pkg_files("dataface.core.inspect.templates")
|
|
117
|
+
|
|
118
|
+
missing, upstream_changed, custom_safe, unchanged = compare_templates(
|
|
119
|
+
resolved, manifest_templates, templates_pkg
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
return ValidateTemplatesResult(
|
|
123
|
+
success=not missing and not upstream_changed,
|
|
124
|
+
missing=missing,
|
|
125
|
+
upstream_changed=upstream_changed,
|
|
126
|
+
custom_safe=custom_safe,
|
|
127
|
+
unchanged=unchanged,
|
|
128
|
+
)
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Typed MCP client install verbs — per-client config writers for agent surfaces."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Literal
|
|
9
|
+
|
|
10
|
+
import tomli_w
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
|
|
13
|
+
if sys.version_info >= (3, 11):
|
|
14
|
+
import tomllib
|
|
15
|
+
else: # pragma: no cover - exercised on 3.10 CI
|
|
16
|
+
import tomli as tomllib
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Client name → (config file path, servers key, detection paths, format).
|
|
20
|
+
# Kept as a plain dict so tests can patch individual entries (e.g. redirect
|
|
21
|
+
# the Claude Desktop path into a tmp directory).
|
|
22
|
+
MCP_CLIENTS: dict[str, tuple[Path, str, tuple[Path, ...], str]] = {
|
|
23
|
+
"cursor": (Path(".cursor/mcp.json"), "mcpServers", (Path(".cursor"),), "json"),
|
|
24
|
+
"codex": (
|
|
25
|
+
Path(".codex/config.toml"),
|
|
26
|
+
"mcp_servers",
|
|
27
|
+
(Path(".codex"), Path("AGENTS.md")),
|
|
28
|
+
"toml",
|
|
29
|
+
),
|
|
30
|
+
"claude": (
|
|
31
|
+
Path.home() / ".config" / "claude" / "config.json",
|
|
32
|
+
"mcpServers",
|
|
33
|
+
(Path.home() / ".config" / "claude",),
|
|
34
|
+
"json",
|
|
35
|
+
),
|
|
36
|
+
"vscode": (Path(".vscode/mcp.json"), "servers", (Path(".vscode"),), "json"),
|
|
37
|
+
"claude-code": (Path(".mcp.json"), "mcpServers", (Path("CLAUDE.md"),), "json"),
|
|
38
|
+
"copilot": (
|
|
39
|
+
Path(".github/copilot/mcp.json"),
|
|
40
|
+
"servers",
|
|
41
|
+
(Path(".github"),),
|
|
42
|
+
"json",
|
|
43
|
+
),
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class McpClient(BaseModel):
|
|
48
|
+
name: str
|
|
49
|
+
config_path: Path
|
|
50
|
+
servers_key: str
|
|
51
|
+
detect_paths: tuple[Path, ...]
|
|
52
|
+
config_format: Literal["json", "toml"]
|
|
53
|
+
current_config: dict[str, Any] | None = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class InstallResult(BaseModel):
|
|
57
|
+
client_name: str
|
|
58
|
+
config_path: Path
|
|
59
|
+
already_configured: bool
|
|
60
|
+
updated: bool
|
|
61
|
+
message: str
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def list_clients() -> list[McpClient]:
|
|
65
|
+
"""Return all supported MCP clients from the current MCP_CLIENTS registry."""
|
|
66
|
+
return [
|
|
67
|
+
McpClient(
|
|
68
|
+
name=name,
|
|
69
|
+
config_path=cfg,
|
|
70
|
+
servers_key=sk,
|
|
71
|
+
detect_paths=dp,
|
|
72
|
+
config_format=fmt, # type: ignore[arg-type]
|
|
73
|
+
)
|
|
74
|
+
for name, (cfg, sk, dp, fmt) in MCP_CLIENTS.items()
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def install_for_client(
|
|
79
|
+
client: McpClient,
|
|
80
|
+
*,
|
|
81
|
+
server_command: list[str],
|
|
82
|
+
config_root: Path,
|
|
83
|
+
force: bool = False,
|
|
84
|
+
) -> InstallResult:
|
|
85
|
+
"""Write the MCP server entry for one client config file.
|
|
86
|
+
|
|
87
|
+
server_command: full argv for the server, e.g. ["dft", "mcp", "serve"].
|
|
88
|
+
config_root: workspace root for resolving relative config paths.
|
|
89
|
+
"""
|
|
90
|
+
server_entry: dict[str, Any] = {
|
|
91
|
+
"command": server_command[0],
|
|
92
|
+
"args": server_command[1:],
|
|
93
|
+
}
|
|
94
|
+
abs_path = (
|
|
95
|
+
client.config_path
|
|
96
|
+
if client.config_path.is_absolute()
|
|
97
|
+
else config_root / client.config_path
|
|
98
|
+
)
|
|
99
|
+
if client.config_format == "toml":
|
|
100
|
+
msg = _upsert_toml_mcp_config(abs_path, client.servers_key, server_entry, force)
|
|
101
|
+
else:
|
|
102
|
+
msg = _upsert_mcp_config(abs_path, client.servers_key, server_entry, force)
|
|
103
|
+
|
|
104
|
+
already = msg is None
|
|
105
|
+
return InstallResult(
|
|
106
|
+
client_name=client.name,
|
|
107
|
+
config_path=abs_path,
|
|
108
|
+
already_configured=already,
|
|
109
|
+
updated=not already and "Updated" in (msg or ""),
|
|
110
|
+
message=msg or f" {abs_path} already has dataface (use -f to update)",
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _upsert_mcp_config(
|
|
115
|
+
config_path: Path,
|
|
116
|
+
servers_key: str,
|
|
117
|
+
server_entry: dict[str, Any],
|
|
118
|
+
force: bool,
|
|
119
|
+
) -> str | None:
|
|
120
|
+
"""Add dataface to a JSON MCP config file, preserving existing content.
|
|
121
|
+
|
|
122
|
+
Returns a status message, or None if skipped (already configured).
|
|
123
|
+
"""
|
|
124
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
125
|
+
|
|
126
|
+
existing: dict[str, Any] = {}
|
|
127
|
+
if config_path.exists():
|
|
128
|
+
try:
|
|
129
|
+
existing = json.loads(config_path.read_text())
|
|
130
|
+
if not isinstance(existing, dict):
|
|
131
|
+
existing = {}
|
|
132
|
+
except (json.JSONDecodeError, OSError):
|
|
133
|
+
existing = {}
|
|
134
|
+
|
|
135
|
+
already_has = servers_key in existing and "dataface" in existing.get(
|
|
136
|
+
servers_key, {}
|
|
137
|
+
)
|
|
138
|
+
if already_has and not force:
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
existing.setdefault(servers_key, {})
|
|
142
|
+
existing[servers_key]["dataface"] = server_entry
|
|
143
|
+
config_path.write_text(json.dumps(existing, indent=2) + "\n")
|
|
144
|
+
return f" {'Updated' if already_has else 'Added dataface to'} {config_path}"
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _upsert_toml_mcp_config(
|
|
148
|
+
config_path: Path,
|
|
149
|
+
servers_key: str,
|
|
150
|
+
server_entry: dict[str, Any],
|
|
151
|
+
force: bool,
|
|
152
|
+
) -> str | None:
|
|
153
|
+
"""Add dataface to a TOML MCP config file, preserving existing content.
|
|
154
|
+
|
|
155
|
+
Returns a status message, or None if skipped (already configured).
|
|
156
|
+
"""
|
|
157
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
158
|
+
|
|
159
|
+
existing: dict[str, Any] = {}
|
|
160
|
+
if config_path.exists():
|
|
161
|
+
existing = tomllib.loads(config_path.read_text())
|
|
162
|
+
|
|
163
|
+
already_has = "dataface" in existing.get(servers_key, {})
|
|
164
|
+
if already_has and not force:
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
existing.setdefault(servers_key, {})
|
|
168
|
+
existing[servers_key]["dataface"] = server_entry
|
|
169
|
+
config_path.write_text(tomli_w.dumps(existing))
|
|
170
|
+
return f" {'Updated' if already_has else 'Added dataface to'} {config_path}"
|