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,658 @@
|
|
|
1
|
+
"""Parameterized query rendering module.
|
|
2
|
+
|
|
3
|
+
Stage: COMPILE
|
|
4
|
+
Purpose: Render Jinja templates to parameterized SQL for SQL injection prevention.
|
|
5
|
+
|
|
6
|
+
Instead of interpolating values directly into SQL strings (vulnerable to injection):
|
|
7
|
+
"SELECT * FROM orders WHERE date >= '2024-01-01'"
|
|
8
|
+
|
|
9
|
+
This module renders to parameterized queries (safe):
|
|
10
|
+
sql = "SELECT * FROM orders WHERE date >= $1"
|
|
11
|
+
params = ["2024-01-01"]
|
|
12
|
+
|
|
13
|
+
Entry Points:
|
|
14
|
+
- render_parameterized(template, variables, dialect) -> ParameterizedQuery
|
|
15
|
+
|
|
16
|
+
The executor then uses: cursor.execute(sql, params)
|
|
17
|
+
|
|
18
|
+
Security Notes:
|
|
19
|
+
- All variable values are passed as parameters, never interpolated
|
|
20
|
+
- Operator validation uses VALID_OPERATORS allowlist
|
|
21
|
+
- Column/table names are validated to prevent injection via identifiers
|
|
22
|
+
- Legacy filter helpers in jinja.py should NOT be used (deprecated)
|
|
23
|
+
|
|
24
|
+
Dependencies:
|
|
25
|
+
- jinja2
|
|
26
|
+
- dataface.execute.dialects (for parameter placeholder styles)
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
import hashlib
|
|
30
|
+
import re
|
|
31
|
+
from collections.abc import Callable, Iterator
|
|
32
|
+
from dataclasses import dataclass, field
|
|
33
|
+
from typing import Any
|
|
34
|
+
|
|
35
|
+
from jinja2 import (
|
|
36
|
+
Environment,
|
|
37
|
+
StrictUndefined,
|
|
38
|
+
TemplateSyntaxError,
|
|
39
|
+
UndefinedError,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
from dataface.core.compile._jinja_helpers import _LenientUndefined, _QueryNamespace
|
|
43
|
+
from dataface.core.compile.errors import JinjaError
|
|
44
|
+
from dataface.core.execute.dialects import VALID_OPERATORS, SQLDialect, get_dialect
|
|
45
|
+
|
|
46
|
+
# Regex pattern for valid SQL identifiers (column names, table names)
|
|
47
|
+
# Allows: alphanumeric, underscore, and dot (for table.column)
|
|
48
|
+
# Must start with letter or underscore
|
|
49
|
+
_VALID_IDENTIFIER_PATTERN = re.compile(
|
|
50
|
+
r"^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)?$"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _validate_identifier(identifier: str, identifier_type: str = "column") -> None:
|
|
55
|
+
"""Validate SQL identifier (column or table.column) to prevent injection.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
identifier: The identifier to validate
|
|
59
|
+
identifier_type: Type for error messages ("column", "table", etc.)
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
ValueError: If identifier is invalid (empty, not a string, or contains invalid chars)
|
|
63
|
+
|
|
64
|
+
Security:
|
|
65
|
+
Only allows alphanumeric characters, underscores, and dots (for table.column).
|
|
66
|
+
Must start with a letter or underscore.
|
|
67
|
+
|
|
68
|
+
Limitations:
|
|
69
|
+
- Does not support schema-qualified names (schema.table.column)
|
|
70
|
+
- Does not support quoted identifiers ("Column Name")
|
|
71
|
+
- Does not support Unicode identifiers
|
|
72
|
+
For these cases, use raw SQL with manual parameterization.
|
|
73
|
+
"""
|
|
74
|
+
if not isinstance(identifier, str):
|
|
75
|
+
raise ValueError(
|
|
76
|
+
f"Invalid {identifier_type} name: must be a string, got {type(identifier).__name__}"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if not identifier:
|
|
80
|
+
raise ValueError(f"Invalid {identifier_type} name: cannot be empty")
|
|
81
|
+
|
|
82
|
+
if not _VALID_IDENTIFIER_PATTERN.match(identifier):
|
|
83
|
+
raise ValueError(
|
|
84
|
+
f"Invalid {identifier_type} name: {identifier!r}. "
|
|
85
|
+
f"Must contain only letters, numbers, underscores, and optionally "
|
|
86
|
+
f"one dot for table.column format. Must start with letter or underscore."
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _validate_operator(operator: str) -> str:
|
|
91
|
+
"""Validate SQL operator against allowlist to prevent injection.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
operator: The operator to validate
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
The normalized (uppercase) operator
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
ValueError: If operator is not in the allowlist
|
|
101
|
+
"""
|
|
102
|
+
op_upper = operator.upper().strip()
|
|
103
|
+
if op_upper not in VALID_OPERATORS:
|
|
104
|
+
raise ValueError(
|
|
105
|
+
f"Invalid SQL operator: {operator!r}. "
|
|
106
|
+
f"Must be one of: {', '.join(sorted(VALID_OPERATORS))}"
|
|
107
|
+
)
|
|
108
|
+
return operator
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass
|
|
112
|
+
class ParameterizedQuery:
|
|
113
|
+
"""Result of parameterized SQL rendering.
|
|
114
|
+
|
|
115
|
+
Attributes:
|
|
116
|
+
sql: SQL with parameter placeholders (e.g., $1, $2)
|
|
117
|
+
params: List of parameter values in order
|
|
118
|
+
template_hash: Hash of the original template for caching
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
sql: str
|
|
122
|
+
params: list[Any] = field(default_factory=list)
|
|
123
|
+
template_hash: str = ""
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class _ParameterCollector:
|
|
127
|
+
"""Collects parameters during Jinja rendering.
|
|
128
|
+
|
|
129
|
+
This class intercepts variable access during Jinja rendering,
|
|
130
|
+
collecting values for parameterization and returning placeholders.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
def __init__(
|
|
134
|
+
self,
|
|
135
|
+
variables: dict[str, Any],
|
|
136
|
+
dialect: SQLDialect,
|
|
137
|
+
exclude_vars: set | None = None,
|
|
138
|
+
):
|
|
139
|
+
"""Initialize parameter collector.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
variables: Original variable values
|
|
143
|
+
dialect: SQL dialect for placeholder generation
|
|
144
|
+
exclude_vars: Variable names to exclude from parameterization
|
|
145
|
+
(e.g., 'queries' namespace, helper functions)
|
|
146
|
+
"""
|
|
147
|
+
self.variables = variables
|
|
148
|
+
self.dialect = dialect
|
|
149
|
+
self.exclude_vars = exclude_vars or set()
|
|
150
|
+
self.params: list[Any] = []
|
|
151
|
+
self._param_index = 0
|
|
152
|
+
# Deduplication only works for indexed-placeholder dialects ($1, $2, …) where
|
|
153
|
+
# the same $N can repeat in SQL against a single param entry. For positional-only
|
|
154
|
+
# placeholders (?, %s) every occurrence needs its own param entry.
|
|
155
|
+
self._deduplicate = dialect.param(1) != dialect.param(2)
|
|
156
|
+
self._seen_params: dict[tuple, int] = {}
|
|
157
|
+
|
|
158
|
+
def get_param(self, name: str, value: Any) -> str:
|
|
159
|
+
"""Get parameter placeholder for a variable.
|
|
160
|
+
|
|
161
|
+
If the same variable with the same value is used multiple times,
|
|
162
|
+
reuses the same parameter. Different values get new parameters,
|
|
163
|
+
even if they have the same variable name (handles {% set %} reassignment).
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
name: Variable name
|
|
167
|
+
value: Variable value
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Parameter placeholder string (e.g., '$1')
|
|
171
|
+
"""
|
|
172
|
+
# Create a key based on name and value
|
|
173
|
+
# Use id() for mutable objects, value itself for immutable primitives
|
|
174
|
+
try:
|
|
175
|
+
# Try to use the value directly (works for hashable types)
|
|
176
|
+
key = (name, value)
|
|
177
|
+
hash(key) # Test if hashable
|
|
178
|
+
except TypeError:
|
|
179
|
+
# For unhashable types (lists, dicts), use object identity
|
|
180
|
+
key = (name, id(value))
|
|
181
|
+
|
|
182
|
+
# Reuse the same placeholder index only for indexed-placeholder dialects.
|
|
183
|
+
if self._deduplicate and key in self._seen_params:
|
|
184
|
+
return self.dialect.param(self._seen_params[key])
|
|
185
|
+
|
|
186
|
+
# Add new parameter
|
|
187
|
+
self._param_index += 1
|
|
188
|
+
self._seen_params[key] = self._param_index
|
|
189
|
+
self.params.append(value)
|
|
190
|
+
return self.dialect.param(self._param_index)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class _NullValue:
|
|
194
|
+
"""Marker for NULL values in parameterized context.
|
|
195
|
+
|
|
196
|
+
Renders as 'NULL' in SQL and is recognized by filter helpers
|
|
197
|
+
as requiring special handling (returning 1=1 for nullable filters).
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
def __str__(self) -> str:
|
|
201
|
+
return "NULL"
|
|
202
|
+
|
|
203
|
+
def __repr__(self) -> str:
|
|
204
|
+
return "NULL"
|
|
205
|
+
|
|
206
|
+
def __bool__(self) -> bool:
|
|
207
|
+
# NULL values are falsy for conditionals
|
|
208
|
+
return False
|
|
209
|
+
|
|
210
|
+
def __eq__(self, other: Any) -> bool:
|
|
211
|
+
return isinstance(other, _NullValue) or other is None
|
|
212
|
+
|
|
213
|
+
def __ne__(self, other: Any) -> bool:
|
|
214
|
+
return not self.__eq__(other)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class _ParameterizedValue:
|
|
218
|
+
"""Wrapper for parameterized variable values.
|
|
219
|
+
|
|
220
|
+
When rendered by Jinja, returns the parameter placeholder.
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
def __init__(self, name: str, value: Any, collector: _ParameterCollector):
|
|
224
|
+
self._name = name
|
|
225
|
+
self._value = value
|
|
226
|
+
self._collector = collector
|
|
227
|
+
|
|
228
|
+
def __str__(self) -> str:
|
|
229
|
+
"""Return parameter placeholder when rendered."""
|
|
230
|
+
return self._collector.get_param(self._name, self._value)
|
|
231
|
+
|
|
232
|
+
def __repr__(self) -> str:
|
|
233
|
+
return self.__str__()
|
|
234
|
+
|
|
235
|
+
# Support common operations that might be used in templates
|
|
236
|
+
def __eq__(self, other: Any) -> bool:
|
|
237
|
+
return self._value == other
|
|
238
|
+
|
|
239
|
+
def __ne__(self, other: Any) -> bool:
|
|
240
|
+
return self._value != other
|
|
241
|
+
|
|
242
|
+
def __bool__(self) -> bool:
|
|
243
|
+
return bool(self._value)
|
|
244
|
+
|
|
245
|
+
def __iter__(self) -> Iterator[Any]:
|
|
246
|
+
if hasattr(self._value, "__iter__"):
|
|
247
|
+
return iter(self._value)
|
|
248
|
+
raise TypeError(f"'{type(self._value).__name__}' is not iterable")
|
|
249
|
+
|
|
250
|
+
def __getitem__(self, key: Any) -> Any:
|
|
251
|
+
if hasattr(self._value, "__getitem__"):
|
|
252
|
+
return self._value[key]
|
|
253
|
+
raise TypeError(f"'{type(self._value).__name__}' is not subscriptable")
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _compute_template_hash(template: str) -> str:
|
|
257
|
+
"""Compute hash of template for caching.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
template: SQL template string
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
SHA-256 hash of template (first 16 chars)
|
|
264
|
+
"""
|
|
265
|
+
return hashlib.sha256(template.encode()).hexdigest()[:16]
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def render_parameterized(
|
|
269
|
+
template: str,
|
|
270
|
+
variables: dict[str, Any] | None = None,
|
|
271
|
+
dialect: SQLDialect | None = None,
|
|
272
|
+
profile_type: str = "postgres",
|
|
273
|
+
strict: bool = True,
|
|
274
|
+
) -> ParameterizedQuery:
|
|
275
|
+
"""Render Jinja template to parameterized SQL.
|
|
276
|
+
|
|
277
|
+
Converts {{ variable }} expressions to database parameter placeholders.
|
|
278
|
+
|
|
279
|
+
For simple variable substitution like:
|
|
280
|
+
"SELECT * FROM orders WHERE region = '{{ region }}'"
|
|
281
|
+
|
|
282
|
+
Produces:
|
|
283
|
+
sql = "SELECT * FROM orders WHERE region = $1"
|
|
284
|
+
params = ["North"]
|
|
285
|
+
|
|
286
|
+
Complex Jinja expressions (conditionals, loops, filters) are still
|
|
287
|
+
evaluated, but variable values within them become parameters.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
template: SQL template with Jinja expressions
|
|
291
|
+
variables: Variable values for substitution
|
|
292
|
+
dialect: SQL dialect instance (overrides profile_type)
|
|
293
|
+
profile_type: Database type string (e.g., 'postgres', 'duckdb')
|
|
294
|
+
strict: If True (default), raises error on undefined variables.
|
|
295
|
+
If False, undefined variables become empty strings (useful for
|
|
296
|
+
chart editor where variables may not all be set).
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
ParameterizedQuery with SQL, params, and template_hash
|
|
300
|
+
|
|
301
|
+
Raises:
|
|
302
|
+
JinjaError: If template syntax is invalid or required variable is missing
|
|
303
|
+
(only in strict mode)
|
|
304
|
+
|
|
305
|
+
Example:
|
|
306
|
+
>>> result = render_parameterized(
|
|
307
|
+
... "SELECT * FROM users WHERE status = '{{ status }}'",
|
|
308
|
+
... variables={"status": "active"},
|
|
309
|
+
... profile_type="postgres"
|
|
310
|
+
... )
|
|
311
|
+
>>> result.sql
|
|
312
|
+
"SELECT * FROM users WHERE status = '$1'"
|
|
313
|
+
>>> result.params
|
|
314
|
+
['active']
|
|
315
|
+
"""
|
|
316
|
+
if not template:
|
|
317
|
+
return ParameterizedQuery(sql="", params=[], template_hash="")
|
|
318
|
+
|
|
319
|
+
# Quick check for Jinja syntax - if no Jinja, return as-is
|
|
320
|
+
if "{{" not in template and "{%" not in template:
|
|
321
|
+
return ParameterizedQuery(
|
|
322
|
+
sql=template,
|
|
323
|
+
params=[],
|
|
324
|
+
template_hash=_compute_template_hash(template),
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
variables = variables or {}
|
|
328
|
+
|
|
329
|
+
# Get dialect
|
|
330
|
+
if dialect is None:
|
|
331
|
+
dialect = get_dialect(profile_type)
|
|
332
|
+
|
|
333
|
+
# Create parameter collector
|
|
334
|
+
collector = _ParameterCollector(
|
|
335
|
+
variables=variables,
|
|
336
|
+
dialect=dialect,
|
|
337
|
+
exclude_vars={"queries", "filter", "filter_date_range"},
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
# Build context with parameterized wrapper for each variable
|
|
341
|
+
context: dict[str, Any] = {}
|
|
342
|
+
|
|
343
|
+
for name, value in variables.items():
|
|
344
|
+
if name in collector.exclude_vars:
|
|
345
|
+
# Keep special context items as-is
|
|
346
|
+
context[name] = value
|
|
347
|
+
elif value is None:
|
|
348
|
+
# None values should render as NULL and be tracked as None
|
|
349
|
+
# We use a special marker that the filter helpers recognize
|
|
350
|
+
context[name] = _NullValue()
|
|
351
|
+
else:
|
|
352
|
+
# Wrap value for parameterization
|
|
353
|
+
context[name] = _ParameterizedValue(name, value, collector)
|
|
354
|
+
|
|
355
|
+
# Add parameterized filter helpers
|
|
356
|
+
context["filter"] = _make_filter_helper(collector, dialect)
|
|
357
|
+
context["filter_date_range"] = _make_filter_date_range_helper(collector, dialect)
|
|
358
|
+
|
|
359
|
+
# Handle queries namespace if present
|
|
360
|
+
if "queries" in variables:
|
|
361
|
+
context["queries"] = variables["queries"]
|
|
362
|
+
|
|
363
|
+
try:
|
|
364
|
+
undefined_cls = StrictUndefined if strict else _LenientUndefined
|
|
365
|
+
env = Environment(undefined=undefined_cls)
|
|
366
|
+
jinja_template = env.from_string(template)
|
|
367
|
+
rendered_sql = jinja_template.render(context)
|
|
368
|
+
|
|
369
|
+
# Clean up any surrounding quotes around parameter placeholders
|
|
370
|
+
# e.g., "'$1'" -> "$1" for proper parameterization
|
|
371
|
+
rendered_sql = _clean_parameter_quotes(
|
|
372
|
+
rendered_sql, dialect, len(collector.params)
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
return ParameterizedQuery(
|
|
376
|
+
sql=rendered_sql,
|
|
377
|
+
params=collector.params,
|
|
378
|
+
template_hash=_compute_template_hash(template),
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
except UndefinedError as e:
|
|
382
|
+
raise JinjaError(f"Undefined variable: {e}", template) from e
|
|
383
|
+
except TemplateSyntaxError as e:
|
|
384
|
+
raise JinjaError(f"Template syntax error: {e}", template) from e
|
|
385
|
+
except ValueError as e:
|
|
386
|
+
# Re-raise validation errors (operator, column name) with context
|
|
387
|
+
raise JinjaError(f"Validation error: {e}", template) from e
|
|
388
|
+
except (KeyError, TypeError) as e:
|
|
389
|
+
# Key lookup failures, type errors
|
|
390
|
+
raise JinjaError(f"Template error: {e}", template) from e
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _clean_parameter_quotes(sql: str, dialect: SQLDialect, param_count: int) -> str:
|
|
394
|
+
"""Remove quotes around parameter placeholders.
|
|
395
|
+
|
|
396
|
+
SQL like "WHERE name = '$1'" should become "WHERE name = $1"
|
|
397
|
+
because the database driver handles string quoting for parameters.
|
|
398
|
+
|
|
399
|
+
Uses regex patterns to avoid incorrectly matching placeholders that appear
|
|
400
|
+
inside SQL string literals or comments.
|
|
401
|
+
|
|
402
|
+
Args:
|
|
403
|
+
sql: Rendered SQL string
|
|
404
|
+
dialect: SQL dialect
|
|
405
|
+
param_count: Number of parameters
|
|
406
|
+
|
|
407
|
+
Returns:
|
|
408
|
+
SQL with quotes removed from around parameter placeholders
|
|
409
|
+
"""
|
|
410
|
+
if param_count == 0:
|
|
411
|
+
return sql
|
|
412
|
+
|
|
413
|
+
result = sql
|
|
414
|
+
dialect_name = dialect.name
|
|
415
|
+
|
|
416
|
+
if dialect_name in ("postgres", "postgresql", "duckdb", "redshift"):
|
|
417
|
+
# $1, $2, ... style - remove surrounding quotes using regex
|
|
418
|
+
# Pattern matches quotes immediately around the placeholder
|
|
419
|
+
for i in range(1, param_count + 1):
|
|
420
|
+
# Escape $ for regex
|
|
421
|
+
placeholder_pattern = rf"'(\${i})'"
|
|
422
|
+
result = re.sub(placeholder_pattern, r"\1", result)
|
|
423
|
+
placeholder_pattern = rf'"(\${i})"'
|
|
424
|
+
result = re.sub(placeholder_pattern, r"\1", result)
|
|
425
|
+
|
|
426
|
+
elif dialect_name in ("mysql", "mariadb"):
|
|
427
|
+
# %s style - use regex to match exactly '%s' or "%s"
|
|
428
|
+
result = re.sub(r"'(%s)'", r"\1", result)
|
|
429
|
+
result = re.sub(r'"(%s)"', r"\1", result)
|
|
430
|
+
|
|
431
|
+
elif dialect_name == "snowflake":
|
|
432
|
+
# ? style - use regex to match exactly '?' or "?"
|
|
433
|
+
result = re.sub(r"'(\?)'", r"\1", result)
|
|
434
|
+
result = re.sub(r'"(\?)"', r"\1", result)
|
|
435
|
+
|
|
436
|
+
elif dialect_name == "bigquery":
|
|
437
|
+
# @param_name style - named params, would need different handling
|
|
438
|
+
# BigQuery uses @name format which is already safe
|
|
439
|
+
pass
|
|
440
|
+
|
|
441
|
+
elif dialect_name in ("databricks", "spark"):
|
|
442
|
+
# :param style - named params
|
|
443
|
+
pass
|
|
444
|
+
|
|
445
|
+
return result
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _make_filter_helper(
|
|
449
|
+
collector: _ParameterCollector,
|
|
450
|
+
dialect: SQLDialect,
|
|
451
|
+
) -> Callable[..., str]:
|
|
452
|
+
"""Create parameterized filter helper.
|
|
453
|
+
|
|
454
|
+
Returns a function that generates parameterized filter clauses:
|
|
455
|
+
{{ filter('column', value) }} -> "column = $1"
|
|
456
|
+
{{ filter('column', value, '!=') }} -> "column != $1"
|
|
457
|
+
|
|
458
|
+
Security:
|
|
459
|
+
- Column names are validated against SQL injection
|
|
460
|
+
- Operators are validated against VALID_OPERATORS allowlist
|
|
461
|
+
- Values are always passed as parameters, never interpolated
|
|
462
|
+
|
|
463
|
+
Args:
|
|
464
|
+
collector: Parameter collector
|
|
465
|
+
dialect: SQL dialect
|
|
466
|
+
|
|
467
|
+
Returns:
|
|
468
|
+
Filter helper function
|
|
469
|
+
"""
|
|
470
|
+
|
|
471
|
+
def filter_helper(
|
|
472
|
+
column: str,
|
|
473
|
+
value: Any,
|
|
474
|
+
operator: str = "=",
|
|
475
|
+
*,
|
|
476
|
+
none: str = "allow",
|
|
477
|
+
) -> str:
|
|
478
|
+
"""Generate parameterized filter clause.
|
|
479
|
+
|
|
480
|
+
Args:
|
|
481
|
+
column: Column name (must be valid SQL identifier, not user input)
|
|
482
|
+
value: Filter value (will be parameterized)
|
|
483
|
+
operator: SQL operator (validated against allowlist)
|
|
484
|
+
none: Fallback when value is null/empty/_NullValue. 'allow' (default)
|
|
485
|
+
returns '1=1' (no constraint, show all rows). 'deny' returns
|
|
486
|
+
'1=0' (zero rows).
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
SQL clause with parameter placeholder, '1=1' (allow all) on missing
|
|
490
|
+
value, or '1=0' (deny all) on missing value when none='deny'.
|
|
491
|
+
|
|
492
|
+
Raises:
|
|
493
|
+
ValueError: If column name, operator, or `none` is invalid.
|
|
494
|
+
"""
|
|
495
|
+
if none not in ("allow", "deny"):
|
|
496
|
+
raise ValueError(f"none= must be 'allow' or 'deny', got {none!r}")
|
|
497
|
+
|
|
498
|
+
# Validate column name to prevent SQL injection
|
|
499
|
+
_validate_identifier(column, "column")
|
|
500
|
+
|
|
501
|
+
# Handle None/empty/NullValue - return fallback per `none` kwarg
|
|
502
|
+
if value is None or value == "" or isinstance(value, _NullValue):
|
|
503
|
+
return "1=0" if none == "deny" else "1=1"
|
|
504
|
+
|
|
505
|
+
# Unwrap if it's already a ParameterizedValue
|
|
506
|
+
actual_value = value._value if isinstance(value, _ParameterizedValue) else value
|
|
507
|
+
|
|
508
|
+
# Handle list for IN clause
|
|
509
|
+
if isinstance(actual_value, list):
|
|
510
|
+
if not actual_value:
|
|
511
|
+
return "1=0" if none == "deny" else "1=1"
|
|
512
|
+
|
|
513
|
+
# Validate column for IN clause too
|
|
514
|
+
# For IN clause, we need multiple parameters
|
|
515
|
+
placeholders = []
|
|
516
|
+
for item in actual_value:
|
|
517
|
+
collector._param_index += 1
|
|
518
|
+
collector.params.append(item)
|
|
519
|
+
placeholders.append(dialect.param(collector._param_index))
|
|
520
|
+
|
|
521
|
+
return f"{column} IN ({', '.join(placeholders)})"
|
|
522
|
+
|
|
523
|
+
# Validate operator to prevent SQL injection
|
|
524
|
+
_validate_operator(operator)
|
|
525
|
+
|
|
526
|
+
# Single value - get parameter placeholder
|
|
527
|
+
collector._param_index += 1
|
|
528
|
+
collector.params.append(actual_value)
|
|
529
|
+
placeholder = dialect.param(collector._param_index)
|
|
530
|
+
|
|
531
|
+
return f"{column} {operator} {placeholder}"
|
|
532
|
+
|
|
533
|
+
return filter_helper
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def _make_filter_date_range_helper(
|
|
537
|
+
collector: _ParameterCollector,
|
|
538
|
+
dialect: SQLDialect,
|
|
539
|
+
) -> Callable[[str, Any], str]:
|
|
540
|
+
"""Create parameterized date range filter helper.
|
|
541
|
+
|
|
542
|
+
Returns a function that generates parameterized BETWEEN clauses:
|
|
543
|
+
{{ filter_date_range('date', date_range) }}
|
|
544
|
+
-> "date BETWEEN $1 AND $2"
|
|
545
|
+
|
|
546
|
+
Security:
|
|
547
|
+
- Column names are validated against SQL injection
|
|
548
|
+
- Date values are always passed as parameters
|
|
549
|
+
|
|
550
|
+
Args:
|
|
551
|
+
collector: Parameter collector
|
|
552
|
+
dialect: SQL dialect
|
|
553
|
+
|
|
554
|
+
Returns:
|
|
555
|
+
Date range filter helper function
|
|
556
|
+
"""
|
|
557
|
+
|
|
558
|
+
def filter_date_range_helper(
|
|
559
|
+
column: str,
|
|
560
|
+
date_range: Any,
|
|
561
|
+
) -> str:
|
|
562
|
+
"""Generate parameterized date range filter.
|
|
563
|
+
|
|
564
|
+
Args:
|
|
565
|
+
column: Date column name (must be valid SQL identifier)
|
|
566
|
+
date_range: Tuple/list of [start, end] dates
|
|
567
|
+
|
|
568
|
+
Returns:
|
|
569
|
+
SQL BETWEEN clause with parameter placeholders
|
|
570
|
+
|
|
571
|
+
Raises:
|
|
572
|
+
ValueError: If column name is invalid
|
|
573
|
+
"""
|
|
574
|
+
# Validate column name to prevent SQL injection
|
|
575
|
+
_validate_identifier(column, "column")
|
|
576
|
+
|
|
577
|
+
if not date_range or isinstance(date_range, _NullValue):
|
|
578
|
+
return "1=1"
|
|
579
|
+
|
|
580
|
+
# Unwrap if it's a ParameterizedValue
|
|
581
|
+
actual_value = (
|
|
582
|
+
date_range._value
|
|
583
|
+
if isinstance(date_range, _ParameterizedValue)
|
|
584
|
+
else date_range
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
# Handle JSON string
|
|
588
|
+
if isinstance(actual_value, str):
|
|
589
|
+
import json
|
|
590
|
+
|
|
591
|
+
try:
|
|
592
|
+
actual_value = json.loads(actual_value)
|
|
593
|
+
except (json.JSONDecodeError, ValueError) as e:
|
|
594
|
+
raise ValueError(f"Invalid date_range JSON: {actual_value!r}") from e
|
|
595
|
+
|
|
596
|
+
if not isinstance(actual_value, (list, tuple)):
|
|
597
|
+
raise ValueError(
|
|
598
|
+
f"date_range must be a list/tuple [start, end], got {type(actual_value).__name__}"
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
if len(actual_value) != 2:
|
|
602
|
+
raise ValueError(
|
|
603
|
+
f"date_range must have exactly 2 elements [start, end], got {len(actual_value)}"
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
start, end = actual_value
|
|
607
|
+
if not start or not end:
|
|
608
|
+
return "1=1" # Empty dates = no filter (intentional)
|
|
609
|
+
|
|
610
|
+
# Add parameters for start and end
|
|
611
|
+
collector._param_index += 1
|
|
612
|
+
collector.params.append(start)
|
|
613
|
+
start_placeholder = dialect.param(collector._param_index)
|
|
614
|
+
|
|
615
|
+
collector._param_index += 1
|
|
616
|
+
collector.params.append(end)
|
|
617
|
+
end_placeholder = dialect.param(collector._param_index)
|
|
618
|
+
|
|
619
|
+
return f"{column} BETWEEN {start_placeholder} AND {end_placeholder}"
|
|
620
|
+
|
|
621
|
+
return filter_date_range_helper
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def render_parameterized_with_queries(
|
|
625
|
+
template: str,
|
|
626
|
+
variables: dict[str, Any] | None = None,
|
|
627
|
+
queries: dict[str, Any] | None = None,
|
|
628
|
+
dialect: SQLDialect | None = None,
|
|
629
|
+
profile_type: str = "postgres",
|
|
630
|
+
) -> ParameterizedQuery:
|
|
631
|
+
"""Render parameterized SQL with query reference support.
|
|
632
|
+
|
|
633
|
+
Like render_parameterized but also handles {{ queries.* }} references.
|
|
634
|
+
|
|
635
|
+
Args:
|
|
636
|
+
template: SQL template with Jinja expressions
|
|
637
|
+
variables: Variable values for substitution
|
|
638
|
+
queries: Query registry for {{ queries.* }} resolution
|
|
639
|
+
dialect: SQL dialect instance
|
|
640
|
+
profile_type: Database type string
|
|
641
|
+
|
|
642
|
+
Returns:
|
|
643
|
+
ParameterizedQuery with SQL, params, and template_hash
|
|
644
|
+
"""
|
|
645
|
+
variables_with_queries: dict[str, Any] | None
|
|
646
|
+
if queries:
|
|
647
|
+
# Include queries in variables for resolution
|
|
648
|
+
variables_with_queries = dict(variables or {})
|
|
649
|
+
variables_with_queries["queries"] = _QueryNamespace(queries)
|
|
650
|
+
else:
|
|
651
|
+
variables_with_queries = variables
|
|
652
|
+
|
|
653
|
+
return render_parameterized(
|
|
654
|
+
template=template,
|
|
655
|
+
variables=variables_with_queries,
|
|
656
|
+
dialect=dialect,
|
|
657
|
+
profile_type=profile_type,
|
|
658
|
+
)
|