bl-odoo 0.2.7__tar.gz → 0.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bl-odoo
3
- Version: 0.2.7
3
+ Version: 0.3.1
4
4
  Summary: A command-line tool for managing Odoo dependencies.
5
5
  Author-email: Your Name <your.email@example.com>
6
6
  License-Expression: MIT
@@ -12,15 +12,19 @@ def run():
12
12
  parser = argparse.ArgumentParser(
13
13
  description="Process a project specification.", formatter_class=argparse.ArgumentDefaultsHelpFormatter
14
14
  )
15
- parser.add_argument(
16
- "-f", "--freeze", const=True, default=None, nargs="?", type=Path, help="Freeze the current state of modules"
17
- )
18
- parser.add_argument(
15
+
16
+ parent_parser = argparse.ArgumentParser(add_help=False)
17
+ parent_parser.add_argument(
19
18
  "-c", "--config", type=Path, help="Path to the project specification file.", default="spec.yaml"
20
19
  )
21
- parser.add_argument("-z", "--frozen", type=Path, help="Path to the frozen specification file.")
22
- parser.add_argument("-j", "--concurrency", type=int, default=28, help="Number of concurrent tasks.")
23
- parser.add_argument("-w", "--workdir", type=Path, help="Working directory. Defaults to config directory.")
20
+ parent_parser.add_argument("-z", "--frozen", type=Path, help="Path to the frozen specification file.")
21
+ parent_parser.add_argument("-j", "--concurrency", type=int, default=28, help="Number of concurrent tasks.")
22
+ parent_parser.add_argument("-w", "--workdir", type=Path, help="Working directory. Defaults to config directory.")
23
+
24
+ sub = parser.add_subparsers(help="subcommand help", dest="command")
25
+ build = sub.add_parser("build", parents=[parent_parser], help="build help")
26
+ freeze = sub.add_parser("freeze", parents=[parent_parser], help="freeze help")
27
+
24
28
  args = parser.parse_args()
25
29
 
26
30
  project_spec = load_spec_file(args.config, args.frozen, args.workdir)
@@ -28,9 +32,9 @@ def run():
28
32
  sys.exit(1)
29
33
 
30
34
  try:
31
- if args.freeze:
35
+ if args.command == "freeze":
32
36
  asyncio.run(freeze_project(project_spec, args.freeze, concurrency=args.concurrency))
33
- else:
37
+ elif args.command == "build":
34
38
  asyncio.run(process_project(project_spec, concurrency=args.concurrency))
35
39
  except Exception:
36
40
  sys.exit(1)
@@ -1,13 +1,11 @@
1
1
  import asyncio
2
2
  import yaml
3
- from operator import countOf
4
3
  from pathlib import Path
5
- from typing import TextIO
6
4
 
7
5
  from rich.console import Console
8
6
  from rich.live import Live
9
7
  from rich.progress import BarColumn, MofNCompleteColumn, Progress, TaskID, TextColumn
10
- from bl.spec_parser import ModuleSpec, ProjectSpec
8
+ from bl.types import RepoInfo, ProjectSpec
11
9
  from bl.utils import get_local_ref, get_module_path, run_git
12
10
 
13
11
  console = Console()
@@ -18,7 +16,7 @@ async def freeze_spec(
18
16
  progress: Progress,
19
17
  task_id: TaskID,
20
18
  module_name: str,
21
- module_spec: ModuleSpec,
19
+ module_spec: RepoInfo,
22
20
  workdir: Path,
23
21
  ):
24
22
  result = {module_name: {}}
@@ -0,0 +1,159 @@
1
+ import re
2
+ import warnings
3
+ from dataclasses import dataclass
4
+ from enum import Enum
5
+ from pathlib import Path
6
+ from typing import Any, Dict, List, Optional, Type
7
+
8
+ import yaml
9
+
10
+ from bl.types import RepoInfo, OriginType, ProjectSpec, RefspecInfo
11
+
12
+
13
+ def make_remote_merge_from_src(src: str) -> tuple[dict, list]:
14
+ """
15
+ Creates a remote and merge entry from the src string.
16
+ """
17
+ remotes = {}
18
+ merges = []
19
+
20
+ parts = src.split(" ", 1)
21
+ remotes["origin"] = parts[0]
22
+ merges.append(f"origin {parts[1]}")
23
+
24
+ return remotes, merges
25
+
26
+
27
+ def get_origin_type(origin_value: str) -> OriginType:
28
+ """
29
+ Determines the origin type based on the origin value.
30
+
31
+ Args:
32
+ origin_value: The origin string to evaluate.
33
+
34
+ Returns:
35
+ The corresponding OriginType.
36
+ """
37
+ # Pattern to match GitHub PR references: refs/pull/{pr_id}/head
38
+ pr_pattern = re.compile(r"^refs/pull/\d+/head$")
39
+ # Pattern to match that matches git reference hashes (40 hex characters)
40
+ ref_pattern = re.compile(r"^[a-z0-9]{40}$")
41
+
42
+ if pr_pattern.match(origin_value):
43
+ return OriginType.PR
44
+ elif ref_pattern.match(origin_value):
45
+ return OriginType.REF
46
+ else:
47
+ return OriginType.BRANCH
48
+
49
+
50
+ def parse_remote_refspec_from_parts(parts: List[str], frozen_repo: Dict[str, Dict[str, str]]):
51
+ if len(parts) == 2:
52
+ parts.insert(1, "")
53
+ else:
54
+ warnings.warn(
55
+ "Deprecated src format: use <url> <sha> format for the src property",
56
+ DeprecationWarning,
57
+ )
58
+ remote_key, _, ref_spec = parts
59
+ ref_type = get_origin_type(ref_spec)
60
+
61
+ ref_name = None
62
+ remote_freezes = frozen_repo.get(remote_key, {})
63
+
64
+ if ref_spec in remote_freezes:
65
+ ref_type = OriginType.REF
66
+ ref_name = ref_spec
67
+ ref_spec = remote_freezes.get(ref_name)
68
+
69
+ return RefspecInfo(remote_key, ref_spec, ref_type, ref_name)
70
+
71
+
72
+ def get_with_syntax_check(name, data, key: str, type: Type):
73
+ result = data.get(key, type())
74
+ if not isinstance(result, type):
75
+ raise Exception(f"Key {key} not of proper syntax should be {str(type)} in {name} description")
76
+ return result
77
+
78
+
79
+ def load_spec_file(config: Path, frozen: Path, workdir: Path) -> Optional[ProjectSpec]:
80
+ """
81
+ Loads and parses the project specification from a YAML file.
82
+
83
+ Args:
84
+ file_path: The path to the YAML specification file.
85
+
86
+ Returns:
87
+ A ProjectSpec object if successful, None otherwise.
88
+ """
89
+ if not config.exists():
90
+ if config.is_relative_to("."):
91
+ config = config.resolve()
92
+ # If the file is not in the current directory, check inside the odoo subdirectory
93
+ odoo_config = config.parent / "odoo" / config.name
94
+ # TODO(franz): should use rich console for prettiness
95
+ if not odoo_config.exists():
96
+ print(f"Error: Neither '{config}' nor '{odoo_config}' exists.")
97
+ return None
98
+ config = odoo_config
99
+ else:
100
+ print(f"Error: File '{config}' does not exist.")
101
+ return None
102
+
103
+ workdir = workdir or config.parent
104
+
105
+ with config.open("r") as f:
106
+ try:
107
+ data: Dict[str, Any] = yaml.safe_load(f)
108
+ except yaml.YAMLError as e:
109
+ print(f"Error parsing YAML file '{config}': {e}")
110
+ return None
111
+
112
+ frozen_mapping: Dict[str, Dict[str, Dict[str, str]]] = {}
113
+ frozen_path = frozen or Path(config).with_name("frozen.yaml")
114
+ if frozen_path.exists():
115
+ try:
116
+ with frozen_path.open("r") as frozen_file:
117
+ loaded_freezes = yaml.safe_load(frozen_file) or {}
118
+ if isinstance(loaded_freezes, dict):
119
+ frozen_mapping = loaded_freezes
120
+ except yaml.YAMLError as e:
121
+ print(f"Error parsing frozen YAML file '{frozen_path}': {e}")
122
+
123
+ repos: Dict[str, RepoInfo] = {}
124
+ for repo_name, repo_data in data.items():
125
+ modules = get_with_syntax_check(repo_name, repo_data, "modules", list)
126
+ src = get_with_syntax_check(repo_name, repo_data, "src", str)
127
+ remotes = get_with_syntax_check(repo_name, repo_data, "remotes", dict)
128
+ merges = get_with_syntax_check(repo_name, repo_data, "merges", list)
129
+ shell_commands = get_with_syntax_check(repo_name, repo_data, "shell_command_after", list)
130
+ patch_globs_to_apply = get_with_syntax_check(repo_name, repo_data, "patch_globs", list)
131
+ target_folder = get_with_syntax_check(repo_name, repo_data, "target_folder", str)
132
+ locales = get_with_syntax_check(repo_name, repo_data, "locales", list)
133
+
134
+ frozen_repo = frozen_mapping.get(repo_name, {})
135
+
136
+ # Parse merges into RefspecInfo objects
137
+ refspec_infos: List[RefspecInfo] = []
138
+ if src:
139
+ # If src is defined, create a remote and merge entry from it
140
+ src_remotes, src_merges = make_remote_merge_from_src(src)
141
+ remotes.update(src_remotes)
142
+ merges = src_merges + merges
143
+
144
+ for merge_entry in merges:
145
+ parts = merge_entry.split(" ", 2)
146
+ refspec_info = parse_remote_refspec_from_parts(parts, frozen_repo)
147
+ refspec_infos.append(refspec_info)
148
+
149
+ repos[repo_name] = RepoInfo(
150
+ modules,
151
+ remotes,
152
+ refspec_infos,
153
+ shell_commands,
154
+ patch_globs_to_apply,
155
+ target_folder,
156
+ locales,
157
+ )
158
+
159
+ return ProjectSpec(repos, workdir)