nao-core 0.0.30__py3-none-any.whl → 0.0.32__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.
Files changed (48) hide show
  1. nao_core/__init__.py +1 -1
  2. nao_core/bin/fastapi/main.py +6 -0
  3. nao_core/bin/migrations-postgres/0005_add_project_tables.sql +39 -0
  4. nao_core/bin/migrations-postgres/meta/0005_snapshot.json +1129 -0
  5. nao_core/bin/migrations-postgres/meta/_journal.json +7 -0
  6. nao_core/bin/migrations-sqlite/0005_add_project_tables.sql +38 -0
  7. nao_core/bin/migrations-sqlite/meta/0005_snapshot.json +1086 -0
  8. nao_core/bin/migrations-sqlite/meta/_journal.json +7 -0
  9. nao_core/bin/nao-chat-server +0 -0
  10. nao_core/bin/public/assets/{code-block-F6WJLWQG-z4zcca7w.js → code-block-F6WJLWQG-TAi8koem.js} +1 -1
  11. nao_core/bin/public/assets/index-BfHcd9Xz.css +1 -0
  12. nao_core/bin/public/assets/{index-DhhS7iVA.js → index-Mzo9bkag.js} +256 -172
  13. nao_core/bin/public/index.html +2 -2
  14. nao_core/commands/chat.py +11 -10
  15. nao_core/commands/init.py +27 -4
  16. nao_core/commands/sync/__init__.py +40 -21
  17. nao_core/commands/sync/accessors.py +218 -139
  18. nao_core/commands/sync/cleanup.py +133 -0
  19. nao_core/commands/sync/providers/__init__.py +30 -0
  20. nao_core/commands/sync/providers/base.py +87 -0
  21. nao_core/commands/sync/providers/databases/__init__.py +17 -0
  22. nao_core/commands/sync/providers/databases/bigquery.py +78 -0
  23. nao_core/commands/sync/providers/databases/databricks.py +79 -0
  24. nao_core/commands/sync/providers/databases/duckdb.py +83 -0
  25. nao_core/commands/sync/providers/databases/postgres.py +78 -0
  26. nao_core/commands/sync/providers/databases/provider.py +123 -0
  27. nao_core/commands/sync/providers/databases/snowflake.py +78 -0
  28. nao_core/commands/sync/providers/repositories/__init__.py +5 -0
  29. nao_core/commands/sync/{repositories.py → providers/repositories/provider.py} +43 -20
  30. nao_core/config/__init__.py +2 -0
  31. nao_core/config/base.py +23 -4
  32. nao_core/config/databases/__init__.py +5 -0
  33. nao_core/config/databases/base.py +1 -0
  34. nao_core/config/databases/postgres.py +78 -0
  35. nao_core/templates/__init__.py +12 -0
  36. nao_core/templates/defaults/databases/columns.md.j2 +23 -0
  37. nao_core/templates/defaults/databases/description.md.j2 +32 -0
  38. nao_core/templates/defaults/databases/preview.md.j2 +22 -0
  39. nao_core/templates/defaults/databases/profiling.md.j2 +34 -0
  40. nao_core/templates/engine.py +133 -0
  41. {nao_core-0.0.30.dist-info → nao_core-0.0.32.dist-info}/METADATA +6 -2
  42. nao_core-0.0.32.dist-info/RECORD +86 -0
  43. nao_core/bin/public/assets/index-ClduEZSo.css +0 -1
  44. nao_core/commands/sync/databases.py +0 -374
  45. nao_core-0.0.30.dist-info/RECORD +0 -65
  46. {nao_core-0.0.30.dist-info → nao_core-0.0.32.dist-info}/WHEEL +0 -0
  47. {nao_core-0.0.30.dist-info → nao_core-0.0.32.dist-info}/entry_points.txt +0 -0
  48. {nao_core-0.0.30.dist-info → nao_core-0.0.32.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,123 @@
1
+ """Database sync provider implementation."""
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from rich.console import Console
7
+ from rich.progress import BarColumn, Progress, SpinnerColumn, TaskProgressColumn, TextColumn
8
+
9
+ from nao_core.commands.sync.accessors import DataAccessor
10
+ from nao_core.commands.sync.cleanup import DatabaseSyncState, cleanup_stale_paths
11
+ from nao_core.commands.sync.registry import get_accessors
12
+ from nao_core.config import AnyDatabaseConfig, NaoConfig
13
+
14
+ from ..base import SyncProvider, SyncResult
15
+ from .bigquery import sync_bigquery
16
+ from .databricks import sync_databricks
17
+ from .duckdb import sync_duckdb
18
+ from .postgres import sync_postgres
19
+ from .snowflake import sync_snowflake
20
+
21
+ console = Console()
22
+
23
+ # Registry mapping database types to their sync functions
24
+ DATABASE_SYNC_FUNCTIONS = {
25
+ "bigquery": sync_bigquery,
26
+ "duckdb": sync_duckdb,
27
+ "databricks": sync_databricks,
28
+ "snowflake": sync_snowflake,
29
+ "postgres": sync_postgres,
30
+ }
31
+
32
+
33
+ class DatabaseSyncProvider(SyncProvider):
34
+ """Provider for syncing database schemas to markdown documentation."""
35
+
36
+ @property
37
+ def name(self) -> str:
38
+ return "Databases"
39
+
40
+ @property
41
+ def emoji(self) -> str:
42
+ return "🗄️"
43
+
44
+ @property
45
+ def default_output_dir(self) -> str:
46
+ return "databases"
47
+
48
+ def get_items(self, config: NaoConfig) -> list[AnyDatabaseConfig]:
49
+ return config.databases
50
+
51
+ def sync(self, items: list[Any], output_path: Path, project_path: Path | None = None) -> SyncResult:
52
+ """Sync all configured databases.
53
+
54
+ Args:
55
+ items: List of database configurations
56
+ output_path: Base path where database schemas are stored
57
+ project_path: Path to the nao project root (for template resolution)
58
+
59
+ Returns:
60
+ SyncResult with datasets and tables synced
61
+ """
62
+ if not items:
63
+ console.print("\n[dim]No databases configured[/dim]")
64
+ return SyncResult(provider_name=self.name, items_synced=0)
65
+
66
+ # Set project path for template resolution
67
+ DataAccessor.set_project_path(project_path)
68
+
69
+ total_datasets = 0
70
+ total_tables = 0
71
+ total_removed = 0
72
+ sync_states: list[DatabaseSyncState] = []
73
+
74
+ console.print(f"\n[bold cyan]{self.emoji} Syncing {self.name}[/bold cyan]")
75
+ console.print(f"[dim]Location:[/dim] {output_path.absolute()}\n")
76
+
77
+ with Progress(
78
+ SpinnerColumn(style="dim"),
79
+ TextColumn("[progress.description]{task.description}"),
80
+ BarColumn(bar_width=30, style="dim", complete_style="cyan", finished_style="green"),
81
+ TaskProgressColumn(),
82
+ console=console,
83
+ transient=False,
84
+ ) as progress:
85
+ for db in items:
86
+ # Get accessors from database config
87
+ db_accessors = get_accessors(db.accessors)
88
+ accessor_names = [a.filename.replace(".md", "") for a in db_accessors]
89
+
90
+ try:
91
+ console.print(f"[dim]{db.name} accessors:[/dim] {', '.join(accessor_names)}")
92
+
93
+ sync_fn = DATABASE_SYNC_FUNCTIONS.get(db.type)
94
+ if sync_fn:
95
+ state = sync_fn(db, output_path, progress, db_accessors)
96
+ sync_states.append(state)
97
+ total_datasets += state.schemas_synced
98
+ total_tables += state.tables_synced
99
+ else:
100
+ console.print(f"[yellow]⚠ Unsupported database type: {db.type}[/yellow]")
101
+ except Exception as e:
102
+ console.print(f"[bold red]✗[/bold red] Failed to sync {db.name}: {e}")
103
+
104
+ # Clean up stale files after all syncs complete
105
+ for state in sync_states:
106
+ removed = cleanup_stale_paths(state, verbose=True)
107
+ total_removed += removed
108
+
109
+ # Build summary
110
+ summary = f"{total_tables} tables across {total_datasets} datasets"
111
+ if total_removed > 0:
112
+ summary += f", {total_removed} stale removed"
113
+
114
+ return SyncResult(
115
+ provider_name=self.name,
116
+ items_synced=total_tables,
117
+ details={
118
+ "datasets": total_datasets,
119
+ "tables": total_tables,
120
+ "removed": total_removed,
121
+ },
122
+ summary=summary,
123
+ )
@@ -0,0 +1,78 @@
1
+ from pathlib import Path
2
+
3
+ from rich.progress import Progress
4
+
5
+ from nao_core.commands.sync.accessors import DataAccessor
6
+ from nao_core.commands.sync.cleanup import DatabaseSyncState
7
+
8
+
9
+ def sync_snowflake(
10
+ db_config,
11
+ base_path: Path,
12
+ progress: Progress,
13
+ accessors: list[DataAccessor],
14
+ ) -> DatabaseSyncState:
15
+ """Sync Snowflake database schema to markdown files.
16
+
17
+ Args:
18
+ db_config: The database configuration
19
+ base_path: Base output path
20
+ progress: Rich progress instance
21
+ accessors: List of data accessors to run
22
+
23
+ Returns:
24
+ DatabaseSyncState with sync results and tracked paths
25
+ """
26
+ conn = db_config.connect()
27
+ db_path = base_path / "type=snowflake" / f"database={db_config.database}"
28
+ state = DatabaseSyncState(db_path=db_path)
29
+
30
+ if db_config.schema:
31
+ schemas = [db_config.schema]
32
+ else:
33
+ schemas = conn.list_databases()
34
+
35
+ schema_task = progress.add_task(
36
+ f"[dim]{db_config.name}[/dim]",
37
+ total=len(schemas),
38
+ )
39
+
40
+ for schema in schemas:
41
+ try:
42
+ all_tables = conn.list_tables(database=schema)
43
+ except Exception:
44
+ progress.update(schema_task, advance=1)
45
+ continue
46
+
47
+ # Filter tables based on include/exclude patterns
48
+ tables = [t for t in all_tables if db_config.matches_pattern(schema, t)]
49
+
50
+ # Skip schema if no tables match
51
+ if not tables:
52
+ progress.update(schema_task, advance=1)
53
+ continue
54
+
55
+ schema_path = db_path / f"schema={schema}"
56
+ schema_path.mkdir(parents=True, exist_ok=True)
57
+ state.add_schema(schema)
58
+
59
+ table_task = progress.add_task(
60
+ f" [cyan]{schema}[/cyan]",
61
+ total=len(tables),
62
+ )
63
+
64
+ for table in tables:
65
+ table_path = schema_path / f"table={table}"
66
+ table_path.mkdir(parents=True, exist_ok=True)
67
+
68
+ for accessor in accessors:
69
+ content = accessor.generate(conn, schema, table)
70
+ output_file = table_path / accessor.filename
71
+ output_file.write_text(content)
72
+
73
+ state.add_table(schema, table)
74
+ progress.update(table_task, advance=1)
75
+
76
+ progress.update(schema_task, advance=1)
77
+
78
+ return state
@@ -0,0 +1,5 @@
1
+ """Repository syncing functionality for cloning and pulling git repositories."""
2
+
3
+ from .provider import RepositorySyncProvider
4
+
5
+ __all__ = ["RepositorySyncProvider"]
@@ -1,12 +1,16 @@
1
- """Repository syncing functionality for cloning and pulling git repositories."""
1
+ """Repository sync provider implementation."""
2
2
 
3
3
  import subprocess
4
4
  from pathlib import Path
5
+ from typing import Any
5
6
 
6
7
  from rich.console import Console
7
8
 
9
+ from nao_core.config import NaoConfig
8
10
  from nao_core.config.repos import RepoConfig
9
11
 
12
+ from ..base import SyncProvider, SyncResult
13
+
10
14
  console = Console()
11
15
 
12
16
 
@@ -76,28 +80,47 @@ def clone_or_pull_repo(repo: RepoConfig, base_path: Path) -> bool:
76
80
  return False
77
81
 
78
82
 
79
- def sync_repositories(repos: list[RepoConfig], base_path: Path) -> int:
80
- """Sync all configured repositories.
83
+ class RepositorySyncProvider(SyncProvider):
84
+ """Provider for syncing git repositories."""
81
85
 
82
- Args:
83
- repos: List of repository configurations
84
- base_path: Base path where repositories are stored
86
+ @property
87
+ def name(self) -> str:
88
+ return "Repositories"
85
89
 
86
- Returns:
87
- Number of successfully synced repositories
88
- """
89
- if not repos:
90
- return 0
90
+ @property
91
+ def emoji(self) -> str:
92
+ return "📦"
93
+
94
+ @property
95
+ def default_output_dir(self) -> str:
96
+ return "repos"
97
+
98
+ def get_items(self, config: NaoConfig) -> list[RepoConfig]:
99
+ return config.repos
100
+
101
+ def sync(self, items: list[Any], output_path: Path, project_path: Path | None = None) -> SyncResult:
102
+ """Sync all configured repositories.
103
+
104
+ Args:
105
+ items: List of repository configurations
106
+ output_path: Base path where repositories are stored
107
+ project_path: Path to the nao project root (unused for repos)
108
+
109
+ Returns:
110
+ SyncResult with number of successfully synced repositories
111
+ """
112
+ if not items:
113
+ return SyncResult(provider_name=self.name, items_synced=0)
91
114
 
92
- base_path.mkdir(parents=True, exist_ok=True)
93
- success_count = 0
115
+ output_path.mkdir(parents=True, exist_ok=True)
116
+ success_count = 0
94
117
 
95
- console.print("\n[bold cyan]📦 Syncing Repositories[/bold cyan]")
96
- console.print(f"[dim]Location:[/dim] {base_path.absolute()}\n")
118
+ console.print(f"\n[bold cyan]{self.emoji} Syncing {self.name}[/bold cyan]")
119
+ console.print(f"[dim]Location:[/dim] {output_path.absolute()}\n")
97
120
 
98
- for repo in repos:
99
- if clone_or_pull_repo(repo, base_path):
100
- success_count += 1
101
- console.print(f" [green]✓[/green] {repo.name}")
121
+ for repo in items:
122
+ if clone_or_pull_repo(repo, output_path):
123
+ success_count += 1
124
+ console.print(f" [green]✓[/green] {repo.name}")
102
125
 
103
- return success_count
126
+ return SyncResult(provider_name=self.name, items_synced=success_count)
@@ -6,6 +6,7 @@ from .databases import (
6
6
  DatabaseType,
7
7
  DatabricksConfig,
8
8
  DuckDBConfig,
9
+ PostgresConfig,
9
10
  SnowflakeConfig,
10
11
  )
11
12
  from .exceptions import InitError
@@ -20,6 +21,7 @@ __all__ = [
20
21
  "DuckDBConfig",
21
22
  "DatabricksConfig",
22
23
  "SnowflakeConfig",
24
+ "PostgresConfig",
23
25
  "DatabaseType",
24
26
  "LLMConfig",
25
27
  "LLMProvider",
nao_core/config/base.py CHANGED
@@ -1,5 +1,8 @@
1
+ import os
2
+ import re
1
3
  from pathlib import Path
2
4
 
5
+ import dotenv
3
6
  import yaml
4
7
  from ibis import BaseBackend
5
8
  from pydantic import BaseModel, Field, model_validator
@@ -9,6 +12,8 @@ from .llm import LLMConfig
9
12
  from .repos import RepoConfig
10
13
  from .slack import SlackConfig
11
14
 
15
+ dotenv.load_dotenv()
16
+
12
17
 
13
18
  class NaoConfig(BaseModel):
14
19
  """nao project configuration."""
@@ -43,8 +48,9 @@ class NaoConfig(BaseModel):
43
48
  def load(cls, path: Path) -> "NaoConfig":
44
49
  """Load the configuration from a YAML file."""
45
50
  config_file = path / "nao_config.yaml"
46
- with config_file.open() as f:
47
- data = yaml.safe_load(f)
51
+ content = config_file.read_text()
52
+ content = cls._process_env_vars(content)
53
+ data = yaml.safe_load(content)
48
54
  return cls.model_validate(data)
49
55
 
50
56
  def get_connection(self, name: str) -> BaseBackend:
@@ -63,11 +69,14 @@ class NaoConfig(BaseModel):
63
69
  """Try to load config from path, returns None if not found or invalid.
64
70
 
65
71
  Args:
66
- path: Directory containing nao_config.yaml. Defaults to current directory.
72
+ path: Directory containing nao_config.yaml. Defaults to NAO_DEFAULT_PROJECT_PATH
73
+ environment variable if set, otherwise current directory.
67
74
  """
68
75
  if path is None:
69
- path = Path.cwd()
76
+ default_path = os.environ.get("NAO_DEFAULT_PROJECT_PATH")
77
+ path = Path(default_path) if default_path else Path.cwd()
70
78
  try:
79
+ os.chdir(path)
71
80
  return cls.load(path)
72
81
  except (FileNotFoundError, ValueError, yaml.YAMLError):
73
82
  return None
@@ -76,3 +85,13 @@ class NaoConfig(BaseModel):
76
85
  def json_schema(cls) -> dict:
77
86
  """Generate JSON schema for the configuration."""
78
87
  return cls.model_json_schema()
88
+
89
+ @staticmethod
90
+ def _process_env_vars(content: str) -> str:
91
+ regex = re.compile(r"\$\{\{\s*env\(['\"]([^'\"]+)['\"]\)\s*\}\}")
92
+
93
+ def replacer(match: re.Match[str]) -> str:
94
+ env_var = match.group(1)
95
+ return os.environ.get(env_var, "")
96
+
97
+ return regex.sub(replacer, content)
@@ -6,6 +6,7 @@ from .base import AccessorType, DatabaseConfig, DatabaseType
6
6
  from .bigquery import BigQueryConfig
7
7
  from .databricks import DatabricksConfig
8
8
  from .duckdb import DuckDBConfig
9
+ from .postgres import PostgresConfig
9
10
  from .snowflake import SnowflakeConfig
10
11
 
11
12
  # =============================================================================
@@ -18,6 +19,7 @@ AnyDatabaseConfig = Annotated[
18
19
  Annotated[DatabricksConfig, Tag("databricks")],
19
20
  Annotated[SnowflakeConfig, Tag("snowflake")],
20
21
  Annotated[DuckDBConfig, Tag("duckdb")],
22
+ Annotated[PostgresConfig, Tag("postgres")],
21
23
  ],
22
24
  Discriminator("type"),
23
25
  ]
@@ -34,6 +36,8 @@ def parse_database_config(data: dict) -> DatabaseConfig:
34
36
  return DatabricksConfig.model_validate(data)
35
37
  elif db_type == "snowflake":
36
38
  return SnowflakeConfig.model_validate(data)
39
+ elif db_type == "postgres":
40
+ return PostgresConfig.model_validate(data)
37
41
  else:
38
42
  raise ValueError(f"Unknown database type: {db_type}")
39
43
 
@@ -47,4 +51,5 @@ __all__ = [
47
51
  "DatabaseType",
48
52
  "DatabricksConfig",
49
53
  "SnowflakeConfig",
54
+ "PostgresConfig",
50
55
  ]
@@ -16,6 +16,7 @@ class DatabaseType(str, Enum):
16
16
  DUCKDB = "duckdb"
17
17
  DATABRICKS = "databricks"
18
18
  SNOWFLAKE = "snowflake"
19
+ POSTGRES = "postgres"
19
20
 
20
21
 
21
22
  class AccessorType(str, Enum):
@@ -0,0 +1,78 @@
1
+ from typing import Literal
2
+
3
+ import ibis
4
+ from ibis import BaseBackend
5
+ from pydantic import Field
6
+ from rich.prompt import Prompt
7
+
8
+ from nao_core.config.exceptions import InitError
9
+
10
+ from .base import DatabaseConfig, console
11
+
12
+
13
+ class PostgresConfig(DatabaseConfig):
14
+ """PostgreSQL-specific configuration."""
15
+
16
+ type: Literal["postgres"] = "postgres"
17
+ host: str = Field(description="PostgreSQL host")
18
+ port: int = Field(default=5432, description="PostgreSQL port")
19
+ database: str = Field(description="Database name")
20
+ user: str = Field(description="Username")
21
+ password: str = Field(description="Password")
22
+ schema_name: str | None = Field(default=None, description="Default schema (optional, uses 'public' if not set)")
23
+
24
+ @classmethod
25
+ def promptConfig(cls) -> "PostgresConfig":
26
+ """Interactively prompt the user for PostgreSQL configuration."""
27
+ console.print("\n[bold cyan]PostgreSQL Configuration[/bold cyan]\n")
28
+
29
+ name = Prompt.ask("[bold]Connection name[/bold]", default="postgres-prod")
30
+
31
+ host = Prompt.ask("[bold]Host[/bold]", default="localhost")
32
+
33
+ port = Prompt.ask("[bold]Port[/bold]", default="5432")
34
+ if not port.isdigit():
35
+ raise InitError("Port must be a valid integer.")
36
+
37
+ database = Prompt.ask("[bold]Database name[/bold]")
38
+ if not database:
39
+ raise InitError("Database name cannot be empty.")
40
+
41
+ user = Prompt.ask("[bold]Username[/bold]")
42
+ if not user:
43
+ raise InitError("Username cannot be empty.")
44
+
45
+ password = Prompt.ask("[bold]Password[/bold]", password=True)
46
+
47
+ schema_name = Prompt.ask(
48
+ "[bold]Default schema[/bold] [dim](optional, uses 'public' if empty)[/dim]",
49
+ default="",
50
+ )
51
+
52
+ return PostgresConfig(
53
+ name=name,
54
+ host=host,
55
+ port=int(port),
56
+ database=database,
57
+ user=user,
58
+ password=password,
59
+ schema_name=schema_name or None,
60
+ )
61
+
62
+ def connect(self) -> BaseBackend:
63
+ """Create an Ibis PostgreSQL connection."""
64
+
65
+ kwargs: dict = {
66
+ "host": self.host,
67
+ "port": self.port,
68
+ "database": self.database,
69
+ "user": self.user,
70
+ "password": self.password,
71
+ }
72
+
73
+ if self.schema_name:
74
+ kwargs["schema"] = self.schema_name
75
+
76
+ return ibis.postgres.connect(
77
+ **kwargs,
78
+ )
@@ -0,0 +1,12 @@
1
+ """Template engine module for nao providers.
2
+
3
+ This module provides a Jinja2-based templating system that allows users
4
+ to customize the output of sync providers (databases, repos, etc.).
5
+
6
+ Default templates are stored in this package and can be overridden by
7
+ placing templates with the same name in the project's `templates/` directory.
8
+ """
9
+
10
+ from .engine import TemplateEngine, get_template_engine
11
+
12
+ __all__ = ["TemplateEngine", "get_template_engine"]
@@ -0,0 +1,23 @@
1
+ {#
2
+ Template: columns.md.j2
3
+ Description: Generates column documentation for a database table
4
+
5
+ Available variables:
6
+ - table_name (str): Name of the table
7
+ - dataset (str): Schema/dataset name
8
+ - columns (list): List of column dictionaries with:
9
+ - name (str): Column name
10
+ - type (str): Data type
11
+ - nullable (bool): Whether the column allows nulls
12
+ - description (str|None): Column description if available
13
+ - column_count (int): Total number of columns
14
+ #}
15
+ # {{ table_name }}
16
+
17
+ **Dataset:** `{{ dataset }}`
18
+
19
+ ## Columns ({{ column_count }})
20
+
21
+ {% for col in columns %}
22
+ - {{ col.name }} ({{ col.type }}{% if col.description %}, "{{ col.description | truncate_middle(256) }}"{% endif %})
23
+ {% endfor %}
@@ -0,0 +1,32 @@
1
+ {#
2
+ Template: description.md.j2
3
+ Description: Generates table metadata and description documentation
4
+
5
+ Available variables:
6
+ - table_name (str): Name of the table
7
+ - dataset (str): Schema/dataset name
8
+ - row_count (int): Total number of rows in the table
9
+ - column_count (int): Number of columns in the table
10
+ - description (str|None): Table description if available
11
+ - columns (list): List of column dictionaries with:
12
+ - name (str): Column name
13
+ - type (str): Data type
14
+ #}
15
+ # {{ table_name }}
16
+
17
+ **Dataset:** `{{ dataset }}`
18
+
19
+ ## Table Metadata
20
+
21
+ | Property | Value |
22
+ |----------|-------|
23
+ | **Row Count** | {{ "{:,}".format(row_count) }} |
24
+ | **Column Count** | {{ column_count }} |
25
+
26
+ ## Description
27
+
28
+ {% if description %}
29
+ {{ description }}
30
+ {% else %}
31
+ _No description available._
32
+ {% endif %}
@@ -0,0 +1,22 @@
1
+ {#
2
+ Template: preview.md.j2
3
+ Description: Generates a preview of table rows in JSONL format
4
+
5
+ Available variables:
6
+ - table_name (str): Name of the table
7
+ - dataset (str): Schema/dataset name
8
+ - rows (list): List of row dictionaries (first N rows of the table)
9
+ - row_count (int): Number of preview rows shown
10
+ - columns (list): List of column dictionaries with:
11
+ - name (str): Column name
12
+ - type (str): Data type
13
+ #}
14
+ # {{ table_name }} - Preview
15
+
16
+ **Dataset:** `{{ dataset }}`
17
+
18
+ ## Rows ({{ row_count }})
19
+
20
+ {% for row in rows %}
21
+ - {{ row | to_json }}
22
+ {% endfor %}
@@ -0,0 +1,34 @@
1
+ {#
2
+ Template: profiling.md.j2
3
+ Description: Generates column-level statistics and profiling data
4
+
5
+ Available variables:
6
+ - table_name (str): Name of the table
7
+ - dataset (str): Schema/dataset name
8
+ - column_stats (list): List of column statistics dictionaries with:
9
+ - name (str): Column name
10
+ - type (str): Data type
11
+ - null_count (int): Number of null values
12
+ - unique_count (int): Number of unique values
13
+ - min_value (str|None): Minimum value (for numeric/temporal columns)
14
+ - max_value (str|None): Maximum value (for numeric/temporal columns)
15
+ - error (str|None): Error message if stats couldn't be computed
16
+ - columns (list): List of column dictionaries with:
17
+ - name (str): Column name
18
+ - type (str): Data type
19
+ #}
20
+ # {{ table_name }} - Profiling
21
+
22
+ **Dataset:** `{{ dataset }}`
23
+
24
+ ## Column Statistics
25
+
26
+ | Column | Type | Nulls | Unique | Min | Max |
27
+ |--------|------|-------|--------|-----|-----|
28
+ {% for stat in column_stats %}
29
+ {% if stat.error %}
30
+ | `{{ stat.name }}` | `{{ stat.type }}` | Error: {{ stat.error }} | | | |
31
+ {% else %}
32
+ | `{{ stat.name }}` | `{{ stat.type }}` | {{ "{:,}".format(stat.null_count) }} | {{ "{:,}".format(stat.unique_count) }} | {{ stat.min_value or "" }} | {{ stat.max_value or "" }} |
33
+ {% endif %}
34
+ {% endfor %}