nao-core 0.0.30__py3-none-any.whl → 0.0.31__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.
- nao_core/__init__.py +1 -1
- nao_core/bin/fastapi/main.py +6 -0
- nao_core/bin/migrations-postgres/0005_add_project_tables.sql +39 -0
- nao_core/bin/migrations-postgres/meta/0005_snapshot.json +1129 -0
- nao_core/bin/migrations-postgres/meta/_journal.json +7 -0
- nao_core/bin/migrations-sqlite/0005_add_project_tables.sql +38 -0
- nao_core/bin/migrations-sqlite/meta/0005_snapshot.json +1086 -0
- nao_core/bin/migrations-sqlite/meta/_journal.json +7 -0
- nao_core/bin/nao-chat-server +0 -0
- nao_core/bin/public/assets/{code-block-F6WJLWQG-z4zcca7w.js → code-block-F6WJLWQG-TAi8koem.js} +1 -1
- nao_core/bin/public/assets/index-BfHcd9Xz.css +1 -0
- nao_core/bin/public/assets/{index-DhhS7iVA.js → index-Mzo9bkag.js} +256 -172
- nao_core/bin/public/index.html +2 -2
- nao_core/commands/chat.py +11 -10
- nao_core/commands/init.py +27 -4
- nao_core/commands/sync/__init__.py +40 -21
- nao_core/commands/sync/accessors.py +218 -139
- nao_core/commands/sync/cleanup.py +133 -0
- nao_core/commands/sync/providers/__init__.py +30 -0
- nao_core/commands/sync/providers/base.py +87 -0
- nao_core/commands/sync/providers/databases/__init__.py +17 -0
- nao_core/commands/sync/providers/databases/bigquery.py +78 -0
- nao_core/commands/sync/providers/databases/databricks.py +79 -0
- nao_core/commands/sync/providers/databases/duckdb.py +83 -0
- nao_core/commands/sync/providers/databases/postgres.py +78 -0
- nao_core/commands/sync/providers/databases/provider.py +123 -0
- nao_core/commands/sync/providers/databases/snowflake.py +78 -0
- nao_core/commands/sync/providers/repositories/__init__.py +5 -0
- nao_core/commands/sync/{repositories.py → providers/repositories/provider.py} +43 -20
- nao_core/config/__init__.py +2 -0
- nao_core/config/base.py +23 -4
- nao_core/config/databases/__init__.py +5 -0
- nao_core/config/databases/base.py +1 -0
- nao_core/config/databases/postgres.py +78 -0
- nao_core/templates/__init__.py +12 -0
- nao_core/templates/defaults/databases/columns.md.j2 +23 -0
- nao_core/templates/defaults/databases/description.md.j2 +32 -0
- nao_core/templates/defaults/databases/preview.md.j2 +22 -0
- nao_core/templates/defaults/databases/profiling.md.j2 +34 -0
- nao_core/templates/engine.py +133 -0
- {nao_core-0.0.30.dist-info → nao_core-0.0.31.dist-info}/METADATA +6 -2
- nao_core-0.0.31.dist-info/RECORD +86 -0
- nao_core/bin/public/assets/index-ClduEZSo.css +0 -1
- nao_core/commands/sync/databases.py +0 -374
- nao_core-0.0.30.dist-info/RECORD +0 -65
- {nao_core-0.0.30.dist-info → nao_core-0.0.31.dist-info}/WHEEL +0 -0
- {nao_core-0.0.30.dist-info → nao_core-0.0.31.dist-info}/entry_points.txt +0 -0
- {nao_core-0.0.30.dist-info → nao_core-0.0.31.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
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
"""Repository
|
|
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
|
-
|
|
80
|
-
"""
|
|
83
|
+
class RepositorySyncProvider(SyncProvider):
|
|
84
|
+
"""Provider for syncing git repositories."""
|
|
81
85
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
86
|
+
@property
|
|
87
|
+
def name(self) -> str:
|
|
88
|
+
return "Repositories"
|
|
85
89
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
93
|
-
|
|
115
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
116
|
+
success_count = 0
|
|
94
117
|
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
126
|
+
return SyncResult(provider_name=self.name, items_synced=success_count)
|
nao_core/config/__init__.py
CHANGED
|
@@ -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
|
-
|
|
47
|
-
|
|
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
|
|
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
|
-
|
|
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
|
]
|
|
@@ -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 %}
|