├── .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 | 
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 |
--------------------------------------------------------------------------------