nao-core 0.0.9__py3-none-any.whl → 0.0.11__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 CHANGED
@@ -1,3 +1,2 @@
1
1
  # nao Core CLI package
2
- __version__ = "0.0.9"
3
-
2
+ __version__ = "0.0.11"
@@ -1,5 +1,6 @@
1
1
  from nao_core.commands.chat import chat
2
+ from nao_core.commands.debug import debug
2
3
  from nao_core.commands.init import init
4
+ from nao_core.commands.sync import sync
3
5
 
4
- __all__ = ["chat", "init"]
5
-
6
+ __all__ = ["chat", "debug", "init", "sync"]
nao_core/commands/chat.py CHANGED
@@ -16,125 +16,117 @@ SERVER_PORT = 5005
16
16
 
17
17
 
18
18
  def get_server_binary_path() -> Path:
19
- """Get the path to the bundled nao-chat-server binary."""
20
- # The binary is in the bin folder relative to this file
21
- cli_dir = Path(__file__).parent.parent
22
- bin_dir = cli_dir / "bin"
23
- binary_path = bin_dir / "nao-chat-server"
19
+ """Get the path to the bundled nao-chat-server binary."""
20
+ # The binary is in the bin folder relative to this file
21
+ cli_dir = Path(__file__).parent.parent
22
+ bin_dir = cli_dir / "bin"
23
+ binary_path = bin_dir / "nao-chat-server"
24
24
 
25
- if not binary_path.exists():
26
- console.print(
27
- f"[bold red]✗[/bold red] Server binary not found at {binary_path}"
28
- )
29
- console.print(
30
- "[dim]Make sure you've built the server with ./scripts/build-server.sh[/dim]"
31
- )
32
- sys.exit(1)
25
+ if not binary_path.exists():
26
+ console.print(f"[bold red]✗[/bold red] Server binary not found at {binary_path}")
27
+ console.print("[dim]Make sure you've built the server with ./scripts/build-server.sh[/dim]")
28
+ sys.exit(1)
33
29
 
34
- return binary_path
30
+ return binary_path
35
31
 
36
32
 
37
33
  def wait_for_server(port: int, timeout: int = 30) -> bool:
38
- """Wait for the server to be ready."""
39
- import socket
40
-
41
- for _ in range(timeout * 10): # Check every 100ms
42
- try:
43
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
44
- sock.settimeout(0.1)
45
- result = sock.connect_ex(("localhost", port))
46
- if result == 0:
47
- return True
48
- except OSError:
49
- pass
50
- sleep(0.1)
51
- return False
34
+ """Wait for the server to be ready."""
35
+ import socket
36
+
37
+ for _ in range(timeout * 10): # Check every 100ms
38
+ try:
39
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
40
+ sock.settimeout(0.1)
41
+ result = sock.connect_ex(("localhost", port))
42
+ if result == 0:
43
+ return True
44
+ except OSError:
45
+ pass
46
+ sleep(0.1)
47
+ return False
52
48
 
53
49
 
54
50
  def chat():
