splent-cli 0.0.1__py3-none-any.whl

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.
splent_cli/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ import os
2
+ import sys
3
+
4
+ # Add the project's root directory to the PYTHONPATH
5
+ # This allows Python to locate modules in the parent directory, enabling relative imports.
6
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
splent_cli/__main__.py ADDED
@@ -0,0 +1,11 @@
1
+ from splent_cli.cli import check_working_dir, cli, load_commands
2
+
3
+
4
+ def main():
5
+ check_working_dir()
6
+ load_commands(cli)
7
+ cli()
8
+
9
+
10
+ if __name__ == "__main__":
11
+ main()
splent_cli/cli.py ADDED
@@ -0,0 +1,59 @@
1
+ import os
2
+ import sys
3
+ import importlib
4
+ import click
5
+ from dotenv import load_dotenv
6
+ from flask.cli import FlaskGroup # 📌 Añadir FlaskGroup
7
+ from splent_app import create_app # 📌 Importar la app de Flask
8
+
9
+ from splent_cli.utils.path_utils import PathUtils
10
+
11
+ load_dotenv()
12
+
13
+
14
+ def check_working_dir():
15
+ working_dir = os.getenv("WORKING_DIR", "").strip()
16
+
17
+ if not working_dir:
18
+ return
19
+
20
+ if working_dir in ["/app", "/vagrant", "/app/", "/vagrant/"] and not os.path.exists(working_dir):
21
+ print(f"⚠️ WARNING: WORKING_DIR is set to '{working_dir}', but the directory does not exist.")
22
+ sys.exit(1)
23
+
24
+
25
+ class SPLENTCLI(FlaskGroup): # 📌 Usamos FlaskGroup para conectar con Flask
26
+ def __init__(self, **kwargs):
27
+ super().__init__(create_app=create_app, **kwargs)
28
+
29
+ def get_command(self, ctx, cmd_name):
30
+ rv = super().get_command(ctx, cmd_name)
31
+ if rv is None:
32
+ click.echo(f"No such command '{cmd_name}'.")
33
+ click.echo("Try 'splent_cli --help' for a list of available commands.")
34
+ return rv
35
+
36
+
37
+ def load_commands(cli_group, commands_dir="splent_cli/commands"):
38
+ """
39
+ Dynamically import all commands in the specified directory and add them to the CLI group.
40
+ """
41
+ commands_path = PathUtils.get_commands_path()
42
+
43
+ for file in os.listdir(commands_path):
44
+ if file.endswith(".py") and not file.startswith("__"):
45
+ module_name = f"splent_cli.commands.{file[:-3]}"
46
+ module = importlib.import_module(module_name)
47
+ for attr_name in dir(module):
48
+ attr = getattr(module, attr_name)
49
+ if isinstance(attr, click.Command):
50
+ cli_group.add_command(attr)
51
+
52
+
53
+ @click.group(cls=SPLENTCLI)
54
+ def cli():
55
+ """A CLI tool to help with project development."""
56
+
57
+
58
+ if __name__ == "__main__":
59
+ cli()
File without changes
@@ -0,0 +1,90 @@
1
+ from pathlib import Path
2
+
3
+ import click
4
+ import shutil
5
+ import os
6
+
7
+ from splent_cli.utils.path_utils import PathUtils
8
+
9
+
10
+ @click.command(
11
+ "clear:cache",
12
+ help="Clears pytest cache in app/modules and the build directory at the root.",
13
+ )
14
+ def clear_cache():
15
+
16
+ if click.confirm(
17
+ "Are you sure you want to clear the pytest cache and the build directory?"
18
+ ):
19
+
20
+ project_root = Path(os.getenv("WORKING_DIR", ""))
21
+
22
+ pytest_cache_dir = os.path.join(
23
+ PathUtils.get_modules_dir(), ".pytest_cache"
24
+ )
25
+
26
+ build_dir = os.path.join(os.getenv("WORKING_DIR", ""), "build")
27
+
28
+ if os.path.exists(pytest_cache_dir):
29
+ try:
30
+ shutil.rmtree(pytest_cache_dir)
31
+ click.echo(click.style("Pytest cache cleared.", fg="green"))
32
+ except Exception as e:
33
+ click.echo(
34
+ click.style(f"Failed to clear pytest cache: {e}", fg="red")
35
+ )
36
+ else:
37
+ click.echo(
38
+ click.style(
39
+ "No pytest cache found. Nothing to clear.", fg="yellow"
40
+ )
41
+ )
42
+
43
+ if os.path.exists(build_dir):
44
+ try:
45
+ shutil.rmtree(build_dir)
46
+ click.echo(click.style("Build directory cleared.", fg="green"))
47
+ except Exception as e:
48
+ click.echo(
49
+ click.style(
50
+ f"Failed to clear build directory: {e}", fg="red"
51
+ )
52
+ )
53
+ else:
54
+ click.echo(
55
+ click.style(
56
+ "No cache or build directory found. Nothing to clear.",
57
+ fg="yellow",
58
+ )
59
+ )
60
+
61
+ pycache_dirs = project_root.rglob("__pycache__")
62
+ for dir in pycache_dirs:
63
+ try:
64
+ shutil.rmtree(dir)
65
+ except Exception as e:
66
+ click.echo(
67
+ click.style(
68
+ f"Failed to clear __pycache__ directory {dir}: {e}",
69
+ fg="red",
70
+ )
71
+ )
72
+ click.echo(
73
+ click.style("All __pycache__ directories cleared.", fg="green")
74
+ )
75
+
76
+ pyc_files = project_root.rglob("*.pyc")
77
+ for file in pyc_files:
78
+ try:
79
+ file.unlink()
80
+ except Exception as e:
81
+ click.echo(
82
+ click.style(
83
+ f"Failed to clear .pyc file {file}: {e}", fg="red"
84
+ )
85
+ )
86
+
87
+ click.echo(click.style("All cache cleared.", fg="green"))
88
+
89
+ else:
90
+ click.echo(click.style("Clear operation cancelled.", fg="yellow"))
@@ -0,0 +1,31 @@
1
+ import click
2
+ import os
3
+
4
+ from splent_cli.utils.path_utils import PathUtils
5
+
6
+
7
+ @click.command("clear:log", help="Clears the 'app.log' file.")
8
+ def clear_log():
9
+ log_file_path = PathUtils.get_app_log_dir()
10
+
11
+ # Check if the log file exists
12
+ if os.path.exists(log_file_path):
13
+ try:
14
+ # Deletes the log file
15
+ os.remove(log_file_path)
16
+ click.echo(
17
+ click.style(
18
+ "The 'app.log' file has been successfully cleared.",
19
+ fg="green",
20
+ )
21
+ )
22
+ except Exception as e:
23
+ click.echo(
24
+ click.style(
25
+ f"Error clearing the 'app.log' file: {e}", fg="red"
26
+ )
27
+ )
28
+ else:
29
+ click.echo(
30
+ click.style("The 'app.log' file does not exist.", fg="yellow")
31
+ )
@@ -0,0 +1,44 @@
1
+ import click
2
+ import shutil
3
+ import os
4
+
5
+ from splent_cli.utils.path_utils import PathUtils
6
+
7
+
8
+ @click.command(
9
+ "clear:uploads",
10
+ help="Clears the contents of the 'uploads' directory without removing the folder.",
11
+ )
12
+ def clear_uploads():
13
+ uploads_dir = PathUtils.get_uploads_dir()
14
+
15
+ # Verify if the 'uploads' folder exists
16
+ if os.path.exists(uploads_dir) and os.path.isdir(uploads_dir):
17
+ try:
18
+ # Iterate over the contents of the directory
19
+ for filename in os.listdir(uploads_dir):
20
+ file_path = os.path.join(uploads_dir, filename)
21
+
22
+ # If it's a file, remove it
23
+ if os.path.isfile(file_path) or os.path.islink(file_path):
24
+ os.remove(file_path)
25
+ # If it's a directory, remove it and its contents
26
+ elif os.path.isdir(file_path):
27
+ shutil.rmtree(file_path)
28
+
29
+ click.echo(
30
+ click.style(
31
+ "The contents of the 'uploads' directory have been successfully cleared.",
32
+ fg="green",
33
+ )
34
+ )
35
+ except Exception as e:
36
+ click.echo(
37
+ click.style(
38
+ f"Error clearing the 'uploads' directory: {e}", fg="red"
39
+ )
40
+ )
41
+ else:
42
+ click.echo(
43
+ click.style("The 'uploads' directory does not exist.", fg="yellow")
44
+ )
@@ -0,0 +1,50 @@
1
+ import os
2
+ import click
3
+ from dotenv import dotenv_values
4
+ from flask.cli import with_appcontext
5
+
6
+ from splent_cli.utils.path_utils import PathUtils
7
+
8
+
9
+ @click.command(
10
+ "compose:env",
11
+ help="Combines .env files from blueprints with the root .env, checking for conflicts.",
12
+ )
13
+ @with_appcontext
14
+ def compose_env():
15
+
16
+ modules_dir = PathUtils.get_modules_dir()
17
+ root_env_path = PathUtils.get_env_dir()
18
+
19
+ # Loads the current root .env variables into a dictionary
20
+ root_env_vars = dotenv_values(root_env_path)
21
+
22
+ # Finds and processes all blueprints .env files
23
+ module_env_paths = [
24
+ os.path.join(root, ".env")
25
+ for root, dirs, files in os.walk(modules_dir)
26
+ if ".env" in files
27
+ ]
28
+ for env_path in module_env_paths:
29
+ blueprint_env_vars = dotenv_values(env_path)
30
+ # Add or update the blueprint variables in the root .env dictionary
31
+ for key, value in blueprint_env_vars.items():
32
+ if key in root_env_vars and root_env_vars[key] != value:
33
+ conflict_msg = (
34
+ f"Conflict found for variable '{key}' in {env_path}. "
35
+ "Keeping the original value."
36
+ )
37
+ click.echo(click.style(conflict_msg, fg="yellow"))
38
+ continue
39
+ root_env_vars[key] = value
40
+
41
+ # Write back to the root .env file
42
+ with open(root_env_path, "w") as root_env_file:
43
+ for key, value in root_env_vars.items():
44
+ root_env_file.write(f"{key}={value}\n")
45
+
46
+ click.echo(
47
+ click.style(
48
+ "Successfully merged .env files without conflicts.", fg="green"
49
+ )
50
+ )
@@ -0,0 +1,46 @@
1
+ import click
2
+ import subprocess
3
+ import os
4
+
5
+ from splent_cli.utils.path_utils import PathUtils
6
+
7
+
8
+ @click.command(
9
+ "coverage",
10
+ help="Runs pytest coverage on the blueprints directory or a specific module.",
11
+ )
12
+ @click.argument("module_name", required=False)
13
+ @click.option(
14
+ "--html", is_flag=True, help="Generates an HTML coverage report."
15
+ )
16
+ def coverage(module_name, html):
17
+ modules_dir = PathUtils.get_modules_dir()
18
+ test_path = modules_dir
19
+
20
+ if module_name:
21
+ test_path = os.path.join(modules_dir, module_name)
22
+ if not os.path.exists(test_path):
23
+ click.echo(
24
+ click.style(
25
+ f"Module '{module_name}' does not exist.", fg="red"
26
+ )
27
+ )
28
+ return
29
+ click.echo(f"Running coverage for the '{module_name}' module...")
30
+ else:
31
+ click.echo("Running coverage for all modules...")
32
+
33
+ coverage_cmd = [
34
+ "pytest",
35
+ "--ignore-glob=*selenium*",
36
+ "--cov=" + test_path,
37
+ test_path,
38
+ ]
39
+
40
+ if html:
41
+ coverage_cmd.extend(["--cov-report", "html"])
42
+
43
+ try:
44
+ subprocess.run(coverage_cmd, check=True)
45
+ except subprocess.CalledProcessError as e:
46
+ click.echo(click.style(f"Error running coverage: {e}", fg="red"))
@@ -0,0 +1,27 @@
1
+ import click
2
+ import subprocess
3
+ from dotenv import load_dotenv
4
+ import os
5
+
6
+
7
+ @click.command(
8
+ "db:console", help="Opens a MariaDB console with credentials from .env."
9
+ )
10
+ def db_console():
11
+ load_dotenv()
12
+
13
+ mariadb_hostname = os.getenv("MARIADB_HOSTNAME")
14
+ mariadb_user = os.getenv("MARIADB_USER")
15
+ mariadb_password = os.getenv("MARIADB_PASSWORD")
16
+ mariadb_database = os.getenv("MARIADB_DATABASE")
17
+
18
+ # Build the command to connect to MariaDB
19
+ mariadb_connect_cmd = f"mysql -h{mariadb_hostname} -u{mariadb_user} -p{mariadb_password} {mariadb_database}"
20
+
21
+ # Execute the command
22
+ try:
23
+ subprocess.run(mariadb_connect_cmd, shell=True, check=True)
24
+ except subprocess.CalledProcessError as e:
25
+ click.echo(
26
+ click.style(f"Error opening MariaDB console: {e}", fg="red")
27
+ )
@@ -0,0 +1,45 @@
1
+ import click
2
+ import subprocess
3
+ from dotenv import load_dotenv
4
+ import os
5
+ from datetime import datetime
6
+
7
+
8
+ @click.command(
9
+ "db:dump",
10
+ help="Creates a dump of the MariaDB database with credentials from .env.",
11
+ )
12
+ @click.argument("filename", required=False)
13
+ def db_dump(filename):
14
+ load_dotenv()
15
+
16
+ mariadb_hostname = os.getenv("MARIADB_HOSTNAME")
17
+ mariadb_user = os.getenv("MARIADB_USER")
18
+ mariadb_password = os.getenv("MARIADB_PASSWORD")
19
+ mariadb_database = os.getenv("MARIADB_DATABASE")
20
+
21
+ # Generate default filename if not provided
22
+ if not filename:
23
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
24
+ filename = f"dump_{timestamp}.sql"
25
+ else:
26
+ # Ensure filename has .sql extension
27
+ if not filename.endswith(".sql"):
28
+ filename += ".sql"
29
+
30
+ # Build the mysqldump command
31
+ dump_cmd = f"mysqldump -h{mariadb_hostname} -u{mariadb_user} -p{mariadb_password} \
32
+ {mariadb_database} > {filename}"
33
+
34
+ # Execute the command
35
+ try:
36
+ subprocess.run(
37
+ dump_cmd, shell=True, check=True, executable="/bin/bash"
38
+ )
39
+ click.echo(
40
+ click.style(
41
+ f"Database dump created successfully: {filename}", fg="green"
42
+ )
43
+ )
44
+ except subprocess.CalledProcessError as e:
45
+ click.echo(click.style(f"Error creating database dump: {e}", fg="red"))
@@ -0,0 +1,37 @@
1
+ import subprocess
2
+ import click
3
+ from flask.cli import with_appcontext
4
+
5
+
6
+ @click.command("db:migrate", help="Generates and applies database migrations.")
7
+ @with_appcontext
8
+ def db_migrate():
9
+ # Generates migrations
10
+ click.echo("Generating database migrations...")
11
+ result_migrate = subprocess.run(["flask", "db", "migrate"])
12
+ if result_migrate.returncode == 0:
13
+ click.echo(
14
+ click.style("Migrations generated successfully.", fg="green")
15
+ )
16
+ else:
17
+ click.echo(
18
+ click.style(
19
+ "Note: No new migrations needed or an error occurred "
20
+ "while generating migrations.",
21
+ fg="yellow",
22
+ )
23
+ )
24
+
25
+ # Applies to migrations
26
+ click.echo("Applying database migrations...")
27
+ result_upgrade = subprocess.run(["flask", "db", "upgrade"])
28
+ if result_upgrade.returncode == 0:
29
+ click.echo(click.style("Migrations applied successfully.", fg="green"))
30
+ else:
31
+ click.echo(
32
+ click.style(
33
+ "Error applying migrations. This may be due to the database "
34
+ "being already up-to-date.",
35
+ fg="yellow",
36
+ )
37
+ )
@@ -0,0 +1,90 @@
1
+ import click
2
+ import shutil
3
+ import os
4
+ import subprocess
5
+ from flask.cli import with_appcontext
6
+ from splent_app import create_app, db
7
+ from sqlalchemy import MetaData
8
+
9
+ from splent_cli.commands.clear_uploads import clear_uploads
10
+
11
+
12
+ @click.command(
13
+ "db:reset",
14
+ help="Resets the database, optionally clears migrations and recreates them.",
15
+ )
16
+ @click.option(
17
+ "--clear-migrations",
18
+ is_flag=True,
19
+ help="Remove all tables including 'alembic_version', clear migrations folder, and recreate migrations.",
20
+ )
21
+ @click.option(
22
+ "-y",
23
+ "--yes",
24
+ is_flag=True,
25
+ help="Confirm the operation without prompting.",
26
+ )
27
+ @with_appcontext
28
+ def db_reset(clear_migrations, yes):
29
+ app = create_app()
30
+ with app.app_context():
31
+ if not yes and not click.confirm(
32
+ "WARNING: This will delete all data and clear uploads. Are you sure?",
33
+ abort=True,
34
+ ):
35
+ return
36
+
37
+ # Deletes data from all tables
38
+ try:
39
+ meta = MetaData()
40
+ meta.reflect(bind=db.engine)
41
+ with db.engine.connect() as conn:
42
+ trans = conn.begin() # Begin transaction
43
+ for table in reversed(meta.sorted_tables):
44
+ if not clear_migrations or table.name != "alembic_version":
45
+ conn.execute(table.delete())
46
+ trans.commit() # End transaction
47
+ click.echo(click.style("All table data cleared.", fg="yellow"))
48
+ subprocess.run(["flask", "db", "stamp", "head"], check=True)
49
+ except Exception as e:
50
+ click.echo(
51
+ click.style(f"Error clearing table data: {e}", fg="red")
52
+ )
53
+ if trans:
54
+ trans.rollback()
55
+ return
56
+
57
+ # Delete the uploads folder
58
+ ctx = click.get_current_context()
59
+ ctx.invoke(clear_uploads)
60
+
61
+ if clear_migrations:
62
+ # Delete the migration folder if it exists.
63
+ migrations_dir = os.path.join(
64
+ os.getenv("WORKING_DIR", ""), "migrations"
65
+ )
66
+ if os.path.isdir(migrations_dir):
67
+ shutil.rmtree(migrations_dir)
68
+ click.echo(
69
+ click.style("Migrations directory cleared.", fg="yellow")
70
+ )
71
+
72
+ # Run flask db init, migrate and upgrade
73
+ try:
74
+ subprocess.run(["flask", "db", "init"], check=True)
75
+ subprocess.run(["flask", "db", "migrate"], check=True)
76
+ subprocess.run(["flask", "db", "upgrade"], check=True)
77
+ click.echo(
78
+ click.style(
79
+ "Database recreated from new migrations.", fg="green"
80
+ )
81
+ )
82
+ except subprocess.CalledProcessError as e:
83
+ click.echo(
84
+ click.style(
85
+ f"Error during migrations reset: {e}", fg="red"
86
+ )
87
+ )
88
+ return
89
+
90
+ click.echo(click.style("Database reset successfully.", fg="green"))
@@ -0,0 +1,123 @@
1
+ import inspect
2
+ import os
3
+ import importlib
4
+ import click
5
+ from flask.cli import with_appcontext
6
+
7
+
8
+ from splent_framework.core.seeders import BaseSeeder
9
+ from splent_cli.commands.db_reset import db_reset
10
+
11
+
12
+ def get_module_seeders(module_path, specific_module=None):
13
+ seeders = []
14
+ for root, dirs, files in os.walk(module_path):
15
+ if "seeders.py" in files:
16
+ relative_path = os.path.relpath(root, module_path)
17
+ module_name = relative_path.replace(os.path.sep, ".")
18
+ full_module_name = f"app.modules.{module_name}.seeders"
19
+
20
+ # If a module was specified and does not match the current one, continue with the next one
21
+ if (
22
+ specific_module
23
+ and specific_module != module_name.split(".")[0]
24
+ ):
25
+ continue
26
+
27
+ seeder_module = importlib.import_module(full_module_name)
28
+ importlib.reload(seeder_module) # Reload the module
29
+
30
+ for attr in dir(seeder_module):
31
+ potential_seeder_class = getattr(seeder_module, attr)
32
+ if (
33
+ inspect.isclass(potential_seeder_class)
34
+ and issubclass(potential_seeder_class, BaseSeeder)
35
+ and potential_seeder_class is not BaseSeeder
36
+ ):
37
+ seeders.append(potential_seeder_class())
38
+
39
+ # Sort seeders by priority
40
+ seeders.sort(key=lambda seeder: seeder.priority)
41
+
42
+ return seeders
43
+
44
+
45
+ @click.command(
46
+ "db:seed",
47
+ help="Populates the database with the seeders defined in each module.",
48
+ )
49
+ @click.option(
50
+ "--reset", is_flag=True, help="Reset the database before seeding."
51
+ )
52
+ @click.option(
53
+ "-y",
54
+ "--yes",
55
+ is_flag=True,
56
+ help="Confirm the operation without prompting.",
57
+ )
58
+ @click.argument("module", required=False)
59
+ @with_appcontext
60
+ def db_seed(reset, yes, module):
61
+
62
+ if reset:
63
+ if yes or click.confirm(
64
+ click.style(
65
+ "This will reset the database, do you want " "to continue?",
66
+ fg="red",
67
+ ),
68
+ abort=True,
69
+ ):
70
+ click.echo(click.style("Resetting the database...", fg="yellow"))
71
+ ctx = click.get_current_context()
72
+ ctx.invoke(db_reset, clear_migrations=False, yes=True)
73
+ else:
74
+ click.echo(click.style("Database reset cancelled.", fg="yellow"))
75
+ return
76
+
77
+ blueprints_module_path = os.path.join(
78
+ os.getenv("WORKING_DIR", ""), "app/modules"
79
+ )
80
+ seeders = get_module_seeders(
81
+ blueprints_module_path, specific_module=module
82
+ )
83
+ success = True # Flag to control the successful flow of the operation
84
+
85
+ if module:
86
+ click.echo(
87
+ click.style(
88
+ f"Seeding data for the '{module}' module...", fg="green"
89
+ )
90
+ )
91
+ else:
92
+ click.echo(click.style("Seeding data for all modules...", fg="green"))
93
+
94
+ for seeder in seeders:
95
+ try:
96
+ seeder.run()
97
+ click.echo(
98
+ click.style(
99
+ f"{seeder.__class__.__name__} performed.", fg="blue"
100
+ )
101
+ )
102
+ except Exception as e:
103
+ click.echo(
104
+ click.style(
105
+ f"Error running seeder {seeder.__class__.__name__}: {e}",
106
+ fg="red",
107
+ )
108
+ )
109
+ click.echo(
110
+ click.style(
111
+ f"Rolled back the transaction of {seeder.__class__.__name__} to keep the session "
112
+ f"clean.",
113
+ fg="yellow",
114
+ )
115
+ )
116
+
117
+ success = False
118
+ break
119
+
120
+ if success:
121
+ click.echo(
122
+ click.style("Database populated with test data.", fg="green")
123
+ )
@@ -0,0 +1,18 @@
1
+ # splent_cli/commands/env.py
2
+
3
+ import click
4
+ from dotenv import dotenv_values
5
+
6
+ from splent_cli.utils.path_utils import PathUtils
7
+
8
+
9
+ @click.command()
10
+ def env():
11
+ """Displays the current .env file values."""
12
+ # Load the .env file
13
+ env_dir = PathUtils.get_env_dir()
14
+ env_values = dotenv_values(env_dir)
15
+
16
+ # Display keys and values
17
+ for key, value in env_values.items():
18
+ click.echo(f"{key}={value}")