Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/basic-command-line-interface-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,9 @@ You only need to re-add those `output.xml` files (with the correct `-z`/`--timez
robotdashboard -r index=0,index=1:4;9,index=10
robotdashboard --removeruns 'run_start=2024-07-30 15:27:20.184407,index=20'
robotdashboard -r alias=some_cool_alias,tag=prod,tag=dev -r alias=alias12345
robotdashboard -r limit=10
robotdashboard -r limit=10
# Log data of removed runs in jsonl
robotdashboard -r limit=10 --log-removed-runs "/myLogDir/removedRuns.jsonl"
```
- Optional: `-r` or `--removeruns` specifies one or more runs to remove.
- Multiple values are separated by commas (,).
Expand Down
15 changes: 15 additions & 0 deletions robotframework_dashboard/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,20 @@ def _parse_arguments(self):
nargs="*",
default=None,
)
db_group.add_argument(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency again I think it would be good to make it similar to other CLI arguments and apart from my 1 mistake with the ssl arguments I never use any '-' dashes. So maybe it would be better to not do it here either: logremovedruns or with my comment from below logdata, or even logremoveddata I'm not sure what a good name would be, open to suggestions here.

Surprisingly AI had a good suggestion for this: --logremoved, which I think is quite clear in it's function and is still readable!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you and your AI colleague, I didn't like the flag name either. I will change this.
(Btw it will probably be at the very least until next week wednesday until I have this PR ready)

"--log-removed-runs",
metavar="PATH",
help=(
"Log (append) run data to jsonl before removing from the db\n"
" • Supply a path to the desired .jsonl\n"
"Examples:\n"
" • '--log-removed-runs /tmp/removed_runs.jsonl'\n"
),
action="store",
type=str,
default=None,
dest="run_rm_log_path",
)
db_group.add_argument(
"-c",
"--databaseclass",
Expand Down Expand Up @@ -637,5 +651,6 @@ def _process_arguments(self, arguments):
"ssl_certfile": ssl_certfile,
"ssl_keyfile": ssl_keyfile,
"log_url": arguments.logurl,
"run_rm_log_path": arguments.run_rm_log_path,
}
return dotdict(provided_args)
40 changes: 32 additions & 8 deletions robotframework_dashboard/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from time import time
from datetime import datetime, timezone
from typing import Union
import json

# Explicit adapter for datetime -> ISO string, replacing the deprecated default
# behaviour removed in Python 3.12+. Compatible with Python 3.8+.
Expand All @@ -13,14 +14,15 @@


class DatabaseProcessor(AbstractDatabaseProcessor):
def __init__(self, database_path: Path):
def __init__(self, database_path: Path, run_rm_log_path=None):
"""This function should handle the connection to the database
And if required the creation of the tables"""
self.database_path = database_path
# handle possible subdirectories before creating database with sqlite
path = Path(self.database_path)
path.parent.mkdir(exist_ok=True, parents=True)
self.connection: sqlite3.Connection
self.run_rm_log_path = run_rm_log_path
# create tables if required
self.open_database()
self._create_tables()
Expand Down Expand Up @@ -383,6 +385,24 @@ def _get_run_paths(self):
run_paths[entry["run_start"]] = entry.get("path") or ""
return run_paths

def _get_run_data(self, run_start):
"""Helper function to get dict of a run """
cursor = self.connection.cursor()
cursor.execute(GET_RUN_INFO_BY_RUN_START, (run_start,))

row = cursor.fetchone()
if not row:
print(f"Could not find run with run_start {run_start}")
return None

columns = [col[0] for col in cursor.description]
return dict(zip(columns, row))

def _log_run_jsonl(self, logpath, run_data):
""" Logs json of a run row to logpath """
with Path(logpath).open("a", encoding="utf-8") as f:
_ = f.write(json.dumps(run_data, default=str) + "\n")