55
- """Start the nao chat UI.
56
-
57
- Launches the nao chat server and opens the web interface in your browser.
58
- """
59
- console.print("\n[bold cyan]💬 Starting nao chat...[/bold cyan]\n")
60
-
61
- binary_path = get_server_binary_path()
62
- bin_dir = binary_path.parent
63
-
64
- console.print(f"[dim]Server binary: {binary_path}[/dim]")
65
- console.print(f"[dim]Working directory: {bin_dir}[/dim]")
66
-
67
- # Try to load nao config from current directory
68
- config = NaoConfig.try_load()
69
- if config:
70
- console.print(f"[bold green]✓[/bold green] Loaded config from {Path.cwd() / 'nao_config.yaml'}")
71
- else:
72
- console.print("[dim]No nao_config.yaml found in current directory[/dim]")
73
-
74
- # Start the server process
75
- process = None
76
- try:
77
- # Set up environment - inherit from parent but ensure we're in the bin dir
78
- # so the server can find the public folder
79
- env = os.environ.copy()
80
-
81
- # Set LLM API key from config if available
82
- if config and config.llm:
83
- env_var_name = f"{config.llm.model.upper()}_API_KEY"
84
- env[env_var_name] = config.llm.api_key
85
- console.print(f"[bold green]✓[/bold green] Set {env_var_name} from config")
86
-
87
- process = subprocess.Popen(
88
- [str(binary_path)],
89
- cwd=str(bin_dir),
90
- env=env,
91
- stdout=subprocess.PIPE,
92
- stderr=subprocess.STDOUT,
93
- text=True,
94
- )
95
-
96
- console.print("[bold green]✓[/bold green] Server starting...")
97
-
98
- # Wait for the server to be ready
99
- if wait_for_server(SERVER_PORT):
100
- url = f"http://localhost:{SERVER_PORT}"
101
- console.print(f"[bold green]✓[/bold green] Server ready at {url}")
102
- console.print("\n[bold]Opening browser...[/bold]")
103
- webbrowser.open(url)
104
- console.print(
105
- "\n[dim]Press Ctrl+C to stop the server[/dim]\n"
106
- )
107
- else:
108
- console.print(
109
- "[bold yellow]⚠[/bold yellow] Server is taking longer than expected to start..."
110
- )
111
- console.print(f"[dim]Check http://localhost:{SERVER_PORT} manually[/dim]")
112
-
113
- # Stream server output to console
114
- if process.stdout:
115
- for line in process.stdout:
116
- # Filter out some of the verbose logging if needed
117
- console.print(f"[dim]{line.rstrip()}[/dim]")
118
-
119
- # Wait for process to complete
120
- process.wait()
121
-
122
- except KeyboardInterrupt:
123
- console.print("\n[bold yellow]Shutting down...[/bold yellow]")
124
- if process:
125
- # Send SIGTERM for graceful shutdown
126
- process.terminate()
127
- try:
128
- process.wait(timeout=5)
129
- except subprocess.TimeoutExpired:
130
- # Force kill if it doesn't respond
131
- process.kill()
132
- process.wait()
133
- console.print("[bold green][/bold green] Server stopped")
134
- sys.exit(0)
135
-
136
- except Exception as e:
137
- console.print(f"[bold red]✗[/bold red] Failed to start server: {e}")
138
- if process:
139
- process.kill()
140
- sys.exit(1)
51
+ """Start the nao chat UI.
52
+
53
+ Launches the nao chat server and opens the web interface in your browser.
54
+ """
55
+ console.print("\n[bold cyan]💬 Starting nao chat...[/bold cyan]\n")
56
+
57
+ binary_path = get_server_binary_path()
58
+ bin_dir = binary_path.parent
59
+
60
+ console.print(f"[dim]Server binary: {binary_path}[/dim]")
61
+ console.print(f"[dim]Working directory: {bin_dir}[/dim]")
62
+
63
+ # Try to load nao config from current directory
64
+ config = NaoConfig.try_load()
65
+ if config:
66
+ console.print(f"[bold green]✓[/bold green] Loaded config from {Path.cwd() / 'nao_config.yaml'}")
67
+ else:
68
+ console.print("[dim]No nao_config.yaml found in current directory[/dim]")
69
+
70
+ # Start the server process
71
+ process = None
72
+ try:
73
+ # Set up environment - inherit from parent but ensure we're in the bin dir
74
+ # so the server can find the public folder
75
+ env = os.environ.copy()
76
+
77
+ # Set LLM API key from config if available
78
+ if config and config.llm:
79
+ env_var_name = f"{config.llm.provider.upper()}_API_KEY"
80
+ env[env_var_name] = config.llm.api_key
81
+ console.print(f"[bold green]✓[/bold green] Set {env_var_name} from config")
82
+
83
+ process = subprocess.Popen(
84
+ [str(binary_path)],
85
+ cwd=str(bin_dir),
86
+ env=env,
87
+ stdout=subprocess.PIPE,
88
+ stderr=subprocess.STDOUT,
89
+ text=True,
90
+ )
91
+
92
+ console.print("[bold green]✓[/bold green] Server starting...")
93
+
94
+ # Wait for the server to be ready
95
+ if wait_for_server(SERVER_PORT):
96
+ url = f"http://localhost:{SERVER_PORT}"
97
+ console.print(f"[bold green]✓[/bold green] Server ready at {url}")
98
+ console.print("\n[bold]Opening browser...[/bold]")
99
+ webbrowser.open(url)
100
+ console.print("\n[dim]Press Ctrl+C to stop the server[/dim]\n")
101
+ else:
102
+ console.print("[bold yellow]⚠[/bold yellow] Server is taking longer than expected to start...")
103
+ console.print(f"[dim]Check http://localhost:{SERVER_PORT} manually[/dim]")
104
+
105
+ # Stream server output to console
106
+ if process.stdout:
107
+ for line in process.stdout:
108
+ # Filter out some of the verbose logging if needed
109
+ console.print(f"[dim]{line.rstrip()}[/dim]")
110
+
111
+ # Wait for process to complete
112
+ process.wait()
113
+
114
+ except KeyboardInterrupt:
115
+ console.print("\n[bold yellow]Shutting down...[/bold yellow]")
116
+ if process:
117
+ # Send SIGTERM for graceful shutdown
118
+ process.terminate()
119
+ try:
120
+ process.wait(timeout=5)
121
+ except subprocess.TimeoutExpired:
122
+ # Force kill if it doesn't respond
123
+ process.kill()
124
+ process.wait()
125
+ console.print("[bold green]✓[/bold green] Server stopped")
126
+ sys.exit(0)
127
+
128
+ except Exception as e:
129
+ console.print(f"[bold red][/bold red] Failed to start server: {e}")
130
+ if process:
131
+ process.kill()
132
+ sys.exit(1)
@@ -0,0 +1,144 @@
1
+ import sys
2
+
3
+ from rich.console import Console
4
+ from rich.table import Table
5
+
6
+ from nao_core.config import NaoConfig
7
+
8
+ console = Console()
9
+
10
+
11
+ def test_database_connection(db_config) -> tuple[bool, str]:
12
+ """Test connectivity to a database.
13
+
14
+ Returns:
15
+ Tuple of (success, message)
16
+ """
17
+ try:
18
+ conn = db_config.connect()
19
+ # Run a simple query to verify the connection works
20
+ if db_config.dataset_id:
21
+ # If dataset is specified, list tables in that dataset
22
+ tables = conn.list_tables()
23
+ table_count = len(tables)
24
+ return True, f"Connected successfully ({table_count} tables found)"
25
+ else:
26
+ # If no dataset, list datasets in the project instead
27
+ datasets = conn.list_databases()
28
+ dataset_count = len(datasets)
29
+ return True, f"Connected successfully ({dataset_count} datasets found)"
30
+ except Exception as e:
31
+ return False, str(e)
32
+
33
+
34
+ def test_llm_connection(llm_config) -> tuple[bool, str]:
35
+ """Test connectivity to an LLM provider.
36
+
37
+ Returns:
38
+ Tuple of (success, message)
39
+ """
40
+ try:
41
+ if llm_config.provider.value == "openai":
42
+ import openai
43
+
44
+ client = openai.OpenAI(api_key=llm_config.api_key)
45
+ # Make a minimal API call to verify the key works
46
+ models = client.models.list()
47
+ # Just check we can iterate (don't need to consume all)
48
+ model_count = sum(1 for _ in models)
49
+ return True, f"Connected successfully ({model_count} models available)"
50
+ else:
51
+ return False, f"Unknown provider: {llm_config.provider}"
52
+ except Exception as e:
53
+ return False, str(e)
54
+
55
+
56
+ def debug():
57
+ """Test connectivity to configured databases and LLMs.
58
+
59
+ Loads the nao configuration from the current directory and tests
60
+ connections to all configured databases and LLM providers.
61
+ """
62
+ console.print("\n[bold cyan]🔍 nao debug - Testing connections...[/bold cyan]\n")
63
+
64
+ # Load config
65
+ config = NaoConfig.try_load()
66
+ if not config:
67
+ console.print("[bold red]✗[/bold red] No nao_config.yaml found in current directory")
68
+ console.print("[dim]Run 'nao init' to create a configuration file[/dim]")
69
+ sys.exit(1)
70
+
71
+ console.print(f"[bold green]✓[/bold green] Loaded config: [cyan]{config.project_name}[/cyan]\n")
72
+
73
+ # Test databases
74
+ if config.databases:
75
+ console.print("[bold]Databases:[/bold]")
76
+ db_table = Table(show_header=True, header_style="bold")
77
+ db_table.add_column("Name")
78
+ db_table.add_column("Type")
79
+ db_table.add_column("Status")
80
+ db_table.add_column("Details")
81
+
82
+ for db in config.databases:
83
+ console.print(f" Testing [cyan]{db.name}[/cyan]...", end=" ")
84
+ success, message = test_database_connection(db)
85
+
86
+ if success:
87
+ console.print("[bold green]✓[/bold green]")
88
+ db_table.add_row(
89
+ db.name,
90
+ db.type,
91
+ "[green]Connected[/green]",
92
+ message,
93
+ )
94
+ else:
95
+ console.print("[bold red]✗[/bold red]")
96
+ # Truncate long error messages
97
+ short_msg = message[:80] + "..." if len(message) > 80 else message
98
+ db_table.add_row(
99
+ db.name,
100
+ db.type,
101
+ "[red]Failed[/red]",
102
+ short_msg,
103
+ )
104
+
105
+ console.print()
106
+ console.print(db_table)
107
+ else:
108
+ console.print("[dim]No databases configured[/dim]")
109
+
110
+ console.print()
111
+
112
+ # Test LLM
113
+ if config.llm:
114
+ console.print("[bold]LLM Provider:[/bold]")
115
+ llm_table = Table(show_header=True, header_style="bold")
116
+ llm_table.add_column("Provider")
117
+ llm_table.add_column("Status")
118
+ llm_table.add_column("Details")
119
+
120
+ console.print(f" Testing [cyan]{config.llm.provider.value}[/cyan]...", end=" ")
121
+ success, message = test_llm_connection(config.llm)
122
+
123
+ if success:
124
+ console.print("[bold green]✓[/bold green]")
125
+ llm_table.add_row(
126
+ config.llm.provider.value,
127
+ "[green]Connected[/green]",
128
+ message,
129
+ )
130
+ else:
131
+ console.print("[bold red]✗[/bold red]")
132
+ short_msg = message[:80] + "..." if len(message) > 80 else message
133
+ llm_table.add_row(
134
+ config.llm.provider.value,
135
+ "[red]Failed[/red]",
136
+ short_msg,
137
+ )
138
+
139
+ console.print()
140
+ console.print(llm_table)
141
+ else:
142
+ console.print("[dim]No LLM configured[/dim]")
143
+
144
+ console.print()
nao_core/commands/init.py CHANGED
@@ -1,72 +1,175 @@
1
1
  from pathlib import Path
