initial commit to push copy to codeberg

This commit is contained in:
Patryk Hegenberg 2024-09-20 16:00:11 +02:00
commit 751bfe568c
20 changed files with 1364 additions and 0 deletions

18
Dockerfile Normal file
View file

@ -0,0 +1,18 @@
FROM python:3.12-bookworm
RUN apt-get update && apt-get install -y \
build-essential \
cmake \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY changelog2 /app/changelog2
WORKDIR /app/changelog2
ENTRYPOINT ["python", "main.py"]

120
README.md Normal file
View file

@ -0,0 +1,120 @@
# Changelog Generator Tool
This project provides an AI-powered tool that automates the generation of changelogs by collecting information from Git logs and Redmine tickets. It uses a crew of AI agents to improve the quality and efficiency of the final changelog.
## Project Overview
The system uses multiple AI agents, each responsible for a specific part of the changelog generation process. This tool is designed to:
- Gather data from Git logs and Redmine tickets.
- Analyze and categorize the collected information.
- Generate a well-formatted changelog or release notes.
- Ensure the output is consistent with project guidelines and requirements.
## Key Components
### Main Script (`main.py`)
The main entry point of the tool. It handles command-line arguments, sets up the environment, and initiates the changelog generation process.
### Crew Definition (`crew.py`)
Defines the AI agents and their tasks using the `crewai` framework. It includes:
- `GitlogAnalyst`: Analyzes Git commit logs.
- `RedmineAnalyst`: Retrieves and analyzes Redmine issues.
- `WritingAgent`: Generates the final changelog or release notes.
### Configuration
- `config/config.py`: Parses configuration settings.
- `config/agents.yaml`: Defines agent configurations.
- `config/tasks.yaml`: Defines task configurations.
### Tools
- `tools/tools.py`: Contains custom tools for retrieving Git commits and Redmine issues.
### Data
- `data/CodeOfConduct.md`: Contains the project's code of conduct (used for context).
- `data/template2.txt`: Changelog template used by the writing agent.
## Usage
1. **Environment Setup for development**:
- This project uses Devbox for development environment management and Poetry for dependency management.
- Set up the environment using:
```
devbox shell
poetry install
```
2. **Running the Tool**:
- The tool is started using a shell script that manages Docker containers.
- Basic usage:
```
./create_changelog.sh [options]
```
- Options:
- `--project`: Specify the Redmine project ID
- `--version`: Specify the version ID
- `--repo`: Path to the Git repository (optional)
- `--type`: Type of output (Changelog/ReleaseNotes)
- `--local`: Use local LLM instead of OpenAI (optional)
- If an error occurs that the docker volume ollama-local cannot be found, create it with `docker volume create ollama-local`
3. **Example Usage**:
```bash
./create_changelog.sh -projects 123 -versions 456 -type Changelog -repo /path/to/repo
```
## Dependencies
- Python 3.x
- Poetry for dependency management
- Devbox for development environment
- Docker for running LLM services
- External services:
- Redmine API
- Git repository
- OpenAI API (optional)
- Local LLM (optional, e.g., Ollama)
## Configuration
The project uses a TOML configuration file named changelog-config.toml for storing essential settings. This file should contain the following entries:
```toml
[DEFAULT]
OPENAI_API_KEY = your_openai_api_key_here
REDMINE_API_KEY = your_redmine_api_key_here
REDMINE_HOST = https://your_redmine_host.com
GIT_REPO_PATH = /path/to/your/git/repo
```
### Configuration File Location
The config.py script searches for the changelog-config.toml file in the following locations, in order:
- Current working directory
- changelog2 subdirectory of the current working directory
- '.config' subdirectory of the current working directory
- User's home directory
- Location specified by the CHANGELOG_CONFIG environment variable
Make sure to create the changelog-config.toml file with your actual API keys and settings before running the tool. Keep this file secure and do not commit it to version control.
However, a copy of changelog-config.toml should be located in the changelog2 subfolder for execution in the docker container.
## Development
- To add new dependencies: `poetry add <package_name>`
- To update dependencies: `poetry update`
## Notes
- The tool can use either OpenAI's models or a local LLM (like Ollama) for text generation.
- Ensure all necessary Docker containers are running before starting the tool.

0
changelog2/__init__.py Normal file
View file

4
changelog2/__main__.py Normal file
View file

@ -0,0 +1,4 @@
from .main import main
if __name__ == "__main__":
main()

View file

@ -0,0 +1,29 @@
gitlog_analyst:
role: >
Git Log Analyst
goal: >
Collect all relevant informations from Git logs
backstory: >
You specialize in analyzing Git Logs. You extract important Information such as commit messages,
dates and categorize the commits by new, changed, and fixed based on their commit message.
redmine_analyst:
role: >
Redmine Analyst
goal: >
Gather information from Redmine tickets
backstory: >
You are an expert in Redmine. You collect relevant information from Redmine tickets for the changelog
writing_agent:
role: >
Technical Writer
goal: >
Transform technical information into clear, concise, and user-friendly documentation
backstory: >
You are an experienced technical writer with a strong background in software development
and documentation. Your expertise lies in translating complex technical concepts into
easily understandable content for various audiences, from end-users to developers.
You have a keen eye for detail and a passion for creating documentation that enhances
user experience and product understanding.

View file

@ -0,0 +1,34 @@
import configparser
import os
from pathlib import Path
def find_config():
possible_locations = [
Path.cwd() / "changelog-config.toml",
Path.cwd() / "changelog2" / "changelog-config.toml",
Path.cwd() / ".config" / "changelog-config.toml",
Path.cwd() / ".config" / "changelog-config.toml",
Path.home() / "changelog-config.toml",
Path(os.getenv("CHANGELOG_CONFIG", "")),
]
for location in possible_locations:
if location.is_file():
return str(location)
raise FileNotFoundError("changelog-config.toml nicht gefunden")
config_path = find_config()
def parse_config():
config = configparser.ConfigParser()
config.read(config_path)
return {
"REDMINE_HOST": config["DEFAULT"]["REDMINE_HOST"],
"REDMINE_API_KEY": config["DEFAULT"]["REDMINE_API_KEY"],
"OPENAI_API_KEY": config["DEFAULT"]["OPENAI_API_KEY"],
"GIT_REPO_PATH": config["DEFAULT"]["GIT_REPO_PATH"],
}

View file

