From b469738c92f4ca75a974eeda83bf2725967c8b6b Mon Sep 17 00:00:00 2001 From: Hamish Campbell Date: Wed, 14 Aug 2024 22:35:34 +1200 Subject: [PATCH] Initial processing support for the Kart plugin --- kart/kartapi.py | 13 ++- kart/metadata.txt | 1 + kart/plugin.py | 12 ++- kart/processing/__init__.py | 32 ++++++++ kart/processing/base.py | 16 ++++ kart/processing/branches.py | 136 +++++++++++++++++++++++++++++++ kart/processing/data.py | 73 +++++++++++++++++ kart/processing/remotes.py | 112 ++++++++++++++++++++++++++ kart/processing/repos.py | 156 ++++++++++++++++++++++++++++++++++++ kart/processing/tags.py | 49 +++++++++++ 10 files changed, 596 insertions(+), 4 deletions(-) create mode 100644 kart/processing/__init__.py create mode 100644 kart/processing/base.py create mode 100644 kart/processing/branches.py create mode 100644 kart/processing/data.py create mode 100644 kart/processing/remotes.py create mode 100644 kart/processing/repos.py create mode 100644 kart/processing/tags.py diff --git a/kart/kartapi.py b/kart/kartapi.py index 3d59f70..00485c1 100644 --- a/kart/kartapi.py +++ b/kart/kartapi.py @@ -293,6 +293,7 @@ def generate_clone_arguments( dst: str, location: Optional[str] = None, extent: Optional[QgsReferencedRectangle] = None, + depth: Optional[int] = None, username: Optional[str] = None, password: Optional[str] = None, ) -> List[str]: @@ -313,6 +314,8 @@ def generate_clone_arguments( if extent is not None: kart_extent = f"{extent.crs().authid()};{extent.asWktPolygon()}" commands.extend(["--spatial-filter", kart_extent]) + if depth is not None: + commands.extend(["--depth", str(depth)]) return commands @@ -322,6 +325,7 @@ def clone( dst: str, location: Optional[str] = None, extent: Optional[QgsReferencedRectangle] = None, + depth: Optional[int] = None, username: Optional[str] = None, password: Optional[str] = None, output_handler: Callable[[str], None] = None, @@ -330,7 +334,7 @@ def clone( Performs a (blocking, main thread only) clone operation """ commands = Repository.generate_clone_arguments( - src, dst, location, extent, username, password + src, dst, location, extent, depth, username, password ) executeKart(commands, feedback=output_handler) return Repository(dst) @@ -396,8 +400,11 @@ def init(self, location=None): else: self.executeKart(["init"]) - def importIntoRepo(self, source): - self.executeKart(["import", source]) + def importIntoRepo(self, source, dataset=None): + importArgs = [source] + if dataset: + importArgs += ["--dataset", dataset] + self.executeKart(["import"] + importArgs) def checkUserConfigured(self): configDict = self._config() diff --git a/kart/metadata.txt b/kart/metadata.txt index 889da78..9d7132f 100644 --- a/kart/metadata.txt +++ b/kart/metadata.txt @@ -23,3 +23,4 @@ repository=https://github.com/koordinates/kart-qgis-plugin tracker=https://github.com/koordinates/kart-qgis-plugin/issues icon=img/kart.png category=Plugins +hasProcessingProvider=yes diff --git a/kart/plugin.py b/kart/plugin.py index f74c253..08cc4fc 100644 --- a/kart/plugin.py +++ b/kart/plugin.py @@ -2,7 +2,7 @@ import os import platform -from qgis.core import QgsProject, Qgis, QgsMessageOutput +from qgis.core import QgsApplication, QgsProject, Qgis, QgsMessageOutput from qgis.PyQt.QtCore import Qt from qgis.PyQt.QtWidgets import QAction @@ -11,6 +11,7 @@ from kart.gui.settingsdialog import SettingsDialog from kart.kartapi import checkKartInstalled, kartVersionDetails from kart.layers import LayerTracker +from kart.processing import KartProvider pluginPath = os.path.dirname(__file__) @@ -19,6 +20,11 @@ class KartPlugin(object): def __init__(self, iface): self.iface = iface + self.provider = None + + def initProcessing(self): + self.provider = KartProvider() + QgsApplication.processingRegistry().addProvider(self.provider) def initGui(self): @@ -43,6 +49,8 @@ def initGui(self): QgsProject.instance().layerWasAdded.connect(self.tracker.layerAdded) QgsProject.instance().crsChanged.connect(self.tracker.updateRubberBands) + self.initProcessing() + def showDock(self): if checkKartInstalled(): self.dock.show() @@ -87,3 +95,5 @@ def unload(self): QgsProject.instance().layerRemoved.disconnect(self.tracker.layerRemoved) QgsProject.instance().layerWasAdded.disconnect(self.tracker.layerAdded) + + QgsApplication.processingRegistry().removeProvider(self.provider) diff --git a/kart/processing/__init__.py b/kart/processing/__init__.py new file mode 100644 index 0000000..1e8cba5 --- /dev/null +++ b/kart/processing/__init__.py @@ -0,0 +1,32 @@ +from qgis.core import QgsProcessingProvider + +from kart.gui import icons + +from .branches import RepoCreateBranch, RepoDeleteBranch, RepoSwitchBranch +from .data import RepoImportData +from .remotes import RepoPullFromRemote, RepoPushToRemote +from .repos import RepoClone, RepoInit +from .tags import RepoCreateTag + + +class KartProvider(QgsProcessingProvider): + def loadAlgorithms(self, *args, **kwargs): + + self.addAlgorithm(RepoInit()) + self.addAlgorithm(RepoClone()) + self.addAlgorithm(RepoCreateTag()) + self.addAlgorithm(RepoSwitchBranch()) + self.addAlgorithm(RepoCreateBranch()) + self.addAlgorithm(RepoDeleteBranch()) + self.addAlgorithm(RepoImportData()) + self.addAlgorithm(RepoPullFromRemote()) + self.addAlgorithm(RepoPushToRemote()) + + def id(self, *args, **kwargs): + return "Kart" + + def name(self, *args, **kwargs): + return self.tr("Kart") + + def icon(self): + return icons.kartIcon diff --git a/kart/processing/base.py b/kart/processing/base.py new file mode 100644 index 0000000..85cd783 --- /dev/null +++ b/kart/processing/base.py @@ -0,0 +1,16 @@ +from qgis.PyQt.QtCore import QCoreApplication +from qgis.core import QgsProcessingAlgorithm + + +class KartAlgorithm(QgsProcessingAlgorithm): + def createInstance(self): + return type(self)() + + def name(self): + return f"kart_{self.__class__.__name__.lower()}" + + def tr(self, string): + return QCoreApplication.translate("Processing", string) + + def initAlgorithm(self, config=None): + return {} diff --git a/kart/processing/branches.py b/kart/processing/branches.py new file mode 100644 index 0000000..8637620 --- /dev/null +++ b/kart/processing/branches.py @@ -0,0 +1,136 @@ +from qgis.core import QgsProcessingParameterFile, QgsProcessingParameterString +from kart.gui import icons + +from .base import KartAlgorithm + + +class RepoCreateBranch(KartAlgorithm): + REPO_PATH = "REPO_PATH" + REPO_BRANCH_NAME = "REPO_BRANCH_NAME" + + def displayName(self): + return self.tr("Create Branch") + + def shortHelpString(self): + return self.tr("Create a new branch") + + def icon(self): + return icons.createBranchIcon + + def initAlgorithm(self, config=None): + + self.addParameter( + QgsProcessingParameterFile( + self.REPO_PATH, + self.tr("Repo Path"), + behavior=QgsProcessingParameterFile.Folder, + ) + ) + + self.addParameter( + QgsProcessingParameterString( + self.REPO_BRANCH_NAME, + self.tr("Branch Name"), + ) + ) + + def processAlgorithm(self, parameters, context, feedback): + from kart.kartapi import Repository + + repo_path = self.parameterAsFile(parameters, self.REPO_PATH, context) + branch_name = self.parameterAsString(parameters, self.REPO_REFISH, context) + + repo = Repository(repo_path) + repo.createBranch(branch_name) + + return { + self.REPO_PATH: repo_path, + } + + +class RepoSwitchBranch(KartAlgorithm): + REPO_PATH = "REPO_PATH" + REPO_BRANCH_NAME = "REPO_BRANCH_NAME" + + def displayName(self): + return self.tr("Switch to Branch") + + def shortHelpString(self): + return self.tr("Switches to a named branch") + + def icon(self): + return icons.checkoutIcon + + def initAlgorithm(self, config=None): + + self.addParameter( + QgsProcessingParameterFile( + self.REPO_PATH, + self.tr("Repo Path"), + behavior=QgsProcessingParameterFile.Folder, + ) + ) + + self.addParameter( + QgsProcessingParameterString( + self.REPO_BRANCH_NAME, + self.tr("Branch Name"), + ) + ) + + def processAlgorithm(self, parameters, context, feedback): + from kart.kartapi import Repository + + repo_path = self.parameterAsFile(parameters, self.REPO_PATH, context) + branch_name = self.parameterAsString(parameters, self.REPO_BRANCH_NAME, context) + + repo = Repository(repo_path) + repo.checkoutBranch(branch_name) + + return { + self.REPO_PATH: repo_path, + } + + +class RepoDeleteBranch(KartAlgorithm): + REPO_PATH = "REPO_PATH" + REPO_BRANCH_NAME = "REPO_BRANCH_NAME" + + def displayName(self): + return self.tr("Delete Branch") + + def shortHelpString(self): + return self.tr("Delete a branch") + + def icon(self): + return icons.deleteIcon + + def initAlgorithm(self, config=None): + + self.addParameter( + QgsProcessingParameterFile( + self.REPO_PATH, + self.tr("Repo Path"), + behavior=QgsProcessingParameterFile.Folder, + ) + ) + + self.addParameter( + QgsProcessingParameterString( + self.REPO_BRANCH_NAME, + self.tr("Branch Name"), + ) + ) + + def processAlgorithm(self, parameters, context, feedback): + from kart.kartapi import Repository + + repo_path = self.parameterAsFile(parameters, self.REPO_PATH, context) + branch_name = self.parameterAsString(parameters, self.REPO_BRANCH_NAME, context) + + repo = Repository(repo_path) + repo.deleteBranch(branch_name) + + return { + self.REPO_PATH: repo_path, + } diff --git a/kart/processing/data.py b/kart/processing/data.py new file mode 100644 index 0000000..50b65d1 --- /dev/null +++ b/kart/processing/data.py @@ -0,0 +1,73 @@ +from qgis.core import ( + QgsProcessingParameterFile, + QgsProcessingParameterString, + QgsProcessingOutputFolder, +) +from kart.gui import icons + +from .base import KartAlgorithm + + +class RepoImportData(KartAlgorithm): + REPO_PATH = "REPO_PATH" + REPO_DATA_PATH = "REPO_DATA_PATH" + REPO_DATASET_NAME = "REPO_DATASET_NAME" + + def displayName(self): + return self.tr("Import Data") + + def shortHelpString(self): + return self.tr("Import data into a repository") + + def icon(self): + return icons.importIcon + + def initAlgorithm(self, config=None): + + self.addParameter( + QgsProcessingParameterFile( + self.REPO_PATH, + self.tr("Repo Path"), + behavior=QgsProcessingParameterFile.Folder, + ) + ) + + self.addParameter( + QgsProcessingParameterFile( + self.REPO_DATA_PATH, + self.tr("Data Path"), + behavior=QgsProcessingParameterFile.File, + ) + ) + + self.addParameter( + QgsProcessingParameterString( + self.REPO_DATASET_NAME, + self.tr("Dataset Name"), + optional=True, + ) + ) + + self.addOutput( + QgsProcessingOutputFolder( + self.REPO_PATH, + self.tr("Repo Path"), + ) + ) + + def processAlgorithm(self, parameters, context, feedback): + from kart.kartapi import Repository + + repo_path = self.parameterAsFile(parameters, self.REPO_PATH, context) + data_path = self.parameterAsFile(parameters, self.REPO_DATA_PATH, context) + dataset_name = self.parameterAsString( + parameters, self.REPO_DATASET_NAME, context + ) + + repo = Repository(repo_path) + repo.importIntoRepo(data_path, dataset_name) + + return { + self.REPO_PATH: repo_path, + self.REPO_DATASET_NAME: dataset_name, + } diff --git a/kart/processing/remotes.py b/kart/processing/remotes.py new file mode 100644 index 0000000..e1c6764 --- /dev/null +++ b/kart/processing/remotes.py @@ -0,0 +1,112 @@ +from qgis.core import QgsProcessingParameterFile, QgsProcessingParameterString +from kart.gui import icons + +from .base import KartAlgorithm + + +class RepoPushToRemote(KartAlgorithm): + REPO_PATH = "REPO_PATH" + REPO_BRANCH_NAME = "REPO_BRANCH_NAME" + REPO_REMOTE_NAME = "REPO_REMOTE_NAME" + + def displayName(self): + return self.tr("Push Changes to Remote") + + def shortHelpString(self): + return self.tr("Sync changes in a repository to a remote location") + + def icon(self): + return icons.pushIcon + + def initAlgorithm(self, config=None): + + self.addParameter( + QgsProcessingParameterFile( + self.REPO_PATH, + self.tr("Repo Path"), + behavior=QgsProcessingParameterFile.Folder, + ) + ) + + self.addParameter( + QgsProcessingParameterString( + self.REPO_BRANCH_NAME, + self.tr("Branch Name"), + defaultValue="main", + ) + ) + self.addParameter( + QgsProcessingParameterString( + self.REPO_REMOTE_NAME, + self.tr("Remote Name"), + defaultValue="origin", + ) + ) + + def processAlgorithm(self, parameters, context, feedback): + from kart.kartapi import Repository + + repo_path = self.parameterAsFile(parameters, self.REPO_PATH, context) + branch_name = self.parameterAsString(parameters, self.REPO_BRANCH_NAME, context) + remote_name = self.parameterAsString(parameters, self.REPO_REMOTE_NAME, context) + + repo = Repository(repo_path) + repo.push(remote_name, branch_name) + + return { + self.REPO_PATH: repo_path, + } + + +class RepoPullFromRemote(KartAlgorithm): + REPO_PATH = "REPO_PATH" + REPO_BRANCH_NAME = "REPO_BRANCH_NAME" + REPO_REMOTE_NAME = "REPO_REMOTE_NAME" + + def displayName(self): + return self.tr("Pull Changes from Remote") + + def shortHelpString(self): + return self.tr("Sync changes in a remote location to a local repository") + + def icon(self): + return icons.pullIcon + + def initAlgorithm(self, config=None): + + self.addParameter( + QgsProcessingParameterFile( + self.REPO_PATH, + self.tr("Repo Path"), + behavior=QgsProcessingParameterFile.Folder, + ) + ) + + self.addParameter( + QgsProcessingParameterString( + self.REPO_BRANCH_NAME, + self.tr("Branch Name"), + defaultValue="main", + ) + ) + self.addParameter( + QgsProcessingParameterString( + self.REPO_REMOTE_NAME, + self.tr("Remote Name"), + defaultValue="origin", + ) + ) + + def processAlgorithm(self, parameters, context, feedback): + from kart.kartapi import Repository + + repo_path = self.parameterAsFile(parameters, self.REPO_PATH, context) + branch_name = self.parameterAsString(parameters, self.REPO_BRANCH_NAME, context) + remote_name = self.parameterAsString(parameters, self.REPO_REMOTE_NAME, context) + + repo = Repository(repo_path) + repo.pull(remote_name, branch_name) + + return { + self.REPO_PATH: repo_path, + } diff --git a/kart/processing/repos.py b/kart/processing/repos.py new file mode 100644 index 0000000..ee87f63 --- /dev/null +++ b/kart/processing/repos.py @@ -0,0 +1,156 @@ +from qgis.core import ( + QgsProcessingParameterFile, + QgsProcessingParameterBoolean, + QgsProcessingParameterNumber, + QgsProcessingParameterString, + QgsProcessingParameterExtent, + QgsProcessingParameterFolderDestination, + QgsProcessingOutputMultipleLayers, + QgsReferencedRectangle, +) +from kart.gui import icons + +from .base import KartAlgorithm + + +class RepoInit(KartAlgorithm): + REPO_PATH = "REPO_PATH" + + def displayName(self): + return self.tr("Create Empty Repo") + + def shortHelpString(self): + return self.tr("Create a new empty repository") + + def icon(self): + return icons.createRepoIcon + + def initAlgorithm(self, config=None): + + self.addParameter( + QgsProcessingParameterFile( + self.REPO_PATH, + self.tr("Repo Path"), + behavior=QgsProcessingParameterFile.Folder, + ) + ) + + def processAlgorithm(self, parameters, context, feedback): + from kart.kartapi import Repository + + repo_path = self.parameterAsString(parameters, self.REPO_PATH, context) + + repo = Repository(repo_path) + repo.init() + + return {self.REPO_PATH: repo_path} + + +class RepoClone(KartAlgorithm): + REPO_CLONE_URL = "REPO_CLONE_URL" + REPO_CLONE_REFISH = "REPO_CLONE_REFISH" + REPO_CLONE_DEPTH = "REPO_CLONE_DEPTH" + REPO_CLONE_SPATIAL_EXTENT = "REPO_CLONE_SPATIAL_EXTENT" + REPO_OUTPUT_FOLDER = "REPO_OUTPUT_FOLDER" + REPO_ADD_TO_MAP = "REPO_ADD_TO_MAP" + REPO_OUTPUT_LAYERS = "REPO_OUTPUT_LAYERS" + + def displayName(self): + return self.tr("Clone Repo") + + def shortHelpString(self): + return self.tr("Clones a repository to a folder") + + def icon(self): + return icons.cloneRepoIcon + + def initAlgorithm(self, config=None): + + self.addParameter( + QgsProcessingParameterString( + self.REPO_CLONE_URL, + self.tr("Repo URL"), + ) + ) + + self.addParameter( + QgsProcessingParameterString( + self.REPO_CLONE_REFISH, + self.tr("Branch/Tag/Ref"), + optional=True, + ) + ) + + self.addParameter( + QgsProcessingParameterExtent( + self.REPO_CLONE_SPATIAL_EXTENT, + self.tr("Spatial Extent"), + optional=True, + ) + ) + + self.addParameter( + QgsProcessingParameterNumber( + self.REPO_CLONE_DEPTH, + self.tr("Depth"), + type=QgsProcessingParameterNumber.Integer, + optional=True, + minValue=1, + ) + ) + + self.addParameter( + QgsProcessingParameterFolderDestination( + self.REPO_OUTPUT_FOLDER, self.tr("Output folder") + ) + ) + + self.addParameter( + QgsProcessingParameterBoolean( + self.REPO_ADD_TO_MAP, self.tr("Add layers to the Map") + ) + ) + + self.addOutput( + QgsProcessingOutputMultipleLayers( + self.REPO_OUTPUT_LAYERS, + self.tr("Output Layers"), + ) + ) + + def processAlgorithm(self, parameters, context, feedback): + from kart.kartapi import Repository + + repo_url = self.parameterAsString(parameters, self.REPO_CLONE_URL, context) + folder = self.parameterAsFile(parameters, self.REPO_OUTPUT_FOLDER, context) + refish = self.parameterAsString(parameters, self.REPO_CLONE_REFISH, context) + depth = self.parameterAsInt(parameters, self.REPO_CLONE_DEPTH, context) + add_layers = self.parameterAsBool(parameters, self.REPO_ADD_TO_MAP, context) + + extent_rect = self.parameterAsExtent(parameters, self.REPO_CLONE_DEPTH, context) + extent_crs = self.parameterAsExtentCrs( + parameters, self.REPO_CLONE_SPATIAL_EXTENT, context + ) + extent = None + if parameters.get(self.REPO_CLONE_SPATIAL_EXTENT): + extent = QgsReferencedRectangle(extent_rect, extent_crs) + + repo = Repository.clone(repo_url, folder, extent=extent, depth=depth or None) + if refish: + repo.checkoutBranch(refish) + + layers = [] + vector_datasets, table_datasets = repo.datasets() + for dataset in vector_datasets: + layers.append(repo.workingCopyLayer(dataset)) + for dataset in table_datasets: + layers.append(repo.workingCopyLayer(dataset)) + + if add_layers: + for layer in layers: + context.context.addLayerToLoadOnCompletion(layer) + + return { + self.REPO_OUTPUT_FOLDER: folder, + self.REPO_OUTPUT_LAYERS: layers, + } diff --git a/kart/processing/tags.py b/kart/processing/tags.py new file mode 100644 index 0000000..6b84f9e --- /dev/null +++ b/kart/processing/tags.py @@ -0,0 +1,49 @@ +from qgis.core import QgsProcessingParameterFile, QgsProcessingParameterString +from kart.gui import icons + +from .base import KartAlgorithm + + +class RepoCreateTag(KartAlgorithm): + REPO_PATH = "REPO_PATH" + REPO_TAG_NAME = "REPO_TAG_NAME" + + def displayName(self): + return self.tr("Create Tag") + + def shortHelpString(self): + return self.tr("Create a new tag") + + def icon(self): + return icons.propertiesIcon + + def initAlgorithm(self, config=None): + + self.addParameter( + QgsProcessingParameterFile( + self.REPO_PATH, + self.tr("Repo Path"), + behavior=QgsProcessingParameterFile.Folder, + ) + ) + + self.addParameter( + QgsProcessingParameterString( + self.REPO_TAG_NAME, + self.tr("Tag Name"), + ) + ) + + def processAlgorithm(self, parameters, context, feedback): + from kart.kartapi import Repository + + repo_path = self.parameterAsFile(parameters, self.REPO_PATH, context) + tag_name = self.parameterAsString(parameters, self.REPO_TAG_NAME, context) + + repo = Repository(repo_path) + repo.createTag(tag_name) + + return { + self.REPO_PATH: repo_path, + self.REPO_TAG_NAME: tag_name, + }