mirror of https://gitlab.com/KKlochko/tui-rsync
Compare commits
No commits in common. '10114da0eebaf2e925b73d5f334feeb5f56fd43d' and 'f2e70ccc85fec6299a6fad734331defbb5deebcc' have entirely different histories.
10114da0ee
...
f2e70ccc85
@ -1,181 +0,0 @@
|
|||||||
pyrightconfig.json
|
|
||||||
*~
|
|
||||||
.\#*
|
|
||||||
\#*\#
|
|
||||||
.*.~undo-tree~
|
|
||||||
*.db
|
|
||||||
|
|
||||||
# Created by https://www.toptal.com/developers/gitignore/api/python
|
|
||||||
# Edit at https://www.toptal.com/developers/gitignore?templates=python
|
|
||||||
|
|
||||||
### Python ###
|
|
||||||
# Byte-compiled / optimized / DLL files
|
|
||||||
__pycache__/
|
|
||||||
*.py[cod]
|
|
||||||
*$py.class
|
|
||||||
|
|
||||||
# C extensions
|
|
||||||
*.so
|
|
||||||
|
|
||||||
# Distribution / packaging
|
|
||||||
.Python
|
|
||||||
build/
|
|
||||||
develop-eggs/
|
|
||||||
dist/
|
|
||||||
downloads/
|
|
||||||
eggs/
|
|
||||||
.eggs/
|
|
||||||
lib/
|
|
||||||
lib64/
|
|
||||||
parts/
|
|
||||||
sdist/
|
|
||||||
var/
|
|
||||||
wheels/
|
|
||||||
share/python-wheels/
|
|
||||||
*.egg-info/
|
|
||||||
.installed.cfg
|
|
||||||
*.egg
|
|
||||||
MANIFEST
|
|
||||||
|
|
||||||
# PyInstaller
|
|
||||||
# Usually these files are written by a python script from a template
|
|
||||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
||||||
*.manifest
|
|
||||||
*.spec
|
|
||||||
|
|
||||||
# Installer logs
|
|
||||||
pip-log.txt
|
|
||||||
pip-delete-this-directory.txt
|
|
||||||
|
|
||||||
# Unit test / coverage reports
|
|
||||||
htmlcov/
|
|
||||||
.tox/
|
|
||||||
.nox/
|
|
||||||
.coverage
|
|
||||||
.coverage.*
|
|
||||||
.cache
|
|
||||||
nosetests.xml
|
|
||||||
coverage.xml
|
|
||||||
*.cover
|
|
||||||
*.py,cover
|
|
||||||
.hypothesis/
|
|
||||||
.pytest_cache/
|
|
||||||
cover/
|
|
||||||
|
|
||||||
# Translations
|
|
||||||
*.mo
|
|
||||||
*.pot
|
|
||||||
|
|
||||||
# Django stuff:
|
|
||||||
*.log
|
|
||||||
local_settings.py
|
|
||||||
db.sqlite3
|
|
||||||
db.sqlite3-journal
|
|
||||||
|
|
||||||
# Flask stuff:
|
|
||||||
instance/
|
|
||||||
.webassets-cache
|
|
||||||
|
|
||||||
# Scrapy stuff:
|
|
||||||
.scrapy
|
|
||||||
|
|
||||||
# Sphinx documentation
|
|
||||||
docs/_build/
|
|
||||||
|
|
||||||
# PyBuilder
|
|
||||||
.pybuilder/
|
|
||||||
target/
|
|
||||||
|
|
||||||
# Jupyter Notebook
|
|
||||||
.ipynb_checkpoints
|
|
||||||
|
|
||||||
# IPython
|
|
||||||
profile_default/
|
|
||||||
ipython_config.py
|
|
||||||
|
|
||||||
# pyenv
|
|
||||||
# For a library or package, you might want to ignore these files since the code is
|
|
||||||
# intended to run in multiple environments; otherwise, check them in:
|
|
||||||
# .python-version
|
|
||||||
|
|
||||||
# pipenv
|
|
||||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
||||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
||||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
||||||
# install all needed dependencies.
|
|
||||||
#Pipfile.lock
|
|
||||||
|
|
||||||
# poetry
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
||||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
||||||
# commonly ignored for libraries.
|
|
||||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
||||||
#poetry.lock
|
|
||||||
|
|
||||||
# pdm
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
||||||
#pdm.lock
|
|
||||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
||||||
# in version control.
|
|
||||||
# https://pdm.fming.dev/#use-with-ide
|
|
||||||
.pdm.toml
|
|
||||||
|
|
||||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
||||||
__pypackages__/
|
|
||||||
|
|
||||||
# Celery stuff
|
|
||||||
celerybeat-schedule
|
|
||||||
celerybeat.pid
|
|
||||||
|
|
||||||
# SageMath parsed files
|
|
||||||
*.sage.py
|
|
||||||
|
|
||||||
# Environments
|
|
||||||
.env
|
|
||||||
.venv
|
|
||||||
env/
|
|
||||||
venv/
|
|
||||||
ENV/
|
|
||||||
env.bak/
|
|
||||||
venv.bak/
|
|
||||||
|
|
||||||
# Spyder project settings
|
|
||||||
.spyderproject
|
|
||||||
.spyproject
|
|
||||||
|
|
||||||
# Rope project settings
|
|
||||||
.ropeproject
|
|
||||||
|
|
||||||
# mkdocs documentation
|
|
||||||
/site
|
|
||||||
|
|
||||||
# mypy
|
|
||||||
.mypy_cache/
|
|
||||||
.dmypy.json
|
|
||||||
dmypy.json
|
|
||||||
|
|
||||||
# Pyre type checker
|
|
||||||
.pyre/
|
|
||||||
|
|
||||||
# pytype static type analyzer
|
|
||||||
.pytype/
|
|
||||||
|
|
||||||
# Cython debug symbols
|
|
||||||
cython_debug/
|
|
||||||
|
|
||||||
# PyCharm
|
|
||||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
||||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
||||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
||||||
#.idea/
|
|
||||||
|
|
||||||
### Python Patch ###
|
|
||||||
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
|
|
||||||
poetry.toml
|
|
||||||
|
|
||||||
# ruff
|
|
||||||
.ruff_cache/
|
|
||||||
|
|
||||||
# End of https://www.toptal.com/developers/gitignore/api/python
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
|||||||
Feature: Creating a backup plan with the CLI
|
|
||||||
|
|
||||||
@fixture.injector
|
|
||||||
@fixture.in_memory_database
|
|
||||||
@fixture.cli
|
|
||||||
Scenario Outline: Create an new unique backup plan with the CLI
|
|
||||||
Given the CLI arguments are "<arguments>"
|
|
||||||
When I run the CLI
|
|
||||||
Then the CLI executed with "success"
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
| arguments |
|
|
||||||
| plans add --label my_label --source /mnt -d /mnt2 -d /mnt3 |
|
|
||||||
| plans add --label label2 --source /mnt -d /mnt2 -d /mnt3 |
|
|
||||||
|
|
||||||
@fixture.injector
|
|
||||||
@fixture.in_memory_database
|
|
||||||
@fixture.cli
|
|
||||||
Scenario Outline: Response of creating an new unique backup plan with the CLI
|
|
||||||
Given the CLI arguments are "<arguments>"
|
|
||||||
When I run the CLI
|
|
||||||
Then the CLI executed with "<result>"
|
|
||||||
And the CLI output contains "<result_message>"
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
| arguments | result | result_message | description |
|
|
||||||
| plans add --label my_label --source /mnt -d /mnt2 -d /mnt3 | success | The backup plan is added. | add a plan |
|
|
||||||
|
|
@ -1,72 +0,0 @@
|
|||||||
Feature: Deleting a backup plan with the CLI
|
|
||||||
|
|
||||||
@fixture.injector
|
|
||||||
@fixture.in_memory_database
|
|
||||||
@fixture.seeds
|
|
||||||
@fixture.cli
|
|
||||||
Scenario Outline: Deleting a backup plan with the CLI
|
|
||||||
Given the CLI arguments are "<arguments>"
|
|
||||||
And I have a backup plan with id="<existing_backup_plan_id>"
|
|
||||||
When I run the CLI
|
|
||||||
Then the CLI executed with "<result>"
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
| arguments | existing_backup_plan_id | result | description |
|
|
||||||
| plans remove one -i 8aa59e7e-dc75-459b-beb5-b710b39be583 | 8aa59e7e-dc75-459b-beb5-b710b39be583 | success | delete an existing plan |
|
|
||||||
| plans remove one -i 8aa59e7e-dc75-459b-aeb5-b710b39be583 | 8aa59e7e-dc75-459b-beb5-b710b39be512 | error | delete a non-existing plan |
|
|
||||||
|
|
||||||
@fixture.injector
|
|
||||||
@fixture.in_memory_database
|
|
||||||
@fixture.cli
|
|
||||||
Scenario Outline: Response of deleting non-existing backup plan with the CLI
|
|
||||||
Given the CLI arguments are "<arguments>"
|
|
||||||
When I run the CLI
|
|
||||||
Then the CLI executed with "<result>"
|
|
||||||
And the CLI contains the error: "<result_error>"
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
| arguments | result | result_error | description |
|
|
||||||
| plans remove one -i 8aa59e7e-dc75-459b-aeb5-b710b39be583 | error | [ERROR] Failed to delete the backup plan, because it doesn't exist. | delete a non-existing plan |
|
|
||||||
|
|
||||||
@fixture.injector
|
|
||||||
@fixture.in_memory_database
|
|
||||||
@fixture.seeds
|
|
||||||
@fixture.cli
|
|
||||||
Scenario Outline: Response of deleting existing backup plans with the CLI
|
|
||||||
Given the CLI arguments are "<arguments>"
|
|
||||||
And I have a backup plan with id="<existing_backup_plan_id>"
|
|
||||||
When I run the CLI
|
|
||||||
Then the CLI executed with "<result>"
|
|
||||||
And the CLI output contains "<result_message>"
|
|
||||||
And the CLI output contains "<existing_backup_plan_id>"
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
| arguments | existing_backup_plan_id | result | result_message | description |
|
|
||||||
| plans remove one -i 8aa59e7e-dc75-459b-aeb5-b710b39be583 | 8aa59e7e-dc75-459b-aeb5-b710b39be583 | success | Removed the backup plan with | delete the backup plan |
|
|
||||||
|
|
||||||
@fixture.injector
|
|
||||||
@fixture.in_memory_database
|
|
||||||
@fixture.cli
|
|
||||||
Scenario Outline: Response of deleting no backup plans with the CLI
|
|
||||||
Given the CLI arguments are "<arguments>"
|
|
||||||
When I run the CLI
|
|
||||||
Then the CLI executed with "<result>"
|
|
||||||
And the CLI output contains "<result_message>"
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
| arguments | result | result_message | description |
|
|
||||||
| plans remove all | success | Nothing to remove. No backup plans. | delete no plans |
|
|
||||||
|
|
||||||
@fixture.injector
|
|
||||||
@fixture.in_memory_database
|
|
||||||
@fixture.seeds
|
|
||||||
@fixture.cli
|
|
||||||
Scenario Outline: Response of deleting all backup plans with the CLI
|
|
||||||
Given the CLI arguments are "<arguments>"
|
|
||||||
When I run the CLI
|
|
||||||
Then the CLI executed with "<result>"
|
|
||||||
And the CLI output contains "<result_message>"
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
| arguments | result | result_message | description |
|
|
||||||
| plans remove all | success | All backup plans removed. | delete all backup plans |
|
|
@ -1,75 +0,0 @@
|
|||||||
Feature: Show a backup plan with the CLI
|
|
||||||
|
|
||||||
@fixture.injector
|
|
||||||
@fixture.in_memory_database
|
|
||||||
@fixture.seeds
|
|
||||||
@fixture.cli
|
|
||||||
Scenario Outline: Show a backup plan with the CLI
|
|
||||||
Given the CLI arguments are "<arguments>"
|
|
||||||
And I have a backup plan with id="<existing_backup_plan_id>"
|
|
||||||
When I run the CLI
|
|
||||||
Then the CLI executed with "<result>"
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
| arguments | existing_backup_plan_id | result | description |
|
|
||||||
| plans show one -i 8aa59e7e-dc75-459b-beb5-b710b39be583 | 8aa59e7e-dc75-459b-beb5-b710b39be583 | success | show an existing plan |
|
|
||||||
| plans show one -i 8aa59e7e-dc75-459b-aeb5-b710b39be583 | 8aa59e7e-dc75-459b-beb5-b710b39be512 | error | show a non-existing plan |
|
|
||||||
|
|
||||||
@fixture.injector
|
|
||||||
@fixture.in_memory_database
|
|
||||||
@fixture.seeds
|
|
||||||
@fixture.cli
|
|
||||||
Scenario Outline: Response of showing existing backup plans with the CLI
|
|
||||||
Given the CLI arguments are "<arguments>"
|
|
||||||
And I have a backup plan with id="<existing_backup_plan_id>"
|
|
||||||
When I run the CLI
|
|
||||||
Then the CLI executed with "<result>"
|
|
||||||
And the CLI output doesn't contains "<result_message>"
|
|
||||||
And the CLI output contains "<existing_backup_plan_id>"
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
| arguments | existing_backup_plan_id | result | result_message | description |
|
|
||||||
| plans show one -i 8aa59e7e-dc75-459b-aeb5-b710b39be583 | 8aa59e7e-dc75-459b-aeb5-b710b39be583 | success | Removed the backup plan with | delete the backup plan |
|
|
||||||
|
|
||||||
@fixture.injector
|
|
||||||
@fixture.in_memory_database
|
|
||||||
@fixture.cli
|
|
||||||
Scenario Outline: Response of showing non-existing backup plans with the CLI
|
|
||||||
Given the CLI arguments are "<arguments>"
|
|
||||||
When I run the CLI
|
|
||||||
Then the CLI executed with "<result>"
|
|
||||||
And the CLI contains the error: "<result_error>"
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
| arguments | result | result_error | description |
|
|
||||||
| plans show one -i 8aa59e7e-dc75-459b-aeb5-b710b39be583 | error | [ERROR] The backup plan was not found. | show a non-existing plan |
|
|
||||||
|
|
||||||
@fixture.injector
|
|
||||||
@fixture.in_memory_database
|
|
||||||
@fixture.cli
|
|
||||||
Scenario Outline: Response of showing no backup plans with the CLI
|
|
||||||
Given the CLI arguments are "<arguments>"
|
|
||||||
When I run the CLI
|
|
||||||
Then the CLI executed with "<result>"
|
|
||||||
And the CLI output contains "<result_message>"
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
| arguments | result | result_message | description |
|
|
||||||
| plans show all | success | No backup plans. | shows no plans |
|
|
||||||
|
|
||||||
@fixture.injector
|
|
||||||
@fixture.in_memory_database
|
|
||||||
@fixture.seeds
|
|
||||||
@fixture.cli
|
|
||||||
Scenario Outline: Response of showing backup plans with the CLI
|
|
||||||
Given the CLI arguments are "<arguments>"
|
|
||||||
And I have a backup plan with id="8aa59e7e-dc75-459b-beb5-b710b39be583"
|
|
||||||
When I run the CLI
|
|
||||||
Then the CLI executed with "<result>"
|
|
||||||
And the CLI output contains "8aa59e7e-dc75-459b-beb5-b710b39be583"
|
|
||||||
And the CLI output doesn't contains "<no_message>"
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
| arguments | result | no_message | description |
|
|
||||||
| plans show all | success | No backup plans. | shows plans |
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
|||||||
Feature: Update a backup plan with the CLI
|
|
||||||
|
|
||||||
@fixture.injector
|
|
||||||
@fixture.in_memory_database
|
|
||||||
@fixture.seeds
|
|
||||||
@fixture.cli
|
|
||||||
Scenario Outline: Replace a backup plan with the CLI
|
|
||||||
Given the CLI arguments are "<arguments>"
|
|
||||||
And I have a backup plan with id="<existing_backup_plan_id>"
|
|
||||||
When I run the CLI
|
|
||||||
Then the CLI executed with "<result>"
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
| arguments | existing_backup_plan_id | result | description |
|
|
||||||
| plans update replace -i 8aa59e7e-dc75-459b-beb5-b710b39be583 -l test -s /mnt -d /mnt2 -d /mnt3 | 8aa59e7e-dc75-459b-beb5-b710b39be583 | success | replace an existing plan |
|
|
||||||
| plans update replace -i 8aa59e7e-dc75-459b-beb5-b710b39be583 -l test -s /mnt -d /mnt2 -d /mnt3 | 8aa59e7e-dc75-459b-beb5-b710b39be512 | error | replace a non-existing plan |
|
|
||||||
|
|
||||||
@fixture.injector
|
|
||||||
@fixture.in_memory_database
|
|
||||||
@fixture.cli
|
|
||||||
Scenario Outline: Response of replacing non-existing backup plans with the CLI
|
|
||||||
Given the CLI arguments are "<arguments>"
|
|
||||||
When I run the CLI
|
|
||||||
Then the CLI executed with "<result>"
|
|
||||||
And the CLI contains the error: "<result_error>"
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
| arguments | result | result_error | description |
|
|
||||||
| plans update replace -i 8aa59e7e-dc75-459b-beb5-b710b39be583 -l test -s /mnt -d /mnt2 -d /mnt3 | error | [ERROR] The backup plan was not found. | replace a non-existing plan |
|
|
||||||
|
|
||||||
@fixture.injector
|
|
||||||
@fixture.in_memory_database
|
|
||||||
@fixture.seeds
|
|
||||||
@fixture.cli
|
|
||||||
Scenario Outline: Response of replacing an existing backup plans with the CLI
|
|
||||||
Given the CLI arguments are "<arguments>"
|
|
||||||
And I have a backup plan with id="<existing_backup_plan_id>"
|
|
||||||
When I run the CLI
|
|
||||||
Then the CLI executed with "<result>"
|
|
||||||
And the CLI output contains "<result_message>"
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
| arguments | existing_backup_plan_id | result | result_message | description |
|
|
||||||
| plans update replace -i 8aa59e7e-dc75-459b-beb5-b710b39be583 -l test -s /mnt -d /mnt2 -d /mnt3 | 8aa59e7e-dc75-459b-beb5-b710b39be583 | success | The backup plan updated. | replace an existing plan |
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
|||||||
Feature: Creating a backup plan
|
|
||||||
|
|
||||||
@fixture.injector
|
|
||||||
@fixture.in_memory_database
|
|
||||||
@fixture.backup_plan_service
|
|
||||||
Scenario Outline: Create an new unique backup plan
|
|
||||||
Given the label "<label>"
|
|
||||||
And the path "<source_path>"
|
|
||||||
And the destinations <destinations>
|
|
||||||
When I create the backup plan
|
|
||||||
Then it should be created successfully
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
| label | source_path | destinations |
|
|
||||||
| usb | /mnt/usb | [] |
|
|
||||||
| db | /db | ["/backup/db"] |
|
|
||||||
| temp | /tmp | ["/backup/tmp1", "/backup/tmp2"] |
|
|
@ -1,28 +0,0 @@
|
|||||||
Feature: Delete a backup plan
|
|
||||||
|
|
||||||
@fixture.injector
|
|
||||||
@fixture.in_memory_database
|
|
||||||
@fixture.seeds
|
|
||||||
Scenario Outline: Deleting a backup plan
|
|
||||||
Given I have a backup plan with id="<existing_backup_plan_id>"
|
|
||||||
When I remove the backup plan with id="<backup_plan_id>"
|
|
||||||
Then the result should be "<result>"
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
| backup_plan_id | existing_backup_plan_id | result | description |
|
|
||||||
| 8aa59e7e-dc75-459b-beb5-b710b39be583 | 8aa59e7e-dc75-459b-beb5-b710b39be583 | success | delete an existing plan |
|
|
||||||
| 8aa59e7e-dc75-459b-aeb5-b710b39be583 | 8aa59e7e-dc75-459b-beb5-b710b39be512 | error | delete a non-existing plan |
|
|
||||||
|
|
||||||
@fixture.injector
|
|
||||||
@fixture.in_memory_database
|
|
||||||
Scenario: Delete no backup plans
|
|
||||||
When I remove all backup plans
|
|
||||||
Then the result value should be "False"
|
|
||||||
|
|
||||||
@fixture.injector
|
|
||||||
@fixture.in_memory_database
|
|
||||||
@fixture.seeds
|
|
||||||
Scenario: Delete all backup plans
|
|
||||||
Given I have a backup plan with id="8aa59e7e-dc75-459b-beb5-b710b39be583"
|
|
||||||
When I remove all backup plans
|
|
||||||
Then the result value should be "True"
|
|
@ -1,14 +0,0 @@
|
|||||||
Feature: Read a backup plan by an id
|
|
||||||
|
|
||||||
@fixture.injector
|
|
||||||
@fixture.in_memory_database
|
|
||||||
@fixture.seeds
|
|
||||||
Scenario Outline: Read a backup plan with the CLI
|
|
||||||
Given I have a backup plan with id="<existing_backup_plan_id>"
|
|
||||||
When I read the backup plan with id="<backup_plan_id>"
|
|
||||||
Then the result should be "<result>"
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
| backup_plan_id | existing_backup_plan_id | result | description |
|
|
||||||
| 8aa59e7e-dc75-459b-beb5-b710b39be583 | 8aa59e7e-dc75-459b-beb5-b710b39be583 | success | delete an existing plan |
|
|
||||||
| 8aa59e7e-dc75-459b-aeb5-b710b39be583 | 8aa59e7e-dc75-459b-beb5-b710b39be512 | error | delete a non-existing plan |
|
|
@ -1,30 +0,0 @@
|
|||||||
Feature: Update a backup plan
|
|
||||||
|
|
||||||
@fixture.injector
|
|
||||||
@fixture.in_memory_database
|
|
||||||
@fixture.seeds
|
|
||||||
Scenario Outline: Update a backup plan's fields
|
|
||||||
Given I have a backup plan with id="<existing_backup_plan_id>"
|
|
||||||
When I update the backup plan with id="<backup_plan_id>" and data="<updated_data>"
|
|
||||||
Then the result should be "<result>"
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
| backup_plan_id | existing_backup_plan_id | updated_data | result | description |
|
|
||||||
| 8aa59e7e-dc75-459b-beb5-b710b39be583 | 8aa59e7e-dc75-459b-beb5-b710b39be583 | {"label": "new_label"} | success | update an existing plan |
|
|
||||||
| 8aa59e7e-dc75-459b-aeb5-b710b39be583 | 8aa59e7e-dc75-459b-beb5-b710b39be512 | {"label": "new_label"} | error | update a non-existing plan |
|
|
||||||
|
|
||||||
@fixture.injector
|
|
||||||
@fixture.in_memory_database
|
|
||||||
@fixture.seeds
|
|
||||||
Scenario Outline: Updated backup plan's fields must be updated
|
|
||||||
Given I have a backup plan with id="<existing_backup_plan_id>"
|
|
||||||
When I update the backup plan with id="<backup_plan_id>" and data="<updated_data>"
|
|
||||||
And I read the backup plan with id="<backup_plan_id>"
|
|
||||||
Then the result should be "<result>"
|
|
||||||
And the backup_plan must have same field: "<updated_data>"
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
| backup_plan_id | existing_backup_plan_id | updated_data | result | description |
|
|
||||||
| 8aa59e7e-dc75-459b-beb5-b710b39be583 | 8aa59e7e-dc75-459b-beb5-b710b39be583 | {"label": "new_label"} | success | update the label |
|
|
||||||
| 8aa59e7e-dc75-459b-beb5-b710b39be583 | 8aa59e7e-dc75-459b-beb5-b710b39be583 | {"source": "/new/mnt"} | success | update the source |
|
|
||||||
| 8aa59e7e-dc75-459b-beb5-b710b39be583 | 8aa59e7e-dc75-459b-beb5-b710b39be583 | {"destinations": ["/mnt2", "/mnt3"]} | success | update the destinations |
|
|
@ -1,62 +0,0 @@
|
|||||||
from behave import fixture, use_fixture
|
|
||||||
from injector import Injector
|
|
||||||
from typer.testing import CliRunner
|
|
||||||
|
|
||||||
from features.support import DataSeeds
|
|
||||||
from tui_rsync.core.components.backup_plan.application.repository.backup_plan_repository import BackupPlanRepository
|
|
||||||
from tui_rsync.core.components.backup_plan.application.services.backup_plan_service import BackupPlanService
|
|
||||||
from tui_rsync.infrastructure.configuration import TestingConfiguration, CurrentConfiguration
|
|
||||||
from tui_rsync.infrastructure.orm import InMemoryDatabaseManager
|
|
||||||
from tui_rsync.user_interface.cli.cli import cli_app
|
|
||||||
|
|
||||||
|
|
||||||
@fixture
|
|
||||||
def setup_cli_runner(context):
|
|
||||||
context.cli_runner = CliRunner()
|
|
||||||
yield context.cli_runner
|
|
||||||
|
|
||||||
|
|
||||||
@fixture
|
|
||||||
def setup_cli_app(context):
|
|
||||||
context.cli_app = cli_app
|
|
||||||
yield context.cli_app
|
|
||||||
|
|
||||||
|
|
||||||
@fixture
|
|
||||||
def setup_in_memory_database_manager(context):
|
|
||||||
context.db = context.injector.get(InMemoryDatabaseManager)
|
|
||||||
yield context.db
|
|
||||||
|
|
||||||
|
|
||||||
@fixture
|
|
||||||
def setup_backup_plan_service(context):
|
|
||||||
context.backup_plan_service = context.injector.get(BackupPlanService)
|
|
||||||
yield context.backup_plan_service
|
|
||||||
|
|
||||||
|
|
||||||
@fixture
|
|
||||||
def setup_injector_for_testing(context):
|
|
||||||
context.injector = Injector([TestingConfiguration()])
|
|
||||||
CurrentConfiguration.set_injector(context.injector)
|
|
||||||
yield context.injector
|
|
||||||
|
|
||||||
|
|
||||||
@fixture
|
|
||||||
def setup_seeds(context):
|
|
||||||
context.data_seeds = DataSeeds(context.injector)
|
|
||||||
context.data_seeds.seeds(context)
|
|
||||||
yield context.data_seeds
|
|
||||||
|
|
||||||
|
|
||||||
def before_tag(context, tag):
|
|
||||||
if tag == "fixture.injector":
|
|
||||||
use_fixture(setup_injector_for_testing, context)
|
|
||||||
if tag == "fixture.in_memory_database":
|
|
||||||
use_fixture(setup_in_memory_database_manager, context)
|
|
||||||
if tag == "fixture.backup_plan_service":
|
|
||||||
use_fixture(setup_backup_plan_service, context)
|
|
||||||
if tag == "fixture.cli":
|
|
||||||
use_fixture(setup_cli_app, context)
|
|
||||||
use_fixture(setup_cli_runner, context)
|
|
||||||
if tag == "fixture.seeds":
|
|
||||||
use_fixture(setup_seeds, context)
|
|
@ -1,126 +0,0 @@
|
|||||||
from behave import given, when, then
|
|
||||||
import json
|
|
||||||
|
|
||||||
from features.support.backup_plan_helpers import compare_destinations, update_backup_plan, compare_backup_plan_fields
|
|
||||||
from features.support.helpers import json_to_dict
|
|
||||||
from tui_rsync.core.components.backup_plan.application.services import RemoveAllBackupPlansService
|
|
||||||
from tui_rsync.core.components.backup_plan.application.services.backup_plan_service import BackupPlanService
|
|
||||||
from tui_rsync.core.components.backup_plan.domain import BackupPlan, Source, Destination
|
|
||||||
from tui_rsync.core.shared_kernel.components.common import UUID
|
|
||||||
from tui_rsync.core.shared_kernel.ports.Exceptions import CommandException
|
|
||||||
from tui_rsync.core.shared_kernel.ports.Exceptions.query_exception import QueryException
|
|
||||||
|
|
||||||
|
|
||||||
@given('the label "{label}"')
|
|
||||||
def given_source_label(context, label):
|
|
||||||
context.label = label
|
|
||||||
|
|
||||||
|
|
||||||
@given('the path "{source_path}"')
|
|
||||||
def given_source_path(context, source_path):
|
|
||||||
context.source_path = source_path
|
|
||||||
|
|
||||||
|
|
||||||
@given('the destinations {destinations_json}')
|
|
||||||
def given_source_destinations(context, destinations_json):
|
|
||||||
context.destinations = json.loads(destinations_json)
|
|
||||||
|
|
||||||
|
|
||||||
@when('I create the backup plan')
|
|
||||||
def add_backup_plan(context):
|
|
||||||
try:
|
|
||||||
context.backup_plan = BackupPlan(
|
|
||||||
label=context.label,
|
|
||||||
source=Source(context.source_path),
|
|
||||||
destinations=list(map(lambda path: Destination(path), context.destinations)),
|
|
||||||
)
|
|
||||||
|
|
||||||
context.backup_plan_service.add(context.backup_plan)
|
|
||||||
|
|
||||||
context.backup_plan = context.backup_plan_service.get_by_id(context.backup_plan.id)
|
|
||||||
except Exception as e:
|
|
||||||
context.exception_raised = True
|
|
||||||
else:
|
|
||||||
context.exception_raised = False
|
|
||||||
|
|
||||||
|
|
||||||
@when('I update the backup plan with id="{backup_plan_id}" and data="{updated_data_json}"')
|
|
||||||
def when_update_backup_plan_with_id(context, backup_plan_id, updated_data_json):
|
|
||||||
try:
|
|
||||||
context.backup_plan_service = context.injector.get(BackupPlanService)
|
|
||||||
context.backup_plan = context.backup_plan_service.get_by_id(UUID(backup_plan_id))
|
|
||||||
|
|
||||||
update_backup_plan(context.backup_plan, json_to_dict(updated_data_json))
|
|
||||||
|
|
||||||
context.backup_plan_service.update(context.backup_plan)
|
|
||||||
except QueryException as e:
|
|
||||||
context.exception_raised = True
|
|
||||||
else:
|
|
||||||
context.exception_raised = False
|
|
||||||
|
|
||||||
|
|
||||||
@when('I remove the backup plan with id="{backup_plan_id}"')
|
|
||||||
def when_remove_backup_plan_with_id(context, backup_plan_id):
|
|
||||||
try:
|
|
||||||
context.backup_plan_service = context.injector.get(BackupPlanService)
|
|
||||||
context.backup_plan_service.delete(UUID(backup_plan_id))
|
|
||||||
except CommandException as e:
|
|
||||||
context.exception_raised = True
|
|
||||||
else:
|
|
||||||
context.exception_raised = False
|
|
||||||
|
|
||||||
|
|
||||||
@when('I remove all backup plans')
|
|
||||||
def when_remove_all_backup_plans(context):
|
|
||||||
try:
|
|
||||||
context.backup_plan_service = context.injector.get(RemoveAllBackupPlansService)
|
|
||||||
context.result_value = context.backup_plan_service.remove_all()
|
|
||||||
except CommandException as e:
|
|
||||||
context.exception_raised = True
|
|
||||||
else:
|
|
||||||
context.exception_raised = False
|
|
||||||
|
|
||||||
|
|
||||||
@when('I read the backup plan with id="{backup_plan_id}"')
|
|
||||||
def when_read_backup_plan_with_id(context, backup_plan_id):
|
|
||||||
try:
|
|
||||||
context.backup_plan_service = context.injector.get(BackupPlanService)
|
|
||||||
context.backup_plan = context.backup_plan_service.get_by_id(UUID(backup_plan_id))
|
|
||||||
except QueryException as e:
|
|
||||||
context.exception_raised = True
|
|
||||||
else:
|
|
||||||
context.exception_raised = False
|
|
||||||
|
|
||||||
|
|
||||||
@then('it should be created successfully')
|
|
||||||
def backup_plan_has_added(context):
|
|
||||||
assert context.exception_raised == False
|
|
||||||
assert context.backup_plan is not None
|
|
||||||
|
|
||||||
assert context.backup_plan.label == context.label
|
|
||||||
assert context.backup_plan.source == Source(context.source_path)
|
|
||||||
|
|
||||||
assert compare_destinations(
|
|
||||||
context.backup_plan.destinations,
|
|
||||||
context.destinations
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@then('the result should be "{result}"')
|
|
||||||
def then_cli_executed_successfully(context, result):
|
|
||||||
match result:
|
|
||||||
case "success":
|
|
||||||
assert context.exception_raised == False
|
|
||||||
case "error":
|
|
||||||
assert context.exception_raised
|
|
||||||
|
|
||||||
|
|
||||||
@then('the result value should be "{result}"')
|
|
||||||
def then_cli_executed_successfully(context, result):
|
|
||||||
assert result == str(context.result_value)
|
|
||||||
|
|
||||||
|
|
||||||
@then('the backup_plan must have same field: "{fields_json}"')
|
|
||||||
def backup_plan_has_added(context, fields_json):
|
|
||||||
assert compare_backup_plan_fields(context.backup_plan, json_to_dict(fields_json))
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
|||||||
from behave import given, when, then
|
|
||||||
|
|
||||||
|
|
||||||
@given('the CLI arguments are "{arguments}"')
|
|
||||||
def given_cli_arguments(context, arguments):
|
|
||||||
context.arguments = arguments.split()
|
|
||||||
|
|
||||||
|
|
||||||
@when('I run the CLI')
|
|
||||||
def when_run_cli(context):
|
|
||||||
context.cli_result = context.cli_runner.invoke(context.cli_app, context.arguments)
|
|
||||||
|
|
||||||
|
|
||||||
@then('the CLI executed with "{result}"')
|
|
||||||
def then_cli_executed_successfully(context, result):
|
|
||||||
match result:
|
|
||||||
case "success":
|
|
||||||
assert context.cli_result.exit_code == 0
|
|
||||||
case "error":
|
|
||||||
assert context.cli_result.exit_code != 0
|
|
||||||
|
|
||||||
|
|
||||||
@then('the CLI output contains "{string}"')
|
|
||||||
def then_cli_output_contains(context, string):
|
|
||||||
print(f"Got: {context.cli_result.stdout}")
|
|
||||||
assert string in context.cli_result.stdout
|
|
||||||
|
|
||||||
|
|
||||||
@then('the CLI output doesn\'t contains "{string}"')
|
|
||||||
def then_cli_output_contains(context, string):
|
|
||||||
print(f"Got: {context.cli_result.stdout}")
|
|
||||||
assert not (string in context.cli_result.stdout)
|
|
||||||
|
|
||||||
|
|
||||||
@then('the CLI contains the error: "{string}"')
|
|
||||||
def then_cli_output_contains_error(context, string):
|
|
||||||
print(f"Got: {context.cli_result.stdout}")
|
|
||||||
assert string in context.cli_result.stdout
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
|||||||
from behave import given
|
|
||||||
|
|
||||||
|
|
||||||
@given('I have a backup plan with id="{existing_backup_plan_id}"')
|
|
||||||
def given_existing_backup_plan_id_seed(context, existing_backup_plan_id):
|
|
||||||
context.data_seeds.create_backup_plan(existing_backup_plan_id)
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
|||||||
from .fake_factories import FakeFactories
|
|
||||||
from .data_seeds import DataSeeds
|
|
||||||
|
|
||||||
__all__ = ['FakeFactories', 'DataSeeds']
|
|
@ -1,57 +0,0 @@
|
|||||||
from features.support.helpers import json_to_dict
|
|
||||||
from tui_rsync.core.components.backup_plan.domain import Destination, BackupPlan, Source
|
|
||||||
|
|
||||||
|
|
||||||
def json_to_backup_plan(backup_plan_json):
|
|
||||||
fields = json_to_dict(backup_plan_json)
|
|
||||||
|
|
||||||
backup_plan = BackupPlan(
|
|
||||||
label=fields['label'],
|
|
||||||
source=Source(fields['source']),
|
|
||||||
destinations=list(map(lambda path: Destination(path), fields['destinations'])),
|
|
||||||
)
|
|
||||||
|
|
||||||
return backup_plan
|
|
||||||
|
|
||||||
|
|
||||||
def update_backup_plan(backup_plan, fields: dict):
|
|
||||||
for field, value in fields.items():
|
|
||||||
match field:
|
|
||||||
case 'label':
|
|
||||||
backup_plan.label = value
|
|
||||||
case 'source':
|
|
||||||
backup_plan.source = Source(value)
|
|
||||||
case 'destinations':
|
|
||||||
backup_plan.destinations = list(map(lambda path: Destination(path), value))
|
|
||||||
|
|
||||||
return backup_plan
|
|
||||||
|
|
||||||
|
|
||||||
def compare_backup_plan_fields(backup_plan, fields: dict):
|
|
||||||
for field, value in fields.items():
|
|
||||||
match field:
|
|
||||||
case 'label':
|
|
||||||
if backup_plan.label != value:
|
|
||||||
return False
|
|
||||||
case 'source':
|
|
||||||
if backup_plan.source.path != value:
|
|
||||||
return False
|
|
||||||
case 'destinations':
|
|
||||||
if not compare_destinations(backup_plan.destinations, value):
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def compare_destinations(actual: list[Destination], expected: list[str]) -> bool:
|
|
||||||
actual_path_set = {destionation.path for destionation in actual}
|
|
||||||
return actual_path_set == set(expected)
|
|
||||||
|
|
||||||
|
|
||||||
def compare_backup_plans(backup_plan, expected):
|
|
||||||
return backup_plan.label == expected.label \
|
|
||||||
and backup_plan.source == expected.source_path \
|
|
||||||
and compare_destinations(
|
|
||||||
backup_plan.destinations,
|
|
||||||
expected.destinations
|
|
||||||
)
|
|
@ -1,19 +0,0 @@
|
|||||||
from injector import Injector
|
|
||||||
|
|
||||||
from tui_rsync.core.components.backup_plan.application.services.backup_plan_service import BackupPlanService
|
|
||||||
from . import FakeFactories
|
|
||||||
|
|
||||||
|
|
||||||
class DataSeeds:
|
|
||||||
def __init__(self, configuration: Injector):
|
|
||||||
self.fake_factories = FakeFactories()
|
|
||||||
self.configuration = configuration
|
|
||||||
|
|
||||||
def create_backup_plan(self, uuid: str | None = None):
|
|
||||||
service = self.configuration.get(BackupPlanService)
|
|
||||||
backup_plan = self.fake_factories.backup_plan(uuid)
|
|
||||||
service.add(backup_plan)
|
|
||||||
return backup_plan
|
|
||||||
|
|
||||||
def seeds(self, context):
|
|
||||||
context.backup_plan_seed = self.create_backup_plan()
|
|
@ -1,34 +0,0 @@
|
|||||||
from faker import Faker
|
|
||||||
|
|
||||||
from tui_rsync.core.components.backup_plan.domain import BackupPlan, Path, BackupPlanId, Source, Destination
|
|
||||||
from tui_rsync.core.shared_kernel.components.common import UUID, Label
|
|
||||||
|
|
||||||
|
|
||||||
class FakeFactories:
|
|
||||||
def __init__(self):
|
|
||||||
self.faker = Faker()
|
|
||||||
|
|
||||||
def uuid(self):
|
|
||||||
return UUID(self.faker.uuid4())
|
|
||||||
|
|
||||||
def backup_plan_id(self):
|
|
||||||
return BackupPlanId(self.uuid().id)
|
|
||||||
|
|
||||||
def label(self) -> Label:
|
|
||||||
return Label(self.faker.sentence(nb_words=4))
|
|
||||||
|
|
||||||
def path(self) -> Path:
|
|
||||||
return Path(self.faker.file_path())
|
|
||||||
|
|
||||||
def source(self) -> Source:
|
|
||||||
return Source(self.path().path)
|
|
||||||
|
|
||||||
def destination(self) -> Destination:
|
|
||||||
return Destination(self.path().path)
|
|
||||||
|
|
||||||
def backup_plan(self, uuid: str | None = None):
|
|
||||||
uuid = self.backup_plan_id() if uuid is None else UUID(uuid)
|
|
||||||
return BackupPlan(id=uuid,
|
|
||||||
label=self.label(),
|
|
||||||
source=self.source(),
|
|
||||||
destinations=[self.destination()])
|
|
@ -1,5 +0,0 @@
|
|||||||
import json
|
|
||||||
|
|
||||||
|
|
||||||
def json_to_dict(json_data):
|
|
||||||
return json.loads(json_data)
|
|
@ -1,32 +0,0 @@
|
|||||||
click==8.1.3 ; python_version >= "3.10" and python_version < "4.0" \
|
|
||||||
--hash=sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e \
|
|
||||||
--hash=sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48
|
|
||||||
colorama==0.4.6 ; python_version >= "3.10" and python_version < "4.0" and platform_system == "Windows" \
|
|
||||||
--hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
|
|
||||||
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
|
|
||||||
injector==0.22.0 ; python_version >= "3.10" and python_version < "4.0" \
|
|
||||||
--hash=sha256:74379ccef3b893bc7d0b7d504c255b5160c5a55e97dc7bdcc73cb33cc7dce3a1 \
|
|
||||||
--hash=sha256:79b2d4a0874c75d3aa735f11c5b32b89d9542711ca07071161882c5e9cc15ed6
|
|
||||||
markdown-it-py==2.2.0 ; python_version >= "3.10" and python_version < "4.0" \
|
|
||||||
--hash=sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30 \
|
|
||||||
--hash=sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1
|
|
||||||
mdurl==0.1.2 ; python_version >= "3.10" and python_version < "4.0" \
|
|
||||||
--hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \
|
|
||||||
--hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba
|
|
||||||
peewee==3.15.4 ; python_version >= "3.10" and python_version < "4.0" \
|
|
||||||
--hash=sha256:2581520c8dfbacd9d580c2719ae259f0637a9e46eda47dfc0ce01864c6366205
|
|
||||||
platformdirs==3.1.1 ; python_version >= "3.10" and python_version < "4.0" \
|
|
||||||
--hash=sha256:024996549ee88ec1a9aa99ff7f8fc819bb59e2c3477b410d90a16d32d6e707aa \
|
|
||||||
--hash=sha256:e5986afb596e4bb5bde29a79ac9061aa955b94fca2399b7aaac4090860920dd8
|
|
||||||
pyfzf==0.3.1 ; python_version >= "3.10" and python_version < "4.0" \
|
|
||||||
--hash=sha256:736f71563461b75f6f85b55345bdc638fa0dc14c32c857c59e8b1ca1cfa3cf4a \
|
|
||||||
--hash=sha256:dd902e34cffeca9c3082f96131593dd20b4b3a9bba5b9dde1b0688e424b46bd2
|
|
||||||
pygments==2.14.0 ; python_version >= "3.10" and python_version < "4.0" \
|
|
||||||
--hash=sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297 \
|
|
||||||
--hash=sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717
|
|
||||||
rich==13.3.1 ; python_version >= "3.10" and python_version < "4.0" \
|
|
||||||
--hash=sha256:125d96d20c92b946b983d0d392b84ff945461e5a06d3867e9f9e575f8697b67f \
|
|
||||||
--hash=sha256:8aa57747f3fc3e977684f0176a88e789be314a99f99b43b75d1e9cb5dc6db9e9
|
|
||||||
typer==0.7.0 ; python_version >= "3.10" and python_version < "4.0" \
|
|
||||||
--hash=sha256:b5e704f4e48ec263de1c0b3a2387cd405a13767d2f907f44c1a08cbad96f606d \
|
|
||||||
--hash=sha256:ff797846578a9f2a201b53442aedeb543319466870fbe1c701eab66dd7681165
|
|
@ -1,53 +0,0 @@
|
|||||||
behave==1.2.6 ; python_version >= "3.10" and python_version < "4.0" \
|
|
||||||
--hash=sha256:b9662327aa53294c1351b0a9c369093ccec1d21026f050c3bd9b3e5cccf81a86 \
|
|
||||||
--hash=sha256:ebda1a6c9e5bfe95c5f9f0a2794e01c7098b3dde86c10a95d8621c5907ff6f1c
|
|
||||||
click==8.1.3 ; python_version >= "3.10" and python_version < "4.0" \
|
|
||||||
--hash=sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e \
|
|
||||||
--hash=sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48
|
|
||||||
colorama==0.4.6 ; python_version >= "3.10" and python_version < "4.0" and platform_system == "Windows" \
|
|
||||||
--hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
|
|
||||||
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
|
|
||||||
faker==35.0.0 ; python_version >= "3.10" and python_version < "4.0" \
|
|
||||||
--hash=sha256:42f2da8cf561e38c72b25e9891168b1e25fec42b6b0b5b0b6cd6041da54af885 \
|
|
||||||
--hash=sha256:926d2301787220e0554c2e39afc4dc535ce4b0a8d0a089657137999f66334ef4
|
|
||||||
injector==0.22.0 ; python_version >= "3.10" and python_version < "4.0" \
|
|
||||||
--hash=sha256:74379ccef3b893bc7d0b7d504c255b5160c5a55e97dc7bdcc73cb33cc7dce3a1 \
|
|
||||||
--hash=sha256:79b2d4a0874c75d3aa735f11c5b32b89d9542711ca07071161882c5e9cc15ed6
|
|
||||||
markdown-it-py==2.2.0 ; python_version >= "3.10" and python_version < "4.0" \
|
|
||||||
--hash=sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30 \
|
|
||||||
--hash=sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1
|
|
||||||
mdurl==0.1.2 ; python_version >= "3.10" and python_version < "4.0" \
|
|
||||||
--hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \
|
|
||||||
--hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba
|
|
||||||
parse-type==0.6.2 ; python_version >= "3.10" and python_version < "4.0" \
|
|
||||||
--hash=sha256:06d39a8b70fde873eb2a131141a0e79bb34a432941fb3d66fad247abafc9766c \
|
|
||||||
--hash=sha256:79b1f2497060d0928bc46016793f1fca1057c4aacdf15ef876aa48d75a73a355
|
|
||||||
parse==1.19.1 ; python_version >= "3.10" and python_version < "4.0" \
|
|
||||||
--hash=sha256:371ed3800dc63983832159cc9373156613947707bc448b5215473a219dbd4362 \
|
|
||||||
--hash=sha256:cc3a47236ff05da377617ddefa867b7ba983819c664e1afe46249e5b469be464
|
|
||||||
peewee==3.15.4 ; python_version >= "3.10" and python_version < "4.0" \
|
|
||||||
--hash=sha256:2581520c8dfbacd9d580c2719ae259f0637a9e46eda47dfc0ce01864c6366205
|
|
||||||
platformdirs==3.1.1 ; python_version >= "3.10" and python_version < "4.0" \
|
|
||||||
--hash=sha256:024996549ee88ec1a9aa99ff7f8fc819bb59e2c3477b410d90a16d32d6e707aa \
|
|
||||||
--hash=sha256:e5986afb596e4bb5bde29a79ac9061aa955b94fca2399b7aaac4090860920dd8
|
|
||||||
pyfzf==0.3.1 ; python_version >= "3.10" and python_version < "4.0" \
|
|
||||||
--hash=sha256:736f71563461b75f6f85b55345bdc638fa0dc14c32c857c59e8b1ca1cfa3cf4a \
|
|
||||||
--hash=sha256:dd902e34cffeca9c3082f96131593dd20b4b3a9bba5b9dde1b0688e424b46bd2
|
|
||||||
pygments==2.14.0 ; python_version >= "3.10" and python_version < "4.0" \
|
|
||||||
--hash=sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297 \
|
|
||||||
--hash=sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717
|
|
||||||
python-dateutil==2.9.0.post0 ; python_version >= "3.10" and python_version < "4.0" \
|
|
||||||
--hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \
|
|
||||||
--hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427
|
|
||||||
rich==13.3.1 ; python_version >= "3.10" and python_version < "4.0" \
|
|
||||||
--hash=sha256:125d96d20c92b946b983d0d392b84ff945461e5a06d3867e9f9e575f8697b67f \
|
|
||||||
--hash=sha256:8aa57747f3fc3e977684f0176a88e789be314a99f99b43b75d1e9cb5dc6db9e9
|
|
||||||
six==1.16.0 ; python_version >= "3.10" and python_version < "4.0" \
|
|
||||||
--hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
|
|
||||||
--hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
|
|
||||||
typer==0.7.0 ; python_version >= "3.10" and python_version < "4.0" \
|
|
||||||
--hash=sha256:b5e704f4e48ec263de1c0b3a2387cd405a13767d2f907f44c1a08cbad96f606d \
|
|
||||||
--hash=sha256:ff797846578a9f2a201b53442aedeb543319466870fbe1c701eab66dd7681165
|
|
||||||
typing-extensions==4.12.2 ; python_version >= "3.10" and python_version < "4.0" \
|
|
||||||
--hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \
|
|
||||||
--hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8
|
|
@ -0,0 +1,4 @@
|
|||||||
|
from tui_rsync.cli.cli import cli_app
|
||||||
|
from tui_rsync.cli.sync import sync
|
||||||
|
from tui_rsync.cli.rsync import Rsync
|
||||||
|
from tui_rsync.cli.label_prompt import LabelPrompt
|
@ -0,0 +1 @@
|
|||||||
|
from tui_rsync.cli.groups.groups import groups
|
@ -0,0 +1,56 @@
|
|||||||
|
################################################################################
|
||||||
|
# Copyright (C) 2023 Kostiantyn Klochko <kostya_klochko@ukr.net> #
|
||||||
|
# #
|
||||||
|
# This file is part of tui-rsync. #
|
||||||
|
# #
|
||||||
|
# tui-rsync is free software: you can redistribute it and/or modify it under #
|
||||||
|
# uthe terms of the GNU General Public License as published by the Free #
|
||||||
|
# Software Foundation, either version 3 of the License, or (at your option) #
|
||||||
|
# any later version. #
|
||||||
|
# #
|
||||||
|
# tui-rsync is distributed in the hope that it will be useful, but WITHOUT ANY #
|
||||||
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS #
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more #
|
||||||
|
# details. #
|
||||||
|
# #
|
||||||
|
# You should have received a copy of the GNU General Public License along with #
|
||||||
|
# tui-rsync. If not, see <https://www.gnu.org/licenses/>. #
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.prompt import Prompt
|
||||||
|
from typing import List, Optional
|
||||||
|
import typer
|
||||||
|
|
||||||
|
from tui_rsync.models.models import Group
|
||||||
|
from tui_rsync.models.models import all_group_labels
|
||||||
|
from tui_rsync.cli.groups.group_prompt import GroupPrompt
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
group_remove = typer.Typer()
|
||||||
|
|
||||||
|
@group_remove.command()
|
||||||
|
def one(
|
||||||
|
group_label: str = typer.Option(
|
||||||
|
None, "--group-label", "-g",
|
||||||
|
help="[b]The label[/] is a uniq identification of a [b]group[/].",
|
||||||
|
show_default=False
|
||||||
|
),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
[red b]Remove[/] an [yellow]existing group[/].
|
||||||
|
"""
|
||||||
|
if group_label is None:
|
||||||
|
group_label = GroupPrompt.get_label_fzf()
|
||||||
|
|
||||||
|
if Group.is_exist(group_label):
|
||||||
|
group = Group.get_group(group_label)
|
||||||
|
group.delete_instance()
|
||||||
|
|
||||||
|
@group_remove.command()
|
||||||
|
def all():
|
||||||
|
"""
|
||||||
|
[red b]Remove[/] [yellow] all existing groups[/].
|
||||||
|
"""
|
||||||
|
for label in all_group_labels().iterator():
|
||||||
|
one(label.label)
|
@ -0,0 +1,63 @@
|
|||||||
|
################################################################################
|
||||||
|
# Copyright (C) 2023 Kostiantyn Klochko <kostya_klochko@ukr.net> #
|
||||||
|
# #
|
||||||
|
# This file is part of tui-rsync. #
|
||||||
|
# #
|
||||||
|
# tui-rsync is free software: you can redistribute it and/or modify it under #
|
||||||
|
# uthe terms of the GNU General Public License as published by the Free #
|
||||||
|
# Software Foundation, either version 3 of the License, or (at your option) #
|
||||||
|
# any later version. #
|
||||||
|
# #
|
||||||
|
# tui-rsync is distributed in the hope that it will be useful, but WITHOUT ANY #
|
||||||
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS #
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more #
|
||||||
|
# details. #
|
||||||
|
# #
|
||||||
|
# You should have received a copy of the GNU General Public License along with #
|
||||||
|
# tui-rsync. If not, see <https://www.gnu.org/licenses/>. #
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.prompt import Prompt
|
||||||
|
from typing import List, Optional
|
||||||
|
import typer
|
||||||
|
|
||||||
|
from tui_rsync.models.models import Group, GroupSource
|
||||||
|
from tui_rsync.cli.groups.group_prompt import GroupPrompt
|
||||||
|
from tui_rsync.models.models import all_group_labels
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
group_show = typer.Typer()
|
||||||
|
|
||||||
|
@group_show.command()
|
||||||
|
def one(
|
||||||
|
group_label: str = typer.Option(
|
||||||
|
None, "--group-label", "-l",
|
||||||
|
help="[b]The label[/] is a uniq identification of a [b]group[/].",
|
||||||
|
show_default=False
|
||||||
|
),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
[green b]Show[/] an [yellow]existing group[/].
|
||||||
|
"""
|
||||||
|
if group_label is None:
|
||||||
|
console.print("What is the [yellow b]label of the group[/]? ")
|
||||||
|
group_label = GroupPrompt.get_label_fzf()
|
||||||
|
|
||||||
|
if not Group.is_exist(group_label):
|
||||||
|
console.print("[red b][ERROR][/] Source does not exists!!!")
|
||||||
|
return
|
||||||
|
|
||||||
|
group = Group.get_group(group_label)
|
||||||
|
|
||||||
|
console.print(group.show_format())
|
||||||
|
|
||||||
|
@group_show.command()
|
||||||
|
def all():
|
||||||
|
"""
|
||||||
|
[green b]Show[/] [yellow]all existing groups[/].
|
||||||
|
"""
|
||||||
|
for label in all_group_labels().iterator():
|
||||||
|
group = Group.get_group(label.label)
|
||||||
|
console.print(group.show_format())
|
||||||
|
|
@ -0,0 +1,89 @@
|
|||||||
|
################################################################################
|
||||||
|
# Copyright (C) 2023 Kostiantyn Klochko <kostya_klochko@ukr.net> #
|
||||||
|
# #
|
||||||
|
# This file is part of tui-rsync. #
|
||||||
|
# #
|
||||||
|
# tui-rsync is free software: you can redistribute it and/or modify it under #
|
||||||
|
# uthe terms of the GNU General Public License as published by the Free #
|
||||||
|
# Software Foundation, either version 3 of the License, or (at your option) #
|
||||||
|
# any later version. #
|
||||||
|
# #
|
||||||
|
# tui-rsync is distributed in the hope that it will be useful, but WITHOUT ANY #
|
||||||
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS #
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more #
|
||||||
|
# details. #
|
||||||
|
# #
|
||||||
|
# You should have received a copy of the GNU General Public License along with #
|
||||||
|
# tui-rsync. If not, see <https://www.gnu.org/licenses/>. #
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.prompt import Prompt
|
||||||
|
from typing import List, Optional
|
||||||
|
import typer
|
||||||
|
|
||||||
|
from tui_rsync.models.models import Group, GroupSource
|
||||||
|
from tui_rsync.cli.label_prompt import LabelPrompt
|
||||||
|
from tui_rsync.cli.groups.group_prompt import GroupPrompt
|
||||||
|
from typer.main import get_group
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
group_update = typer.Typer()
|
||||||
|
|
||||||
|
@group_update.command()
|
||||||
|
def label(
|
||||||
|
group_label: str = typer.Option(
|
||||||
|
None, "--group-label", "-l",
|
||||||
|
help="[b]The label[/] is a uniq identification of a [b]group[/].",
|
||||||
|
show_default=False
|
||||||
|
),
|
||||||
|
new_group_label: str = typer.Option(
|
||||||
|
None, "--new-group-label", "-nl",
|
||||||
|
help="[b]The new label[/] will replace the [b]old group label[/].",
|
||||||
|
show_default=False
|
||||||
|
),
|
||||||
|
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
[green b]Update[/] an [yellow]existing group label[/].
|
||||||
|
"""
|
||||||
|
if group_label is None:
|
||||||
|
console.print("What is the [yellow b]old label of group[/]? ")
|
||||||
|
group_label = GroupPrompt.get_label_fzf()
|
||||||
|
|
||||||
|
if new_group_label is None:
|
||||||
|
question = "What is the [yellow b]new label of the group[/]? "
|
||||||
|
new_group_label = GroupPrompt.ask_uuid(question)
|
||||||
|
|
||||||
|
if Group.is_exist(group_label):
|
||||||
|
group = Group.get_group(group_label)
|
||||||
|
group.update_label(new_group_label)
|
||||||
|
|
||||||
|
@group_update.command()
|
||||||
|
def labels(
|
||||||
|
group_label: str = typer.Option(
|
||||||
|
None, "--group-label", "-g",
|
||||||
|
help="[b]The label[/] is a uniq identification of a [b]group[/].",
|
||||||
|
show_default=False
|
||||||
|
),
|
||||||
|
new_labels: str = typer.Option(
|
||||||
|
None, "--new-labels", "-nl",
|
||||||
|
help="[b]The new label[/] will replace the [b]old source label[/].",
|
||||||
|
show_default=False
|
||||||
|
),
|
||||||
|
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
[green b]Update[/] [yellow]the group[/] with a [bold]the label[/].
|
||||||
|
[b]The chosen sources[/] will be updated for [b]the group[/].
|
||||||
|
"""
|
||||||
|
if group_label is None:
|
||||||
|
group_label = GroupPrompt.get_label_fzf()
|
||||||
|
|
||||||
|
if new_labels is None:
|
||||||
|
new_labels = LabelPrompt.get_labels()
|
||||||
|
|
||||||
|
group = Group.get_group(group_label)
|
||||||
|
group.remove_sources()
|
||||||
|
GroupSource.create_group_sources(group, new_labels)
|
||||||
|
group.save()
|
@ -0,0 +1,56 @@
|
|||||||
|
################################################################################
|
||||||
|
# Copyright (C) 2023 Kostiantyn Klochko <kostya_klochko@ukr.net> #
|
||||||
|
# #
|
||||||
|
# This file is part of tui-rsync. #
|
||||||
|
# #
|
||||||
|
# tui-rsync is free software: you can redistribute it and/or modify it under #
|
||||||
|
# uthe terms of the GNU General Public License as published by the Free #
|
||||||
|
# Software Foundation, either version 3 of the License, or (at your option) #
|
||||||
|
# any later version. #
|
||||||
|
# #
|
||||||
|
# tui-rsync is distributed in the hope that it will be useful, but WITHOUT ANY #
|
||||||
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS #
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more #
|
||||||
|
# details. #
|
||||||
|
# #
|
||||||
|
# You should have received a copy of the GNU General Public License along with #
|
||||||
|
# tui-rsync. If not, see <https://www.gnu.org/licenses/>. #
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.prompt import Confirm, Prompt
|
||||||
|
from typing import List, Optional
|
||||||
|
import typer
|
||||||
|
|
||||||
|
from tui_rsync.cli.label_prompt import LabelPrompt
|
||||||
|
from tui_rsync.cli.rsync import Rsync
|
||||||
|
from tui_rsync.models.models import Group, count_all_labels_except
|
||||||
|
from tui_rsync.cli.groups.group_show import group_show
|
||||||
|
from tui_rsync.cli.groups.group_update import group_update
|
||||||
|
from tui_rsync.cli.groups.group_remove import group_remove
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
groups = typer.Typer()
|
||||||
|
groups.add_typer(group_show, name="show", help="Show groups")
|
||||||
|
groups.add_typer(group_update, name="update", help="Update groups")
|
||||||
|
groups.add_typer(group_remove, name="remove", help="Remove groups")
|
||||||
|
|
||||||
|
@groups.command()
|
||||||
|
def add(
|
||||||
|
group_label: str = typer.Option(
|
||||||
|
None, "--group-label", "-g",
|
||||||
|
help="[b]The label[/] is a uniq identification of a [b]group[/].",
|
||||||
|
show_default=False
|
||||||
|
),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
[green b]Create[/] a [yellow]new group[/] with a [bold]uniq[/] label.
|
||||||
|
[b]The chosen sources[/] will be united into [b]the group[/].
|
||||||
|
"""
|
||||||
|
if group_label is None:
|
||||||
|
question = "Would you like to change [yellow b]the group label[/]?"
|
||||||
|
group_label = LabelPrompt.ask_uuid(question)
|
||||||
|
|
||||||
|
labels = LabelPrompt.get_labels()
|
||||||
|
Group.create_save(group_label, labels)
|
||||||
|
|
@ -0,0 +1,70 @@
|
|||||||
|
################################################################################
|
||||||
|
# Copyright (C) 2023 Kostiantyn Klochko <kostya_klochko@ukr.net> #
|
||||||
|
# #
|
||||||
|
# This file is part of tui-rsync. #
|
||||||
|
# #
|
||||||
|
# tui-rsync is free software: you can redistribute it and/or modify it under #
|
||||||
|
# uthe terms of the GNU General Public License as published by the Free #
|
||||||
|
# Software Foundation, either version 3 of the License, or (at your option) #
|
||||||
|
# any later version. #
|
||||||
|
# #
|
||||||
|
# tui-rsync is distributed in the hope that it will be useful, but WITHOUT ANY #
|
||||||
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS #
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more #
|
||||||
|
# details. #
|
||||||
|
# #
|
||||||
|
# You should have received a copy of the GNU General Public License along with #
|
||||||
|
# tui-rsync. If not, see <https://www.gnu.org/licenses/>. #
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.prompt import Confirm, Prompt, IntPrompt
|
||||||
|
from pyfzf import FzfPrompt
|
||||||
|
import uuid
|
||||||
|
from tui_rsync.models.models import all_labels, all_labels_except
|
||||||
|
from tui_rsync.models.models import count_all_labels_except
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
|
||||||
|
class LabelPrompt:
|
||||||
|
@staticmethod
|
||||||
|
def ask_uuid(
|
||||||
|
question: str = "Would you like to change [yellow b]the label[/]?"
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Return the label or the default uuid value.
|
||||||
|
"""
|
||||||
|
uid = uuid.uuid4().hex
|
||||||
|
label = Prompt.ask(question, default=uid)
|
||||||
|
return label
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_label_fzf() -> str:
|
||||||
|
fzf = FzfPrompt()
|
||||||
|
return fzf.prompt(all_labels().iterator())[0]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_label_except_fzf(labels = None) -> str:
|
||||||
|
fzf = FzfPrompt()
|
||||||
|
return fzf.prompt(all_labels_except(labels).iterator())[0]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_labels(labels = None) -> list:
|
||||||
|
confirm_question = "Would you like to add a source/sources to the group?"
|
||||||
|
is_fzf = Confirm.ask(confirm_question, default=True)
|
||||||
|
|
||||||
|
if not is_fzf:
|
||||||
|
return []
|
||||||
|
|
||||||
|
count_question = "How much would you like to add sources to the group?"
|
||||||
|
count = IntPrompt.ask(count_question, default=1)
|
||||||
|
|
||||||
|
count_max = count_all_labels_except(labels)
|
||||||
|
count = count_max if count > count_max else count
|
||||||
|
|
||||||
|
labels = []
|
||||||
|
for i in range(count):
|
||||||
|
option = LabelPrompt.get_label_except_fzf(labels)
|
||||||
|
labels.append(option)
|
||||||
|
|
||||||
|
return labels
|
@ -0,0 +1,40 @@
|
|||||||
|
################################################################################
|
||||||
|
# Copyright (C) 2023 Kostiantyn Klochko <kostya_klochko@ukr.net> #
|
||||||
|
# #
|
||||||
|
# This file is part of tui-rsync. #
|
||||||
|
# #
|
||||||
|
# tui-rsync is free software: you can redistribute it and/or modify it #
|
||||||
|
# under the terms of the GNU General Public License as published by the Free #
|
||||||
|
# Software Foundation, either version 3 of the License, or (at your option) #
|
||||||
|
# any later version. #
|
||||||
|
# #
|
||||||
|
# tui-rsync is distributed in the hope that it will be useful, but #
|
||||||
|
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY #
|
||||||
|
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||||
|
# more details. #
|
||||||
|
# #
|
||||||
|
# You should have received a copy of the GNU General Public License along with #
|
||||||
|
# tui-rsync. If not, see <https://www.gnu.org/licenses/>. #
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
from sys import stderr
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.prompt import Prompt
|
||||||
|
from pyfzf import FzfPrompt
|
||||||
|
import os
|
||||||
|
from tui_rsync.models.models import Destination, Path
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
err_console = Console(stderr=True)
|
||||||
|
|
||||||
|
class PathPrompt:
|
||||||
|
@staticmethod
|
||||||
|
def get_backup_fzf(label:str) -> str:
|
||||||
|
dests_count = len(Destination.get_all(label))
|
||||||
|
if dests_count == 0:
|
||||||
|
err_console.print("[red b]No backups!!![/]")
|
||||||
|
return ""
|
||||||
|
if dests_count == 1:
|
||||||
|
return Destination.get_all(label).get()
|
||||||
|
fzf = FzfPrompt()
|
||||||
|
return fzf.prompt(Destination.get_all(label).iterator())[0]
|
@ -0,0 +1 @@
|
|||||||
|
from tui_rsync.cli.source.source import source
|
@ -0,0 +1,56 @@
|
|||||||
|
################################################################################
|
||||||
|
# Copyright (C) 2023 Kostiantyn Klochko <kostya_klochko@ukr.net> #
|
||||||
|
# #
|
||||||
|
# This file is part of tui-rsync. #
|
||||||
|
# #
|
||||||
|
# tui-rsync is free software: you can redistribute it and/or modify it under #
|
||||||
|
# uthe terms of the GNU General Public License as published by the Free #
|
||||||
|
# Software Foundation, either version 3 of the License, or (at your option) #
|
||||||
|
# any later version. #
|
||||||
|
# #
|
||||||
|
# tui-rsync is distributed in the hope that it will be useful, but WITHOUT ANY #
|
||||||
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS #
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more #
|
||||||
|
# details. #
|
||||||
|
# #
|
||||||
|
# You should have received a copy of the GNU General Public License along with #
|
||||||
|
# tui-rsync. If not, see <https://www.gnu.org/licenses/>. #
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.prompt import Prompt
|
||||||
|
from typing import List, Optional
|
||||||
|
import typer
|
||||||
|
|
||||||
|
from tui_rsync.models.models import Source, Destination, SyncCommand, Path
|
||||||
|
from tui_rsync.cli.label_prompt import LabelPrompt
|
||||||
|
from tui_rsync.models.models import all_labels
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
source_remove = typer.Typer()
|
||||||
|
|
||||||
|
@source_remove.command()
|
||||||
|
def one(
|
||||||
|
label: str = typer.Option(
|
||||||
|
None, "--label", "-l",
|
||||||
|
help="[b]The label[/] is a uniq identification of a [b]source[/].",
|
||||||
|
show_default=False
|
||||||
|
)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
[red b]Remove[/] an [yellow]existing source[/].
|
||||||
|
"""
|
||||||
|
if label is None:
|
||||||
|
label = LabelPrompt.get_label_fzf()
|
||||||
|
|
||||||
|
if Source.is_exist(label):
|
||||||
|
src = Source.get_source(label)
|
||||||
|
src.delete_instance()
|
||||||
|
|
||||||
|
@source_remove.command()
|
||||||
|
def all():
|
||||||
|
"""
|
||||||
|
[red b]Remove[/] [yellow] all existing sources[/].
|
||||||
|
"""
|
||||||
|
for label in all_labels().iterator():
|
||||||
|
one(label.label)
|
@ -0,0 +1,64 @@
|
|||||||
|
################################################################################
|
||||||
|
# Copyright (C) 2023 Kostiantyn Klochko <kostya_klochko@ukr.net> #
|
||||||
|
# #
|
||||||
|
# This file is part of tui-rsync. #
|
||||||
|
# #
|
||||||
|
# tui-rsync is free software: you can redistribute it and/or modify it under #
|
||||||
|
# uthe terms of the GNU General Public License as published by the Free #
|
||||||
|
# Software Foundation, either version 3 of the License, or (at your option) #
|
||||||
|
# any later version. #
|
||||||
|
# #
|
||||||
|
# tui-rsync is distributed in the hope that it will be useful, but WITHOUT ANY #
|
||||||
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS #
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more #
|
||||||
|
# details. #
|
||||||
|
# #
|
||||||
|
# You should have received a copy of the GNU General Public License along with #
|
||||||
|
# tui-rsync. If not, see <https://www.gnu.org/licenses/>. #
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.prompt import Prompt
|
||||||
|
from typing import List, Optional
|
||||||
|
import typer
|
||||||
|
|
||||||
|
from tui_rsync.models.models import Source, Destination, SyncCommand, Path
|
||||||
|
from tui_rsync.cli.label_prompt import LabelPrompt
|
||||||
|
from tui_rsync.models.models import all_labels
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
source_show = typer.Typer()
|
||||||
|
|
||||||
|
@source_show.command()
|
||||||
|
def one(
|
||||||
|
label: str = typer.Option(
|
||||||
|
None, "--label", "-l",
|
||||||
|
help="[b]The label[/] is a uniq identification of a [b]source[/].",
|
||||||
|
show_default=False
|
||||||
|
),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
[green b]Show[/] an [yellow]existing source[/].
|
||||||
|
"""
|
||||||
|
if label is None:
|
||||||
|
console.print("What is the [yellow b]label of source[/]? ")
|
||||||
|
label = LabelPrompt.get_label_fzf()
|
||||||
|
|
||||||
|
if not Source.is_exist(label):
|
||||||
|
console.print("[red b][ERROR][/] Source does not exists!!!")
|
||||||
|
return
|
||||||
|
|
||||||
|
source = Source.get_source(label)
|
||||||
|
|
||||||
|
console.print(source.show_format())
|
||||||
|
|
||||||
|
@source_show.command()
|
||||||
|
def all():
|
||||||
|
"""
|
||||||
|
[green b]Show[/] [yellow]all existing sources[/].
|
||||||
|
"""
|
||||||
|
|
||||||
|
for label in all_labels().iterator():
|
||||||
|
source = Source.get_source(label.label)
|
||||||
|
console.print(source.show_format())
|
||||||
|
|
@ -0,0 +1,111 @@
|
|||||||
|
################################################################################
|
||||||
|
# Copyright (C) 2023 Kostiantyn Klochko <kostya_klochko@ukr.net> #
|
||||||
|
# #
|
||||||
|
# This file is part of tui-rsync. #
|
||||||
|
# #
|
||||||
|
# tui-rsync is free software: you can redistribute it and/or modify it under #
|
||||||
|
# uthe terms of the GNU General Public License as published by the Free #
|
||||||
|
# Software Foundation, either version 3 of the License, or (at your option) #
|
||||||
|
# any later version. #
|
||||||
|
# #
|
||||||
|
# tui-rsync is distributed in the hope that it will be useful, but WITHOUT ANY #
|
||||||
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS #
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more #
|
||||||
|
# details. #
|
||||||
|
# #
|
||||||
|
# You should have received a copy of the GNU General Public License along with #
|
||||||
|
# tui-rsync. If not, see <https://www.gnu.org/licenses/>. #
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.prompt import Prompt
|
||||||
|
from typing import List, Optional
|
||||||
|
import typer
|
||||||
|
|
||||||
|
from tui_rsync.models.models import Source, Destination, SyncCommand, Path
|
||||||
|
from tui_rsync.cli.label_prompt import LabelPrompt
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
source_update = typer.Typer()
|
||||||
|
|
||||||
|
@source_update.command()
|
||||||
|
def label(
|
||||||
|
label: str = typer.Option(
|
||||||
|
None, "--label", "-l",
|
||||||
|
help="[b]The label[/] is a uniq identification of a [b]source[/].",
|
||||||
|
show_default=False
|
||||||
|
),
|
||||||
|
new_label: str = typer.Option(
|
||||||
|
None, "--new-label", "-nl",
|
||||||
|
help="[b]The new label[/] will replace the [b]old source label[/].",
|
||||||
|
show_default=False
|
||||||
|
),
|
||||||
|
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
[green b]Update[/] an [yellow]existing source label[/].
|
||||||
|
"""
|
||||||
|
if label is None:
|
||||||
|
console.print("What is the [yellow b]old label of source[/]? ")
|
||||||
|
label = LabelPrompt.get_label_fzf()
|
||||||
|
|
||||||
|
if new_label is None:
|
||||||
|
question = "What is the [yellow b]new label of the source[/]? "
|
||||||
|
new_label = LabelPrompt.ask_uuid(question)
|
||||||
|
|
||||||
|
if Source.is_exist(label):
|
||||||
|
src = Source.get_source(label)
|
||||||
|
src.update_label(new_label)
|
||||||
|
|
||||||
|
@source_update.command()
|
||||||
|
def source(
|
||||||
|
label: str = typer.Option(
|
||||||
|
None, "--label", "-l",
|
||||||
|
help="[b]The label[/] is a uniq identification of a [b]source[/].",
|
||||||
|
show_default=False
|
||||||
|
),
|
||||||
|
new_source_path: str = typer.Option(
|
||||||
|
None, "--new-label", "-nl",
|
||||||
|
help="[b]The new source[/] will replace the [b]old source[/].",
|
||||||
|
show_default=False
|
||||||
|
),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
[green b]Update[/] a source path of an [yellow]existing source[/].
|
||||||
|
"""
|
||||||
|
if label is None:
|
||||||
|
console.print("What is the [yellow b]label of source[/]? ")
|
||||||
|
label = LabelPrompt.get_label_fzf()
|
||||||
|
|
||||||
|
if new_source_path is None:
|
||||||
|
question = "What is the [yellow b]new source path of the source[/]? "
|
||||||
|
new_source_path = console.input(question)
|
||||||
|
|
||||||
|
if Source.is_exist(label):
|
||||||
|
src = Source.get_source(label)
|
||||||
|
src.update_source_path(new_source_path)
|
||||||
|
|
||||||
|
@source_update.command()
|
||||||
|
def args(
|
||||||
|
label: str = typer.Option(
|
||||||
|
None, "--label", "-l",
|
||||||
|
help="[b]The label[/] is a uniq identification of a [b]source[/].",
|
||||||
|
show_default=False
|
||||||
|
),
|
||||||
|
args: str = typer.Option(
|
||||||
|
None, "--args", "-a",
|
||||||
|
help="[b yellow]rsync[/] [b]arguments[/].",
|
||||||
|
show_default=False
|
||||||
|
)
|
||||||
|
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
[green b]Update[/] an [yellow]existing source args[/].
|
||||||
|
"""
|
||||||
|
if args is None:
|
||||||
|
args = console.input("What is the [yellow b]rsync args of source[/]? ")
|
||||||
|
|
||||||
|
if Source.is_exist(label):
|
||||||
|
src = Source.get_source(label)
|
||||||
|
src.update_args(args)
|
||||||
|
|
@ -0,0 +1,115 @@
|
|||||||
|
################################################################################
|
||||||
|
# Copyright (C) 2023 Kostiantyn Klochko <kostya_klochko@ukr.net> #
|
||||||
|
# #
|
||||||
|
# This file is part of tui-rsync. #
|
||||||
|
# #
|
||||||
|
# tui-rsync is free software: you can redistribute it and/or modify it under #
|
||||||
|
# uthe terms of the GNU General Public License as published by the Free #
|
||||||
|
# Software Foundation, either version 3 of the License, or (at your option) #
|
||||||
|
# any later version. #
|
||||||
|
# #
|
||||||
|
# tui-rsync is distributed in the hope that it will be useful, but WITHOUT ANY #
|
||||||
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS #
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more #
|
||||||
|
# details. #
|
||||||
|
# #
|
||||||
|
# You should have received a copy of the GNU General Public License along with #
|
||||||
|
# tui-rsync. If not, see <https://www.gnu.org/licenses/>. #
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.prompt import Prompt
|
||||||
|
from typing import List, Optional
|
||||||
|
import typer
|
||||||
|
|
||||||
|
from tui_rsync.models.models import Source, Group, Destination, SyncCommand
|
||||||
|
from tui_rsync.models.models import Path
|
||||||
|
from tui_rsync.models.models import all_labels
|
||||||
|
from tui_rsync.cli.label_prompt import LabelPrompt
|
||||||
|
from tui_rsync.cli.groups.group_prompt import GroupPrompt
|
||||||
|
from tui_rsync.cli.path_prompt import PathPrompt
|
||||||
|
from tui_rsync.cli.rsync import Rsync
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
sync = typer.Typer()
|
||||||
|
|
||||||
|
skip_error = "[yellow b]Skippped[/] because the [red b]path was unavailable[/]."
|
||||||
|
|
||||||
|
@sync.command()
|
||||||
|
def one(
|
||||||
|
label: str = typer.Option(
|
||||||
|
None, "--label", "-l",
|
||||||
|
help="[b]The label[/] is a uniq identification of a [b]source[/].",
|
||||||
|
show_default=False
|
||||||
|
),
|
||||||
|
dry: bool = typer.Option(
|
||||||
|
False, "-d", "--dry-run",
|
||||||
|
help="The command will [b]show[/] information about what will be changed.",
|
||||||
|
)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
[green b]Sync[/] a [yellow]source[/] with the [yellow b]label[/] and its backups.
|
||||||
|
[yellow b]Skips[/] if not available.
|
||||||
|
"""
|
||||||
|
if label is None:
|
||||||
|
label = LabelPrompt.get_label_fzf()
|
||||||
|
src = Source.get_source(label)
|
||||||
|
rsync = Rsync(str(src.args))
|
||||||
|
for dest in src.destinations:
|
||||||
|
if not dest.path.is_exists():
|
||||||
|
console.print(skip_error)
|
||||||
|
continue
|
||||||
|
if dry:
|
||||||
|
response = rsync.dry_one(str(src.source), str(dest))
|
||||||
|
out, err = response
|
||||||
|
console.print(f"{bstr_nonan(out)} {bstr_nonan(err)}")
|
||||||
|
else:
|
||||||
|
rsync.run_one(str(src.source), str(dest))
|
||||||
|
|
||||||
|
@sync.command()
|
||||||
|
def group(
|
||||||
|
group_label: str = typer.Option(
|
||||||
|
None, "--group-label", "-g",
|
||||||
|
help="[b]The label[/] is a uniq identification of a [b]group[/].",
|
||||||
|
show_default=False
|
||||||
|
),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
[green b]Sync[/] a [yellow]group[/] with the [yellow b] label[/].
|
||||||
|
"""
|
||||||
|
if group_label is None:
|
||||||
|
group_label = GroupPrompt.get_label_fzf()
|
||||||
|
group = Group.get_group(group_label)
|
||||||
|
|
||||||
|
for src in group.get_sources():
|
||||||
|
one(src.label)
|
||||||
|
|
||||||
|
@sync.command()
|
||||||
|
def all():
|
||||||
|
"""
|
||||||
|
[green b]Sync[/] [yellow]all sources[/] with theirs backups.
|
||||||
|
"""
|
||||||
|
for label in all_labels().iterator():
|
||||||
|
one(label.label)
|
||||||
|
|
||||||
|
@sync.command()
|
||||||
|
def recover(
|
||||||
|
label: str = typer.Option(
|
||||||
|
None, "--label", "-l",
|
||||||
|
help="[b]The label[/] is a uniq identification of a [b]source[/].",
|
||||||
|
show_default=False
|
||||||
|
),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
[green b]Sync[/] the [yellow]chosen backup[/] with its source.
|
||||||
|
"""
|
||||||
|
if label is None:
|
||||||
|
label = LabelPrompt.get_label_fzf()
|
||||||
|
src = Source.get(Source.label == label)
|
||||||
|
dest = PathPrompt.get_backup_fzf(label)
|
||||||
|
|
||||||
|
rsync = Rsync(str(src.args))
|
||||||
|
rsync.run_one(str(dest), str(src.source))
|
||||||
|
|
||||||
|
def bstr_nonan(obj):
|
||||||
|
return "" if obj is None else obj.decode()
|
@ -0,0 +1,2 @@
|
|||||||
|
from tui_rsync.config.app import App
|
||||||
|
|
@ -1,5 +0,0 @@
|
|||||||
from .backup_command_port import BackupCommandPort
|
|
||||||
from .backup_sync_command import BackupSyncCommand
|
|
||||||
from .backup_sync_command_dry_run import BackupSyncCommandDryRun
|
|
||||||
|
|
||||||
__all__ = ['BackupCommandPort', 'BackupSyncCommand', 'BackupSyncCommandDryRun']
|
|
@ -1,7 +0,0 @@
|
|||||||
from abc import ABC, abstractmethod
|
|
||||||
|
|
||||||
|
|
||||||
class BackupCommandPort(ABC):
|
|
||||||
@abstractmethod
|
|
||||||
def run(self):
|
|
||||||
pass
|
|
@ -1,14 +0,0 @@
|
|||||||
import shlex
|
|
||||||
from subprocess import Popen, PIPE
|
|
||||||
|
|
||||||
from . import BackupCommandPort
|
|
||||||
|
|
||||||
|
|
||||||
class BackupSyncCommand(BackupCommandPort):
|
|
||||||
def __init__(self, source: str, destination: str, args: str):
|
|
||||||
self._args = ["rsync"] + shlex.split(args) + [source, destination]
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
output = Popen(self._args, stdout=PIPE)
|
|
||||||
response = output.communicate()
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
|||||||
import shlex
|
|
||||||
from subprocess import Popen, PIPE
|
|
||||||
|
|
||||||
from . import BackupCommandPort
|
|
||||||
|
|
||||||
|
|
||||||
class BackupSyncCommandDryRun(BackupCommandPort):
|
|
||||||
def __init__(self, source: str, destination: str, args: str):
|
|
||||||
self._args = ["rsync"] + shlex.split(args) + ['--dry-run', source, destination]
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
output = Popen(self._args, stdout=PIPE)
|
|
||||||
return output.communicate()
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
|||||||
from .remove_all_backup_plans_command import RemoveAllBackupPlansCommand
|
|
||||||
from .remove_backup_plan_destinations_command import RemoveBackupPlanDestinationsCommand
|
|
||||||
from .remove_backup_plan_command import RemoveBackupPlanCommand
|
|
||||||
|
|
||||||
__all__ = ['RemoveAllBackupPlansCommand', 'RemoveBackupPlanCommand', 'RemoveBackupPlanDestinationsCommand']
|
|
@ -1,12 +0,0 @@
|
|||||||
from tui_rsync.core.ports.orm import DatabaseManagerPort
|
|
||||||
from tui_rsync.infrastructure.orm.models import BackupPlanModel, DestinationModel
|
|
||||||
|
|
||||||
|
|
||||||
class RemoveAllBackupPlansCommand:
|
|
||||||
def __init__(self, database_manager: DatabaseManagerPort):
|
|
||||||
self.databaseManager = database_manager
|
|
||||||
|
|
||||||
def execute(self) -> bool:
|
|
||||||
rows = BackupPlanModel.delete().execute()
|
|
||||||
rows = DestinationModel.delete().execute() + rows
|
|
||||||
return rows != 0
|
|
@ -1,18 +0,0 @@
|
|||||||
from tui_rsync.core.ports.orm import DatabaseManagerPort
|
|
||||||
from tui_rsync.core.shared_kernel.components.common import UUID
|
|
||||||
from tui_rsync.core.shared_kernel.ports.Exceptions import CommandException
|
|
||||||
from tui_rsync.infrastructure.orm.models import DestinationModel, BackupPlanModel
|
|
||||||
|
|
||||||
|
|
||||||
class RemoveBackupPlanCommand:
|
|
||||||
def __init__(self, database_manager: DatabaseManagerPort):
|
|
||||||
self.databaseManager = database_manager
|
|
||||||
|
|
||||||
def execute(self, uuid: UUID) -> bool:
|
|
||||||
deleted = BackupPlanModel.delete().where(BackupPlanModel.id == uuid.id).execute()
|
|
||||||
deleted = DestinationModel.delete().where(DestinationModel.source == uuid.id).execute() | deleted
|
|
||||||
|
|
||||||
if deleted == 0:
|
|
||||||
raise CommandException("Failed to delete the backup plan, because it doesn't exist.")
|
|
||||||
|
|
||||||
return True
|
|
@ -1,12 +0,0 @@
|
|||||||
from tui_rsync.core.ports.orm import DatabaseManagerPort
|
|
||||||
from tui_rsync.core.shared_kernel.components.common import UUID
|
|
||||||
from tui_rsync.infrastructure.orm.models import DestinationModel
|
|
||||||
|
|
||||||
|
|
||||||
class RemoveBackupPlanDestinationsCommand:
|
|
||||||
def __init__(self, database_manager: DatabaseManagerPort):
|
|
||||||
self.databaseManager = database_manager
|
|
||||||
|
|
||||||
def execute(self, backup_plan_uuid: UUID) -> bool:
|
|
||||||
rows = DestinationModel.delete().where(DestinationModel.source == backup_plan_uuid.id).execute()
|
|
||||||
return rows != 0
|
|
@ -1,5 +0,0 @@
|
|||||||
from .get_all_backup_plans_query import GetAllBackupPlansQuery
|
|
||||||
from .get_backup_plan_by_id_query import GetBackupPlanByIdQuery
|
|
||||||
from .get_backup_plan_count_query import GetBackupPlanCountQuery
|
|
||||||
|
|
||||||
__all__ = ['GetAllBackupPlansQuery', 'GetBackupPlanByIdQuery', 'GetBackupPlanCountQuery']
|
|
@ -1,12 +0,0 @@
|
|||||||
from tui_rsync.core.ports.orm import DatabaseManagerPort
|
|
||||||
from tui_rsync.infrastructure.orm.dto.dtos import BackupPlanDTO
|
|
||||||
|
|
||||||
from tui_rsync.infrastructure.orm.models import BackupPlanModel
|
|
||||||
|
|
||||||
|
|
||||||
class GetAllBackupBackupPlansQuery:
|
|
||||||
def __init__(self, database_manager: DatabaseManagerPort):
|
|
||||||
self.databaseManager = database_manager
|
|
||||||
|
|
||||||
def execute(self):
|
|
||||||
return (BackupPlanDTO.to_domain(model) for model in BackupPlanModel.select().iterator())
|
|
@ -1,12 +0,0 @@
|
|||||||
from tui_rsync.core.ports.orm import DatabaseManagerPort
|
|
||||||
from tui_rsync.infrastructure.orm.dto.dtos import BackupPlanDTO
|
|
||||||
|
|
||||||
from tui_rsync.infrastructure.orm.models import BackupPlanModel
|
|
||||||
|
|
||||||
|
|
||||||
class GetAllBackupPlansQuery:
|
|
||||||
def __init__(self, database_manager: DatabaseManagerPort):
|
|
||||||
self.databaseManager = database_manager
|
|
||||||
|
|
||||||
def execute(self):
|
|
||||||
return (BackupPlanDTO.to_domain(model) for model in BackupPlanModel.select().iterator())
|
|
@ -1,20 +0,0 @@
|
|||||||
from tui_rsync.core.components.backup_plan.domain import BackupPlan
|
|
||||||
from tui_rsync.core.ports.orm import DatabaseManagerPort
|
|
||||||
from tui_rsync.core.shared_kernel.components.common import UUID
|
|
||||||
from tui_rsync.core.shared_kernel.ports.Exceptions.query_exception import QueryException
|
|
||||||
from tui_rsync.infrastructure.orm.dto.dtos import BackupPlanDTO
|
|
||||||
|
|
||||||
from tui_rsync.infrastructure.orm.models import BackupPlanModel
|
|
||||||
|
|
||||||
|
|
||||||
class GetBackupPlanByIdQuery:
|
|
||||||
def __init__(self, database_manager: DatabaseManagerPort):
|
|
||||||
self.databaseManager = database_manager
|
|
||||||
|
|
||||||
def execute(self, uuid: UUID) -> BackupPlan:
|
|
||||||
model = BackupPlanModel.get_or_none(BackupPlanModel.id == uuid.id)
|
|
||||||
|
|
||||||
if model is None:
|
|
||||||
raise QueryException("The backup plan was not found.")
|
|
||||||
|
|
||||||
return BackupPlanDTO.to_domain(model)
|
|
@ -1,11 +0,0 @@
|
|||||||
from tui_rsync.core.ports.orm import DatabaseManagerPort
|
|
||||||
|
|
||||||
from tui_rsync.infrastructure.orm.models import BackupPlanModel
|
|
||||||
|
|
||||||
|
|
||||||
class GetBackupPlanCountQuery:
|
|
||||||
def __init__(self, database_manager: DatabaseManagerPort):
|
|
||||||
self.databaseManager = database_manager
|
|
||||||
|
|
||||||
def execute(self):
|
|
||||||
return BackupPlanModel.select().count()
|
|
@ -1,4 +0,0 @@
|
|||||||
from .backup_plan_repository_port import BackupPlanRepositoryPort
|
|
||||||
from .backup_plan_repository import BackupPlanRepository
|
|
||||||
|
|
||||||
__all__ = ['BackupPlanRepositoryPort', 'BackupPlanRepository']
|
|
@ -1,49 +0,0 @@
|
|||||||
from typing import Optional
|
|
||||||
|
|
||||||
from tui_rsync.core.ports.orm import DatabaseManagerPort
|
|
||||||
from tui_rsync.infrastructure.orm.dto.dtos import BackupPlanDTO
|
|
||||||
from .backup_plan_repository_port import BackupPlanRepositoryPort
|
|
||||||
|
|
||||||
from tui_rsync.core.components.backup_plan.domain import BackupPlan
|
|
||||||
from tui_rsync.core.shared_kernel.components.common import UUID
|
|
||||||
|
|
||||||
from tui_rsync.infrastructure.orm.models import BackupPlanModel
|
|
||||||
from ..commands import RemoveBackupPlanDestinationsCommand, RemoveBackupPlanCommand
|
|
||||||
from ..queries import GetBackupPlanByIdQuery
|
|
||||||
|
|
||||||
|
|
||||||
class BackupPlanRepository(BackupPlanRepositoryPort):
|
|
||||||
def __init__(self, database_manager: DatabaseManagerPort):
|
|
||||||
self.databaseManager = database_manager
|
|
||||||
|
|
||||||
def add(self, backup_plan: BackupPlan):
|
|
||||||
model = BackupPlanDTO.to_model(backup_plan)
|
|
||||||
|
|
||||||
model.save(force_insert=True)
|
|
||||||
for destination in model.destinations:
|
|
||||||
destination.save(force_insert=True)
|
|
||||||
|
|
||||||
def get_by_id(self, uuid: UUID) -> BackupPlan:
|
|
||||||
query = GetBackupPlanByIdQuery(self.databaseManager)
|
|
||||||
return query.execute(uuid)
|
|
||||||
|
|
||||||
def update(self, backup_plan: BackupPlan):
|
|
||||||
updated_model = BackupPlanDTO.to_model(backup_plan)
|
|
||||||
query = GetBackupPlanByIdQuery(self.databaseManager)
|
|
||||||
|
|
||||||
old_plan = query.execute(backup_plan.id)
|
|
||||||
model = BackupPlanDTO.to_model(old_plan)
|
|
||||||
remove_backup_plan_destinations_command = RemoveBackupPlanDestinationsCommand(self.databaseManager)
|
|
||||||
|
|
||||||
model.label = updated_model.label
|
|
||||||
model.source = updated_model.source
|
|
||||||
|
|
||||||
remove_backup_plan_destinations_command.execute(backup_plan.id)
|
|
||||||
|
|
||||||
model.save()
|
|
||||||
for destination in updated_model.destinations:
|
|
||||||
destination.save(force_insert=True)
|
|
||||||
|
|
||||||
def delete(self, uuid: UUID) -> bool:
|
|
||||||
command = RemoveBackupPlanCommand(self.databaseManager)
|
|
||||||
return command.execute(uuid)
|
|
@ -1,23 +0,0 @@
|
|||||||
from abc import ABC, abstractmethod
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from tui_rsync.core.components.backup_plan.domain import BackupPlan
|
|
||||||
from tui_rsync.core.shared_kernel.components.common import UUID
|
|
||||||
|
|
||||||
|
|
||||||
class BackupPlanRepositoryPort(ABC):
|
|
||||||
@abstractmethod
|
|
||||||
def add(self, backup_plan: BackupPlan):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_by_id(self, uuid: UUID) -> Optional[BackupPlan]:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def update(self, backup_plan: BackupPlan):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def delete(self, uuid: UUID) -> bool:
|
|
||||||
pass
|
|
@ -1,11 +0,0 @@
|
|||||||
from .backup_plan_service import BackupPlanService
|
|
||||||
from .get_all_backup_plans_service import GetAllBackupPlansService
|
|
||||||
from .get_backup_plan_count_service import GetBackupPlanCountService
|
|
||||||
from .remove_all_backup_plans_service import RemoveAllBackupPlansService
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'BackupPlanService',
|
|
||||||
'GetAllBackupPlansService',
|
|
||||||
'GetBackupPlanCountService',
|
|
||||||
'RemoveAllBackupPlansService',
|
|
||||||
]
|
|
@ -1,23 +0,0 @@
|
|||||||
from typing import Optional
|
|
||||||
|
|
||||||
from tui_rsync.core.components.backup_plan.application.repository import BackupPlanRepositoryPort
|
|
||||||
|
|
||||||
from tui_rsync.core.components.backup_plan.domain import BackupPlan
|
|
||||||
from tui_rsync.core.shared_kernel.components.common import UUID
|
|
||||||
|
|
||||||
|
|
||||||
class BackupPlanService:
|
|
||||||
def __init__(self, backup_plan_repository: BackupPlanRepositoryPort):
|
|
||||||
self.backup_plan_repository = backup_plan_repository
|
|
||||||
|
|
||||||
def add(self, backup_plan: BackupPlan):
|
|
||||||
self.backup_plan_repository.add(backup_plan)
|
|
||||||
|
|
||||||
def get_by_id(self, uuid: UUID) -> Optional[BackupPlan]:
|
|
||||||
return self.backup_plan_repository.get_by_id(uuid)
|
|
||||||
|
|
||||||
def update(self, backup_plan: BackupPlan):
|
|
||||||
return self.backup_plan_repository.update(backup_plan)
|
|
||||||
|
|
||||||
def delete(self, uuid: UUID) -> bool:
|
|
||||||
return self.backup_plan_repository.delete(uuid)
|
|
@ -1,13 +0,0 @@
|
|||||||
from typing import List
|
|
||||||
|
|
||||||
from tui_rsync.core.components.backup_plan.application.queries import GetAllBackupPlansQuery
|
|
||||||
|
|
||||||
from tui_rsync.core.components.backup_plan.domain import BackupPlan
|
|
||||||
|
|
||||||
|
|
||||||
class GetAllBackupPlansService:
|
|
||||||
def __init__(self, get_all_backup_plan_query: GetAllBackupPlansQuery):
|
|
||||||
self.get_all_backup_plan_query = get_all_backup_plan_query
|
|
||||||
|
|
||||||
def get_all(self) -> List[BackupPlan]:
|
|
||||||
return self.get_all_backup_plan_query.execute()
|
|
@ -1,16 +0,0 @@
|
|||||||
from typing import List
|
|
||||||
|
|
||||||
from tui_rsync.core.components.backup_plan.application.queries import GetBackupPlanCountQuery
|
|
||||||
|
|
||||||
from tui_rsync.core.components.backup_plan.domain import BackupPlan
|
|
||||||
|
|
||||||
|
|
||||||
class GetBackupPlanCountService:
|
|
||||||
def __init__(self, get_backup_plan_count_query: GetBackupPlanCountQuery):
|
|
||||||
self.get_backup_plan_count_query = get_backup_plan_count_query
|
|
||||||
|
|
||||||
def count(self) -> int:
|
|
||||||
return self.get_backup_plan_count_query.execute()
|
|
||||||
|
|
||||||
def is_empty(self) -> bool:
|
|
||||||
return self.count() == 0
|
|
@ -1,9 +0,0 @@
|
|||||||
from tui_rsync.core.components.backup_plan.application.commands import RemoveAllBackupPlansCommand
|
|
||||||
|
|
||||||
|
|
||||||
class RemoveAllBackupPlansService:
|
|
||||||
def __init__(self, remove_all_backup_plan_command: RemoveAllBackupPlansCommand):
|
|
||||||
self.remove_all_backup_plan_command = remove_all_backup_plan_command
|
|
||||||
|
|
||||||
def remove_all(self) -> bool:
|
|
||||||
return self.remove_all_backup_plan_command.execute()
|
|
@ -1,4 +0,0 @@
|
|||||||
from .entities import BackupPlan
|
|
||||||
from .value_objects import BackupPlanId, Path, Source, Destination
|
|
||||||
|
|
||||||
__all__ = ['BackupPlan', 'BackupPlanId', 'Path', 'Source', 'Destination']
|
|
@ -1,3 +0,0 @@
|
|||||||
from .backup_plan import BackupPlan
|
|
||||||
|
|
||||||
__all__ = ['BackupPlan']
|
|
@ -1,14 +0,0 @@
|
|||||||
from dataclasses import dataclass, field
|
|
||||||
from typing import List
|
|
||||||
|
|
||||||
from ..value_objects import BackupPlanId, Source, Destination
|
|
||||||
from tui_rsync.core.shared_kernel.components.common.domain import Label
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class BackupPlan:
|
|
||||||
source: Source
|
|
||||||
destinations: List[Destination]
|
|
||||||
|
|
||||||
id: BackupPlanId = field(default_factory=lambda: BackupPlanId.generate())
|
|
||||||
label: Label = field(default='')
|
|
@ -1,6 +0,0 @@
|
|||||||
from .backup_plan_id import BackupPlanId
|
|
||||||
from .path import Path
|
|
||||||
from .source import Source
|
|
||||||
from .destionation import Destination
|
|
||||||
|
|
||||||
__all__ = ['BackupPlanId', 'Path', 'Source', 'Destination']
|
|
@ -1,5 +0,0 @@
|
|||||||
from tui_rsync.core.shared_kernel.components.common.domain import UUID
|
|
||||||
|
|
||||||
|
|
||||||
class BackupPlanId(UUID):
|
|
||||||
pass
|
|
@ -1,5 +0,0 @@
|
|||||||
from .path import Path
|
|
||||||
|
|
||||||
|
|
||||||
class Destination(Path):
|
|
||||||
pass
|
|
@ -1,6 +0,0 @@
|
|||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Path:
|
|
||||||
path: str
|
|
@ -1,5 +0,0 @@
|
|||||||
from .path import Path
|
|
||||||
|
|
||||||
|
|
||||||
class Source(Path):
|
|
||||||
pass
|
|
@ -1,3 +0,0 @@
|
|||||||
from .user_data_paths_port import UserDataPathsPort
|
|
||||||
|
|
||||||
__all__ = ['UserDataPathsPort']
|
|
@ -1,15 +0,0 @@
|
|||||||
from abc import ABC, abstractmethod
|
|
||||||
|
|
||||||
|
|
||||||
class UserDataPathsPort(ABC):
|
|
||||||
@abstractmethod
|
|
||||||
def safe_create_user_data_dir(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_user_db_path(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_user_data_dir(self):
|
|
||||||
pass
|
|
@ -1,3 +0,0 @@
|
|||||||
from .database_manager_port import DatabaseManagerPort
|
|
||||||
|
|
||||||
__all__ = ['DatabaseManagerPort']
|
|
@ -1,11 +0,0 @@
|
|||||||
from abc import ABC, abstractmethod
|
|
||||||
|
|
||||||
|
|
||||||
class DatabaseManagerPort(ABC):
|
|
||||||
@abstractmethod
|
|
||||||
def get_connection(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def create_tables(self):
|
|
||||||
pass
|
|
@ -1,3 +0,0 @@
|
|||||||
from .backup_sync_service import BackupSyncService
|
|
||||||
|
|
||||||
__all__ = ['BackupSyncService']
|
|
@ -1,22 +0,0 @@
|
|||||||
from tui_rsync.core.components.backup.application.backup_commands import BackupSyncCommand, BackupSyncCommandDryRun
|
|
||||||
from tui_rsync.core.components.backup_plan.application.repository import BackupPlanRepositoryPort
|
|
||||||
from tui_rsync.core.shared_kernel.components.common import UUID
|
|
||||||
from tui_rsync.user_interface.cli.shared_kernel.components.prompts.applications.prompts import ChoosePromptPort
|
|
||||||
|
|
||||||
|
|
||||||
class BackupRestoreService:
|
|
||||||
def __init__(self, backup_plan_repository: BackupPlanRepositoryPort):
|
|
||||||
self.backup_plan_repository = backup_plan_repository
|
|
||||||
|
|
||||||
def sync_by_plan_id(self, uuid: UUID, choose_prompt: ChoosePromptPort, args: str = '', dry_run: bool = False):
|
|
||||||
backup_plan = self.backup_plan_repository.get_by_id(uuid)
|
|
||||||
|
|
||||||
destinations = (destination.path for destination in backup_plan.destinations)
|
|
||||||
destination = choose_prompt.choose(destinations)
|
|
||||||
|
|
||||||
if dry_run:
|
|
||||||
backup_command = BackupSyncCommandDryRun(destination, backup_plan.source.path, args)
|
|
||||||
backup_command.run()
|
|
||||||
else:
|
|
||||||
backup_command = BackupSyncCommand(destination, backup_plan.source.path, args)
|
|
||||||
backup_command.run()
|
|
@ -1,20 +0,0 @@
|
|||||||
from tui_rsync.core.components.backup.application.backup_commands import BackupSyncCommand, BackupSyncCommandDryRun
|
|
||||||
from tui_rsync.core.components.backup_plan.application.repository import BackupPlanRepositoryPort
|
|
||||||
from tui_rsync.core.shared_kernel.components.common import UUID
|
|
||||||
|
|
||||||
|
|
||||||
class BackupSyncService:
|
|
||||||
def __init__(self, backup_plan_repository: BackupPlanRepositoryPort):
|
|
||||||
self.backup_plan_repository = backup_plan_repository
|
|
||||||
|
|
||||||
def sync_by_plan_id(self, uuid: UUID, args: str = '', dry_run: bool = False):
|
|
||||||
backup_plan = self.backup_plan_repository.get_by_id(uuid)
|
|
||||||
|
|
||||||
if dry_run:
|
|
||||||
for destination in backup_plan.destinations:
|
|
||||||
backup_command = BackupSyncCommandDryRun(backup_plan.source.path, destination.path, args)
|
|
||||||
backup_command.run()
|
|
||||||
else:
|
|
||||||
for destination in backup_plan.destinations:
|
|
||||||
backup_command = BackupSyncCommand(backup_plan.source.path, destination.path, args)
|
|
||||||
backup_command.run()
|
|
@ -1,3 +0,0 @@
|
|||||||
from .domain import UUID, Label
|
|
||||||
|
|
||||||
__all__ = ['UUID', 'Label']
|
|
@ -1,3 +0,0 @@
|
|||||||
from .value_objects import UUID, Label
|
|
||||||
|
|
||||||
__all__ = ['UUID', 'Label']
|
|
@ -1,4 +0,0 @@
|
|||||||
from .uuid import UUID
|
|
||||||
from .label import Label
|
|
||||||
|
|
||||||
__all__ = ['UUID', 'Label']
|
|
@ -1,6 +0,0 @@
|
|||||||
from dataclasses import dataclass, field
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class Label:
|
|
||||||
label: str = field(default='')
|
|
@ -1,14 +0,0 @@
|
|||||||
from dataclasses import dataclass, field
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class UUID:
|
|
||||||
id: str = field(default_factory=lambda: UUID.generate().id)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def generate(cls) -> 'UUID':
|
|
||||||
return cls(str(uuid.uuid4()))
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.id
|
|
@ -1,4 +0,0 @@
|
|||||||
from .app_exception import AppException
|
|
||||||
from .command_exception import CommandException
|
|
||||||
|
|
||||||
__all__ = ['AppException', 'CommandException']
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue