mirror of
https://gitlab.com/KKlochko/tui-rsync.git
synced 2026-09-16 18:37:44 +00:00
Compare commits
64
Commits
f2e70ccc85
...
10114da0ee
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10114da0ee | ||
|
|
0b723c8770 | ||
|
|
62ff10dec4 | ||
|
|
7ef5b9975c | ||
|
|
e0f8ec9db5 | ||
|
|
1ec7c6464a | ||
|
|
a16e43462a | ||
|
|
2438dead07 | ||
|
|
cabcde00c7 | ||
|
|
2874dc76ad | ||
|
|
76e555aefb | ||
|
|
c1268abc1d | ||
|
|
9729f442c2 | ||
|
|
b999975695 | ||
|
|
476a0ef8cf | ||
|
|
d652469748 | ||
|
|
cae4ce50c5 | ||
|
|
769edabbd3 | ||
|
|
a4a0f32f52 | ||
|
|
5e606b0e83 | ||
|
|
add3c3d59e | ||
|
|
2d1870535f | ||
|
|
7094a0d866 | ||
|
|
18eb9b4d91 | ||
|
|
6b247fbe4e | ||
|
|
ecb495a22a | ||
|
|
f9b49c69bd | ||
|
|
bc22e8a8bd | ||
|
|
b32e5d32b0 | ||
|
|
87682a056f | ||
|
|
359dcdfca6 | ||
|
|
192f930f8a | ||
|
|
f708cdf3d4 | ||
|
|
70f60ff715 | ||
|
|
a0846b93a1 | ||
|
|
eb01a26713 | ||
|
|
ca350eef80 | ||
|
|
f3915d93a9 | ||
|
|
ea0cc3dab0 | ||
|
|
ea5d98490d | ||
|
|
b9564d75e7 | ||
|
|
027fee47a6 | ||
|
|
4bfdcf066b | ||
|
|
c8d2b84cfc | ||
|
|
0abefda95a | ||
|
|
72f650fd64 | ||
|
|
b10726796d | ||
|
|
ccede52883 | ||
|
|
690cc7de9c | ||
|
|
2834ad8bee | ||
|
|
f3c600bb8f | ||
|
|
594d9dea3f | ||
|
|
c67dc14003 | ||
|
|
66c389abfa | ||
|
|
d3bc4bd5a2 | ||
|
|
befdc9ea93 | ||
|
|
9280cdaf7d | ||
|
|
e04b9894b9 | ||
|
|
9c88462bc0 | ||
|
|
5758ff277c | ||
|
|
f12cd7d6f7 | ||
|
|
9dec626025 | ||
|
|
7f9a29190a | ||
|
|
64fa91bbc1 |
+8
-2
@@ -3,6 +3,12 @@ type: docker
|
||||
name: default
|
||||
|
||||
steps:
|
||||
- name: test
|
||||
image: python:3.10-alpine
|
||||
commands:
|
||||
- pip install -r requirements_dev.txt --cache-dir=/package_cache
|
||||
- behave --stop
|
||||
|
||||
- name: publishing
|
||||
image: python:3.10-alpine
|
||||
environment:
|
||||
@@ -17,5 +23,5 @@ steps:
|
||||
- $POETRY_HOME/bin/poetry install
|
||||
- $POETRY_HOME/bin/poetry publish --build --username $PYPI_USERNAME --password $PYPI_PASSWORD
|
||||
when:
|
||||
branch:
|
||||
- main
|
||||
event: tag
|
||||
|
||||
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
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
|
||||
|
||||
@@ -61,4 +61,7 @@
|
||||
Added *show all* option for Source.
|
||||
** 0.8.16 <2023-07-01 Sat>
|
||||
Added *show all* option for Group.
|
||||
** 1.0.0 <2025-02-02 Sun>
|
||||
Rewrite the application to update the architecture.
|
||||
Updated Groups temporary removed.
|
||||
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ Dependencies
|
||||
Author
|
||||
======
|
||||
|
||||
Kostiantyn Klochko (c) 2023
|
||||
Kostiantyn Klochko (c) 2023-2025
|
||||
|
||||
License
|
||||
=======
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
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 |
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
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 |
|
||||
@@ -0,0 +1,75 @@
|
||||
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 |
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
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 |
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
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"] |
|
||||
@@ -0,0 +1,28 @@
|
||||
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"
|
||||
@@ -0,0 +1,14 @@
|
||||
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 |
|
||||
@@ -0,0 +1,30 @@
|
||||
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 |
|
||||
@@ -0,0 +1,62 @@
|
||||
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)
|
||||
@@ -0,0 +1,126 @@
|
||||
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))
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
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
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
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)
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
from .fake_factories import FakeFactories
|
||||
from .data_seeds import DataSeeds
|
||||
|
||||
__all__ = ['FakeFactories', 'DataSeeds']
|
||||
@@ -0,0 +1,57 @@
|
||||
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
|
||||
)
|
||||
@@ -0,0 +1,19 @@
|
||||
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()
|
||||
@@ -0,0 +1,34 @@
|
||||
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()])
|
||||
@@ -0,0 +1,5 @@
|
||||
import json
|
||||
|
||||
|
||||
def json_to_dict(json_data):
|
||||
return json.loads(json_data)
|
||||
Generated
+126
-2
@@ -1,4 +1,25 @@
|
||||
# This file is automatically @generated by Poetry and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "behave"
|
||||
version = "1.2.6"
|
||||
description = "behave is behaviour-driven development, Python style"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
files = [
|
||||
{file = "behave-1.2.6-py2.py3-none-any.whl", hash = "sha256:ebda1a6c9e5bfe95c5f9f0a2794e01c7098b3dde86c10a95d8621c5907ff6f1c"},
|
||||
{file = "behave-1.2.6.tar.gz", hash = "sha256:b9662327aa53294c1351b0a9c369093ccec1d21026f050c3bd9b3e5cccf81a86"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
parse = ">=1.8.2"
|
||||
parse-type = ">=0.4.2"
|
||||
six = ">=1.11"
|
||||
|
||||
[package.extras]
|
||||
develop = ["coverage", "invoke (>=0.21.0)", "modernize (>=0.5)", "path.py (>=8.1.2)", "pathlib", "pycmd", "pylint", "pytest (>=3.0)", "pytest-cov", "tox"]
|
||||
docs = ["sphinx (>=1.6)", "sphinx-bootstrap-theme (>=0.6)"]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
@@ -27,6 +48,37 @@ files = [
|
||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "faker"
|
||||
version = "35.0.0"
|
||||
description = "Faker is a Python package that generates fake data for you."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "Faker-35.0.0-py3-none-any.whl", hash = "sha256:926d2301787220e0554c2e39afc4dc535ce4b0a8d0a089657137999f66334ef4"},
|
||||
{file = "faker-35.0.0.tar.gz", hash = "sha256:42f2da8cf561e38c72b25e9891168b1e25fec42b6b0b5b0b6cd6041da54af885"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
python-dateutil = ">=2.4"
|
||||
typing-extensions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "injector"
|
||||
version = "0.22.0"
|
||||
description = "Injector - Python dependency injection framework, inspired by Guice"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "injector-0.22.0-py2.py3-none-any.whl", hash = "sha256:74379ccef3b893bc7d0b7d504c255b5160c5a55e97dc7bdcc73cb33cc7dce3a1"},
|
||||
{file = "injector-0.22.0.tar.gz", hash = "sha256:79b2d4a0874c75d3aa735f11c5b32b89d9542711ca07071161882c5e9cc15ed6"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
dev = ["black (==24.3.0)", "build (==1.0.3)", "check-manifest (==0.49)", "click (==8.1.7)", "coverage[toml] (==7.3.2)", "exceptiongroup (==1.2.0)", "importlib-metadata (==7.0.0)", "iniconfig (==2.0.0)", "mypy (==1.7.1)", "mypy-extensions (==1.0.0)", "packaging (==23.2)", "pathspec (==0.12.1)", "platformdirs (==4.1.0)", "pluggy (==1.3.0)", "pyproject-hooks (==1.0.0)", "pytest (==7.4.3)", "pytest-cov (==4.1.0)", "tomli (==2.0.1)", "typing-extensions (==4.9.0)", "zipp (==3.17.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "2.2.0"
|
||||
@@ -64,6 +116,39 @@ files = [
|
||||
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parse"
|
||||
version = "1.19.1"
|
||||
description = "parse() is the opposite of format()"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "parse-1.19.1-py2.py3-none-any.whl", hash = "sha256:371ed3800dc63983832159cc9373156613947707bc448b5215473a219dbd4362"},
|
||||
{file = "parse-1.19.1.tar.gz", hash = "sha256:cc3a47236ff05da377617ddefa867b7ba983819c664e1afe46249e5b469be464"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parse-type"
|
||||
version = "0.6.2"
|
||||
description = "Simplifies to build parse types based on the parse module"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*"
|
||||
files = [
|
||||
{file = "parse_type-0.6.2-py2.py3-none-any.whl", hash = "sha256:06d39a8b70fde873eb2a131141a0e79bb34a432941fb3d66fad247abafc9766c"},
|
||||
{file = "parse_type-0.6.2.tar.gz", hash = "sha256:79b1f2497060d0928bc46016793f1fca1057c4aacdf15ef876aa48d75a73a355"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
parse = {version = ">=1.18.0", markers = "python_version >= \"3.0\""}
|
||||
six = ">=1.15"
|
||||
|
||||
[package.extras]
|
||||
develop = ["build (>=0.5.1)", "coverage (>=4.4)", "pylint", "pytest (<5.0)", "pytest (>=5.0)", "pytest-cov", "pytest-html (>=1.19.0)", "ruff", "tox (>=2.8,<4.0)", "twine (>=1.13.0)", "virtualenv (<20.22.0)", "virtualenv (>=20.0.0)"]
|
||||
docs = ["Sphinx (>=1.6)", "sphinx-bootstrap-theme (>=0.6.0)"]
|
||||
testing = ["pytest (<5.0)", "pytest (>=5.0)", "pytest-html (>=1.19.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "peewee"
|
||||
version = "3.15.4"
|
||||
@@ -118,6 +203,21 @@ files = [
|
||||
[package.extras]
|
||||
plugins = ["importlib-metadata"]
|
||||
|
||||
[[package]]
|
||||
name = "python-dateutil"
|
||||
version = "2.9.0.post0"
|
||||
description = "Extensions to the standard Python datetime module"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
|
||||
files = [
|
||||
{file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
|
||||
{file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
six = ">=1.5"
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "13.3.1"
|
||||
@@ -137,6 +237,18 @@ pygments = ">=2.14.0,<3.0.0"
|
||||
[package.extras]
|
||||
jupyter = ["ipywidgets (>=7.5.1,<9)"]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.16.0"
|
||||
description = "Python 2 and 3 compatibility utilities"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
files = [
|
||||
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
|
||||
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typer"
|
||||
version = "0.7.0"
|
||||
@@ -158,7 +270,19 @@ dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2
|
||||
doc = ["cairosvg (>=2.5.2,<3.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pillow (>=9.3.0,<10.0.0)"]
|
||||
test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "pytest (>=4.4.0,<8.0.0)", "pytest-cov (>=2.10.0,<5.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<4.0.0)", "rich (>=10.11.0,<13.0.0)", "shellingham (>=1.3.0,<2.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.12.2"
|
||||
description = "Backported and Experimental Type Hints for Python 3.8+"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
|
||||
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
|
||||
]
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "63d5db59d0d54f5818a8274de848b03549d5df693c2310b67743e7e6c9e1968e"
|
||||
content-hash = "a120a93b02cdb7d460b634c268f377711e70a391395715d5635058193c108084"
|
||||
|
||||
+7
-2
@@ -1,8 +1,8 @@
|
||||
[tool.poetry]
|
||||
name = "tui-rsync"
|
||||
version = "0.8.16"
|
||||
version = "1.0.0"
|
||||
description = "tui-rsync will help you to manage yours backups."
|
||||
authors = ["Kostiantyn Klochko <kostya_klochko@ukr.net>"]
|
||||
authors = ["Kostiantyn Klochko <kklochko@protonmail.com>"]
|
||||
readme = "README.rst"
|
||||
license = "GPL-3.0-or-later"
|
||||
packages = [{include = "tui_rsync"}]
|
||||
@@ -19,10 +19,15 @@ typer = "^0.7.0"
|
||||
peewee = "^3.15.4"
|
||||
pyfzf = "^0.3.1"
|
||||
platformdirs = "^3.1.1"
|
||||
injector = "^0.22.0"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
tui-rsync = "tui_rsync.main:main"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
behave = "^1.2.6"
|
||||
faker = "^35.0.0"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
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
|
||||
@@ -0,0 +1,53 @@
|
||||
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
|
||||
@@ -1,4 +0,0 @@
|
||||
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
|
||||
@@ -1 +0,0 @@
|
||||
from tui_rsync.cli.groups.groups import groups
|
||||
@@ -1,56 +0,0 @@
|
||||
################################################################################
|
||||
# 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)
|
||||
@@ -1,63 +0,0 @@
|
||||
################################################################################
|
||||
# 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())
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
################################################################################
|
||||
# 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()
|
||||
@@ -1,56 +0,0 @@
|
||||
################################################################################
|
||||
# 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)
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
################################################################################
|
||||
# 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
|
||||
@@ -1,40 +0,0 @@
|
||||
################################################################################
|
||||
# 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]
|
||||
@@ -1 +0,0 @@
|
||||
from tui_rsync.cli.source.source import source
|
||||
@@ -1,56 +0,0 @@
|
||||
################################################################################
|
||||
# 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)
|
||||
@@ -1,64 +0,0 @@
|
||||
################################################################################
|
||||
# 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())
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
################################################################################
|
||||
# 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)
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
################################################################################
|
||||
# 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()
|
||||
@@ -1,2 +0,0 @@
|
||||
from tui_rsync.config.app import App
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
from .backup_command_port import BackupCommandPort
|
||||
from .backup_sync_command import BackupSyncCommand
|
||||
from .backup_sync_command_dry_run import BackupSyncCommandDryRun
|
||||
|
||||
__all__ = ['BackupCommandPort', 'BackupSyncCommand', 'BackupSyncCommandDryRun']
|
||||
@@ -0,0 +1,7 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class BackupCommandPort(ABC):
|
||||
@abstractmethod
|
||||
def run(self):
|
||||
pass
|
||||
@@ -0,0 +1,14 @@
|
||||
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()
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
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()
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
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']
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
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
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
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
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
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
|
||||
@@ -0,0 +1,5 @@
|
||||
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']
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
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())
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
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())
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
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)
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
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()
|
||||
@@ -0,0 +1,4 @@
|
||||
from .backup_plan_repository_port import BackupPlanRepositoryPort
|
||||
from .backup_plan_repository import BackupPlanRepository
|
||||
|
||||
__all__ = ['BackupPlanRepositoryPort', 'BackupPlanRepository']
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
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)
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
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
|
||||
@@ -0,0 +1,11 @@
|
||||
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',
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
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)
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
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()
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
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
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
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()
|
||||
@@ -0,0 +1,4 @@
|
||||
from .entities import BackupPlan
|
||||
from .value_objects import BackupPlanId, Path, Source, Destination
|
||||
|
||||
__all__ = ['BackupPlan', 'BackupPlanId', 'Path', 'Source', 'Destination']
|
||||
@@ -0,0 +1,3 @@
|
||||
from .backup_plan import BackupPlan
|
||||
|
||||
__all__ = ['BackupPlan']
|
||||
@@ -0,0 +1,14 @@
|
||||
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='')
|
||||
@@ -0,0 +1,6 @@
|
||||
from .backup_plan_id import BackupPlanId
|
||||
from .path import Path
|
||||
from .source import Source
|
||||
from .destionation import Destination
|
||||
|
||||
__all__ = ['BackupPlanId', 'Path', 'Source', 'Destination']
|
||||
@@ -0,0 +1,5 @@
|
||||
from tui_rsync.core.shared_kernel.components.common.domain import UUID
|
||||
|
||||
|
||||
class BackupPlanId(UUID):
|
||||
pass
|
||||
@@ -0,0 +1,5 @@
|
||||
from .path import Path
|
||||
|
||||
|
||||
class Destination(Path):
|
||||
pass
|
||||
@@ -0,0 +1,6 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Path:
|
||||
path: str
|
||||
@@ -0,0 +1,5 @@
|
||||
from .path import Path
|
||||
|
||||
|
||||
class Source(Path):
|
||||
pass
|
||||
@@ -0,0 +1,3 @@
|
||||
from .user_data_paths_port import UserDataPathsPort
|
||||
|
||||
__all__ = ['UserDataPathsPort']
|
||||
@@ -0,0 +1,15 @@
|
||||
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
|
||||
@@ -0,0 +1,3 @@
|
||||
from .database_manager_port import DatabaseManagerPort
|
||||
|
||||
__all__ = ['DatabaseManagerPort']
|
||||
@@ -0,0 +1,11 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class DatabaseManagerPort(ABC):
|
||||
@abstractmethod
|
||||
def get_connection(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def create_tables(self):
|
||||
pass
|
||||
@@ -0,0 +1,3 @@
|
||||
from .backup_sync_service import BackupSyncService
|
||||
|
||||
__all__ = ['BackupSyncService']
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
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()
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
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()
|
||||
@@ -0,0 +1,3 @@
|
||||
from .domain import UUID, Label
|
||||
|
||||
__all__ = ['UUID', 'Label']
|
||||
@@ -0,0 +1,3 @@
|
||||
from .value_objects import UUID, Label
|
||||
|
||||
__all__ = ['UUID', 'Label']
|
||||
@@ -0,0 +1,4 @@
|
||||
from .uuid import UUID
|
||||
from .label import Label
|
||||
|
||||
__all__ = ['UUID', 'Label']
|
||||
@@ -0,0 +1,6 @@
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Label:
|
||||
label: str = field(default='')
|
||||
@@ -0,0 +1,14 @@
|
||||
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
|
||||
@@ -0,0 +1,4 @@
|
||||
from .app_exception import AppException
|
||||
from .command_exception import CommandException
|
||||
|
||||
__all__ = ['AppException', 'CommandException']
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
class AppException(Exception):
|
||||
pass
|
||||
@@ -0,0 +1,6 @@
|
||||
from . import AppException
|
||||
|
||||
|
||||
class CommandException(AppException):
|
||||
"""Command failed to change persistence data."""
|
||||
pass
|
||||
@@ -0,0 +1,6 @@
|
||||
from . import AppException
|
||||
|
||||
|
||||
class QueryException(AppException):
|
||||
"""Query failed."""
|
||||
pass
|
||||
@@ -0,0 +1,6 @@
|
||||
from .user_data_paths import UserDataPaths
|
||||
from .current_configuration import CurrentConfiguration
|
||||
from .configuration import Configuration
|
||||
from .testing_configuration import TestingConfiguration
|
||||
|
||||
__all__ = ['UserDataPaths', 'CurrentConfiguration', 'Configuration', 'TestingConfiguration']
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user