2
+ from typing import Annotated
2
3
 
4
+ from cyclopts import Parameter
3
5
  from rich.console import Console
4
6
  from rich.prompt import Confirm, Prompt
5
7
 
6
- from nao_core.config import LLMConfig, LLMProvider, NaoConfig
8
+ from nao_core.config import BigQueryConfig, DatabaseConfig, DatabaseType, LLMConfig, LLMProvider, NaoConfig
7
9
 
8
10
  console = Console()
9
11
 
10
12
 
11
- def init():
12
- """Initialize a new nao project.
13
+ class InitError(Exception):
14
+ """Base exception for init command errors."""
13
15
 
14
- Creates a project folder with a nao_config.yaml configuration file.
15
- """
16
- console.print("\n[bold cyan]🚀 nao project initialization[/bold cyan]\n")
16
+ pass
17
17
 
18
- project_name = Prompt.ask("[bold]Enter your project name[/bold]")
19
18
 
20
- if not project_name:
21
- console.print("[bold red]✗[/bold red] Project name cannot be empty.")
22
- return
19
+ class EmptyProjectNameError(InitError):
20
+ """Raised when project name is empty."""
23
21
 
24
- project_path = Path(project_name)
22
+ def __init__(self):
23
+ super().__init__("Project name cannot be empty.")
25
24
 
26
- if project_path.exists():
27
- console.print(
28
- f"[bold red]✗[/bold red] Folder [yellow]'{project_name}'[/yellow] already exists."
29
- )
30
- return
31
25
 
32
- # LLM Configuration
33
- llm_config = None
34
- setup_llm = Confirm.ask("\n[bold]Set up LLM configuration?[/bold]", default=True)
26
+ class ProjectExistsError(InitError):
27
+ """Raised when project folder already exists."""
35
28
 
36
- if setup_llm:
37
- console.print("\n[bold cyan]LLM Configuration[/bold cyan]\n")
29
+ def __init__(self, project_name: str):
30
+ self.project_name = project_name
31
+ super().__init__(f"Folder '{project_name}' already exists.")
38
32
 
39
- provider_choices = [p.value for p in LLMProvider]
40
- llm_provider = Prompt.ask(
41
- "[bold]Select LLM provider[/bold]",
42
- choices=provider_choices,
43
- default=provider_choices[0],
44
- )
45
33
 
46
- api_key = Prompt.ask(
47
- f"[bold]Enter your {llm_provider.upper()} API key[/bold]",
48
- password=True,
49
- )
34
+ class EmptyApiKeyError(InitError):
35
+ """Raised when API key is empty."""
50
36
 
51
- if not api_key:
52
- console.print("[bold red]✗[/bold red] API key cannot be empty.")
53
- return
37
+ def __init__(self):
38
+ super().__init__("API key cannot be empty.")
54
39
 
55
- llm_config = LLMConfig(
56
- model=LLMProvider(llm_provider),
57
- api_key=api_key,
58
- )
59
40
 
60
- project_path.mkdir(parents=True)
41
+ def setup_project_name(force: bool = False) -> tuple[str, Path]:
42
+ """Setup the project name."""
43
+ project_name = Prompt.ask("[bold]Enter your project name[/bold]")
61
44
 
62
- config = NaoConfig(project_name=project_name, llm=llm_config)
63
- config.save(project_path)
45
+ if not project_name:
46
+ raise EmptyProjectNameError()
64
47
 
