|
import dash |
|
from dash import dcc, html, Input, Output, State |
|
import dash_bootstrap_components as dbc |
|
from dash.exceptions import PreventUpdate |
|
import requests |
|
import base64 |
|
import json |
|
import google.generativeai as genai |
|
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type |
|
import threading |
|
import os |
|
from io import BytesIO |
|
|
|
|
|
GITHUB_TOKEN = os.environ.get('GITHUB_TOKEN') |
|
GEMINI_API_KEY = os.environ.get('GEMINI_API_KEY') |
|
|
|
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP]) |
|
|
|
|
|
generated_file = None |
|
|
|
def fetch_git_files(git_url, personal_access_token, git_provider): |
|
try: |
|
|
|
parts = git_url.split('/') |
|
owner = parts[3] |
|
repo = parts[4].split('.git')[0] |
|
branch = 'main' |
|
|
|
|
|
dependency_files = [ |
|
'requirements.txt', 'package.json', 'Gemfile', 'pom.xml', |
|
'build.gradle', 'composer.json', 'Cargo.toml', 'go.mod', 'Pipfile' |
|
] |
|
|
|
all_content = "" |
|
|
|
|
|
headers = { |
|
"Authorization": f"token {personal_access_token}", |
|
"Accept": "application/vnd.github.v3+json" |
|
} |
|
|
|
base_url = { |
|
'GitHub': 'https://api.github.com', |
|
'GitLab': 'https://gitlab.com/api/v4', |
|
'Gitea': 'https://gitea.com/api/v1' |
|
}.get(git_provider) |
|
|
|
for file_path in dependency_files: |
|
|
|
if git_provider == 'GitHub': |
|
api_url = f"{base_url}/repos/{owner}/{repo}/contents/{file_path}?ref={branch}" |
|
elif git_provider == 'GitLab': |
|
api_url = f"{base_url}/projects/{owner}%2F{repo}/repository/files/{file_path}/raw?ref={branch}" |
|
elif git_provider == 'Gitea': |
|
api_url = f"{base_url}/repos/{owner}/{repo}/contents/{file_path}?ref={branch}" |
|
|
|
|
|
response = requests.get(api_url, headers=headers) |
|
|
|
if response.status_code == 200: |
|
if git_provider == 'GitHub' or git_provider == 'Gitea': |
|
content = response.json() |
|
if isinstance(content, dict) and 'content' in content: |
|
file_content = base64.b64decode(content['content']).decode('utf-8') |
|
all_content += f"\n\n--- {file_path} ---\n{file_content}" |
|
elif git_provider == 'GitLab': |
|
file_content = response.text |
|
all_content += f"\n\n--- {file_path} ---\n{file_content}" |
|
|
|
if not all_content: |
|
return "Error: No dependency files found in the repository." |
|
|
|
return all_content |
|
except requests.exceptions.RequestException as e: |
|
return f"Error accessing {git_provider}: {str(e)}" |
|
except json.JSONDecodeError: |
|
return f"Error: Unable to parse {git_provider} API response for {file_path}" |
|
|
|
def process_chunk_with_gemini(chunk, gemini_api_key): |
|
genai.configure(api_key=gemini_api_key) |
|
model = genai.GenerativeModel('gemini-2.5-pro-preview-03-25') |
|
|
|
prompt = f""" |
|
Analyze the following file content for open-source license information: |
|
|
|
{chunk} |
|
|
|
Please provide: |
|
1. A numbered list with the name dependency and version as the title |
|
2. 1st bullet under title has a brief summary of what the dependency does |
|
3. 2nd bullet under title has the license name |
|
4. 3rd bullet under title has a hyperlink to the license file |
|
5. Provide no other information such as greeting or summary as the purpose is to catalog and document all open source licenses used. |
|
""" |
|
|
|
try: |
|
response = model.generate_content(prompt) |
|
return response.text |
|
except Exception as e: |
|
print(f"Error processing chunk: {str(e)}") |
|
return f"Error processing chunk: {str(e)}" |
|
|
|
@retry(stop=stop_after_attempt(5), wait=wait_exponential(multiplier=1, min=4, max=10), retry=retry_if_exception_type(Exception)) |
|
def process_with_gemini(file_content, gemini_api_key): |
|
chunk_size = 2000 |
|
chunks = [file_content[i:i+chunk_size] for i in range(0, len(file_content), chunk_size)] |
|
|
|
results = [] |
|
for chunk in chunks: |
|
try: |
|
result = process_chunk_with_gemini(chunk, gemini_api_key) |
|
results.append(result) |
|
except Exception as e: |
|
print(f"Error processing chunk: {str(e)}") |
|
results.append(f"Error processing chunk: {str(e)}") |
|
|
|
combined_result = "\n\n".join(results) |
|
return combined_result |
|
|
|
def process_input(git_url, personal_access_token, git_provider): |
|
global generated_file |
|
generated_file = None |
|
|
|
if not git_url.startswith(f"https://{git_provider.lower()}.com/"): |
|
return f"Error: Invalid {git_provider} URL. Please use the format: https://{git_provider.lower()}.com/username/repository.git" |
|
if not personal_access_token.strip(): |
|
return "Error: Personal Access Token is empty. Please provide a valid token." |
|
|
|
file_content = fetch_git_files(git_url, personal_access_token, git_provider) |
|
if file_content.startswith("Error:"): |
|
return file_content |
|
|
|
try: |
|
|
|
analysis = process_with_gemini(file_content, GEMINI_API_KEY) |
|
generated_file = analysis.encode() |
|
return "Analysis complete. Click the download button to get the results." |
|
except Exception as e: |
|
return f"Error processing the files: {str(e)}" |
|
|
|
app.layout = dbc.Container([ |
|
html.H1("Open Source License Extractor", className="my-4"), |
|
html.P("Provide a Git repository URL to analyze open-source licenses from dependency files.", className="mb-4"), |
|
dbc.Card([ |
|
dbc.CardBody([ |
|
dbc.Row([ |
|
dbc.Col([ |
|
dcc.Dropdown( |
|
id='git-provider', |
|
options=[ |
|
{'label': 'GitHub', 'value': 'GitHub'}, |
|
{'label': 'GitLab', 'value': 'GitLab'}, |
|
{'label': 'Gitea', 'value': 'Gitea'} |
|
], |
|
value='GitHub', |
|
className="mb-3" |
|
), |
|
dbc.Input(id="git-url", placeholder="Enter Git Repository URL", type="text", className="mb-3"), |
|
dbc.Input(id="personal-access-token", placeholder="Enter Git Personal Access Token", type="password", className="mb-3"), |
|
dbc.Button("Analyze", id="analyze-button", color="primary", className="mb-3"), |
|
dbc.Button("Download Results", id="download-button", color="secondary", className="mb-3 ml-2", disabled=True), |
|
dcc.Download(id="download-analysis"), |
|
html.Div(id="output", className="mt-3"), |
|
dcc.Loading( |
|
id="loading", |
|
type="dot", |
|
children=[html.Div(id="loading-output")] |
|
) |
|
]) |
|
]) |
|
]) |
|
]) |
|
], fluid=True) |
|
|
|
@app.callback( |
|
[Output("output", "children"), |
|
Output("download-button", "disabled"), |
|
Output("loading-output", "children")], |
|
[Input("analyze-button", "n_clicks")], |
|
[State("git-url", "value"), |
|
State("personal-access-token", "value"), |
|
State("git-provider", "value")], |
|
prevent_initial_call=True |
|
) |
|
def update_output(n_clicks, git_url, personal_access_token, git_provider): |
|
if n_clicks is None: |
|
raise PreventUpdate |
|
|
|
def process(): |
|
global generated_file |
|
result = process_input(git_url, personal_access_token, git_provider) |
|
return result, generated_file is not None, "" |
|
|
|
return process() |
|
|
|
@app.callback( |
|
Output("download-analysis", "data"), |
|
Input("download-button", "n_clicks"), |
|
prevent_initial_call=True |
|
) |
|
def download_analysis(n_clicks): |
|
if n_clicks is None: |
|
raise PreventUpdate |
|
|
|
if generated_file is None: |
|
return dash.no_update |
|
|
|
return dcc.send_bytes(generated_file, "license_analysis.txt") |
|
|
|
if __name__ == '__main__': |
|
print("Starting the Dash application...") |
|
app.run(debug=True, host='0.0.0.0', port=7860) |
|
print("Dash application has finished running.") |