def remove_runs(self, remove_runs: list):
"""This function removes all provided runs and all their corresponding data"""
run_starts, run_names, run_aliases, run_tags, _ = self._get_runs()
Expand Down Expand Up @@ -492,13 +512,17 @@ def _remove_by_limit(self, run: str, run_starts: list):

def _remove_run(self, run_start: str):
"""Helper function to remove the data from all tables"""
self.connection.cursor().execute(DELETE_FROM_RUNS.format(run_start=run_start))
self.connection.cursor().execute(DELETE_FROM_SUITES.format(run_start=run_start))
self.connection.cursor().execute(DELETE_FROM_TESTS.format(run_start=run_start))
self.connection.cursor().execute(
DELETE_FROM_KEYWORDS.format(run_start=run_start)
)
self.connection.commit()
# grab data of the run for later to log in jsonl if flag set
run_data = self._get_run_data(run_start) if self.run_rm_log_path else None
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm just wondering if this is the nicest solution. Here you implement it only for the runs, but what if people want to use other data than the run data, like test/keyword/suite data?

Isn't it a good idea to add a flag for the datatypes you want to write to a file?
I'm just thinking about how that could work in the CLI, since that would require 2 arguments, a path and a folder.

Maybe a nice addition could be to just add a default .jsonl path, like removed_data.jsonl, in which we can then add the data like:

{ run: "...", suite: "...", test: "...", keyword: "..."}
{ run: "...", suite: "...", test: "...", keyword: "..."}
{ run: "...", suite: "...", test: "...", keyword: "..."}

Then in the CLI maybe we could do something like mapping the values all, run, suite, test and keyword combined with a possible path, or a default if not provided. And if no argument is provided at all just use all and a set filename in the root folder.

--log-removed-runs        # no argument, assume all datatypes + default .jsonl file: `removed_data.jsonl`
--log-removed-runs  all          # assume default path again but for only run data
--log-removed-runs  custom/path/to/file.jsonl         # assume all
--log-removed-runs  "run:path/to/custom file.jsonl"
--log-removed-runs  "run:test:suite:keyword:path/to/custom file.jsonl"
--log-removed-runs  test:keyword

This is quite a change in the implementation, but I think this would just be a lot more flexible for future use instead of only making it for the run data. I do recognize that this could also make the .jsonl a lot bigger but that is up to the user then.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That could be a nice addition for some people, I will change it, thank you.

--log-removed-runs all # assume default path again but for only run data

did you mean to type runs instead of all there? (and I would name the flag --logremoved if thats okay?)

Copy link
Copy Markdown
Collaborator

@timdegroot1996 timdegroot1996 May 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah the flag updated comment I made later, but obviously that's the better name!

did you mean to type runs instead of all there?

In that example I did actually mean all, since that's a valid option flag that I would suggest. My suggestion is pretty much the following:

  1. Name it logremoved
  2. This cli take 1 optional input
  3. When the input is not provided, assume the option is all so log all data types, and use a default .jsonl file path in the root
  4. When an option is provided it should be one of the following:
    • Data type(s), split by a colon
    • File path, split by a colon
    • Valid data types: all, run, suite, test, keyword
    • Assume anything else is a file path

So valid examples would be (but there could obviously be more different combinations):

--logremoved
--logremoved run:keyword:my/path/to.jsonl
--logremoved my/path/to.jsonl
--logremoved all
--logremoved run:suite

Does this make sense?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay yeah I read too fast >.>

You meant this line:

--log-removed-runs all # assume default path again but for only run data

And obviously I meant to type run 😅
Although both could be valid:

--logremoved all           # assume default path for all data
--logremoved run         # assume default path but only run data

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for looking it over and the quick answer!
Makes sense, I will implement it that way and get back to you when its done!
Have a nice weekend!

