From 0ffe0731e6aca624695dd528761d2e459256ee67 Mon Sep 17 00:00:00 2001 From: Jan200101 Date: Tue, 29 Mar 2022 11:35:51 +0200 Subject: Initial commit --- .gitignore | 6 ++++ LICENSE | 22 ++++++++++++ MANIFEST.in | 3 ++ README.md | 3 ++ labbot/__init__.py | 2 ++ labbot/__main__.py | 79 ++++++++++++++++++++++++++++++++++++++++++++ labbot/addons/merge-label.py | 73 ++++++++++++++++++++++++++++++++++++++++ labbot/bot.py | 13 ++++++++ labbot/config.py | 27 +++++++++++++++ requirements.txt | 3 ++ setup.py | 58 ++++++++++++++++++++++++++++++++ 11 files changed, 289 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 README.md create mode 100644 labbot/__init__.py create mode 100644 labbot/__main__.py create mode 100644 labbot/addons/merge-label.py create mode 100644 labbot/bot.py create mode 100644 labbot/config.py create mode 100644 requirements.txt create mode 100755 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7236703 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +env +build +dist +*.egg-info +__pycache__ +*.pyc diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1b5a336 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2022 Jan Drögehoff + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..bb910eb --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include README.md +include LICENSE +include requirements.txt diff --git a/README.md b/README.md new file mode 100644 index 0000000..0a334f8 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# lab-bot + +a modular gitlab bot oriented towards automation diff --git a/labbot/__init__.py b/labbot/__init__.py new file mode 100644 index 0000000..9561ba9 --- /dev/null +++ b/labbot/__init__.py @@ -0,0 +1,2 @@ + +__version__ = "0.0.0" diff --git a/labbot/__main__.py b/labbot/__main__.py new file mode 100644 index 0000000..5e0a3ae --- /dev/null +++ b/labbot/__main__.py @@ -0,0 +1,79 @@ +import click +from typing import List +from importlib import import_module + +from labbot.config import read_config, write_config +from labbot.bot import Bot + +@click.group() +def main(): + pass + +@main.command(help="Create a new instance") +@click.option("--name", prompt=True, help="Name the instance will be given") +@click.option("--access_token", prompt=True, hide_input=True, help="Access Token to interact with the API") +@click.option("--secret", prompt=True, default="", help="Secret to receive webhook requests [can be empty]") +def setup(**data): + instance_name = data.pop("name", "").replace(" ", "_").lower() + + if not read_config(instance_name): + write_config(instance_name, data) + click.echo(f"You can start your instance by running `lab-bot run {instance_name}`") + else: + click.echo(f"an instance with the name {instance_name} already exists") + +@main.command(help="Configure an existing instance") +@click.argument('name') +@click.option("--access_token", required=False) +@click.option("--secret", required=False) +def config(name, **data): + + data = {k:v for k,v in data.items() if v} + + conf = read_config(name) + if conf: + if data: + conf.update(data) + write_config(name, conf) + click.echo("configured") + else: + click.echo(f"nothing to change") + else: + click.echo(f"{name} is not an instance") + pass + + +def load_addons(instance: Bot, addons: List[str]): + for addon in addons: + import_module(f"labbot.addons.{addon}").setup(instance) + click.echo(f"Imported {addon}") + +@main.command(help="Run an instance") +@click.option("--port", default=8080) +@click.argument('name') +def run(name, port): + conf = read_config(name) + + if not conf: + click.echo(f"{name} is not an instance") + return + + instance = Bot( + secret=conf["secret"], + access_token=conf["access_token"] + ) + + load_addons(instance, [ + "merge-label" + ]) + + click.echo(f"Started {name}") + + instance.run( + port=port + ) + + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/labbot/addons/merge-label.py b/labbot/addons/merge-label.py new file mode 100644 index 0000000..e980e6c --- /dev/null +++ b/labbot/addons/merge-label.py @@ -0,0 +1,73 @@ +import os +import re + +title_regex = r"^(?:#|)(\d+)\s*" +word_regex = r"^#(\d+)$" +relation_keywords = [ + "related" +] +relation_distance = 2 + +state_label = { + "opened": "Code-Review", + "merged": "Testing", + "closed": "In Progress", +} +async def merge_request_hook(event, gl, *args, **kwargs): + state = event.object_attributes["state"] + related_issues = [] + + match = re.search(title_regex, event.object_attributes["title"]) + if match: + related_issues.append(match.group(1)) + + for line in event.object_attributes["description"].split("\\n"): + line = line.lower() + line_list = line.split(" ") + + for keyword in relation_keywords: + try: + keyword_index = line_list.index(keyword) + + min_pos = keyword_index - relation_distance + if min_pos < 0: + min_pos = 0 + + max_pos = keyword_index + relation_distance + if max_pos >= len(line_list): + max_pos = len(line_list)-1 + + for word in line_list[min_pos:max_pos+1]: + match = re.search(word_regex, word) + if match: + related_issues.append(match.group(1)) + + except ValueError: + pass + + for issue in related_issues: + if not issue: continue + + base_url = f"/projects/{event.project_id}/issues/{issue}" + + delete_labels = list(state_label.values()) + + try: + label = state_label[state] + if label in delete_labels: + delete_labels.remove(label) + + remove_labels = ",".join(delete_labels) + + await gl.put(base_url, data={ + "add_labels": label, + "remove_labels": remove_labels, + }) + + except KeyError: + # unknown state + pass + +def setup(bot): + bot.register(merge_request_hook, "Merge Request Hook") + pass \ No newline at end of file diff --git a/labbot/bot.py b/labbot/bot.py new file mode 100644 index 0000000..3ea1929 --- /dev/null +++ b/labbot/bot.py @@ -0,0 +1,13 @@ +from gidgetlab.aiohttp import GitLabBot + +class Bot: + + def __init__(self, *args, **kwargs): + self.instance = GitLabBot("lab-bot", **kwargs) + pass + + def register(self, func, *args, **kwargs): + return self.instance.router.register(*args, **kwargs)(func) + + def run(self, *args, **kwargs): + self.instance.run(*args, **kwargs) \ No newline at end of file diff --git a/labbot/config.py b/labbot/config.py new file mode 100644 index 0000000..3e8bcba --- /dev/null +++ b/labbot/config.py @@ -0,0 +1,27 @@ +import os +import json +from appdirs import user_config_dir + +def config_dir() -> str: + path = user_config_dir("labbot") + os.makedirs(path, exist_ok=True) + + return path + +def write_config(name: str, data: dict) -> None: + conf_path = os.path.join(config_dir(), f"{name}.json") + + with open(conf_path, "w") as file: + json.dump(data, file) + + if data != read_config(name): + raise ValueError("Config could not be saved properly") + +def read_config(name: str) -> dict: + conf_path = os.path.join(config_dir(), f"{name}.json") + + try: + with open(conf_path, "r") as file: + return json.load(file) + except FileNotFoundError: + return {} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..12b02db --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +gidgetlab[aiohttp] +appdirs +click diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..3c5c1e9 --- /dev/null +++ b/setup.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +import setuptools +import re + +requirements = [] +with open('requirements.txt') as f: + requirements = f.read().splitlines() + +version = None +with open('labbot/__init__.py') as f: + version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', f.read(), re.MULTILINE).group(1) + +if not version: + raise RuntimeError('version is not set') + +long_description = "" +with open("README.md", "r") as fh: + long_description = fh.read() + + +setuptools.setup( + name="lab-bot", + version=version, + author="Jan Drögehoff", + author_email="jandroegehoff@gmail.com", + description="a modular gitlab bot oriented towards automation", + long_description=long_description, + long_description_content_type="text/markdown", + url="", + packages=[ + "labbot", + "labbot.addons" + ], + license="MIT", + install_requires=requirements, + include_package_data=True, + classifiers=[ + "Development Status :: 3 - Alpha", + + "Programming Language :: Python :: 3", + + "License :: OSI Approved :: MIT License", + + "Operating System :: POSIX", + "Operating System :: POSIX :: BSD", + "Operating System :: POSIX :: Linux", + + "Topic :: Software Development :: Libraries :: Python Modules", + ], + python_requires=">=3.6", + + entry_points={ + 'console_scripts': [ + 'lab-bot=labbot.__main__:main', + ], + }, + +) -- cgit v1.2.3