65
- console.print()
66
- console.print(f"[bold green]✓[/bold green] Created project [cyan]{project_name}[/cyan]")
67
- console.print(
68
- f"[bold green]✓[/bold green] Created [dim]{project_path / 'nao_config.yaml'}[/dim]"
69
- )
70
- console.print()
71
- console.print("[bold green]Done![/bold green] Your nao project is ready. 🎉")
48
+ project_path = Path(project_name)
72
49
 
50
+ if project_path.exists() and not force:
51
+ raise ProjectExistsError(project_name)
52
+
53
+ project_path.mkdir(parents=True, exist_ok=True)
54
+
55
+ return project_name, project_path
56
+
57
+
58
+ def setup_bigquery() -> BigQueryConfig:
59
+ """Setup a BigQuery database configuration."""
60
+ console.print("\n[bold cyan]BigQuery Configuration[/bold cyan]\n")
61
+
62
+ name = Prompt.ask("[bold]Connection name[/bold]", default="bigquery-prod")
63
+
64
+ project_id = Prompt.ask("[bold]GCP Project ID[/bold]")
65
+ if not project_id:
66
+ raise InitError("GCP Project ID cannot be empty.")
67
+
68
+ dataset_id = Prompt.ask("[bold]Default dataset[/bold] [dim](optional, press Enter to skip)[/dim]", default="")
69
+
70
+ credentials_path = Prompt.ask(
71
+ "[bold]Service account JSON path[/bold] [dim](optional, uses ADC if empty)[/dim]",
72
+ default="",
73
+ )
74
+
75
+ return BigQueryConfig(
76
+ name=name,
77
+ project_id=project_id,
78
+ dataset_id=dataset_id or None,
79
+ credentials_path=credentials_path or None,
80
+ )
81
+
82
+
83
+ def setup_databases() -> list[DatabaseConfig]:
84
+ """Setup database configurations."""
85
+ databases: list[DatabaseConfig] = []
86
+
87
+ should_setup = Confirm.ask("\n[bold]Set up database connections?[/bold]", default=True)
88
+
89
+ if not should_setup:
90
+ return databases
91
+
92
+ while True:
93
+ console.print("\n[bold cyan]Database Configuration[/bold cyan]\n")
94
+
95
+ db_type_choices = [t.value for t in DatabaseType]
96
+ db_type = Prompt.ask(
97
+ "[bold]Select database type[/bold]",
98
+ choices=db_type_choices,
99
+ default=db_type_choices[0],
100
+ )
101
+
102
+ if db_type == DatabaseType.BIGQUERY.value:
103
+ db_config = setup_bigquery()
104
+ databases.append(db_config)
105
+ console.print(f"\n[bold green]✓[/bold green] Added database [cyan]{db_config.name}[/cyan]")
106
+
107
+ add_another = Confirm.ask("\n[bold]Add another database?[/bold]", default=False)
108
+ if not add_another:
109
+ break
110
+
111
+ return databases
112
+
113
+
114
+ def setup_llm() -> LLMConfig | None:
115
+ """Setup the LLM configuration."""
116
+ llm_config = None
117
+ should_setup = Confirm.ask("\n[bold]Set up LLM configuration?[/bold]", default=True)
118
+
119
+ if should_setup:
120
+ console.print("\n[bold cyan]LLM Configuration[/bold cyan]\n")
121
+
122
+ provider_choices = [p.value for p in LLMProvider]
123
+ llm_provider = Prompt.ask(
124
+ "[bold]Select LLM provider[/bold]",
125
+ choices=provider_choices,
126
+ default=provider_choices[0],
127
+ )
128
+
129
+ api_key = Prompt.ask(
130
+ f"[bold]Enter your {llm_provider.upper()} API key[/bold]",
131
+ password=True,
132
+ )
133
+
134
+ if not api_key:
135
+ raise EmptyApiKeyError()
136
+
137
+ llm_config = LLMConfig(
138
+ provider=LLMProvider(llm_provider),
139
+ api_key=api_key,
140
+ )
141
+
142
+ return llm_config
143
+
144
+
145
+ def init(
146
+ *,
147
+ force: Annotated[bool, Parameter(name=["-f", "--force"])] = False,
148
+ ):
149
+ """Initialize a new nao project.
150
+
151
+ Creates a project folder with a nao_config.yaml configuration file.
152
+
153
+ Parameters
154
+ ----------
155
+ force : bool
156
+ Force re-initialization even if the folder already exists.
157
+ """
158
+ console.print("\n[bold cyan]🚀 nao project initialization[/bold cyan]\n")
159
+
160
+ try:
161
+ project_name, project_path = setup_project_name(force=force)
162
+ config = NaoConfig(
163
+ project_name=project_name,
164
+ databases=setup_databases(),
165
+ llm=setup_llm(),
166
+ )
167
+ config.save(project_path)
168
+
169
+ console.print()
170
+ console.print(f"[bold green]✓[/bold green] Created project [cyan]{project_name}[/cyan]")
171
+ console.print(f"[bold green]✓[/bold green] Created [dim]{project_path / 'nao_config.yaml'}[/dim]")
172
+ console.print()
173
+ console.print("[bold green]Done![/bold green] Your nao project is ready. 🎉")
174
+ except InitError as e:
175
+ console.print(f"[bold red]✗[/bold red] {e}")
@@ -0,0 +1,150 @@
1
+ import sys
2
+ from pathlib import Path
3
+
4
+ from rich.console import Console
5
+ from rich.progress import BarColumn, Progress, SpinnerColumn, TaskProgressColumn, TextColumn
6
+
7
+ from nao_core.config import NaoConfig
8
+
9
+ console = Console()
10
+
11
+
12
+ def get_table_schema_markdown(conn, dataset: str, table: str) -> str:
13
+ """Generate markdown content describing a table's columns."""
14
+ try:
15
+ # Get the table reference and its schema
16
+ full_table_name = f"{dataset}.{table}"
17
+ t = conn.table(full_table_name)
18
+ schema = t.schema()
19
+
20
+ lines = [
21
+ f"# {table}",
22
+ "",
23
+ f"**Dataset:** `{dataset}`",
24
+ "",
25
+ "## Columns",
26
+ "",
27
+ "| Column | Type | Nullable |",
28
+ "|--------|------|----------|",
29
+ ]
30
+
31
+ for name, dtype in schema.items():
32
+ nullable = "Yes" if dtype.nullable else "No"
33
+ lines.append(f"| `{name}` | `{dtype}` | {nullable} |")
34
+
35
+ return "\n".join(lines)
36
+ except Exception as e:
37
+ return f"# {table}\n\nError fetching schema: {e}"
38
+
39
+
40
+ def sync_bigquery(db_config, base_path: Path, progress: Progress) -> tuple[int, int]:
41
+ """Sync BigQuery database schema to markdown files.
42
+
43
+ Returns:
44
+ Tuple of (datasets_synced, tables_synced)
45
+ """
46
+ conn = db_config.connect()
47
+ db_path = base_path / "bigquery" / db_config.name
48
+
49
+ datasets_synced = 0
50
+ tables_synced = 0
51
+
52
+ # Get datasets to sync
53
+ if db_config.dataset_id:
54
+ datasets = [db_config.dataset_id]
55
+ else:
56
+ datasets = conn.list_databases()
57
+
58
+ dataset_task = progress.add_task(
59
+ f"[dim]{db_config.name}[/dim]",
60
+ total=len(datasets),
61
+ )
62
+
63
+ for dataset in datasets:
64
+ dataset_path = db_path / dataset
65
+ dataset_path.mkdir(parents=True, exist_ok=True)
66
+ datasets_synced += 1
67
+
68
+ # List tables in this dataset
69
+ try:
70
+ tables = conn.list_tables(database=dataset)
71
+ except Exception:
72
+ progress.update(dataset_task, advance=1)
73
+ continue
74
+
75
+ table_task = progress.add_task(
76
+ f" [cyan]{dataset}[/cyan]",
77
+ total=len(tables),
78
+ )
79
+
80
+ for table in tables:
81
+ table_path = dataset_path / table
82
+ table_path.mkdir(parents=True, exist_ok=True)
83
+
84
+ columns_md = get_table_schema_markdown(conn, dataset, table)
85
+ columns_file = table_path / "columns.md"
86
+ columns_file.write_text(columns_md)
87
+ tables_synced += 1
88
+
89
+ progress.update(table_task, advance=1)
90
+
91
+ progress.update(dataset_task, advance=1)
92
+
93
+ return datasets_synced, tables_synced
94
+
95
+
96
+ def sync(output_dir: str = "databases"):
97
+ """Sync database schemas to local markdown files.
98
+
99
+ Creates a folder structure with table schemas:
100
+ databases/bigquery/<connection>/<dataset>/<table>/columns.md
101
+
102
+ Args:
103
+ output_dir: Output directory for the database schemas (default: "databases")
104
+ """
105
+ console.print("\n[bold cyan]🔄 nao sync[/bold cyan]\n")
106
+
107
+ # Load config
108
+ config = NaoConfig.try_load()
109
+ if not config:
110
+ console.print("[bold red]✗[/bold red] No nao_config.yaml found in current directory")
111
+ console.print("[dim]Run 'nao init' to create a configuration file[/dim]")
112
+ sys.exit(1)
113
+
114
+ console.print(f"[dim]Project:[/dim] {config.project_name}")
115
+
116
+ if not config.databases:
117
+ console.print("[dim]No databases configured[/dim]")
118
+ return
119
+
120
+ base_path = Path(output_dir)
121
+ total_datasets = 0
122
+ total_tables = 0
123
+
124
+ console.print()
125
+
126
+ with Progress(
127
+ SpinnerColumn(style="dim"),
128
+ TextColumn("[progress.description]{task.description}"),
129
+ BarColumn(bar_width=30, style="dim", complete_style="cyan", finished_style="green"),
130
+ TaskProgressColumn(),
131
+ console=console,
132
+ transient=False,
133
+ ) as progress:
134
+ for db in config.databases:
135
+ try:
136
+ if db.type == "bigquery":
137
+ datasets, tables = sync_bigquery(db, base_path, progress)
138
+ total_datasets += datasets
139
+ total_tables += tables
140
+ else:
141
+ console.print(f"[yellow]⚠ Unsupported database type: {db.type}[/yellow]")
142
+ except Exception as e:
143
+ console.print(f"[bold red]✗[/bold red] Failed to sync {db.name}: {e}")
144
+
145
+ console.print()
146
+ console.print(
147
+ f"[green]✓[/green] Synced [bold]{total_tables}[/bold] tables across [bold]{total_datasets}[/bold] datasets"
148
+ )
149
+ console.print(f"[dim] → {base_path.absolute()}[/dim]")
150
+ console.print()
nao_core/config.py CHANGED
@@ -1,64 +1,136 @@
1
1
  from enum import Enum
2
2
  from pathlib import Path
3
+ from typing import Literal
3
4
 
5
+ import ibis
4
6
  import yaml
5
- from pydantic import BaseModel, Field
7
+ from ibis import BaseBackend
8
+ from pydantic import BaseModel, Field, model_validator
6
9
 
7
10
 
8
11
  class LLMProvider(str, Enum):
9
- """Supported LLM providers."""
12
+ """Supported LLM providers."""
10
13
 
11
- OPENAI = "openai"
14
+ OPENAI = "openai"
15
+
16
+
17
+ class DatabaseType(str, Enum):
18
+ """Supported database types."""
19
+
20
+ BIGQUERY = "bigquery"
21
+
22
+
23
+ class BigQueryConfig(BaseModel):
24
+ """BigQuery-specific configuration."""
25
+
26
+ type: Literal["bigquery"] = "bigquery"
27
+ name: str = Field(description="A friendly name for this connection")
28
+ project_id: str = Field(description="GCP project ID")
29
+ dataset_id: str | None = Field(default=None, description="Default BigQuery dataset")
30
+ credentials_path: str | None = Field(
31
+ default=None,
32
+ description="Path to service account JSON file. If not provided, uses Application Default Credentials (ADC)",
33
+ )
34
+
35
+ def connect(self) -> BaseBackend:
36
+ """Create an Ibis BigQuery connection."""
37
+ kwargs: dict = {"project_id": self.project_id}
38
+
39
+ if self.dataset_id:
40
+ kwargs["dataset_id"] = self.dataset_id
41
+
42
+ if self.credentials_path:
43
+ from google.oauth2 import service_account
44
+
45
+ credentials = service_account.Credentials.from_service_account_file(
46
+ self.credentials_path,
47
+ scopes=["https://www.googleapis.com/auth/bigquery"],
48
+ )
49
+ kwargs["credentials"] = credentials
50
+
51
+ return ibis.bigquery.connect(**kwargs)
52
+
53
+
54
+ DatabaseConfig = BigQueryConfig
55
+
56
+
57
+ def parse_database_config(data: dict) -> DatabaseConfig:
58
+ """Parse a database config dict into the appropriate type."""
59
+ db_type = data.get("type")
60
+ if db_type == "bigquery":
61
+ return BigQueryConfig.model_validate(data)
62
+ else:
63
+ raise ValueError(f"Unknown database type: {db_type}")
12
64
 
13
65
 
14
66
  class LLMConfig(BaseModel):
15
- """LLM configuration."""
67
+ """LLM configuration."""
68
+
69
+ provider: LLMProvider = Field(description="The LLM provider to use")
70
+ api_key: str = Field(description="The API key to use")
16
71
 
17
- model: LLMProvider = Field(description="The LLM provider to use")
18
- api_key: str = Field(description="The API key to use")
19
72
 
20
73
  class NaoConfig(BaseModel):
21
- """nao project configuration."""
22
-
23
- project_name: str = Field(description="The name of the nao project")
24
- llm: LLMConfig | None = Field(default=None, description="The LLM configuration")
25
-
26
-
27
- def save(self, path: Path) -> None:
28
- """Save the configuration to a YAML file."""
29
- config_file = path / "nao_config.yaml"
30
- with config_file.open("w") as f:
31
- yaml.dump(
32
- self.model_dump(mode="json"),
33
- f,
34
- default_flow_style=False,
35
- sort_keys=False,
36
- allow_unicode=True,
37
- )
38
-
39
- @classmethod
40
- def load(cls, path: Path) -> "NaoConfig":
41
- """Load the configuration from a YAML file."""
42
- config_file = path / "nao_config.yaml"
43
- with config_file.open() as f:
44
- data = yaml.safe_load(f)
45
- return cls.model_validate(data)
46
-
47
- @classmethod
48
- def try_load(cls, path: Path | None = None) -> "NaoConfig | None":
49
- """Try to load config from path, returns None if not found or invalid.
50
-
51
- Args:
52
- path: Directory containing nao_config.yaml. Defaults to current directory.
53
- """
54
- if path is None:
55
- path = Path.cwd()
56
- try:
57
- return cls.load(path)
58
- except (FileNotFoundError, ValueError, yaml.YAMLError):
59
- return None
60
-
61
- @classmethod
62
- def json_schema(cls) -> dict:
63
- """Generate JSON schema for the configuration."""
64
- return cls.model_json_schema()
74
+ """nao project configuration."""
75
+
76
+ project_name: str = Field(description="The name of the nao project")
77
+ databases: list[BigQueryConfig] = Field(description="The databases to use")
78
+ llm: LLMConfig | None = Field(default=None, description="The LLM configuration")
79
+
80
+ @model_validator(mode="before")
81
+ @classmethod
82
+ def parse_databases(cls, data: dict) -> dict:
83
+ """Parse database configs into their specific types."""
84
+ if "databases" in data and isinstance(data["databases"], list):
85
+ data["databases"] = [parse_database_config(db) if isinstance(db, dict) else db for db in data["databases"]]
86
+ return data
87
+
88
+ def save(self, path: Path) -> None:
89
+ """Save the configuration to a YAML file."""
90
+ config_file = path / "nao_config.yaml"
91
+ with config_file.open("w") as f:
92
+ yaml.dump(
93
+ self.model_dump(mode="json", by_alias=True),
94
+ f,
95
+ default_flow_style=False,
96
+ sort_keys=False,
97
+ allow_unicode=True,
98
+ )
99
+
100
+ @classmethod
101
+ def load(cls, path: Path) -> "NaoConfig":
102
+ """Load the configuration from a YAML file."""
103
+ config_file = path / "nao_config.yaml"
104
+ with config_file.open() as f:
105
+ data = yaml.safe_load(f)
106
+ return cls.model_validate(data)
107
+
108
+ def get_connection(self, name: str) -> BaseBackend:
109
+ """Get an Ibis connection by database name."""
110
+ for db in self.databases:
111
+ if db.name == name:
112
+ return db.connect()
113
+ raise ValueError(f"Database '{name}' not found in configuration")
114
+
115
+ def get_all_connections(self) -> dict[str, BaseBackend]:
116
+ """Get all Ibis connections as a dict keyed by name."""
117
+ return {db.name: db.connect() for db in self.databases}
118
+
119
+ @classmethod
120
+ def try_load(cls, path: Path | None = None) -> "NaoConfig | None":
121
+ """Try to load config from path, returns None if not found or invalid.
122
+
123
+ Args:
124
+ path: Directory containing nao_config.yaml. Defaults to current directory.
125
+ """
126
+ if path is None:
127
+ path = Path.cwd()
128
+ try:
129
+ return cls.load(path)
130
+ except (FileNotFoundError, ValueError, yaml.YAMLError):
131
+ return None
132
+
133
+ @classmethod
134
+ def json_schema(cls) -> dict:
135
+ """Generate JSON schema for the configuration."""
136
+ return cls.model_json_schema()
nao_core/main.py CHANGED
@@ -1,11 +1,13 @@
1
1
  from cyclopts import App
2
2
 
3
- from nao_core.commands import chat, init
3
+ from nao_core.commands import chat, debug, init, sync
4
4
 
5
5
  app = App()
6
6
 
7
7
  app.command(chat)
8
+ app.command(debug)
8
9
  app.command(init)
10
+ app.command(sync)
9
11
 
10
12
  if __name__ == "__main__":