with self.connection:
cursor = self.connection.cursor()
cursor.execute(DELETE_FROM_RUNS.format(run_start=run_start))
if cursor.rowcount > 0: # only cleanup related tables if a run would be deleted
cursor.execute(DELETE_FROM_SUITES.format(run_start=run_start))
cursor.execute(DELETE_FROM_TESTS.format(run_start=run_start))
cursor.execute(DELETE_FROM_KEYWORDS.format(run_start=run_start))
if self.run_rm_log_path and run_data:
self._log_run_jsonl(self.run_rm_log_path, run_data)

def vacuum_database(self):
"""This function vacuums the database to reduce the size after removing runs"""
Expand Down
1 change: 1 addition & 0 deletions robotframework_dashboard/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def main():
arguments.timezone,
arguments.log_url,
arguments.custom_filters,
arguments.run_rm_log_path,
)
# If arguments.start_server is provided override some required args
if arguments.start_server:
Expand Down
2 changes: 2 additions & 0 deletions robotframework_dashboard/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,5 @@
UPDATE_RUN_PATH = """ UPDATE runs SET path="{path}" WHERE run_start="{run_start}" """

VACUUM_DATABASE = """ VACUUM """

GET_RUN_INFO_BY_RUN_START = """ SELECT * FROM runs WHERE run_start = ? """
11 changes: 10 additions & 1 deletion robotframework_dashboard/robotdashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def __init__(
timezone: str = "",
log_url: Optional[str] = None,
custom_filters: str = "",
run_rm_log_path: Optional[str] = None,
):
"""Sets the parameters provided in the command line"""
self.database_path = database_path
Expand All @@ -57,6 +58,7 @@ def __init__(
self.timezone = timezone
self.log_url = log_url
self.custom_filters = custom_filters
self.run_rm_log_path = run_rm_log_path

def initialize_database(self, suppress=True):
"""Function that initializes the database if it does not exist
Expand All @@ -66,7 +68,7 @@ def initialize_database(self, suppress=True):
if not suppress:
console += self._print_console(f" 1. Database preparation")
if not self.database_class:
self.database = DatabaseProcessor(self.database_path)
self.database = DatabaseProcessor(self.database_path, run_rm_log_path=self.run_rm_log_path)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency let's just do self.run_rm_log_path here instead of the run_rm_log_path=self.run_rm_log_path, or update the other one, I don't have a preference as long as it's the same.

else:
if not suppress:
console += self._print_console(
Expand All @@ -80,6 +82,13 @@ def initialize_database(self, suppress=True):
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
self.database = module.DatabaseProcessor(self.database_path)
is_rm_log_unsupported = not hasattr(self.database, "run_rm_log_path")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you add this logic? I'm curious when this can actually happen? Because if the flag exists, the logic should be correct. If it does not exist the flag will fail when tried in the CLI right?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah it just landed that this could be for custom database class implementations maybe?

Copy link
Copy Markdown
Contributor Author

@HuntTheSun HuntTheSun May 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that is exactly what its for.
It justs prints a warning if their custom db doesn't implement the attribute "run_rm_log_path" and they use the flag. setattr prevents an AttributeError if run_rm_log_path is accessed but not present on the custom db. I think I check if it exists before using, but I just want to make sure if any changes are made I don't introduce random crashes on custom db
At least I hope that is what will happen, I will have to check, I haven't yet played around with custom db.

setattr(self.database, "run_rm_log_path", self.run_rm_log_path)
if is_rm_log_unsupported and self.run_rm_log_path and not suppress:
console += self._print_console(
" WARNING The custom database class does not explicitly support '--log-removed-runs'. "
"Logging might be skipped if the custom class overrides the deletion logic."
)
if not suppress:
console += self._print_console(
f" created database: '{self.database_path}'"
Expand Down
1 change: 1 addition & 0 deletions tests/python/test_arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ def _make_namespace(**kwargs):
"ssl_keyfile": None,
"logurl": None,
"custom_filters": None,
"run_rm_log_path": None,
}
defaults.update(kwargs)
return argparse.Namespace(**defaults)
Expand Down
Loading