cmdbox-cli 1.0.0__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.
- cmdbox/__init__.py +0 -0
- cmdbox/cli/__init__.py +0 -0
- cmdbox/cli/app.py +125 -0
- cmdbox/cli/commands/__init__.py +0 -0
- cmdbox/cli/commands/alias_fallback.py +102 -0
- cmdbox/cli/commands/command_crud.py +429 -0
- cmdbox/cli/commands/command_run.py +255 -0
- cmdbox/cli/commands/history.py +109 -0
- cmdbox/cli/commands/init.py +54 -0
- cmdbox/cli/commands/settings.py +62 -0
- cmdbox/cli/commands/tag_crud.py +277 -0
- cmdbox/cli/commands/variable_crud.py +349 -0
- cmdbox/cli/common/__init__.py +0 -0
- cmdbox/cli/common/errors.py +58 -0
- cmdbox/cli/common/update_fields.py +88 -0
- cmdbox/cli/completions/__init__.py +0 -0
- cmdbox/cli/completions/commands.py +26 -0
- cmdbox/cli/completions/fields.py +31 -0
- cmdbox/cli/completions/tags.py +24 -0
- cmdbox/cli/completions/variables.py +26 -0
- cmdbox/cli/handlers/__init__.py +0 -0
- cmdbox/cli/handlers/command_handlers.py +357 -0
- cmdbox/cli/handlers/common_handlers.py +15 -0
- cmdbox/cli/handlers/history_handlers.py +94 -0
- cmdbox/cli/handlers/init_handler.py +127 -0
- cmdbox/cli/handlers/run_handler.py +178 -0
- cmdbox/cli/handlers/settings_handler.py +59 -0
- cmdbox/cli/handlers/tag_handlers.py +220 -0
- cmdbox/cli/handlers/variable_handlers.py +272 -0
- cmdbox/cli/prompts/__init__.py +0 -0
- cmdbox/cli/prompts/completers.py +161 -0
- cmdbox/cli/prompts/prompts.py +108 -0
- cmdbox/cli/prompts/validators.py +46 -0
- cmdbox/cli/ui/__init__.py +0 -0
- cmdbox/cli/ui/console.py +31 -0
- cmdbox/cli/ui/editor.py +141 -0
- cmdbox/cli/ui/presenters/__init__.py +0 -0
- cmdbox/cli/ui/presenters/app_presenter.py +8 -0
- cmdbox/cli/ui/presenters/command_presenter.py +168 -0
- cmdbox/cli/ui/presenters/history_presenter.py +83 -0
- cmdbox/cli/ui/presenters/init_instructions.py +52 -0
- cmdbox/cli/ui/presenters/init_presenter.py +57 -0
- cmdbox/cli/ui/presenters/result_presenter.py +144 -0
- cmdbox/cli/ui/presenters/settings_presenter.py +130 -0
- cmdbox/cli/ui/presenters/tag_presenter.py +97 -0
- cmdbox/cli/ui/presenters/variable_presenter.py +103 -0
- cmdbox/cli/ui/primitives.py +410 -0
- cmdbox/cli/ui/theme.py +43 -0
- cmdbox/cli/ui/theme_builder.py +49 -0
- cmdbox/common/__init__.py +0 -0
- cmdbox/common/io.py +34 -0
- cmdbox/container.py +156 -0
- cmdbox/core/__init__.py +0 -0
- cmdbox/core/fields.py +48 -0
- cmdbox/core/paths.py +52 -0
- cmdbox/database.py +65 -0
- cmdbox/exceptions.py +10 -0
- cmdbox/init/__init__.py +0 -0
- cmdbox/init/detect.py +82 -0
- cmdbox/init/integrations/bash.sh +10 -0
- cmdbox/init/integrations/cmd.bat +14 -0
- cmdbox/init/integrations/fish.fish +11 -0
- cmdbox/init/integrations/powershell.ps1 +14 -0
- cmdbox/init/integrations/zsh.sh +10 -0
- cmdbox/init/io.py +68 -0
- cmdbox/init/specs.py +54 -0
- cmdbox/logging_setup/__init__.py +0 -0
- cmdbox/logging_setup/log_config.py +123 -0
- cmdbox/logging_setup/log_decorators.py +40 -0
- cmdbox/logging_setup/log_handlers.py +94 -0
- cmdbox/migrations/__init__.py +1 -0
- cmdbox/migrations/errors.py +10 -0
- cmdbox/migrations/runner.py +127 -0
- cmdbox/migrations/versions/__init__.py +0 -0
- cmdbox/models.py +165 -0
- cmdbox/repositories/__init__.py +0 -0
- cmdbox/repositories/base_repository.py +181 -0
- cmdbox/repositories/command_repository.py +391 -0
- cmdbox/repositories/errors.py +120 -0
- cmdbox/repositories/history_repository.py +155 -0
- cmdbox/repositories/results.py +37 -0
- cmdbox/repositories/tag_repository.py +91 -0
- cmdbox/repositories/validators.py +256 -0
- cmdbox/repositories/variable_repository.py +324 -0
- cmdbox/resolve/__init__.py +0 -0
- cmdbox/resolve/errors.py +65 -0
- cmdbox/resolve/lookup.py +137 -0
- cmdbox/resolve/resolver.py +402 -0
- cmdbox/resolve/type_defs.py +96 -0
- cmdbox/runtime/__init__.py +0 -0
- cmdbox/runtime/executor.py +454 -0
- cmdbox/runtime/results.py +25 -0
- cmdbox/runtime/shell.py +90 -0
- cmdbox/services/__init__.py +0 -0
- cmdbox/services/command_services.py +261 -0
- cmdbox/services/errors.py +37 -0
- cmdbox/services/field_selection.py +162 -0
- cmdbox/services/history_service.py +68 -0
- cmdbox/services/run_service.py +204 -0
- cmdbox/services/tag_services.py +134 -0
- cmdbox/services/variable_services.py +224 -0
- cmdbox/settings/__init__.py +0 -0
- cmdbox/settings/models.py +129 -0
- cmdbox/settings/settings_repository.py +36 -0
- cmdbox/settings/settings_service.py +144 -0
- cmdbox/version.py +1 -0
- cmdbox_cli-1.0.0.dist-info/METADATA +125 -0
- cmdbox_cli-1.0.0.dist-info/RECORD +112 -0
- cmdbox_cli-1.0.0.dist-info/WHEEL +5 -0
- cmdbox_cli-1.0.0.dist-info/entry_points.txt +2 -0
- cmdbox_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
- cmdbox_cli-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import re
|
|
3
|
+
import logging
|
|
4
|
+
from logging.handlers import RotatingFileHandler
|
|
5
|
+
|
|
6
|
+
from cmdbox.logging_setup.log_config import LogConfig, get_logger
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SecretRedactionFilter(logging.Filter):
|
|
10
|
+
"""
|
|
11
|
+
A logging filter to redact sensitive information from log messages.
|
|
12
|
+
|
|
13
|
+
This filter is designed to sanitize log messages by redacting sensitive
|
|
14
|
+
data such as tokens and passwords. The redaction process is applied
|
|
15
|
+
whenever a log record passes through the filter, ensuring that such
|
|
16
|
+
sensitive information is not exposed in logs.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
20
|
+
if isinstance(record.msg, str):
|
|
21
|
+
record.msg = self._redact(record.msg)
|
|
22
|
+
return True
|
|
23
|
+
|
|
24
|
+
def _redact(self, s: str) -> str:
|
|
25
|
+
s = re.sub(r"(?i)\b(token\s*=\s*)(\S+)", r"\1[REDACTED]", s)
|
|
26
|
+
s = re.sub(r"(?i)\b(password\s*=\s*)(\S+)", r"\1[REDACTED]", s)
|
|
27
|
+
return s
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class RunIdFilter(logging.Filter):
|
|
31
|
+
"""
|
|
32
|
+
A logging filter that appends a unique run identifier to log records.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
run_id (str): The identifier for the current run, which will be
|
|
36
|
+
appended to log records.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, run_id: str) -> None:
|
|
40
|
+
super().__init__()
|
|
41
|
+
self.run_id = run_id
|
|
42
|
+
|
|
43
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
44
|
+
record.run_id = self.run_id
|
|
45
|
+
return True
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def configure_logging(config: LogConfig, run_id: str) -> None:
|
|
49
|
+
"""
|
|
50
|
+
Configures logging for the application, setting up a logger with both console and file
|
|
51
|
+
handlers as specified by the provided configuration. Filters and formatters are applied
|
|
52
|
+
to ensure appropriate logging output.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
config (LogConfig): Configuration object specifying the logging settings, including
|
|
56
|
+
log levels, file path, maximum file size, and number of backups.
|
|
57
|
+
run_id (str): Unique identifier for the current run, used to tag log records.
|
|
58
|
+
"""
|
|
59
|
+
logger = get_logger()
|
|
60
|
+
logger.setLevel(logging.DEBUG) # capture everything, then filter
|
|
61
|
+
logger.propagate = False
|
|
62
|
+
|
|
63
|
+
for h in list(logger.handlers):
|
|
64
|
+
logger.removeHandler(h)
|
|
65
|
+
|
|
66
|
+
formatter = logging.Formatter(
|
|
67
|
+
fmt="%(asctime)s.%(msecs)03d | %(levelname)s | %(run_id)s | %(name)s | %(message)s",
|
|
68
|
+
datefmt="%Y-%m-%d, %H:%M:%S",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
secret_redaction_filter = SecretRedactionFilter()
|
|
72
|
+
run_id_filter = RunIdFilter(run_id)
|
|
73
|
+
|
|
74
|
+
# Console handler
|
|
75
|
+
ch = logging.StreamHandler(sys.stderr)
|
|
76
|
+
ch.setLevel(config.console_level)
|
|
77
|
+
ch.setFormatter(formatter)
|
|
78
|
+
ch.addFilter(secret_redaction_filter)
|
|
79
|
+
ch.addFilter(run_id_filter)
|
|
80
|
+
logger.addHandler(ch)
|
|
81
|
+
|
|
82
|
+
# File handler
|
|
83
|
+
if config.file_enabled:
|
|
84
|
+
fh = RotatingFileHandler(
|
|
85
|
+
filename=str(config.file_path),
|
|
86
|
+
maxBytes=int(config.max_bytes),
|
|
87
|
+
backupCount=int(config.backups),
|
|
88
|
+
encoding="utf-8",
|
|
89
|
+
)
|
|
90
|
+
fh.setLevel(config.file_level)
|
|
91
|
+
fh.setFormatter(formatter)
|
|
92
|
+
fh.addFilter(secret_redaction_filter)
|
|
93
|
+
fh.addFilter(run_id_filter)
|
|
94
|
+
logger.addHandler(fh)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Migration runner package for CmdBox schema versioning."""
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import logging
|
|
3
|
+
import pkgutil
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
from peewee import SqliteDatabase
|
|
8
|
+
|
|
9
|
+
from cmdbox.migrations.errors import MigrationError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
log = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def ensure_migrated(db_path: str) -> None:
|
|
16
|
+
"""
|
|
17
|
+
Ensures the database is migrated to the current version. Connects to the SQLite database,
|
|
18
|
+
checks the user version, and applies any necessary migrations to bring the database
|
|
19
|
+
to the current version. If the database version is ahead of the current application
|
|
20
|
+
version, an error is raised.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
db_path: A string representing the path to the SQLite database file.
|
|
24
|
+
|
|
25
|
+
Raises:
|
|
26
|
+
MigrationError: If the database version is greater than the application's
|
|
27
|
+
current version, indicating the application is outdated and needs updating.
|
|
28
|
+
"""
|
|
29
|
+
db = SqliteDatabase(db_path)
|
|
30
|
+
db.connect()
|
|
31
|
+
version = get_user_version(db)
|
|
32
|
+
db.close()
|
|
33
|
+
|
|
34
|
+
current_version = get_current_version()
|
|
35
|
+
|
|
36
|
+
if version == 0:
|
|
37
|
+
db.connect()
|
|
38
|
+
set_user_version(db, current_version)
|
|
39
|
+
db.close()
|
|
40
|
+
log.debug("Fresh database stamped at v%d", current_version)
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
if version == current_version:
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
if version > current_version:
|
|
47
|
+
raise MigrationError(
|
|
48
|
+
f"Database version {version} is ahead of the application (v{current_version}). Please update CmdBox."
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
migrations = load_migrations()
|
|
52
|
+
path = Path(db_path)
|
|
53
|
+
|
|
54
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
55
|
+
|
|
56
|
+
for v in range(version, current_version):
|
|
57
|
+
migrate(v, path, migrations, timestamp)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def migrate(version: int, path: Path, migrations: dict, timestamp: str) -> None:
|
|
61
|
+
"""
|
|
62
|
+
Migrates a database from one version to the next using the provided migration mapping.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
version (int): The current version of the database.
|
|
66
|
+
path (Path): The file path of the database to migrate.
|
|
67
|
+
migrations (dict): A dictionary where keys are version numbers and values are callable
|
|
68
|
+
migration functions. Each function must accept the current database path and the
|
|
69
|
+
path to the new database as string arguments.
|
|
70
|
+
timestamp (str): A timestamp used for creating backups.
|
|
71
|
+
|
|
72
|
+
Raises:
|
|
73
|
+
MigrationError: If no migration function exists for the current version.
|
|
74
|
+
"""
|
|
75
|
+
if version not in migrations:
|
|
76
|
+
raise MigrationError(
|
|
77
|
+
f"No migration found for v{version} to v{version + 1}. The installation may be corrupt."
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
new_path = path.parent / (path.name + ".new")
|
|
81
|
+
|
|
82
|
+
log.info("Migrating database v%d -> v%d...", version, version + 1)
|
|
83
|
+
migrations[version](str(path), str(new_path))
|
|
84
|
+
|
|
85
|
+
new_db = SqliteDatabase(str(new_path))
|
|
86
|
+
new_db.connect()
|
|
87
|
+
set_user_version(new_db, version + 1)
|
|
88
|
+
new_db.close()
|
|
89
|
+
|
|
90
|
+
backup_db(path, version, timestamp)
|
|
91
|
+
new_path.rename(path)
|
|
92
|
+
|
|
93
|
+
log.info("Migration to v%d complete.", version + 1)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def load_migrations() -> dict:
|
|
97
|
+
import cmdbox.migrations as migrations_pkg
|
|
98
|
+
|
|
99
|
+
migrations = {}
|
|
100
|
+
for _, name, _ in pkgutil.iter_modules(migrations_pkg.__path__):
|
|
101
|
+
if not (name.startswith("m") and name[1:].isdigit()):
|
|
102
|
+
continue
|
|
103
|
+
module = importlib.import_module(f"cmdbox.migrations.versions.{name}")
|
|
104
|
+
migrations[module.VERSION] = module.migrate
|
|
105
|
+
return migrations
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def get_current_version() -> int:
|
|
109
|
+
migrations = load_migrations()
|
|
110
|
+
return max(migrations.keys(), default=1)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def get_user_version(db: SqliteDatabase) -> int:
|
|
114
|
+
return db.execute_sql("PRAGMA user_version").fetchone()[0]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def set_user_version(db: SqliteDatabase, version: int) -> None:
|
|
118
|
+
db.execute_sql(f"PRAGMA user_version = {version}")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def backup_db(db_path: Path, version: int, timestamp: str) -> Path:
|
|
122
|
+
backup_dir = db_path.parent / "backups"
|
|
123
|
+
backup_dir.mkdir(exist_ok=True)
|
|
124
|
+
bak_path = backup_dir / f"{db_path.stem}_v{version}_{timestamp}.bak"
|
|
125
|
+
db_path.rename(bak_path)
|
|
126
|
+
log.debug("Backed up v%d database to %s", version, bak_path.name)
|
|
127
|
+
return bak_path
|
|
File without changes
|
cmdbox/models.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from peewee import (
|
|
4
|
+
Model,
|
|
5
|
+
CharField,
|
|
6
|
+
IntegerField,
|
|
7
|
+
DateTimeField,
|
|
8
|
+
TextField,
|
|
9
|
+
ForeignKeyField,
|
|
10
|
+
)
|
|
11
|
+
from cmdbox.database import db
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BaseModel(Model):
|
|
15
|
+
"""
|
|
16
|
+
BaseModel serves as the foundation for database models.
|
|
17
|
+
|
|
18
|
+
This class extends the 'Model' class from the Peewee library, providing a
|
|
19
|
+
base for all database models within the application. It centralizes the
|
|
20
|
+
database connection, ensuring consistency across all derived models.
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
Meta (class): Configuration for the database connection, linking models
|
|
24
|
+
to the shared database instance.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
class Meta:
|
|
28
|
+
database = db
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Command(BaseModel):
|
|
32
|
+
"""
|
|
33
|
+
Represents a command model.
|
|
34
|
+
|
|
35
|
+
This class provides a structure for storing and managing command-related data,
|
|
36
|
+
including an alias for the command, its template, timestamps for its creation
|
|
37
|
+
and last update, and a counter for how many times the command has been used.
|
|
38
|
+
|
|
39
|
+
Attributes:
|
|
40
|
+
alias (CharField): Unique identifier for the command.
|
|
41
|
+
template (CharField): The associated template string for the command.
|
|
42
|
+
description (CharField): A description of the command and what it does.
|
|
43
|
+
cwd (CharField): The current working directory for the command execution.
|
|
44
|
+
shell (CharField): The shell to use for command execution.
|
|
45
|
+
env (TextField): Environment variables to set for the command execution.
|
|
46
|
+
timeout (IntegerField): Number of seconds before the process is killed.
|
|
47
|
+
date_created (DateTimeField): Timestamp indicating when the command was created.
|
|
48
|
+
last_updated (DateTimeField): Timestamp indicating when the command was last updated.
|
|
49
|
+
used (IntegerField): Counter representing how many times the command has been used.
|
|
50
|
+
last_used (DateTimeField): Timestamp indicating when the command was last used.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
alias = CharField(unique=True)
|
|
54
|
+
template = TextField()
|
|
55
|
+
description = TextField(null=True, default=None)
|
|
56
|
+
cwd = CharField(null=True, default=None)
|
|
57
|
+
shell = CharField(null=True, default=None)
|
|
58
|
+
env = TextField(null=True, default=None)
|
|
59
|
+
timeout = IntegerField(null=True, default=None)
|
|
60
|
+
date_created = DateTimeField(default=datetime.now)
|
|
61
|
+
last_updated = DateTimeField(default=datetime.now)
|
|
62
|
+
used = IntegerField(default=0)
|
|
63
|
+
last_used = DateTimeField(null=True, default=None)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Variable(BaseModel):
|
|
67
|
+
"""
|
|
68
|
+
Represents a variable model.
|
|
69
|
+
|
|
70
|
+
A variable model is a key-value pair that can be stored and recalled in commands
|
|
71
|
+
and other variables.
|
|
72
|
+
|
|
73
|
+
Attributes:
|
|
74
|
+
name (CharField): The unique name of the configuration variable.
|
|
75
|
+
value (CharField): The value associated with the configuration variable.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
name = CharField(unique=True)
|
|
79
|
+
value = CharField()
|
|
80
|
+
date_created = DateTimeField(default=datetime.now)
|
|
81
|
+
last_updated = DateTimeField(default=datetime.now)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class Tag(BaseModel):
|
|
85
|
+
"""
|
|
86
|
+
Represents a Tag model with attributes for tagging, description, and timestamps.
|
|
87
|
+
|
|
88
|
+
This class is designed to store tag information as a name and description to allow
|
|
89
|
+
users to organize their commands and variables into categories and to give them a
|
|
90
|
+
convenient way to search for and filter them later.
|
|
91
|
+
|
|
92
|
+
Attributes:
|
|
93
|
+
name (CharField): A unique name identifier for the tag.
|
|
94
|
+
description (TextField): A detailed textual description of the tag.
|
|
95
|
+
date_created (DateTimeField): The timestamp indicating when the tag was created.
|
|
96
|
+
last_updated (DateTimeField): The timestamp indicating the last update for the tag.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
name = CharField(unique=True)
|
|
100
|
+
description = TextField(null=True, default=None)
|
|
101
|
+
date_created = DateTimeField(default=datetime.now)
|
|
102
|
+
last_updated = DateTimeField(default=datetime.now)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class CommandTag(BaseModel):
|
|
106
|
+
"""
|
|
107
|
+
Represents the relationship between commands and tags.
|
|
108
|
+
|
|
109
|
+
This class is used as an intermediary table to establish a many-to-many
|
|
110
|
+
relationship between a command and a tag.
|
|
111
|
+
|
|
112
|
+
Attributes:
|
|
113
|
+
command (ForeignKeyField): Refers to a command associated with a tag.
|
|
114
|
+
tag (ForeignKeyField): Refers to a tag associated with a command.
|
|
115
|
+
date_created (DateTimeField): DateTimeField indicating when the relationship was created.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
command = ForeignKeyField(Command, backref="tags", on_delete="CASCADE")
|
|
119
|
+
tag = ForeignKeyField(Tag, backref="commands", on_delete="CASCADE")
|
|
120
|
+
date_created = DateTimeField(default=datetime.now)
|
|
121
|
+
|
|
122
|
+
class Meta:
|
|
123
|
+
indexes = ((("command", "tag"), True),)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class VariableTag(BaseModel):
|
|
127
|
+
"""
|
|
128
|
+
Represents a many-to-many relationship between Variable and Tag models.
|
|
129
|
+
|
|
130
|
+
This class serves as an intermediary table to establish a many-to-many relationship
|
|
131
|
+
between a variable and a tag.
|
|
132
|
+
|
|
133
|
+
Attributes:
|
|
134
|
+
variable: (ForeignKeyField) Refers to a variable associated with a tag.
|
|
135
|
+
tag: (ForeignKeyField) Refers to a tag associated with a variable.
|
|
136
|
+
date_created (DateTimeField): DateTimeField indicating when the relationship was created.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
variable = ForeignKeyField(Variable, backref="tags", on_delete="CASCADE")
|
|
140
|
+
tag = ForeignKeyField(Tag, backref="variables", on_delete="CASCADE")
|
|
141
|
+
date_created = DateTimeField(default=datetime.now)
|
|
142
|
+
|
|
143
|
+
class Meta:
|
|
144
|
+
indexes = ((("variable", "tag"), True),)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class CommandHistory(BaseModel):
|
|
148
|
+
|
|
149
|
+
id = TextField(primary_key=True) # uuid4().hex - 32 char, no dashes
|
|
150
|
+
alias = CharField() # alias as invoked, not a FK
|
|
151
|
+
template = TextField() # raw template at time of run
|
|
152
|
+
resolved = TextField() # fully resolved command string
|
|
153
|
+
variables_used = TextField(null=True) # JSON: {"name": "Homer"} or null
|
|
154
|
+
exit_code = IntegerField(null=True)
|
|
155
|
+
ran_at = DateTimeField(default=datetime.now)
|
|
156
|
+
|
|
157
|
+
class Meta:
|
|
158
|
+
table_name = "command_history"
|
|
159
|
+
indexes = (
|
|
160
|
+
(("alias",), False),
|
|
161
|
+
(("ran_at",), False),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
ALL_MODELS = [Command, Variable, Tag, CommandTag, VariableTag, CommandHistory]
|
|
File without changes
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
from typing import Sequence, Generic, TypeVar, Type
|
|
2
|
+
|
|
3
|
+
from peewee import Model, fn, IntegrityError, Node
|
|
4
|
+
|
|
5
|
+
M = TypeVar("M", bound=Model)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BaseRepository(Generic[M]):
|
|
9
|
+
model: Type[M]
|
|
10
|
+
|
|
11
|
+
def _search(
|
|
12
|
+
self,
|
|
13
|
+
query: str,
|
|
14
|
+
secondary_ordering: str,
|
|
15
|
+
fields: str | Sequence[str] | None = None,
|
|
16
|
+
limit: int = 25,
|
|
17
|
+
) -> list[M]:
|
|
18
|
+
"""
|
|
19
|
+
Searches for commands matching the given query across specified fields.
|
|
20
|
+
|
|
21
|
+
This function performs a case-insensitive search for the query in the specified
|
|
22
|
+
fields of the Command model. It calculates a relevance score for each match
|
|
23
|
+
based on the position of the query within the fields and sorts the results by
|
|
24
|
+
relevance. Relevance is determined by a weighted score with the highest weight
|
|
25
|
+
given to most occurrences and the second part of the score given to how early
|
|
26
|
+
the search term occurs in the field.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
query (str): The search query to match in the specified fields.
|
|
30
|
+
secondary_ordering (str): The field to sort the results by, after relevance.
|
|
31
|
+
fields (str | Sequence[str] | None): The fields to search within. By
|
|
32
|
+
default, searches within "name" and "description". Can be a single field
|
|
33
|
+
name as a string or a sequence of field names.
|
|
34
|
+
limit (int): The maximum number of results to return. Default is 25.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
list[Command]: A list of Command objects matching the search query, sorted
|
|
38
|
+
by relevance.
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
ValueError: If any provided field does not exist on the Command model.
|
|
42
|
+
"""
|
|
43
|
+
if not query:
|
|
44
|
+
return []
|
|
45
|
+
if isinstance(fields, str):
|
|
46
|
+
fields = self._get_sequence(fields)
|
|
47
|
+
if fields is None or len(fields) == 0:
|
|
48
|
+
return []
|
|
49
|
+
or_clauses = []
|
|
50
|
+
relevance_parts = []
|
|
51
|
+
query_lower = query.lower()
|
|
52
|
+
for field_name in fields:
|
|
53
|
+
if not hasattr(self.model, field_name) or field_name == "":
|
|
54
|
+
raise ValueError(f"Invalid field: {field_name}")
|
|
55
|
+
field = getattr(self.model, field_name)
|
|
56
|
+
or_clauses.append(fn.LOWER(field).contains(query_lower))
|
|
57
|
+
first_pos = fn.INSTR(fn.lower(field), query_lower)
|
|
58
|
+
occurrences = (
|
|
59
|
+
fn.LENGTH(field) - fn.LENGTH(fn.REPLACE(field, query_lower, ""))
|
|
60
|
+
) / len(query_lower)
|
|
61
|
+
score = (occurrences * 1000) - first_pos
|
|
62
|
+
relevance_parts.append(score)
|
|
63
|
+
|
|
64
|
+
condition = or_clauses[0]
|
|
65
|
+
for clause in or_clauses[1:]:
|
|
66
|
+
condition |= clause
|
|
67
|
+
|
|
68
|
+
if len(relevance_parts) > 1:
|
|
69
|
+
relevance = fn.MIN(*relevance_parts).alias("relevance")
|
|
70
|
+
else:
|
|
71
|
+
relevance = relevance_parts[0].alias("relevance")
|
|
72
|
+
|
|
73
|
+
query_obj = (
|
|
74
|
+
self.model.select(self.model, relevance)
|
|
75
|
+
.where(condition)
|
|
76
|
+
.order_by(
|
|
77
|
+
relevance.desc(),
|
|
78
|
+
getattr(self.model, secondary_ordering),
|
|
79
|
+
)
|
|
80
|
+
.limit(limit)
|
|
81
|
+
)
|
|
82
|
+
return list(query_obj)
|
|
83
|
+
|
|
84
|
+
def _resolve_order_token(self, token: str) -> Node:
|
|
85
|
+
"""
|
|
86
|
+
Resolves the ordering token for determining the sorting order of a field.
|
|
87
|
+
|
|
88
|
+
This method takes an ordering token, identifies whether the sorting is
|
|
89
|
+
ascending or descending, and ensures the token corresponds to a valid
|
|
90
|
+
model attribute for sorting.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
token (str): The ordering token indicating the field for sorting. A
|
|
94
|
+
token starting with '-' indicates descending sort, otherwise it
|
|
95
|
+
is ascending. The token must correspond to a valid attribute
|
|
96
|
+
in `self.model`.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Model: An object representing the ordering of the specified field
|
|
100
|
+
in ascending or descending order.
|
|
101
|
+
|
|
102
|
+
Raises:
|
|
103
|
+
ValueError: If the token is empty or if it references an invalid
|
|
104
|
+
field in the model.
|
|
105
|
+
"""
|
|
106
|
+
if not token:
|
|
107
|
+
raise ValueError("Empty order_by token.")
|
|
108
|
+
desc = token.startswith("-")
|
|
109
|
+
field_name = token[1:] if desc else token
|
|
110
|
+
try:
|
|
111
|
+
field = getattr(self.model, field_name)
|
|
112
|
+
except AttributeError:
|
|
113
|
+
raise ValueError(f"Invalid order_by field: {token}")
|
|
114
|
+
return field.desc() if desc else field.asc()
|
|
115
|
+
|
|
116
|
+
def _resolve_ordering(self, order_by: str | Sequence[str]) -> Sequence[Node]:
|
|
117
|
+
"""
|
|
118
|
+
Resolves ordering tokens based on the specified order_by input.
|
|
119
|
+
|
|
120
|
+
This method processes the input order_by parameter, which could either
|
|
121
|
+
be a single string of comma-separated tokens or a sequence of strings.
|
|
122
|
+
It ensures the tokens are parsed and resolved appropriately, returning a
|
|
123
|
+
sequence of resolved models.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
order_by: A string containing comma-separated tokens, or a sequence
|
|
127
|
+
of strings representing ordering keys.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Sequence[Model]: A sequence of resolved models based on the specified
|
|
131
|
+
ordering tokens.
|
|
132
|
+
|
|
133
|
+
Raises:
|
|
134
|
+
ValueError: If the order_by sequence is empty.
|
|
135
|
+
"""
|
|
136
|
+
tokens = self._get_sequence(order_by)
|
|
137
|
+
if not tokens:
|
|
138
|
+
raise ValueError("Empty order_by sequence.")
|
|
139
|
+
return [self._resolve_order_token(token) for token in tokens]
|
|
140
|
+
|
|
141
|
+
def _get_sequence(self, items: str | Sequence[str]) -> list[str]:
|
|
142
|
+
"""
|
|
143
|
+
Processes a string or sequence of strings into a list of stripped strings.
|
|
144
|
+
|
|
145
|
+
If the input is a single string, it is split by commas, and whitespace around each
|
|
146
|
+
split item is stripped. If the input is already a sequence of strings, it is converted
|
|
147
|
+
into a list without any modifications.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
items (str | Sequence[str]): A string to be split into a list of strings or an
|
|
151
|
+
existing sequence of strings.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
list[str]: A list of processed strings.
|
|
155
|
+
|
|
156
|
+
Raises:
|
|
157
|
+
ValueError: If the input is neither a string nor a sequence of strings.
|
|
158
|
+
"""
|
|
159
|
+
if isinstance(items, str):
|
|
160
|
+
return [item.strip() for item in items.split(",") if item.strip()]
|
|
161
|
+
else:
|
|
162
|
+
return [item.strip() for item in items if item.strip()]
|
|
163
|
+
|
|
164
|
+
def _is_unique_name_violation(self, exc: IntegrityError) -> bool:
|
|
165
|
+
"""
|
|
166
|
+
Checks if the exception indicates a unique constraint violation.
|
|
167
|
+
|
|
168
|
+
This method analyzes the provided IntegrityError to determine if the
|
|
169
|
+
error was caused by a UNIQUE constraint violation on a field of the
|
|
170
|
+
database table associated with the model.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
exc (IntegrityError): The IntegrityError exception to be checked.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
bool: True if the exception indicates a unique constraint violation;
|
|
177
|
+
otherwise, False.
|
|
178
|
+
"""
|
|
179
|
+
msg = str(exc)
|
|
180
|
+
table = self.model._meta.table_name
|
|
181
|
+
return "UNIQUE constraint failed" in msg and f"{table}.name" in msg
|