From 19b56c565e01021c43c64bb0c83189a075893902 Mon Sep 17 00:00:00 2001 From: trisdoan Date: Mon, 18 May 2026 15:05:45 +0700 Subject: [PATCH] feat(new): add tlc new command to write .odoo-dev.json config --- pyproject.toml | 2 + tests/test_new.py | 699 +++++++++++++++++++++++++++++++ trobz_local/assets/odoo-dev.json | 30 ++ trobz_local/main.py | 34 ++ trobz_local/odoo_dev_config.py | 256 +++++++++++ uv.lock | 191 ++++++++- 6 files changed, 1211 insertions(+), 1 deletion(-) create mode 100644 tests/test_new.py create mode 100644 trobz_local/assets/odoo-dev.json create mode 100644 trobz_local/odoo_dev_config.py diff --git a/pyproject.toml b/pyproject.toml index fc5bcbf..bfba49d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,8 @@ license-files = ["LICENSE"] dependencies = [ "gitpython>=3.1.45", + "jsonschema>=4.0", + "odoo-addons-path>=1.4.0", "pydantic>=2.12.5", "tomli>=2.3.0", "typer>=0.20", diff --git a/tests/test_new.py b/tests/test_new.py new file mode 100644 index 0000000..e969734 --- /dev/null +++ b/tests/test_new.py @@ -0,0 +1,699 @@ +"""Tests for tlc new command and odoo_dev_config module.""" + +import json +import os +from pathlib import Path +from unittest.mock import patch + +from typer.testing import CliRunner + +from trobz_local.main import app + +runner = CliRunner() + +SCHEMA_PATH = Path(__file__).parent.parent / "trobz_local" / "assets" / "odoo-dev.json" + + +# ============================================================================= +# Helpers +# ============================================================================= + + +def _read_config(target_dir: Path) -> dict: + config_file = target_dir / ".odoo-dev.json" + return json.loads(config_file.read_text()) + + +# ============================================================================= +# 1. managed_layout — CLI returns managed layout with odoo_dir +# ============================================================================= + + +def test_managed_layout(tmp_path): + """Managed layout (e.g. trobz): sourcePaths == odoo_dir, layout != webonly.""" + cwd = tmp_path / "project" + cwd.mkdir() + code_dir = tmp_path / "code" + code_dir.mkdir() + + odoo_addons = tmp_path / "project" / "odoo" / "addons" + odoo_addons.mkdir(parents=True) + odoo_odoo_addons = tmp_path / "project" / "odoo" / "odoo" / "addons" + odoo_odoo_addons.mkdir(parents=True) + + cli_result = { + "layout": "Trobz", + "odoo_dir": [str(odoo_addons), str(odoo_odoo_addons)], + "version": "18.0", + "addons_path": "", + } + + target_dir = cwd / ".claude" + + with ( + patch("trobz_local.odoo_dev_config._run_addons_path_cli", return_value=cli_result), + patch("trobz_local.main._get_cwd", return_value=cwd), + patch("trobz_local.main._get_global_config_dir", return_value=tmp_path / "global"), + ): + env = {**os.environ, "TLC_CODE_DIR": str(code_dir)} + result = runner.invoke(app, ["--no-newcomer", "new"], env=env) + assert result.exit_code == 0, result.output + + config = _read_config(target_dir) + odoo_block = config["odoo_source"] + assert odoo_block["layout"] == "trobz" + assert odoo_block["sourcePaths"] != [] + assert str(odoo_addons) in odoo_block["sourcePaths"] or str(odoo_odoo_addons) in odoo_block["sourcePaths"] + + +# ============================================================================= +# 2. local_layout — CLI returns fallback, local odoo checkout exists +# ============================================================================= + + +def test_local_layout(tmp_path): + """CLI fallback + local Odoo checkout at TLC_CODE_DIR/odoo/odoo/18.0 → layout=local.""" + cwd = tmp_path / "project" + cwd.mkdir() + code_dir = tmp_path / "code" + + checkout_root = code_dir / "odoo" / "odoo" / "18.0" + addons_dir = checkout_root / "addons" + addons_dir.mkdir(parents=True) + odoo_addons_dir = checkout_root / "odoo" / "addons" + odoo_addons_dir.mkdir(parents=True) + + cli_result = { + "layout": "fallback", + "odoo_dir": [], + "version": "18.0", + "addons_path": "", + } + + target_dir = cwd / ".claude" + + with ( + patch("trobz_local.odoo_dev_config._run_addons_path_cli", return_value=cli_result), + patch("trobz_local.main._get_cwd", return_value=cwd), + patch("trobz_local.main._get_global_config_dir", return_value=tmp_path / "global"), + ): + env = {**os.environ, "TLC_CODE_DIR": str(code_dir)} + result = runner.invoke(app, ["--no-newcomer", "new"], env=env) + assert result.exit_code == 0, result.output + + config = _read_config(target_dir) + odoo_block = config["odoo_source"] + assert odoo_block["layout"] == "local" + assert str(addons_dir) in odoo_block["sourcePaths"] + assert str(odoo_addons_dir) in odoo_block["sourcePaths"] + for sp in odoo_block["sourcePaths"]: + assert Path(sp).exists(), f"sourcePath does not exist: {sp}" + + +# ============================================================================= +# 3. local_broken_tree — CLI returns fallback, local checkout exists but no addons dirs +# ============================================================================= + + +def test_local_broken_tree(tmp_path): + """CLI fallback + local checkout present but no addons dirs → downgrade to webonly.""" + cwd = tmp_path / "project" + cwd.mkdir() + code_dir = tmp_path / "code" + + # Create checkout root but NO addons or odoo/addons inside + checkout_root = code_dir / "odoo" / "odoo" / "18.0" + checkout_root.mkdir(parents=True) + + cli_result = { + "layout": "fallback", + "odoo_dir": [], + "version": "18.0", + "addons_path": "", + } + + target_dir = cwd / ".claude" + + with ( + patch("trobz_local.odoo_dev_config._run_addons_path_cli", return_value=cli_result), + patch("trobz_local.main._get_cwd", return_value=cwd), + patch("trobz_local.main._get_global_config_dir", return_value=tmp_path / "global"), + ): + env = {**os.environ, "TLC_CODE_DIR": str(code_dir)} + result = runner.invoke(app, ["--no-newcomer", "new"], env=env) + assert result.exit_code == 0, result.output + + config = _read_config(target_dir) + odoo_block = config["odoo_source"] + assert odoo_block["layout"] == "webonly" + assert odoo_block["sourcePaths"] == [] + + +# ============================================================================= +# 4. webonly_with_version — CLI returns fallback, no local checkout +# ============================================================================= + + +def test_webonly_with_version(tmp_path): + """CLI fallback, no local source, version resolvable → webonly with version.""" + cwd = tmp_path / "project" + cwd.mkdir() + code_dir = tmp_path / "code" + code_dir.mkdir(parents=True) + + cli_result = { + "layout": "fallback", + "odoo_dir": [], + "version": "18.0", + "addons_path": "", + } + + target_dir = cwd / ".claude" + + with ( + patch("trobz_local.odoo_dev_config._run_addons_path_cli", return_value=cli_result), + patch("trobz_local.main._get_cwd", return_value=cwd), + patch("trobz_local.main._get_global_config_dir", return_value=tmp_path / "global"), + ): + env = {**os.environ, "TLC_CODE_DIR": str(code_dir)} + result = runner.invoke(app, ["--no-newcomer", "new"], env=env) + assert result.exit_code == 0, result.output + + config = _read_config(target_dir) + odoo_block = config["odoo_source"] + assert odoo_block["layout"] == "webonly" + assert odoo_block["sourcePaths"] == [] + assert odoo_block.get("version") == "18.0" + + +# ============================================================================= +# 5. webonly_unresolvable — CLI returns null layout and null version +# ============================================================================= + + +def test_webonly_unresolvable(tmp_path): + """CLI null layout + null version, no local source → webonly, version key ABSENT.""" + cwd = tmp_path / "project" + cwd.mkdir() + code_dir = tmp_path / "code" + code_dir.mkdir(parents=True) + + cli_result = { + "layout": None, + "odoo_dir": [], + "version": None, + "addons_path": "", + } + + target_dir = cwd / ".claude" + + with ( + patch("trobz_local.odoo_dev_config._run_addons_path_cli", return_value=cli_result), + patch("trobz_local.main._get_cwd", return_value=cwd), + patch("trobz_local.main._get_global_config_dir", return_value=tmp_path / "global"), + ): + env = {**os.environ, "TLC_CODE_DIR": str(code_dir)} + result = runner.invoke(app, ["--no-newcomer", "new"], env=env) + assert result.exit_code == 0, result.output + + config = _read_config(target_dir) + odoo_block = config["odoo_source"] + assert odoo_block["layout"] == "webonly" + assert odoo_block["sourcePaths"] == [] + assert "version" not in odoo_block + + +# ============================================================================= +# 6. manifest_less_no_exit — CLI absent/broken → tlc new does NOT crash +# ============================================================================= + + +def test_manifest_less_no_exit(tmp_path): + """CLI absent/broken (_run_addons_path_cli returns None) → tlc new must NOT crash.""" + cwd = tmp_path / "project" + cwd.mkdir() + code_dir = tmp_path / "code" + code_dir.mkdir(parents=True) + + target_dir = cwd / ".claude" + + with ( + patch("trobz_local.odoo_dev_config._run_addons_path_cli", return_value=None), + patch("trobz_local.main._get_cwd", return_value=cwd), + patch("trobz_local.main._get_global_config_dir", return_value=tmp_path / "global"), + ): + env = {**os.environ, "TLC_CODE_DIR": str(code_dir)} + result = runner.invoke(app, ["--no-newcomer", "new"], env=env) + + assert result.exit_code == 0, result.output + config = _read_config(target_dir) + assert config["odoo_source"]["layout"] == "webonly" + + +# ============================================================================= +# 7. cli_version_null_git_branch_fallback — CLI version null, branch has version prefix +# ============================================================================= + + +def test_cli_version_null_git_branch_fallback(tmp_path): + """CLI version null → git-branch fallback yields version from branch name.""" + cwd = tmp_path / "project" + cwd.mkdir() + code_dir = tmp_path / "code" + code_dir.mkdir(parents=True) + + cli_result = { + "layout": None, + "odoo_dir": [], + "version": None, + "addons_path": "", + } + + target_dir = cwd / ".claude" + + with ( + patch("trobz_local.odoo_dev_config._run_addons_path_cli", return_value=cli_result), + patch("trobz_local.odoo_dev_config._git_branch_version", return_value="18.0"), + patch("trobz_local.main._get_cwd", return_value=cwd), + patch("trobz_local.main._get_global_config_dir", return_value=tmp_path / "global"), + ): + env = {**os.environ, "TLC_CODE_DIR": str(code_dir)} + result = runner.invoke(app, ["--no-newcomer", "new"], env=env) + assert result.exit_code == 0, result.output + + config = _read_config(target_dir) + odoo_block = config["odoo_source"] + assert odoo_block.get("version") == "18.0" + + +# ============================================================================= +# 8. global_flag — -g writes to ~/.claude/ not CWD +# ============================================================================= + + +def test_global_flag(tmp_path): + """--global flag writes odoo-dev.json to the global dir, not CWD/.claude/.""" + cwd = tmp_path / "project" + cwd.mkdir() + code_dir = tmp_path / "code" + code_dir.mkdir(parents=True) + + fake_home_claude = tmp_path / "home" / ".claude" + fake_home_claude.mkdir(parents=True) + + cli_result = { + "layout": None, + "odoo_dir": [], + "version": "18.0", + "addons_path": "", + } + + with ( + patch("trobz_local.odoo_dev_config._run_addons_path_cli", return_value=cli_result), + patch("trobz_local.main._get_cwd", return_value=cwd), + patch("trobz_local.main._get_global_config_dir", return_value=fake_home_claude), + ): + env = {**os.environ, "TLC_CODE_DIR": str(code_dir)} + result = runner.invoke(app, ["--no-newcomer", "new", "-g"], env=env) + assert result.exit_code == 0, result.output + + global_config = fake_home_claude / ".odoo-dev.json" + assert global_config.exists(), "Config not written to global dir" + local_config = cwd / ".claude" / ".odoo-dev.json" + assert not local_config.exists(), "Config must NOT be written to CWD when --global used" + + +# ============================================================================= +# 9. overwrite_force — --force overwrites, creates .bak, sibling keys untouched +# ============================================================================= + + +def test_overwrite_force(tmp_path): + """--force overwrites existing odoo-dev.json, creates .bak, preserves other root keys.""" + cwd = tmp_path / "project" + cwd.mkdir() + code_dir = tmp_path / "code" + code_dir.mkdir(parents=True) + + target_dir = cwd / ".claude" + target_dir.mkdir(parents=True) + + existing = { + "odoo_source": {"layout": "webonly", "sourcePaths": [], "version": "17.0"}, + "extra_key": "should_remain", + } + config_file = target_dir / ".odoo-dev.json" + config_file.write_text(json.dumps(existing)) + + cli_result = { + "layout": None, + "odoo_dir": [], + "version": "18.0", + "addons_path": "", + } + + with ( + patch("trobz_local.odoo_dev_config._run_addons_path_cli", return_value=cli_result), + patch("trobz_local.main._get_cwd", return_value=cwd), + patch("trobz_local.main._get_global_config_dir", return_value=tmp_path / "global"), + ): + env = {**os.environ, "TLC_CODE_DIR": str(code_dir)} + result = runner.invoke(app, ["--no-newcomer", "new", "--force"], env=env) + assert result.exit_code == 0, result.output + + bak_file = target_dir / ".odoo-dev.json.bak" + assert bak_file.exists(), ".bak file not created" + + updated = json.loads(config_file.read_text()) + assert updated["odoo_source"]["version"] == "18.0" + assert updated.get("extra_key") == "should_remain" + + +# ============================================================================= +# 10. schema_valid — written file validates against JSON Schema +# ============================================================================= + + +def test_schema_valid(tmp_path): + """Written odoo-dev.json validates against schema/odoo-dev.json.""" + import jsonschema + + cwd = tmp_path / "project" + cwd.mkdir() + code_dir = tmp_path / "code" + code_dir.mkdir(parents=True) + + target_dir = cwd / ".claude" + + cli_result = { + "layout": None, + "odoo_dir": [], + "version": "18.0", + "addons_path": "", + } + + with ( + patch("trobz_local.odoo_dev_config._run_addons_path_cli", return_value=cli_result), + patch("trobz_local.main._get_cwd", return_value=cwd), + patch("trobz_local.main._get_global_config_dir", return_value=tmp_path / "global"), + ): + env = {**os.environ, "TLC_CODE_DIR": str(code_dir)} + result = runner.invoke(app, ["--no-newcomer", "new"], env=env) + assert result.exit_code == 0, result.output + + schema = json.loads(SCHEMA_PATH.read_text()) + config = _read_config(target_dir) + jsonschema.validate(config, schema) + + +# ============================================================================= +# 11. local_layout_with_enterprise — enterprise sibling present → enterprisePaths set +# ============================================================================= + + +def test_local_layout_with_enterprise(tmp_path): + """Local layout + enterprise/ sibling exists → enterprisePaths included.""" + import jsonschema + + cwd = tmp_path / "project" + cwd.mkdir() + code_dir = tmp_path / "code" + + checkout_root = code_dir / "odoo" / "odoo" / "18.0" + addons_dir = checkout_root / "addons" + addons_dir.mkdir(parents=True) + + enterprise_root = code_dir / "odoo" / "enterprise" / "18.0" + enterprise_module = enterprise_root / "some_module" / "__manifest__.py" + enterprise_module.parent.mkdir(parents=True) + enterprise_module.write_text("{}") + + cli_result = { + "layout": "fallback", + "odoo_dir": [], + "version": "18.0", + "addons_path": "", + } + + target_dir = cwd / ".claude" + + with ( + patch("trobz_local.odoo_dev_config._run_addons_path_cli", return_value=cli_result), + patch("trobz_local.main._get_cwd", return_value=cwd), + patch("trobz_local.main._get_global_config_dir", return_value=tmp_path / "global"), + ): + env = {**os.environ, "TLC_CODE_DIR": str(code_dir)} + result = runner.invoke(app, ["--no-newcomer", "new"], env=env) + assert result.exit_code == 0, result.output + + config = _read_config(target_dir) + odoo_block = config["odoo_source"] + assert odoo_block["layout"] == "local" + assert odoo_block["enterprisePaths"] == [str(enterprise_root)] + + schema = json.loads(SCHEMA_PATH.read_text()) + jsonschema.validate(config, schema) + + +# ============================================================================= +# 12. local_layout_without_enterprise — no enterprise sibling → no enterprisePaths key +# ============================================================================= + + +def test_local_layout_without_enterprise(tmp_path): + """Local layout, no enterprise/ sibling → enterprisePaths key absent.""" + cwd = tmp_path / "project" + cwd.mkdir() + code_dir = tmp_path / "code" + + checkout_root = code_dir / "odoo" / "odoo" / "18.0" + addons_dir = checkout_root / "addons" + addons_dir.mkdir(parents=True) + + cli_result = { + "layout": "fallback", + "odoo_dir": [], + "version": "18.0", + "addons_path": "", + } + + target_dir = cwd / ".claude" + + with ( + patch("trobz_local.odoo_dev_config._run_addons_path_cli", return_value=cli_result), + patch("trobz_local.main._get_cwd", return_value=cwd), + patch("trobz_local.main._get_global_config_dir", return_value=tmp_path / "global"), + ): + env = {**os.environ, "TLC_CODE_DIR": str(code_dir)} + result = runner.invoke(app, ["--no-newcomer", "new"], env=env) + assert result.exit_code == 0, result.output + + config = _read_config(target_dir) + odoo_block = config["odoo_source"] + assert odoo_block["layout"] == "local" + assert "enterprisePaths" not in odoo_block + + +# ============================================================================= +# 13. managed_layout_no_enterprise_omits_key — no enterprise sibling → no key +# ============================================================================= + + +def test_managed_layout_no_enterprise_omits_key(tmp_path): + """Managed layout (trobz) without enterprise sibling: enterprisePaths key absent.""" + cwd = tmp_path / "project" + cwd.mkdir() + code_dir = tmp_path / "code" + code_dir.mkdir() + + odoo_addons = tmp_path / "project" / "odoo" / "addons" + odoo_addons.mkdir(parents=True) + + cli_result = { + "layout": "Trobz", + "odoo_dir": [str(odoo_addons)], + "version": "18.0", + "addons_path": "", + } + + target_dir = cwd / ".claude" + + with ( + patch("trobz_local.odoo_dev_config._run_addons_path_cli", return_value=cli_result), + patch("trobz_local.main._get_cwd", return_value=cwd), + patch("trobz_local.main._get_global_config_dir", return_value=tmp_path / "global"), + ): + env = {**os.environ, "TLC_CODE_DIR": str(code_dir)} + result = runner.invoke(app, ["--no-newcomer", "new"], env=env) + assert result.exit_code == 0, result.output + + config = _read_config(target_dir) + odoo_block = config["odoo_source"] + assert odoo_block["layout"] == "trobz" + assert "enterprisePaths" not in odoo_block + + +# ============================================================================= +# 14. managed_layout_empty_source_paths_falls_through — all odoo_dir missing → fallthrough +# ============================================================================= + + +def test_managed_layout_empty_source_paths_falls_through(tmp_path): + """Managed layout with all-missing odoo_dir entries: falls through to webonly, no empty sourcePaths.""" + cwd = tmp_path / "project" + cwd.mkdir() + code_dir = tmp_path / "code" + code_dir.mkdir() + + cli_result = { + "layout": "Trobz", + "odoo_dir": ["/does/not/exist/addons"], + "version": "18.0", + "addons_path": "", + } + + target_dir = cwd / ".claude" + + with ( + patch("trobz_local.odoo_dev_config._run_addons_path_cli", return_value=cli_result), + patch("trobz_local.main._get_cwd", return_value=cwd), + patch("trobz_local.main._get_global_config_dir", return_value=tmp_path / "global"), + ): + env = {**os.environ, "TLC_CODE_DIR": str(code_dir)} + result = runner.invoke(app, ["--no-newcomer", "new"], env=env) + assert result.exit_code == 0, result.output + + config = _read_config(target_dir) + odoo_block = config["odoo_source"] + # Must NOT emit a managed block with empty sourcePaths + assert odoo_block["layout"] != "trobz" or odoo_block.get("sourcePaths") != [] + # Falls through to webonly (no local checkout was created) + assert odoo_block["layout"] == "webonly" + + +# ============================================================================= +# 15. odoosh_layout_with_enterprise_symlink — cwd/enterprise symlink → enterprisePaths set +# ============================================================================= + + +def test_odoosh_layout_with_enterprise_symlink(tmp_path): + """odoo.sh layout with cwd/enterprise symlink: enterprisePaths resolves to real target.""" + cwd = tmp_path / "project" + cwd.mkdir() + code_dir = tmp_path / "code" + code_dir.mkdir() + + odoo_addons = cwd / "odoo" / "addons" + odoo_addons.mkdir(parents=True) + + # Create a real enterprise dir and symlink it at cwd/enterprise + real_enterprise = tmp_path / "real-enterprise" + real_enterprise.mkdir() + enterprise_link = cwd / "enterprise" + enterprise_link.symlink_to(real_enterprise) + + cli_result = { + "layout": "odoo.sh", + "odoo_dir": [str(odoo_addons)], + "version": "18.0", + "addons_path": "", + } + + target_dir = cwd / ".claude" + + with ( + patch("trobz_local.odoo_dev_config._run_addons_path_cli", return_value=cli_result), + patch("trobz_local.main._get_cwd", return_value=cwd), + patch("trobz_local.main._get_global_config_dir", return_value=tmp_path / "global"), + ): + env = {**os.environ, "TLC_CODE_DIR": str(code_dir)} + result = runner.invoke(app, ["--no-newcomer", "new"], env=env) + assert result.exit_code == 0, result.output + + config = _read_config(target_dir) + odoo_block = config["odoo_source"] + assert odoo_block["layout"] == "odoosh" + assert "enterprisePaths" in odoo_block + assert odoo_block["enterprisePaths"] == [str(real_enterprise.resolve())] + + +# ============================================================================= +# 16. odoosh_layout_without_enterprise — no cwd/enterprise dir → no key +# ============================================================================= + + +def test_odoosh_layout_without_enterprise(tmp_path): + """odoo.sh layout without cwd/enterprise dir: enterprisePaths key absent.""" + cwd = tmp_path / "project" + cwd.mkdir() + code_dir = tmp_path / "code" + code_dir.mkdir() + + odoo_addons = cwd / "odoo" / "addons" + odoo_addons.mkdir(parents=True) + + cli_result = { + "layout": "odoo.sh", + "odoo_dir": [str(odoo_addons)], + "version": "18.0", + "addons_path": "", + } + + target_dir = cwd / ".claude" + + with ( + patch("trobz_local.odoo_dev_config._run_addons_path_cli", return_value=cli_result), + patch("trobz_local.main._get_cwd", return_value=cwd), + patch("trobz_local.main._get_global_config_dir", return_value=tmp_path / "global"), + ): + env = {**os.environ, "TLC_CODE_DIR": str(code_dir)} + result = runner.invoke(app, ["--no-newcomer", "new"], env=env) + assert result.exit_code == 0, result.output + + config = _read_config(target_dir) + odoo_block = config["odoo_source"] + assert odoo_block["layout"] == "odoosh" + assert "enterprisePaths" not in odoo_block + + +# ============================================================================= +# 17. trobz_layout_with_enterprise_sibling — code_dir/odoo/enterprise/ present +# ============================================================================= + + +def test_trobz_layout_with_enterprise_sibling(tmp_path): + """Managed trobz layout with enterprise sibling at code_dir/odoo/enterprise/.""" + cwd = tmp_path / "project" + cwd.mkdir() + code_dir = tmp_path / "code" + code_dir.mkdir() + + odoo_addons = cwd / "odoo" / "addons" + odoo_addons.mkdir(parents=True) + + enterprise_dir = code_dir / "odoo" / "enterprise" / "18.0" + enterprise_dir.mkdir(parents=True) + + cli_result = { + "layout": "Trobz", + "odoo_dir": [str(odoo_addons)], + "version": "18.0", + "addons_path": "", + } + + target_dir = cwd / ".claude" + + with ( + patch("trobz_local.odoo_dev_config._run_addons_path_cli", return_value=cli_result), + patch("trobz_local.main._get_cwd", return_value=cwd), + patch("trobz_local.main._get_global_config_dir", return_value=tmp_path / "global"), + ): + env = {**os.environ, "TLC_CODE_DIR": str(code_dir)} + result = runner.invoke(app, ["--no-newcomer", "new"], env=env) + assert result.exit_code == 0, result.output + + config = _read_config(target_dir) + odoo_block = config["odoo_source"] + assert odoo_block["layout"] == "trobz" + assert "enterprisePaths" in odoo_block + assert odoo_block["enterprisePaths"] == [str(enterprise_dir.resolve())] diff --git a/trobz_local/assets/odoo-dev.json b/trobz_local/assets/odoo-dev.json new file mode 100644 index 0000000..5f1326d --- /dev/null +++ b/trobz_local/assets/odoo-dev.json @@ -0,0 +1,30 @@ +{ + "type": "object", + "required": ["odoo_source"], + "properties": { + "odoo_source": { + "type": "object", + "required": ["layout", "sourcePaths"], + "properties": { + "layout": { + "type": "string", + "enum": ["trobz", "c2c", "odoosh", "doodba", "local", "webonly"] + }, + "version": { + "type": "string", + "pattern": "^\\d+\\.\\d+$" + }, + "sourcePaths": { + "type": "array", + "items": {"type": "string"} + }, + "enterprisePaths": { + "type": "array", + "items": {"type": "string"} + } + }, + "additionalProperties": false + } + }, + "additionalProperties": true +} diff --git a/trobz_local/main.py b/trobz_local/main.py index da572ca..93da669 100644 --- a/trobz_local/main.py +++ b/trobz_local/main.py @@ -22,6 +22,7 @@ install_uv_tools, setup_postgresql_repo, ) +from .odoo_dev_config import resolve_odoo_config, write_odoo_dev_config from .postgres import ( check_postgres_running, check_user_exists, @@ -655,3 +656,36 @@ def doctor(): if has_fail: raise typer.Exit(code=1) + + +def _get_cwd() -> Path: + return Path.cwd() + + +def _get_global_config_dir() -> Path: + return Path.home() / ".claude" + + +@app.command() +def new( + ctx: typer.Context, + global_: bool = typer.Option(False, "--global", "-g", help="Write to ~/.claude/ instead of CWD/.claude/"), + force: bool = typer.Option(False, "--force", help="Skip overwrite confirmation."), +): + """Detect Odoo layout and write .odoo-dev.json config.""" + _run_new(ctx, global_=global_, force=force) + + +def _run_new(ctx: typer.Context, *, global_: bool = False, force: bool = False): + import os + + cwd = _get_cwd() + tlc_code_dir = os.environ.get("TLC_CODE_DIR") + code_dir = Path(tlc_code_dir) if tlc_code_dir else get_code_root() + + target_dir = _get_global_config_dir() if global_ else cwd / ".claude" + + target_dir.mkdir(parents=True, exist_ok=True) + + odoo_block = resolve_odoo_config(cwd, code_dir) + write_odoo_dev_config(target_dir, odoo_block, force=force) diff --git a/trobz_local/odoo_dev_config.py b/trobz_local/odoo_dev_config.py new file mode 100644 index 0000000..7d4a585 --- /dev/null +++ b/trobz_local/odoo_dev_config.py @@ -0,0 +1,256 @@ +"""Odoo source resolution and odoo-dev.json write logic for `tlc new`.""" + +from __future__ import annotations + +import json +import os +import re +import shutil +import subprocess +from pathlib import Path + +import jsonschema +import typer + +# Maps CLI-emitted detector names to .odoo-dev.json schema enum values. +_DETECTOR_NAME_MAP = { + "Trobz": "trobz", + "Camptocamp": "c2c", + "Camptocamp (Legacy)": "c2c", + "odoo.sh": "odoosh", + "Doodba": "doodba", +} + +_SCHEMA_PATH = Path(__file__).parent / "assets" / "odoo-dev.json" + +_MANAGED_LAYOUTS = frozenset(_DETECTOR_NAME_MAP) + + +def _load_schema() -> dict: + return json.loads(_SCHEMA_PATH.read_text()) + + +# --------------------------------------------------------------------------- +# CLI subprocess helper +# --------------------------------------------------------------------------- + + +def _run_addons_path_cli(cwd: Path) -> dict | None: + """Run ``odoo-addons-path --format json `` and return parsed JSON. + + Returns None on missing binary, non-zero exit, timeout, or JSON parse error. + The caller treats None as "no managed layout detected". + """ + try: + proc = subprocess.run( # noqa: S603 + ["odoo-addons-path", "--format", "json", str(cwd)], # noqa: S607 + capture_output=True, + text=True, + timeout=10, + ) + if proc.returncode != 0: + typer.secho( + f"odoo-addons-path exited {proc.returncode}; falling through to local/webonly", + fg=typer.colors.YELLOW, + err=True, + ) + return None + return json.loads(proc.stdout) + except FileNotFoundError: + typer.secho("odoo-addons-path not found; falling through to local/webonly", fg=typer.colors.YELLOW, err=True) + return None + except (subprocess.TimeoutExpired, json.JSONDecodeError) as exc: + typer.secho( + f"odoo-addons-path unusable ({exc}); falling through to local/webonly", + fg=typer.colors.YELLOW, + err=True, + ) + return None + + +# --------------------------------------------------------------------------- +# Git-branch version fallback (patchable in tests) +# --------------------------------------------------------------------------- + + +def _git_branch_version(cwd: Path) -> str | None: + """Return the version prefix from the current git branch, or None.""" + try: + result = subprocess.run( + ["git", "branch", "--show-current"], # noqa: S607 + capture_output=True, + text=True, + cwd=str(cwd), + timeout=5, + ) + if result.returncode == 0: + match = re.match(r"^(\d+\.\d+)", result.stdout.strip()) + if match: + return match.group(1) + except Exception: # noqa: S110 + pass + return None + + +# --------------------------------------------------------------------------- +# Core resolution +# --------------------------------------------------------------------------- + + +def _build_managed_block( + layout_name: str, odoo_dir: list, version: str | None, cwd: Path, code_dir: Path +) -> dict | None: + """Return managed-layout odoo block, or None to fall through.""" + schema_layout = _DETECTOR_NAME_MAP[layout_name] + source_paths = [str(p) for p in odoo_dir if Path(p).is_dir()] + if not source_paths: + typer.secho( + f"{layout_name} detected but no odoo_dir entries exist on disk " + f"({odoo_dir}); falling through to local/webonly", + fg=typer.colors.YELLOW, + err=True, + ) + return None + block: dict = {"layout": schema_layout, "sourcePaths": source_paths} + if version: + block["version"] = version + enterprise_paths = _derive_enterprise_paths(layout=schema_layout, cwd=cwd, code_dir=code_dir, version=version) + if enterprise_paths: + block["enterprisePaths"] = enterprise_paths + return block + + +def _build_local_block(version: str, cwd: Path, code_dir: Path) -> dict | None: + """Return local-layout odoo block, or None when checkout is absent/empty.""" + checkout_root = code_dir / "odoo" / "odoo" / version + if not checkout_root.exists(): + return None + source_paths = _derive_local_source_paths(checkout_root) + if not source_paths: + return None + block: dict = {"layout": "local", "version": version, "sourcePaths": source_paths} + enterprise_paths = _derive_enterprise_paths(layout="local", cwd=cwd, code_dir=code_dir, version=version) + if enterprise_paths: + block["enterprisePaths"] = enterprise_paths + return block + + +def resolve_odoo_config(cwd: Path, code_dir: Path) -> dict: + """Detect Odoo layout and build the ``odoo`` block for odoo-dev.json.""" + cli = _run_addons_path_cli(cwd) + + layout_name = cli.get("layout") if cli else None + odoo_dir = cli.get("odoo_dir", []) if cli else [] + version = cli.get("version") if cli else None + + if layout_name in _MANAGED_LAYOUTS and odoo_dir: + block = _build_managed_block(layout_name, odoo_dir, version, cwd, code_dir) + if block is not None: + return block + + if not version: + version = _git_branch_version(cwd) + + if version: + block = _build_local_block(version, cwd, code_dir) + if block is not None: + return block + + odoo_block = {"layout": "webonly", "sourcePaths": []} + if version: + odoo_block["version"] = version + return odoo_block + + +def _resolve_symlink(path: Path) -> str: + """Return the absolute, symlink-resolved path as a string.""" + return str(path.resolve(strict=False)) + + +def _derive_enterprise_paths( + *, + layout: str, + cwd: Path, + code_dir: Path, + version: str | None, +) -> list[str]: + """Return enterprise addon paths for the detected layout, or [].""" + if layout == "odoosh": + candidate = cwd / "enterprise" + if candidate.is_dir(): + return [_resolve_symlink(candidate)] + return [] + + if layout in {"trobz", "local"} and version: + candidate = code_dir / "odoo" / "enterprise" / version + if candidate.is_dir(): + return [_resolve_symlink(candidate)] + return [] + + # c2c / doodba: no convention yet + return [] + + +def _derive_local_source_paths(checkout_root: Path) -> list[str]: + """Return existing standard addons dirs under a local Odoo checkout.""" + candidates = [ + checkout_root / "addons", + checkout_root / "odoo" / "addons", + ] + return [str(p) for p in candidates if p.is_dir()] + + +# --------------------------------------------------------------------------- +# Write +# --------------------------------------------------------------------------- + + +def write_odoo_dev_config( + target_dir: Path, + odoo_block: dict, + *, + force: bool = False, +) -> None: + """Write (or overwrite) odoo-dev.json in target_dir. + + - Backs up existing file to .odoo-dev.json.bak before overwrite. + - Merges: replaces only the ``odoo`` key; sibling keys are preserved. + - Validates result against the JSON Schema before writing. + - Uses atomic write (.tmp → os.replace). + """ + config_file = target_dir / ".odoo-dev.json" + tmp_file = target_dir / ".odoo-dev.json.tmp" + bak_file = target_dir / ".odoo-dev.json.bak" + + existing_data: dict = {} + if config_file.exists(): + shutil.copy2(str(config_file), str(bak_file)) + if not force and not typer.confirm(f"{config_file} already exists. Overwrite?", default=False): + raise typer.Abort() + try: + existing_data = json.loads(config_file.read_text()) + except json.JSONDecodeError: + existing_data = {} + + # Merge: replace odoo key, keep siblings + new_data = {**existing_data, "odoo_source": odoo_block} + + # Validate + schema = _load_schema() + jsonschema.validate(new_data, schema) + + # Print resolved sourcePaths so user sees what was cached + typer.secho("Resolved sourcePaths:", fg=typer.colors.CYAN) + for sp in odoo_block.get("sourcePaths", []): + typer.echo(f" {sp}") + enterprise_paths = odoo_block.get("enterprisePaths") + if enterprise_paths: + typer.secho("Resolved enterprisePaths:", fg=typer.colors.CYAN) + for ep in enterprise_paths: + typer.echo(f" {ep}") + + # Atomic write + tmp_file.write_text(json.dumps(new_data, indent=2) + "\n") + os.replace(str(tmp_file), str(config_file)) + + typer.secho(f"Written: {config_file}", fg=typer.colors.GREEN) diff --git a/uv.lock b/uv.lock index 88fd6a9..3c26685 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.10" [[package]] @@ -11,6 +11,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + [[package]] name = "certifi" version = "2026.1.4" @@ -378,6 +387,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + [[package]] name = "markdown-it-py" version = "4.0.0" @@ -493,6 +529,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, ] +[[package]] +name = "odoo-addons-path" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/da/f00447be09b031f756dce1a3d0d9821e0a74818865f1db9e5fcbd87f6278/odoo_addons_path-1.3.0.tar.gz", hash = "sha256:dbc3115a62cb63067eb048abb9ac078e68b497b7fa97387ff5ff74c5d38368fe", size = 458931, upload-time = "2026-05-11T07:58:32.146Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/b1/fe7263cfa05eb20402aaec25352f4a6be187ed022c9132200a0dda327f6c/odoo_addons_path-1.3.0-py3-none-any.whl", hash = "sha256:5fe341f401f85fa86a103eee3b886808b1597ed4586a460052c9312939391cf1", size = 20897, upload-time = "2026-05-11T07:58:30.53Z" }, +] + [[package]] name = "packaging" version = "25.0" @@ -811,6 +860,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + [[package]] name = "requests" version = "2.32.5" @@ -851,6 +914,128 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, ] +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, + { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, + { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, + { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, + { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, + { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + [[package]] name = "ruff" version = "0.14.10" @@ -959,6 +1144,8 @@ version = "0.8.0" source = { editable = "." } dependencies = [ { name = "gitpython" }, + { name = "jsonschema" }, + { name = "odoo-addons-path" }, { name = "pydantic" }, { name = "tomli" }, { name = "typer" }, @@ -982,6 +1169,8 @@ dev = [ [package.metadata] requires-dist = [ { name = "gitpython", specifier = ">=3.1.45" }, + { name = "jsonschema", specifier = ">=4.0" }, + { name = "odoo-addons-path", specifier = ">=1.3.0" }, { name = "pydantic", specifier = ">=2.12.5" }, { name = "tomli", specifier = ">=2.3.0" }, { name = "typer", specifier = ">=0.20" },