chatsbom 0.2.7__py3-none-any.whl → 0.2.9__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.
chatsbom/commands/chat.py CHANGED
@@ -2,6 +2,7 @@
2
2
  import asyncio
3
3
  import json
4
4
  import os
5
+ from contextlib import suppress
5
6
  from dataclasses import dataclass
6
7
  from dataclasses import field
7
8
  from datetime import datetime
@@ -127,6 +128,12 @@ class ChatSBOMApp(App):
127
128
  self._update_status()
128
129
 
129
130
  async def on_mount(self) -> None:
131
+ """Initialize the Claude Agent SDK client."""
132
+ import tempfile
133
+ stderr_file = tempfile.NamedTemporaryFile(
134
+ mode='w', delete=False, suffix='.log',
135
+ )
136
+
130
137
  opts = ClaudeAgentOptions(
131
138
  disallowed_tools=[
132
139
  'Read', 'Write', 'Edit',
@@ -139,9 +146,24 @@ class ChatSBOMApp(App):
139
146
  ),
140
147
  },
141
148
  system_prompt=SYSTEM_PROMPT,
149
+ env={
150
+ k: v for k, v in [
151
+ ('ANTHROPIC_BASE_URL', os.getenv('ANTHROPIC_BASE_URL')),
152
+ ] if v
153
+ },
154
+ debug_stderr=stderr_file,
142
155
  )
156
+
143
157
  self.client = ClaudeSDKClient(options=opts)
144
- await self.client.__aenter__()
158
+ try:
159
+ await self.client.__aenter__()
160
+ except Exception as e:
161
+ self._handle_init_error(e, stderr_file.name)
162
+ raise
163
+ finally:
164
+ stderr_file.close()
165
+ with suppress(OSError):
166
+ os.unlink(stderr_file.name)
145
167
 
146
168
  log = self.query_one('#log', RichLog)
147
169
  log.write('[bold green]ChatSBOM Agent[/] - Query examples:')
@@ -150,6 +172,33 @@ class ChatSBOMApp(App):
150
172
  self.query_one('#loading').add_class('hidden')
151
173
  self._update_status()
152
174
 
175
+ def _handle_init_error(self, error: Exception, stderr_path: str) -> None:
176
+ """Display initialization error with context."""
177
+ from rich.console import Console
178
+ from rich.panel import Panel
179
+ from pathlib import Path
180
+
181
+ lines = [
182
+ f'[red]{error}[/red]',
183
+ f'[dim]{type(error).__name__}[/dim]',
184
+ ]
185
+
186
+ if stderr := Path(stderr_path).read_text().strip():
187
+ lines += ['', '[yellow]stderr:[/yellow]', f'[dim]{stderr}[/dim]']
188
+
189
+ if os.geteuid() == 0:
190
+ lines += ['', '[yellow]⚠ Cannot use bypassPermissions as root. Run as non-root user.[/yellow]']
191
+
192
+ if url := os.getenv('ANTHROPIC_BASE_URL'):
193
+ lines += ['', f'[dim]API: {url}[/dim]']
194
+
195
+ Console().print(
196
+ Panel(
197
+ '\n'.join(lines),
198
+ title='[red]Init Failed[/red]', border_style='red',
199
+ ),
200
+ )
201
+
153
202
  async def on_unmount(self) -> None:
154
203
  if self.client:
155
204
  try:
@@ -159,6 +159,8 @@ class Storage:
159
159
  self.filepath = filepath
160
160
  self.visited_ids: set[int] = set()
161
161
  self.min_stars_seen: float = float('inf')
162
+ # Ensure parent directories exist
163
+ os.makedirs(os.path.dirname(self.filepath), exist_ok=True)
162
164
  self._load_existing()
163
165
 
164
166
  def _load_existing(self):
@@ -134,10 +134,10 @@ def convert_project(project_dir: Path, output_format: str, overwrite: bool) -> C
134
134
 
135
135
  def main(
136
136
  input_dir: str = typer.Option(
137
- 'data', help='Root data directory',
137
+ 'data/sbom', help='Root data directory',
138
138
  ),
139
139
  concurrency: int = typer.Option(
140
- 4, help='Number of concurrent syft processes',
140
+ 16, help='Number of concurrent syft processes',
141
141
  ),
142
142
  output_format: str = typer.Option(
143
143
  'json', '--format', help='Syft output format (json, spdx-json, cyclonedx-json)',
@@ -163,10 +163,10 @@ def load_targets(jsonl_path: str) -> list[dict]:
163
163
 
164
164
  def main(
165
165
  input_file: str | None = typer.Option(
166
- None, help='Input JSONL file path (default: {language}.jsonl)',
166
+ None, help='Input JSONL file path (default: data/github/{language}.jsonl)',
167
167
  ),
168
168
  output_dir: str = typer.Option(
169
- 'data', help='Download destination directory',
169
+ 'data/sbom', help='Download destination directory',
170
170
  ),
171
171
  language: Language | None = typer.Option(
172
172
  None, help='Target Language (default: all)',
@@ -220,7 +220,7 @@ def main(
220
220
  if input_file:
221
221
  target_file = input_file
222
222
  else:
223
- target_file = f"{lang}.jsonl"
223
+ target_file = f"data/github/{lang}.jsonl"
224
224
 
225
225
  # Check if file exists, if not, skip efficiently
226
226
  if not os.path.exists(target_file):
@@ -119,8 +119,8 @@ def scan_artifacts(meta_context: dict[str, Any]) -> list[list[Any]]:
119
119
  repo = meta_context['repo']
120
120
  repo_id = meta_context['id']
121
121
 
122
- # Expected path: data/{language}/{owner}/{repo}/**/sbom.json
123
- base_dir = Path('data') / language / owner / repo
122
+ # Expected path: data/sbom/{language}/{owner}/{repo}/**/sbom.json
123
+ base_dir = Path('data/sbom') / language / owner / repo
124
124
  if not base_dir.exists():
125
125
  return []
126
126
 
@@ -302,7 +302,7 @@ def main(
302
302
  else:
303
303
  langs_to_process = language if language else list(Language)
304
304
  for lang in langs_to_process:
305
- f = Path(f"{lang.value}.jsonl")
305
+ f = Path(f"data/github/{lang.value}.jsonl")
306
306
  if f.exists():
307
307
  files_to_process.append(f)
308
308
  else:
@@ -1,4 +1,6 @@
1
1
  """ClickHouse connection utilities."""
2
+ import socket
3
+
2
4
  import clickhouse_connect
3
5
  import typer
4
6
  from rich.console import Console
@@ -14,85 +16,129 @@ def check_clickhouse_connection(
14
16
  require_database: bool = True,
15
17
  ) -> bool:
16
18
  """
17
- Check ClickHouse connection and optionally verify database existence.
18
-
19
- Args:
20
- host: ClickHouse host
21
- port: ClickHouse HTTP port
22
- user: ClickHouse username
23
- password: ClickHouse password
24
- database: Database to check
25
- console: Rich console for output (creates one if None)
26
- require_database: If True, check that the database exists
27
-
28
- Returns:
29
- True if connection (and database) check passed, False otherwise
30
-
31
- Raises:
32
- typer.Exit: If connection fails
19
+ Check ClickHouse connection with multi-step validation.
20
+
21
+ Steps:
22
+ 1. Network - is the server reachable?
23
+ 2. Authentication - are credentials valid?
24
+ 3. Database - does it exist and is it accessible?
25
+ 4. Tables - do required tables exist?
33
26
  """
34
- if console is None:
35
- console = Console()
27
+ console = console or Console()
28
+
29
+ if not _check_network(host, port, console):
30
+ raise typer.Exit(1)
31
+
32
+ if not _check_auth(host, port, user, password, console):
33
+ raise typer.Exit(1)
34
+
35
+ if not require_database:
36
+ return True
37
+
38
+ if not _check_database(host, port, user, password, database, console):
39
+ raise typer.Exit(1)
40
+
41
+ if not _check_tables(host, port, user, password, database, console):
42
+ raise typer.Exit(1)
43
+
44
+ return True
36
45
 
37
- # Step 1: Test basic connectivity
46
+
47
+ def _check_network(host: str, port: int, console: Console) -> bool:
48
+ """Step 1: Check network connectivity."""
49
+ try:
50
+ with socket.create_connection((host, port), timeout=5):
51
+ return True
52
+ except TimeoutError:
53
+ console.print(
54
+ f'[bold red]Error:[/] Connection to [cyan]{host}:{port}[/] timed out.\n\n'
55
+ '[green]Solution:[/] [cyan]docker compose up -d[/]',
56
+ )
57
+ except OSError as e:
58
+ console.print(
59
+ f'[bold red]Error:[/] Cannot reach [cyan]{host}:{port}[/]\n'
60
+ f'[dim]{e}[/dim]\n\n'
61
+ '[green]Solution:[/] [cyan]docker compose up -d[/]',
62
+ )
63
+ return False
64
+
65
+
66
+ def _check_auth(host: str, port: int, user: str, password: str, console: Console) -> bool:
67
+ """Step 2: Check authentication."""
38
68
  try:
39
69
  client = clickhouse_connect.get_client(
40
- host=host,
41
- port=port,
42
- username=user,
43
- password=password,
44
- database='default',
70
+ host=host, port=port, username=user, password=password, database='default',
45
71
  )
46
72
  client.query('SELECT 1')
73
+ return True
47
74
  except Exception as e:
48
- console.print(
49
- f'[bold red]Error:[/] Failed to connect to ClickHouse at '
50
- f'[cyan]{host}:{port}[/]\n\n'
51
- f'Details: {e}\n\n'
52
- 'Please ensure ClickHouse is running:\n\n'
53
- '[bold]Option 1:[/] Use docker compose\n'
54
- ' [cyan]docker compose up -d[/]\n\n'
55
- '[bold]Option 2:[/] Use docker run\n'
56
- ' Step 1: Start ClickHouse\n'
57
- ' [cyan]docker run --rm -d --name clickhouse \\\n'
58
- ' -p 8123:8123 -p 9000:9000 \\\n'
59
- ' -e CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1 \\\n'
60
- ' -v ./database:/var/lib/clickhouse \\\n'
61
- ' clickhouse/clickhouse-server[/]\n\n'
62
- ' Step 2: Create admin and guest users\n'
63
- ' [cyan]docker exec -it clickhouse clickhouse-client -q \\\n'
64
- " \"CREATE USER IF NOT EXISTS admin IDENTIFIED BY 'admin'\"[/]\n"
65
- ' [cyan]docker exec -it clickhouse clickhouse-client -q \\\n'
66
- " \"CREATE USER IF NOT EXISTS guest IDENTIFIED BY 'guest'\"[/]\n"
67
- ' [cyan]docker exec -it clickhouse clickhouse-client -q \\\n'
68
- " \"GRANT CURRENT GRANTS ON *.* TO admin WITH GRANT OPTION\"[/]\n"
69
- ' [cyan]docker exec -it clickhouse clickhouse-client -q \\\n'
70
- " \"GRANT SELECT ON *.* TO guest\"[/]\n",
71
- )
72
- raise typer.Exit(1)
75
+ err = str(e).lower()
76
+ if any(x in err for x in ['authentication', 'password', 'denied', 'incorrect']):
77
+ console.print(
78
+ f'[bold red]Error:[/] Authentication failed for [cyan]{user}[/]\n\n'
79
+ '[green]Solution:[/] Create user:\n'
80
+ f' [cyan]docker exec clickhouse clickhouse-client -q \\\n'
81
+ f' "CREATE USER IF NOT EXISTS {user} IDENTIFIED BY \'<password>\'"[/]',
82
+ )
83
+ else:
84
+ console.print(f'[bold red]Error:[/] Auth failed: [dim]{e}[/dim]')
85
+ return False
86
+
73
87
 
74
- # Step 2: Check database exists (if required)
75
- if require_database:
76
- try:
77
- result = client.query(
78
- 'SELECT name FROM system.databases WHERE name = {db:String}',
79
- parameters={'db': database},
88
+ def _check_database(
89
+ host: str, port: int, user: str, password: str, database: str, console: Console,
90
+ ) -> bool:
91
+ """Step 3: Check database access."""
92
+ try:
93
+ client = clickhouse_connect.get_client(
94
+ host=host, port=port, username=user, password=password, database=database,
95
+ )
96
+ client.query('SELECT 1')
97
+ return True
98
+ except Exception as e:
99
+ err = str(e).lower()
100
+ if 'unknown database' in err:
101
+ console.print(
102
+ f'[bold red]Error:[/] Database [cyan]{database}[/] does not exist.\n\n'
103
+ '[green]Solution:[/] [cyan]chatsbom index --language go[/]',
80
104
  )
81
- if not result.result_rows:
82
- console.print(
83
- f'[bold red]Error:[/] Database [cyan]{database}[/] '
84
- 'does not exist.\n\n'
85
- 'Please run the import command first to create and '
86
- 'populate the database:\n\n'
87
- ' [cyan]chatsbom index[/]',
88
- )
89
- raise typer.Exit(1)
90
- except typer.Exit:
91
- raise
92
- except Exception as e:
105
+ elif any(x in err for x in ['access', 'denied', 'grant', 'not allowed']):
93
106
  console.print(
94
- f'[bold red]Error:[/] Failed to check database: {e}',
107
+ f'[bold red]Error:[/] User [cyan]{user}[/] cannot access [cyan]{database}[/]\n\n'
108
+ '[green]Solution:[/] Grant access:\n'
109
+ f' [cyan]docker exec clickhouse clickhouse-client -q \\\n'
110
+ f' "GRANT SELECT ON {database}.* TO {user}"[/]\n\n'
111
+ '[dim]Or update config/clickhouse/users.d/guest.xml[/dim]',
95
112
  )
96
- raise typer.Exit(1)
113
+ else:
114
+ console.print(
115
+ f'[bold red]Error:[/] Cannot access [cyan]{database}[/]: [dim]{e}[/dim]',
116
+ )
117
+ return False
97
118
 
98
- return True
119
+
120
+ def _check_tables(
121
+ host: str, port: int, user: str, password: str, database: str, console: Console,
122
+ ) -> bool:
123
+ """Step 4: Check required tables exist."""
124
+ required = {'repositories', 'artifacts'}
125
+
126
+ try:
127
+ client = clickhouse_connect.get_client(
128
+ host=host, port=port, username=user, password=password, database=database,
129
+ )
130
+ result = client.query('SHOW TABLES')
131
+ existing = {row[0] for row in result.result_rows}
132
+
133
+ if missing := required - existing:
134
+ console.print(
135
+ f'[bold red]Error:[/] Missing tables: [cyan]{", ".join(sorted(missing))}[/]\n\n'
136
+ '[green]Solution:[/] [cyan]chatsbom index --language go[/]',
137
+ )
138
+ return False
139
+ return True
140
+ except Exception as e:
141
+ console.print(
142
+ f'[bold red]Error:[/] Cannot check tables: [dim]{e}[/dim]',
143
+ )
144
+ return False
chatsbom/core/client.py CHANGED
@@ -1,4 +1,5 @@
1
1
  from datetime import timedelta
2
+ from pathlib import Path
2
3
 
3
4
  import requests
4
5
  import requests_cache
@@ -10,7 +11,7 @@ logger = structlog.get_logger('client')
10
11
 
11
12
 
12
13
  def get_http_client(
13
- cache_name: str = 'http_cache',
14
+ cache_name: str = 'data/http/cache.sqlite3',
14
15
  expire_after: int = 86400, # 24 hours
15
16
  retries: int = 3,
16
17
  pool_size: int = 50,
@@ -19,6 +20,10 @@ def get_http_client(
19
20
  Returns a requests session with caching and retry logic.
20
21
  """
21
22
 
23
+ # Ensure the data directory exists
24
+ cache_path = Path(cache_name)
25
+ cache_path.parent.mkdir(parents=True, exist_ok=True)
26
+
22
27
  # Configure Caching
23
28
  # We want to cache 200 OK and 404 Not Found (negative caching)
24
29
  session = requests_cache.CachedSession(
chatsbom/core/config.py CHANGED
@@ -11,12 +11,12 @@ class PathConfig:
11
11
  """File path configuration."""
12
12
 
13
13
  # Base directories
14
- data_dir: Path = field(default_factory=lambda: Path('data'))
14
+ data_dir: Path = field(default_factory=lambda: Path('data/sbom'))
15
15
  output_dir: Path = field(default_factory=lambda: Path('.'))
16
16
 
17
17
  # File naming conventions
18
18
  sbom_filename: str = 'sbom.json'
19
- repo_list_pattern: str = '{language}.jsonl'
19
+ repo_list_pattern: str = 'data/github/{language}.jsonl'
20
20
 
21
21
  def get_repo_list_path(self, language: str) -> Path:
22
22
  """Get the path for repository list file."""
@@ -64,7 +64,7 @@ def validate_download_structure(data_dir: Path, language: str) -> bool:
64
64
  Validate download directory structure.
65
65
 
66
66
  Expected structure:
67
- data/{language}/{owner}/{repo}/{branch}/[files]
67
+ data/sbom/{language}/{owner}/{repo}/{branch}/[files]
68
68
 
69
69
  Returns:
70
70
  True if valid
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chatsbom
3
- Version: 0.2.7
3
+ Version: 0.2.9
4
4
  Summary: ChatSBOM - Talk to your Supply Chain. Chat with SBOMs.
5
5
  Requires-Python: >=3.12
6
6
  Requires-Dist: claude-agent-sdk>=0.1.0
@@ -95,6 +95,7 @@ ChatSBOM follows a clean, modular architecture with high cohesion and low coupli
95
95
  collect → download → convert → index → status/query/chat
96
96
  ↓ ↓ ↓ ↓
97
97
  .jsonl files/ sbom.json database
98
+ (github/) (sbom/) (sbom/) (clickhouse/)
98
99
  ```
99
100
 
100
101
  ### Core Modules
@@ -2,24 +2,24 @@ chatsbom/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  chatsbom/__main__.py,sha256=QVs4sO1YUqJt642_Gw4wyiDBwQ5YeL4I9v_xEDkTPdM,1228
3
3
  chatsbom/__version__.py,sha256=JodF7HbCCwUPFhWltShEeNCAWHPTwRcPvSm7qWfMTCQ,411
4
4
  chatsbom/commands/__init__.py,sha256=vOL_QwoyFQhEIfs-rf0LVieAjStfkqlhqkjaZ_1p7gQ,15
5
- chatsbom/commands/chat.py,sha256=_X0bgXKIKTLAR-UKry0guCeRtnh08Bou5io5sTnYHBc,11236
6
- chatsbom/commands/collect.py,sha256=ApweQztO889o4X4lBrxlIwdYorDUSiE01M4-XMO0HUM,16536
7
- chatsbom/commands/convert.py,sha256=etc6s_tT4VAvjOCZs8dnHppbTYEeAHI6kHJikkUWD64,8249
8
- chatsbom/commands/download.py,sha256=I9TwEXO1zrh9-DwvUUMh4iZ1W5ZCXbtE4jcJiK2KqnY,10218
9
- chatsbom/commands/index.py,sha256=DlK5FtwOFoJqy9gA6mYvfuVQLL9S15KRrH7_Qw7l0Fk,10633
5
+ chatsbom/commands/chat.py,sha256=jmToyhbg_yoKgX-yRf60Gl3u4oy00A1BzQDO0ktVoi8,12835
6
+ chatsbom/commands/collect.py,sha256=dPTX0o9thBeVhiGVRJWrRYQSxo5gZZ72EI2UVCuvbH8,16645
7
+ chatsbom/commands/convert.py,sha256=qIERQed1Lky2XZ0cjA8N9G3BHIChj_0-ogxUBWD1nyU,8255
8
+ chatsbom/commands/download.py,sha256=4UCyQTaj2NBKBhyvsCkSuezW0kc6VFHQTI4hZLRMxug,10247
9
+ chatsbom/commands/index.py,sha256=ycUBCdi01-l5fzgW0S67nhVdkpPyBiagmRUFy0VpQ-Q,10655
10
10
  chatsbom/commands/query.py,sha256=bIlz4vXClbcOkoHCLnjqmfcXpMrDzEPGNhmltZe5SHk,5709
11
11
  chatsbom/commands/status.py,sha256=AsesBz3F_Mz30n3By2ZMQ0P1kUJPEJkyBHqh3mx1qFA,8553
12
12
  chatsbom/core/__init__.py,sha256=x8qFrlBTgFn7TkmBN11ysPCUzRWBY350c5AN4u-3dWE,17
13
- chatsbom/core/clickhouse.py,sha256=sw4gWiaC2BE4QJlVWDsoDULKFB6YslB0W1FM630IY94,3530
14
- chatsbom/core/client.py,sha256=GSnOOAxoSUKU1pLXPX4W3cM5G4Iv7fQnknW-YkcX43Q,1400
15
- chatsbom/core/config.py,sha256=K4vDe387iVwwMY9aTDbDLlUPVOHCKLcA5ZMK000MQgk,3992
13
+ chatsbom/core/clickhouse.py,sha256=rk2Hs6KfuAD2hYq-N6YV_Ufzrf49JtXGUiNk97JLXdk,4973
14
+ chatsbom/core/client.py,sha256=gSiRbf-PEClu9KVoWNxS4L6WQlRuPpg-dyfBa57iSK4,1569
15
+ chatsbom/core/config.py,sha256=wMD9oja8CBUNqwwOHUTFzbvS0bPzN6Fq7Hbi__ZsE-8,4009
16
16
  chatsbom/core/repository.py,sha256=0LqBwlal5PxkhnAcTwtP0eLIMNKCjAYQNKIybjUTAPs,9837
17
17
  chatsbom/core/schema.py,sha256=oZUNNjS3UerZtIMo9-KOL-2E3XwS8FT9JNQStWq4tYE,1267
18
- chatsbom/core/validation.py,sha256=v4boiht4hK73nuZNYpzLTkZKsNSoFPMUDIX-8sP2E9g,3927
18
+ chatsbom/core/validation.py,sha256=m1123AzUFSVFAuqJO6y5j4Q3DWPCKj70ApBbI5yJ9rg,3932
19
19
  chatsbom/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
20
  chatsbom/models/framework.py,sha256=Whlsbnn64YwftMifMEMVvhsWADC9uupBV4QN1i1BoNQ,2833
21
21
  chatsbom/models/language.py,sha256=RgbBQZfG630uNZ7w-BtB1LZ5B5Q_nEExGrwUadfpRqE,3601
22
- chatsbom-0.2.7.dist-info/METADATA,sha256=--56Qd-BYDIvcJIvbypG5jGM8Hch2Zd0XcnRn6LDaCY,4115
23
- chatsbom-0.2.7.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
24
- chatsbom-0.2.7.dist-info/entry_points.txt,sha256=906Ig6u2FwWk3ftNi4mth03N1NRgP4-B2p9kpppcqWA,51
25
- chatsbom-0.2.7.dist-info/RECORD,,
22
+ chatsbom-0.2.9.dist-info/METADATA,sha256=zuW5BOx3dige_d1dKTNtr1XnS23PLw-1bz2ofPewYz8,4161
23
+ chatsbom-0.2.9.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
24
+ chatsbom-0.2.9.dist-info/entry_points.txt,sha256=906Ig6u2FwWk3ftNi4mth03N1NRgP4-B2p9kpppcqWA,51
25
+ chatsbom-0.2.9.dist-info/RECORD,,