11
- app()
13
+ app()
@@ -0,0 +1,130 @@
1
+ Metadata-Version: 2.4
2
+ Name: nao-core
3
+ Version: 0.0.11
4
+ Summary: nao Core is your analytics context builder with the best chat interface.
5
+ Project-URL: Homepage, https://getnao.io
6
+ Project-URL: Repository, https://github.com/naolabs/chat
7
+ Author: nao Labs
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: ai,analytics,chat
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: MacOS
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: cyclopts>=4.4.4
23
+ Requires-Dist: ibis-framework[bigquery]>=9.0.0
24
+ Requires-Dist: openai>=1.0.0
25
+ Requires-Dist: pydantic>=2.10.0
26
+ Requires-Dist: pyyaml>=6.0.0
27
+ Requires-Dist: rich>=14.0.0
28
+ Description-Content-Type: text/markdown
29
+
30
+ # nao CLI
31
+
32
+ Command-line interface for nao chat.
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ pip install nao-core
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ ```bash
43
+ nao --help
44
+ Usage: nao COMMAND
45
+
46
+ ╭─ Commands ────────────────────────────────────────────────────────────────╮
47
+ │ chat Start the nao chat UI. │
48
+ │ init Initialize a new nao project. │
49
+ │ --help (-h) Display this message and exit. │
50
+ │ --version Display application version. │
51
+ ╰───────────────────────────────────────────────────────────────────────────╯
52
+ ```
53
+
54
+ ### Initialize a new nao project
55
+
56
+ ```bash
57
+ nao init
58
+ ```
59
+
60
+ This will create a new nao project in the current directory. It will prompt you for a project name and ask you if you want to set up an LLM configuration.
61
+
62
+ ### Start the nao chat UI
63
+
64
+ ```bash
65
+ nao chat
66
+ ```
67
+
68
+ This will start the nao chat UI. It will open the chat interface in your browser at `http://localhost:5005`.
69
+
70
+ ## Development
71
+
72
+ ### Building the package
73
+
74
+ ```bash
75
+ cd cli
76
+ python build.py --help
77
+ Usage: build.py [OPTIONS]
78
+
79
+ Build and package nao-core CLI.
80
+
81
+ ╭─ Commands ────────────────────────────────────────────────────────────────────────────────────────────────╮
82
+ │ --help (-h) Display this message and exit. │
83
+ │ --version Display application version. │
84
+ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯
85
+ ╭─ Parameters ──────────────────────────────────────────────────────────────────────────────────────────────╮
86
+ │ --force -f --no-force Force rebuild the server binary [default: False] │
87
+ │ --skip-server -s --no-skip-server Skip server build, only build Python package [default: False] │
88
+ │ --bump Bump version before building (patch, minor, major) [choices: patch, │
89
+ │ minor, major] │
90
+ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯
91
+ ```
92
+
93
+ This will:
94
+ 1. Build the frontend with Vite
95
+ 2. Compile the backend with Bun into a standalone binary
96
+ 3. Bundle everything into a Python wheel in `dist/`
97
+
98
+ Options:
99
+ - `--force` / `-f`: Force rebuild the server binary
100
+ - `--skip-server`: Skip server build, only build Python package
101
+ - `--bump`: Bump version before building (patch, minor, major)
102
+
103
+ ### Installing for development
104
+
105
+ ```bash
106
+ cd cli
107
+ pip install -e .
108
+ ```
109
+
110
+ ### Publishing to PyPI
111
+
112
+ ```bash
113
+ # Build first
114
+ python build.py
115
+
116
+ # Publish
117
+ uv publish dist/*
118
+ ```
119
+
120
+ ## Architecture
121
+
122
+ ```
123
+ nao chat (CLI command)
124
+ ↓ spawns
125
+ nao-chat-server (Bun-compiled binary)
126
+ ↓ serves
127
+ Backend API + Frontend Static Files
128
+
129
+ Browser at http://localhost:5005
130
+ ```
@@ -0,0 +1,20 @@
1
+ nao_core/__init__.py,sha256=OTB7vGhVCdxrovMSyxjPvmW5myiN0jPFano1PGXh6Dc,46
2
+ nao_core/config.py,sha256=uNZ_q6f6IC3i7e8SFu-CXJhcCboNNm3UjnL4KHpw-cw,4527
3
+ nao_core/main.py,sha256=f00vLL4s2B2kCMa8y3lI56LX3TnUppWzYmqM6eOGomA,205
4
+ nao_core/bin/db.sqlite,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ nao_core/bin/nao-chat-server,sha256=zqD16vlL_FKNOrbrRyvaEJGpfwfIOtWgLkvdgfxDP44,63220848
6
+ nao_core/bin/public/favicon.ico,sha256=PRD32mxgMXg0AIFmjErFs66XQ8qaJiqw_NMS-7n0i90,3870
7
+ nao_core/bin/public/index.html,sha256=NTU3lowrcs_NGlGdrPulJb7k9vmfbHTm55HKk9SfVV0,673
8
+ nao_core/bin/public/assets/index-BSxC58nD.js,sha256=KfT52WftnDWjgEEplssDzkzOPgCR_1jaXnczs_Gn3bk,178296
9
+ nao_core/bin/public/assets/index-Dh3br3Ia.js,sha256=UlskObLoJLwNKErNk6DrjrBdsbemOJ9BT16_ejD5234,333150
10
+ nao_core/bin/public/assets/index-heKLHGGE.css,sha256=9tGsJsaCXrJ0-GQptxyrmuQhX4rWcUNLvWmR8n0-5vY,30119
11
+ nao_core/commands/__init__.py,sha256=rFCuUyA9wM4hS6CxCt2M_5W4VysyQXh6HkA_hyRCxPc,207
12
+ nao_core/commands/chat.py,sha256=xhcBfP40IedunfLX73VNDM5VOGBDnqqii4khoVE-imY,4430
13
+ nao_core/commands/debug.py,sha256=Hdtb6_2F-a9Nr-aU_exdnPZY0-Lf9dgV1QX6QxPr9P8,4794
14
+ nao_core/commands/init.py,sha256=lL4Zn8KIhgH2hRTFOBuQIHpiReSeOF517J4s4zBdwEk,5175
15
+ nao_core/commands/sync.py,sha256=P_AHbTwIifmvhvAJuYR3zA9gdBD2QbaONHQArzS4nXc,4654
16
+ nao_core-0.0.11.dist-info/METADATA,sha256=XWK2ujf5mNjTE0aEjG2FYforbf8nE8PI6GtwLtbYiaI,5143
17
+ nao_core-0.0.11.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
18
+ nao_core-0.0.11.dist-info/entry_points.txt,sha256=SZakIiNybgS3pl_OEZVLyLSweadeBFoEMBECMoj9czY,42
19
+ nao_core-0.0.11.dist-info/licenses/LICENSE,sha256=rn5YtWB6E5hPQI49tCTNSyqlArWGsB6HzA5FfSbRHRs,1066
20
+ nao_core-0.0.11.dist-info/RECORD,,
@@ -1,93 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: nao-core
3
- Version: 0.0.9
4
- Summary: nao Core is your analytics context builder with the best chat interface.
5
- Project-URL: Homepage, https://github.com/naolabs/chat
6
- Project-URL: Repository, https://github.com/naolabs/chat
7
- Author: nao Labs
8
- License-Expression: MIT
9
- License-File: LICENSE
10
- Keywords: ai,analytics,chat
11
- Classifier: Development Status :: 3 - Alpha
12
- Classifier: Environment :: Console
13
- Classifier: Intended Audience :: Developers
14
- Classifier: License :: OSI Approved :: MIT License
15
- Classifier: Operating System :: MacOS
16
- Classifier: Programming Language :: Python :: 3
17
- Classifier: Programming Language :: Python :: 3.10
18
- Classifier: Programming Language :: Python :: 3.11
19
- Classifier: Programming Language :: Python :: 3.12
20
- Classifier: Programming Language :: Python :: 3.13
21
- Requires-Python: >=3.10
22
- Requires-Dist: cyclopts>=4.4.4
23
- Requires-Dist: pydantic>=2.10.0
24
- Requires-Dist: pyyaml>=6.0.0
25
- Requires-Dist: rich>=14.0.0
26
- Description-Content-Type: text/markdown
27
-
28
- # nao CLI
29
-
30
- Command-line interface for nao chat.
31
-
32
- ## Installation
33
-
34
- ```bash
35
- pip install nao-core
36
- ```
37
-
38
- ## Usage
39
-
40
- ### Start the chat interface
41
-
42
- ```bash
43
- nao chat
44
- ```
45
-
46
- This will start the nao chat server and open the web interface in your browser at `http://localhost:5005`.
47
-
48
- ## Development
49
-
50
- ### Building the package
51
-
52
- ```bash
53
- cd cli
54
- python build.py
55
- ```
56
-
57
- This will:
58
- 1. Build the frontend with Vite
59
- 2. Compile the backend with Bun into a standalone binary
60
- 3. Bundle everything into a Python wheel in `dist/`
61
-
62
- Options:
63
- - `--force` / `-f`: Force rebuild the server binary
64
- - `--skip-server`: Skip server build, only build Python package
65
-
66
- ### Installing for development
67
-
68
- ```bash
69
- cd cli
70
- pip install -e .
71
- ```
72
-
73
- ### Publishing to PyPI
74
-
75
- ```bash
76
- # Build first
77
- python build.py
78
-
79
- # Publish
80
- uv publish dist/*
81
- ```
82
-
83
- ## Architecture
84
-
85
- ```
86
- nao chat (CLI command)
87
- ↓ spawns
88
- nao-chat-server (Bun-compiled binary)
89
- ↓ serves
90
- Backend API + Frontend Static Files
91
-
92
- Browser at http://localhost:5005
93
- ```
@@ -1,18 +0,0 @@
1
- nao_core/__init__.py,sha256=u4cYroaAmhjQLYOiejO2wTSrfGh4JcAAqucY_38BWw4,46
2
- nao_core/config.py,sha256=O6H-qnsv6cLeNdyMmI4gBivLk3mlbJGM2xaF6kMgOlE,1644
3
- nao_core/main.py,sha256=4RbHIM9lZOZoZKAJhibPGUI_ZaE1FaBgN4YDhgF2mpU,152
4
- nao_core/bin/db.sqlite,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- nao_core/bin/nao-chat-server,sha256=zqD16vlL_FKNOrbrRyvaEJGpfwfIOtWgLkvdgfxDP44,63220848
6
- nao_core/bin/public/favicon.ico,sha256=PRD32mxgMXg0AIFmjErFs66XQ8qaJiqw_NMS-7n0i90,3870
7
- nao_core/bin/public/index.html,sha256=NTU3lowrcs_NGlGdrPulJb7k9vmfbHTm55HKk9SfVV0,673
8
- nao_core/bin/public/assets/index-BSxC58nD.js,sha256=KfT52WftnDWjgEEplssDzkzOPgCR_1jaXnczs_Gn3bk,178296
9
- nao_core/bin/public/assets/index-Dh3br3Ia.js,sha256=UlskObLoJLwNKErNk6DrjrBdsbemOJ9BT16_ejD5234,333150
10
- nao_core/bin/public/assets/index-heKLHGGE.css,sha256=9tGsJsaCXrJ0-GQptxyrmuQhX4rWcUNLvWmR8n0-5vY,30119
11
- nao_core/commands/__init__.py,sha256=d6Aq8CSfgpfsFkyT2i7IywC1guB853UH5RYw2MvjelE,109
12
- nao_core/commands/chat.py,sha256=iuG2R_v3XBg04AEjUDyxSQbOaznRmWWujXe4FNVBKOo,3856
13
- nao_core/commands/init.py,sha256=howdDyAAZ-Ox3Betmqk6C2TfuUBWEwkEUk-Nm40gcTY,1866
14
- nao_core-0.0.9.dist-info/METADATA,sha256=6khBkxOY8G745XqHHJwplPtvX721t3oY9Ve1tMRp9dU,1971
15
- nao_core-0.0.9.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
16
- nao_core-0.0.9.dist-info/entry_points.txt,sha256=SZakIiNybgS3pl_OEZVLyLSweadeBFoEMBECMoj9czY,42
17
- nao_core-0.0.9.dist-info/licenses/LICENSE,sha256=rn5YtWB6E5hPQI49tCTNSyqlArWGsB6HzA5FfSbRHRs,1066
18
- nao_core-0.0.9.dist-info/RECORD,,