bl-odoo 0.1.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.
bl_odoo-0.1.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Your Name
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
bl_odoo-0.1.1/PKG-INFO ADDED
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.4
2
+ Name: bl-odoo
3
+ Version: 0.1.1
4
+ Summary: A command-line tool for managing Odoo dependencies.
5
+ Author-email: Your Name <your.email@example.com>
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: pyyaml>=6.0.3
11
+ Requires-Dist: rich
12
+ Requires-Dist: typer
13
+ Provides-Extra: dev
14
+ Requires-Dist: ruff; extra == "dev"
15
+ Requires-Dist: kalong; extra == "dev"
16
+ Dynamic: license-file
17
+
18
+ # bl
19
+
20
+ A new Python project.
@@ -0,0 +1,3 @@
1
+ # bl
2
+
3
+ A new Python project.
File without changes
@@ -0,0 +1,18 @@
1
+ import sys
2
+ from bl.spec_parser import load_spec_file
3
+ from bl.spec_processor import process_project
4
+ from pathlib import Path
5
+ import asyncio
6
+
7
+
8
+ def run():
9
+ # Example usage:
10
+ file_name = sys.argv[1] if len(sys.argv) > 1 else "spec.yaml"
11
+ spec_file = file_name
12
+ project_spec = load_spec_file(spec_file)
13
+ if project_spec is not None:
14
+ asyncio.run(process_project(project_spec, workdir=Path("."), concurrency=16))
15
+
16
+
17
+ if __name__ == "__main__":
18
+ run()
@@ -0,0 +1,183 @@
1
+ import yaml
2
+ import re
3
+ from typing import List, Dict, Any, Optional
4
+ from enum import Enum
5
+
6
+
7
+ def make_remote_merge_from_src(src: str) -> str:
8
+ """
9
+ Creates a remote and merge entry from the src string.
10
+ """
11
+ remotes = {}
12
+ merges = []
13
+
14
+ parts = src.split(" ", 1)
15
+ remotes["origin"] = parts[0]
16
+ merges.append(f"origin {parts[1]}")
17
+
18
+ return remotes, merges
19
+
20
+
21
+ def get_origin_type(origin_value: str) -> OriginType:
22
+ """
23
+ Determines the origin type based on the origin value.
24
+
25
+ Args:
26
+ origin_value: The origin string to evaluate.
27
+
28
+ Returns:
29
+ The corresponding OriginType.
30
+ """
31
+ # Pattern to match GitHub PR references: refs/pull/{pr_id}/head
32
+ pr_pattern = re.compile(r"^refs/pull/\d+/head$")
33
+ # Pattern to match that matches git reference hashes (40 hex characters)
34
+ ref_pattern = re.compile(r"^[a-z0-9]{40}$")
35
+
36
+ if pr_pattern.match(origin_value):
37
+ return OriginType.PR
38
+ elif ref_pattern.match(origin_value):
39
+ return OriginType.REF
40
+ else:
41
+ return OriginType.BRANCH
42
+
43
+
44
+ class OriginType(Enum):
45
+ """Type of origin reference."""
46
+
47
+ BRANCH = "branch"
48
+ PR = "pr"
49
+ REF = "ref"
50
+
51
+
52
+ class ModuleOrigin:
53
+ """Represents an origin reference for a module."""
54
+
55
+ def __init__(
56
+ self,
57
+ remote: str,
58
+ origin: str,
59
+ type: OriginType,
60
+ ):
61
+ self.remote = remote
62
+ self.origin = origin
63
+ self.type = type
64
+
65
+ def __repr__(self) -> str:
66
+ return f"ModuleOrigin(remote={self.remote!r}, origin={self.origin!r}, type={self.type.value})"
67
+
68
+
69
+ class ModuleSpec:
70
+ """Represents the specification for a set of modules."""
71
+
72
+ def __init__(
73
+ self,
74
+ modules: List[str],
75
+ remotes: Optional[Dict[str, str]] = None,
76
+ origins: Optional[List[ModuleOrigin]] = None,
77
+ shell_commands: Optional[List[str]] = None,
78
+ patch_globs_to_apply: Optional[List[str]] = None,
79
+ target_folder: Optional[str] = None,
80
+ ):
81
+ self.modules = modules
82
+ self.remotes = remotes
83
+ self.origins = origins
84
+ self.shell_commands = shell_commands
85
+ self.patch_globs_to_apply = patch_globs_to_apply
86
+ self.target_folder = None
87
+
88
+ def __repr__(self) -> str:
89
+ return f"ModuleSpec(modules={self.modules}, remotes={self.remotes}, origins={self.origins})"
90
+
91
+
92
+ class ProjectSpec:
93
+ """Represents the overall project specification from the YAML file."""
94
+
95
+ def __init__(self, specs: Dict[str, ModuleSpec]):
96
+ self.specs = specs
97
+
98
+ def __repr__(self) -> str:
99
+ return f"ProjectSpec(specs={self.specs})"
100
+
101
+
102
+ def load_spec_file(file_path: str) -> Optional[ProjectSpec]:
103
+ """
104
+ Loads and parses the project specification from a YAML file.
105
+
106
+ Args:
107
+ file_path: The path to the YAML specification file.
108
+
109
+ Returns:
110
+ A ProjectSpec object if successful, None otherwise.
111
+ """
112
+ try:
113
+ with open(file_path, "r") as f:
114
+ data: Dict[str, Any] = yaml.safe_load(f)
115
+
116
+ specs: Dict[str, ModuleSpec] = {}
117
+ for section_name, section_data in data.items():
118
+ modules = section_data.get("modules", [])
119
+ src = section_data.get("src")
120
+ remotes = section_data.get("remotes") or {}
121
+ merges = section_data.get("merges") or []
122
+ shell_commands = section_data.get("shell_command_after") or None
123
+ patch_globs_to_apply = section_data.get("patch_globs") or None
124
+
125
+ # Parse merges into ModuleOrigin objects
126
+ origins: List[ModuleOrigin] = []
127
+ if src:
128
+ # If src is defined, create a remote and merge entry from it
129
+ src_remotes, src_merges = make_remote_merge_from_src(src)
130
+ remotes.update(src_remotes)
131
+ merges = src_merges + merges
132
+
133
+ for merge_entry in merges:
134
+ parts = merge_entry.split(" ", 2)
135
+ if len(parts) == 2:
136
+ remote_key = parts[0]
137
+ origin_value = parts[1]
138
+
139
+ # Determine type: PR if matches refs/pull/{pr_id}/head pattern, otherwise branch
140
+ origin_type = get_origin_type(origin_value)
141
+
142
+ origins.append(ModuleOrigin(remote_key, origin_value, origin_type))
143
+ if len(parts) == 3 and remote_key not in remotes:
144
+ remote_key = parts[0]
145
+ origin_value = parts[2]
146
+
147
+ origin_type = get_origin_type(origin_value)
148
+
149
+ origins.append(ModuleOrigin(remote_key, origin_value, origin_type))
150
+
151
+ specs[section_name] = ModuleSpec(
152
+ modules,
153
+ remotes if remotes else None,
154
+ origins if origins else None,
155
+ shell_commands,
156
+ patch_globs_to_apply,
157
+ )
158
+
159
+ return ProjectSpec(specs)
160
+
161
+ except FileNotFoundError:
162
+ print(f"Error: The file '{file_path}' was not found.")
163
+ return None
164
+ except yaml.YAMLError as e:
165
+ print(f"Error parsing YAML file '{file_path}': {e}")
166
+ return None
167
+ except Exception as e:
168
+ print(f"An unexpected error occurred while processing '{file_path}': {e}")
169
+ return None
170
+
171
+
172
+ if __name__ == "__main__":
173
+ # Example usage:
174
+ spec_file = "spec.yaml"
175
+ project_spec = load_spec_file(spec_file)
176
+
177
+ if project_spec:
178
+ print("Successfully loaded project specification:")
179
+ # You can access the data like this:
180
+ # print(project_spec.specs['odoo'].modules)
181
+ # print(project_spec.specs['server-ux'].remotes)
182
+ # print(project_spec.specs['server-ux'].origins)
183
+ print([spec for name, spec in project_spec.specs.items() if name == "queue"])
@@ -0,0 +1,417 @@
1
+ import asyncio
2
+ import os
3
+ import hashlib
4
+ import warnings
5
+ from pathlib import Path
6
+ from typing import List, Dict, Optional, Any
7
+ from typing_extensions import deprecated
8
+ from rich.progress import MofNCompleteColumn, Progress, SpinnerColumn, TextColumn, BarColumn, TaskID
9
+ from rich.live import Live
10
+ from rich.console import Console
11
+ from .spec_parser import ProjectSpec, ModuleSpec, ModuleOrigin, OriginType
12
+
13
+ BASE_DEPTH_VALUE = 10000
14
+
15
+ console = Console()
16
+
17
+
18
+ def rich_warning(message, category, filename, lineno, file=None, line=None):
19
+ console.print(f"[yellow]Warning:[/] {category.__name__}: {message}\n[dim]{filename}:{lineno}[/]")
20
+
21
+
22
+ warnings.showwarning = rich_warning
23
+ warnings.simplefilter("default", DeprecationWarning)
24
+
25
+
26
+ english_env = os.environ.copy()
27
+ # Ensure git outputs in English for consistent parsing
28
+ english_env["LANG"] = "en_US.UTF-8"
29
+
30
+
31
+ def create_clone_args(base_origin: ModuleOrigin, remote_url: str) -> List[str]:
32
+ """Creates git clone arguments based on the base origin."""
33
+ args = [
34
+ "clone",
35
+ "--no-checkout",
36
+ "--filter=blob:none",
37
+ "--depth",
38
+ "1",
39
+ ]
40
+
41
+ if base_origin.type == OriginType.REF:
42
+ args += [
43
+ "--revision",
44
+ base_origin.origin,
45
+ ]
46
+ else:
47
+ args += [
48
+ "--origin",
49
+ base_origin.remote,
50
+ "--single-branch",
51
+ "--branch",
52
+ base_origin.origin,
53
+ ]
54
+
55
+ args += [
56
+ remote_url,
57
+ ]
58
+
59
+ return args
60
+
61
+
62
+ def _get_local_ref(origin: ModuleOrigin) -> str:
63
+ """Generates a local reference name for a given origin."""
64
+ if origin.type == OriginType.PR:
65
+ pr_id = origin.origin.split("/")[-2]
66
+ return f"pr/{pr_id}"
67
+ else:
68
+ return f"loc-{origin.origin}"
69
+
70
+
71
+ class SpecProcessor:
72
+ """
73
+ Processes a ProjectSpec by concurrently cloning and merging modules.
74
+ """
75
+
76
+ def __init__(self, workdir: Path, concurrency: int = 4):
77
+ self.workdir = workdir
78
+ self.modules_dir = workdir / "external-src"
79
+ self.cache_dir = workdir / "_cache"
80
+ self.concurrency = concurrency
81
+ self.semaphore = asyncio.Semaphore(concurrency)
82
+
83
+ def _get_cache_path(self, remote_url: str) -> Path:
84
+ """Returns a unique cache path for a given remote URL."""
85
+ url_hash = hashlib.sha256(remote_url.encode()).hexdigest()[:12]
86
+ return self.cache_dir / url_hash
87
+
88
+ def get_module_path(self, module_name: str, module_spec: ModuleSpec) -> Path:
89
+ """Returns the path to the module directory."""
90
+ if module_name == "odoo" and module_spec.target_folder is None:
91
+ console.print(
92
+ "[yellow]Warning:[/] importing 'odoo' without a target_folder "
93
+ + "property is deprecated. Use target_folder: 'src/' in spec.yaml."
94
+ )
95
+ return self.workdir / "src/"
96
+ else:
97
+ return self.workdir / "external-src" / module_name
98
+
99
+ async def run_git(self, *args: str, cwd: Optional[Path] = None) -> tuple[int, str, str]:
100
+ """Executes a git command asynchronously."""
101
+ proc = await asyncio.create_subprocess_exec(
102
+ "git",
103
+ *args,
104
+ stdout=asyncio.subprocess.PIPE,
105
+ stderr=asyncio.subprocess.PIPE,
106
+ cwd=str(cwd) if cwd else None,
107
+ env=english_env,
108
+ )
109
+ stdout, stderr = await proc.communicate()
110
+ returncode = proc.returncode if proc.returncode is not None else -1
111
+ return returncode, stdout.decode().strip(), stderr.decode().strip()
112
+
113
+ @deprecated(
114
+ "run_shell_commands is deprecated if used to apply patches. Use patch_globs properties in spec.yaml instead."
115
+ )
116
+ async def run_shell_commands(
117
+ self, progress: Progress, task_id: TaskID, spec: ModuleSpec, module_path: Path
118
+ ) -> None:
119
+ for cmd in spec.shell_commands:
120
+ progress.update(task_id, status=f"Running shell command: {cmd}...")
121
+ proc = await asyncio.create_subprocess_shell(
122
+ cmd,
123
+ cwd=str(module_path),
124
+ stdout=asyncio.subprocess.PIPE,
125
+ stderr=asyncio.subprocess.PIPE,
126
+ env=english_env,
127
+ )
128
+ stdout, stderr = await proc.communicate()
129
+ if proc.returncode != 0:
130
+ # This is a sanity check because people usually put "git am" commands
131
+ # in shell_commands, so we abort any ongoing git am
132
+ await self.run_git("am", "--abort", cwd=str(module_path))
133
+ progress.update(
134
+ task_id,
135
+ status=f"[red]Shell command failed: {cmd}\nError: {stderr.decode().strip()}",
136
+ )
137
+ return -1
138
+ return 0
139
+
140
+ async def deepen_fetch(
141
+ self,
142
+ remote_url: str,
143
+ origin: str,
144
+ local_ref: str,
145
+ module_path: Path,
146
+ depth: str,
147
+ ) -> tuple[int, str, str]:
148
+ await self.run_git(
149
+ "fetch",
150
+ "--deepen",
151
+ depth,
152
+ remote_url,
153
+ f"{origin}:{local_ref}",
154
+ cwd=module_path,
155
+ )
156
+
157
+ async def try_merge(
158
+ self,
159
+ progress: Progress,
160
+ task_id: TaskID,
161
+ remote_url: str,
162
+ local_ref: str,
163
+ module_path: Path,
164
+ origin: ModuleOrigin,
165
+ base_origin: ModuleOrigin,
166
+ ) -> bool:
167
+ # Merge
168
+ for i in range(2):
169
+ progress.update(
170
+ task_id, status=f"Merging {local_ref} into {base_origin.origin} (attempt {i + 2})...", advance=0.1
171
+ )
172
+ ret, out, err = await self.run_git("merge", "--no-edit", local_ref, cwd=module_path)
173
+
174
+ if "CONFLICT" in out:
175
+ progress.update(task_id, status=f"[purple]Merge conflict while merging {origin.origin}")
176
+ return ret, out, err
177
+
178
+ if ret != 0:
179
+ await self.run_git("merge", "--abort", cwd=module_path)
180
+
181
+ depth = str(BASE_DEPTH_VALUE ** (i + 1))
182
+ progress.update(task_id, status=f"[yellow]Deepening fetch to depth {depth}...")
183
+ fetch_origin = origin.origin
184
+ await self.deepen_fetch(
185
+ remote_url,
186
+ fetch_origin,
187
+ local_ref,
188
+ module_path,
189
+ depth,
190
+ )
191
+ else:
192
+ return ret, out, err
193
+
194
+ ret, out, err = await self.run_git(
195
+ "fetch",
196
+ "--unshallow",
197
+ remote_url,
198
+ f"{origin.origin}:{local_ref}",
199
+ cwd=module_path,
200
+ )
201
+ if ret != 0:
202
+ progress.update(task_id, status=f"[red]epen fetch failed while merging {local_ref}: {err}")
203
+ return ret, out, err
204
+
205
+ ret, out, err = await self.run_git("merge", "--no-edit", local_ref, cwd=module_path)
206
+ if ret != 0:
207
+ progress.update(task_id, status=f"[red]Merge conflict in {origin.origin}: {err}")
208
+ # In case of conflict, we might want to abort the merge
209
+ await self.run_git("merge", "--abort", cwd=module_path)
210
+
211
+ return ret, out, err
212
+
213
+ async def process_module(
214
+ self, name: str, spec: ModuleSpec, progress: Progress, count_progress: Progress, count_task: TaskID
215
+ ) -> None:
216
+ """Processes a single ModuleSpec."""
217
+ total_steps = len(spec.origins) if spec.origins else 1
218
+
219
+ async with self.semaphore:
220
+ task_id = progress.add_task(f"[cyan]{name}", status="Waiting...", total=total_steps)
221
+ try:
222
+ if not spec.origins:
223
+ progress.update(task_id, status="[yellow]No origins defined", completed=1)
224
+ return -1
225
+
226
+ module_path = self.get_module_path(name, spec)
227
+
228
+ # 1. Initialize with first origin
229
+ base_origin = spec.origins[0]
230
+ remote_url = (spec.remotes or {}).get(base_origin.remote) or base_origin.remote
231
+
232
+ if not module_path.exists() or not module_path.is_dir():
233
+ progress.update(task_id, status=f"Cloning {base_origin.origin}...")
234
+
235
+ # Clone shallowly with blobless filter and no checkout
236
+ # We don't use the cache yet for simplicity, but we follow the optimized command
237
+ # User --revision for specific commit checkout if needed
238
+ if name == "odoo":
239
+ ret, out, err = await self.run_git(
240
+ "clone",
241
+ "--filter=blob:none",
242
+ "--depth",
243
+ "1",
244
+ "--origin",
245
+ base_origin.remote,
246
+ "--single-branch",
247
+ "--branch",
248
+ base_origin.origin,
249
+ remote_url,
250
+ module_path,
251
+ )
252
+ else:
253
+ args = create_clone_args(base_origin, remote_url)
254
+
255
+ ret, out, err = await self.run_git(
256
+ *args,
257
+ str(module_path),
258
+ )
259
+
260
+ for name, url in (spec.remotes or {}).items():
261
+ if name != "origin":
262
+ await self.run_git("remote", "add", name, url, cwd=module_path)
263
+ await self.run_git("config", f"remote.{name}.promisor", "true", cwd=module_path)
264
+ await self.run_git(
265
+ "config", f"remote.{name}.partialclonefilter", "blob:none", cwd=module_path
266
+ )
267
+
268
+ if ret != 0:
269
+ progress.update(task_id, status=f"[red]Clone failed why cloning base branch: {err}")
270
+ return ret
271
+ else:
272
+ ret, out, err = await self.run_git("status", "--porcelain", cwd=module_path)
273
+
274
+ if out != "":
275
+ progress.update(task_id, status=f"[red]Repo is dirty:\n{out}")
276
+ return ret
277
+ # Reset all the local origin to their remote origins
278
+ progress.update(
279
+ task_id,
280
+ status=(
281
+ f"Resetting existing repository for {base_origin.origin}"
282
+ + " to {base_origin.remote}/{base_origin.origin}..."
283
+ ),
284
+ )
285
+ ret, out, err = await self.run_git(
286
+ "reset",
287
+ "--hard",
288
+ f"{base_origin.remote}/{base_origin.origin}",
289
+ cwd=module_path,
290
+ )
291
+ if ret != 0:
292
+ progress.update(task_id, status=f"[red]Reset failed: {err}")
293
+ return ret
294
+
295
+ for origin in spec.origins[1:]:
296
+ local_ref = _get_local_ref(origin)
297
+ # This is probably the best thing but for now this works good enough
298
+ # TODO(franz): find something better
299
+ ret, out, err = await self.run_git("branch", "-d", local_ref, cwd=module_path)
300
+
301
+ if name != "odoo":
302
+ # We don't do sparse checkout for odoo because the odoo repo does not work at
303
+ # all like the other repos (modules are in addons/ and src/addons/) instead of
304
+ # at the root of the repo
305
+
306
+ # TODO(franz): there is probably a way to make it work, but for now we skip it
307
+ # this is probably a good way to gain performance
308
+
309
+ # 2. Sparse Checkout setup
310
+ progress.update(task_id, status="Configuring sparse checkout...")
311
+ await self.run_git("sparse-checkout", "init", "--cone", cwd=module_path)
312
+ if spec.modules:
313
+ await self.run_git("sparse-checkout", "set", *spec.modules, cwd=module_path)
314
+
315
+ # 3. Checkout base
316
+ await self.run_git("checkout", base_origin.origin, cwd=module_path)
317
+ progress.advance(task_id)
318
+
319
+ # 4. Fetch and Merge remaining origins
320
+ for origin in spec.origins[1:]:
321
+ progress.update(
322
+ task_id,
323
+ status=(
324
+ f"Merging {origin.remote}/{origin.origin}"
325
+ + f" into {base_origin.remote}/{base_origin.origin}..."
326
+ ),
327
+ advance=0.1,
328
+ )
329
+
330
+ remote_url = (spec.remotes or {}).get(origin.remote) or origin.remote
331
+
332
+ local_ref = _get_local_ref(origin)
333
+
334
+ ret, out, err = await self.run_git(
335
+ "fetch", "--depth", "1", remote_url, f"{origin.origin}:{local_ref}", cwd=module_path
336
+ )
337
+
338
+ if ret != 0:
339
+ # Should not necessarily crash
340
+ progress.update(task_id, status=f"[red]Fetch failed for {origin.origin}")
341
+ return ret
342
+
343
+ # Try to merge
344
+ ret, out, err = await self.try_merge(
345
+ progress, task_id, remote_url, local_ref, module_path, origin, base_origin
346
+ )
347
+ if ret != 0:
348
+ return ret
349
+
350
+ progress.advance(task_id)
351
+
352
+ if spec.shell_commands:
353
+ ret = await self.run_shell_commands(progress, task_id, spec, module_path)
354
+ if ret != 0:
355
+ return ret
356
+
357
+ if spec.patch_globs_to_apply:
358
+ for glob in spec.patch_globs_to_apply:
359
+ progress.update(task_id, status=f"Applying patches: {glob}...", advance=0.1)
360
+ ret, out, err = await self.run_git("am", glob, cwd=module_path)
361
+ if ret != 0:
362
+ await self.run_git("am", "--abort", cwd=module_path)
363
+ progress.update(task_id, status=f"[red]Applying patches failed: {err}")
364
+ return ret
365
+
366
+ progress.update(task_id, status="[green]Complete")
367
+ progress.remove_task(task_id)
368
+ count_progress.advance(count_task)
369
+
370
+ except Exception as e:
371
+ progress.update(task_id, status=f"[red]Error: {str(e)}")
372
+
373
+ async def process_project(self, project_spec: ProjectSpec) -> None:
374
+ """Processes all modules in a ProjectSpec."""
375
+ self.modules_dir.mkdir(parents=True, exist_ok=True)
376
+
377
+ task_list_progress = Progress(
378
+ SpinnerColumn(),
379
+ TextColumn("[progress.description]{task.description}"),
380
+ BarColumn(),
381
+ TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
382
+ TextColumn("{task.fields[status]}"),
383
+ console=console,
384
+ refresh_per_second=10,
385
+ )
386
+ task_count_progress = Progress(
387
+ MofNCompleteColumn(),
388
+ console=console,
389
+ refresh_per_second=10,
390
+ )
391
+ task_list_progress.start()
392
+ task_count_progress.start()
393
+ count_task = task_count_progress.add_task("Processing Modules", total=len(project_spec.specs))
394
+ try:
395
+ tasks = []
396
+ for name, spec in project_spec.specs.items():
397
+ tasks.append(
398
+ self.process_module(
399
+ name,
400
+ spec,
401
+ task_list_progress,
402
+ task_count_progress,
403
+ count_task,
404
+ )
405
+ )
406
+
407
+ await asyncio.gather(*tasks)
408
+ finally:
409
+ task_list_progress.stop()
410
+ task_count_progress.stop()
411
+
412
+
413
+ async def process_project(project_spec: ProjectSpec, workdir: Path, concurrency: int = 4) -> None:
414
+ """Helper function to run the SpecProcessor."""
415
+ processor = SpecProcessor(workdir, concurrency)
416
+ # project_spec.specs = {name: spec for name, spec in project_spec.specs.items() if name == "sale-workflow"}
417
+ await processor.process_project(project_spec)
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.4
2
+ Name: bl-odoo
3
+ Version: 0.1.1
4
+ Summary: A command-line tool for managing Odoo dependencies.
5
+ Author-email: Your Name <your.email@example.com>
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: pyyaml>=6.0.3
11
+ Requires-Dist: rich
12
+ Requires-Dist: typer
13
+ Provides-Extra: dev
14
+ Requires-Dist: ruff; extra == "dev"
15
+ Requires-Dist: kalong; extra == "dev"
16
+ Dynamic: license-file
17
+
18
+ # bl
19
+
20
+ A new Python project.
@@ -0,0 +1,13 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ bl/__init__.py
5
+ bl/__main__.py
6
+ bl/spec_parser.py
7
+ bl/spec_processor.py
8
+ bl_odoo.egg-info/PKG-INFO
9
+ bl_odoo.egg-info/SOURCES.txt
10
+ bl_odoo.egg-info/dependency_links.txt
11
+ bl_odoo.egg-info/entry_points.txt
12
+ bl_odoo.egg-info/requires.txt
13
+ bl_odoo.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ bl = bl.__main__:run
@@ -0,0 +1,7 @@
1
+ pyyaml>=6.0.3
2
+ rich
3
+ typer
4
+
5
+ [dev]
6
+ ruff
7
+ kalong
@@ -0,0 +1 @@
1
+ bl
@@ -0,0 +1,44 @@
1
+ [project]
2
+ name = "bl-odoo"
3
+ version = "0.1.1"
4
+ description = "A command-line tool for managing Odoo dependencies."
5
+ authors = [
6
+ { name="Your Name", email="your.email@example.com" },
7
+ ]
8
+ dependencies = [
9
+ "pyyaml>=6.0.3",
10
+ "rich",
11
+ "typer",
12
+ ]
13
+ requires-python = ">=3.9"
14
+ readme = "README.md"
15
+ license = "MIT"
16
+ license-files = ["LICENSE"]
17
+
18
+ [project.optional-dependencies]
19
+ dev = [
20
+ "ruff",
21
+ "kalong",
22
+ ]
23
+
24
+ [project.scripts]
25
+ bl = "bl.__main__:run"
26
+
27
+ [build-system]
28
+ requires = ["setuptools>=61.0"]
29
+ build-backend = "setuptools.build_meta"
30
+
31
+ [tool.setuptools]
32
+ packages = ["bl"]
33
+
34
+ [tool.setuptools.package-data]
35
+ "bl" = ["py.typed"]
36
+
37
+ [tool.ruff]
38
+ line-length = 120
39
+
40
+ [tool.ruff.lint]
41
+ select = ["E", "F", "I"]
42
+
43
+ [tool.mypy]
44
+ strict = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+