Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package obs-scm-bridge for openSUSE:Factory checked in at 2026-09-01 15:50:50 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/obs-scm-bridge (Old) and /work/SRC/openSUSE:Factory/.obs-scm-bridge.new.1265 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "obs-scm-bridge" Tue Sep 1 15:50:50 2026 rev:27 rq:1375026 version:0.8.0 Changes: -------- --- /work/SRC/openSUSE:Factory/obs-scm-bridge/obs-scm-bridge.changes 2026-07-17 01:44:28.903280145 +0200 +++ /work/SRC/openSUSE:Factory/.obs-scm-bridge.new.1265/obs-scm-bridge.changes 2026-09-01 15:51:34.708500542 +0200 @@ -1,0 +2,7 @@ +Tue Aug 25 07:59:08 UTC 2026 - Adrian Schröter <[email protected]> + +- 0.8.0 + * factored out python module to parse directory structure + * _manifest file validation via schema + +------------------------------------------------------------------- Old: ---- obs-scm-bridge-0.7.5.obscpio New: ---- obs-scm-bridge-0.8.0.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ obs-scm-bridge.spec ++++++ --- /var/tmp/diff_new_pack.96US6Z/_old 2026-09-01 15:51:35.931543212 +0200 +++ /var/tmp/diff_new_pack.96US6Z/_new 2026-09-01 15:51:35.936543386 +0200 @@ -29,7 +29,7 @@ %endif Name: obs-scm-bridge -Version: 0.7.5 +Version: 0.8.0 Release: 0 Summary: A help service to work with git repositories in OBS License: GPL-2.0-or-later ++++++ PKGBUILD ++++++ --- /var/tmp/diff_new_pack.96US6Z/_old 2026-09-01 15:51:36.024546456 +0200 +++ /var/tmp/diff_new_pack.96US6Z/_new 2026-09-01 15:51:36.031546700 +0200 @@ -1,5 +1,5 @@ pkgname=obs-scm-bridge -pkgver=0.7.5 +pkgver=0.8.0 pkgrel=0 pkgdesc='A help service to work with git repositories in OBS' arch=('any') ++++++ _service ++++++ --- /var/tmp/diff_new_pack.96US6Z/_old 2026-09-01 15:51:36.085548584 +0200 +++ /var/tmp/diff_new_pack.96US6Z/_new 2026-09-01 15:51:36.090548759 +0200 @@ -2,8 +2,8 @@ <service name="obs_scm" mode="manual"> <param name="url">https://github.com/openSUSE/obs-scm-bridge</param> <param name="scm">git</param> - <param name="revision">0.7.5</param> - <param name="version">0.7.5</param> + <param name="revision">0.8.0</param> + <param name="version">0.8.0</param> </service> <service mode="manual" name="set_version" /> ++++++ debian.changelog ++++++ --- /var/tmp/diff_new_pack.96US6Z/_old 2026-09-01 15:51:36.134550294 +0200 +++ /var/tmp/diff_new_pack.96US6Z/_new 2026-09-01 15:51:36.138550434 +0200 @@ -1,4 +1,4 @@ -build (0.7.5) unstable; urgency=low +build (0.8.0) unstable; urgency=low * Update to current git trunk - add sles11sp2 build config and adapt autodetection ++++++ obs-scm-bridge-0.7.5.obscpio -> obs-scm-bridge-0.8.0.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/obs-scm-bridge-0.7.5/.github/workflows/ci.yml new/obs-scm-bridge-0.8.0/.github/workflows/ci.yml --- old/obs-scm-bridge-0.7.5/.github/workflows/ci.yml 2026-07-16 12:33:25.000000000 +0200 +++ new/obs-scm-bridge-0.8.0/.github/workflows/ci.yml 2026-08-21 07:52:29.000000000 +0200 @@ -16,7 +16,7 @@ - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: - python-version: '3.11' + python-version: '3.13' cache: 'pip' - uses: actions/cache@v3 with: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/obs-scm-bridge-0.7.5/.gitignore new/obs-scm-bridge-0.8.0/.gitignore --- old/obs-scm-bridge-0.7.5/.gitignore 1970-01-01 01:00:00.000000000 +0100 +++ new/obs-scm-bridge-0.8.0/.gitignore 2026-08-21 07:52:29.000000000 +0200 @@ -0,0 +1 @@ +__pycache__ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/obs-scm-bridge-0.7.5/AGENTS.md new/obs-scm-bridge-0.8.0/AGENTS.md --- old/obs-scm-bridge-0.7.5/AGENTS.md 1970-01-01 01:00:00.000000000 +0100 +++ new/obs-scm-bridge-0.8.0/AGENTS.md 2026-08-21 07:52:29.000000000 +0200 @@ -0,0 +1,14 @@ +# Agent Guidelines + +All code in Python modules within this repository must be compatible with Python 3.6 and later. +The main script obs_scm_bridge is used with a new python 3.13 and does not need compability +to older python versions. +When writing or modifying code: + +- Use only features and syntax available since Python 3.6 +- Avoid deprecated APIs (e.g., `ast.Str`, `ast.NameConstant`, `ast.Num` which were removed in Python 3.14) +- Use f-strings (available since 3.6) +- Use type hints from `typing` module (available since 3.5, but ensure compatibility) +- Prefer standard library over third-party when possible +- Test with the oldest supported Python version (3.6+) and with python3.13 use for the main script. + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/obs-scm-bridge-0.7.5/DEVELOP.md new/obs-scm-bridge-0.8.0/DEVELOP.md --- old/obs-scm-bridge-0.7.5/DEVELOP.md 2026-07-16 12:33:25.000000000 +0200 +++ new/obs-scm-bridge-0.8.0/DEVELOP.md 2026-08-21 07:52:29.000000000 +0200 @@ -6,3 +6,10 @@ To run the tests in parallel, add the `-n auto` or `-n $nproc` parameter. To run only a specific test case, append the `-k $TEST_CASE_NAME` parameter. +The container based integration tests (test/test_service.py) require the +pytest-container dev dependency and a working docker. To run only the plain +unit tests, e.g. for the manifest parsing, run: +```ShellSession +$ poetry run pytest -vv test/test_manifest.py test/test_validate.py +``` + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/obs-scm-bridge-0.7.5/Makefile new/obs-scm-bridge-0.8.0/Makefile --- old/obs-scm-bridge-0.7.5/Makefile 2026-07-16 12:33:25.000000000 +0200 +++ new/obs-scm-bridge-0.8.0/Makefile 2026-08-21 07:52:29.000000000 +0200 @@ -8,6 +8,7 @@ install: install -d $(DESTDIR)$(servicedir) install -m 0755 obs_scm_bridge $(DESTDIR)$(servicedir) + cp -r scm_bridge $(DESTDIR)$(servicedir)/ test: flake8 set_version tests/ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/obs-scm-bridge-0.7.5/README.md new/obs-scm-bridge-0.8.0/README.md --- old/obs-scm-bridge-0.7.5/README.md 2026-07-16 12:33:25.000000000 +0200 +++ new/obs-scm-bridge-0.8.0/README.md 2026-08-21 07:52:29.000000000 +0200 @@ -50,6 +50,11 @@ It is recomended to use git submodules for each package if it is a larger project. This allows partial cloning of the specific package. +It is recommended to have also a _manifest file defining the package layout. +Check manifest_schema.json file for the schema documentation. A minimal +file can be empty, just to identify the git repository as package set for pbuild +and OBS. + Special directives ================== diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/obs-scm-bridge-0.7.5/example/_manifest new/obs-scm-bridge-0.8.0/example/_manifest --- old/obs-scm-bridge-0.7.5/example/_manifest 1970-01-01 01:00:00.000000000 +0100 +++ new/obs-scm-bridge-0.8.0/example/_manifest 2026-08-21 07:52:29.000000000 +0200 @@ -0,0 +1,9 @@ +# An example _manifest file + +packages: + +subdirectories: + - pkg/a + - pkg/b + - pkg/lib + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/obs-scm-bridge-0.7.5/manifest_schema.json new/obs-scm-bridge-0.8.0/manifest_schema.json --- old/obs-scm-bridge-0.7.5/manifest_schema.json 1970-01-01 01:00:00.000000000 +0100 +++ new/obs-scm-bridge-0.8.0/manifest_schema.json 2026-08-21 07:52:29.000000000 +0200 @@ -0,0 +1,28 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Project Manifest", + "description": "Schema for _manifest file in pbuild/OBS project git repositories. An empty file is allowed and simply identifies the git repository as package set.", + "type": ["object", "null"], + "properties": { + "packages": { + "type": ["array", "null"], + "items": { + "type": "string", + "pattern": "^[a-zA-Z0-9\\-_\\+][a-zA-Z0-9.\\-_\\+]*$" + }, + "description": "List of directories to be used as build package" + }, + "skip_missing_subdirectories": { + "type": "boolean" + }, + "subdirectories": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[a-zA-Z0-9\\-_\\+][a-zA-Z0-9.\\-_\\+/]*$" + }, + "description": "List of subdirectory paths where each subdirectory inside is considered to be a build package" + } + }, + "additionalProperties": false +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/obs-scm-bridge-0.7.5/obs_scm_bridge new/obs-scm-bridge-0.8.0/obs_scm_bridge --- old/obs-scm-bridge-0.7.5/obs_scm_bridge 2026-07-16 12:33:25.000000000 +0200 +++ new/obs-scm-bridge-0.8.0/obs_scm_bridge 2026-08-21 07:52:29.000000000 +0200 @@ -19,11 +19,11 @@ import logging import subprocess import tempfile -import yaml from html import escape from typing import Dict, List, Optional, TextIO, Tuple, Union import urllib.parse -import configparser + +from scm_bridge.project import ProjectScanner download_assets = '/usr/lib/build/download_assets' critical_instances_config = "/etc/obs/services/scm-bridge/critical-instances" @@ -74,14 +74,11 @@ self.no_lfs = False self.enforce_bcntsynctag = False # for project level mode - self.processed = {} self.git_store = None self.shallow_clone = shallow_clone self.create_obsinfo = create_obsinfo self.add_noobsinfo = False - #: git submodule config, if present - self.gsmconfig: Optional[configparser.ConfigParser] = None - self.gsmpath = {} + # git submodule revisions self.gsmrevisions = {} self.asset_types_filter = [] # all by default @@ -603,7 +600,7 @@ def list_submodule_revisions(self, subdir: str) -> Dict[str, str]: revisions = {} - cmd = [ 'git', '-C', self.clonedir, 'ls-tree', 'HEAD', '--end-of-options', (subdir if subdir else '.') ] + cmd = [ 'git', '-C', self.clonedir, 'ls-tree', 'HEAD', '--end-of-options', (subdir + '/' if subdir else '.') ] output = self.run_cmd(cmd, fatal="git ls-tree") for line in output.splitlines(): lstree = line.split(maxsplit=4) @@ -611,35 +608,29 @@ revisions[lstree[3]] = lstree[2] return revisions - def process_package_submodule(self, name: str, subdir: str) -> None: - if not self._REGEXP.match(name): - logging.warning("submodule name contains invalid char: %s", name) - return - - section = self.gsmpath[subdir + name] - if not section: - logging.warning("submodule not configured for %s", subdir + name) - return - gsmsection = self.gsmconfig[section] + def get_submodule_revision(self, path: str) -> str: + dir = os.path.dirname(path) + revisions = self.gsmrevisions.get(dir) + if not revisions: + revisions = self.gsmrevisions[dir] = self.list_submodule_revisions(dir) + revision = revisions.get(path) + if not revision: + self.die(f"could not determine revision of submodule for {path}") + return revision - urlstr = gsmsection['url'] + def process_package_submodule(self, name: str, path: str, info) -> None: + (section, config) = info + urlstr = config['url'] if not urlstr: self.die(f"url not defined for submodule {section}") - - revisions = self.gsmrevisions.get(subdir) - if not revisions: - revisions = self.gsmrevisions[subdir] = self.list_submodule_revisions(subdir) - - revision = revisions.get(gsmsection['path']) - if not revision: - self.die(f"could not determine revision of submodule for {gsmsection['path']}") + revision = self.get_submodule_revision(path) # write xml file and register the module url = list(urllib.parse.urlparse(urlstr)) url[5] = revision - if 'branch' in gsmsection: + if 'branch' in config: query = urllib.parse.parse_qs(url[4], keep_blank_values=True) - query['trackingbranch'] = [gsmsection['branch']] + query['trackingbranch'] = [config['branch']] url[4] = urllib.parse.urlencode(query, doseq=True) if self.arch: query = urllib.parse.parse_qs(url[4], keep_blank_values=True) @@ -660,7 +651,8 @@ # need to append a '/' to the base url so that the relative # path is properly resolved, otherwise we might descend one # directory too far - unparsed_url = urllib.parse.urljoin(self.scmtoolurl+'/', unparsed_url) + base_url = self.scmtoolurl + '/' + unparsed_url = urllib.parse.urljoin(base_url, unparsed_url) if self.url[0][0:4] == 'git+': unparsed_url = 'git+' + unparsed_url @@ -669,20 +661,15 @@ self.write_package_xml_file(name, unparsed_url, projectscmsync) self.write_info_file(name + ".info", revision) - def process_package_subdirectory(self, name: str, subdir: str) -> None: - # current directory is self.outdir - if not self._REGEXP.match(name): - logging.warning("directory name contains invalid char: %s", name) - return - + def process_package_subdirectory(self, name: str, path: str) -> None: # add subdir info file - info = self.get_subdir_info(subdir + name) + info = self.get_subdir_info(path) self.write_info_file(name + ".info", info) # add subdir parameter to url url = self.url.copy() query = urllib.parse.parse_qs(url[4], keep_blank_values=True) - query['subdir'] = subdir + name + query['subdir'] = path url[4] = urllib.parse.urlencode(query, doseq=True) if self.revision: url[5] = self.revision @@ -702,38 +689,11 @@ self.write_package_xml_file(name, urllib.parse.urlunparse(url)) def process_package_locallink(self, name: str, target: str) -> None: - if not self._REGEXP.match(name): - logging.warning("locallink name contains invalid char: %s", name) - return if not self._REGEXP.match(target): logging.warning("locallink target contains invalid char: %s", target) return self.write_package_xml_local_link(name, target) - def parse_gsmconfig(self): - self.gsmconfig = configparser.ConfigParser() - - # the parser stumbles over a mix of space and tabs. So, let's strip - # leading whitespaces first - gitmodules = None - with open(self.clonedir + '/.gitmodules') as f: - gitmodules = f.read() - gitmodules = "\n".join([line.lstrip() for line in gitmodules.split("\n")]) - - self.gsmconfig.read_string(gitmodules) - gsmpath = {} - for section in self.gsmconfig.sections(): - gsmconfig = self.gsmconfig[section] - if 'path' not in gsmconfig: - logging.warning("path not defined for git submodule " + section) - continue - path = gsmconfig['path'] - if path in gsmpath: - logging.warning("multiple definitions of %s path in git submodule config", path) - continue - gsmpath[path] = section - self.gsmpath = gsmpath - def generate_project_files(self) -> None: clonedir = tempfile.mkdtemp(prefix="obs-scm-bridge") self.clonedir = clonedir @@ -746,108 +706,26 @@ self.write_obsinfo() os.chdir(self.outdir) subdir = self.subdir + '/' if self.subdir else '' - if os.path.isfile(clonedir + '/.gitmodules'): - self.parse_gsmconfig() - self.generate_package_xml_files_of_directory(subdir) + scanner = ProjectScanner(clonedir) + for name, path, kind, info in scanner.list_packages(subdir): + if not self._REGEXP.match(name): + logging.warning("%s name contains invalid char: %s", kind, name) + continue + if kind == 'config': + shutil.move(self.clonedir + '/' + path, '_config') + elif kind == 'link': + self.process_package_locallink(name, info) + elif kind == 'submodule': + self.process_package_submodule(name, path, info) + elif kind == 'subdirectory': + self.process_package_subdirectory(name, path) + else: + self.die(f"unsupported kind in list_packages output: {kind}") + if self.keep_meta and subdir == '' and os.path.isdir(clonedir + '/.git'): + shutil.move(clonedir + '/.git', '.') shutil.rmtree(clonedir) self.clonedir = None - def read_project_manifest(self, filename): - packages = None - subdirectories = [] - manifest_yml = None - with open(filename) as stream: - manifest_yml = yaml.safe_load(stream) - if 'packages' in manifest_yml: - packages = [] - if manifest_yml.get('packages'): - for name in manifest_yml['packages']: - if not name or name.startswith('.') or name.startswith('/'): - logging.warning("illegal packages entry '%s'", name) - continue - if '/' in name or '*' in name: # for now - logging.warning("packages entry with '/' or '*' not implemented yet") - continue - packages.append(name) - if manifest_yml.get('subdirectories'): - for newsubdir in manifest_yml['subdirectories']: - if newsubdir: - subdirectories.append(newsubdir) - return packages, subdirectories - - def read_project_subdirs(self, filename): - packages = None - subdirectories = [] - subdir_yml = None - with open(filename) as stream: - subdir_yml = yaml.safe_load(stream) - for newsubdir in subdir_yml['subdirs']: - if newsubdir: - subdirectories.append(newsubdir) - if 'toplevel' not in subdir_yml or subdir_yml['toplevel'] != 'include': - packages = [] - return packages, subdirectories - - def generate_package_xml_files_of_directory(self, subdir) -> None: - if subdir: - self.verify_subdir(subdir) - self.check_subdir(subdir) - directory = (self.clonedir + '/' + subdir).rstrip('/') - logging.debug("check %s (subdir=%s)", directory, subdir) - - if os.path.isfile(directory + '/_config'): - shutil.move(directory + '/_config', '.') - self.processed['_config'] = True - - packages = None - subdirectories = [] - - if os.path.isfile(directory + '/_manifest'): - (packages, subdirectories) = self.read_project_manifest(directory + '/_manifest') - elif os.path.isfile(directory + '/_subdirs'): - (packages, subdirectories) = self.read_project_subdirs(directory + '/_subdirs') - - # handle all subdirectories - for newsubdir in subdirectories: - if (subdir + newsubdir + '/') in self.processed: - continue - self.processed[subdir + newsubdir + '/'] = True - self.generate_package_xml_files_of_directory(subdir + newsubdir + '/') - - if packages is None: - logging.debug("walk via %s", directory) - packages = sorted(os.listdir(directory)) - - # handle plain files and directories - for name in packages: - if name in self.processed: - continue # already handled - fname = directory + '/' + name - if name == '.git': - if self.keep_meta and subdir == '': - shutil.move(fname, '.') - self.processed[name] = True - elif name[0] == '.': - continue - elif os.path.islink(fname): - target = os.readlink(fname).rstrip('/') # this is no recursive lookup, but is there a usecase? - if not target or '/' in target or target.startswith('.'): - logging.warning("only local links are supported, skipping: %s -> %s", name, target) - continue - if not os.path.isdir(directory + '/' + target): - logging.debug("skipping dangling symlink %s -> %s", name, target) - continue - self.process_package_locallink(name, target) - self.processed[name] = True - elif os.path.isdir(fname): - if (subdir + name + '/') in self.processed: - continue # already handled in _subdir loop - if (subdir + name) in self.gsmpath: - self.process_package_submodule(name, subdir) - else: - self.process_package_subdirectory(name, subdir) - self.processed[name] = True - if __name__ == '__main__': parser = argparse.ArgumentParser( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/obs-scm-bridge-0.7.5/poetry.lock new/obs-scm-bridge-0.8.0/poetry.lock --- old/obs-scm-bridge-0.7.5/poetry.lock 2026-07-16 12:33:25.000000000 +0200 +++ new/obs-scm-bridge-0.8.0/poetry.lock 2026-08-21 07:52:29.000000000 +0200 @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. [[package]] name = "atomicwrites" @@ -6,6 +6,8 @@ description = "Atomic file writes." optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["dev"] +markers = "sys_platform == \"win32\"" files = [ {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, ] @@ -16,6 +18,7 @@ description = "Classes Without Boilerplate" optional = false python-versions = ">=3.6" +groups = ["main", "dev"] files = [ {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, @@ -26,18 +29,7 @@ dev = ["attrs[docs,tests]"] docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"] tests = ["attrs[tests-no-zope]", "zope.interface"] -tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"] - -[[package]] -name = "cached-property" -version = "1.5.2" -description = "A decorator for caching properties in classes." -optional = false -python-versions = "*" -files = [ - {file = "cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130"}, - {file = "cached_property-1.5.2-py2.py3-none-any.whl", hash = "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"}, -] +tests-no-zope = ["cloudpickle ; platform_python_implementation == \"CPython\"", "cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990) ; platform_python_implementation == \"CPython\"", "mypy (>=0.971,<0.990) ; platform_python_implementation == \"CPython\"", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version < \"3.11\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version < \"3.11\"", "pytest-xdist[psutil]", "pytest-xdist[psutil]"] [[package]] name = "colorama" @@ -45,28 +37,20 @@ description = "Cross-platform colored terminal text." optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["dev"] +markers = "sys_platform == \"win32\"" files = [ {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, ] [[package]] -name = "dataclasses" -version = "0.8" -description = "A backport of the dataclasses module for Python 3.6" -optional = false -python-versions = ">=3.6, <3.7" -files = [ - {file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"}, - {file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"}, -] - -[[package]] name = "deprecation" version = "2.1.0" description = "A library to handle automated deprecations" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a"}, {file = "deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff"}, @@ -81,6 +65,7 @@ description = "execnet: rapid multi-Python deployment" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["dev"] files = [ {file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"}, {file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"}, @@ -95,6 +80,7 @@ description = "A platform independent file lock." optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "filelock-3.4.1-py3-none-any.whl", hash = "sha256:a4bc51381e01502a30e9f06dd4fa19a1712eab852b6fb0f84fd7cce0793d8ca3"}, {file = "filelock-3.4.1.tar.gz", hash = "sha256:0f12f552b42b5bf60dba233710bf71337d35494fc8bdd4fd6d9f6d082ad45e06"}, @@ -105,42 +91,61 @@ testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"] [[package]] -name = "importlib-metadata" -version = "4.8.3" -description = "Read metadata from Python packages" +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" optional = false -python-versions = ">=3.6" +python-versions = "*" +groups = ["dev"] files = [ - {file = "importlib_metadata-4.8.3-py3-none-any.whl", hash = "sha256:65a9576a5b2d58ca44d133c42a241905cc45e34d2c06fd5ba2bafa221e5d7b5e"}, - {file = "importlib_metadata-4.8.3.tar.gz", hash = "sha256:766abffff765960fcc18003801f7044eb6755ffae4521c8e8ce8e83b9c9b0668"}, + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +description = "An implementation of JSON Schema validation for Python" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce"}, + {file = "jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326"}, ] [package.dependencies] -typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} -zipp = ">=0.5" +attrs = ">=22.2.0" +jsonschema-specifications = ">=2023.3.6" +referencing = ">=0.28.4" +rpds-py = ">=0.25.0" [package.extras] -docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"] -perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pep517", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy", "pytest-perf (>=0.9.2)"] +format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "rfc3987-syntax (>=1.1.0)", "uri-template", "webcolors (>=24.6.0)"] [[package]] -name = "iniconfig" -version = "1.1.1" -description = "iniconfig: brain-dead simple config-ini parsing" +name = "jsonschema-specifications" +version = "2025.9.1" +description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" optional = false -python-versions = "*" +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, - {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, + {file = "jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe"}, + {file = "jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d"}, ] +[package.dependencies] +referencing = ">=0.31.0" + [[package]] name = "packaging" version = "21.3" description = "Core utilities for Python packages" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, @@ -155,14 +160,12 @@ description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] -[package.dependencies] -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} - [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] @@ -173,6 +176,7 @@ description = "library with cross-python path, ini-parsing, io, code, log facilities" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["dev"] files = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, @@ -184,6 +188,7 @@ description = "Python parsing module" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"}, {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, @@ -198,6 +203,7 @@ description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "pytest-7.0.1-py3-none-any.whl", hash = "sha256:9ce3ff477af913ecf6321fe337b93a2c0dcf2a0a1439c43f5452112c1e4280db"}, {file = "pytest-7.0.1.tar.gz", hash = "sha256:e30905a0c131d3d94b89624a1cc5afec3e0ba2fbdb151867d8e0ebd49850f171"}, @@ -207,7 +213,6 @@ atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" @@ -223,21 +228,15 @@ description = "Pytest fixtures for writing container based tests" optional = false python-versions = ">=3.6.2,<4.0" +groups = ["dev"] files = [] develop = false [package.dependencies] -cached-property = {version = "^1.5", markers = "python_version < \"3.8\""} -dataclasses = {version = ">=0.8", markers = "python_version < \"3.7\""} deprecation = "^2.1" filelock = "^3.4" pytest = ">= 3.10" -pytest-testinfra = [ - {version = ">=6.4.0", markers = "python_version < \"3.7\""}, - {version = ">=7.0", markers = "python_version >= \"3.7\" and python_version < \"3.8\""}, - {version = ">=8.0", markers = "python_version >= \"3.8\""}, -] -typing-extensions = {version = ">=3.0", markers = "python_version < \"3.8\""} +pytest-testinfra = {version = ">=8.0", markers = "python_version >= \"3.8\""} [package.source] type = "git" @@ -247,50 +246,11 @@ [[package]] name = "pytest-testinfra" -version = "6.8.0" -description = "Test infrastructures" -optional = false -python-versions = ">=3.6" -files = [ - {file = "pytest-testinfra-6.8.0.tar.gz", hash = "sha256:07c8c2c472aca7d83099ebc5f850d383721cd654b66c60ffbb145e45e584ff99"}, - {file = "pytest_testinfra-6.8.0-py3-none-any.whl", hash = "sha256:56ac1dfc61342632a1189091473e253db1a3cdcecce0d49d6a769f33cd264814"}, -] - -[package.dependencies] -pytest = "!=3.0.2" - -[package.extras] -ansible = ["ansible"] -paramiko = ["paramiko"] -salt = ["salt"] -winrm = ["pywinrm"] - -[[package]] -name = "pytest-testinfra" -version = "7.0.0" -description = "Test infrastructures" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pytest-testinfra-7.0.0.tar.gz", hash = "sha256:38c2ce2df4e25f685636c7db9ac15083a7cf3e4a8a997d5fa654e8a7bedeadce"}, - {file = "pytest_testinfra-7.0.0-py3-none-any.whl", hash = "sha256:0b28076d7088fb0c8e868119639f1259f95dd0e735ae8045ead34433ce8cbc98"}, -] - -[package.dependencies] -pytest = "!=3.0.2" - -[package.extras] -ansible = ["ansible"] -paramiko = ["paramiko"] -salt = ["salt"] -winrm = ["pywinrm"] - -[[package]] -name = "pytest-testinfra" version = "8.1.0" description = "Test infrastructures" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pytest-testinfra-8.1.0.tar.gz", hash = "sha256:9b40828b58fb7ac2bff29cc1465934adf434f10dd5a108f60535dee52660a63d"}, {file = "pytest_testinfra-8.1.0-py3-none-any.whl", hash = "sha256:bf7df6306244da6a832f977766e75408317315aa1452c41c30e2541ef7978007"}, @@ -311,6 +271,7 @@ description = "pytest xdist plugin for distributed testing and loop-on-failing modes" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "pytest-xdist-3.0.2.tar.gz", hash = "sha256:688da9b814370e891ba5de650c9327d1a9d861721a524eb917e620eec3e90291"}, {file = "pytest_xdist-3.0.2-py3-none-any.whl", hash = "sha256:9feb9a18e1790696ea23e1434fa73b325ed4998b0e9fcb221f16fd1945e6df1b"}, @@ -331,6 +292,7 @@ description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, @@ -386,11 +348,155 @@ ] [[package]] +name = "referencing" +version = "0.37.0" +description = "JSON Referencing + Python" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231"}, + {file = "referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +rpds-py = ">=0.7.0" +typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""} + +[[package]] +name = "rpds-py" +version = "2026.6.3" +description = "Python bindings to Rust's persistent data structures (rpds)" +optional = false +python-versions = ">=3.11" +groups = ["main"] +files = [ + {file = "rpds_py-2026.6.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7b689145a1485c335569bd056464f3243a29af7ed3871c7be31ad624ba239bc7"}, + {file = "rpds_py-2026.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db08f45aecde626498fb3df07bcf6d2ec040af42e859a4f5040d79c200342911"}, + {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc992ab27b15f852c76755eb2ab7dce86585ddadba6fa5946e58556088845b4"}, + {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f88d653e7b3b779d71ae7454e20dcc9b6bae903f33c269db9f2be41bda3f261"}, + {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e52655eaf81e32593abedaa4bfe33170c8cfedf3365ed9be6e11e07f148f0278"}, + {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dfcc8b909769d19db55c7cc9541eb64b9b774b1057ffffb4f1048070475bb9f9"}, + {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c1255b302953c86a486b81d330d5ee1d5bd937691ce271b6be0ef0e299eaab7"}, + {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8d2294a31386bfa251d8c8a39472beee17db67d4f1a6eabea665d35c9a4461c3"}, + {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8f23ead891a3b762f35ab3b04623da7056545b48aa60d59957e6789914545da"}, + {file = "rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:421aba32367055614287a4292b6a17f1939c9452299f7a0209c117e990b646d4"}, + {file = "rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1e5822dfc2f0d4ab7e745eaa6d85945069329beeccef965af3f3bb26058fcab6"}, + {file = "rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83e35b57523816c8613fd0776b40cd8bb9f596b37ddd2692eb4a6bb5ab2f8c93"}, + {file = "rpds_py-2026.6.3-cp311-cp311-win32.whl", hash = "sha256:de3eceba0b683bcbb1ab93da016d0270df1f9ae7be716b40214c5dafac6ea45a"}, + {file = "rpds_py-2026.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127"}, + {file = "rpds_py-2026.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:168c733a7112e071bb7a66460e667edfcff06c017a3c523f7a8a8e08d0140804"}, + {file = "rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0"}, + {file = "rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf"}, + {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24"}, + {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e"}, + {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975"}, + {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680"}, + {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6"}, + {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a"}, + {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4"}, + {file = "rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa"}, + {file = "rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc"}, + {file = "rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822"}, + {file = "rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed"}, + {file = "rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f"}, + {file = "rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96"}, + {file = "rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223"}, + {file = "rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f"}, + {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f"}, + {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7"}, + {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6"}, + {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af"}, + {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf"}, + {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885"}, + {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4"}, + {file = "rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7"}, + {file = "rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d"}, + {file = "rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97"}, + {file = "rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0"}, + {file = "rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80"}, + {file = "rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb"}, + {file = "rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e"}, + {file = "rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd"}, + {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d"}, + {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda"}, + {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8"}, + {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53"}, + {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504"}, + {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc"}, + {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77"}, + {file = "rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698"}, + {file = "rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd"}, + {file = "rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d"}, + {file = "rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8"}, + {file = "rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5"}, + {file = "rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2"}, + {file = "rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13"}, + {file = "rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05"}, + {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba"}, + {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617"}, + {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9"}, + {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb"}, + {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885"}, + {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a"}, + {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868"}, + {file = "rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187"}, + {file = "rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107"}, + {file = "rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba"}, + {file = "rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369"}, + {file = "rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146"}, + {file = "rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5c2dc92304aa48a4a60443b548bb12f12e119d4b72f314015e67b9e1be97fca"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:127e08c0642d880cf32ca47ec2a4a77b901f7e2dd1ad9762adb13955d72ffcc9"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bb68f03f395eb793220b45c097bd4d8c32944393da0fad8b999efac0868fc8c"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3450b693fde92133e9f51060568a4c31fcca76d5e53bbd611e689ca446517e9"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:5e8d07bddee435a2ff6f1920e18feff28d0bc4533e42f4bf6927fbd073312c41"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a83ae6c67b7676b9878378547ca8e93ed77a580037bcbcd1d32f739e1e6089c"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2bfd04c19ddbd6640de0b51894d764bd2758854d5b75bd102d2ef10cb9c293a9"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:ca6546b66be9dc4738b1b043d5ebd5488c66c578c5ff0fd0e8065313fe3afb76"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826"}, + {file = "rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4"}, +] + +[[package]] name = "tomli" version = "1.2.3" description = "A lil' TOML parser" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "tomli-1.2.3-py3-none-any.whl", hash = "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"}, {file = "tomli-1.2.3.tar.gz", hash = "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f"}, @@ -398,31 +504,18 @@ [[package]] name = "typing-extensions" -version = "4.1.1" -description = "Backported and Experimental Type Hints for Python 3.6+" +version = "4.16.0" +description = "Backported and Experimental Type Hints for Python 3.9+" optional = false -python-versions = ">=3.6" -files = [ - {file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"}, - {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, -] - -[[package]] -name = "zipp" -version = "3.6.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.6" +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version < \"3.13\"" files = [ - {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"}, - {file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"}, + {file = "typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8"}, + {file = "typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5"}, ] -[package.extras] -docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"] -testing = ["func-timeout", "jaraco.itertools", "pytest (>=4.6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy"] - [metadata] -lock-version = "2.0" -python-versions = ">=3.6.2,<4.0" -content-hash = "cd22d3efa3e332c8e38fe0c4f2a42edc2975e3cdefb2ea69a0567cf722bbd71a" +lock-version = "2.1" +python-versions = ">=3.11.0,<4.0" +content-hash = "749616fe5c46f1a460b5822340fd71dc67332bded084eba17855ec12d3c2214c" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/obs-scm-bridge-0.7.5/pyproject.toml new/obs-scm-bridge-0.8.0/pyproject.toml --- old/obs-scm-bridge-0.7.5/pyproject.toml 2026-07-16 12:33:25.000000000 +0200 +++ new/obs-scm-bridge-0.8.0/pyproject.toml 2026-08-21 07:52:29.000000000 +0200 @@ -8,13 +8,14 @@ packages = [{include = "obs_scm_bridge"}] [tool.poetry.dependencies] -python = ">=3.6.2,<4.0" +python = ">=3.11.0,<4.0" pyyaml = ">=6.0.0" +jsonschema = ">=4.0.0" [tool.poetry.group.dev.dependencies] pytest-container = { git = "https://github.com/dcermak/pytest_container.git", branch = "main" } pytest-xdist = ">=3.0" [build-system] -requires = ["poetry-core", "pyyaml"] +requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/obs-scm-bridge-0.7.5/scm_bridge/__init__.py new/obs-scm-bridge-0.8.0/scm_bridge/__init__.py --- old/obs-scm-bridge-0.7.5/scm_bridge/__init__.py 1970-01-01 01:00:00.000000000 +0100 +++ new/obs-scm-bridge-0.8.0/scm_bridge/__init__.py 2026-08-21 07:52:29.000000000 +0200 @@ -0,0 +1 @@ +# scm_bridge package \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/obs-scm-bridge-0.7.5/scm_bridge/project.py new/obs-scm-bridge-0.8.0/scm_bridge/project.py --- old/obs-scm-bridge-0.7.5/scm_bridge/project.py 1970-01-01 01:00:00.000000000 +0100 +++ new/obs-scm-bridge-0.8.0/scm_bridge/project.py 2026-08-21 07:52:29.000000000 +0200 @@ -0,0 +1,167 @@ +# scm_bridge/project.py +# Scanning of a git clone for OBS packages and package containers. +# +# This module must stay compatible with Python 3.6 and later. + +import configparser +import logging +import os +import sys + +import yaml + + +class ProjectScanner(object): + """Scan a git clone directory for OBS packages and package containers.""" + + def __init__(self, directory): + self.directory = directory + self.processed = {} + self.gsmpath = self._parse_gitmodules(directory) + + @staticmethod + def _parse_gitmodules(directory): + gsmpath = {} + filename = os.path.join(directory, '.gitmodules') + if not os.path.isfile(filename): + return gsmpath + + # the parser stumbles over a mix of space and tabs. So, let's strip + # leading whitespaces first + with open(filename) as f: + gitmodules = f.read() + gitmodules = "\n".join([line.lstrip() for line in gitmodules.split("\n")]) + + gsmconfig = configparser.ConfigParser() + gsmconfig.read_string(gitmodules) + for section in gsmconfig.sections(): + config = gsmconfig[section] + if 'path' not in config: + logging.warning("path not defined for git submodule " + section) + continue + path = config['path'] + if path in gsmpath: + logging.warning("multiple definitions of %s path in git submodule config", path) + continue + gsmpath[path] = (section, config) + return gsmpath + + def _die(self, msg): + logging.error(msg) + sys.exit(1) + + def _verify_subdir(self, subdir): + if subdir.startswith('-'): + self._die(f"illegal sub-directory '{subdir}'") + + def _check_subdir(self, subdir): + fromdir = os.path.join(self.directory, subdir) + if not os.path.realpath(fromdir + '/').startswith(os.path.realpath(self.directory + '/')): + self._die(f"subdir {subdir} is not below clone directory") + if not os.path.isdir(fromdir): + self._die(f"subdir {subdir} does not exist") + + def list_packages(self, subdir=''): + result = [] + if subdir: + self._verify_subdir(subdir) + self._check_subdir(subdir) + subdir = subdir.rstrip('/') + '/' # make sure subdir ends with a slash + directory = (self.directory + '/' + subdir).rstrip('/') + logging.debug("check %s (subdir=%s)", directory, subdir) + + if '_config' not in self.processed and os.path.isfile(directory + '/_config'): + result.append(('_config', subdir + '_config', 'config', None)) + self.processed['_config'] = True + + packages = None + subdirectories = [] + + if os.path.isfile(directory + '/_manifest'): + (packages, subdirectories) = self.read_project_manifest(directory + '/_manifest') + elif os.path.isfile(directory + '/_subdirs'): + (packages, subdirectories) = self.read_project_subdirs(directory + '/_subdirs') + + # handle all subdirectories + for newsubdir in subdirectories: + if (subdir + newsubdir + '/') in self.processed: + continue + self.processed[subdir + newsubdir + '/'] = True + result += self.list_packages(subdir + newsubdir + '/') + + if packages is None: + logging.debug("walk via %s", directory) + packages = sorted(os.listdir(directory)) + + # handle plain files and directories + for name in packages: + if name[0] == '.': + continue + if name in self.processed: + continue # already handled + fname = directory + '/' + name + if os.path.islink(fname): + target = os.readlink(fname).rstrip('/') # this is no recursive lookup, but is there a usecase? + if not target or '/' in target or target.startswith('.'): + logging.warning("only local links are supported, skipping: %s -> %s", name, target) + continue + if not os.path.isdir(directory + '/' + target): + logging.debug("skipping dangling symlink %s -> %s", name, target) + continue + result.append((name, subdir + name, 'link', target)) + self.processed[name] = True + elif os.path.isdir(fname): + if (subdir + name + '/') in self.processed: + continue # already handled in _subdir loop + if (subdir + name) in self.gsmpath: + result.append((name, subdir + name, 'submodule', self.gsmpath[subdir + name])) + else: + result.append((name, subdir + name, 'subdirectory', None)) + self.processed[name] = True + + return result + + def read_project_manifest(self, filename): + packages = None + subdirectories = [] + manifest_yml = None + directory = os.path.dirname(filename) + with open(filename) as stream: + manifest_yml = yaml.safe_load(stream) + + if manifest_yml is None: + # allowing an empty _manifest file + return packages, subdirectories + + if 'packages' in manifest_yml: + packages = [] + if manifest_yml.get('packages'): + for name in manifest_yml['packages']: + if not name or name.startswith('.') or name.startswith('/'): + logging.warning("illegal packages entry '%s'", name) + continue + if '/' in name or '*' in name: # for now + logging.warning("packages entry with '/' or '*' not implemented yet") + continue + packages.append(name) + if manifest_yml.get('subdirectories'): + for newsubdir in manifest_yml['subdirectories']: + if manifest_yml.get('skip_missing_subdirectories') == "true": + if not os.path.exists(directory + "/" + newsubdir): + continue + if newsubdir: + subdirectories.append(newsubdir) + return packages, subdirectories + + def read_project_subdirs(self, filename): + packages = None + subdirectories = [] + subdir_yml = None + with open(filename) as stream: + subdir_yml = yaml.safe_load(stream) + for newsubdir in subdir_yml['subdirs']: + if newsubdir: + subdirectories.append(newsubdir) + if 'toplevel' not in subdir_yml or subdir_yml['toplevel'] != 'include': + packages = [] + return packages, subdirectories \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/obs-scm-bridge-0.7.5/test/test_service.py new/obs-scm-bridge-0.8.0/test/test_service.py --- old/obs-scm-bridge-0.7.5/test/test_service.py 2026-07-16 12:33:25.000000000 +0200 +++ new/obs-scm-bridge-0.8.0/test/test_service.py 2026-08-21 07:52:29.000000000 +0200 @@ -14,7 +14,7 @@ _LIBECONF_URL = "https://github.com/openSUSE/libeconf" CONTAINERFILE = f"""RUN set -eux; \ - zypper -n in python311 git-core diff python311-PyYAML; \ + zypper -n in python313 git-core python313-PyYAML python313-jsonschema diffutils; \ . /etc/os-release && [[ ${{NAME}} = "SLES" ]] || zypper -n in git-lfs; \ zypper -n in -f --recommends build; \ zypper -n clean; rm -rf /var/log/zypp* @@ -29,7 +29,7 @@ cd {_RPMS_DIR}ring0 && \ git init && git submodule add {_AAA_BASE_URL} && \ git commit -m "add aaa_base" && \ - git submodule add ../libeconf && git commit -m "add libeconf" && \ + git submodule add -b master ../libeconf && git commit -m "add libeconf" && \ cd aaa_base && git rev-parse HEAD > /src/aaa_base RUN set -euo pipefail; \ @@ -40,7 +40,8 @@ git add libeconf aaa_base; git commit -m "initial commit"; COPY obs_scm_bridge /usr/bin/ -RUN sed -i 's,^#!/usr/bin/python3.*,#!/usr/bin/python3.11,' /usr/bin/obs_scm_bridge +COPY scm_bridge /usr/bin/scm_bridge/ +RUN sed -i 's,^#!/usr/bin/python3.*,#!/usr/bin/python3.13,' /usr/bin/obs_scm_bridge RUN chmod +x /usr/bin/obs_scm_bridge """ @@ -63,7 +64,7 @@ extra_environment_variables=EXTRA_ENVIRONMENT_VARIABLES ) -CONTAINER_IMAGES = [TUMBLEWEED, LEAP_LATEST, BCI_BASE_LATEST] +CONTAINER_IMAGES = [TUMBLEWEED, LEAP_LATEST] #, BCI_BASE_LATEST] _OBS_SCM_BRIDGE_CMD = "obs_scm_bridge --debug 1" @@ -131,7 +132,7 @@ ): assert file_name in files - def _test_pkg_xml(pkg_name: str, expected_url: str, expected_head_hash: str): + def _test_pkg_xml(pkg_name: str, expected_url: str, expected_head_hash: str, tracking_branch: str): conf = ET.fromstring( auto_container_per_test.connection.file( f"{dest}/{pkg_name}.xml" @@ -139,11 +140,13 @@ ) assert conf.attrib["name"] == pkg_name scm_sync_elements = conf.findall("scmsync") + if tracking_branch: + expected_url += f"?trackingbranch={tracking_branch}" assert len(scm_sync_elements) == 1 and scm_sync_elements[0].text assert f"{expected_url}#{expected_head_hash}" in scm_sync_elements[0].text - _test_pkg_xml("aaa_base", _AAA_BASE_URL, aaa_base_hash) - _test_pkg_xml("libeconf", f"{_RPMS_DIR}libeconf", libeconf_hash) + _test_pkg_xml("aaa_base", _AAA_BASE_URL, aaa_base_hash, None) + _test_pkg_xml("libeconf", f"{_RPMS_DIR}libeconf", libeconf_hash, "master") for pkg_name, pkg_head_hash in ( ("aaa_base", aaa_base_hash), @@ -194,6 +197,58 @@ ) +def test_checks_out_project_with_submodules( + auto_container_per_test: ContainerData, +) -> None: + """Smoke test that we can clone a git repository that contains packages as + git submodules. + + Additionally, we verify that the $pkg.xml and $pkg.info files are present + and their contents are sane. + + """ + dest = "/tmp/ring0" + repo = f"file://{_RPMS_DIR}ring0" + auto_container_per_test.connection.check_output( + f"{_OBS_SCM_BRIDGE_CMD} --outdir {dest} --url {repo} --projectmode 1", + ) + + libeconf_hash, aaa_base_hash = ( + auto_container_per_test.connection.file( + f"/src/{pkg_name}" + ).content_string.strip() + for pkg_name in ("libeconf", "aaa_base") + ) + + for pkg_name, pkg_url, pkg_head_hash, tracking_branch in ( + ("libeconf", f"file://{_RPMS_DIR}libeconf", libeconf_hash, "master"), + ("aaa_base", _AAA_BASE_URL, aaa_base_hash, None), + ): + info_file = auto_container_per_test.connection.file(f"{dest}/{pkg_name}.info") + assert info_file.exists + assert info_file.content_string.strip() == pkg_head_hash + + # the xml file should have essentially only the following contents: + # <package name="$pkg_name"> + # <url>$project_url</url> + # <scmsync>$submodule_url#$revision</scmsync> + # </package> + pkg_meta = ET.fromstring( + auto_container_per_test.connection.file( + f"{dest}/{pkg_name}.xml" + ).content_string + ) + assert pkg_meta.attrib["name"] == pkg_name + scm_sync = pkg_meta.findall("scmsync") + if tracking_branch: + pkg_url += f"?trackingbranch={tracking_branch}" + assert ( + len(scm_sync) == 1 + and scm_sync[0].text + and scm_sync[0].text == f"{pkg_url}#{pkg_head_hash}" + ) + + LFS_REPO = "https://src.opensuse.org/pool/trivy.git" @@ -233,7 +288,7 @@ @pytest.mark.parametrize( "git_repo_url,expected_head", [ - (f"{_LIBECONF_URL}{fragment}", commit) + (f"{_LIBECONF_URL}?trackingbranch=master{fragment}", commit) for fragment, commit in ( ("", None), ("#master", None), diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/obs-scm-bridge-0.7.5/test/test_validate.py new/obs-scm-bridge-0.8.0/test/test_validate.py --- old/obs-scm-bridge-0.7.5/test/test_validate.py 1970-01-01 01:00:00.000000000 +0100 +++ new/obs-scm-bridge-0.8.0/test/test_validate.py 2026-08-21 07:52:29.000000000 +0200 @@ -0,0 +1,11 @@ +import os + +from scm_bridge.project import ProjectScanner + + +def test_manifest(): + directory = "example" + scanner = ProjectScanner(directory) + packages, subdirectories = scanner.read_project_manifest(os.path.join(directory, "_manifest")) + assert packages == [] + assert subdirectories == ["pkg/a", "pkg/b", "pkg/lib"] \ No newline at end of file ++++++ obs-scm-bridge.dsc ++++++ --- /var/tmp/diff_new_pack.96US6Z/_old 2026-09-01 15:51:36.369558493 +0200 +++ /var/tmp/diff_new_pack.96US6Z/_new 2026-09-01 15:51:36.377558772 +0200 @@ -1,6 +1,6 @@ Format: 1.0 Source: build -Version: 0.7.5 +Version: 0.8.0 Binary: build Maintainer: Adrian Schroeter <[email protected]> Architecture: all ++++++ obs-scm-bridge.obsinfo ++++++ --- /var/tmp/diff_new_pack.96US6Z/_old 2026-09-01 15:51:36.404559714 +0200 +++ /var/tmp/diff_new_pack.96US6Z/_new 2026-09-01 15:51:36.409559888 +0200 @@ -1,5 +1,5 @@ name: obs-scm-bridge -version: 0.7.5 -mtime: 1784198005 -commit: ee841c773542fb47489f3d40f4a85eaad5630e0c +version: 0.8.0 +mtime: 1787291549 +commit: 971a83f4f139b2ea8f7bf42aaeada6eb17e46278
