Repository navigation
feat/0.9.0: propose command + web API with automatic metadata extraction #74
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -730,6 +730,158 @@ def export_command(packages, manager, solver, conflicts, format, output_file): | |
| click.echo(content) | ||
|
|
||
|
|
||
| @cli.command(name="propose") | ||
| @click.argument("packages", nargs=-1) | ||
| @click.option( | ||
| "--manager", | ||
| "-m", | ||
| type=str, | ||
| default=None, | ||
| help="Тип пакетного менеджера (apt, pip, pacman)", | ||
| ) | ||
| @click.option( | ||
| "--solver", | ||
| "-s", | ||
| type=str, | ||
| default=None, | ||
| help="Тип солвера (greedy, z3, pulp, ortools, maxsat, minisat, enhanced_greedy)", | ||
| ) | ||
| @click.option( | ||
| "--output", | ||
| "-o", | ||
| type=click.Choice(["text", "json"]), | ||
| default="text", | ||
| help="Формат вывода", | ||
| ) | ||
| @click.option( | ||
| "--explain", | ||
| "-e", | ||
| is_flag=True, | ||
| default=False, | ||
| help="Показать причины отбора/отклонения пакетов", | ||
| ) | ||
| def propose(packages, manager, solver, output, explain): | ||
| """ | ||
| Предложить оптимальный набор пакетов с автоматическим извлечением метаданных. | ||
|
|
||
| Автоматически извлекает depends/conflicts из репозитория и выбирает | ||
| максимальное совместное множество. | ||
|
|
||
| Примеры: | ||
|
|
||
| package-maximizer propose nginx apache2 php-fpm --manager apt --explain | ||
|
|
||
| package-maximizer propose requests urllib3 --manager pip --solver z3 | ||
| """ | ||
| obj = click.get_current_context().obj or {} | ||
| cfg = obj.get("config") | ||
| if manager is None: | ||
| manager = cfg.default_manager if cfg else "apt" | ||
| if solver is None: | ||
| solver = cfg.default_solver if cfg else "greedy" | ||
|
|
||
| try: | ||
| try: | ||
| manager_enum = PackageManagerType(manager) | ||
| except ValueError: | ||
| click.echo(f"Ошибка: Неизвестный пакетный менеджер '{manager}'", err=True) | ||
| sys.exit(1) | ||
|
|
||
| from ..adapters import get_adapter | ||
|
|
||
| try: | ||
| adapter = get_adapter(manager) | ||
| except ValueError as e: | ||
| click.echo(f"Ошибка: {e}", err=True) | ||
| sys.exit(1) | ||
|
|
||
| click.echo(f"Извлечение метаданных для {len(packages)} пакетов ({manager})...") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Keep JSON stdout machine-readable. When Write diagnostics to stderr, or suppress them in JSON mode. Add a test that calls Also applies to: 806-809, 813-818 🤖 Prompt for AI Agents |
||
|
|
||
| package_objs = [] | ||
| not_found = [] | ||
| for pkg_name in packages: | ||
| metadata = adapter.fetch(pkg_name) | ||
| if metadata and metadata.name: | ||
| package_objs.append(metadata.to_package()) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift Resolve dependencies that are outside the requested package list. Both flows pass only directly requested packages to the solver.
📍 Affects 2 files
🤖 Prompt for AI Agents |
||
| click.echo( | ||
| f" ✓ {pkg_name}: {len(metadata.depends)} зависимостей, " | ||
| f"{len(metadata.conflicts)} конфликтов" | ||
| ) | ||
| else: | ||
| not_found.append(pkg_name) | ||
| package_objs.append(Package(name=pkg_name, status="candidate")) | ||
| click.echo(f" ⚠ {pkg_name}: метаданные не найдены") | ||
|
|
||
| if not_found: | ||
| click.echo( | ||
| f"\nПредупреждение: метаданные не найдены для: {', '.join(not_found)}" | ||
| ) | ||
|
|
||
| if not package_objs: | ||
| click.echo("Ошибка: нет пакетов для обработки", err=True) | ||
| sys.exit(1) | ||
|
|
||
| try: | ||
| maximizer = PackageMaximizer(manager=manager_enum, solver=solver) | ||
| except ValueError: | ||
| click.echo(f"Ошибка: Неизвестный солвер '{solver}'", err=True) | ||
| sys.exit(1) | ||
|
|
||
| result = maximizer.solve(package_objs) | ||
|
|
||
| if output == "json": | ||
| output_data = { | ||
| "manager": manager, | ||
| "solver": solver, | ||
| "input": [p.name for p in package_objs], | ||
| "output": result, | ||
| "count": len(result), | ||
| "metadata_fetched": len(packages) - len(not_found), | ||
| } | ||
| click.echo(json.dumps(output_data, indent=2)) | ||
| else: | ||
| click.echo(f"\nМенеджер: {manager}") | ||
| click.echo(f"Солвер: {solver}") | ||
| click.echo(f"Входные пакеты: {len(package_objs)}") | ||
| click.echo(f"Выбранные пакеты: {len(result)}") | ||
| click.echo(f"Результат: {', '.join(result)}") | ||
|
|
||
| if explain: | ||
| selected_set = set(result) | ||
| all_names = {p.name for p in package_objs} | ||
| excluded = all_names - selected_set | ||
|
|
||
| if excluded: | ||
| click.echo(f"\nПричины отклонения:") | ||
| from ..core.model_encoder import encode_packages | ||
|
|
||
| constraints = encode_packages(package_objs) | ||
|
|
||
| for name in sorted(excluded): | ||
| reasons = [] | ||
| for a, b in constraints.conflicts: | ||
| if a == name and b in selected_set: | ||
| reasons.append(f"конфликт с {b}") | ||
| elif b == name and a in selected_set: | ||
| reasons.append(f"конфликт с {a}") | ||
|
|
||
| deps = constraints.dependencies.get(name, []) | ||
| for dep in deps: | ||
| if dep not in selected_set: | ||
| reasons.append(f"не выбрана зависимость {dep}") | ||
|
|
||
| if reasons: | ||
| click.echo(f" - {name}: {'; '.join(reasons)}") | ||
| else: | ||
| click.echo(f" - {name}: не оптимально для данного солвера") | ||
| else: | ||
| click.echo(f"\nВсе пакеты выбраны!") | ||
| except Exception as e: | ||
| logger.error(f"Ошибка: {e}", exc_info=True) | ||
| click.echo(f"Ошибка: {e}", err=True) | ||
| sys.exit(1) | ||
|
|
||
|
|
||
| @cli.command(name="tui") | ||
| def tui_command() -> None: | ||
| """ | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
🔎 Supported by static analysis
🏁 Script executed:
Repository: dominicusin/package-maximizer
Length of output: 23736
🏁 Script executed:
Repository: dominicusin/package-maximizer
Length of output: 18455
🌐 Web query:
site:pip.pypa.io/en/stable/cli/pip_show/ pip show installed distributions metadata💡 Result:
The pip show command is used to display information about one or more installed Python packages [1]. The output is formatted as RFC-compliant mail headers [1]. By default, the command displays the following metadata for each package [1]: - Name: The name of the package. - Version: The installed version of the package. - Summary: A brief description of the package. - Home-page: The URL for the package homepage. - Author: The author of the package. - Author-email: The contact email for the author. - License: The license under which the package is distributed. - Location: The file path where the package is installed. - Requires: A list of dependencies the package relies on. When using the --verbose flag, additional metadata is included, such as [1]: - Metadata-Version: The version of the package metadata format. - Installer: The tool used to install the package. - Classifiers: A list of Trove classifiers categorized by the package. Additionally, the --files flag can be used to include a full list of all files installed by the package [1]. The command can be executed using the syntax python -m pip show [options] [1].
Citations:
Use an index-aware metadata source for package proposals.
PipMetadataAdapter.fetchusespip show, which reads metadata only for installed packages. If a package is available in the index but is not installed locally, the CLI and web proposal flows treat the missing result as metadata not found and omit its dependencies and conflicts. Use an index-aware resolver or metadata client, and add a test for this case.🧰 Tools
🪛 ast-grep (0.45.2)
[error] 178-183: Command coming from incoming request
Context: subprocess.run(
["pip", "show", package_name],
capture_output=True,
text=True,
timeout=10,
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(subprocess-from-request)
🪛 Ruff (0.16.3)
[error] 180-180: Starting a process with a partial executable path
(S607)
🤖 Prompt for AI Agents