@ -0,0 +1,47 @@
gitlog_analysis_task:
description: >
Collect all relevant Git commits since the last tag for the projects {project_id} which are seperated by ",".
Analyze the commits and categorize them according to new features, changes, and fixes.
Git Repo: {repo_path}
expected_output: >
A JSON string containing a list of relevant Git commits with their hash, date, message, a {changelog_type} entry understandable for end users
and category (new feature, change, or fix).
redmine_analysis_task:
description: >
Collect all relevant Redmine tickets for the projects {project_id} and their versions {version_id}.
Analyze the tickets and categorize them based on their type and importance.
expected_output: >
A JSON string containing a list of relevant Redmine tickets with their ID, subject, tracker,
a {changelog_type} entry understandable for end users and a brief summary of their content and importance.
formatting_task:
description: >
Format the collected Git commits and Redmine tickets into a structured {changelog_type}.
Ensure that the information is organized logically and easy to read.
expected_output: >
A well-formatted {changelog_type} text that includes both Git commits and Redmine tickets
in a clear and organized manner, with proper categorization and highlighting of key changes.
writing_task:
description: >
Using the information from gitlog and redmine tickets, create a user-friendly and informative {changelog_type} document.
Translate technical details into clear, concise language suitable for both technical
and non-technical readers. Highlight key features, changes, and fixes.
expected_output: >
A polished {changelog_type} document that effectively communicates all relevant changes,
improvements, and fixes in a way that enhances user understanding and engagement.
The document should maintain consistency with the rules provided by the RAG tool and should fit the template from the template_tool
while accurately reflecting the current changelog's content.
instructions: >
1. Review the template provided by the template tool.
2. Identify the key sections and formatting used.
3. Analyze the content of the gitlog and redmine issues.
4. Structure the new changelog following the templates format, including similar sections.
5. Translate technical details into user-friendly language.
6. Ensure all significant changes, features, and fixes are included and highlighted.
7. Maintain a consistent tone and style throughout the document.
8. Double-check that the final document is informative, clear, and engaging for all readers.
9. Skip issues/log entries not relevant for the user
10. Do not include any additional output

153
changelog2/crew.py Normal file
View file

@ -0,0 +1,153 @@
import os
from typing import List
from config.config import parse_config
from crewai import Agent, Crew, Process, Task
from crewai.project import CrewBase, agent, crew, task
from crewai.tasks.conditional_task import ConditionalTask
from crewai_tools import FileReadTool
from langchain_ollama import ChatOllama
from langchain_openai import ChatOpenAI
from pydantic import BaseModel
from tools.tools import (
get_categorized_commits_since_last_tag,
get_commits_since_last_tag,
get_redmine_issues,
)
config = parse_config()
os.environ["OPENAI_API_KEY"] = config["OPENAI_API_KEY"]
rag_tool = FileReadTool(file_path="./data/CodeOfConduct.md")
template_tool = FileReadTool(file_path="./data/template2.txt")
llmLocal = ChatOllama(model="gemma2", base_url="http://ollama:11434")
llmOpenAI = ChatOpenAI(
base_url="https://api.openai.com/v1/",
model_name="gpt-4o",
temperature=0.4,
)
@CrewBase
class ChangelogCrew:
"""Changelog writing crew"""
agents_config = "config/agents.yaml"
tasks_config = "config/tasks.yaml"
def __init__(self, git_task: bool = False, local: bool = True):
self.git_task = git_task
self.local = local
if self.local:
os.environ["OPENAI_API_BASE"] = "http://ollama:11434"
os.environ["OPENAI_MODEL_NAME"] = "gemma2"
else:
os.environ["OPENAI_API_BASE"] = "https://api.openai.com/v1/chat/completions"
@agent
def gitlog_analyst(self) -> Agent:
if self.local:
llm = llmLocal
else:
llm = llmOpenAI
return Agent(
config=self.agents_config["gitlog_analyst"],
tools=[get_categorized_commits_since_last_tag],
llm=llm,
max_iter=5,
verbose=False,
memory=False,
)
@agent
def redmine_analyst(self) -> Agent:
if self.local:
llm = llmLocal
else:
llm = llmOpenAI
return Agent(
config=self.agents_config["redmine_analyst"],
tools=[get_redmine_issues],
llm=llm,
max_iter=5,
verbose=False,
memory=False,
)
@agent
def writing_agent(self) -> Agent:
if self.local:
llm = llmLocal
else:
llm = llmOpenAI
with open("./data/template2.txt", "r") as template_file:
template_content = template_file.read()
return Agent(
config=self.agents_config["writing_agent"],
tools=[template_tool],
llm=llm,
max_iter=5,
verbose=False,
memory=False,
template=template_content,
)
@task
def redmine_analysis_task(self) -> Task:
return Task(
config=self.tasks_config["redmine_analysis_task"],
agent=self.redmine_analyst(),
)
@task
def gitlog_analysis_task(self) -> ConditionalTask:
return ConditionalTask(
config=self.tasks_config["gitlog_analysis_task"],
condition=lambda context: self.git_task,
agent=self.gitlog_analyst(),
)
@task
def writing_task(self) -> Task:
context = []
context.append(self.redmine_analysis_task())
if self.git_task:
context.append(self.gitlog_analysis_task())
return Task(
config=self.tasks_config["writing_task"],
agent=self.writing_agent(),
context=context,
)
@crew
def crew(self) -> Crew:
"""Creates the Changelog crew"""
return Crew(
agents=self.agents,
tasks=self.tasks,
process=Process.sequential,
verbose=False,
)
class GitCommit(BaseModel):
hash: str
author: str
date: str
message: str
category: str
class RedmineTicket(BaseModel):
id: int
subject: str
tracker: str
summary: str
class Changelog(BaseModel):
version: str
date: str
changes: List[str]
fixes: List[str]
new_features: List[str]

View file

