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
 

Reply via email to