├── .github ├── reaction.yml ├── release-drafter.yml ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── dependabot.yml ├── workflows │ ├── pythonpublish.yml │ └── pythonpackage.yml ├── config.yml └── stale.yml ├── cli2telegram ├── const.py ├── __init__.py ├── config.py ├── util.py ├── cli.py └── daemon.py ├── cli2telegram.toml_example ├── cli2telegram.service_example ├── tests ├── __init__.py └── test_message_processing.py ├── pyproject.toml ├── .gitignore ├── README.md ├── poetry.lock └── LICENSE /.github/reaction.yml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /cli2telegram/const.py: -------------------------------------------------------------------------------- 1 | CODEBLOCK_MARKER = "```" 2 | CODEBLOCK_MARKER_START = f"{CODEBLOCK_MARKER}\n" 3 | CODEBLOCK_MARKER_END = f"\n{CODEBLOCK_MARKER}" 4 | 5 | TELEGRAM_MESSAGE_LENGTH_LIMIT = 4096 6 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | categories: 2 | - title: 🚀 Features and ✨ Enhancements 3 | label: enhancement 4 | - title: 🐛 Bugfixes 5 | label: bug 6 | change-template: "* $TITLE (#$NUMBER) by @$AUTHOR" 7 | template: | 8 | ## What’s Changed 9 | 10 | $CHANGES 11 | -------------------------------------------------------------------------------- /cli2telegram.toml_example: -------------------------------------------------------------------------------- 1 | [cli2telegram] 2 | log_level="WARNING" 3 | 4 | [cli2telegram.telegram] 5 | bot_token="1234:6783948932dsdsadsadas" 6 | chat_id="12345678" 7 | 8 | [cli2telegram.retry] 9 | enabled="True" 10 | timeout="10s" 11 | give_up_after="4h" 12 | 13 | [cli2telegram.daemon] 14 | pipe_path="/tmp/cli2telegram" 15 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [markusressel] 4 | # patreon: # Replace with a single Patreon username 5 | # open_collective: # Replace with a single Open Collective username 6 | # ko_fi: # Replace with a single Ko-fi username 7 | # tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | # community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | # liberapay: TheAlgorithms 10 | # issuehunt: # Replace with a single IssueHunt username 11 | # otechie: # Replace with a single Otechie username 12 | # custom: ['https://paypal.me/markusressel/1'] -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. Linux] 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | registries: 3 | python-index-pypi-python-org-simple: 4 | type: python-index 5 | url: https://pypi.python.org/simple/ 6 | username: "${{secrets.PYTHON_INDEX_PYPI_PYTHON_ORG_SIMPLE_USERNAME}}" 7 | password: "${{secrets.PYTHON_INDEX_PYPI_PYTHON_ORG_SIMPLE_PASSWORD}}" 8 | 9 | updates: 10 | - package-ecosystem: github-actions 11 | directory: "/" 12 | schedule: 13 | # Check for updates to GitHub Actions every week 14 | interval: "weekly" 15 | - package-ecosystem: pip 16 | insecure-external-code-execution: allow 17 | directory: "/" 18 | schedule: 19 | interval: daily 20 | time: "16:00" 21 | timezone: Europe/Berlin 22 | open-pull-requests-limit: 10 23 | registries: 24 | - python-index-pypi-python-org-simple 25 | -------------------------------------------------------------------------------- /cli2telegram.service_example: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=cli2telegram Daemon 3 | After=syslog.target network.target 4 | 5 | [Service] 6 | # Change the user and group variables here. 7 | User=cli2telegram 8 | Group=cli2telegram 9 | 10 | Type=simple 11 | 12 | # Change the path to cli2telegram here 13 | ExecStart=/path/to/cli2telegram -d 14 | TimeoutStopSec=20 15 | KillMode=process 16 | Restart=on-failure 17 | StandardOutput=file:/var/log/cli2telegram.log 18 | StandardError=file:/var/log/cli2telegram.err 19 | 20 | # These lines optionally isolate (sandbox) cli2telegram 21 | # Make sure to add any paths it might use to the list below (space-separated). 22 | #ReadWritePaths=/path/to/cli2telegram /path/to/named/pipe 23 | #ProtectSystem=strict 24 | #PrivateDevices=true 25 | #ProtectHome=true 26 | 27 | [Install] 28 | WantedBy=multi-user.target 29 | -------------------------------------------------------------------------------- /.github/workflows/pythonpublish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [ created ] 6 | 7 | env: 8 | GITHUB_RELEASE: True 9 | 10 | jobs: 11 | deploy: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Set up Python 16 | uses: actions/setup-python@v5 17 | with: 18 | python-version: '3.x' 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade pip 22 | pip install setuptools wheel poetry 23 | - name: Build and publish 24 | run: | 25 | poetry build 26 | - name: Publish a Python distribution to PyPI 27 | uses: pypa/gh-action-pypi-publish@release/v1 28 | with: 29 | user: __token__ 30 | password: ${{ secrets.PYPI_PASSWORD }} 31 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # cli2telegram 2 | # Copyright (c) 2019. Markus Ressel 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as 6 | # published by the Free Software Foundation, either version 3 of the 7 | # License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | import unittest 18 | 19 | 20 | class TestBase(unittest.TestCase): 21 | if __name__ == '__main__': 22 | unittest.main() 23 | -------------------------------------------------------------------------------- /.github/config.yml: -------------------------------------------------------------------------------- 1 | # Configuration for new-issue-welcome - https://github.com/behaviorbot/new-issue-welcome 2 | 3 | # Comment to be posted to on first time issues 4 | newIssueWelcomeComment: > 5 | Thanks for opening your first issue here! :tada: 6 | 7 | # Configuration for new-pr-welcome - https://github.com/behaviorbot/new-pr-welcome 8 | 9 | # Comment to be posted to on PRs from first time contributors in your repository 10 | newPRWelcomeComment: > 11 | Thanks for opening this pull request! :nerd_face: 12 | 13 | # Configuration for first-pr-merge - https://github.com/behaviorbot/first-pr-merge 14 | 15 | # Comment to be posted to on pull requests merged by a first time user 16 | firstPRMergeComment: > 17 | Congrats on merging your first pull request here! You should be proud of yourself :1st_place_medal: 18 | ![Congratulations](https://media.giphy.com/media/ehhuGD0nByYxO/giphy.gif) 19 | 20 | # It is recommend to include as many gifs and emojis as possible 21 | -------------------------------------------------------------------------------- /.github/workflows/pythonpackage.yml: -------------------------------------------------------------------------------- 1 | name: Python package 2 | 3 | on: [ push ] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | max-parallel: 4 11 | matrix: 12 | python-version: [ 3.7, 3.8, 3.9, "3.10" ] 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Set up Python ${{ matrix.python-version }} 17 | uses: actions/setup-python@v5 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | pip install poetry 24 | poetry install 25 | - name: Lint with flake8 26 | run: | 27 | pip install flake8 28 | # stop the build if there are Python syntax errors or undefined names 29 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 30 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 31 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 32 | - name: Test with pytest 33 | run: | 34 | cd tests 35 | poetry run pytest 36 | -------------------------------------------------------------------------------- /cli2telegram/__init__.py: -------------------------------------------------------------------------------- 1 | # cli2telegram 2 | # Copyright (c) 2019. Markus Ressel 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as 6 | # published by the Free Software Foundation, either version 3 of the 7 | # License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | import datetime 17 | from datetime import timedelta 18 | 19 | 20 | class RetryLimitReachedException(Exception): 21 | """ 22 | Helper exception to handle giving up retries 23 | """ 24 | 25 | def __init__(self, started_trying: datetime, tried_for: timedelta): 26 | self.message = f"All retries failed after {tried_for}. Started trying at {started_trying}." 27 | super().__init__(self.message) 28 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "cli2telegram" 3 | version = "2.0.0" 4 | description = "Small utility to send Telegram messages from the CLI." 5 | 6 | license = "AGPL-3.0-or-later" 7 | 8 | authors = [ 9 | "Markus Ressel ", 10 | ] 11 | 12 | readme = 'README.md' 13 | 14 | repository = "https://github.com/markusressel/cli2telegram" 15 | homepage = "https://github.com/markusressel/cli2telegram" 16 | 17 | keywords = ['cli', 'telegram', 'notification'] 18 | 19 | classifiers = [ 20 | "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)", 21 | "Programming Language :: Python :: 3 :: Only", 22 | "Programming Language :: Python :: 3", 23 | 'Programming Language :: Python :: 3.7', 24 | 'Programming Language :: Python :: 3.8', 25 | 'Programming Language :: Python :: 3.9', 26 | "Programming Language :: Python :: 3.10", 27 | "Development Status :: 5 - Production/Stable" 28 | ] 29 | 30 | [build-system] 31 | requires = ["poetry-core>=1.0.0"] 32 | build-backend = "poetry.core.masonry.api" 33 | 34 | [tool.poetry.dependencies] 35 | python = "^3.7" # Compatible python versions must be declared here 36 | 37 | container-app-conf = "*" 38 | click = "*" 39 | emoji = "*" 40 | python-telegram-bot = "*" 41 | aiofiles = "*" 42 | 43 | [tool.poetry.dev-dependencies] 44 | pytest = "*" 45 | 46 | [tool.pytest.ini_options] 47 | testpaths = [ 48 | "tests", 49 | ] 50 | 51 | [tool.poetry.scripts] 52 | cli2telegram = 'cli2telegram.cli:cli' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | 107 | # PyCharm 108 | .idea 109 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-stale - https://github.com/probot/stale 2 | 3 | # Number of days of inactivity before an Issue or Pull Request becomes stale 4 | daysUntilStale: 60 5 | 6 | # Number of days of inactivity before an Issue or Pull Request with the stale label is closed. 7 | # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. 8 | daysUntilClose: 14 9 | 10 | # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable 11 | exemptLabels: 12 | - pinned 13 | - security 14 | - bug 15 | - enhancement 16 | 17 | # Set to true to ignore issues in a project (defaults to false) 18 | exemptProjects: false 19 | 20 | # Set to true to ignore issues in a milestone (defaults to false) 21 | exemptMilestones: false 22 | 23 | # Set to true to ignore issues with an assignee (defaults to false) 24 | exemptAssignees: true 25 | 26 | # Label to use when marking as stale 27 | staleLabel: wontfix 28 | 29 | # Comment to post when marking as stale. Set to `false` to disable 30 | markComment: > 31 | This issue has been automatically marked as stale because it has not had 32 | recent activity. It will be closed if no further activity occurs. Thank you 33 | for your contributions. 34 | 35 | # Comment to post when removing the stale label. 36 | # unmarkComment: > 37 | # Your comment here. 38 | 39 | # Comment to post when closing a stale Issue or Pull Request. 40 | closeComment: > 41 | There has been no incentive by contributors or maintainers to revive this stale issue and it will now be closed. 42 | 43 | # Limit the number of actions per hour, from 1-30. Default is 30 44 | limitPerRun: 30 45 | 46 | # Limit to only `issues` or `pulls` 47 | only: issues 48 | 49 | # Optionally, specify configuration settings that are specific to just 'issues' or 'pulls': 50 | # pulls: 51 | # daysUntilStale: 30 52 | # markComment: > 53 | # This pull request has been automatically marked as stale because it has not had 54 | # recent activity. It will be closed if no further activity occurs. Thank you 55 | # for your contributions. 56 | 57 | # issues: 58 | # exemptLabels: 59 | # - confirmed 60 | -------------------------------------------------------------------------------- /cli2telegram/config.py: -------------------------------------------------------------------------------- 1 | # cli2telegram 2 | # Copyright (c) 2019. Markus Ressel 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as 6 | # published by the Free Software Foundation, either version 3 of the 7 | # License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | import logging 17 | import re 18 | 19 | from container_app_conf import ConfigBase 20 | from container_app_conf.entry.bool import BoolConfigEntry 21 | from container_app_conf.entry.int import IntConfigEntry 22 | from container_app_conf.entry.string import StringConfigEntry 23 | from container_app_conf.entry.timedelta import TimeDeltaConfigEntry 24 | from container_app_conf.source.env_source import EnvSource 25 | from container_app_conf.source.toml_source import TomlSource 26 | from container_app_conf.source.yaml_source import YamlSource 27 | 28 | FILE_NAME = "cli2telegram" 29 | 30 | KEY_ROOT = "cli2telegram" 31 | KEY_TELEGRAM = "telegram" 32 | KEY_RETRY = "retry" 33 | 34 | KEY_DAEMON = "daemon" 35 | 36 | 37 | class Config(ConfigBase): 38 | 39 | def __new__(cls, *args, **kwargs): 40 | data_sources = [ 41 | EnvSource(), 42 | YamlSource(FILE_NAME), 43 | TomlSource(FILE_NAME) 44 | ] 45 | kwargs["data_sources"] = data_sources 46 | return super(Config, cls).__new__(cls, *args, **kwargs) 47 | 48 | LOG_LEVEL = StringConfigEntry( 49 | description="Log level", 50 | key_path=[ 51 | KEY_ROOT, 52 | "log_level" 53 | ], 54 | regex=re.compile(f"{'|'.join(logging._nameToLevel.keys())}", flags=re.IGNORECASE), 55 | default="INFO", 56 | ) 57 | 58 | TELEGRAM_BOT_TOKEN = StringConfigEntry( 59 | key_path=[KEY_ROOT, KEY_TELEGRAM, "bot_token"], 60 | description="ID of the telegram chat to send messages to.", 61 | required=True, 62 | secret=True, 63 | example="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11" 64 | ) 65 | 66 | TELEGRAM_CHAT_ID = StringConfigEntry( 67 | key_path=[KEY_ROOT, KEY_TELEGRAM, "chat_id"], 68 | description="ID of the telegram chat to send messages to.", 69 | required=True, 70 | secret=True, 71 | example="-123456789" 72 | ) 73 | 74 | RETRY_ENABLED = BoolConfigEntry( 75 | key_path=[KEY_ROOT, KEY_RETRY, "enabled"], 76 | description="Whether to retry sending messages or not.", 77 | default=True, 78 | ) 79 | 80 | RETRY_TIMEOUT = TimeDeltaConfigEntry( 81 | key_path=[KEY_ROOT, KEY_RETRY, "timeout"], 82 | description="Timeout between tries.", 83 | default="10s", 84 | ) 85 | 86 | RETRY_GIVE_UP_AFTER = TimeDeltaConfigEntry( 87 | key_path=[KEY_ROOT, KEY_RETRY, "give_up_after"], 88 | description="Time interval after which the retry should be cancelled.", 89 | default="1h", 90 | ) 91 | 92 | DAEMON_PIPE_PATH = StringConfigEntry( 93 | key_path=[KEY_ROOT, KEY_DAEMON, "pipe_path"], 94 | description="Unix named pipe path.", 95 | default="/tmp/cli2telegram", 96 | example="/path/to/some/named/pipe" 97 | ) 98 | 99 | DAEMON_PIPE_PERMISSIONS = IntConfigEntry( 100 | key_path=[KEY_ROOT, KEY_DAEMON, "pipe_permissions"], 101 | description="Unix file permissions for the named pipe.", 102 | default=0o666, 103 | ) 104 | -------------------------------------------------------------------------------- /tests/test_message_processing.py: -------------------------------------------------------------------------------- 1 | # cli2telegram 2 | # Copyright (c) 2019. Markus Ressel 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as 6 | # published by the Free Software Foundation, either version 3 of the 7 | # License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | from cli2telegram.cli import prepare_messages 17 | from cli2telegram.util import prepare_code_message, split_message 18 | from tests import TestBase 19 | 20 | 21 | class MessageProcessingTest(TestBase): 22 | 23 | def test_long_message_with_codeblock_doesnt_exceed_limit(self): 24 | std_in = "A" * 4096 + "B" * 4096 25 | messages = prepare_messages(std_in, True) 26 | 27 | assert len(messages) == 3 28 | for m in messages: 29 | assert len(m) <= 4096 30 | assert "B" not in messages[0] 31 | assert "A" in messages[1] 32 | assert "B" in messages[1] 33 | assert "A" not in messages[2] 34 | 35 | def test_long_message_doesnt_exceed_limit(self): 36 | std_in = "A" * 4096 + "B" * 4096 37 | messages = prepare_messages(std_in, False) 38 | 39 | assert len(messages) == 2 40 | for i, m in enumerate(messages): 41 | if i < len(messages) - 1: 42 | assert len(m) == 4096 43 | else: 44 | assert len(m) <= 4096 45 | assert "B" not in messages[0] 46 | assert "A" not in messages[1] 47 | 48 | def test_long_message_is_split(self): 49 | length = 32 50 | std_in = "A" * length + "B" * length 51 | messages = split_message(std_in, length) 52 | 53 | assert len(messages) == 2 54 | for i, m in enumerate(messages): 55 | if i < len(messages) - 1: 56 | assert len(m) == length 57 | else: 58 | assert len(m) <= length 59 | assert "B" not in messages[0] 60 | assert "A" not in messages[1] 61 | 62 | def test_prepare_code_message(self): 63 | message = "Code" 64 | output = prepare_code_message(message) 65 | 66 | assert output.startswith("```\n") 67 | assert output.endswith("\n```") 68 | assert message[0] in output 69 | 70 | def test_newlines_are_retained_in_code_block(self): 71 | std_in = "\nCo\nde\n" 72 | output = prepare_code_message(std_in) 73 | 74 | assert output == "```\n\nCo\nde\n\n```" 75 | 76 | def test_message_preparation(self): 77 | std_in = """ZFS has finished a scrub:\n 78 | \n 79 | eid: 3337\n 80 | class: scrub_finish\n 81 | host: \n 82 | time: 2019-12-01 19:36:01+0100\n 83 | pool: rpool\n 84 | state: ONLINE\n 85 | scan: scrub repaired 0B in 0 days 00:00:24 with 0 errors on Sun Dec 1 19:36:01 2019\n 86 | config:\n 87 | \n 88 | \tNAME STATE READ WRITE CKSUM\n 89 | \trpool ONLINE 0 0 0\n 90 | \t mirror-0 ONLINE 0 0 0\n 91 | \t ata-Samsung_SSD_860_EVO_500GB_S4XBNF1M807031E-part2 ONLINE 0 0 0\n 92 | \t ata-Samsung_SSD_860_EVO_500GB_S4XBNF1M807033B-part2 ONLINE 0 0 0\n 93 | \n 94 | errors: No known data errors\n""" 95 | 96 | output = prepare_code_message(std_in) 97 | assert output 98 | -------------------------------------------------------------------------------- /cli2telegram/util.py: -------------------------------------------------------------------------------- 1 | # cli2telegram 2 | # Copyright (c) 2019. Markus Ressel 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as 6 | # published by the Free Software Foundation, either version 3 of the 7 | # License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | import logging 17 | import time 18 | from datetime import datetime, timedelta 19 | 20 | from telegram import Bot, InlineKeyboardMarkup, Message 21 | from telegram.ext import Application 22 | 23 | from cli2telegram import RetryLimitReachedException 24 | from cli2telegram.const import CODEBLOCK_MARKER_START, CODEBLOCK_MARKER_END 25 | 26 | LOGGER = logging.getLogger(__name__) 27 | 28 | 29 | async def send_message( 30 | bot: Bot, chat_id: str, message: str, parse_mode: str = None, reply_to: int = None, 31 | menu: InlineKeyboardMarkup = None 32 | ) -> Message: 33 | """ 34 | Sends a text message to the given chat 35 | :param bot: the bot 36 | :param chat_id: the chat id to send the message to 37 | :param message: the message to chat (may contain emoji aliases) 38 | :param parse_mode: specify whether to parse the text as markdown or HTML 39 | :param reply_to: the message id to reply to 40 | :param menu: inline keyboard menu markup 41 | """ 42 | from emoji import emojize 43 | emojized_text = emojize(message, language='alias') 44 | return await bot.send_message( 45 | chat_id=chat_id, parse_mode=parse_mode, text=emojized_text, reply_to_message_id=reply_to, reply_markup=menu, 46 | connect_timeout=10, read_timeout=10, write_timeout=10 47 | ) 48 | 49 | 50 | def split_message(message: str, length: int) -> [str]: 51 | """ 52 | Split lines into multiple messages if the maximum number of character is exceeded. 53 | :param message: original message 54 | :param length: max length of a single chunk 55 | :return: list of messages 56 | """ 57 | return [message[i:i + length] for i in range(0, len(message), length)] 58 | 59 | 60 | def prepare_code_message(message: str) -> str: 61 | """ 62 | Wraps the given message inside of a code block. 63 | If the message already is contained within a code block, nothing is changed. 64 | :param message: message 65 | :return: prepared message 66 | """ 67 | result = message 68 | if not result.startswith(CODEBLOCK_MARKER_START): 69 | result = CODEBLOCK_MARKER_START + result 70 | if not result.endswith(CODEBLOCK_MARKER_END): 71 | result = result + CODEBLOCK_MARKER_END 72 | 73 | return result 74 | 75 | 76 | async def try_send_message( 77 | app: Application, 78 | chat_id: str, message: str, 79 | retry: bool, retry_timeout: timedelta, give_up_after: timedelta 80 | ): 81 | """ 82 | Sends a message 83 | :param app: telegram application 84 | :param chat_id: chat id 85 | :param message: the message to send 86 | :param retry: whether to retry if something fails 87 | :param retry_timeout: time to wait between retries 88 | :param give_up_after: when to give up trying 89 | """ 90 | started_trying = datetime.now() 91 | success = False 92 | while not success: 93 | try: 94 | await send_message(bot=app.bot, chat_id=chat_id, message=message, parse_mode="markdown") 95 | success = True 96 | except Exception as ex: 97 | LOGGER.exception(ex) 98 | 99 | if not retry: 100 | break 101 | 102 | tried_for = datetime.now() - started_trying 103 | if tried_for > give_up_after: 104 | LOGGER.warning(f"Giving up after trying for: {tried_for}") 105 | raise RetryLimitReachedException(started_trying, tried_for) 106 | 107 | timeout_seconds = retry_timeout.total_seconds() 108 | LOGGER.error(f"Error sending message, retrying in {timeout_seconds} seconds...") 109 | time.sleep(timeout_seconds) 110 | -------------------------------------------------------------------------------- /cli2telegram/cli.py: -------------------------------------------------------------------------------- 1 | # cli2telegram 2 | # Copyright (c) 2019. Markus Ressel 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as 6 | # published by the Free Software Foundation, either version 3 of the 7 | # License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | import asyncio 17 | import logging 18 | import sys 19 | from typing import Tuple, List 20 | 21 | import click 22 | from telegram.ext import Application 23 | 24 | from cli2telegram import util 25 | from cli2telegram.config import Config 26 | from cli2telegram.const import CODEBLOCK_MARKER_END, CODEBLOCK_MARKER_START, TELEGRAM_MESSAGE_LENGTH_LIMIT 27 | from cli2telegram.daemon import Daemon 28 | from cli2telegram.util import split_message, prepare_code_message 29 | 30 | LOGGER = logging.getLogger(__name__) 31 | # LOGGER.setLevel(logging.DEBUG) 32 | # LOGGER.addHandler(logging.FileHandler("/tmp/cli2telegram")) 33 | 34 | CONFIG = Config(validate=False) 35 | 36 | CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) 37 | 38 | loop = asyncio.get_event_loop() 39 | 40 | 41 | @click.command(context_settings=CONTEXT_SETTINGS) 42 | @click.option('-b', '--bot-token', 'bot_token', default=None, type=str, help='Telegram Bot Token') 43 | @click.option('-c', '--chat-id', 'chat_id', default=None, type=str, help='Telegram Chat ID') 44 | @click.option('-C', '--code-block', 'code_block', is_flag=True, default=False, help='Send message in a code block') 45 | @click.option('-d', '--daemon', 'daemon', is_flag=True, help='Daemon mode') 46 | @click.option('-p', '--pipe', 'pipe', default=None, type=str, help='File path to the pipe used in daemon mode') 47 | @click.argument('lines', type=str, nargs=-1) 48 | @click.version_option() 49 | def cli(bot_token: str or None, chat_id: str or None, code_block: bool, lines: Tuple[str], daemon: bool, 50 | pipe: str or None): 51 | """ 52 | cli entry method 53 | """ 54 | 55 | # set log level globally 56 | log_level = logging._nameToLevel.get(str(CONFIG.LOG_LEVEL.value).upper(), CONFIG.LOG_LEVEL.default) 57 | LOGGER.setLevel(log_level) 58 | logging.getLogger("cli2telegram").setLevel(log_level) 59 | 60 | if bot_token is not None: 61 | CONFIG.TELEGRAM_BOT_TOKEN.value = bot_token 62 | if chat_id is not None: 63 | CONFIG.TELEGRAM_CHAT_ID.value = chat_id 64 | if pipe is not None: 65 | CONFIG.DAEMON_PIPE_PATH.value = pipe 66 | CONFIG.validate() 67 | 68 | # ------------------ 69 | # daemon 70 | 71 | if daemon: 72 | d = Daemon(CONFIG) 73 | d.run() 74 | return 75 | 76 | # ------------------ 77 | # one-shot operation 78 | 79 | if len(lines) <= 0: 80 | # read message from stdin 81 | lines = [] 82 | for line in sys.stdin: 83 | lines.append(line) 84 | 85 | LOGGER.debug(f"Message text: {lines}") 86 | if not lines or len(lines) < 1: 87 | LOGGER.warning("Message is empty, ignoring.") 88 | return 89 | 90 | text = "".join(lines) 91 | messages = prepare_messages(text, code_block) 92 | 93 | app = Application.builder().token(CONFIG.TELEGRAM_BOT_TOKEN.value).build() 94 | 95 | tasks = asyncio.gather( 96 | _send_messages(app, messages), 97 | ) 98 | 99 | loop.run_until_complete(tasks) 100 | 101 | 102 | def prepare_messages(text: str, code_block: bool) -> List[str]: 103 | result = [] 104 | 105 | LOGGER.debug("Processing message...") 106 | 107 | length = TELEGRAM_MESSAGE_LENGTH_LIMIT 108 | if code_block: 109 | length -= (len(CODEBLOCK_MARKER_START) + len(CODEBLOCK_MARKER_END)) 110 | messages = split_message(text, length) 111 | for message in messages: 112 | if code_block: 113 | message = prepare_code_message(message) 114 | result.append(message) 115 | 116 | return result 117 | 118 | 119 | async def _send_messages(app, messages): 120 | for message in messages: 121 | await util.try_send_message( 122 | app=app, 123 | chat_id=CONFIG.TELEGRAM_CHAT_ID.value, 124 | message=message, 125 | retry=CONFIG.RETRY_ENABLED.value, 126 | retry_timeout=CONFIG.RETRY_TIMEOUT.value, 127 | give_up_after=CONFIG.RETRY_GIVE_UP_AFTER.value 128 | ) 129 | 130 | 131 | if __name__ == '__main__': 132 | cli() 133 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cli2telegram 2 | Small utility to send Telegram messages from the CLI. 3 | 4 | This can f.ex. be used to 5 | * quickly send arbitrary messages to a Telegram chat of your choice 6 | * use it as a replacement for the "mail" program on linux 7 | * use Telegram as a notification backend for the ZFS Event Daemon (zed) 8 | 9 | ## Features 10 | * [x] Read messages from argument or STDIN 11 | * [x] (Optional) Configuration file 12 | * [x] Retry sending messages for a specified amount of time 13 | * [x] Handling of long messages 14 | * [x] Run as a daemon and echo messages into a linux pipe 15 | 16 | ## Examples 17 | 18 | ```shell 19 | cli2telegram -h 20 | 21 | # From arguments 22 | cli2telegram "This is a message" 23 | 24 | cli2telegram "Header" "This is a multiline message." 25 | 26 | # From STDIN 27 | echo My Message | cli2telegram 28 | 29 | printf "Header\nThis is a multiline message." | cli2telegram 30 | 31 | # Config via parameters 32 | printf "Message" | cli2telegram -b "123456789:ABCDEFG" -c "123456789" 33 | 34 | # Config via parameters and send message in a code block 35 | printf "Message" | cli2telegram -b "123456789:ABCDEFG" -c "123456789" -C 36 | 37 | # as a Daemon 38 | cli2telegram -d -p "/tmp/cli2telegram" 39 | echo "hello world!" > /tmp/cli2telegram 40 | ``` 41 | 42 | # Install 43 | 44 | To use this utility install it either using: 45 | ``` 46 | pip install cli2telegram 47 | ``` 48 | 49 | or - if you don't want to install it globally - using f.ex. [venv-install](https://github.com/markusressel/venv-install): 50 | ``` 51 | venv-install cli2telegram cli2telegram 52 | ``` 53 | 54 | or your custom `venv` manager of choice. 55 | 56 | # Configuration 57 | 58 | To be able to send you messages you have to provide a **bot token** and a **chat id**. 59 | You can configure cli2telegram using cli parameters, a configuration file, 60 | environment variables, or a combination of them. 61 | 62 | ## Parameters 63 | 64 | | Name | Type | Description | 65 | |----------------------|--------|-------------------------------------------| 66 | | `-b`, `--bot-token` | String | Telegram Bot Token | 67 | | `-c`, `--chat-id` | String | Telegram Chat ID | 68 | | `-C`, `--code-block` | String | Send message inside a code block | 69 | | `-d`, `--daemon` | Flag | Run as a daemon | 70 | | `-p`, `--pipe` | String | File path to the pipe used in daemon mode | 71 | 72 | ## File 73 | cli2telegram uses [container-app-conf](https://github.com/markusressel/container-app-conf) so you can use YAML, TOML, or ENV to set those. 74 | 75 | Have a look at the [cli2telegram.toml_example](cli2telegram.toml_example) file to get an idea. 76 | 77 | # Daemon 78 | 79 | When running cli2telegram as a daemon, the pipe will close for a brief amount of time between receiving input messages. 80 | If you are sending multiple messages to the pipe using f.ex. a script, make sure to wait a bit (f.ex. 0.5 seconds) 81 | between sending messages, otherwise: 82 | * multiple messages may be received as one 83 | * messages may get lost 84 | * you may receive a `Broken pipe` error 85 | 86 | # Use Cases 87 | 88 | ## ZFS Event Daemon (ZED) 89 | To make `zed` call cli2telegram we will trick it and make it use cli2telegram as an E-Mail client. 90 | 91 | Edit `/etc/zfs/zed.d/zed.rc` as root: 92 | ```bash 93 | sudo nano -w /etc/zfs/zed.d/zed.rc 94 | ``` 95 | 96 | and 97 | * uncomment `ZED_EMAIL_ADDR`, the value does not matter since we use our own email script, but **it is necessary to set a value to make ZED send 'emails'** 98 | * set `ZED_EMAIL_PROG` to the path of the script, f.ex. `/usr/bin/cli2telegram` 99 | * it is important to note that zed does not seem to work if your command needs arguments to run 100 | ``` 101 | # this must not be empty! 102 | ZED_EMAIL_ADDR="root" 103 | 104 | [...] 105 | 106 | ZED_EMAIL_PROG="/usr/bin/cli2telegram" 107 | 108 | [...] 109 | 110 | # this must not be empty! 111 | ZED_EMAIL_OPTS="#zfs #$(hostname)" 112 | 113 | [...] 114 | 115 | # If you want to receive email no matter the state of your pool, you’ll want to set: 116 | ZED_NOTIFY_VERBOSE=1 117 | 118 | [...] 119 | ``` 120 | 121 | Since `zed` will run your scripts as root, if you want to use a config file 122 | you have to put it in f.ex. `/root/.config/cli2telegram.toml`. 123 | 124 | # Contributing 125 | 126 | GitHub is for social coding: if you want to write code, I encourage contributions through pull requests from forks 127 | of this repository. Create GitHub tickets for bugs and new features and comment on the ones that you are interested in. 128 | 129 | # License 130 | 131 | ```text 132 | cli2telegram by Markus Ressel 133 | Copyright (C) 2018 Markus Ressel 134 | 135 | This program is free software: you can redistribute it and/or modify 136 | it under the terms of the GNU Affero General Public License as 137 | published by the Free Software Foundation, either version 3 of the 138 | License, or (at your option) any later version. 139 | 140 | This program is distributed in the hope that it will be useful, 141 | but WITHOUT ANY WARRANTY; without even the implied warranty of 142 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 143 | GNU Affero General Public License for more details. 144 | 145 | You should have received a copy of the GNU Affero General Public License 146 | along with this program. If not, see . 147 | ``` 148 | -------------------------------------------------------------------------------- /cli2telegram/daemon.py: -------------------------------------------------------------------------------- 1 | # cli2telegram 2 | # Copyright (c) 2019. Markus Ressel 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as 6 | # published by the Free Software Foundation, either version 3 of the 7 | # License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | import asyncio 17 | import logging 18 | import os 19 | import sys 20 | from signal import signal, SIGINT, SIGTERM 21 | from time import time, sleep 22 | 23 | import aiofiles 24 | from telegram.ext import ApplicationBuilder 25 | 26 | from cli2telegram import RetryLimitReachedException 27 | from cli2telegram.config import Config 28 | from cli2telegram.util import try_send_message, prepare_code_message 29 | 30 | LOGGER = logging.getLogger(__name__) 31 | HANDLER = logging.StreamHandler(sys.stdout) 32 | FORMATTER = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 33 | HANDLER.setFormatter(FORMATTER) 34 | LOGGER.addHandler(HANDLER) 35 | 36 | 37 | class Daemon: 38 | def __init__(self, config: Config): 39 | self.config = config 40 | self.running = True 41 | self.pipe_file_path = config.DAEMON_PIPE_PATH.value 42 | self.message_queue = asyncio.Queue() 43 | self._app = ApplicationBuilder().token(config.TELEGRAM_BOT_TOKEN.value).build() 44 | 45 | def run(self): 46 | if os.path.exists(self.pipe_file_path): 47 | raise AssertionError(f"Pipe path '{self.pipe_file_path}' already exists!") 48 | try: 49 | os.mkfifo(self.pipe_file_path, mode=self.config.DAEMON_PIPE_PERMISSIONS.value) 50 | except OSError as ose: 51 | LOGGER.error(ose) 52 | raise ose 53 | 54 | signal(SIGINT, self.signal_handler) # ctrl+c 55 | signal(SIGTERM, self.signal_handler) # systemctl stop 56 | 57 | loop = asyncio.get_event_loop() 58 | tasks = asyncio.gather( 59 | self.send_message_worker(), 60 | self.read_pipe_loop(), 61 | ) 62 | loop.run_until_complete(tasks) 63 | loop.close() 64 | 65 | async def read_pipe_loop(self): 66 | LOGGER.info(f"Daemon is listening for input on {self.pipe_file_path}...") 67 | while True: 68 | LOGGER.debug("Waiting for new input on pipe") 69 | 70 | async with aiofiles.open(self.pipe_file_path, 'r') as pipe: 71 | pipe_buffer = '' 72 | while True: 73 | data = await pipe.read() 74 | LOGGER.debug(f"received: {data}") 75 | if len(data) == 0 and self.running: 76 | LOGGER.debug("No new data from pipe, adding current buffer to message queue") 77 | await self.message_queue.put(pipe_buffer) 78 | break 79 | elif not self.running: 80 | break 81 | else: 82 | pipe_buffer += data 83 | 84 | if not self.running: 85 | break 86 | 87 | async def send_message_worker(self): 88 | while True: 89 | LOGGER.debug("Waiting for item in message queue...") 90 | message = await self.message_queue.get() 91 | LOGGER.debug(f"Got new message: {message}") 92 | 93 | if not message.strip(): 94 | LOGGER.warning("Message is empty, ignoring") 95 | self.message_queue.task_done() 96 | continue 97 | 98 | prepared_message = prepare_code_message(message) 99 | LOGGER.debug(f"Trying to send message from queue...") 100 | 101 | try: 102 | await try_send_message( 103 | app=self._app, 104 | chat_id=self.config.TELEGRAM_CHAT_ID.value, 105 | message=prepared_message, 106 | retry=self.config.RETRY_ENABLED.value, 107 | retry_timeout=self.config.RETRY_TIMEOUT.value, 108 | give_up_after=self.config.RETRY_GIVE_UP_AFTER.value) 109 | LOGGER.debug(f"Message sent successfully") 110 | except RetryLimitReachedException as ex: 111 | LOGGER.exception(ex) 112 | LOGGER.warning(f"Retry limit exceeded while trying to send message, it will be skipped: {message}") 113 | 114 | self.message_queue.task_done() 115 | 116 | def signal_handler(self, signal_received, frame): 117 | self.running = False 118 | 119 | LOGGER.debug(f"Caught {signal_received}. Attempting to send all messages from queue.") 120 | 121 | try: 122 | os.unlink(self.pipe_file_path) 123 | except OSError as ose: 124 | LOGGER.exception(ose) 125 | if os.path.exists(self.pipe_file_path): 126 | LOGGER.warning("Unable to clean up named pipe") 127 | 128 | stop = time() + 5 129 | 130 | while self.message_queue._unfinished_tasks and time() < stop: 131 | sleep(1) 132 | 133 | sys.exit(0) 134 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "aiofiles" 5 | version = "23.2.1" 6 | description = "File support for asyncio." 7 | optional = false 8 | python-versions = ">=3.7" 9 | files = [ 10 | {file = "aiofiles-23.2.1-py3-none-any.whl", hash = "sha256:19297512c647d4b27a2cf7c34caa7e405c0d60b5560618a29a9fe027b18b0107"}, 11 | {file = "aiofiles-23.2.1.tar.gz", hash = "sha256:84ec2218d8419404abcb9f0c02df3f34c6e0a68ed41072acfb1cef5cbc29051a"}, 12 | ] 13 | 14 | [[package]] 15 | name = "anyio" 16 | version = "3.6.2" 17 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 18 | optional = false 19 | python-versions = ">=3.6.2" 20 | files = [ 21 | {file = "anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"}, 22 | {file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"}, 23 | ] 24 | 25 | [package.dependencies] 26 | idna = ">=2.8" 27 | sniffio = ">=1.1" 28 | typing-extensions = {version = "*", markers = "python_version < \"3.8\""} 29 | 30 | [package.extras] 31 | doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] 32 | test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"] 33 | trio = ["trio (>=0.16,<0.22)"] 34 | 35 | [[package]] 36 | name = "certifi" 37 | version = "2024.7.4" 38 | description = "Python package for providing Mozilla's CA Bundle." 39 | optional = false 40 | python-versions = ">=3.6" 41 | files = [ 42 | {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, 43 | {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, 44 | ] 45 | 46 | [[package]] 47 | name = "click" 48 | version = "8.1.8" 49 | description = "Composable command line interface toolkit" 50 | optional = false 51 | python-versions = ">=3.7" 52 | files = [ 53 | {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, 54 | {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, 55 | ] 56 | 57 | [package.dependencies] 58 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 59 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 60 | 61 | [[package]] 62 | name = "colorama" 63 | version = "0.4.6" 64 | description = "Cross-platform colored terminal text." 65 | optional = false 66 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 67 | files = [ 68 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 69 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 70 | ] 71 | 72 | [[package]] 73 | name = "container-app-conf" 74 | version = "5.3.0" 75 | description = "Convenient configuration of containerized applications" 76 | optional = false 77 | python-versions = "*" 78 | files = [ 79 | {file = "container_app_conf-5.3.0-py3-none-any.whl", hash = "sha256:fa7d9530da2f62d4fed720a0b29ea854aadf0626104a2c17aace467608711160"}, 80 | {file = "container_app_conf-5.3.0.tar.gz", hash = "sha256:81fa5e7da03834ab6a7b9ad09a93dd767ecb57085751dfb7b4cf6570d51768db"}, 81 | ] 82 | 83 | [package.dependencies] 84 | py-range-parse = "*" 85 | python-dateutil = "*" 86 | pytimeparse = "*" 87 | "ruamel.yaml" = "*" 88 | "ruamel.yaml.clib" = "*" 89 | six = "*" 90 | toml = "*" 91 | voluptuous = "*" 92 | 93 | [[package]] 94 | name = "emoji" 95 | version = "2.14.0" 96 | description = "Emoji for Python" 97 | optional = false 98 | python-versions = ">=3.7" 99 | files = [ 100 | {file = "emoji-2.14.0-py3-none-any.whl", hash = "sha256:fcc936bf374b1aec67dda5303ae99710ba88cc9cdce2d1a71c5f2204e6d78799"}, 101 | {file = "emoji-2.14.0.tar.gz", hash = "sha256:f68ac28915a2221667cddb3e6c589303c3c6954c6c5af6fefaec7f9bdf72fdca"}, 102 | ] 103 | 104 | [package.dependencies] 105 | typing-extensions = {version = ">=4.7.0", markers = "python_version < \"3.9\""} 106 | 107 | [package.extras] 108 | dev = ["coverage", "pytest (>=7.4.4)"] 109 | 110 | [[package]] 111 | name = "exceptiongroup" 112 | version = "1.1.1" 113 | description = "Backport of PEP 654 (exception groups)" 114 | optional = false 115 | python-versions = ">=3.7" 116 | files = [ 117 | {file = "exceptiongroup-1.1.1-py3-none-any.whl", hash = "sha256:232c37c63e4f682982c8b6459f33a8981039e5fb8756b2074364e5055c498c9e"}, 118 | {file = "exceptiongroup-1.1.1.tar.gz", hash = "sha256:d484c3090ba2889ae2928419117447a14daf3c1231d5e30d0aae34f354f01785"}, 119 | ] 120 | 121 | [package.extras] 122 | test = ["pytest (>=6)"] 123 | 124 | [[package]] 125 | name = "h11" 126 | version = "0.14.0" 127 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 128 | optional = false 129 | python-versions = ">=3.7" 130 | files = [ 131 | {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, 132 | {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, 133 | ] 134 | 135 | [package.dependencies] 136 | typing-extensions = {version = "*", markers = "python_version < \"3.8\""} 137 | 138 | [[package]] 139 | name = "httpcore" 140 | version = "0.16.3" 141 | description = "A minimal low-level HTTP client." 142 | optional = false 143 | python-versions = ">=3.7" 144 | files = [ 145 | {file = "httpcore-0.16.3-py3-none-any.whl", hash = "sha256:da1fb708784a938aa084bde4feb8317056c55037247c787bd7e19eb2c2949dc0"}, 146 | {file = "httpcore-0.16.3.tar.gz", hash = "sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb"}, 147 | ] 148 | 149 | [package.dependencies] 150 | anyio = ">=3.0,<5.0" 151 | certifi = "*" 152 | h11 = ">=0.13,<0.15" 153 | sniffio = "==1.*" 154 | 155 | [package.extras] 156 | http2 = ["h2 (>=3,<5)"] 157 | socks = ["socksio (==1.*)"] 158 | 159 | [[package]] 160 | name = "httpx" 161 | version = "0.24.0" 162 | description = "The next generation HTTP client." 163 | optional = false 164 | python-versions = ">=3.7" 165 | files = [ 166 | {file = "httpx-0.24.0-py3-none-any.whl", hash = "sha256:447556b50c1921c351ea54b4fe79d91b724ed2b027462ab9a329465d147d5a4e"}, 167 | {file = "httpx-0.24.0.tar.gz", hash = "sha256:507d676fc3e26110d41df7d35ebd8b3b8585052450f4097401c9be59d928c63e"}, 168 | ] 169 | 170 | [package.dependencies] 171 | certifi = "*" 172 | httpcore = ">=0.15.0,<0.18.0" 173 | idna = "*" 174 | sniffio = "*" 175 | 176 | [package.extras] 177 | brotli = ["brotli", "brotlicffi"] 178 | cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] 179 | http2 = ["h2 (>=3,<5)"] 180 | socks = ["socksio (==1.*)"] 181 | 182 | [[package]] 183 | name = "idna" 184 | version = "3.7" 185 | description = "Internationalized Domain Names in Applications (IDNA)" 186 | optional = false 187 | python-versions = ">=3.5" 188 | files = [ 189 | {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, 190 | {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, 191 | ] 192 | 193 | [[package]] 194 | name = "importlib-metadata" 195 | version = "6.1.0" 196 | description = "Read metadata from Python packages" 197 | optional = false 198 | python-versions = ">=3.7" 199 | files = [ 200 | {file = "importlib_metadata-6.1.0-py3-none-any.whl", hash = "sha256:ff80f3b5394912eb1b108fcfd444dc78b7f1f3e16b16188054bd01cb9cb86f09"}, 201 | {file = "importlib_metadata-6.1.0.tar.gz", hash = "sha256:43ce9281e097583d758c2c708c4376371261a02c34682491a8e98352365aad20"}, 202 | ] 203 | 204 | [package.dependencies] 205 | typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} 206 | zipp = ">=0.5" 207 | 208 | [package.extras] 209 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 210 | perf = ["ipython"] 211 | testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] 212 | 213 | [[package]] 214 | name = "iniconfig" 215 | version = "2.0.0" 216 | description = "brain-dead simple config-ini parsing" 217 | optional = false 218 | python-versions = ">=3.7" 219 | files = [ 220 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 221 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 222 | ] 223 | 224 | [[package]] 225 | name = "packaging" 226 | version = "23.0" 227 | description = "Core utilities for Python packages" 228 | optional = false 229 | python-versions = ">=3.7" 230 | files = [ 231 | {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, 232 | {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, 233 | ] 234 | 235 | [[package]] 236 | name = "pluggy" 237 | version = "1.0.0" 238 | description = "plugin and hook calling mechanisms for python" 239 | optional = false 240 | python-versions = ">=3.6" 241 | files = [ 242 | {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, 243 | {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, 244 | ] 245 | 246 | [package.dependencies] 247 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 248 | 249 | [package.extras] 250 | dev = ["pre-commit", "tox"] 251 | testing = ["pytest", "pytest-benchmark"] 252 | 253 | [[package]] 254 | name = "py-range-parse" 255 | version = "1.0.5" 256 | description = "Parses commonly used range notations to python objects" 257 | optional = false 258 | python-versions = "*" 259 | files = [ 260 | {file = "py_range_parse-1.0.5-py3-none-any.whl", hash = "sha256:a9d6b66f8e10dd26f5ff9726daef5b70bb32368a00a4563bd65d062c73d7c979"}, 261 | {file = "py_range_parse-1.0.5.tar.gz", hash = "sha256:15cf56ab4483814162f57f3b2bfd3ae68f6c4ea4cd7e710c881a5ce97f516c2c"}, 262 | ] 263 | 264 | [[package]] 265 | name = "pytest" 266 | version = "7.4.4" 267 | description = "pytest: simple powerful testing with Python" 268 | optional = false 269 | python-versions = ">=3.7" 270 | files = [ 271 | {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, 272 | {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, 273 | ] 274 | 275 | [package.dependencies] 276 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 277 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 278 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 279 | iniconfig = "*" 280 | packaging = "*" 281 | pluggy = ">=0.12,<2.0" 282 | tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} 283 | 284 | [package.extras] 285 | testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 286 | 287 | [[package]] 288 | name = "python-dateutil" 289 | version = "2.8.2" 290 | description = "Extensions to the standard Python datetime module" 291 | optional = false 292 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 293 | files = [ 294 | {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, 295 | {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, 296 | ] 297 | 298 | [package.dependencies] 299 | six = ">=1.5" 300 | 301 | [[package]] 302 | name = "python-telegram-bot" 303 | version = "20.3" 304 | description = "We have made you a wrapper you can't refuse" 305 | optional = false 306 | python-versions = ">=3.7" 307 | files = [ 308 | {file = "python-telegram-bot-20.3.tar.gz", hash = "sha256:73e46a534be9d1c790ce8b494765cca18a5c2f3f5b4932d83bcb06bb0051eb4a"}, 309 | {file = "python_telegram_bot-20.3-py3-none-any.whl", hash = "sha256:1185edee387db7b08027e87b67fa9a3cc3263ae5ab5bb55513acd1bca5c3cf4b"}, 310 | ] 311 | 312 | [package.dependencies] 313 | httpx = ">=0.24.0,<0.25.0" 314 | 315 | [package.extras] 316 | all = ["APScheduler (>=3.10.1,<3.11.0)", "aiolimiter (>=1.0.0,<1.1.0)", "cachetools (>=5.3.0,<5.4.0)", "cryptography (>=39.0.1)", "httpx[http2]", "httpx[socks]", "pytz (>=2018.6)", "tornado (>=6.2,<7.0)"] 317 | callback-data = ["cachetools (>=5.3.0,<5.4.0)"] 318 | ext = ["APScheduler (>=3.10.1,<3.11.0)", "aiolimiter (>=1.0.0,<1.1.0)", "cachetools (>=5.3.0,<5.4.0)", "pytz (>=2018.6)", "tornado (>=6.2,<7.0)"] 319 | http2 = ["httpx[http2]"] 320 | job-queue = ["APScheduler (>=3.10.1,<3.11.0)", "pytz (>=2018.6)"] 321 | passport = ["cryptography (>=39.0.1)"] 322 | rate-limiter = ["aiolimiter (>=1.0.0,<1.1.0)"] 323 | socks = ["httpx[socks]"] 324 | webhooks = ["tornado (>=6.2,<7.0)"] 325 | 326 | [[package]] 327 | name = "pytimeparse" 328 | version = "1.1.8" 329 | description = "Time expression parser" 330 | optional = false 331 | python-versions = "*" 332 | files = [ 333 | {file = "pytimeparse-1.1.8-py2.py3-none-any.whl", hash = "sha256:04b7be6cc8bd9f5647a6325444926c3ac34ee6bc7e69da4367ba282f076036bd"}, 334 | {file = "pytimeparse-1.1.8.tar.gz", hash = "sha256:e86136477be924d7e670646a98561957e8ca7308d44841e21f5ddea757556a0a"}, 335 | ] 336 | 337 | [[package]] 338 | name = "ruamel-yaml" 339 | version = "0.17.21" 340 | description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" 341 | optional = false 342 | python-versions = ">=3" 343 | files = [ 344 | {file = "ruamel.yaml-0.17.21-py3-none-any.whl", hash = "sha256:742b35d3d665023981bd6d16b3d24248ce5df75fdb4e2924e93a05c1f8b61ca7"}, 345 | {file = "ruamel.yaml-0.17.21.tar.gz", hash = "sha256:8b7ce697a2f212752a35c1ac414471dc16c424c9573be4926b56ff3f5d23b7af"}, 346 | ] 347 | 348 | [package.dependencies] 349 | "ruamel.yaml.clib" = {version = ">=0.2.6", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.11\""} 350 | 351 | [package.extras] 352 | docs = ["ryd"] 353 | jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] 354 | 355 | [[package]] 356 | name = "ruamel-yaml-clib" 357 | version = "0.2.7" 358 | description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" 359 | optional = false 360 | python-versions = ">=3.5" 361 | files = [ 362 | {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5859983f26d8cd7bb5c287ef452e8aacc86501487634573d260968f753e1d71"}, 363 | {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:debc87a9516b237d0466a711b18b6ebeb17ba9f391eb7f91c649c5c4ec5006c7"}, 364 | {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:df5828871e6648db72d1c19b4bd24819b80a755c4541d3409f0f7acd0f335c80"}, 365 | {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:efa08d63ef03d079dcae1dfe334f6c8847ba8b645d08df286358b1f5293d24ab"}, 366 | {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-win32.whl", hash = "sha256:763d65baa3b952479c4e972669f679fe490eee058d5aa85da483ebae2009d231"}, 367 | {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:d000f258cf42fec2b1bbf2863c61d7b8918d31ffee905da62dede869254d3b8a"}, 368 | {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:045e0626baf1c52e5527bd5db361bc83180faaba2ff586e763d3d5982a876a9e"}, 369 | {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:1a6391a7cabb7641c32517539ca42cf84b87b667bad38b78d4d42dd23e957c81"}, 370 | {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:9c7617df90c1365638916b98cdd9be833d31d337dbcd722485597b43c4a215bf"}, 371 | {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:41d0f1fa4c6830176eef5b276af04c89320ea616655d01327d5ce65e50575c94"}, 372 | {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-win32.whl", hash = "sha256:f6d3d39611ac2e4f62c3128a9eed45f19a6608670c5a2f4f07f24e8de3441d38"}, 373 | {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:da538167284de58a52109a9b89b8f6a53ff8437dd6dc26d33b57bf6699153122"}, 374 | {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4b3a93bb9bc662fc1f99c5c3ea8e623d8b23ad22f861eb6fce9377ac07ad6072"}, 375 | {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-macosx_12_0_arm64.whl", hash = "sha256:a234a20ae07e8469da311e182e70ef6b199d0fbeb6c6cc2901204dd87fb867e8"}, 376 | {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:15910ef4f3e537eea7fe45f8a5d19997479940d9196f357152a09031c5be59f3"}, 377 | {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:370445fd795706fd291ab00c9df38a0caed0f17a6fb46b0f607668ecb16ce763"}, 378 | {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-win32.whl", hash = "sha256:ecdf1a604009bd35c674b9225a8fa609e0282d9b896c03dd441a91e5f53b534e"}, 379 | {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-win_amd64.whl", hash = "sha256:f34019dced51047d6f70cb9383b2ae2853b7fc4dce65129a5acd49f4f9256646"}, 380 | {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2aa261c29a5545adfef9296b7e33941f46aa5bbd21164228e833412af4c9c75f"}, 381 | {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-macosx_12_0_arm64.whl", hash = "sha256:f01da5790e95815eb5a8a138508c01c758e5f5bc0ce4286c4f7028b8dd7ac3d0"}, 382 | {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:40d030e2329ce5286d6b231b8726959ebbe0404c92f0a578c0e2482182e38282"}, 383 | {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c3ca1fbba4ae962521e5eb66d72998b51f0f4d0f608d3c0347a48e1af262efa7"}, 384 | {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-win32.whl", hash = "sha256:7bdb4c06b063f6fd55e472e201317a3bb6cdeeee5d5a38512ea5c01e1acbdd93"}, 385 | {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:be2a7ad8fd8f7442b24323d24ba0b56c51219513cfa45b9ada3b87b76c374d4b"}, 386 | {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:91a789b4aa0097b78c93e3dc4b40040ba55bef518f84a40d4442f713b4094acb"}, 387 | {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:99e77daab5d13a48a4054803d052ff40780278240a902b880dd37a51ba01a307"}, 388 | {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:3243f48ecd450eddadc2d11b5feb08aca941b5cd98c9b1db14b2fd128be8c697"}, 389 | {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:8831a2cedcd0f0927f788c5bdf6567d9dc9cc235646a434986a852af1cb54b4b"}, 390 | {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-win32.whl", hash = "sha256:3110a99e0f94a4a3470ff67fc20d3f96c25b13d24c6980ff841e82bafe827cac"}, 391 | {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:92460ce908546ab69770b2e576e4f99fbb4ce6ab4b245345a3869a0a0410488f"}, 392 | {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5bc0667c1eb8f83a3752b71b9c4ba55ef7c7058ae57022dd9b29065186a113d9"}, 393 | {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:4a4d8d417868d68b979076a9be6a38c676eca060785abaa6709c7b31593c35d1"}, 394 | {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bf9a6bc4a0221538b1a7de3ed7bca4c93c02346853f44e1cd764be0023cd3640"}, 395 | {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a7b301ff08055d73223058b5c46c55638917f04d21577c95e00e0c4d79201a6b"}, 396 | {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-win32.whl", hash = "sha256:d5e51e2901ec2366b79f16c2299a03e74ba4531ddcfacc1416639c557aef0ad8"}, 397 | {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:184faeaec61dbaa3cace407cffc5819f7b977e75360e8d5ca19461cd851a5fc5"}, 398 | {file = "ruamel.yaml.clib-0.2.7.tar.gz", hash = "sha256:1f08fd5a2bea9c4180db71678e850b995d2a5f4537be0e94557668cf0f5f9497"}, 399 | ] 400 | 401 | [[package]] 402 | name = "six" 403 | version = "1.16.0" 404 | description = "Python 2 and 3 compatibility utilities" 405 | optional = false 406 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 407 | files = [ 408 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 409 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 410 | ] 411 | 412 | [[package]] 413 | name = "sniffio" 414 | version = "1.3.0" 415 | description = "Sniff out which async library your code is running under" 416 | optional = false 417 | python-versions = ">=3.7" 418 | files = [ 419 | {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, 420 | {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, 421 | ] 422 | 423 | [[package]] 424 | name = "toml" 425 | version = "0.10.2" 426 | description = "Python Library for Tom's Obvious, Minimal Language" 427 | optional = false 428 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 429 | files = [ 430 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 431 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 432 | ] 433 | 434 | [[package]] 435 | name = "tomli" 436 | version = "2.0.1" 437 | description = "A lil' TOML parser" 438 | optional = false 439 | python-versions = ">=3.7" 440 | files = [ 441 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 442 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 443 | ] 444 | 445 | [[package]] 446 | name = "typing-extensions" 447 | version = "4.7.1" 448 | description = "Backported and Experimental Type Hints for Python 3.7+" 449 | optional = false 450 | python-versions = ">=3.7" 451 | files = [ 452 | {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, 453 | {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, 454 | ] 455 | 456 | [[package]] 457 | name = "voluptuous" 458 | version = "0.13.1" 459 | description = "" 460 | optional = false 461 | python-versions = "*" 462 | files = [ 463 | {file = "voluptuous-0.13.1-py3-none-any.whl", hash = "sha256:4b838b185f5951f2d6e8752b68fcf18bd7a9c26ded8f143f92d6d28f3921a3e6"}, 464 | {file = "voluptuous-0.13.1.tar.gz", hash = "sha256:e8d31c20601d6773cb14d4c0f42aee29c6821bbd1018039aac7ac5605b489723"}, 465 | ] 466 | 467 | [[package]] 468 | name = "zipp" 469 | version = "3.15.0" 470 | description = "Backport of pathlib-compatible object wrapper for zip files" 471 | optional = false 472 | python-versions = ">=3.7" 473 | files = [ 474 | {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, 475 | {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, 476 | ] 477 | 478 | [package.extras] 479 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 480 | testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] 481 | 482 | [metadata] 483 | lock-version = "2.0" 484 | python-versions = "^3.7" 485 | content-hash = "d1274262eda885e4777e803949138c8979ae8e3dcab0a2697277a82df76b56f4" 486 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | --------------------------------------------------------------------------------