mirror of
https://github.com/ZettaIO/restic-compose-backup.git
synced 2025-10-10 12:20:58 +00:00
Compare commits
25 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
2cbc5aa6fa | ||
|
ffa2dfc119 | ||
|
cfc92b2284 | ||
|
216202dec7 | ||
|
fab988a05e | ||
|
164834d3a9 | ||
|
a0dfb04aa7 | ||
|
7f588c57ab | ||
|
e01f7c6cff | ||
|
102073cb70 | ||
|
e060c28c93 | ||
|
14903f3bbd | ||
|
96bd419a24 | ||
|
75ab549370 | ||
|
6f06d25db5 | ||
|
0a9e5edfe4 | ||
|
130be30268 | ||
|
0af9f2e8ee | ||
|
c59f022a55 | ||
|
98fe448348 | ||
|
3708bb9100 | ||
|
d7039cccf4 | ||
|
864c026402 | ||
|
fcd18ba1cb | ||
|
915695043c |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -24,3 +24,6 @@ alerts.env
|
|||||||
# docs
|
# docs
|
||||||
build/
|
build/
|
||||||
docs/_build
|
docs/_build
|
||||||
|
|
||||||
|
# tests
|
||||||
|
.tox
|
||||||
|
13
README.md
13
README.md
@@ -38,8 +38,6 @@ Automatically detects and backs up volumes, mysql, mariadb and postgres database
|
|||||||
docker pull zettaio/restic-compose-backup
|
docker pull zettaio/restic-compose-backup
|
||||||
```
|
```
|
||||||
|
|
||||||
.. or clone this repo and build it.
|
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Required env variables for restic:
|
Required env variables for restic:
|
||||||
@@ -81,7 +79,7 @@ version: '3'
|
|||||||
services:
|
services:
|
||||||
# The backup service
|
# The backup service
|
||||||
backup:
|
backup:
|
||||||
build: restic-compose-backup
|
image: zettaio/restic-compose-backup
|
||||||
environment:
|
environment:
|
||||||
- RESTIC_REPOSITORY=<whatever restic supports>
|
- RESTIC_REPOSITORY=<whatever restic supports>
|
||||||
- RESTIC_PASSWORD=hopefullyasecturepw
|
- RESTIC_PASSWORD=hopefullyasecturepw
|
||||||
@@ -178,20 +176,19 @@ path `/databases/<service_name>/dump.sql` or similar.
|
|||||||
restic-compose-backup.postgres: true
|
restic-compose-backup.postgres: true
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## Running Tests
|
## Running Tests
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python setup.py develop
|
pip install -e src/
|
||||||
pip install -r tests/requirements.txt
|
pip install -r src/tests/requirements.txt
|
||||||
pytest tests
|
tox
|
||||||
```
|
```
|
||||||
|
|
||||||
## Building Docs
|
## Building Docs
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install -r docs/requirements.txt
|
pip install -r docs/requirements.txt
|
||||||
python setup.py build_sphinx
|
python src/setup.py build_sphinx
|
||||||
```
|
```
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
@@ -1,9 +1,10 @@
|
|||||||
version: '3'
|
version: '3'
|
||||||
services:
|
services:
|
||||||
backup:
|
backup:
|
||||||
build: .
|
build: ./src
|
||||||
env_file:
|
env_file:
|
||||||
- restic_compose_backup.env
|
- restic_compose_backup.env
|
||||||
|
- alerts.env
|
||||||
volumes:
|
volumes:
|
||||||
# Map in docker socket
|
# Map in docker socket
|
||||||
- /var/run/docker.sock:/tmp/docker.sock:ro
|
- /var/run/docker.sock:/tmp/docker.sock:ro
|
||||||
@@ -11,7 +12,7 @@ services:
|
|||||||
- ./restic_data:/restic_data
|
- ./restic_data:/restic_data
|
||||||
- ./restic_cache:/cache
|
- ./restic_cache:/cache
|
||||||
# Map in project source in dev
|
# Map in project source in dev
|
||||||
- .:/restic-compose-backup
|
- ./src:/restic-compose-backup
|
||||||
web:
|
web:
|
||||||
image: nginx
|
image: nginx
|
||||||
labels:
|
labels:
|
||||||
|
@@ -22,8 +22,7 @@ copyright = '2019, Zetta.IO Technology AS'
|
|||||||
author = 'Zetta.IO Technology AS'
|
author = 'Zetta.IO Technology AS'
|
||||||
|
|
||||||
# The full version, including alpha/beta/rc tags
|
# The full version, including alpha/beta/rc tags
|
||||||
release = '0.2.0'
|
release = '0.3.2'
|
||||||
|
|
||||||
|
|
||||||
# -- General configuration ---------------------------------------------------
|
# -- General configuration ---------------------------------------------------
|
||||||
|
|
||||||
|
@@ -1,13 +1,21 @@
|
|||||||
# Making a release
|
# Making a release
|
||||||
|
|
||||||
- Update version in setup.py
|
- Update version in `setup.py`
|
||||||
|
- Update version in `docs/conf.py`
|
||||||
|
- Update version in `restic_compose_backup/__init__.py`
|
||||||
- Build and tag image
|
- Build and tag image
|
||||||
- push: `docker push zettaio/restic-compose-backup:<version>`
|
- push: `docker push zettaio/restic-compose-backup:<version>`
|
||||||
- Ensure RTD has new docs published
|
- Ensure RTD has new docs published
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
|
When releasing a bugfix version we need to update the
|
||||||
|
main image as well.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker build . --tag zettaio/restic-compose-backup:0.2.0
|
docker build src --tag zettaio/restic-compose-backup:0.3
|
||||||
docker push zettaio/restic-compose-backup:0.2.0
|
docker build src --tag zettaio/restic-compose-backup:0.3.2
|
||||||
|
|
||||||
|
docker push zettaio/restic-compose-backup:0.3
|
||||||
|
docker push zettaio/restic-compose-backup:0.3.2
|
||||||
```
|
```
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
[pytest]
|
[pytest]
|
||||||
testpaths = tests
|
testpaths = src/tests
|
||||||
python_files=test*.py
|
python_files=test*.py
|
||||||
addopts = -v --verbose
|
addopts = -v --verbose
|
||||||
|
@@ -1,37 +0,0 @@
|
|||||||
"""
|
|
||||||
"""
|
|
||||||
import smtplib
|
|
||||||
from email.mime.text import MIMEText
|
|
||||||
|
|
||||||
EMAIL_HOST = "smtp.gmail.com"
|
|
||||||
EMAIL_PORT = 465
|
|
||||||
EMAIL_HOST_USER = ""
|
|
||||||
EMAIL_HOST_PASSWORD = ""
|
|
||||||
EMAIL_SEND_TO = ['']
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
send_mail("Hello world!")
|
|
||||||
|
|
||||||
|
|
||||||
def send_mail(text):
|
|
||||||
msg = MIMEText(text)
|
|
||||||
msg['Subject'] = "Message from restic-compose-backup"
|
|
||||||
msg['From'] = EMAIL_HOST_USER
|
|
||||||
msg['To'] = ', '.join(EMAIL_SEND_TO)
|
|
||||||
|
|
||||||
try:
|
|
||||||
print("Connecting to {} port {}".format(EMAIL_HOST, EMAIL_PORT))
|
|
||||||
server = smtplib.SMTP_SSL(EMAIL_HOST, EMAIL_PORT)
|
|
||||||
server.ehlo()
|
|
||||||
server.login(EMAIL_HOST_USER, EMAIL_HOST_PASSWORD)
|
|
||||||
server.sendmail(EMAIL_HOST_USER, EMAIL_SEND_TO, msg.as_string())
|
|
||||||
print('Email Sent')
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
finally:
|
|
||||||
server.close()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
1
src/restic_compose_backup/__init__.py
Normal file
1
src/restic_compose_backup/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
__version__ = '0.3.2'
|
@@ -2,7 +2,6 @@ import logging
|
|||||||
|
|
||||||
from restic_compose_backup.alerts.smtp import SMTPAlert
|
from restic_compose_backup.alerts.smtp import SMTPAlert
|
||||||
from restic_compose_backup.alerts.discord import DiscordWebhookAlert
|
from restic_compose_backup.alerts.discord import DiscordWebhookAlert
|
||||||
from restic_compose_backup.config import Config
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -24,6 +23,7 @@ def send(subject: str = None, body: str = None, alert_type: str = 'INFO'):
|
|||||||
)
|
)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.error("Exception raised when sending alert [%s]: %s", instance.name, ex)
|
logger.error("Exception raised when sending alert [%s]: %s", instance.name, ex)
|
||||||
|
logger.exception(ex)
|
||||||
|
|
||||||
if len(alert_classes) == 0:
|
if len(alert_classes) == 0:
|
||||||
logger.info("No alerts configured")
|
logger.info("No alerts configured")
|
||||||
@@ -36,7 +36,7 @@ def configured_alert_types():
|
|||||||
|
|
||||||
for cls in BACKENDS:
|
for cls in BACKENDS:
|
||||||
instance = cls.create_from_env()
|
instance = cls.create_from_env()
|
||||||
logger.debug("Alert backend '%s' configured: %s", cls.name, instance != None)
|
logger.debug("Alert backend '%s' configured: %s", cls.name, instance is not None)
|
||||||
if instance:
|
if instance:
|
||||||
entires.append(instance)
|
entires.append(instance)
|
||||||
|
|
@@ -1,6 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from restic_compose_backup.alerts.base import BaseAlert
|
from restic_compose_backup.alerts.base import BaseAlert
|
||||||
@@ -41,6 +40,6 @@ class DiscordWebhookAlert(BaseAlert):
|
|||||||
}
|
}
|
||||||
response = requests.post(self.url, params={'wait': True}, json=data)
|
response = requests.post(self.url, params={'wait': True}, json=data)
|
||||||
if response.status_code not in self.success_codes:
|
if response.status_code not in self.success_codes:
|
||||||
log.error("Discord webhook failed: %s: %s", response.status_code, response.content)
|
logger.error("Discord webhook failed: %s: %s", response.status_code, response.content)
|
||||||
else:
|
else:
|
||||||
logger.info('Discord webhook successful')
|
logger.info('Discord webhook successful')
|
@@ -51,6 +51,6 @@ class SMTPAlert(BaseAlert):
|
|||||||
server.sendmail(self.user, self.to, msg.as_string())
|
server.sendmail(self.user, self.to, msg.as_string())
|
||||||
logger.info('Email sent')
|
logger.info('Email sent')
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.error(ex)
|
logger.exception(ex)
|
||||||
finally:
|
finally:
|
||||||
server.close()
|
server.close()
|
@@ -35,7 +35,13 @@ def run(image: str = None, command: str = None, volumes: dict = None,
|
|||||||
line = ""
|
line = ""
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
line += next(stream).decode()
|
# TODO: figure out why..
|
||||||
|
# Apparently the stream can be bytes or strings.
|
||||||
|
data = next(stream)
|
||||||
|
if isinstance(data, bytes):
|
||||||
|
line += data.decode()
|
||||||
|
elif isinstance(data, str):
|
||||||
|
line += data
|
||||||
if line.endswith('\n'):
|
if line.endswith('\n'):
|
||||||
break
|
break
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
@@ -51,9 +57,9 @@ def run(image: str = None, command: str = None, volumes: dict = None,
|
|||||||
fd.write('\n')
|
fd.write('\n')
|
||||||
logger.info(line)
|
logger.info(line)
|
||||||
|
|
||||||
|
|
||||||
container.reload()
|
container.reload()
|
||||||
logger.debug("Container ExitCode %s", container.attrs['State']['ExitCode'])
|
logger.debug("Container ExitCode %s", container.attrs['State']['ExitCode'])
|
||||||
|
container.stop()
|
||||||
container.remove()
|
container.remove()
|
||||||
|
|
||||||
return container.attrs['State']['ExitCode']
|
return container.attrs['State']['ExitCode']
|
@@ -1,5 +1,4 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import pprint
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from restic_compose_backup import (
|
from restic_compose_backup import (
|
||||||
@@ -43,13 +42,21 @@ def main():
|
|||||||
elif args.action == 'alert':
|
elif args.action == 'alert':
|
||||||
alert(config, containers)
|
alert(config, containers)
|
||||||
|
|
||||||
|
elif args.action == 'version':
|
||||||
|
import restic_compose_backup
|
||||||
|
print(restic_compose_backup.__version__)
|
||||||
|
|
||||||
|
|
||||||
def status(config, containers):
|
def status(config, containers):
|
||||||
"""Outputs the backup config for the compose setup"""
|
"""Outputs the backup config for the compose setup"""
|
||||||
logger.info("Status for compose project '%s'", containers.project_name)
|
logger.info("Status for compose project '%s'", containers.project_name)
|
||||||
|
logger.info("Repository: '%s'", config.repository)
|
||||||
logger.info("Backup currently running?: %s", containers.backup_process_running)
|
logger.info("Backup currently running?: %s", containers.backup_process_running)
|
||||||
logger.info("%s Detected Config %s", "-" * 25, "-" * 25)
|
logger.info("%s Detected Config %s", "-" * 25, "-" * 25)
|
||||||
|
|
||||||
|
logger.info("Initializing repository (may fail if already initalized)")
|
||||||
|
restic.init_repo(config.repository)
|
||||||
|
|
||||||
backup_containers = containers.containers_for_backup()
|
backup_containers = containers.containers_for_backup()
|
||||||
for container in backup_containers:
|
for container in backup_containers:
|
||||||
logger.info('service: %s', container.service_name)
|
logger.info('service: %s', container.service_name)
|
||||||
@@ -63,10 +70,11 @@ def status(config, containers):
|
|||||||
ping = instance.ping()
|
ping = instance.ping()
|
||||||
logger.info(' - %s (is_ready=%s)', instance.container_type, ping == 0)
|
logger.info(' - %s (is_ready=%s)', instance.container_type, ping == 0)
|
||||||
if ping != 0:
|
if ping != 0:
|
||||||
logger.error("Database '%s' in service %s cannot be reached", instance.container_type, container.service_name)
|
logger.error("Database '%s' in service %s cannot be reached",
|
||||||
|
instance.container_type, container.service_name)
|
||||||
|
|
||||||
if len(backup_containers) == 0:
|
if len(backup_containers) == 0:
|
||||||
logger.info("No containers in the project has 'restic-compose-backup.enabled' label")
|
logger.info("No containers in the project has 'restic-compose-backup.*' label")
|
||||||
|
|
||||||
logger.info("-" * 67)
|
logger.info("-" * 67)
|
||||||
|
|
||||||
@@ -77,11 +85,6 @@ def backup(config, containers):
|
|||||||
if containers.backup_process_running:
|
if containers.backup_process_running:
|
||||||
raise ValueError("Backup process already running")
|
raise ValueError("Backup process already running")
|
||||||
|
|
||||||
logger.info("Initializing repository (may fail if already initalized)")
|
|
||||||
|
|
||||||
# TODO: Errors when repo already exists
|
|
||||||
restic.init_repo(config.repository)
|
|
||||||
|
|
||||||
# Map all volumes from the backup container into the backup process container
|
# Map all volumes from the backup container into the backup process container
|
||||||
volumes = containers.this_container.volumes
|
volumes = containers.this_container.volumes
|
||||||
|
|
||||||
@@ -89,6 +92,7 @@ def backup(config, containers):
|
|||||||
mounts = containers.generate_backup_mounts('/volumes')
|
mounts = containers.generate_backup_mounts('/volumes')
|
||||||
volumes.update(mounts)
|
volumes.update(mounts)
|
||||||
|
|
||||||
|
try:
|
||||||
result = backup_runner.run(
|
result = backup_runner.run(
|
||||||
image=containers.this_container.image,
|
image=containers.this_container.image,
|
||||||
command='restic-compose-backup start-backup-process',
|
command='restic-compose-backup start-backup-process',
|
||||||
@@ -100,6 +104,15 @@ def backup(config, containers):
|
|||||||
"com.docker.compose.project": containers.project_name,
|
"com.docker.compose.project": containers.project_name,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
except Exception as ex:
|
||||||
|
logger.exception(ex)
|
||||||
|
alerts.send(
|
||||||
|
subject="Exception during backup",
|
||||||
|
body=str(ex),
|
||||||
|
alert_type='ERROR',
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
logger.info('Backup container exit code: %s', result)
|
logger.info('Backup container exit code: %s', result)
|
||||||
|
|
||||||
# Alert the user if something went wrong
|
# Alert the user if something went wrong
|
||||||
@@ -133,7 +146,7 @@ def start_backup_process(config, containers):
|
|||||||
logger.error('Backup command exited with non-zero code: %s', vol_result)
|
logger.error('Backup command exited with non-zero code: %s', vol_result)
|
||||||
errors = True
|
errors = True
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.error(ex)
|
logger.exception(ex)
|
||||||
errors = True
|
errors = True
|
||||||
|
|
||||||
# back up databases
|
# back up databases
|
||||||
@@ -148,7 +161,7 @@ def start_backup_process(config, containers):
|
|||||||
logger.error('Backup command exited with non-zero code: %s', result)
|
logger.error('Backup command exited with non-zero code: %s', result)
|
||||||
errors = True
|
errors = True
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.error(ex)
|
logger.exception(ex)
|
||||||
errors = True
|
errors = True
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
@@ -173,7 +186,8 @@ def cleanup(config, containers):
|
|||||||
)
|
)
|
||||||
logger.info('Prune stale data freeing storage space')
|
logger.info('Prune stale data freeing storage space')
|
||||||
prune_result = restic.prune(config.repository)
|
prune_result = restic.prune(config.repository)
|
||||||
return forget_result == 0 and prune_result == 0
|
return forget_result and prune_result
|
||||||
|
|
||||||
|
|
||||||
def snapshots(config, containers):
|
def snapshots(config, containers):
|
||||||
"""Display restic snapshots"""
|
"""Display restic snapshots"""
|
||||||
@@ -195,7 +209,7 @@ def parse_args():
|
|||||||
parser = argparse.ArgumentParser(prog='restic_compose_backup')
|
parser = argparse.ArgumentParser(prog='restic_compose_backup')
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'action',
|
'action',
|
||||||
choices=['status', 'snapshots', 'backup', 'start-backup-process', 'alert', 'cleanup'],
|
choices=['status', 'snapshots', 'backup', 'start-backup-process', 'alert', 'cleanup', 'version'],
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--log-level',
|
'--log-level',
|
@@ -6,7 +6,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
def test():
|
def test():
|
||||||
return run_command(['ls', '/volumes'])
|
return run(['ls', '/volumes'])
|
||||||
|
|
||||||
|
|
||||||
def ping_mysql(host, port, username) -> int:
|
def ping_mysql(host, port, username) -> int:
|
||||||
@@ -23,7 +23,7 @@ def ping_mysql(host, port, username) -> int:
|
|||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
def ping_mariadb(host, port, username): #, password) -> int:
|
def ping_mariadb(host, port, username) -> int:
|
||||||
"""Check if the mariadb is up and can be reached"""
|
"""Check if the mariadb is up and can be reached"""
|
||||||
return run([
|
return run([
|
||||||
'mysqladmin',
|
'mysqladmin',
|
@@ -68,7 +68,7 @@ class Container:
|
|||||||
def get_config_env(self, name) -> str:
|
def get_config_env(self, name) -> str:
|
||||||
"""Get a config environment variable by name"""
|
"""Get a config environment variable by name"""
|
||||||
# convert to dict and fetch env var by name
|
# convert to dict and fetch env var by name
|
||||||
data = {i[0:i.find('=')]: i[i.find('=')+1:] for i in self.environment}
|
data = {i[0:i.find('=')]: i[i.find('=') + 1:] for i in self.environment}
|
||||||
return data.get(name)
|
return data.get(name)
|
||||||
|
|
||||||
def set_config_env(self, name, value):
|
def set_config_env(self, name, value):
|
@@ -13,6 +13,7 @@ LOG_LEVELS = {
|
|||||||
'error': logging.ERROR,
|
'error': logging.ERROR,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def setup(level: str = 'warning'):
|
def setup(level: str = 'warning'):
|
||||||
"""Set up logging"""
|
"""Set up logging"""
|
||||||
level = level or ""
|
level = level or ""
|
@@ -68,7 +68,7 @@ def snapshots(repository: str, last=True) -> Tuple[str, str]:
|
|||||||
|
|
||||||
|
|
||||||
def forget(repository: str, daily: str, weekly: str, monthly: str, yearly: str):
|
def forget(repository: str, daily: str, weekly: str, monthly: str, yearly: str):
|
||||||
return restic(repository, [
|
return commands.run(restic(repository, [
|
||||||
'forget',
|
'forget',
|
||||||
'--keep-daily',
|
'--keep-daily',
|
||||||
daily,
|
daily,
|
||||||
@@ -78,13 +78,13 @@ def forget(repository: str, daily: str, weekly: str, monthly: str, yearly: str):
|
|||||||
monthly,
|
monthly,
|
||||||
'--keep-yearly',
|
'--keep-yearly',
|
||||||
yearly,
|
yearly,
|
||||||
])
|
]))
|
||||||
|
|
||||||
|
|
||||||
def prune(repository: str):
|
def prune(repository: str):
|
||||||
return restic(repository, [
|
return commands.run(restic(repository, [
|
||||||
'prune',
|
'prune',
|
||||||
])
|
]))
|
||||||
|
|
||||||
|
|
||||||
def check(repository: str):
|
def check(repository: str):
|
@@ -3,12 +3,12 @@ from setuptools import setup, find_namespace_packages
|
|||||||
setup(
|
setup(
|
||||||
name="restic-compose-backup",
|
name="restic-compose-backup",
|
||||||
url="https://github.com/ZettaIO/restic-compose-backup",
|
url="https://github.com/ZettaIO/restic-compose-backup",
|
||||||
version="0.3.0",
|
version="0.3.2",
|
||||||
author="Einar Forselv",
|
author="Einar Forselv",
|
||||||
author_email="eforselv@gmail.com",
|
author_email="eforselv@gmail.com",
|
||||||
packages=find_namespace_packages(include=['restic_compose_backup']),
|
packages=find_namespace_packages(include=['restic_compose_backup']),
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'docker==3.7.2',
|
'docker==4.1.*',
|
||||||
],
|
],
|
||||||
entry_points={'console_scripts': [
|
entry_points={'console_scripts': [
|
||||||
'restic-compose-backup = restic_compose_backup.cli:main',
|
'restic-compose-backup = restic_compose_backup.cli:main',
|
@@ -1 +1,2 @@
|
|||||||
pytest==4.3.1
|
pytest==4.3.1
|
||||||
|
tox
|
56
tox.ini
Normal file
56
tox.ini
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# Ensure that this file do not contain non-ascii characters
|
||||||
|
# as flake8 can fail to parse the file on OS X and Windows
|
||||||
|
|
||||||
|
[tox]
|
||||||
|
skipsdist = True
|
||||||
|
setupdir={toxinidir}/src
|
||||||
|
envlist =
|
||||||
|
py37
|
||||||
|
pep8
|
||||||
|
|
||||||
|
[testenv]
|
||||||
|
usedevelop = True
|
||||||
|
basepython =
|
||||||
|
py37: python3.7
|
||||||
|
|
||||||
|
deps =
|
||||||
|
-r{toxinidir}/tests/requirements.txt
|
||||||
|
commands =
|
||||||
|
; coverage run --source=restic_compose_backup -m pytest tests/
|
||||||
|
; coverage report
|
||||||
|
pytest
|
||||||
|
|
||||||
|
[testenv:pep8]
|
||||||
|
usedevelop = false
|
||||||
|
deps = flake8
|
||||||
|
basepython = python3.7
|
||||||
|
commands = flake8
|
||||||
|
|
||||||
|
[pytest]
|
||||||
|
norecursedirs = tests/* .venv/* .tox/* build/ docs/
|
||||||
|
|
||||||
|
[flake8]
|
||||||
|
# H405: multi line docstring summary not separated with an empty line
|
||||||
|
# D100: Missing docstring in public module
|
||||||
|
# D101: Missing docstring in public class
|
||||||
|
# D102: Missing docstring in public method
|
||||||
|
# D103: Missing docstring in public function
|
||||||
|
# D104: Missing docstring in public package
|
||||||
|
# D105: Missing docstring in magic method
|
||||||
|
# D200: One-line docstring should fit on one line with quotes
|
||||||
|
# D202: No blank lines allowed after function docstring
|
||||||
|
# D203: 1 blank required before class docstring.
|
||||||
|
# D204: 1 blank required after class docstring
|
||||||
|
# D205: Blank line required between one-line summary and description.
|
||||||
|
# D207: Docstring is under-indented
|
||||||
|
# D208: Docstring is over-indented
|
||||||
|
# D211: No blank lines allowed before class docstring
|
||||||
|
# D301: Use r""" if any backslashes in a docstring
|
||||||
|
# D400: First line should end with a period.
|
||||||
|
# D401: First line should be in imperative mood.
|
||||||
|
# *** E302 expected 2 blank lines, found 1
|
||||||
|
# *** W503 line break before binary operator
|
||||||
|
ignore = H405,D100,D101,D102,D103,D104,D105,D200,D202,D203,D204,D205,D211,D301,D400,D401,W503
|
||||||
|
show-source = True
|
||||||
|
max-line-length = 120
|
||||||
|
exclude = .tox,.venv*,tests,build,conf.py
|
Reference in New Issue
Block a user