chatsbom 0.2.8__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/collect.py +2 -0
- chatsbom/commands/convert.py +2 -2
- chatsbom/commands/download.py +3 -3
- chatsbom/commands/index.py +3 -3
- chatsbom/core/clickhouse.py +116 -70
- chatsbom/core/client.py +6 -1
- chatsbom/core/config.py +2 -2
- chatsbom/core/validation.py +1 -1
- {chatsbom-0.2.8.dist-info → chatsbom-0.2.9.dist-info}/METADATA +2 -1
- {chatsbom-0.2.8.dist-info → chatsbom-0.2.9.dist-info}/RECORD +12 -12
- {chatsbom-0.2.8.dist-info → chatsbom-0.2.9.dist-info}/WHEEL +0 -0
- {chatsbom-0.2.8.dist-info → chatsbom-0.2.9.dist-info}/entry_points.txt +0 -0
chatsbom/commands/collect.py
CHANGED
|
@@ -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):
|
chatsbom/commands/convert.py
CHANGED
|
@@ -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
|
-
|
|
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)',
|
chatsbom/commands/download.py
CHANGED
|
@@ -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):
|
chatsbom/commands/index.py
CHANGED
|
@@ -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:
|
chatsbom/core/clickhouse.py
CHANGED
|
@@ -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
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
'
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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:[/]
|
|
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
|
-
|
|
113
|
+
else:
|
|
114
|
+
console.print(
|
|
115
|
+
f'[bold red]Error:[/] Cannot access [cyan]{database}[/]: [dim]{e}[/dim]',
|
|
116
|
+
)
|
|
117
|
+
return False
|
|
97
118
|
|
|
98
|
-
|
|
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 = '
|
|
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."""
|
chatsbom/core/validation.py
CHANGED
|
@@ -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.
|
|
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
|
|
@@ -3,23 +3,23 @@ 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
5
|
chatsbom/commands/chat.py,sha256=jmToyhbg_yoKgX-yRf60Gl3u4oy00A1BzQDO0ktVoi8,12835
|
|
6
|
-
chatsbom/commands/collect.py,sha256=
|
|
7
|
-
chatsbom/commands/convert.py,sha256=
|
|
8
|
-
chatsbom/commands/download.py,sha256=
|
|
9
|
-
chatsbom/commands/index.py,sha256=
|
|
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=
|
|
14
|
-
chatsbom/core/client.py,sha256=
|
|
15
|
-
chatsbom/core/config.py,sha256=
|
|
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=
|
|
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.
|
|
23
|
-
chatsbom-0.2.
|
|
24
|
-
chatsbom-0.2.
|
|
25
|
-
chatsbom-0.2.
|
|
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,,
|
|
File without changes
|
|
File without changes
|