summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatias Linares <matias.linares@comprandoengrupo.net>2024-09-20 20:46:28 -0300
committerMatias Linares <matias.linares@comprandoengrupo.net>2024-09-20 20:46:28 -0300
commit504d29accac51c537d5dcd42b129deb6f7463457 (patch)
treeb8e0837c359ecd10db1855a6cfa6146c91a219c7
downloadkodereviewer-504d29accac51c537d5dcd42b129deb6f7463457.tar.gz
Initial commit
-rw-r--r--.gitignore162
-rw-r--r--MANIFEST.in1
-rw-r--r--README.md3
-rw-r--r--kodereviewer/__init__.py0
-rw-r--r--kodereviewer/__main__.py3
-rw-r--r--kodereviewer/app.py56
-rw-r--r--kodereviewer/data.py70
-rw-r--r--kodereviewer/mdconverter.py30
-rw-r--r--kodereviewer/network_manager.py58
-rw-r--r--kodereviewer/project.py49
-rw-r--r--kodereviewer/project_model.py92
-rw-r--r--kodereviewer/pull_request_model.py66
-rw-r--r--kodereviewer/qml/AddRepositoryPage.qml97
-rw-r--r--kodereviewer/qml/Main.qml85
-rw-r--r--kodereviewer/qml/ProjectListPage.qml130
-rw-r--r--kodereviewer/qml/SettingsPage.qml39
-rw-r--r--kodereviewer/qml/WelcomePage.qml117
-rwxr-xr-xorg.deprecated.kodereviewer.desktop8
-rw-r--r--org.deprecated.kodereviewer.svg314
-rw-r--r--pyproject.toml32
20 files changed, 1412 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..efa407c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,162 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+# in version control.
+# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
+.pdm.toml
+.pdm-python
+.pdm-build/
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/ \ No newline at end of file
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..92295a5
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1 @@
+include kodereviewer/qml/*.qml
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..20b9c16
--- /dev/null
+++ b/README.md
@@ -0,0 +1,3 @@
+# Kodereviewer
+
+A code review tool
diff --git a/kodereviewer/__init__.py b/kodereviewer/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/kodereviewer/__init__.py
diff --git a/kodereviewer/__main__.py b/kodereviewer/__main__.py
new file mode 100644
index 0000000..87c61d9
--- /dev/null
+++ b/kodereviewer/__main__.py
@@ -0,0 +1,3 @@
+from . import app
+
+app.main()
diff --git a/kodereviewer/app.py b/kodereviewer/app.py
new file mode 100644
index 0000000..a2eb5f4
--- /dev/null
+++ b/kodereviewer/app.py
@@ -0,0 +1,56 @@
+#!/usr/bin/env python3
+
+import os
+import sys
+import signal
+
+from KI18n import KLocalizedContext, KLocalizedString
+from PySide6.QtGui import QGuiApplication
+from PySide6.QtCore import QUrl, QByteArray
+from PySide6.QtQml import QQmlApplicationEngine, qmlRegisterType
+
+from kodereviewer.mdconverter import MdConverter
+from kodereviewer.project_model import ProjectModel
+from kodereviewer.pull_request_model import PullRequestModel
+from kodereviewer.network_manager import NetworkManager
+
+
+def main():
+ """Initializes and manages the application execution"""
+ app = QGuiApplication(sys.argv)
+ engine = QQmlApplicationEngine()
+ # KLocalizedString::setApplicationDomain("tutorial");
+ KLocalizedString.setApplicationDomain(QByteArray(b"kodereviewer"))
+
+ app.setOrganizationName("Deprecated")
+ app.setOrganizationDomain("deprecated.org")
+ app.setApplicationName("Kode Reviewer")
+ app.setDesktopFileName("kodereviewer")
+
+ """Needed to close the app with Ctrl+C"""
+ signal.signal(signal.SIGINT, signal.SIG_DFL)
+
+ """Needed to get proper KDE style outside of Plasma"""
+ if not os.environ.get("QT_QUICK_CONTROLS_STYLE"):
+ os.environ["QT_QUICK_CONTROLS_STYLE"] = "org.kde.desktop"
+
+ qmlRegisterType(MdConverter, "org.deprecated.kodereviewer", 1, 0, "MdConverter")
+ qmlRegisterType(ProjectModel, "org.deprecated.kodereviewer", 1, 0, "ProjectModel")
+ qmlRegisterType(PullRequestModel, "org.deprecated.kodereviewer", 1, 0, "PullRequestModel")
+ qmlRegisterType(NetworkManager, "org.deprecated.kodereviewer", 1, 0, "NetworkManager")
+
+ localized_context = KLocalizedContext()
+ engine.rootContext().setContextObject(localized_context)
+ base_path = os.path.abspath(os.path.dirname(__file__))
+ engine.addImportPath(f"file://{base_path}/qml")
+ url = QUrl(f"file://{base_path}/qml/Main.qml")
+ engine.load(url)
+
+ if len(engine.rootObjects()) == 0:
+ quit()
+
+ app.exec()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/kodereviewer/data.py b/kodereviewer/data.py
new file mode 100644
index 0000000..7d3b73a
--- /dev/null
+++ b/kodereviewer/data.py
@@ -0,0 +1,70 @@
+from datetime import datetime
+from enum import Enum
+from typing import Any, Optional
+
+from PySide6.QtCore import QObject, QSettings, Signal, Slot, Property, qDebug
+from PySide6.QtNetwork import QHttpHeaders, QNetworkAccessManager, QNetworkReply, QNetworkRequestFactory
+from PySide6.QtQml import QmlElement
+
+
+class User(QObject):
+ username: str
+ avatar_url: str
+
+ def __init__(self, data: dict[str, Any]):
+ super().__init__()
+ self.username = data['login']
+ self.avatar_url = data['avatar_url']
+
+
+class Label(QObject):
+ name: str
+ color: str
+ description: str
+
+ def __init__(self, data: dict[str, Any]):
+ super().__init__()
+ self.name = data['name']
+ self.color = data['color']
+ self.description = data['description']
+
+
+
+class State(Enum):
+ OPEN = 'open'
+ CLOSED = 'closed'
+ DRAFT = 'draft'
+
+
+class PullRequest(QObject):
+
+ number: int
+ title: str
+ state: State
+ url: str
+ body: str | None
+ created_at: datetime
+ updated_at: datetime
+
+ user: User
+ assignee: User | None
+ labels: list[str]
+
+
+ def __init__(self, data: dict[str, Any]):
+ super().__init__()
+ self.number = data['number']
+ self.state = data['state']
+ self.title = data['title']
+ self.url = data['html_url']
+ self.body = data['body']
+ self.created_at = datetime.fromisoformat(data['created_at'])
+ self.updated_at = datetime.fromisoformat(data['updated_at'])
+
+ self.user = User(data['user'])
+ self.assignee = None
+ if data['assignee'] is not None:
+ self.assignee = User(data['assignee'])
+
+ labels = [Label(label) for label in data['labels']]
+
diff --git a/kodereviewer/mdconverter.py b/kodereviewer/mdconverter.py
new file mode 100644
index 0000000..1d0a653
--- /dev/null
+++ b/kodereviewer/mdconverter.py
@@ -0,0 +1,30 @@
+from markdown import markdown
+from PySide6.QtCore import QObject, Signal, Slot, Property
+from PySide6.QtQml import QmlElement
+
+QML_IMPORT_NAME = "org.deprecated.kodereviewer"
+QML_IMPORT_MAJOR_VERSION = 1
+
+
+@QmlElement
+class MdConverter(QObject):
+ """A simple markdown converter"""
+
+ sourceTextChanged = Signal()
+
+ def __init__(self, _source_text=""):
+ super().__init__()
+ self._source_text = _source_text
+
+ @Property(str, notify=sourceTextChanged)
+ def sourceText(self):
+ return self._source_text
+
+ @sourceText.setter
+ def sourceText(self, val: str):
+ self._source_text = val
+ self.sourceTextChanged.emit()
+
+ @Slot(result=str)
+ def mdFormat(self):
+ return markdown(self._source_text)
diff --git a/kodereviewer/network_manager.py b/kodereviewer/network_manager.py
new file mode 100644
index 0000000..2d8b8aa
--- /dev/null
+++ b/kodereviewer/network_manager.py
@@ -0,0 +1,58 @@
+from typing import Optional
+
+from PySide6.QtCore import QObject, QSettings, Signal, Slot, Property, qDebug
+from PySide6.QtNetwork import QHttpHeaders, QNetworkAccessManager, QNetworkReply, QNetworkRequestFactory
+from PySide6.QtQml import QmlElement
+
+from kodereviewer.project import Project
+
+QML_IMPORT_NAME = "org.deprecated.kodereviewer"
+QML_IMPORT_MAJOR_VERSION = 1
+
+@QmlElement
+class NetworkManager(QObject):
+
+ _project: Optional[Project]
+ _manager: QNetworkAccessManager
+ _request_factory: QNetworkRequestFactory
+
+ projectChanged = Signal()
+
+ def __init__(self):
+ super().__init__()
+ self._project = None
+ self._manager = QNetworkAccessManager()
+
+ self._request_factory = QNetworkRequestFactory()
+ settings = QSettings()
+ github_token: str = str(settings.value("githubToken"))
+ headers = QHttpHeaders()
+ headers.append(QHttpHeaders.WellKnownHeader.Accept, "application/vnd.github+json")
+ headers.append(QHttpHeaders.WellKnownHeader.Authorization,
+ f'Bearer {github_token}')
+ headers.append(QHttpHeaders.WellKnownHeader.UserAgent, "kodereviewer")
+ print(f'Authorization: Bearer {github_token}')
+ self._request_factory.setCommonHeaders(headers)
+
+ self._manager.finished.connect(self.reply_finished)
+
+ def project(self) -> Optional[Project]:
+ return self._project
+
+ def set_project(self, project: Optional[Project]):
+ if project is None:
+ return
+ self._project = project
+ self.projectChanged.emit()
+
+ base_url = f'https://api.github.com/repos/{self._project.owner}/{self._project.name}'
+ self._request_factory.setBaseUrl(base_url)
+
+ project = Property(Project, fget=project, fset=set_project)
+
+ def reply_finished(self, reply: QNetworkReply):
+ self._project.load_pull_requests(reply.readAll())
+
+ @Slot()
+ def getPullRequests(self) -> None:
+ self._manager.get(self._request_factory.createRequest("/pulls"))
diff --git a/kodereviewer/project.py b/kodereviewer/project.py
new file mode 100644
index 0000000..669ee90
--- /dev/null
+++ b/kodereviewer/project.py
@@ -0,0 +1,49 @@
+import json
+from PySide6.QtCore import QByteArray, QObject, QUrl, Signal, Slot, Property
+from PySide6.QtQml import QmlElement
+
+from kodereviewer.data import PullRequest
+
+QML_IMPORT_NAME = "org.deprecated.kodereviewer"
+QML_IMPORT_MAJOR_VERSION = 1
+
+@QmlElement
+class Project(QObject):
+ """Represents a github project"""
+
+ _name: str
+ _owner: str
+ _url: QUrl
+ _pull_requests: list[PullRequest]
+
+ pullRequestChanged = Signal()
+
+ def __init__(self, name: str, owner: str, url: QUrl):
+ super().__init__()
+ self._name = name
+ self._owner = owner
+ self._url = url
+ self._pull_requests = []
+
+ @Property(str)
+ def name(self) -> str:
+ return self._name
+
+ @Property(str)
+ def owner(self) -> str:
+ return self._owner
+
+ @Property(QUrl)
+ def url(self) -> QUrl:
+ return self._url
+
+ @Property(list)
+ def pullRequests(self) -> list[PullRequest]:
+ return self._pull_requests
+
+ def load_pull_requests(self, response: QByteArray) -> None:
+ data = json.loads(response.toStdString())
+ self._pull_requests = [
+ PullRequest(pr) for pr in data
+ ]
+ self.pullRequestChanged.emit()
diff --git a/kodereviewer/project_model.py b/kodereviewer/project_model.py
new file mode 100644
index 0000000..c676157
--- /dev/null
+++ b/kodereviewer/project_model.py
@@ -0,0 +1,92 @@
+import json
+from os import path
+from typing import Any
+
+from PySide6.QtCore import QAbstractListModel, QByteArray, QModelIndex, QObject, QPersistentModelIndex, QStandardPaths, QUrl, Qt, Signal, Slot, Property
+from PySide6.QtQml import QmlElement
+
+from kodereviewer.project import Project
+
+QML_IMPORT_NAME = "org.deprecated.kodereviewer"
+QML_IMPORT_MAJOR_VERSION = 1
+
+
+@QmlElement
+class ProjectModel(QAbstractListModel):
+ """Projects list!"""
+
+ projects: list[Project]
+
+ NameRole = Qt.ItemDataRole.UserRole + 1
+ OwnerRole = NameRole + 1
+ UrlRole = OwnerRole + 1
+
+ def __init__(self):
+ super().__init__()
+ self.projects = []
+
+ project_config = self._project_file()
+ try:
+ with open(project_config) as fp:
+ data = json.load(fp)
+ if isinstance(data, list):
+ self._load_projects(data)
+ except OSError as e:
+ pass
+
+ def _project_file(self) -> str:
+ app_data = QStandardPaths.writableLocation(QStandardPaths.AppDataLocation)
+ project_config = path.join(app_data, 'projects.json')
+ return project_config
+
+ def data(self,
+ index: QModelIndex | QPersistentModelIndex,
+ role: int = Qt.ItemDataRole.DisplayRole) -> object:
+ project = self.projects[index.row()]
+
+ if role == Qt.ItemDataRole.DisplayRole:
+ return project.name
+ if role == self.NameRole:
+ return project.name
+ if role == self.OwnerRole:
+ return project.owner
+ if role == self.UrlRole:
+ return project.url
+
+ return None
+
+ def rowCount(self, parent: QModelIndex | QPersistentModelIndex = QModelIndex()) -> int:
+ return len(self.projects)
+
+ def roleNames(self) -> dict[int, QByteArray]:
+ return {
+ self.NameRole: QByteArray(b"name"),
+ self.OwnerRole: QByteArray(b"owner"),
+ self.UrlRole: QByteArray(b"url")
+ }
+
+ @Slot(int, result=Project)
+ def get(self, index: int) -> Project:
+ return self.projects[index]
+
+ def _load_projects(self, data: list[dict[str, Any]]) -> None:
+ for project in data:
+ self.projects.append(Project(
+ project['name'],
+ project['owner'],
+ project['url']
+ ))
+
+ def _save_projects(self):
+ project_config = self._project_file()
+ data = [{'name': project.name, 'owner': project.owner, 'url': project.url} for project in self.projects]
+
+ with open(self._project_file(), 'w') as fp:
+ json.dump(data, fp)
+
+ @Slot(str, str, str)
+ def add(self, name: str, owner: str, url: str) -> None:
+ self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
+ self.projects.append(Project(name, owner, url))
+ self._save_projects()
+ self.endInsertRows()
diff --git a/kodereviewer/pull_request_model.py b/kodereviewer/pull_request_model.py
new file mode 100644
index 0000000..e08bf6e
--- /dev/null
+++ b/kodereviewer/pull_request_model.py
@@ -0,0 +1,66 @@
+import json
+from os import path
+from typing import Any, Optional
+
+from PySide6.QtCore import QAbstractListModel, QByteArray, QModelIndex, QObject, QPersistentModelIndex, QStandardPaths, QUrl, Qt, Signal, Slot, Property
+from PySide6.QtQml import QmlElement
+
+from kodereviewer.project import Project
+from kodereviewer.data import PullRequest
+
+QML_IMPORT_NAME = "org.deprecated.kodereviewer"
+QML_IMPORT_MAJOR_VERSION = 1
+
+@QmlElement
+class PullRequestModel(QAbstractListModel):
+
+ _project: Optional[Project]
+
+ NumberRole = Qt.ItemDataRole.UserRole + 1
+ TitleRole = NumberRole + 1
+
+ def __init__(self):
+ super().__init__()
+
+ def data(self,
+ index: QModelIndex | QPersistentModelIndex,
+ role: int = Qt.ItemDataRole.DisplayRole) -> object:
+ if self._project is None:
+ return None
+
+ pull_request = self._project.pullRequests[index.row()]
+ if role == self.NumberRole:
+ return pull_request.number
+ if role == self.TitleRole:
+ return pull_request.title
+ if role == Qt.ItemDataRole.DisplayRole:
+ return f'{pull_request.number} - {pull_request.title}'
+ return None
+
+ def rowCount(self, parent: QModelIndex | QPersistentModelIndex = QModelIndex()) -> int:
+ if self._project is not None:
+ return len(self._project.pullRequests)
+ return 0
+
+ def roleNames(self) -> dict[int, QByteArray]:
+ return {
+ self.NumberRole: QByteArray(b"number"),
+ self.TitleRole: QByteArray(b"title"),
+ }
+
+ def get_project(self) -> Optional[Project]:
+ return self._project
+
+ def set_project(self, project: Optional[Project])-> None:
+ if project is None:
+ return
+ self._project = project
+ print("Connecting!")
+ self._project.pullRequestChanged.connect(self._reset_model)
+
+ project = Property(Project, fget=get_project, fset=set_project)
+
+ def _reset_model(self) -> None:
+ print("Reseting pull request model")
+ self.beginResetModel()
+ self.endResetModel()
diff --git a/kodereviewer/qml/AddRepositoryPage.qml b/kodereviewer/qml/AddRepositoryPage.qml
new file mode 100644
index 0000000..2bd26fe
--- /dev/null
+++ b/kodereviewer/qml/AddRepositoryPage.qml
@@ -0,0 +1,97 @@
+import QtCore
+import QtQuick 6.7
+import QtQuick.Controls 6 as QQC2
+import QtQuick.Layouts 6.7
+
+import org.kde.kirigami as Kirigami
+import org.kde.kirigamiaddons.formcard as FormCard
+
+FormCard.FormCardPage {
+ id: root
+ title: "Add a new repository"
+
+ signal accepted(string url, string name, string owner, string displayName)
+
+ FormCard.FormHeader {
+ title: "Github information"
+ }
+
+ FormCard.FormCard {
+ FormCard.FormTextFieldDelegate {
+ id: urlField
+ label: "URL"
+ onTextChanged: root.fillDataFromUrl(text)
+ }
+
+ FormCard.FormDelegateSeparator {}
+
+ FormCard.FormTextFieldDelegate {
+ id: nameField
+ label: "Name"
+ }
+
+ FormCard.FormDelegateSeparator {}
+
+ FormCard.FormTextFieldDelegate {
+ id: ownerField
+ label: "Owner"
+ }
+
+ FormCard.FormDelegateSeparator {}
+ }
+
+ FormCard.FormHeader {
+ title: "General information"
+ }
+
+ FormCard.FormCard {
+ FormCard.FormTextFieldDelegate {
+ id: displayNameField
+ label: "Display name"
+ }
+ }
+
+
+ FormCard.FormHeader {
+ title: "Git"
+ }
+
+ FormCard.FormCard {
+ FormCard.FormCheckDelegate {
+ id: cloneCheck
+ text: "Clone repository"
+ checked: false
+ }
+
+ FormCard.FormDelegateSeparator {}
+
+ FormCard.FormTextFieldDelegate {
+ id: cloneDirectory
+ label: "Clone directory"
+ enabled: cloneCheck.checked
+ }
+ }
+
+
+ footer: QQC2.ToolBar {
+ contentItem: QQC2.DialogButtonBox {
+ standardButtons: QQC2.Dialog.Ok | QQC2.Dialog.Cancel
+ onAccepted: root.accepted(
+ urlField.text,
+ nameField.text,
+ ownerField.text,
+ displayNameField.text
+ )
+ onRejected: applicationWindow().pageStack.pop()
+ }
+ }
+
+ function fillDataFromUrl(text) {
+ const s = text.split("/")
+ if (s.length > 2) {
+ nameField.text = s[s.length -1]
+ ownerField.text = s[s.length - 2]
+ displayNameField.text = s[s.length -1]
+ }
+ }
+}
diff --git a/kodereviewer/qml/Main.qml b/kodereviewer/qml/Main.qml
new file mode 100644
index 0000000..0b41a05
--- /dev/null
+++ b/kodereviewer/qml/Main.qml
@@ -0,0 +1,85 @@
+pragma ComponentBehavior: Bound
+import QtQuick
+import QtCore
+import QtQuick.Controls as Controls
+import QtQuick.Layouts
+
+import org.kde.kirigami as Kirigami
+import org.kde.kirigamiaddons.formcard as FormCard
+
+import org.deprecated.kodereviewer 1.0
+
+Kirigami.ApplicationWindow {
+ id: root
+
+ title: qsTr("Kode Reviewer")
+
+ minimumWidth: Kirigami.Units.gridUnit * 20
+ minimumHeight: Kirigami.Units.gridUnit * 20
+ width: minimumWidth
+ height: minimumHeight
+
+ signal projectSelected()
+
+ property Project project
+
+ property NetworkManager connection: NetworkManager {
+ project: root.project
+ }
+
+ Settings {
+ id: settings
+ property alias width: root.width
+ property alias height: root.height
+ property string githubToken: ""
+ }
+
+ pageStack.initialPage: initPage
+
+ Component {
+ id: initPage
+ WelcomePage {
+ onProjectSelected: project => {
+ root.project = project
+ root.projectSelected()
+ }
+ }
+ }
+
+ Loader {
+ id: projectListPageLoader
+ active: false
+ sourceComponent: Component {
+ ProjectListPage {
+ connection: root.connection
+ project: root.project
+ }
+ }
+ }
+
+ Loader {
+ id: placeHolderPageLoader
+ active: false
+ sourceComponent: Component {
+ Kirigami.Page {
+
+ Kirigami.Theme.colorSet: Kirigami.Theme.View
+ Kirigami.Theme.inherit: false
+ title: "Select a pull request"
+ spacing: Kirigami.Units.largeSpacing * 2
+ Kirigami.PlaceholderMessage {
+ anchors.centerIn: parent
+ icon.name: "org.deprecated.kodereviewer"
+ text: "Select a pull request"
+ }
+ }
+ }
+ }
+
+ onProjectSelected: {
+ projectListPageLoader.active = true
+ placeHolderPageLoader.active = true
+ pageStack.replace(projectListPageLoader.item)
+ pageStack.push(placeHolderPageLoader.item)
+ }
+}
diff --git a/kodereviewer/qml/ProjectListPage.qml b/kodereviewer/qml/ProjectListPage.qml
new file mode 100644
index 0000000..334799e
--- /dev/null
+++ b/kodereviewer/qml/ProjectListPage.qml
@@ -0,0 +1,130 @@
+pragma ComponentBehavior: Bound
+import QtQuick 2.15 // Removing version break onCurrentItemChanged
+import QtQuick.Layouts
+import QtQuick.Controls as QQC2
+
+import org.kde.kirigamiaddons.delegates as Delegates
+import org.kde.kitemmodels 1 as KItemModels
+import org.kde.kirigami as Kirigami
+
+import org.deprecated.kodereviewer
+
+Kirigami.Page {
+ id: root
+
+ required property NetworkManager connection
+ required property Project project
+
+
+ readonly property int currentWidth: _private.currentWidth + 1
+
+ onCurrentWidthChanged: pageStack.defaultColumnWidth = root.currentWidth
+ Component.onCompleted: {
+ pageStack.defaultColumnWidth = root.currentWidth
+ connection.getPullRequests()
+ }
+
+ Kirigami.Theme.colorSet: Kirigami.Theme.View
+ Kirigami.Theme.inherit: false
+
+ PullRequestModel {
+ id: pullRequestModel
+ project: root.project
+ }
+
+ KItemModels.KSortFilterProxyModel {
+ id: pullRequestFilterModel
+ sourceModel: pullRequestModel
+ filterRoleName: "title"
+ }
+
+ title: "Pull Requests"
+
+ actions: [
+ Kirigami.Action {
+ id: searchAction
+ icon.name: "search"
+ shortcut: Shortcut {
+ sequence: "Ctrl+F"
+ onActivated: {
+ print("Shortcut triggered")
+ searchAction.trigger()
+ }
+ }
+
+ onTriggered: print("search triggered")
+ }
+ ]
+
+ contentItem: QQC2.StackView {
+ id: stackView
+ anchors.fill: parent
+
+ initialItem: pullRequestListView
+
+ Component {
+ id: pullRequestListView
+ QQC2.ScrollView {
+ ListView {
+ id: view
+ model: pullRequestFilterModel
+ clip: true
+ delegate: Delegates.RoundedItemDelegate {
+ required property int number
+ required property string title
+ required property int index
+ highlighted: ListView.isCurrentItem
+
+ text: `${number} - ${title}`
+ icon.name: "vcs-merge-request"
+ }
+ }
+ }
+ }
+ }
+
+ MouseArea {
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ parent: applicationWindow().overlay.parent
+
+ x: root.currentWidth - width / 2
+ width: Kirigami.Units.smallSpacing * 2
+ z: root.z + 1
+ enabled: true
+ visible: enabled
+ cursorShape: Qt.SplitHCursor
+
+ property int _lastX
+
+ onPressed: mouse => {
+ _lastX = mouse.x;
+ }
+ onPositionChanged: mouse => {
+ if (_lastX == -1) {
+ return;
+ }
+ if (mouse.x > _lastX) {
+ // _private.currentWidth = _private.currentWidth + (_lastX + mouse.x);
+ _private.currentWidth = Math.min(_private.defaultWidth, _private.currentWidth + (mouse.x - _lastX))
+ } else if (mouse.x < _lastX) {
+ const tmpWidth = _private.currentWidth - (_lastX - mouse.x);
+ if (tmpWidth > _private.minWidth)
+ _private.currentWidth = tmpWidth;
+
+ }
+ }
+ }
+
+ /*
+ * Hold the modifiable currentWidth in a private object so that only internal
+ * members can modify it.
+ */
+ QtObject {
+ id: _private
+ property int currentWidth: defaultWidth
+ readonly property int defaultWidth: Kirigami.Units.gridUnit * 17
+ readonly property int minWidth: Kirigami.Units.gridUnit * 2
+ }
+
+}
diff --git a/kodereviewer/qml/SettingsPage.qml b/kodereviewer/qml/SettingsPage.qml
new file mode 100644
index 0000000..83e5c90
--- /dev/null
+++ b/kodereviewer/qml/SettingsPage.qml
@@ -0,0 +1,39 @@
+import QtCore
+import QtQuick 6.7
+import QtQuick.Controls 6 as QQC2
+import QtQuick.Layouts 6.7
+
+import org.kde.kirigami as Kirigami
+import org.kde.kirigamiaddons.formcard as FormCard
+
+
+FormCard.FormCardPage {
+ id: root
+
+ actions: [
+ Kirigami.Action {
+ id: saveAction
+ text: "Save"
+ onTriggered: {
+ settings.sync()
+ }
+ }
+ ]
+
+ Settings {
+ id: settings
+ property string githubToken: githubTokenField.text
+ }
+
+ FormCard.FormHeader {
+ title: "Authorization"
+ }
+
+ FormCard.FormCard {
+ FormCard.FormTextFieldDelegate {
+ id: githubTokenField
+ text: settings.githubToken
+ label: "Github Token"
+ }
+ }
+}
diff --git a/kodereviewer/qml/WelcomePage.qml b/kodereviewer/qml/WelcomePage.qml
new file mode 100644
index 0000000..af74d01
--- /dev/null
+++ b/kodereviewer/qml/WelcomePage.qml
@@ -0,0 +1,117 @@
+import QtCore
+import QtQuick 6.7
+import QtQuick.Controls 6 as QQC2
+import QtQuick.Layouts 6.7
+
+import org.kde.kirigami as Kirigami
+import org.kde.kirigamiaddons.formcard as FormCard
+import org.kde.kirigamiaddons.settings as KirigamiSettings
+
+import org.deprecated.kodereviewer
+
+FormCard.FormCardPage {
+ id: root
+
+ title: "Welcome"
+
+ property int projectCount: projectModel.rowCount()
+
+ signal projectSelected(Project project)
+
+ ProjectModel {
+ id: projectModel
+
+ onModelReset: {
+ projectCount = projectModel.rowCount()
+ }
+ }
+
+ Component {
+ id: addRepositoryPage
+ AddRepositoryPage {
+ onAccepted: (url, name, owner, displayName) => {
+ projectModel.add(displayName, owner, url)
+ applicationWindow().pageStack.pop()
+ }
+ }
+ }
+ KirigamiSettings.ConfigurationView {
+ id: configuration
+
+ window: applicationWindow() as Kirigami.ApplicationWindow
+
+ modules: [
+ KirigamiSettings.ConfigurationModule {
+ moduleId: "appearance"
+ text: i18nc("@action:button", "General")
+ icon.name: "preferences-system-symbolic"
+ page: () => Qt.createComponent("SettingsPage.qml")
+ },
+ KirigamiSettings.ConfigurationModule {
+ moduleId: "about"
+ text: i18nc("@action:button", "About Kode Reviewer")
+ icon.name: "help-about"
+ page: () => Qt.createComponent("org.kde.kirigamiaddons.formcard", "AboutPage")
+ category: i18nc("@title:group", "About")
+ },
+ KirigamiSettings.ConfigurationModule {
+ moduleId: "aboutkde"
+ text: i18nc("@action:button", "About KDE")
+ icon.name: "kde"
+ page: () => Qt.createComponent("org.kde.kirigamiaddons.formcard", "AboutKDE")
+ category: i18nc("@title:group", "About")
+ }
+ ]
+ }
+
+ Kirigami.Heading {
+ id: welcomeMessage
+
+ text: "Welcome to Kode Reviewer"
+
+ Layout.alignment: Qt.AlignHCenter
+ Layout.topMargin: Kirigami.Units.largeSpacing
+ }
+
+ FormCard.FormHeader {
+ id: existingProjectsHeader
+ title: "Existing projects"
+ visible: root.projectCount > 0
+ }
+
+ FormCard.FormCard {
+ visible: existingProjectsHeader.visible
+
+ Repeater {
+ id: loadedProjects
+ model: projectModel
+ delegate: FormCard.FormButtonDelegate {
+ required property string name
+ required property string url
+ required property int index
+ text: name
+ description: url
+ onClicked: root.projectSelected(projectModel.get(index))
+ }
+ }
+ }
+
+ FormCard.FormHeader {
+ title: "Add new project"
+ }
+
+ FormCard.FormCard {
+ FormCard.FormButtonDelegate {
+ text: "Add new project"
+ onClicked: applicationWindow().pageStack.push(addRepositoryPage)
+ }
+ }
+
+ FormCard.FormCard {
+ FormCard.FormButtonDelegate {
+ text: "Settings"
+ icon.name: 'settings-configure-symbolic'
+ onClicked: configuration.open()
+ }
+ }
+}
diff --git a/org.deprecated.kodereviewer.desktop b/org.deprecated.kodereviewer.desktop
new file mode 100755
index 0000000..e4ae541
--- /dev/null
+++ b/org.deprecated.kodereviewer.desktop
@@ -0,0 +1,8 @@
+[Desktop Entry]
+Name=Kode reviewer
+Exec=kode-reviewer
+Icon=kode-reviewer
+Comment=A tool to make code reviews
+Type=Application
+Terminal=false
+Categories=Development;Debugger;Qt;
diff --git a/org.deprecated.kodereviewer.svg b/org.deprecated.kodereviewer.svg
new file mode 100644
index 0000000..305cd1d
--- /dev/null
+++ b/org.deprecated.kodereviewer.svg
@@ -0,0 +1,314 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ width="48"
+ height="48"
+ viewBox="0 0 48 48"
+ version="1.1"
+ id="svg1"
+ inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
+ sodipodi:docname="kodereviewer.svg"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <sodipodi:namedview
+ id="namedview1"
+ pagecolor="#ffffff"
+ bordercolor="#000000"
+ borderopacity="0.25"
+ inkscape:showpageshadow="2"
+ inkscape:pageopacity="0.0"
+ inkscape:pagecheckerboard="0"
+ inkscape:deskcolor="#d1d1d1"
+ inkscape:document-units="px"
+ showguides="false"
+ inkscape:zoom="4"
+ inkscape:cx="6"
+ inkscape:cy="23.375"
+ inkscape:window-width="1920"
+ inkscape:window-height="1022"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="layer5">
+ <sodipodi:guide
+ position="23.9975,-4.6411364"
+ orientation="1,0"
+ id="guide5"
+ inkscape:locked="false" />
+ <sodipodi:guide
+ position="4.2472727,24"
+ orientation="0,1"
+ id="guide6"
+ inkscape:locked="false"
+ inkscape:label=""
+ inkscape:color="rgb(0,134,229)" />
+ </sodipodi:namedview>
+ <defs
+ id="defs1">
+ <linearGradient
+ id="swatch67">
+ <stop
+ style="stop-color:#ffffff;stop-opacity:1;"
+ offset="0"
+ id="stop67" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient58"
+ inkscape:collect="always"
+ inkscape:label="shadow">
+ <stop
+ style="stop-color:#1a1212;stop-opacity:0.36863258;"
+ offset="0"
+ id="stop57" />
+ <stop
+ style="stop-color:#fefefe;stop-opacity:0;"
+ offset="1"
+ id="stop58" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient55"
+ inkscape:label="KDE">
+ <stop
+ style="stop-color:#00cbff;stop-opacity:1;"
+ offset="0.00026041"
+ id="stop54" />
+ <stop
+ style="stop-color:#ff2291;stop-opacity:1;"
+ offset="1"
+ id="stop55" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient15"
+ inkscape:collect="always">
+ <stop
+ style="stop-color:#d0d0d0;stop-opacity:1;"
+ offset="0"
+ id="stop15" />
+ <stop
+ style="stop-color:#fafafa;stop-opacity:1;"
+ offset="1"
+ id="stop16" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient2"
+ inkscape:label="KDE">
+ <stop
+ style="stop-color:#00cbff;stop-opacity:1;"
+ offset="0"
+ id="stop4" />
+ <stop
+ style="stop-color:#00cbff;stop-opacity:1;"
+ offset="0.99973959"
+ id="stop5" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient2-1"
+ inkscape:label="KDE">
+ <stop
+ style="stop-color:#00cbff;stop-opacity:1;"
+ offset="0.00026041"
+ id="stop3" />
+ <stop
+ style="stop-color:#228eff;stop-opacity:1;"
+ offset="1"
+ id="stop2" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2-1"
+ id="linearGradient3"
+ x1="4"
+ y1="24"
+ x2="44"
+ y2="24"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0.975,0,0,1,0.1,0)" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient15"
+ id="linearGradient16"
+ x1="19.682396"
+ y1="16.570776"
+ x2="27.682396"
+ y2="16.570776"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(-52.477341,-19.080291)" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient15"
+ id="linearGradient18"
+ x1="29.232605"
+ y1="25.395569"
+ x2="37.232605"
+ y2="25.395569"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(-58.628174,7.8370361)" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient15"
+ id="linearGradient20"
+ x1="19.814938"
+ y1="30.319584"
+ x2="27.814938"
+ y2="30.319584"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(-59.265166,-15.163406)" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient58"
+ id="linearGradient33"
+ x1="32.493103"
+ y1="25.552551"
+ x2="47.312561"
+ y2="26.146179"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="rotate(45,33.255293,25.325471)" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient58"
+ id="linearGradient35"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(-4.9084664,-30.418656)"
+ x1="33.445984"
+ y1="25.811829"
+ x2="47.096008"
+ y2="25.455994" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient58"
+ id="linearGradient68"
+ gradientUnits="userSpaceOnUse"
+ x1="24.064537"
+ y1="30.420015"
+ x2="32.667351"
+ y2="39.118153" />
+ </defs>
+ <g
+ inkscape:label="Fondo"
+ inkscape:groupmode="layer"
+ id="layer1"
+ style="display:inline">
+ <g
+ id="g7"
+ transform="rotate(90,24,24)">
+ <circle
+ style="display:inline;fill:#0374ea;fill-opacity:1;stroke-width:1.085;stroke-dasharray:none"
+ id="path1-6"
+ cx="24"
+ cy="24"
+ r="20"
+ inkscape:label="sombra" />
+ <ellipse
+ style="display:inline;fill:url(#linearGradient3);stroke-width:1.07135;stroke-dasharray:none"
+ id="path1"
+ cx="23.5"
+ cy="24"
+ rx="19.5"
+ ry="20"
+ inkscape:label="fondo" />
+ </g>
+ </g>
+ <g
+ inkscape:groupmode="layer"
+ id="layer5"
+ inkscape:label="shadow">
+ <path
+ d="m 36.873047,37.724609 -10.363281,-10.365234 -0.002,0.002 a 4,4 0 0 1 1.169922,1.923828 4,4 0 0 1 -2.828125,4.898438 4,4 0 0 1 -3.988281,-1.177735 l -0.0098,0.0098 8.699219,8.699219 a 19.5,19.012501 0 0 0 7.322266,-3.990235 z"
+ style="fill:url(#linearGradient68);stroke-width:1.085;fill-rule:nonzero;fill-opacity:1"
+ id="path62" />
+ <path
+ d="m 42.613281,29.125 -6.689453,-6.6875 -5.65625,5.65625 8.166016,8.166016 A 19.5,19.012501 0 0 0 42.613281,29.125 Z"
+ style="display:inline;opacity:1;vector-effect:none;fill:url(#linearGradient33);stroke-width:1.085;stop-color:#000000;stop-opacity:1"
+ id="path57" />
+ <path
+ id="path51"
+ style="fill:url(#linearGradient35);stroke-width:1.085;stroke-dasharray:none"
+ d="M 28.643349 -9.0238979 A 4 4 0 0 1 32.326657 -6.0642693 A 4 4 0 0 1 29.49823 -1.1642403 A 4 4 0 0 1 28.190359 -1.0468495 L 28.190359 -1.0219903 L 48.189603 -1.0233713 L 48.189603 -1.5371286 L 41.267691 -1.5371286 L 41.26631 -1.566131 A 4 4 0 0 1 38.62847 -2.7124174 A 4 4 0 0 1 38.62847 -8.3692717 A 4 4 0 0 1 39.490256 -9.0238979 L 28.643349 -9.0238979 z "
+ transform="rotate(45)" />
+ <g
+ id="g44-5"
+ style="display:inline" />
+ </g>
+ <g
+ inkscape:groupmode="layer"
+ id="layer4"
+ inkscape:label="Git lines"
+ style="display:inline">
+ <g
+ id="g43">
+ <rect
+ style="display:inline;fill:#f9f9f9;fill-opacity:0.7;stroke-width:1.085;stroke-dasharray:none"
+ id="rect13-8"
+ width="2"
+ height="12"
+ x="4.5583954"
+ y="30.228466"
+ transform="rotate(-45)" />
+ <path
+ d="m 15.826172,6.8457031 c -0.629358,0.300465 -1.239339,0.6346349 -1.828125,1 L 22.402344,16.25 23.816406,14.835938 Z"
+ style="display:inline;fill:#f9f9f9;fill-opacity:0.7;stroke-width:1.085"
+ id="path32" />
+ <rect
+ style="display:inline;fill:#f9f9f9;fill-opacity:0.7;stroke-width:1.085;stroke-dasharray:none"
+ id="rect13-3"
+ width="2"
+ height="12"
+ x="22.973835"
+ y="17.202845" />
+ </g>
+ </g>
+ <g
+ inkscape:groupmode="layer"
+ id="layer2"
+ inkscape:label="Git"
+ style="display:inline">
+ <g
+ id="g44">
+ <circle
+ style="fill:url(#linearGradient18);stroke-width:2.06845;stroke-dasharray:none"
+ id="path5-5"
+ cx="-25.395569"
+ cy="33.232605"
+ r="4"
+ transform="rotate(-90)" />
+ <circle
+ style="display:inline;fill:url(#linearGradient16);stroke-width:2.06845;stroke-dasharray:none"
+ id="path5"
+ cx="-28.794945"
+ cy="-2.509515"
+ r="4"
+ transform="rotate(-150)" />
+ <circle
+ style="fill:url(#linearGradient20);stroke-width:2.06845;stroke-dasharray:none"
+ id="path5-2"
+ cx="-35.45023"
+ cy="15.156178"
+ r="4"
+ transform="rotate(-105)" />
+ </g>
+ </g>
+ <g
+ id="g32"
+ style="display:none">
+ <path
+ d="M 43.990234,23.623047 C 43.92087,34.340771 34.992755,42.99498 24,43 13.059009,42.991474 4.1556521,34.412714 4.0175781,23.746094 4.0111803,23.830691 4.0053208,23.915328 4,24 4,35.045684 12.954316,44 24,44 35.045684,44 44,35.045684 44,24 43.9979,23.874321 43.9947,23.748664 43.9902,23.623047 Z"
+ style="display:inline;fill:#1a1a1a;stroke-width:1.085"
+ id="path31" />
+ <path
+ d="M 22.970703,4.0253906 C 12.403316,4.5476611 4,13.067006 4,23.5 4.00533,23.58207 4.011187,23.6641 4.017578,23.746094 4.1520846,13.151708 12.506458,4.5645588 22.970703,4.0253906 Z"
+ style="display:inline;fill:#1a1a1a;stroke-width:1.07135"
+ id="path30" />
+ <path
+ style="display:inline;fill:#1a1a1a;stroke-width:1.07135"
+ d="M 24,4 C 13.059565,4.008734 4.1564669,12.806537 4.0175781,23.746094 4.1556524,34.412735 13.058987,42.991474 24,43 13.5066,43 5,34.4934 5,24 5,13.5066 13.5066,5 24,5 34.4934,5 43,13.5066 43,24 43,34.4934 34.4934,43 24,43 34.992777,42.99498 43.92087,34.340793 43.990234,23.623047 43.791369,13.073716 35.450854,4.557314 25.029297,4.0253906 24.688408,4.0079914 24.34513,4.0001072 24,4 Z"
+ id="path29" />
+ <path
+ d="M 24,4 A 20,20 0 0 1 43.990234,23.623047 20,19.5 0 0 0 44,23.5 20,19.5 0 0 0 24,4 Z"
+ style="display:inline;fill:#1a1a1a;stroke-width:1.07135"
+ id="path28" />
+ </g>
+</svg>
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..4bed84a
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,32 @@
+[project]
+name = "org.deprecated.kodereviewer"
+version = "0.1"
+authors = [
+ {name = "Matias Linares", email = "matias@deprecated.org" }
+]
+description = "Code review tool"
+readme = "README.md"
+
+dependencies = [
+ "PySide6",
+ "markdown"
+]
+
+[options]
+packages = "kodereviewer"
+include_package_data = true
+
+[build-system]
+requires = ["setuptools", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[options.data_files]
+"share/applications" = "org.deprecated.kodereviewer.desktop"
+"share/icons/hicolor/scalable/apps" = "org.deprecated.kodereviewer.svg"
+
+[project.scripts]
+kodereviewer = "kodereviewer.app:main"
+
+[tool.pyright]
+venv = "venv"
+venvPath = "."