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,511 @@
|
|
|
1
|
+
"""Jinja template resolution module.
|
|
2
|
+
|
|
3
|
+
Stage: COMPILE (Part of Step 3: Normalization)
|
|
4
|
+
Purpose: Resolve Jinja templates in queries and SQL strings.
|
|
5
|
+
|
|
6
|
+
Entry Points:
|
|
7
|
+
- resolve_jinja_template(template: str, variables: Dict) -> str
|
|
8
|
+
- resolve_query_filters(filters: Dict, variables: Dict) -> Dict
|
|
9
|
+
- extract_variable_dependencies(template: str) -> Set[str]
|
|
10
|
+
|
|
11
|
+
Inputs:
|
|
12
|
+
- Template strings with {{ variable }} syntax
|
|
13
|
+
- Variable values for substitution
|
|
14
|
+
|
|
15
|
+
Outputs:
|
|
16
|
+
- Resolved strings with variables substituted
|
|
17
|
+
- Set of variable names referenced in templates
|
|
18
|
+
|
|
19
|
+
Dependencies:
|
|
20
|
+
- jinja2 (Environment, Template, meta)
|
|
21
|
+
|
|
22
|
+
Errors:
|
|
23
|
+
- JinjaError: Template syntax or resolution errors
|
|
24
|
+
|
|
25
|
+
See also:
|
|
26
|
+
- compile/normalizer.py: Uses this module
|
|
27
|
+
|
|
28
|
+
Security Note:
|
|
29
|
+
When using strict=False (lenient mode), undefined variables are silently
|
|
30
|
+
converted to empty strings. This mode should ONLY be used in interactive
|
|
31
|
+
contexts (chart editor) where the user is actively editing and expects
|
|
32
|
+
partial results. Production rendering should always use strict=True.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
import logging
|
|
36
|
+
import re
|
|
37
|
+
from typing import Any
|
|
38
|
+
|
|
39
|
+
from jinja2 import (
|
|
40
|
+
Environment,
|
|
41
|
+
StrictUndefined,
|
|
42
|
+
TemplateError,
|
|
43
|
+
TemplateSyntaxError,
|
|
44
|
+
Undefined,
|
|
45
|
+
UndefinedError,
|
|
46
|
+
meta,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
from dataface.core.compile._jinja_helpers import _LenientUndefined, _QueryNamespace
|
|
50
|
+
from dataface.core.compile.errors import JinjaError
|
|
51
|
+
|
|
52
|
+
logger = logging.getLogger(__name__)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# Jinja environment with strict undefined (errors on missing variables)
|
|
56
|
+
_jinja_env = Environment(undefined=StrictUndefined)
|
|
57
|
+
|
|
58
|
+
# Lenient Jinja environment (undefined variables become empty/None)
|
|
59
|
+
_jinja_env_lenient = Environment(undefined=_LenientUndefined)
|
|
60
|
+
|
|
61
|
+
# Known context items that are NOT user variables (helper functions, namespaces)
|
|
62
|
+
_KNOWN_CONTEXT = {"filter", "filter_date_range", "queries"}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def extract_variable_dependencies(template_str: str) -> set[str]:
|
|
66
|
+
"""Extract variable names referenced in a Jinja template.
|
|
67
|
+
|
|
68
|
+
Uses Jinja's AST parser to find all undeclared variables in the template.
|
|
69
|
+
Filters out known helper functions and namespaces.
|
|
70
|
+
|
|
71
|
+
This is used during normalization to track which variables a query,
|
|
72
|
+
chart title, or other template depends on.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
template_str: String potentially containing Jinja expressions
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Set of variable names referenced in the template
|
|
79
|
+
|
|
80
|
+
Example:
|
|
81
|
+
>>> extract_variable_dependencies(
|
|
82
|
+
... "SELECT * FROM orders WHERE region = '{{ region }}'"
|
|
83
|
+
... )
|
|
84
|
+
{'region'}
|
|
85
|
+
|
|
86
|
+
>>> extract_variable_dependencies(
|
|
87
|
+
... "SELECT * WHERE {{ filter('region', region) }}"
|
|
88
|
+
... )
|
|
89
|
+
{'region'}
|
|
90
|
+
"""
|
|
91
|
+
if not template_str or not isinstance(template_str, str):
|
|
92
|
+
return set()
|
|
93
|
+
|
|
94
|
+
# Quick check - no Jinja syntax
|
|
95
|
+
if "{{" not in template_str and "{%" not in template_str:
|
|
96
|
+
return set()
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
env = Environment()
|
|
100
|
+
ast = env.parse(template_str)
|
|
101
|
+
undeclared = meta.find_undeclared_variables(ast)
|
|
102
|
+
# Filter out known context items (helpers, not user variables)
|
|
103
|
+
return undeclared - _KNOWN_CONTEXT
|
|
104
|
+
except TemplateSyntaxError:
|
|
105
|
+
# Jinja syntax errors are expected here for invalid templates.
|
|
106
|
+
# Return empty set - the actual render will catch and report the syntax error
|
|
107
|
+
# with proper context. This is intentionally silent because this function
|
|
108
|
+
# is only used for dependency detection, not validation.
|
|
109
|
+
return set()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def resolve_jinja_template(
|
|
113
|
+
template: str,
|
|
114
|
+
variables: dict[str, Any] | None = None,
|
|
115
|
+
queries: dict[str, Any] | None = None,
|
|
116
|
+
strict: bool = True,
|
|
117
|
+
) -> str:
|
|
118
|
+
"""Resolve a Jinja template string.
|
|
119
|
+
|
|
120
|
+
Stage: COMPILE (Step 3: Normalization - Jinja Resolution)
|
|
121
|
+
|
|
122
|
+
Resolves {{ variable }} expressions in template strings.
|
|
123
|
+
Also handles {{ queries.query_name }} references for SQL composition.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
template: String potentially containing Jinja expressions
|
|
127
|
+
variables: Variable values for substitution
|
|
128
|
+
queries: Query registry for {{ queries.* }} resolution
|
|
129
|
+
strict: If True (default), raises error on undefined variables.
|
|
130
|
+
If False, undefined variables become None/empty (useful for
|
|
131
|
+
chart editor where variables may not all be set).
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Resolved string with variables substituted
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
JinjaError: If template syntax is invalid or (if strict) variable not found
|
|
138
|
+
|
|
139
|
+
Example:
|
|
140
|
+
>>> resolve_jinja_template(
|
|
141
|
+
... "SELECT * FROM users WHERE status = '{{ status }}'",
|
|
142
|
+
... variables={"status": "active"}
|
|
143
|
+
... )
|
|
144
|
+
"SELECT * FROM users WHERE status = 'active'"
|
|
145
|
+
"""
|
|
146
|
+
if not template or not isinstance(template, str):
|
|
147
|
+
return template
|
|
148
|
+
|
|
149
|
+
# Quick check for Jinja syntax
|
|
150
|
+
if "{{" not in template and "{%" not in template:
|
|
151
|
+
return template
|
|
152
|
+
|
|
153
|
+
variables = variables or {}
|
|
154
|
+
queries = queries or {}
|
|
155
|
+
|
|
156
|
+
# Build context with variables and query helper
|
|
157
|
+
context = {**variables}
|
|
158
|
+
|
|
159
|
+
# Add queries namespace for {{ queries.query_name }} resolution
|
|
160
|
+
if queries:
|
|
161
|
+
context["queries"] = _QueryNamespace(queries)
|
|
162
|
+
|
|
163
|
+
# Add helper functions
|
|
164
|
+
context["filter"] = _filter_helper
|
|
165
|
+
context["filter_date_range"] = _filter_date_range_helper
|
|
166
|
+
|
|
167
|
+
# Use strict or lenient environment
|
|
168
|
+
jinja_env = _jinja_env if strict else _jinja_env_lenient
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
jinja_template = jinja_env.from_string(template)
|
|
172
|
+
return jinja_template.render(context)
|
|
173
|
+
except UndefinedError as e:
|
|
174
|
+
raise JinjaError(f"Undefined variable: {e}", template) from e
|
|
175
|
+
except TemplateSyntaxError as e:
|
|
176
|
+
raise JinjaError(f"Template syntax error: {e}", template) from e
|
|
177
|
+
except TemplateError as e:
|
|
178
|
+
raise JinjaError(f"Template error: {e}", template) from e
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def resolve_query_filters(
|
|
182
|
+
filters: dict[str, Any],
|
|
183
|
+
variables: dict[str, Any] | None = None,
|
|
184
|
+
) -> dict[str, Any]:
|
|
185
|
+
"""Resolve Jinja expressions in filter values.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
filters: Dictionary of filter conditions
|
|
189
|
+
variables: Variable values for substitution
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
Filters with resolved values
|
|
193
|
+
|
|
194
|
+
Example:
|
|
195
|
+
>>> resolve_query_filters(
|
|
196
|
+
... {"status": "{{ selected_status }}"},
|
|
197
|
+
... variables={"selected_status": "active"}
|
|
198
|
+
... )
|
|
199
|
+
{"status": "active"}
|
|
200
|
+
"""
|
|
201
|
+
if not filters:
|
|
202
|
+
return filters
|
|
203
|
+
|
|
204
|
+
resolved: dict[str, Any] = {}
|
|
205
|
+
for key, value in filters.items():
|
|
206
|
+
if isinstance(value, str):
|
|
207
|
+
resolved[key] = resolve_jinja_template(value, variables)
|
|
208
|
+
elif isinstance(value, dict):
|
|
209
|
+
resolved[key] = resolve_query_filters(value, variables)
|
|
210
|
+
elif isinstance(value, list):
|
|
211
|
+
resolved[key] = [
|
|
212
|
+
resolve_jinja_template(v, variables) if isinstance(v, str) else v
|
|
213
|
+
for v in value
|
|
214
|
+
]
|
|
215
|
+
else:
|
|
216
|
+
resolved[key] = value
|
|
217
|
+
|
|
218
|
+
return resolved
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def detect_query_dependencies(queries: dict[str, Any]) -> dict[str, list[str]]:
|
|
222
|
+
"""Detect dependencies between queries for circular reference detection.
|
|
223
|
+
|
|
224
|
+
Scans query SQL for {{ queries.* }} references and builds a dependency graph.
|
|
225
|
+
Raises an error if circular dependencies are detected.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
queries: Dictionary of query definitions
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
Dictionary mapping query name to list of dependencies
|
|
232
|
+
|
|
233
|
+
Raises:
|
|
234
|
+
JinjaError: If circular dependencies detected
|
|
235
|
+
"""
|
|
236
|
+
dependencies: dict[str, list[str]] = {}
|
|
237
|
+
|
|
238
|
+
for name, query in queries.items():
|
|
239
|
+
deps: list[str] = []
|
|
240
|
+
|
|
241
|
+
# Get SQL content
|
|
242
|
+
sql = None
|
|
243
|
+
if hasattr(query, "sql"):
|
|
244
|
+
sql = query.sql
|
|
245
|
+
elif isinstance(query, dict):
|
|
246
|
+
sql = query.get("sql")
|
|
247
|
+
|
|
248
|
+
if sql:
|
|
249
|
+
# Find {{ queries.* }} references
|
|
250
|
+
pattern = r"\{\{\s*queries\.(\w+)\s*\}\}"
|
|
251
|
+
matches = re.findall(pattern, sql)
|
|
252
|
+
deps.extend(matches)
|
|
253
|
+
|
|
254
|
+
dependencies[name] = deps
|
|
255
|
+
|
|
256
|
+
# Check for circular dependencies
|
|
257
|
+
_detect_circular(dependencies)
|
|
258
|
+
|
|
259
|
+
return dependencies
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _detect_circular(dependencies: dict[str, list[str]]) -> None:
|
|
263
|
+
"""Detect circular dependencies using DFS.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
dependencies: Dependency graph
|
|
267
|
+
|
|
268
|
+
Raises:
|
|
269
|
+
JinjaError: If circular dependency found
|
|
270
|
+
"""
|
|
271
|
+
visited: set = set()
|
|
272
|
+
rec_stack: set = set()
|
|
273
|
+
|
|
274
|
+
def dfs(node: str, path: list[str]) -> None:
|
|
275
|
+
if node in rec_stack:
|
|
276
|
+
cycle = path[path.index(node) :] + [node]
|
|
277
|
+
raise JinjaError(
|
|
278
|
+
f"Circular query dependency detected: {' -> '.join(cycle)}"
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
if node in visited:
|
|
282
|
+
return
|
|
283
|
+
|
|
284
|
+
visited.add(node)
|
|
285
|
+
rec_stack.add(node)
|
|
286
|
+
|
|
287
|
+
for dep in dependencies.get(node, []):
|
|
288
|
+
if dep in dependencies: # Only follow known queries
|
|
289
|
+
dfs(dep, path + [node])
|
|
290
|
+
|
|
291
|
+
rec_stack.remove(node)
|
|
292
|
+
|
|
293
|
+
for query_name in dependencies:
|
|
294
|
+
dfs(query_name, [])
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _topological_sort(dependencies: dict[str, list[str]]) -> list[str]:
|
|
298
|
+
"""Return query names in topological order (dependencies first).
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
dependencies: Dependency graph from detect_query_dependencies()
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
List of query names with leaf nodes first
|
|
305
|
+
"""
|
|
306
|
+
visited: set[str] = set()
|
|
307
|
+
order: list[str] = []
|
|
308
|
+
|
|
309
|
+
def visit(name: str) -> None:
|
|
310
|
+
if name in visited:
|
|
311
|
+
return
|
|
312
|
+
visited.add(name)
|
|
313
|
+
for dep in dependencies.get(name, []):
|
|
314
|
+
if dep in dependencies:
|
|
315
|
+
visit(dep)
|
|
316
|
+
order.append(name)
|
|
317
|
+
|
|
318
|
+
for name in dependencies:
|
|
319
|
+
visit(name)
|
|
320
|
+
|
|
321
|
+
return order
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _get_query_sql(query: Any) -> str:
|
|
325
|
+
"""Extract the SQL string a _QueryNamespace would return for a query."""
|
|
326
|
+
if hasattr(query, "sql"):
|
|
327
|
+
sql = query.sql
|
|
328
|
+
return sql if isinstance(sql, str) else ""
|
|
329
|
+
elif isinstance(query, dict):
|
|
330
|
+
sql = query.get("sql", "")
|
|
331
|
+
return sql if isinstance(sql, str) else ""
|
|
332
|
+
return str(query)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _substitute_query_refs(sql: str, resolved: dict[str, str]) -> str:
|
|
336
|
+
"""Replace {{ queries.X }} tokens with already-resolved SQL.
|
|
337
|
+
|
|
338
|
+
Uses regex substitution so that only query references are expanded;
|
|
339
|
+
variable expressions ({{ var }}) and other Jinja constructs are
|
|
340
|
+
left untouched for a later resolve_jinja_template pass.
|
|
341
|
+
"""
|
|
342
|
+
_QUERY_REF = re.compile(r"\{\{\s*queries\.(\w+)\s*\}\}")
|
|
343
|
+
|
|
344
|
+
def _replacer(m: re.Match) -> str:
|
|
345
|
+
name = m.group(1)
|
|
346
|
+
if name in resolved:
|
|
347
|
+
return resolved[name]
|
|
348
|
+
return m.group(0) # leave unresolved if not in map
|
|
349
|
+
|
|
350
|
+
return _QUERY_REF.sub(_replacer, sql)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def resolve_query_sql_with_dependencies(
|
|
354
|
+
query_name: str,
|
|
355
|
+
queries: dict[str, Any],
|
|
356
|
+
variables: dict[str, Any] | None = None,
|
|
357
|
+
) -> str:
|
|
358
|
+
"""Resolve a query's SQL, fully expanding nested {{ queries.* }} refs.
|
|
359
|
+
|
|
360
|
+
Two-phase approach:
|
|
361
|
+
Phase 1 — Expand {{ queries.* }} in topological order using regex so
|
|
362
|
+
each query sees fully expanded dependency SQL. Variable
|
|
363
|
+
expressions are left untouched.
|
|
364
|
+
Phase 2 — Run the target query's expanded SQL through Jinja to resolve
|
|
365
|
+
variables, filters, and other template constructs.
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
query_name: The target query to resolve
|
|
369
|
+
queries: Full query registry (name -> query object or dict)
|
|
370
|
+
variables: Variable values for Jinja substitution
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
Fully resolved SQL string for the target query
|
|
374
|
+
|
|
375
|
+
Raises:
|
|
376
|
+
JinjaError: If circular dependencies detected or resolution fails
|
|
377
|
+
"""
|
|
378
|
+
# Build dependency graph (validates no cycles)
|
|
379
|
+
deps = detect_query_dependencies(queries)
|
|
380
|
+
|
|
381
|
+
# Topological sort — leaves (no deps) come first
|
|
382
|
+
order = _topological_sort(deps)
|
|
383
|
+
|
|
384
|
+
# Phase 1: expand {{ queries.* }} refs in dependency order
|
|
385
|
+
expanded: dict[str, str] = {}
|
|
386
|
+
for name in order:
|
|
387
|
+
if name not in queries:
|
|
388
|
+
continue
|
|
389
|
+
raw_sql = _get_query_sql(queries[name])
|
|
390
|
+
if raw_sql and ("{{ queries." in raw_sql or "{{queries." in raw_sql):
|
|
391
|
+
expanded[name] = _substitute_query_refs(raw_sql, expanded)
|
|
392
|
+
else:
|
|
393
|
+
expanded[name] = raw_sql
|
|
394
|
+
|
|
395
|
+
target_sql = expanded.get(query_name, _get_query_sql(queries.get(query_name, "")))
|
|
396
|
+
|
|
397
|
+
# Phase 2: resolve variables / filters via Jinja
|
|
398
|
+
if target_sql and ("{{" in target_sql or "{%" in target_sql):
|
|
399
|
+
target_sql = resolve_jinja_template(target_sql, variables=variables)
|
|
400
|
+
|
|
401
|
+
return target_sql
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _filter_helper(
|
|
405
|
+
column: str,
|
|
406
|
+
value: Any,
|
|
407
|
+
operator: str = "=",
|
|
408
|
+
none: str = "allow",
|
|
409
|
+
) -> str:
|
|
410
|
+
"""Generate SQL filter clause (DEPRECATED - use parameterized version).
|
|
411
|
+
|
|
412
|
+
.. deprecated::
|
|
413
|
+
This helper uses string interpolation which is vulnerable to SQL injection.
|
|
414
|
+
Use the parameterized filter helper from dataface.core.compile.parameterized instead,
|
|
415
|
+
which is automatically used when executing queries via the SqlAdapter.
|
|
416
|
+
|
|
417
|
+
Security Warning:
|
|
418
|
+
This function directly interpolates values into SQL strings. If values
|
|
419
|
+
come from user input, this creates SQL injection vulnerabilities.
|
|
420
|
+
The parameterized version in parameterized.py should be used instead.
|
|
421
|
+
|
|
422
|
+
Helper function available in templates as {{ filter(...) }}.
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
column: Column name
|
|
426
|
+
value: Filter value
|
|
427
|
+
operator: SQL operator (=, !=, >, <, etc.)
|
|
428
|
+
none: Fallback when value is null/empty/undefined. 'allow' (default) returns
|
|
429
|
+
'1=1' (no constraint, show all rows). 'deny' returns '1=0' (zero rows).
|
|
430
|
+
|
|
431
|
+
Returns:
|
|
432
|
+
SQL clause like "column = 'value'", "1=1" (allow all) if value is None/undefined,
|
|
433
|
+
or "1=0" (deny all) if value is None/undefined and none='deny'.
|
|
434
|
+
|
|
435
|
+
Raises:
|
|
436
|
+
ValueError: If none is not 'allow' or 'deny'.
|
|
437
|
+
"""
|
|
438
|
+
if none not in ("allow", "deny"):
|
|
439
|
+
raise ValueError(f"none= must be 'allow' or 'deny', got {none!r}")
|
|
440
|
+
|
|
441
|
+
# Handle undefined Jinja variables (from lenient mode)
|
|
442
|
+
if isinstance(value, Undefined):
|
|
443
|
+
return "1=0" if none == "deny" else "1=1"
|
|
444
|
+
if value is None or value == "":
|
|
445
|
+
return "1=0" if none == "deny" else "1=1"
|
|
446
|
+
|
|
447
|
+
if isinstance(value, str):
|
|
448
|
+
return f"{column} {operator} '{value}'"
|
|
449
|
+
elif isinstance(value, (int, float)):
|
|
450
|
+
return f"{column} {operator} {value}"
|
|
451
|
+
elif isinstance(value, list):
|
|
452
|
+
if not value:
|
|
453
|
+
return "1=0" if none == "deny" else "1=1"
|
|
454
|
+
quoted = [f"'{v}'" if isinstance(v, str) else str(v) for v in value]
|
|
455
|
+
return f"{column} IN ({', '.join(quoted)})"
|
|
456
|
+
|
|
457
|
+
return f"{column} {operator} '{value}'"
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _filter_date_range_helper(
|
|
461
|
+
column: str,
|
|
462
|
+
date_range: Any,
|
|
463
|
+
) -> str:
|
|
464
|
+
"""Generate SQL date range filter (DEPRECATED - use parameterized version).
|
|
465
|
+
|
|
466
|
+
.. deprecated::
|
|
467
|
+
This helper uses string interpolation which is vulnerable to SQL injection.
|
|
468
|
+
Use the parameterized filter_date_range helper from dataface.core.compile.parameterized
|
|
469
|
+
instead, which is automatically used when executing queries via the SqlAdapter.
|
|
470
|
+
|
|
471
|
+
Security Warning:
|
|
472
|
+
This function directly interpolates values into SQL strings. If values
|
|
473
|
+
come from user input, this creates SQL injection vulnerabilities.
|
|
474
|
+
The parameterized version in parameterized.py should be used instead.
|
|
475
|
+
|
|
476
|
+
Helper function for {{ filter_date_range(column, date_range) }}.
|
|
477
|
+
|
|
478
|
+
Args:
|
|
479
|
+
column: Date column name
|
|
480
|
+
date_range: Tuple/list of [start, end] dates, or JSON string
|
|
481
|
+
|
|
482
|
+
Returns:
|
|
483
|
+
SQL BETWEEN clause or "1=1" (always true) if invalid/undefined
|
|
484
|
+
"""
|
|
485
|
+
if not date_range:
|
|
486
|
+
return "1=1"
|
|
487
|
+
|
|
488
|
+
# Handle JSON string (from URL parameters)
|
|
489
|
+
if isinstance(date_range, str):
|
|
490
|
+
import json
|
|
491
|
+
|
|
492
|
+
try:
|
|
493
|
+
date_range = json.loads(date_range)
|
|
494
|
+
except (json.JSONDecodeError, ValueError) as e:
|
|
495
|
+
raise ValueError(f"Invalid date_range JSON: {date_range!r}") from e
|
|
496
|
+
|
|
497
|
+
if not isinstance(date_range, (list, tuple)):
|
|
498
|
+
raise ValueError(
|
|
499
|
+
f"date_range must be a list/tuple [start, end], got {type(date_range).__name__}"
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
if len(date_range) != 2:
|
|
503
|
+
raise ValueError(
|
|
504
|
+
f"date_range must have exactly 2 elements [start, end], got {len(date_range)}"
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
start, end = date_range
|
|
508
|
+
if not start or not end:
|
|
509
|
+
return "1=1" # Empty dates = no filter (intentional)
|
|
510
|
+
|
|
511
|
+
return f"{column} BETWEEN '{start}' AND '{end}'"
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Jinja env for chart-label templates and ``where:`` expressions.
|
|
2
|
+
|
|
3
|
+
Lives in compile/ so ChartLabels' pydantic validators can parse without
|
|
4
|
+
reaching into the render layer (the dependency direction stays
|
|
5
|
+
compile → shared, render → shared, never render → compile or compile →
|
|
6
|
+
render).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from jinja2 import Environment, StrictUndefined
|
|
14
|
+
|
|
15
|
+
from dataface.core.render.format_utils import format_d3
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def label_jinja_env() -> Environment:
|
|
19
|
+
"""Return the Jinja env used to compile and render label templates.
|
|
20
|
+
|
|
21
|
+
Strict-undefined: a typo like ``{{ pct }}`` (instead of ``percent``)
|
|
22
|
+
raises immediately rather than silently rendering empty.
|
|
23
|
+
"""
|
|
24
|
+
env = Environment(undefined=StrictUndefined, autoescape=False)
|
|
25
|
+
env.filters["format"] = _format_filter
|
|
26
|
+
return env
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _format_filter(value: Any, spec: str) -> str:
|
|
30
|
+
"""Jinja filter: ``{{ value | format('$,.0f') }}`` → d3-formatted."""
|
|
31
|
+
return format_d3(value, spec)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def strip_jinja_braces(expr: str) -> str:
|
|
35
|
+
"""Accept ``{{ x }}`` or bare ``x`` for ``where:`` — return the bare expression.
|
|
36
|
+
|
|
37
|
+
Authors naturally write ``where: "{{ value > 5 }}"`` (matching the
|
|
38
|
+
``template:`` syntax), but Jinja's ``compile_expression`` wants the
|
|
39
|
+
bare expression (``value > 5``). Tolerate the single-outer-pair case;
|
|
40
|
+
reject mixed forms (``"{{ a }} and {{ b }}"`` etc.) loudly.
|
|
41
|
+
"""
|
|
42
|
+
s = expr.strip()
|
|
43
|
+
if not (s.startswith("{{") and s.endswith("}}")):
|
|
44
|
+
return s
|
|
45
|
+
inner = s[2:-2]
|
|
46
|
+
if "{{" in inner or "}}" in inner:
|
|
47
|
+
raise ValueError(
|
|
48
|
+
"where: expression contains nested {{ ... }}; use a bare Python "
|
|
49
|
+
"expression instead (e.g. ``value > 5`` not "
|
|
50
|
+
"``{{ value > 5 }} and {{ other }}``)."
|
|
51
|
+
)
|
|
52
|
+
return inner.strip()
|