@ -0,0 +1,57 @@
# Code of Conduct für Änderungsprotokolle
## Allgemeine Richtlinien
1. Klarheit und Konsistenz: Alle Einträge müssen klar und konsistent formuliert sein, um Verwirrung zu vermeiden. Verwenden Sie klare, präzise Sprache und folgen Sie den unten aufgeführten Formatierungsrichtlinien.
2. Abkürzungen und Akronyme: Verwenden Sie standardisierte Abkürzungen für Module und Komponenten. Eine Liste der häufig verwendeten Abkürzungen finden Sie im Abschnitt "Abkürzungen".
3. Versionsnummern: Geben Sie die Versionsnummern der betroffenen Komponenten klar an. Verwenden Sie die Formatierung Komponente: alte Version -> neue Version.
4. Kategorien von Änderungen: Gliedern Sie Änderungen in die Kategorien NEW, CHANGED, FIXED, und DOCUMENTATION. Jede Kategorie sollte klar vom Rest des Dokuments abgegrenzt sein.
## Struktur und Formatierung
### Titel und Metadaten
Titel: Verwenden Sie das Format = [Produktname] - Changelog. Beispiel: = TIXstream FX - Changelog.
Website und Sprache: Geben Sie die Website und die Sprache an. Beispiel:
### Copyright und Versionsinfo
- Copyright: Fügen Sie das Copyright-Dokument ein. Beispiel:
- Versionsinfo: Geben Sie die Versionsinformationen ein. Beispiel:
### Änderungsprotokoll
- Einführung: Erklären Sie den Zweck des Dokuments und die Bedeutung der einzelnen Einträge. Beispiel:
- Versionseinträge: Jede Version sollte mit der Versionsnummer und dem Datum beginnen. Beispiel:
- Neue Funktionen (NEW): Listen Sie neue Funktionen und Features auf. Verwenden Sie nummerierte Punkte oder Aufzählungen.
- Änderungen (CHANGED): Dokumentieren Sie wesentliche Änderungen an bestehenden Funktionen oder Systemen.
- Fehlerbehebungen (FIXED): Beschreiben Sie behobene Fehler und Probleme.
- Dokumentation (DOCUMENTATION): Erwähnen Sie Änderungen an der Dokumentation oder neuen Dokumenten.
### Eintragsformatierung
- Klarheit der Beschreibungen: Beschreibungen sollten so präzise wie möglich sein. Vermeiden Sie unnötigen Jargon und halten Sie die Erklärungen einfach.
- Verweise auf Tickets: Falls zutreffend, verwenden Sie Ticket-IDs zur Referenz. Beispiel: (_#12345_).
- Modul- und Komponentennamen: Verwenden Sie die offiziellen Namen und Abkürzungen für Komponenten. Beispiel:
### Abkürzungen
Use standard abbreviations for components:
- AM: Access Manager
- FXweb: FX Web Tools
- TCC: TIXEL Control Center
- TXEC: TIXstream Express Client
- TXEJM: TIXstream Express Job Manager
- TXSFX: TIXstream FX
- TXY: TIXway

View file

@ -0,0 +1,57 @@
= {Produktname} - Changelog
:website: http://www.example.com
:lang: en
:encoding: utf-8
.COPYRIGHT
***********************************************************************
include::../shared-doc/src/copyright.adoc[]
***********************************************************************
Software Version -- Document Version:
include::version.string[]
== Redmine Issues
// Hier können Sie die Redmine-Issues auflisten
// Beispiel:
* #12345 - Beschreibung des Issues
* #12346 - Beschreibung des Issues
* #12347 - Beschreibung des Issues
== Changelog
This document lists all relevant changes applied over time to {Produktname} packages and their software components.
Each entry in the list shows changes made to the *Services*, the installation *Bundle* or the *Documentation*.
Entries may have an _ID_ at the end of the line for internal reference.
== Version {Versionsnummer} ({Datum})
*NEW*
{{#each ticket or log entry}}
=== _({{id/hash}})_ {{subject}} ===
* {{Your generated changelog entry}} _({{ticket_id/git hash}})_
{{/each}}
*CHANGED*
{{#each ticket or log entry}}
=== _({{id/hash}})_ {{subject}} ===
* {{Your generated changelog entry}} _({{ticket_id/git hash}})_
{{/each}}
*FIXED*
{{#each ticket or log entry}}
=== _({{id/hash}})_ {{subject}} ===
* {{Your generated changelog entry}} _({{ticket_id/git hash}})_
{{/each}}
*DOCUMENTATION*
{{#each ticket or log entry}}
=== _({{id/hash}})_ {{subject}} ===
* {{Your generated changelog entry}} _({{ticket_id/git hash}})_
{{/each}}

22
changelog2/gittest.sh Executable file
View file

@ -0,0 +1,22 @@
#!/bin/bash
if ! git describe --tags --abbrev=0 &>/dev/null; then
echo "Keine Tags gefunden. Verwende den ersten Commit als Startpunkt."
START_POINT=$(git rev-list --max-parents=0 HEAD)
else
START_POINT=$(git describe --tags --abbrev=0)
fi
echo "Changelog seit $START_POINT:"
echo ""
echo "Neue Features:"
git log --no-merges --pretty=format:"- %s" $START_POINT..HEAD | grep "^feat:" | sort
echo ""
echo "Fehlerbehebungen:"
git log --no-merges --pretty=format:"- %s" $START_POINT..HEAD | grep "^fix:" | sort
echo ""
echo "Dokumentation:"
git log --no-merges --pretty=format:"- %s" $START_POINT..HEAD | grep "^docs:" | sort
echo ""
echo "Andere Änderungen:"
git log --no-merges --pretty=format:"- [%h] %s (%an)" $START_POINT..HEAD | grep -vE "^- (feat|fix|docs):" | sort

154
changelog2/main.py Normal file
View file

@ -0,0 +1,154 @@
#!/usr/bin/env python
import argparse
import json
import os
import ssl
import urllib.request as request
from config.config import parse_config
from crew import ChangelogCrew
from langchain_ollama import ChatOllama
from langchain_openai import ChatOpenAI
config = parse_config()
os.environ["OPENAI_API_KEY"] = config["OPENAI_API_KEY"]
# os.environ["OPENAI_API_BASE"] = "http://ollama:11434"
# os.environ["OPENAI_MODEL_NAME"] = "llama3.1:8b" # Adjust based on available model
llmLocal = ChatOllama(
model="llama3.1:8b", base_url="http://ollama:11434", temperatur=0.4
)
llmOpenAI = ChatOpenAI(
model_name="gpt-4o",
temperature=0.4,
)
config["git_task"] = False
def makeRedmineRequest(url: str, APIKey: str):
context = ssl.create_default_context()
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
req = request.Request(url, method="GET")
req.add_header("Content-Type", "application/json")
req.add_header("X-Redmine-API-Key", APIKey)
return request.urlopen(req, context=context)
def makeProjectRequest(url: str, APIKey: str) -> dict:
res = makeRedmineRequest(url, APIKey)
json_data = json.load(res)
project_info = {project["name"]: project["id"] for project in json_data["projects"]}
return project_info
def makeVersionRequest(url: str, APIKey: str, project_id: int) -> dict:
res = makeRedmineRequest(url, APIKey)
json_data = json.load(res)
version_info = {
version["name"]: version["id"]
for version in json_data["versions"]
if version["project"]["id"] == project_id and version["status"] == "open"
}
return version_info
def main():
desc = "This is a tool that generates changelog messages from Redmine and Git using AI."
parser = argparse.ArgumentParser(description=desc, add_help=True)
parser.add_argument(
"-p",
"--project",
help="The project ID for which text is to be generated",
# type=int,
type=str,
)
parser.add_argument(
"-f",
"--fixed-version",
help="The version ID of the project for which text is to be generated",
# type=int,
type=str,
)
parser.add_argument(
"-r",
"--repo",
help="The path to the Git repository",
type=str,
)
parser.add_argument(
"-t",
"--type",
help="The type of text to be generated",
choices=["Changelog", "ReleaseNotes", "test"],
type=str,
)
parser.add_argument(
"-l",
"--local",
help="Execute the generator using a local llm",
default=False,
action="store_true",
)
args = parser.parse_args()
if not args.project:
project_infos = makeProjectRequest(
url=config["REDMINE_HOST"] + "/projects.json",
APIKey=config["REDMINE_API_KEY"],
)
print("Enter a project id when calling. There is a choice:")
for project_name, project_id in project_infos.items():
print(f"{project_name} (ID: {project_id})")
exit(0)
if args.project and not args.fixed_version:
print("Enter a version id when calling. There is a choice:")
fixed_version = makeVersionRequest(
url=config["REDMINE_HOST"]
+ "/projects/"
+ str(args.project)
+ "/versions.json",
APIKey=config["REDMINE_API_KEY"],
project_id=args.project,
)
for version_name, version_id in fixed_version.items():
print(f"{version_name}: {version_id}")
exit(0)
if args.project and args.fixed_version and not args.type:
args.type = input("Please choose a type (Changelog/ReleaseNotes)")
customField = ""
prompt = ""
if args.project and args.fixed_version:
if args.type == "Changelog":
customField = "&cf_21=Changelog"
elif args.type == "ReleaseNotes":
customField = "&cf_21=Release_notes"
else:
customField = ""
if args.repo != None:
os.environ["GIT_REPO_PATH"] = args.repo
config["git_task"] = True
else:
os.environ["GIT_REPO_PATH"] = ""
changelog_crew = ChangelogCrew(git_task=config["git_task"], local=args.local)
result = changelog_crew.crew().kickoff(
inputs={
"project_id": args.project,
"version_id": args.fixed_version,
"repo_path": args.repo,
"changelog_type": args.type,
}
)
print("######################")
print(result)
if __name__ == "__main__":
main()

159
changelog2/tools/tools.py Normal file
View file

@ -0,0 +1,159 @@
import json
import re
import ssl
import urllib.request as request
from typing import Dict, List
import git
from config.config import parse_config
from crewai_tools import tool
config = parse_config()
@tool("Get Categorized Git Commits Since Last Tag")
def get_categorized_commits_since_last_tag(
repo_path: str = config["GIT_REPO_PATH"],
) -> str:
"""
Retrieves and categorizes commits since the last tag in the Git repository using git shortlog.
Args:
repo_path (str): Path to the Git repository.
Returns:
str: JSON string containing categorized commit information.
"""
repo = git.Repo(repo_path)
tags = sorted(repo.tags, key=lambda t: t.commit.committed_datetime)
last_tag = tags[-1] if tags else None
if not last_tag:
range_spec = "HEAD"
else:
range_spec = f"{last_tag.name}..HEAD"
shortlog = repo.git.shortlog("-sne", range_spec)
author_commits = parse_shortlog(shortlog)
commits = list(repo.iter_commits(range_spec))
categorized_commits = categorize_commits(commits)
result = {
"last_tag": last_tag.name if last_tag else "No tags",
"author_commits": author_commits,
"categorized_commits": categorized_commits,
}
return json.dumps(result, indent=2)
def parse_shortlog(shortlog: str) -> List[Dict[str, str]]:
"""Parse git shortlog output."""
author_commits = []
for line in shortlog.split("\n"):
if line.strip():
count, author = line.strip().split("\t")
author_commits.append(
{"commit_count": int(count.strip()), "author": author.strip()}
)
return author_commits
def categorize_commits(commits: List[git.Commit]) -> Dict[str, List[Dict[str, str]]]:
"""Categorize commits based on their message."""
categories = {"features": [], "fixes": [], "documentation": [], "others": []}
for commit in commits:
commit_info = {
"hash": commit.hexsha[:7],
"message": commit.summary,
"date": commit.committed_datetime.isoformat(),
}
if commit.summary.startswith("feat:"):
categories["features"].append(commit_info)
elif commit.summary.startswith("fix:"):
categories["fixes"].append(commit_info)
elif commit.summary.startswith("docs:"):
categories["documentation"].append(commit_info)
else:
categories["others"].append(commit_info)
return categories
@tool("Get Git Commits Since Last Tag")
def get_commits_since_last_tag(repo_path: str = config["GIT_REPO_PATH"]) -> str:
"""
Retrieves all commits since the last tag in the Git repository.
Args:
repo_path (str): Path to the Git repository.
Returns:
str: JSON string containing commit information.
"""
repo = git.Repo(repo_path)
tags = sorted(repo.tags, key=lambda t: t.commit.committed_datetime)
last_tag = tags[-1] if tags else None
if not last_tag:
commits = list(repo.iter_commits())
else:
commits = list(repo.iter_commits(f"{last_tag.name}..HEAD"))
commit_info = [
{
"hash": commit.hexsha,
"date": commit.committed_datetime.isoformat(),
"message": commit.message.strip(),
}
for commit in commits
]
return json.dumps(commit_info)
@tool("Get Redmine Issues")
def get_redmine_issues(project_id: int, version_id: int, changelog_type: str) -> str:
"""
Retrieves issues for a specific Redmine project and version.
Args:
project_id (int): ID of the Redmine project.
version_id (int): ID of the project version.
changelog_type (str): Type of changelog ("Changelog" or "Release_notes").
Returns:
str: JSON string containing issue information.
"""
if changelog_type == "Changelog":
custom_field = "&cf_21=Changelog"
elif changelog_type == "ReleaseNotes":
custom_field = "&cf_21=Release_notes"
else:
custom_field = ""
url = f"{config['REDMINE_HOST']}/issues.json?project_id={project_id}&fixed_version_id={version_id}&status_id=*{custom_field}"
data = make_redmine_request(url)
issues = [
{
"id": issue["id"],
"subject": issue["subject"],
"tracker": issue["tracker"]["name"],
}
for issue in data["issues"]
]
return json.dumps(issues)
def make_redmine_request(url):
context = ssl.create_default_context()
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
req = request.Request(url, method="GET")
req.add_header("Content-Type", "application/json")
req.add_header("X-Redmine-API-Key", config["REDMINE_API_KEY"])
return json.load(request.urlopen(req, context=context))

31
compose.yaml Normal file
View file

@ -0,0 +1,31 @@
services:
ollama:
image: ollama/ollama:latest
container_name: ollama
ports:
- "11434:11434"
volumes:
- ollama-local:/root/.ollama
networks:
- app-network
changelog:
build: .
container_name: changelog
# depends_on:
# - ollama
environment:
- OPENAI_API_BASE=http://ollama:11434
volumes:
- ./changelog2:/app/changelog2
networks:
- app-network
command: [""]
volumes:
ollama-local:
external: true
networks:
app-network:
driver: bridge

55
create-changelog.sh Executable file
View file

@ -0,0 +1,55 @@
#!/bin/bash
set -e
# Funktion zur Anzeige der Verwendung
usage() {
echo "Verwendung: $0 --projects <projects> --versions <versions> [--repo <repo>] [--local <true/false>] [--type <Changelog/ReleaseNotes]"
}
# Argumente parsen
while [[ "$#" -gt 0 ]]; do
case $1 in
--projects) projects="$2"; shift ;;
--versions) versions="$2"; shift ;;
--repo) repo="$2"; shift ;;
--type) type="$2"; shift ;;
--local) local="$2"; shift ;;
*) usage ;;
esac
shift
done
# Überprüfen der erforderlichen Argumente
if [ -z "$projects" ] || [ -z "$versions" ]; then
echo "Verwendung: $0 --projects <projects> --versions <versions> [--repo <repo>] [--local <true/false>] [--type <Changelog/ReleaseNotes]"
docker compose up changelog -d
docker compose run --rm changelog
fi
# Setzen des Standard-Werts für local, falls nicht angegeben
local=${local:-false}
# Funktion zum Starten des lokalen Setups
start_local_setup() {
echo "Starte lokales Setup..."
docker compose up -d ollama && docker exec ollama ollama pull gemma2
docker compose run --rm changelog -p "$projects" -f "$versions" ${repo:+-r "$repo"} ${type:+-t "$type"}
docker compose down
}
# Funktion zum Ausführen des Changelog-Containers
run_changelog_container() {
echo "Führe Changelog-Container aus..."
docker compose up changelog -d
docker compose run --rm changelog -p "$projects" -f "$versions" ${repo:+-r "$repo"} ${type:+-t "$type"}
docker compose down
}
# Hauptlogik
echo "Projects: $projects"
echo "Versions: $versions"
echo "Type: $type"
if [ "$local" = "true" ]; then
start_local_setup
else
run_changelog_container
fi

21
devbox.json Normal file
View file

@ -0,0 +1,21 @@
{
"$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.12.0/.schema/devbox.schema.json",
"packages": [
"libstdcxx5",
"stdenv.cc.cc.lib",
"python312",
"poetry"
],
"shell": {
"init_hook": [
"export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$PWD/.devbox/nix/profile/default/lib:$NIX_CC/lib",
"poetry shell"
],
"scripts": {
"test": [
"echo \"Error: no test specified\" && exit 1"
]
}
}
}

23
devbox.lock Normal file
View file

@ -0,0 +1,23 @@
{
"lockfile_version": "1",
"packages": {
"libstdcxx5": {
"resolved": "github:NixOS/nixpkgs/75a52265bda7fd25e06e3a67dee3f0354e73243c#libstdcxx5",
"source": "nixpkg"
},
"poetry": {
"plugin_version": "0.0.4",
"resolved": "github:NixOS/nixpkgs/75a52265bda7fd25e06e3a67dee3f0354e73243c#poetry",
"source": "nixpkg"
},
"python312": {
"plugin_version": "0.0.3",
"resolved": "github:NixOS/nixpkgs/75a52265bda7fd25e06e3a67dee3f0354e73243c#python312",
"source": "nixpkg"
},
"stdenv.cc.cc.lib": {
"resolved": "github:NixOS/nixpkgs/75a52265bda7fd25e06e3a67dee3f0354e73243c#stdenv.cc.cc.lib",
"source": "nixpkg"
}
}
}

121
get_redmine_infos.py Normal file
View file

@ -0,0 +1,121 @@
import urllib.request as request
import json
from dotenv import load_dotenv
import os
import ssl
import argparse
import textwrap
def makeRedmineRequest(url: str, APIKey: str):
context = ssl.create_default_context()
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
req = request.Request(url, method="GET")
req.add_header("Content-Type", "application/json")
req.add_header("X-Redmine-API-Key", APIKey)
return request.urlopen(req, context=context)
def makeProjectRequest(url: str, APIKey: str) -> dict:
res = makeRedmineRequest(url, APIKey)
json_data = json.load(res)
project_info = {project["name"]: project["id"] for project in json_data["projects"]}
return project_info
def makeVersionRequest(url: str, APIKey: str, project_id: int) -> dict:
res = makeRedmineRequest(url, APIKey)
json_data = json.load(res)
version_info = {
version["name"]: version["id"]
for version in json_data["versions"]
if version["project"]["id"] == project_id and version["status"] == "open"
}
return version_info
def main():
try:
load_dotenv("./.env")
rdmnHost = os.getenv("REDMINE_HOST")
rdmnAPIKey = os.getenv("REDMINE_API_KEY")
openaiAPIKey = os.getenv("OPENAI_API_KEY")
except Exception as e:
print("No Env_variables set: ", e)
finally:
rdmnHost = "https://redmine.tixeltec.de/redmine"
if rdmnAPIKey is None or openaiAPIKey is None:
print(
"Please set environment variables for REDMINE_API_KEY and/or OPENAI_API_KEY"
)
exit(1)
desc = """This is a simple tool that allows you to generate changelog messages from Redmine using AI.
Usage:
1. Create a `.env` file . Fill in the environment variables as follows:
```
REDMINE_HOST=your_redmine_host
REDMINE_API_KEY=your_redmine_api_key
OPENAI_API_KEY=your_openai_api_key
```
2. Run the script with the following arguments:\n
- `-p, --project`: The project ID for which the changelog should be generated.
- `-f, --fixed-version`: The version ID for which the changelog should be generated.
- `-t, --type`: The type of text to be generated (Changelog or Release Notes).
3. If you don't provide a project ID or version ID, a list of available projects and versions will be displayed for you to choose from.
4. The generated changelog will be output in AsciiDoc format on the console.
Examples:
python3 changelog_generator.py -p 47 -f 755 -t Changelog
python3 changelog_generator.py -p 47 -f 755 -t ReleaseNotes
"""
clp = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
description=textwrap.dedent(desc),
add_help=True,
)
clp.add_argument(
"-p",
"--project",
help="The project for which text is to be generated",
type=int,
)
clp.add_argument(
"-f",
"--fixed-version",
help="The version of the project for which text is to be generated",
type=int,
)
clp.add_argument(
"-t",
"--type",
help="The type of text to be generated",
choices=["Changelog", "ReleaseNotes"],
type=str,
)
args = clp.parse_args()
if not args.project:
project_infos = makeProjectRequest(
url=rdmnHost + "/projects.json", APIKey=rdmnAPIKey
)
print("Enter a project id when calling. There is a choice:")
for project_name, project_id in project_infos.items():
print(f"{project_name} (ID: {project_id})")
if args.project and not args.fixed_version:
print("Enter a version id when calling. There is a choice:")
fixed_version = makeVersionRequest(
url=rdmnHost + "/projects/" + str(args.project) + "/versions.json",
APIKey=rdmnAPIKey,
project_id=args.project,
)
for version_name, version_id in fixed_version.items():
print(f"{version_name}: {version_id}")
if __name__ == "__main__":
main()

29
pyproject.toml Normal file
View file

@ -0,0 +1,29 @@
[tool.poetry]
name = "changelog2"
version = "0.0.1"
description = "An AI-Agent based tool for creating changelogs"
authors = ["Patryk Hegenberg <hegenberg@tixeltec.com>"]
readme = "README.md"
[tool.poetry.scripts]
changelog2 = "changelog2.main:main"
[tool.poetry.dependencies]
python = ">=3.11,<=3.13"
langchain = "^0.2.16"
llama-index = "^0.11.8"
crewai = { extras = ["tools"], version = "^0.55.2" }
gitpython = "^3.1.43"
langchain-ollama = "^0.1.3"
nltk = "^3.9.1"
[tool.poetry.group.dev.dependencies]
black = "^24.8.0"
flake8 = "^7.1.1"
pytest = "^8.3.2"
pyinstaller = "^6.10.0"
staticx = "^0.14.1"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

230
requirements.txt Normal file
View file

@ -0,0 +1,230 @@
aiohappyeyeballs==2.4.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
aiohttp==3.10.5 ; python_version >= "3.11" and python_full_version <= "3.13.0"
aiosignal==1.3.1 ; python_version >= "3.11" and python_full_version <= "3.13.0"
alembic==1.13.2 ; python_version >= "3.11" and python_full_version <= "3.13.0"
annotated-types==0.7.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
anyio==4.4.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
appdirs==1.4.4 ; python_version >= "3.11" and python_full_version <= "3.13.0"
asgiref==3.8.1 ; python_version >= "3.11" and python_full_version <= "3.13.0"
attrs==24.2.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
auth0-python==4.7.2 ; python_version >= "3.11" and python_full_version <= "3.13.0"
backoff==2.2.1 ; python_version >= "3.11" and python_full_version <= "3.13.0"
bcrypt==4.2.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
beautifulsoup4==4.12.3 ; python_version >= "3.11" and python_full_version <= "3.13.0"
boto3==1.35.16 ; python_version >= "3.11" and python_full_version <= "3.13.0"
botocore==1.35.16 ; python_version >= "3.11" and python_full_version <= "3.13.0"
build==1.2.2 ; python_version >= "3.11" and python_full_version <= "3.13.0"
cachetools==5.5.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
certifi==2024.8.30 ; python_version >= "3.11" and python_full_version <= "3.13.0"
cffi==1.17.1 ; python_version >= "3.11" and python_full_version <= "3.13.0" and (platform_python_implementation != "PyPy" or os_name == "nt") and (platform_python_implementation != "PyPy" or implementation_name != "pypy")
charset-normalizer==3.3.2 ; python_version >= "3.11" and python_full_version <= "3.13.0"
chroma-hnswlib==0.7.3 ; python_version >= "3.11" and python_full_version <= "3.13.0"
chromadb==0.4.24 ; python_version >= "3.11" and python_full_version <= "3.13.0"
click==8.1.7 ; python_version >= "3.11" and python_full_version <= "3.13.0"
cohere==5.9.1 ; python_version >= "3.11" and python_full_version <= "3.13.0"
colorama==0.4.6 ; python_version >= "3.11" and python_full_version <= "3.13.0" and (platform_system == "Windows" or sys_platform == "win32" or os_name == "nt")
coloredlogs==15.0.1 ; python_version >= "3.11" and python_full_version <= "3.13.0"
crewai-tools==0.12.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
crewai[tools]==0.55.2 ; python_version >= "3.11" and python_full_version <= "3.13.0"
cryptography==43.0.1 ; python_version >= "3.11" and python_full_version <= "3.13.0"
dataclasses-json==0.6.7 ; python_version >= "3.11" and python_full_version <= "3.13.0"
decorator==5.1.1 ; python_version >= "3.11" and python_full_version <= "3.13.0"
deprecated==1.2.14 ; python_version >= "3.11" and python_full_version <= "3.13.0"
deprecation==2.1.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
dirtyjson==1.0.8 ; python_version >= "3.11" and python_full_version <= "3.13.0"
distro==1.9.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
docker==7.1.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
docstring-parser==0.16 ; python_version >= "3.11" and python_full_version <= "3.13.0"
docx2txt==0.8 ; python_version >= "3.11" and python_full_version <= "3.13.0"
embedchain==0.1.121 ; python_version >= "3.11" and python_full_version <= "3.13.0"
fastapi==0.114.1 ; python_version >= "3.11" and python_full_version <= "3.13.0"
fastavro==1.9.7 ; python_version >= "3.11" and python_full_version <= "3.13.0"
filelock==3.16.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
flatbuffers==24.3.25 ; python_version >= "3.11" and python_full_version <= "3.13.0"
frozenlist==1.4.1 ; python_version >= "3.11" and python_full_version <= "3.13.0"
fsspec==2024.9.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
gitdb==4.0.11 ; python_version >= "3.11" and python_full_version <= "3.13.0"
gitpython==3.1.43 ; python_version >= "3.11" and python_full_version <= "3.13.0"
google-api-core==2.19.2 ; python_version >= "3.11" and python_full_version <= "3.13.0"
google-api-core[grpc]==2.19.2 ; python_version >= "3.11" and python_full_version <= "3.13.0"
google-auth==2.34.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
google-cloud-aiplatform==1.66.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
google-cloud-bigquery==3.25.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
google-cloud-core==2.4.1 ; python_version >= "3.11" and python_full_version <= "3.13.0"
google-cloud-resource-manager==1.12.5 ; python_version >= "3.11" and python_full_version <= "3.13.0"
google-cloud-storage==2.18.2 ; python_version >= "3.11" and python_full_version <= "3.13.0"
google-crc32c==1.6.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
google-resumable-media==2.7.2 ; python_version >= "3.11" and python_full_version <= "3.13.0"
googleapis-common-protos==1.65.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
googleapis-common-protos[grpc]==1.65.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
gptcache==0.1.44 ; python_version >= "3.11" and python_full_version <= "3.13.0"
greenlet==3.1.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
grpc-google-iam-v1==0.13.1 ; python_version >= "3.11" and python_full_version <= "3.13.0"
grpcio-status==1.62.3 ; python_version >= "3.11" and python_full_version <= "3.13.0"
grpcio-tools==1.62.3 ; python_version >= "3.11" and python_full_version <= "3.13.0"
grpcio==1.66.1 ; python_version >= "3.11" and python_full_version <= "3.13.0"
h11==0.14.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
h2==4.1.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
hpack==4.0.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
httpcore==1.0.5 ; python_version >= "3.11" and python_full_version <= "3.13.0"
httptools==0.6.1 ; python_version >= "3.11" and python_full_version <= "3.13.0"
httpx-sse==0.4.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
httpx==0.27.2 ; python_version >= "3.11" and python_full_version <= "3.13.0"
httpx[http2]==0.27.2 ; python_version >= "3.11" and python_full_version <= "3.13.0"
huggingface-hub==0.24.6 ; python_version >= "3.11" and python_full_version <= "3.13.0"
humanfriendly==10.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
hyperframe==6.0.1 ; python_version >= "3.11" and python_full_version <= "3.13.0"
idna==3.8 ; python_version >= "3.11" and python_full_version <= "3.13.0"
importlib-metadata==8.4.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
importlib-resources==6.4.5 ; python_version >= "3.11" and python_full_version <= "3.13.0"
iniconfig==2.0.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
instructor==1.3.3 ; python_version >= "3.11" and python_full_version <= "3.13.0"
jiter==0.4.2 ; python_version >= "3.11" and python_full_version <= "3.13.0"
jmespath==1.0.1 ; python_version >= "3.11" and python_full_version <= "3.13.0"
joblib==1.4.2 ; python_version >= "3.11" and python_full_version <= "3.13.0"
json-repair==0.25.3 ; python_version >= "3.11" and python_full_version <= "3.13.0"
jsonpatch==1.33 ; python_version >= "3.11" and python_full_version <= "3.13.0"
jsonpointer==3.0.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
jsonref==1.1.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
kubernetes==30.1.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
lancedb==0.5.7 ; python_version >= "3.11" and python_full_version <= "3.13.0"
langchain-cohere==0.1.9 ; python_version >= "3.11" and python_full_version <= "3.13.0"
langchain-community==0.2.16 ; python_version >= "3.11" and python_full_version <= "3.13.0"
langchain-core==0.2.39 ; python_version >= "3.11" and python_full_version <= "3.13.0"
langchain-experimental==0.0.65 ; python_version >= "3.11" and python_full_version <= "3.13.0"
langchain-ollama==0.1.3 ; python_version >= "3.11" and python_full_version <= "3.13.0"
langchain-openai==0.1.23 ; python_version >= "3.11" and python_full_version <= "3.13.0"
langchain-text-splitters==0.2.4 ; python_version >= "3.11" and python_full_version <= "3.13.0"
langchain==0.2.16 ; python_version >= "3.11" and python_full_version <= "3.13.0"
langsmith==0.1.117 ; python_version >= "3.11" and python_full_version <= "3.13.0"
llama-cloud==0.0.17 ; python_version >= "3.11" and python_full_version <= "3.13.0"
llama-index-agent-openai==0.3.1 ; python_version >= "3.11" and python_full_version <= "3.13.0"
llama-index-cli==0.3.1 ; python_version >= "3.11" and python_full_version <= "3.13.0"
llama-index-core==0.11.8 ; python_version >= "3.11" and python_full_version <= "3.13.0"
llama-index-embeddings-openai==0.2.4 ; python_version >= "3.11" and python_full_version <= "3.13.0"
llama-index-indices-managed-llama-cloud==0.3.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
llama-index-legacy==0.9.48.post3 ; python_version >= "3.11" and python_full_version <= "3.13.0"
llama-index-llms-openai==0.2.3 ; python_version >= "3.11" and python_full_version <= "3.13.0"
llama-index-multi-modal-llms-openai==0.2.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
llama-index-program-openai==0.2.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
llama-index-question-gen-openai==0.2.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
llama-index-readers-file==0.2.1 ; python_version >= "3.11" and python_full_version <= "3.13.0"
llama-index-readers-llama-parse==0.3.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
llama-index==0.11.8 ; python_version >= "3.11" and python_full_version <= "3.13.0"
llama-parse==0.5.5 ; python_version >= "3.11" and python_full_version <= "3.13.0"
mako==1.3.5 ; python_version >= "3.11" and python_full_version <= "3.13.0"
markdown-it-py==3.0.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
markupsafe==2.1.5 ; python_version >= "3.11" and python_full_version <= "3.13.0"
marshmallow==3.22.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
mdurl==0.1.2 ; python_version >= "3.11" and python_full_version <= "3.13.0"
mem0ai==0.0.20 ; python_version >= "3.11" and python_full_version <= "3.13.0"
mmh3==4.1.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
monotonic==1.6 ; python_version >= "3.11" and python_full_version <= "3.13.0"
mpmath==1.3.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
multidict==6.1.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
mypy-extensions==1.0.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
nest-asyncio==1.6.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
networkx==3.3 ; python_version >= "3.11" and python_full_version <= "3.13.0"
nltk==3.9.1 ; python_version >= "3.11" and python_full_version <= "3.13.0"
nodeenv==1.9.1 ; python_version >= "3.11" and python_full_version <= "3.13.0"
numpy==1.26.4 ; python_version >= "3.11" and python_full_version <= "3.13.0"
oauthlib==3.2.2 ; python_version >= "3.11" and python_full_version <= "3.13.0"
ollama==0.3.3 ; python_version >= "3.11" and python_full_version <= "3.13.0"
onnxruntime==1.19.2 ; python_version >= "3.11" and python_full_version <= "3.13.0"
openai==1.44.1 ; python_version >= "3.11" and python_full_version <= "3.13.0"
opentelemetry-api==1.27.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
opentelemetry-exporter-otlp-proto-common==1.27.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
opentelemetry-exporter-otlp-proto-grpc==1.27.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
opentelemetry-exporter-otlp-proto-http==1.27.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
opentelemetry-instrumentation-asgi==0.48b0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
opentelemetry-instrumentation-fastapi==0.48b0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
opentelemetry-instrumentation==0.48b0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
opentelemetry-proto==1.27.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
opentelemetry-sdk==1.27.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
opentelemetry-semantic-conventions==0.48b0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
opentelemetry-util-http==0.48b0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
orjson==3.10.7 ; python_version >= "3.11" and python_full_version <= "3.13.0"
outcome==1.3.0.post0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
overrides==7.7.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
packaging==24.1 ; python_version >= "3.11" and python_full_version <= "3.13.0"
pandas==2.2.2 ; python_version >= "3.11" and python_full_version <= "3.13.0"
parameterized==0.9.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
pillow==10.4.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
pluggy==1.5.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
portalocker==2.10.1 ; python_version >= "3.11" and python_full_version <= "3.13.0"
posthog==3.6.5 ; python_version >= "3.11" and python_full_version <= "3.13.0"
proto-plus==1.24.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
protobuf==4.25.4 ; python_version >= "3.11" and python_full_version <= "3.13.0"
pulsar-client==3.5.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
py==1.11.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
pyarrow==17.0.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
pyasn1-modules==0.4.1 ; python_version >= "3.11" and python_full_version <= "3.13.0"
pyasn1==0.6.1 ; python_version >= "3.11" and python_full_version <= "3.13.0"
pycparser==2.22 ; python_version >= "3.11" and python_full_version <= "3.13.0" and (platform_python_implementation != "PyPy" or os_name == "nt") and (platform_python_implementation != "PyPy" or implementation_name != "pypy")
pydantic-core==2.23.3 ; python_version >= "3.11" and python_full_version <= "3.13.0"
pydantic==2.9.1 ; python_version >= "3.11" and python_full_version <= "3.13.0"
pygments==2.18.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
pyjwt==2.9.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
pylance==0.9.18 ; python_version >= "3.11" and python_full_version <= "3.13.0"
pypdf==4.3.1 ; python_version >= "3.11" and python_full_version <= "3.13.0"
pypika==0.48.9 ; python_version >= "3.11" and python_full_version <= "3.13.0"
pyproject-hooks==1.1.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
pyreadline3==3.4.1 ; sys_platform == "win32" and python_version >= "3.11" and python_full_version <= "3.13.0"
pyright==1.1.380 ; python_version >= "3.11" and python_full_version <= "3.13.0"
pysbd==0.3.4 ; python_version >= "3.11" and python_full_version <= "3.13.0"
pysocks==1.7.1 ; python_version >= "3.11" and python_full_version <= "3.13.0"
pytest==8.3.3 ; python_version >= "3.11" and python_full_version <= "3.13.0"
python-dateutil==2.9.0.post0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
python-dotenv==1.0.1 ; python_version >= "3.11" and python_full_version <= "3.13.0"
pytube==15.0.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
pytz==2024.2 ; python_version >= "3.11" and python_full_version <= "3.13.0"
pywin32==306 ; python_version >= "3.11" and python_full_version <= "3.13.0" and (sys_platform == "win32" or platform_system == "Windows")
pyyaml==6.0.2 ; python_version >= "3.11" and python_full_version <= "3.13.0"
qdrant-client==1.11.1 ; python_version >= "3.11" and python_full_version <= "3.13.0"
ratelimiter==1.2.0.post0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
regex==2024.7.24 ; python_version >= "3.11" and python_full_version <= "3.13.0"
requests-oauthlib==2.0.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
requests==2.32.3 ; python_version >= "3.11" and python_full_version <= "3.13.0"
retry==0.9.2 ; python_version >= "3.11" and python_full_version <= "3.13.0"
rich==13.8.1 ; python_version >= "3.11" and python_full_version <= "3.13.0"
rsa==4.9 ; python_version >= "3.11" and python_full_version <= "3.13.0"
s3transfer==0.10.2 ; python_version >= "3.11" and python_full_version <= "3.13.0"
schema==0.7.7 ; python_version >= "3.11" and python_full_version <= "3.13.0"
selenium==4.24.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
semver==3.0.2 ; python_version >= "3.11" and python_full_version <= "3.13.0"
setuptools==74.1.2 ; python_version >= "3.11" and python_full_version <= "3.13.0"
shapely==2.0.6 ; python_version >= "3.11" and python_full_version <= "3.13.0"
shellingham==1.5.4 ; python_version >= "3.11" and python_full_version <= "3.13.0"
six==1.16.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
smmap==5.0.1 ; python_version >= "3.11" and python_full_version <= "3.13.0"
sniffio==1.3.1 ; python_version >= "3.11" and python_full_version <= "3.13.0"
sortedcontainers==2.4.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
soupsieve==2.6 ; python_version >= "3.11" and python_full_version <= "3.13.0"
sqlalchemy==2.0.34 ; python_version >= "3.11" and python_full_version <= "3.13.0"
sqlalchemy[asyncio]==2.0.34 ; python_version >= "3.11" and python_full_version <= "3.13.0"
starlette==0.38.5 ; python_version >= "3.11" and python_full_version <= "3.13.0"
striprtf==0.0.26 ; python_version >= "3.11" and python_full_version <= "3.13.0"
sympy==1.13.2 ; python_version >= "3.11" and python_full_version <= "3.13.0"
tabulate==0.9.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
tenacity==8.5.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
tiktoken==0.7.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
tokenizers==0.20.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
tqdm==4.66.5 ; python_version >= "3.11" and python_full_version <= "3.13.0"
trio-websocket==0.11.1 ; python_version >= "3.11" and python_full_version <= "3.13.0"
trio==0.26.2 ; python_version >= "3.11" and python_full_version <= "3.13.0"
typer==0.12.5 ; python_version >= "3.11" and python_full_version <= "3.13.0"
types-requests==2.32.0.20240907 ; python_version >= "3.11" and python_full_version <= "3.13.0"
typing-extensions==4.12.2 ; python_version >= "3.11" and python_full_version <= "3.13.0"
typing-inspect==0.9.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
tzdata==2024.1 ; python_version >= "3.11" and python_full_version <= "3.13.0"
urllib3==2.2.2 ; python_version >= "3.11" and python_full_version <= "3.13.0"
urllib3[socks]==2.2.2 ; python_version >= "3.11" and python_full_version <= "3.13.0"
uvicorn[standard]==0.30.6 ; python_version >= "3.11" and python_full_version <= "3.13.0"
uvloop==0.20.0 ; (sys_platform != "win32" and sys_platform != "cygwin") and platform_python_implementation != "PyPy" and python_version >= "3.11" and python_full_version <= "3.13.0"
watchfiles==0.24.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
websocket-client==1.8.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
websockets==13.0.1 ; python_version >= "3.11" and python_full_version <= "3.13.0"
wrapt==1.16.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
wsproto==1.2.0 ; python_version >= "3.11" and python_full_version <= "3.13.0"
yarl==1.11.1 ; python_version >= "3.11" and python_full_version <= "3.13.0"
zipp==3.20.1 ; python_version >= "3.11" and python_full_version <= "3.13.0"