Added venv support to install command
This commit is contained in:
		| @@ -1,5 +1,6 @@ | ||||
| import json | ||||
| import os | ||||
| import shutil | ||||
| import subprocess | ||||
| import textwrap | ||||
| import time | ||||
| @@ -9,12 +10,12 @@ from cpl_cli.command_abc import CommandABC | ||||
| from cpl_cli.configuration.build_settings import BuildSettings | ||||
| from cpl_cli.configuration.project_settings import ProjectSettings | ||||
| from cpl_cli.configuration.settings_helper import SettingsHelper | ||||
| from cpl_cli.configuration.venv_helper_service import VenvHelper | ||||
| from cpl_cli.error import Error | ||||
| from cpl_core.configuration.configuration_abc import ConfigurationABC | ||||
| from cpl_core.console.console import Console | ||||
| from cpl_core.console.foreground_color_enum import ForegroundColorEnum | ||||
| from cpl_core.environment.application_environment_abc import \ | ||||
|     ApplicationEnvironmentABC | ||||
| from cpl_core.environment.application_environment_abc import ApplicationEnvironmentABC | ||||
| from cpl_core.utils.pip import Pip | ||||
| from packaging import version | ||||
|  | ||||
| @@ -57,12 +58,27 @@ class InstallService(CommandABC): | ||||
|     def _wait(self, t: int, *args, source: str = None, stdout=None, stderr=None): | ||||
|         time.sleep(t) | ||||
|  | ||||
|     def _init_venv(self): | ||||
|         if self._is_virtual: | ||||
|             return | ||||
|         venv_path = os.path.abspath(os.path.join(self._env.working_directory, self._project_settings.python_executable)) | ||||
|  | ||||
|         if not os.path.exists(venv_path): | ||||
|             Console.spinner( | ||||
|                 f'Creating venv {venv_path}', | ||||
|                 VenvHelper.create_venv, | ||||
|                 venv_path, | ||||
|                 text_foreground_color=ForegroundColorEnum.green, | ||||
|                 spinner_foreground_color=ForegroundColorEnum.cyan | ||||
|             ) | ||||
|  | ||||
|         Pip.set_executable(venv_path) | ||||
|  | ||||
|     def _install_project(self): | ||||
|         """ | ||||
|         Installs dependencies of CPl project | ||||
|         :return: | ||||
|         """ | ||||
|  | ||||
|         if self._project_settings is None or self._build_settings is None: | ||||
|             Error.error('The command requires to be run in an CPL project, but a project could not be found.') | ||||
|             return | ||||
| @@ -71,8 +87,6 @@ class InstallService(CommandABC): | ||||
|             Error.error(f'Found invalid dependencies in {self._project_file}.') | ||||
|             return | ||||
|  | ||||
|         if not self._is_virtual: | ||||
|             Pip.set_executable(self._project_settings.python_executable) | ||||
|         for dependency in self._project_settings.dependencies: | ||||
|             Console.spinner( | ||||
|                 f'Installing: {dependency}', | ||||
| @@ -94,9 +108,6 @@ class InstallService(CommandABC): | ||||
|         :return: | ||||
|         """ | ||||
|         is_already_in_project = False | ||||
|         if not self._is_virtual: | ||||
|             Pip.set_executable(self._project_settings.python_executable) | ||||
|  | ||||
|         if self._project_settings is None or self._build_settings is None: | ||||
|             Error.error('The command requires to be run in an CPL project, but a project could not be found.') | ||||
|             return | ||||
| @@ -200,7 +211,11 @@ class InstallService(CommandABC): | ||||
|             args.remove('simulate') | ||||
|             Console.write_line('Running in simulation mode:') | ||||
|  | ||||
|         self._init_venv() | ||||
|  | ||||
|         if len(args) == 0: | ||||
|             self._install_project() | ||||
|         else: | ||||
|             self._install_package(args[0]) | ||||
|  | ||||
|         # shutil.rmtree(os.path.abspath(os.path.join(self._env.working_directory, self._project_settings.python_executable))) | ||||
|   | ||||
| @@ -3,12 +3,12 @@ import sys | ||||
| import traceback | ||||
| from typing import Optional | ||||
|  | ||||
| from cpl_cli.configuration.project_settings_name_enum import ProjectSettingsNameEnum | ||||
| from cpl_cli.configuration.version_settings import VersionSettings | ||||
| from cpl_cli.error import Error | ||||
| from cpl_core.configuration.configuration_model_abc import ConfigurationModelABC | ||||
| from cpl_core.console.console import Console | ||||
| from cpl_core.console.foreground_color_enum import ForegroundColorEnum | ||||
| from cpl_cli.configuration.version_settings import VersionSettings | ||||
| from cpl_cli.configuration.project_settings_name_enum import ProjectSettingsNameEnum | ||||
| from cpl_cli.error import Error | ||||
|  | ||||
|  | ||||
| class ProjectSettings(ConfigurationModelABC): | ||||
| @@ -114,13 +114,10 @@ class ProjectSettings(ConfigurationModelABC): | ||||
|             self._python_version = settings[ProjectSettingsNameEnum.python_version.value] | ||||
|             self._python_path = settings[ProjectSettingsNameEnum.python_path.value] | ||||
|  | ||||
|             if ProjectSettingsNameEnum.python_path.value in settings and \ | ||||
|                     sys.platform in settings[ProjectSettingsNameEnum.python_path.value]: | ||||
|             if ProjectSettingsNameEnum.python_path.value in settings and sys.platform in settings[ProjectSettingsNameEnum.python_path.value]: | ||||
|                 path = settings[ProjectSettingsNameEnum.python_path.value][sys.platform] | ||||
|                 if not os.path.isfile(path) and not os.path.islink(path): | ||||
|                     if path != '' and path is not None: | ||||
|                         Error.warn(f'{ProjectSettingsNameEnum.python_path.value} not found') | ||||
|  | ||||
|                 if path == '' or path is None: | ||||
|                     Error.warn(f'{ProjectSettingsNameEnum.python_path.value} not found') | ||||
|                     path = sys.executable | ||||
|             else: | ||||
|                 path = sys.executable | ||||
| @@ -134,7 +131,6 @@ class ProjectSettings(ConfigurationModelABC): | ||||
|  | ||||
|         except Exception as e: | ||||
|             Console.set_foreground_color(ForegroundColorEnum.red) | ||||
|             Console.write_line( | ||||
|                 f'[ ERROR ] [ {__name__} ]: Reading error in {ProjectSettings.__name__} settings') | ||||
|             Console.write_line(f'[ ERROR ] [ {__name__} ]: Reading error in {ProjectSettings.__name__} settings') | ||||
|             Console.write_line(f'[ EXCEPTION ] [ {__name__} ]: {e} -> {traceback.format_exc()}') | ||||
|             Console.set_foreground_color(ForegroundColorEnum.default) | ||||
|   | ||||
							
								
								
									
										15
									
								
								src/cpl_cli/configuration/venv_helper_service.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/cpl_cli/configuration/venv_helper_service.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| import os | ||||
| import subprocess | ||||
| import sys | ||||
|  | ||||
|  | ||||
| class VenvHelper: | ||||
|  | ||||
|     @staticmethod | ||||
|     def create_venv(path): | ||||
|         subprocess.run( | ||||
|             [sys.executable, '-m', 'venv', os.path.abspath(os.path.join(path, '../../'))], | ||||
|             stdout=subprocess.DEVNULL, | ||||
|             stderr=subprocess.DEVNULL, | ||||
|             stdin=subprocess.DEVNULL | ||||
|         ) | ||||
| @@ -19,7 +19,9 @@ | ||||
|       "cpl-core>=2022.6.17.dev8" | ||||
|     ], | ||||
|     "PythonVersion": ">=3.10", | ||||
|     "PythonPath": {}, | ||||
|     "PythonPath": { | ||||
|       "linux": "../../venv" | ||||
|     }, | ||||
|     "Classifiers": [] | ||||
|   }, | ||||
|   "BuildSettings": { | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import pkg_resources | ||||
| from cpl_cli.cli import CLI | ||||
| from cpl_cli.startup import Startup | ||||
| from cpl_cli.startup_argument_extension import StartupArgumentExtension | ||||
| from cpl_cli.startup_workspace_extension import StartupWorkspaceExtension | ||||
| from cpl_core.application.application_builder import ApplicationBuilder | ||||
| from cpl_core.application.startup_extension_abc import StartupExtensionABC | ||||
|  | ||||
| @@ -31,6 +32,7 @@ def get_startup_extensions() -> list[Type[StartupExtensionABC]]: | ||||
| def main(): | ||||
|     app_builder = ApplicationBuilder(CLI) | ||||
|     app_builder.use_startup(Startup) | ||||
|     app_builder.use_extension(StartupWorkspaceExtension) | ||||
|     app_builder.use_extension(StartupArgumentExtension) | ||||
|     for extension in get_startup_extensions(): | ||||
|         app_builder.use_extension(extension) | ||||
|   | ||||
| @@ -1,5 +1,23 @@ | ||||
| import os | ||||
|  | ||||
| from cpl_cli.command.add_service import AddService | ||||
| from cpl_cli.command.build_service import BuildService | ||||
| from cpl_cli.command.custom_script_service import CustomScriptService | ||||
| from cpl_cli.command.generate_service import GenerateService | ||||
| from cpl_cli.command.help_service import HelpService | ||||
| from cpl_cli.command.install_service import InstallService | ||||
| from cpl_cli.command.new_service import NewService | ||||
| from cpl_cli.command.publish_service import PublishService | ||||
| from cpl_cli.command.remove_service import RemoveService | ||||
| from cpl_cli.command.run_service import RunService | ||||
| from cpl_cli.command.start_service import StartService | ||||
| from cpl_cli.command.uninstall_service import UninstallService | ||||
| from cpl_cli.command.update_service import UpdateService | ||||
| from cpl_cli.command.version_service import VersionService | ||||
| from cpl_cli.validators.project_validator import ProjectValidator | ||||
|  | ||||
| from cpl_cli.validators.workspace_validator import WorkspaceValidator | ||||
|  | ||||
| from cpl_core.console import Console | ||||
|  | ||||
| from cpl_cli.error import Error | ||||
| @@ -37,4 +55,22 @@ class Startup(StartupABC): | ||||
|         services.add_transient(PublisherABC, PublisherService) | ||||
|         services.add_transient(LiveServerService) | ||||
|  | ||||
|         services.add_transient(WorkspaceValidator) | ||||
|         services.add_transient(ProjectValidator) | ||||
|  | ||||
|         services.add_transient(AddService) | ||||
|         services.add_transient(BuildService) | ||||
|         services.add_transient(CustomScriptService) | ||||
|         services.add_transient(GenerateService) | ||||
|         services.add_transient(HelpService) | ||||
|         services.add_transient(InstallService) | ||||
|         services.add_transient(NewService) | ||||
|         services.add_transient(PublishService) | ||||
|         services.add_transient(RemoveService) | ||||
|         services.add_transient(RunService) | ||||
|         services.add_transient(StartService) | ||||
|         services.add_transient(UninstallService) | ||||
|         services.add_transient(UpdateService) | ||||
|         services.add_transient(VersionService) | ||||
|  | ||||
|         return services.build_service_provider() | ||||
|   | ||||
| @@ -1,11 +1,5 @@ | ||||
| import os | ||||
| from typing import Optional | ||||
|  | ||||
| from cpl_core.console import Console | ||||
|  | ||||
| from cpl_cli.command.add_service import AddService | ||||
| from cpl_cli.command.build_service import BuildService | ||||
| from cpl_cli.command.custom_script_service import CustomScriptService | ||||
| from cpl_cli.command.generate_service import GenerateService | ||||
| from cpl_cli.command.help_service import HelpService | ||||
| from cpl_cli.command.install_service import InstallService | ||||
| @@ -17,7 +11,6 @@ from cpl_cli.command.start_service import StartService | ||||
| from cpl_cli.command.uninstall_service import UninstallService | ||||
| from cpl_cli.command.update_service import UpdateService | ||||
| from cpl_cli.command.version_service import VersionService | ||||
| from cpl_cli.configuration.workspace_settings import WorkspaceSettings | ||||
| from cpl_cli.validators.project_validator import ProjectValidator | ||||
| from cpl_cli.validators.workspace_validator import WorkspaceValidator | ||||
| from cpl_core.application.startup_extension_abc import StartupExtensionABC | ||||
| @@ -25,7 +18,6 @@ from cpl_core.configuration.argument_type_enum import ArgumentTypeEnum | ||||
| from cpl_core.configuration.configuration_abc import ConfigurationABC | ||||
| from cpl_core.dependency_injection.service_collection_abc import ServiceCollectionABC | ||||
| from cpl_core.environment.application_environment_abc import ApplicationEnvironmentABC | ||||
| from cpl_core.utils.string import String | ||||
|  | ||||
|  | ||||
| class StartupArgumentExtension(StartupExtensionABC): | ||||
| @@ -33,40 +25,7 @@ class StartupArgumentExtension(StartupExtensionABC): | ||||
|     def __init__(self): | ||||
|         pass | ||||
|  | ||||
|     @staticmethod | ||||
|     def _search_project_json(working_directory: str) -> Optional[str]: | ||||
|         project_name = None | ||||
|         name = os.path.basename(working_directory) | ||||
|         for r, d, f in os.walk(working_directory): | ||||
|             for file in f: | ||||
|                 if file.endswith('.json'): | ||||
|                     f_name = file.split('.json')[0] | ||||
|                     if f_name == name or String.convert_to_camel_case(f_name).lower() == String.convert_to_camel_case(name).lower(): | ||||
|                         project_name = f_name | ||||
|                         break | ||||
|  | ||||
|         return project_name | ||||
|  | ||||
|     def _read_cpl_environment(self, config: ConfigurationABC, env: ApplicationEnvironmentABC): | ||||
|         workspace: Optional[WorkspaceSettings] = config.get_configuration(WorkspaceSettings) | ||||
|         config.add_configuration('PATH_WORKSPACE', env.working_directory) | ||||
|         if workspace is not None: | ||||
|             for script in workspace.scripts: | ||||
|                 config.create_console_argument(ArgumentTypeEnum.Executable, '', script, [], CustomScriptService) | ||||
|             return | ||||
|  | ||||
|         project = self._search_project_json(env.working_directory) | ||||
|         if project is not None: | ||||
|             project = f'{project}.json' | ||||
|  | ||||
|         if project is None: | ||||
|             return | ||||
|  | ||||
|         config.add_json_file(project, optional=True, output=False) | ||||
|  | ||||
|     def configure_configuration(self, config: ConfigurationABC, env: ApplicationEnvironmentABC): | ||||
|         config.add_json_file('cpl-workspace.json', path=env.working_directory, optional=True, output=False) | ||||
|  | ||||
|         config.create_console_argument(ArgumentTypeEnum.Executable, '', 'add', ['a', 'A'], AddService, True, validators=[WorkspaceValidator]) \ | ||||
|             .add_console_argument(ArgumentTypeEnum.Flag, '--', 'simulate', ['s', 'S']) | ||||
|         config.create_console_argument(ArgumentTypeEnum.Executable, '', 'build', ['b', 'B'], BuildService, True, validators=[ProjectValidator]) | ||||
| @@ -107,23 +66,5 @@ class StartupArgumentExtension(StartupExtensionABC): | ||||
|         config.for_each_argument(lambda a: a.add_console_argument(ArgumentTypeEnum.Flag, '--', 'help', ['h', 'H'])) | ||||
|         config.create_console_argument(ArgumentTypeEnum.Executable, '', 'help', ['h', 'H'], HelpService) | ||||
|  | ||||
|         self._read_cpl_environment(config, env) | ||||
|  | ||||
|     def configure_services(self, services: ServiceCollectionABC, env: ApplicationEnvironmentABC): | ||||
|         services.add_transient(WorkspaceValidator) | ||||
|         services.add_transient(ProjectValidator) | ||||
|  | ||||
|         services.add_transient(AddService) | ||||
|         services.add_transient(BuildService) | ||||
|         services.add_transient(CustomScriptService) | ||||
|         services.add_transient(GenerateService) | ||||
|         services.add_transient(HelpService) | ||||
|         services.add_transient(InstallService) | ||||
|         services.add_transient(NewService) | ||||
|         services.add_transient(PublishService) | ||||
|         services.add_transient(RemoveService) | ||||
|         services.add_transient(RunService) | ||||
|         services.add_transient(StartService) | ||||
|         services.add_transient(UninstallService) | ||||
|         services.add_transient(UpdateService) | ||||
|         services.add_transient(VersionService) | ||||
|         pass | ||||
|   | ||||
							
								
								
									
										55
									
								
								src/cpl_cli/startup_workspace_extension.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								src/cpl_cli/startup_workspace_extension.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | ||||
| import os | ||||
| from typing import Optional | ||||
|  | ||||
| from cpl_cli.command.custom_script_service import CustomScriptService | ||||
| from cpl_cli.configuration.workspace_settings import WorkspaceSettings | ||||
| from cpl_core.application.startup_extension_abc import StartupExtensionABC | ||||
| from cpl_core.configuration.argument_type_enum import ArgumentTypeEnum | ||||
| from cpl_core.configuration.configuration_abc import ConfigurationABC | ||||
| from cpl_core.dependency_injection.service_collection_abc import ServiceCollectionABC | ||||
| from cpl_core.environment.application_environment_abc import ApplicationEnvironmentABC | ||||
| from cpl_core.utils.string import String | ||||
|  | ||||
|  | ||||
| class StartupWorkspaceExtension(StartupExtensionABC): | ||||
|  | ||||
|     def __init__(self): | ||||
|         pass | ||||
|  | ||||
|     @staticmethod | ||||
|     def _search_project_json(working_directory: str) -> Optional[str]: | ||||
|         project_name = None | ||||
|         name = os.path.basename(working_directory) | ||||
|         for r, d, f in os.walk(working_directory): | ||||
|             for file in f: | ||||
|                 if file.endswith('.json'): | ||||
|                     f_name = file.split('.json')[0] | ||||
|                     if f_name == name or String.convert_to_camel_case(f_name).lower() == String.convert_to_camel_case(name).lower(): | ||||
|                         project_name = f_name | ||||
|                         break | ||||
|  | ||||
|         return project_name | ||||
|  | ||||
|     def _read_cpl_environment(self, config: ConfigurationABC, env: ApplicationEnvironmentABC): | ||||
|         workspace: Optional[WorkspaceSettings] = config.get_configuration(WorkspaceSettings) | ||||
|         config.add_configuration('PATH_WORKSPACE', env.working_directory) | ||||
|         if workspace is not None: | ||||
|             for script in workspace.scripts: | ||||
|                 config.create_console_argument(ArgumentTypeEnum.Executable, '', script, [], CustomScriptService) | ||||
|             return | ||||
|  | ||||
|         project = self._search_project_json(env.working_directory) | ||||
|         if project is not None: | ||||
|             project = f'{project}.json' | ||||
|  | ||||
|         if project is None: | ||||
|             return | ||||
|  | ||||
|         config.add_json_file(project, optional=True, output=False) | ||||
|  | ||||
|     def configure_configuration(self, config: ConfigurationABC, env: ApplicationEnvironmentABC): | ||||
|         config.add_json_file('cpl-workspace.json', path=env.working_directory, optional=True, output=False) | ||||
|         self._read_cpl_environment(config, env) | ||||
|  | ||||
|     def configure_services(self, services: ServiceCollectionABC, env: ApplicationEnvironmentABC): | ||||
|         pass | ||||
| @@ -4,19 +4,22 @@ import sys | ||||
| from contextlib import suppress | ||||
| from typing import Optional | ||||
|  | ||||
| from cpl_core.console import Console | ||||
|  | ||||
|  | ||||
| class Pip: | ||||
|     r"""Executes pip commands""" | ||||
|     _executable = sys.executable | ||||
|     _env = os.environ | ||||
|     _is_venv = False | ||||
|  | ||||
|     """Getter""" | ||||
|  | ||||
|     @classmethod | ||||
|     def get_executable(cls) -> str: | ||||
|         return cls._executable | ||||
|  | ||||
|     """Setter""" | ||||
|  | ||||
|     @classmethod | ||||
|     def set_executable(cls, executable: str): | ||||
|         r"""Sets the executable | ||||
| @@ -26,25 +29,28 @@ class Pip: | ||||
|             executable: :class:`str` | ||||
|                 The python command | ||||
|         """ | ||||
|         if executable is not None and executable != sys.executable: | ||||
|             cls._executable = executable | ||||
|             if os.path.islink(cls._executable): | ||||
|                 cls._is_venv = True | ||||
|                 path = os.path.dirname(os.path.dirname(cls._executable)) | ||||
|                 cls._env = os.environ | ||||
|                 if sys.platform == 'win32': | ||||
|                     cls._env['PATH'] = f'{path}\\bin' + os.pathsep + os.environ.get('PATH', '') | ||||
|                 else: | ||||
|                     cls._env['PATH'] = f'{path}/bin' + os.pathsep + os.environ.get('PATH', '') | ||||
|                 cls._env['VIRTUAL_ENV'] = path | ||||
|         if executable is None or executable == sys.executable: | ||||
|             return | ||||
|  | ||||
|         cls._executable = executable | ||||
|         if not os.path.islink(cls._executable): | ||||
|             return | ||||
|  | ||||
|         path = os.path.dirname(os.path.dirname(cls._executable)) | ||||
|         cls._env = os.environ | ||||
|         if sys.platform == 'win32': | ||||
|             cls._env['PATH'] = f'{path}\\bin' + os.pathsep + os.environ.get('PATH', '') | ||||
|         else: | ||||
|             cls._env['PATH'] = f'{path}/bin' + os.pathsep + os.environ.get('PATH', '') | ||||
|         cls._env['VIRTUAL_ENV'] = path | ||||
|  | ||||
|     @classmethod | ||||
|     def reset_executable(cls): | ||||
|         r"""Resets the executable to system standard""" | ||||
|         cls._executable = sys.executable | ||||
|         cls._is_venv = False | ||||
|  | ||||
|     """Public utils functions""" | ||||
|  | ||||
|     @classmethod | ||||
|     def get_package(cls, package: str) -> Optional[str]: | ||||
|         r"""Gets given package py local pip list | ||||
| @@ -60,8 +66,6 @@ class Pip: | ||||
|         result = None | ||||
|         with suppress(Exception): | ||||
|             args = [cls._executable, "-m", "pip", "show", package] | ||||
|             if cls._is_venv: | ||||
|                 args = ["pip", "show", package] | ||||
|  | ||||
|             result = subprocess.check_output( | ||||
|                 args, | ||||
| @@ -92,8 +96,6 @@ class Pip: | ||||
|             Bytes string of the command result | ||||
|         """ | ||||
|         args = [cls._executable, "-m", "pip", "list", "--outdated"] | ||||
|         if cls._is_venv: | ||||
|             args = ["pip", "list", "--outdated"] | ||||
|  | ||||
|         return subprocess.check_output(args, env=cls._env) | ||||
|  | ||||
| @@ -115,8 +117,6 @@ class Pip: | ||||
|                 Stderr of subprocess.run | ||||
|         """ | ||||
|         pip_args = [cls._executable, "-m", "pip", "install"] | ||||
|         if cls._is_venv: | ||||
|             pip_args = ["pip", "install"] | ||||
|  | ||||
|         for arg in args: | ||||
|             pip_args.append(arg) | ||||
| @@ -126,6 +126,7 @@ class Pip: | ||||
|             pip_args.append(source) | ||||
|  | ||||
|         pip_args.append(package) | ||||
|         print(pip_args) | ||||
|         subprocess.run(pip_args, stdout=stdout, stderr=stderr, env=cls._env) | ||||
|  | ||||
|     @classmethod | ||||
| @@ -142,8 +143,6 @@ class Pip: | ||||
|                 Stderr of subprocess.run | ||||
|         """ | ||||
|         args = [cls._executable, "-m", "pip", "uninstall", "--yes", package] | ||||
|         if cls._is_venv: | ||||
|             args = ["pip", "uninstall", "--yes", package] | ||||
|  | ||||
|         subprocess.run( | ||||
|             args, | ||||
|   | ||||
| @@ -20,7 +20,7 @@ | ||||
|     ], | ||||
|     "PythonVersion": ">=3.10", | ||||
|     "PythonPath": { | ||||
|       "linux": "../../../../../../cpl-env/bin/python3.9", | ||||
|       "linux": "../../venv/bin/python", | ||||
|       "win32": "" | ||||
|     }, | ||||
|     "Classifiers": [] | ||||
|   | ||||
		Reference in New Issue
	
	Block a user