Compare commits

...

33 Commits

Author SHA1 Message Date
Bibo-Joshi 0015fcdf9d Bump Version to v22.5 (#4979) 2025-09-27 15:33:00 +02:00
Bibo-Joshi 54f80cb54f Documentation Improvements (#4974) 2025-09-27 15:09:47 +02:00
Bibo-Joshi 4bc2f1f60b Fix Handling of Infinite Bootstrap Retries (#4973) 2025-09-27 14:40:26 +02:00
renovate[bot] 4f7f1ba21b Update dependency furo to v2025.9.25 (#4978)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-27 08:50:07 +00:00
renovate[bot] 9c0efaf2f6 Update Ruff to v0.13.2 (#4977)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-27 02:17:29 +00:00
renovate[bot] 8434c0aaea Update dependency astral-sh/uv to v0.8.22 (#4975)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-27 02:16:05 +00:00
renovate[bot] 96138510ca Update github/codeql-action action to v3.30.5 (#4976)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-27 02:15:20 +00:00
Aweryc 6e951a9fd3 Convenience Functionality for BusinessOpeningHours (#4861)
Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com>
2025-09-24 17:23:23 +02:00
Abdelrahman Elkheir 1f5cfc8f9b Move Parameter ReplyParameters.checklist_task_id to Last Position (#4972) 2025-09-24 09:48:03 +02:00
renovate[bot] 066ba5bb32 Update Ruff to v0.13.1 (#4962)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Harshil <37377066+harshil21@users.noreply.github.com>
2025-09-22 06:24:56 +00:00
renovate[bot] 0375b7d701 Update actions/stale action to v10 (#4964)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 06:09:58 +00:00
renovate[bot] 67db3df426 Update actions/setup-python action to v6 (#4963)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 05:59:46 +00:00
Harshil 152269cdcd Tune Renovate Configuration (#4968)
Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com>
2025-09-22 07:56:44 +02:00
renovate[bot] 8043bf265d Lock file maintenance (#4967)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 00:29:06 +00:00
Bibo-Joshi 14f8e89cef Properly Pin Dependency to astral/setup-uv in Copilot Setup Steps (#4965) 2025-09-20 06:41:56 +00:00
renovate[bot] 0f7d9ec5da Update astral-sh/setup-uv action to v6.7.0 (#4961)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-20 00:44:38 +00:00
renovate[bot] 047844f9af Update Mypy to v1.18.2 (#4960)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-20 00:37:31 +00:00
renovate[bot] caceaf71b8 Update dependency astral-sh/uv to v0.8.19 (#4959)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-20 00:36:32 +00:00
renovate[bot] cfa9230f15 Update astral-sh/setup-uv digest to 208b0c0 (#4958)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-20 00:35:13 +00:00
renovate[bot] b37ba3a44e Lock file maintenance (#4955)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-18 17:58:08 +00:00
renovate[bot] 1116de4ebd Lock file maintenance (#4954)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-18 02:32:12 +00:00
renovate[bot] 437c4d7bfb Update pre-commit hook cachetools to >=5.5.2,<5.6.0 (#4951)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com>
2025-09-15 20:50:01 +00:00
renovate[bot] 630db8f0ef Update pre-commit hook APScheduler to ~=3.11.0 (#4950)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com>
2025-09-15 22:44:45 +02:00
renovate[bot] 008fcdea5f Update Mypy to v1.18.1 (#4949)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com>
2025-09-15 20:25:43 +00:00
renovate[bot] 7aa1356089 Update Chango to v0.5.0 (#4948)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 18:34:02 +02:00
renovate[bot] ecfb4583d6 Update pypa/gh-action-pypi-publish action to v1.13.0 (#4952)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 18:33:10 +02:00
renovate[bot] f56371fae3 Update Pylint to v3.3.8 (#4947)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 18:33:04 +02:00
renovate[bot] 9f3c5e4f2b Update github/codeql-action action to v3.30.3 (#4946)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 18:32:00 +02:00
renovate[bot] 489561d531 Update dependency astral-sh/uv to v0.8.17 (#4945)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 18:31:55 +02:00
renovate[bot] 568e63933c Update codecov/codecov-action action to v5.5.1 (#4944)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 18:31:42 +02:00
renovate[bot] f8f12e7bd4 Update astral-sh/setup-uv digest to b75a909 (#4943)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 18:27:15 +02:00
renovate[bot] 94afda2b69 Lock File Maintenance (#4938)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 18:25:46 +02:00
Harshil a81fc86792 Tweak Renovate Configuration (#4953)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-15 17:15:04 +02:00
75 changed files with 1231 additions and 616 deletions
+16 -20
View File
@@ -8,7 +8,7 @@
],
// Add pull request labels:
"labels": ["dependencies"],
"labels": ["⚙️ ci-cd"],
// Bump even patch versions:
"bumpVersion": "patch",
@@ -19,24 +19,12 @@
// Update the lock files:
"lockFileMaintenance": {
"enabled": true,
"schedule": ["* * * * 1,4"] // Run sometime on Monday and Thursday
"schedule": ["* 0-3 1 * *"], // https://docs.renovatebot.com/presets-schedule/#schedulemonthly
"automerge": true
},
// Bump the versions even in other files:
"bumpVersions": [
{
"name": "Update dependency versions in README.rst",
"filePatterns": ["README.rst"],
"matchStrings": [
"cryptography>=(?<version>\\d+\\.\\d+\\.\\d+)",
"aiolimiter~=(?<version>\\d+\\.\\d+\\.\\d+)",
"tornado~=(?<version>\\d+\\.\\d+)",
"cachetools>=(?<version>\\d+\\.\\d+\\.\\d+)", // Lower bound only
"APScheduler>=(?<version>\\d+\\.\\d+\\.\\d+)" // Lower bound only
],
"bumpType": "minor"
}
],
// Enable automerge globally:
"automerge": true,
// Group package updates together:
"packageRules": [
@@ -62,10 +50,18 @@
"matchPackageNames": ["chango", "Bibo-Joshi/chango"],
"groupName": "Chango"
},
// Automerge PR's for minor/patch/pin/digest (except major) updates:
// Don't automerge major updates for project dependencies:
{
"matchUpdateTypes": ["minor", "patch", "pin", "digest"],
"automerge": true
"matchUpdateTypes": ["major"],
"matchDepTypes": ["project.dependencies", "project.optional-dependencies"],
"automerge": false
},
// Apply the "dependencies" label to all updates of optional/required dependencies:
{
"matchDepTypes": ["project.optional-dependencies", "project.dependencies"],
"labels": ["⚙️ dependencies"]
}
],
+2 -2
View File
@@ -36,7 +36,7 @@ jobs:
fi
# Create the new fragment
- uses: Bibo-Joshi/chango@9d6bd9d7612eca5fab2c5161687011be59baaf19 # 0.4.0
- uses: Bibo-Joshi/chango@212fc662da1b1026f335e110270d75690df05758 # 0.5.0
with:
github-token: ${{ secrets.CHANGO_PAT }}
query-issue-types: true
@@ -45,7 +45,7 @@ jobs:
# Run `chango release` if applicable - needs some additional setup.
- name: Set up Python
if: steps.check_title.outputs.IS_RELEASE_PR == 'true'
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: "3.x"
+2 -2
View File
@@ -31,10 +31,10 @@ jobs:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6
uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
with:
# Install a specific version of uv.
version: "0.8.3"
version: "0.8.22"
# Install 3.13:
python-version: 3.13
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
with:
persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
with:
persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
+2 -2
View File
@@ -21,13 +21,13 @@ jobs:
with:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
- name: Run zizmor
run: uvx zizmor --persona=pedantic --format sarif . > results.sarif
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload SARIF file
uses: github/codeql-action/upload-sarif@2d92b76c45b91eb80fc44c74ce3fce0ee94e8f9d # v3.30.0
uses: github/codeql-action/upload-sarif@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
with:
sarif_file: results.sarif
category: zizmor
+2 -2
View File
@@ -21,7 +21,7 @@ jobs:
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: "3.x"
- name: Install pypa/build
@@ -60,7 +60,7 @@ jobs:
name: python-package-distributions
path: dist/
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
compute-signatures:
name: Compute SHA1 Sums and Sign with Sigstore
+2 -2
View File
@@ -21,7 +21,7 @@ jobs:
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: "3.x"
- name: Install pypa/build
@@ -60,7 +60,7 @@ jobs:
name: python-package-distributions
path: dist/
- name: Publish to Test PyPI
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
with:
repository-url: https://test.pypi.org/legacy/
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
# For adding labels and closing
issues: write
steps:
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0
- uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0
with:
# PRs never get stale
days-before-stale: 3
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
with:
persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
+2 -2
View File
@@ -26,7 +26,7 @@ jobs:
with:
persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
@@ -83,7 +83,7 @@ jobs:
.test_report_optionals_junit.xml
- name: Submit coverage
uses: codecov/codecov-action@fdcc8476540edceab3de004e990f80d881c6cc00 # v5.5.0
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
with:
env_vars: OS,PYTHON
name: ${{ matrix.os }}-${{ matrix.python-version }}
+9 -9
View File
@@ -8,7 +8,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: 'v0.12.7'
rev: 'v0.13.2'
hooks:
# Run the linter:
- id: ruff-check
@@ -17,7 +17,7 @@ repos:
- id: ruff-format
name: ruff format
- repo: https://github.com/PyCQA/pylint
rev: v3.3.7
rev: v3.3.8
hooks:
- id: pylint
files: ^(?!(tests|docs)).*\.py$
@@ -25,12 +25,12 @@ repos:
additional_dependencies:
- httpx~=0.27
- tornado~=6.4
- APScheduler~=3.10.4
- cachetools>=5.3.3,<5.5.0
- APScheduler>=3.10.4,<3.12.0
- cachetools>=5.3.3,<6.3.0
- aiolimiter~=1.1,<1.3
- . # this basically does `pip install -e .`
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.16.1
rev: v1.18.2
hooks:
- id: mypy
name: mypy-ptb
@@ -42,8 +42,8 @@ repos:
- types-cachetools
- httpx~=0.27
- tornado~=6.4
- APScheduler~=3.10.4
- cachetools>=5.3.3,<5.5.0
- APScheduler>=3.10.4,<3.12.0
- cachetools>=5.3.3,<6.3.0
- aiolimiter~=1.1,<1.3
- . # this basically does `pip install -e .`
- id: mypy
@@ -55,6 +55,6 @@ repos:
- --follow-imports=silent
additional_dependencies:
- tornado~=6.4
- APScheduler~=3.10.4
- cachetools>=5.3.3,<5.5.0
- APScheduler>=3.10.4,<3.12.0
- cachetools>=5.3.3,<6.3.0
- . # this basically does `pip install -e .`
@@ -0,0 +1,10 @@
features = """
Add convenience methods for ``BusinessOpeningHours``:
* ``get_opening_hours_for_day``: returns the opening hours applicable for a specific date
* ``is_open``: indicates whether the business is open at the specified date and time.
"""
[[pull_requests]]
uid = "4861"
author_uid = "Aweryc"
closes_threads = ["4194"]
@@ -0,0 +1,5 @@
internal = "Lock file maintenance"
[[pull_requests]]
uid = "4938"
author_uid = "renovate[bot]"
closes_threads = []
@@ -0,0 +1,5 @@
internal = "Update astral-sh/setup-uv digest to b75a909"
[[pull_requests]]
uid = "4943"
author_uid = "renovate[bot]"
closes_threads = []
@@ -0,0 +1,5 @@
internal = "Update codecov/codecov-action action to v5.5.1"
[[pull_requests]]
uid = "4944"
author_uid = "renovate[bot]"
closes_threads = []
@@ -0,0 +1,5 @@
internal = "Update dependency astral-sh/uv to v0.8.17"
[[pull_requests]]
uid = "4945"
author_uid = "renovate[bot]"
closes_threads = []
@@ -0,0 +1,5 @@
internal = "Update github/codeql-action action to v3.30.3"
[[pull_requests]]
uid = "4946"
author_uid = "renovate[bot]"
closes_threads = []
@@ -0,0 +1,5 @@
internal = "Update Pylint to v3.3.8"
[[pull_requests]]
uid = "4947"
author_uid = "renovate[bot]"
closes_threads = []
@@ -0,0 +1,5 @@
internal = "Update Chango to v0.5.0"
[[pull_requests]]
uid = "4948"
author_uids = ["renovate[bot]"]
closes_threads = []
@@ -0,0 +1,5 @@
internal = "Update Mypy to v1.18.1"
[[pull_requests]]
uid = "4949"
author_uids = ["renovate[bot]"]
closes_threads = []
@@ -0,0 +1,5 @@
internal = "Align pre-commit hook APScheduler to with ``pyproject.toml``"
[[pull_requests]]
uid = "4950"
author_uids = ["renovate[bot]", "Bibo-Joshi"]
closes_threads = []
@@ -0,0 +1,5 @@
internal = "Align pre-commit hook cachetools to with ``pyproject.toml``"
[[pull_requests]]
uid = "4951"
author_uids = ["renovate[bot]", "Bibo-Joshi"]
closes_threads = []
@@ -0,0 +1,5 @@
internal = "Update pypa/gh-action-pypi-publish action to v1.13.0"
[[pull_requests]]
uid = "4952"
author_uid = "renovate[bot]"
closes_threads = []
@@ -0,0 +1,5 @@
internal = "Renovate: No README updates, label behaviour change, automerge lockfiles"
[[pull_requests]]
uid = "4953"
author_uid = "harshil21"
closes_threads = []
@@ -0,0 +1,5 @@
internal = "Lock file maintenance"
[[pull_requests]]
uid = "4954"
author_uids = ["renovate[bot]"]
closes_threads = []
@@ -0,0 +1,5 @@
internal = "Lock file maintenance"
[[pull_requests]]
uid = "4955"
author_uids = ["renovate[bot]"]
closes_threads = []
@@ -0,0 +1,5 @@
internal = "Update astral-sh/setup-uv digest to 208b0c0"
[[pull_requests]]
uid = "4958"
author_uids = ["renovate[bot]"]
closes_threads = []
@@ -0,0 +1,5 @@
internal = "Update dependency astral-sh/uv to v0.8.19"
[[pull_requests]]
uid = "4959"
author_uids = ["renovate[bot]"]
closes_threads = []
@@ -0,0 +1,5 @@
internal = "Update Mypy to v1.18.2"
[[pull_requests]]
uid = "4960"
author_uids = ["renovate[bot]"]
closes_threads = []
@@ -0,0 +1,5 @@
internal = "Update astral-sh/setup-uv action to v6.7.0"
[[pull_requests]]
uid = "4961"
author_uids = ["renovate[bot]"]
closes_threads = []
@@ -0,0 +1,5 @@
internal = "Update Ruff to v0.13.1"
[[pull_requests]]
uid = "4962"
author_uids = ["renovate[bot]"]
closes_threads = []
@@ -0,0 +1,5 @@
internal = "Update actions/stale action to v10"
[[pull_requests]]
uid = "4964"
author_uids = ["renovate[bot]"]
closes_threads = []
@@ -0,0 +1,5 @@
internal = "Properly Pin Dependency to ``astral/setup-uv`` in Copilot Setup Steps"
[[pull_requests]]
uid = "4965"
author_uids = ["Bibo-Joshi"]
closes_threads = []
@@ -0,0 +1,5 @@
internal = "Lock file maintenance"
[[pull_requests]]
uid = "4967"
author_uids = ["renovate[bot]"]
closes_threads = []
@@ -0,0 +1,5 @@
internal = "Tune Renovate Configuration"
[[pull_requests]]
uid = "4968"
author_uids = ["harshil21"]
closes_threads = []
@@ -0,0 +1,10 @@
breaking = """Move param ``ReplyParameters.checklist_task_id`` to last position.
.. hint::
This change addresses a breaking change accidentally introduced in version 22.4 where the introduction of the new parameter was not done in a backward compatible way.
Existing code using keyword arguments is unaffected. Only code using positional arguments (and based on version 22.4) may need updates.
"""
[[pull_requests]]
uid = "4972"
author_uids = ["aelkheir"]
closes_threads = []
@@ -0,0 +1,5 @@
bugfixes = "Fix Handling of Infinite Bootstrap Retries in ``Application.run_*`` and ``Updater.start_*``"
[[pull_requests]]
uid = "4973"
author_uids = ["Bibo-Joshi"]
closes_threads = ["4966"]
@@ -0,0 +1,4 @@
documentation = "Documentation Improvemennts"
[[pull_requests]]
uid = "4974"
author_uids = ["Bibo-Joshi"]
@@ -0,0 +1,5 @@
internal = "Update dependency astral-sh/uv to v0.8.22"
[[pull_requests]]
uid = "4975"
author_uids = ["renovate[bot]"]
closes_threads = []
@@ -0,0 +1,5 @@
internal = "Update github/codeql-action action to v3.30.5"
[[pull_requests]]
uid = "4976"
author_uids = ["renovate[bot]"]
closes_threads = []
@@ -0,0 +1,5 @@
internal = "Update Ruff to v0.13.2"
[[pull_requests]]
uid = "4977"
author_uids = ["renovate[bot]"]
closes_threads = []
@@ -0,0 +1,5 @@
internal = "Update dependency furo to v2025.9.25"
[[pull_requests]]
uid = "4978"
author_uids = ["renovate[bot]"]
closes_threads = []
@@ -0,0 +1,5 @@
other = "Bump Version to v22.5"
[[pull_requests]]
uid = "4979"
author_uids = ["Bibo-Joshi"]
closes_threads = []
+5 -5
View File
@@ -116,9 +116,9 @@ tests = [
"pytest-cov"
]
docs = [
"chango~=0.4.0; python_version >= '3.12'",
"chango~=0.5.0; python_version >= '3.12'",
"sphinx==8.2.3; python_version >= '3.11'",
"furo==2025.7.19",
"furo==2025.9.25",
"sphinx-paramlinks==0.6.0",
"sphinxcontrib-mermaid==1.0.0",
"sphinx-copybutton==0.5.2",
@@ -133,9 +133,9 @@ docs = [
]
linting = [
"pre-commit",
"ruff==0.12.7",
"mypy==1.16.1",
"pylint==3.3.7"
"ruff==0.13.2",
"mypy==1.18.2",
"pylint==3.3.8"
]
all = [{ include-group = "tests" }, { include-group = "docs" }, { include-group = "linting"}]
+113 -5
View File
@@ -21,15 +21,24 @@
import datetime as dtm
from collections.abc import Sequence
from typing import TYPE_CHECKING, Optional
from typing import TYPE_CHECKING, Optional, Union
from zoneinfo import ZoneInfo
from telegram._chat import Chat
from telegram._files.location import Location
from telegram._files.sticker import Sticker
from telegram._telegramobject import TelegramObject
from telegram._user import User
from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
from telegram._utils.argumentparsing import (
de_json_optional,
de_list_optional,
parse_sequence_arg,
)
from telegram._utils.datetime import (
extract_tzinfo_from_defaults,
from_timestamp,
get_zone_info,
)
from telegram._utils.types import JSONDict
if TYPE_CHECKING:
@@ -449,7 +458,7 @@ class BusinessOpeningHoursInterval(TelegramObject):
Examples:
A day has (24 * 60 =) 1440 minutes, a week has (7 * 1440 =) 10080 minutes.
Starting the the minute's sequence from Monday, example values of
Starting the minute's sequence from Monday, example values of
:attr:`opening_minute`, :attr:`closing_minute` will map to the following day times:
* Monday - 8am to 8:30pm:
@@ -552,7 +561,7 @@ class BusinessOpeningHours(TelegramObject):
time intervals describing business opening hours.
"""
__slots__ = ("opening_hours", "time_zone_name")
__slots__ = ("_cached_zone_info", "opening_hours", "time_zone_name")
def __init__(
self,
@@ -567,10 +576,109 @@ class BusinessOpeningHours(TelegramObject):
opening_hours
)
self._cached_zone_info: Optional[ZoneInfo] = None
self._id_attrs = (self.time_zone_name, self.opening_hours)
self._freeze()
@property
def _zone_info(self) -> ZoneInfo:
if self._cached_zone_info is None:
self._cached_zone_info = get_zone_info(self.time_zone_name)
return self._cached_zone_info
def get_opening_hours_for_day(
self, date: dtm.date, time_zone: Union[dtm.tzinfo, str, None] = None
) -> tuple[tuple[dtm.datetime, dtm.datetime], ...]:
"""Returns the opening hours intervals for a specific day as datetime objects.
.. versionadded:: 22.5
Args:
date (:obj:`datetime.date`): The date to get opening hours for.
time_zone (:obj:`datetime.tzinfo` | :obj:`str`, optional): Timezone to use for the
returned datetime objects. If not specified, then :attr:`time_zone_name` be used.
Returns:
tuple[tuple[:obj:`datetime.datetime`, :obj:`datetime.datetime`], ...]:
A tuple of datetime pairs representing opening and closing times for the specified day.
Each pair consists of ``(opening_time, closing_time)``.
Returns an empty tuple if there are no opening hours for the given day.
"""
week_day = date.weekday()
res = []
if isinstance(time_zone, str):
tz_target: dtm.tzinfo = get_zone_info(time_zone)
elif time_zone is None:
tz_target = self._zone_info
else:
tz_target = time_zone
for interval in self.opening_hours:
int_open = interval.opening_time
int_close = interval.closing_time
if int_open[0] != week_day:
continue
# To get the correct localization, we first need to create the dtm object in
# self.time_zone_name, then convert it to the target timezone. We could check if
# self._zone_info == tz_target and skip the conversion, but it's not worth the added
# complexity.
result_int_open = dtm.datetime(
year=date.year,
month=date.month,
day=date.day,
hour=int_open[1],
minute=int_open[2],
tzinfo=self._zone_info,
).astimezone(tz_target)
result_int_close = dtm.datetime(
year=date.year,
month=date.month,
day=date.day,
hour=int_close[1],
minute=int_close[2],
tzinfo=self._zone_info,
).astimezone(tz_target)
res.append((result_int_open, result_int_close))
# The sorting is currently an implementation detail
return tuple(sorted(res, key=lambda x: x[0]))
def is_open(self, datetime: dtm.datetime) -> bool:
"""Check if the business is open at the specified datetime.
.. versionadded:: 22.5
Args:
datetime (:obj:`datetime.datetime`): The datetime to check.
If the object is timezone-naive, it is assumed to be in the
timezone specified by :attr:`time_zone_name`.
Returns:
:obj:`bool`: True if the business is open at the specified time, False otherwise.
"""
datetime_in_native_tz = (
datetime.replace(tzinfo=self._zone_info) if datetime.tzinfo is None else datetime
).astimezone(self._zone_info)
minute_of_week = (
datetime_in_native_tz.weekday() * 1440
+ datetime_in_native_tz.hour * 60
+ datetime_in_native_tz.minute
)
for interval in self.opening_hours:
if interval.opening_minute <= minute_of_week < interval.closing_minute:
return True
return False
@classmethod
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "BusinessOpeningHours":
"""See :meth:`telegram.TelegramObject.de_json`."""
+7 -1
View File
@@ -377,6 +377,12 @@ class ReplyParameters(TelegramObject):
.. versionadded:: 20.8
.. versionchanged:: 22.5
The :paramref:`checklist_task_id` parameter has been moved to the last position to
maintain backward compatibility with versions prior to 22.4.
This resolves a breaking change accidentally introduced in version 22.4. See the changelog
for version 22.5 for more information.
Args:
message_id (:obj:`int`): Identifier of the message that will be replied to in the current
chat, or in the chat :paramref:`chat_id` if it is specified.
@@ -449,11 +455,11 @@ class ReplyParameters(TelegramObject):
message_id: int,
chat_id: Optional[Union[int, str]] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
checklist_task_id: Optional[int] = None,
quote: Optional[str] = None,
quote_parse_mode: ODVInput[str] = DEFAULT_NONE,
quote_entities: Optional[Sequence[MessageEntity]] = None,
quote_position: Optional[int] = None,
checklist_task_id: Optional[int] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
+2 -1
View File
@@ -38,6 +38,7 @@ if TYPE_CHECKING:
from telegram import Bot
Tele_co = TypeVar("Tele_co", bound="TelegramObject", covariant=True)
Tele = TypeVar("Tele", bound="TelegramObject")
class TelegramObject:
@@ -457,7 +458,7 @@ class TelegramObject:
return tuple(cls.de_json(d, bot) for d in data)
@contextmanager
def _unfrozen(self: Tele_co) -> Iterator[Tele_co]:
def _unfrozen(self: Tele) -> Iterator[Tele]:
"""Context manager to temporarily unfreeze the object. For internal use only.
Note:
+16
View File
@@ -32,6 +32,7 @@ import contextlib
import datetime as dtm
import os
import time
import zoneinfo
from typing import TYPE_CHECKING, Optional, Union
from telegram._utils.warnings import warn
@@ -231,6 +232,21 @@ def _datetime_to_float_timestamp(dt_obj: dtm.datetime) -> float:
return dt_obj.timestamp()
def get_zone_info(tz: str) -> zoneinfo.ZoneInfo:
"""Wrapper around the `ZoneInfo` constructor with slightly more helpful error message
in case tzdata is not installed.
"""
try:
return zoneinfo.ZoneInfo(tz)
except zoneinfo.ZoneInfoNotFoundError as err:
raise zoneinfo.ZoneInfoNotFoundError(
f"No time zone found with key {tz}. "
"Make sure to use a valid time zone name and "
f"correctly install the tzdata (https://pypi.org/project/tzdata/) package if "
"your system does not provide the time zone data."
) from err
def get_timedelta_value(
value: Optional[dtm.timedelta], attribute: str
) -> Optional[Union[int, dtm.timedelta]]:
+1 -1
View File
@@ -51,6 +51,6 @@ class Version(NamedTuple):
__version_info__: Final[Version] = Version(
major=22, minor=4, micro=0, releaselevel="final", serial=0
major=22, minor=5, micro=0, releaselevel="final", serial=0
)
__version__: Final[str] = str(__version_info__)
+1 -1
View File
@@ -116,8 +116,8 @@ class ApplicationHandlerStop(Exception):
class Application(
Generic[BT, CCT, UD, CD, BD, JQ],
contextlib.AbstractAsyncContextManager["Application"],
Generic[BT, CCT, UD, CD, BD, JQ],
):
"""This class dispatches all kinds of updates to its registered handlers, and is the entry
point to a PTB application.
+1 -1
View File
@@ -54,7 +54,7 @@ class PersistenceInput(NamedTuple):
callback_data: bool = True
class BasePersistence(Generic[UD, CD, BD], ABC):
class BasePersistence(ABC, Generic[UD, CD, BD]):
"""Interface class for adding persistence to your bot.
Subclass this object for different implementations of a persistent bot.
+2 -2
View File
@@ -580,7 +580,7 @@ class ExtBot(Bot, Generic[RLARGS]):
if isinstance(obj, CallbackQuery):
self.callback_data_cache.process_callback_query(obj)
return obj # type: ignore[return-value]
return obj
if isinstance(obj, Message):
if obj.reply_to_message:
@@ -595,7 +595,7 @@ class ExtBot(Bot, Generic[RLARGS]):
# Finally, handle the message itself
self.callback_data_cache.process_message(message=obj)
return obj # type: ignore[return-value]
return obj
if isinstance(obj, ChatFullInfo) and obj.pinned_message:
self.callback_data_cache.process_message(obj.pinned_message)
+1 -1
View File
@@ -33,7 +33,7 @@ RT = TypeVar("RT")
UT = TypeVar("UT")
class BaseHandler(Generic[UT, CCT, RT], ABC):
class BaseHandler(ABC, Generic[UT, CCT, RT]):
"""The base class for all update handlers. Create custom handlers by inheriting from it.
Warning:
+1 -2
View File
@@ -382,6 +382,7 @@ class Updater(contextlib.AbstractAsyncContextManager["Updater"]):
interval=poll_interval,
stop_event=self.__polling_task_stop_event,
max_retries=-1,
repeat_on_success=True,
),
name="Updater:start_polling:polling_task",
)
@@ -704,7 +705,6 @@ class Updater(contextlib.AbstractAsyncContextManager["Updater"]):
# delete_webhook for polling
if drop_pending_updates or not webhook_url:
await network_retry_loop(
is_running=lambda: self.running,
action_cb=bootstrap_del_webhook,
description="Bootstrap delete Webhook",
interval=bootstrap_interval,
@@ -716,7 +716,6 @@ class Updater(contextlib.AbstractAsyncContextManager["Updater"]):
# so we set it anyhow.
if webhook_url:
await network_retry_loop(
is_running=lambda: self.running,
action_cb=bootstrap_set_webhook,
description="Bootstrap Set Webhook",
interval=bootstrap_interval,
+15 -3
View File
@@ -51,12 +51,14 @@ async def network_retry_loop(
stop_event: Optional[asyncio.Event] = None,
is_running: Optional[Callable[[], bool]] = None,
max_retries: int,
repeat_on_success: bool = False,
) -> None:
"""Perform a loop calling `action_cb`, retrying after network errors.
Stop condition for loop in case of ``max_retries < 0``:
* `is_running()` evaluates :obj:`False`
* or `stop_event` is set.
* `stop_event` is set.
* calling `action_cb` succeeds and `repeat_on_success` is :obj:`False`.
Additional stop condition for loop in case of `max_retries >= 0``:
* a call to `action_cb` succeeds
@@ -87,8 +89,18 @@ async def network_retry_loop(
* 0: No retries.
* > 0: Number of retries.
repeat_on_success (:obj:`bool`): Whether to repeat the action after a successful call.
Defaults to :obj:`False`.
Raises:
ValueError: When passing `repeat_on_success=True` and `max_retries >= 0`. This case is
currently not supported.
"""
infinite_loop = max_retries < 0
if repeat_on_success and max_retries >= 0: # pragma: no cover
# This case here is only for completeness. It should not be used anywhere in the library.
raise ValueError("Cannot use repeat_on_success=True with max_retries >= 0")
log_prefix = f"Network Retry Loop ({description}):"
effective_is_running = is_running or (lambda: True)
@@ -120,7 +132,7 @@ async def network_retry_loop(
while effective_is_running():
try:
await do_action()
if not infinite_loop:
if not repeat_on_success:
_LOGGER.debug("%s Action succeeded. Stopping loop.", log_prefix)
break
except RetryAfter as exc:
+1 -1
View File
@@ -760,7 +760,7 @@ class TestSendMediaGroupWithoutRequest:
):
with pytest.raises(
ValueError,
match="You can only supply either group caption or media with captions.",
match="You can only supply either group caption or media with captions\\.",
):
await offline_bot.send_media_group(chat_id, group, caption="foo")
+1 -1
View File
@@ -420,7 +420,7 @@ class TestPassportWithoutRequest(PassportTestBase):
# Different error messages for different cryptography versions
with pytest.raises(
ValueError, match="(Could not deserialize key data)|(Unable to load PEM file)"
ValueError, match=r"(Could not deserialize key data)|(Unable to load PEM file)"
):
Bot(offline_bot.token, private_key=b"Invalid key!")
+21 -2
View File
@@ -23,6 +23,7 @@ import zoneinfo
import pytest
from telegram._utils import datetime as tg_dtm
from telegram._utils.datetime import get_zone_info
from telegram.ext import Defaults
# sample time specification values categorised into absolute / delta / time-of-day
@@ -138,7 +139,10 @@ class TestDatetime:
# of an xpass when the test is run in a timezone with the same UTC offset
ref_datetime = dtm.datetime(1970, 1, 1, 12)
utc_offset = timezone.utcoffset(ref_datetime)
ref_t, time_of_day = tg_dtm._datetime_to_float_timestamp(ref_datetime), ref_datetime.time()
ref_t, time_of_day = (
tg_dtm._datetime_to_float_timestamp(ref_datetime),
ref_datetime.time(),
)
aware_time_of_day = tg_dtm.localize(ref_datetime, timezone).timetz()
# first test that naive time is assumed to be utc:
@@ -168,7 +172,7 @@ class TestDatetime:
assert tg_dtm.to_timestamp(i) == int(tg_dtm.to_float_timestamp(i)), f"Failed for {i}"
def test_to_timestamp_none(self):
# this 'convenience' behaviour has been left left for backwards compatibility
# this 'convenience' behaviour has been left for backwards compatibility
assert tg_dtm.to_timestamp(None) is None
def test_from_timestamp_none(self):
@@ -193,6 +197,21 @@ class TestDatetime:
assert tg_dtm.extract_tzinfo_from_defaults(bot) is None
assert tg_dtm.extract_tzinfo_from_defaults(raw_bot) is None
def test_get_zone_info_with_valid_timezone_string(self):
"""Test with a valid timezone string."""
tz = "Asia/Tokyo"
result = get_zone_info(tz)
assert isinstance(result, zoneinfo.ZoneInfo)
assert str(result) == "Asia/Tokyo"
def test_get_zone_info_with_invalid_timezone_string(self):
"""Test with an invalid timezone string."""
with pytest.raises(
zoneinfo.ZoneInfoNotFoundError,
match=r"No time zone found.*Invalid/Timezone.*install the tzdata",
):
get_zone_info("Invalid/Timezone")
@pytest.mark.parametrize(
("arg", "timedelta_result", "number_result"),
[
+1 -1
View File
@@ -75,7 +75,7 @@ class TestFiles:
telegram._utils.files.parse_file_input(string, local_mode=False), InputFile
)
elif expected_non_local is ValueError:
with pytest.raises(ValueError, match="but local mode is not enabled."):
with pytest.raises(ValueError, match="but local mode is not enabled\\."):
telegram._utils.files.parse_file_input(string, local_mode=False)
else:
assert (
+52
View File
@@ -2359,6 +2359,58 @@ class TestApplication:
thread.join(timeout=10)
assert not thread.is_alive(), "Test took to long to run. Aborting"
@pytest.mark.parametrize("method_name", ["run_polling", "run_webhook"])
async def test_run_polling_webhook_infinite_bootstrap_retries(
self, monkeypatch, offline_bot, method_name
):
"""Here we simply test that setting `bootstrap_retries=-1` does not lead to the wrong
infinite-loop behavior reported in #4966. Raising an exception on the first call to
`initialize` ensures that a retry actually happens.
"""
def thread_target():
asyncio.set_event_loop(asyncio.new_event_loop())
async def post_init(application):
application.stop_running()
app = (
ApplicationBuilder()
.bot(offline_bot)
.application_class(PytestApplication)
.post_init(post_init)
.build()
)
async def do_pass(*args, **kwargs):
pass
monkeypatch.setattr(app.bot, "initialize", do_pass)
monkeypatch.setattr(app.bot, "delete_webhook", do_pass)
original_initialize = app.initialize
async def initialize(*args, **kwargs):
if self.count >= 3:
pytest.fail("Should be called only once. Test failed.")
self.count += 1
if self.count == 1:
raise TelegramError("Test Exception")
await original_initialize(*args, **kwargs)
monkeypatch.setattr(app, "initialize", initialize)
getattr(app, method_name)(
bootstrap_retries=-1,
close_loop=False,
stop_signals=None,
)
thread = Thread(target=thread_target)
thread.start()
thread.join(timeout=10)
assert not thread.is_alive(), "Test took to long to run. Aborting"
def test_signal_handlers(self, offline_bot, monkeypatch):
# this test should make sure that signal handlers are set by default on Linux + Mac,
# and not on Windows.
+1 -1
View File
@@ -110,7 +110,7 @@ class TestApplicationBuilder:
ApplicationBuilder()
def test_build_without_token(self, builder):
with pytest.raises(RuntimeError, match="No bot token was set."):
with pytest.raises(RuntimeError, match="No bot token was set\\."):
builder.build()
def test_build_custom_bot(self, builder, bot):
+2 -2
View File
@@ -404,7 +404,7 @@ class TestBasePersistence:
@default_papp
def test_set_bot_error(self, papp):
with pytest.raises(TypeError, match="when using telegram.ext.ExtBot"):
with pytest.raises(TypeError, match="when using telegram\\.ext\\.ExtBot"):
papp.persistence.set_bot(Bot(papp.bot.token))
# just making sure that setting an ExtBoxt without callback_data_cache doesn't raise an
@@ -419,7 +419,7 @@ class TestBasePersistence:
self.store_data = PersistenceInput(False, False, False, False)
with pytest.raises(
TypeError, match="persistence must be based on telegram.ext.BasePersistence"
TypeError, match="persistence must be based on telegram\\.ext\\.BasePersistence"
):
ApplicationBuilder().bot(bot).persistence(MyPersistence()).build()
+2 -2
View File
@@ -200,12 +200,12 @@ class TestCallbackContext:
callback_context = CallbackContext.from_update(update, app)
with pytest.raises(RuntimeError, match="This telegram.ext.ExtBot instance does not"):
with pytest.raises(RuntimeError, match="This telegram\\.ext\\.ExtBot instance does not"):
callback_context.drop_callback_data(None)
try:
app.bot = raw_bot
with pytest.raises(RuntimeError, match="telegram.Bot does not allow for"):
with pytest.raises(RuntimeError, match="telegram\\.Bot does not allow for"):
callback_context.drop_callback_data(None)
finally:
app.bot = bot
+1 -1
View File
@@ -320,7 +320,7 @@ class TestCallbackDataCache:
data=out.inline_keyboard[0][1].callback_data,
)
with pytest.raises(KeyError, match="CallbackQuery was not found in cache."):
with pytest.raises(KeyError, match="CallbackQuery was not found in cache\\."):
callback_data_cache.drop_data(callback_query)
callback_data_cache.process_callback_query(callback_query)
+1 -1
View File
@@ -629,7 +629,7 @@ class TestJobQueue:
async def test_attribute_error(self):
job = Job(self.job_run_once)
with pytest.raises(
AttributeError, match="nor 'apscheduler.job.Job' has attribute 'error'"
AttributeError, match="nor 'apscheduler\\.job\\.Job' has attribute 'error'"
):
job.error
+1 -1
View File
@@ -210,7 +210,7 @@ class TestMessageReactionHandler:
)
async def test_username_with_anonymous_reaction(self, app, allowed_types, kwargs):
with pytest.raises(
ValueError, match="You can not filter for users and include anonymous reactions."
ValueError, match="You can not filter for users and include anonymous reactions\\."
):
MessageReactionHandler(self.callback, message_reaction_types=allowed_types, **kwargs)
+1 -1
View File
@@ -54,7 +54,7 @@ class TestBaseRateLimiter:
request_received = None
async def test_no_rate_limiter(self, bot):
with pytest.raises(ValueError, match="if a `ExtBot.rate_limiter` is set"):
with pytest.raises(ValueError, match="if a `ExtBot\\.rate_limiter` is set"):
await bot.send_message(chat_id=42, text="test", rate_limit_args="something")
async def test_argument_passing(self, bot_info, monkeypatch, bot):
+50 -1
View File
@@ -849,7 +849,7 @@ class TestUpdater:
)
async def test_no_unix(self, updater):
async with updater:
with pytest.raises(RuntimeError, match="binding unix sockets."):
with pytest.raises(RuntimeError, match="binding unix sockets\\."):
await updater.start_webhook(unix="DoesntMatter", webhook_url="TOKEN")
async def test_start_webhook_already_running(self, updater, monkeypatch):
@@ -1141,3 +1141,52 @@ class TestUpdater:
await updater.stop()
assert not updater.running
@pytest.mark.parametrize("method_name", ["start_polling", "start_webhook"])
async def test_infinite_bootstrap_retries(self, updater, monkeypatch, method_name):
"""Here we simply test that setting `bootstrap_retries=-1` does not lead to the wrong
infinite-loop behavior reported in #4966. Raising an exception on the first call to
`set/delete_webhook` ensures that a retry actually happens.
"""
original_delete_webhook = updater.bot.delete_webhook
original_set_webhook = updater.bot.set_webhook
counts = {"delete": 0, "set": 0}
def patch_builder(func, name):
async def wrapped(*args, **kwargs):
if counts[name] >= 3:
pytest.fail("Should be called only once. Test failed.")
counts[name] += 1
if counts[name] == 1:
raise TelegramError("1")
return await func(*args, **kwargs)
return wrapped
async def get_updates(*args, **kwargs):
return []
monkeypatch.setattr(
updater.bot, "delete_webhook", patch_builder(original_delete_webhook, "delete")
)
monkeypatch.setattr(updater.bot, "set_webhook", patch_builder(original_set_webhook, "set"))
monkeypatch.setattr(updater.bot, "get_updates", get_updates)
kwargs = {"bootstrap_retries": -1}
if method_name == "start_webhook":
kwargs.update(
{
"listen": "127.0.0.1",
"port": randrange(1024, 49152),
}
)
async with updater:
task = asyncio.create_task(getattr(updater, method_name)(**kwargs))
try:
await asyncio.wait_for(task, timeout=10)
except TimeoutError:
pytest.fail(f"{method_name} did not succeed within the timeout. Aborting.")
finally:
await updater.stop()
+1 -1
View File
@@ -273,7 +273,7 @@ class TestRequestWithoutRequest:
with pytest.raises(
BadRequest,
match="{'unknown': '42'}",
match="\\{'unknown': '42'\\}",
):
await httpx_request.post(None, None, None)
+2 -2
View File
@@ -2672,7 +2672,7 @@ class TestBotWithRequest:
# No need to duplicate here.
async def test_invalid_token_server_response(self):
with pytest.raises(InvalidToken, match="The token `12` was rejected by the server."):
with pytest.raises(InvalidToken, match="The token `12` was rejected by the server\\."):
async with ExtBot(token="12"):
pass
@@ -3835,7 +3835,7 @@ class TestBotWithRequest:
#
# The error message Hide_requester_missing started showing up instead of
# User_already_participant. Don't know why …
with pytest.raises(BadRequest, match="User_already_participant|Hide_requester_missing"):
with pytest.raises(BadRequest, match=r"User_already_participant|Hide_requester_missing"):
await bot.decline_chat_join_request(chat_id=channel_id, user_id=chat_id)
async def test_set_chat_photo(self, bot, channel_id):
+225
View File
@@ -17,6 +17,7 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
import datetime as dtm
from zoneinfo import ZoneInfo
import pytest
@@ -554,3 +555,227 @@ class TestBusinessOpeningHoursWithoutRequest(BusinessTestBase):
assert boh1 != boh3
assert hash(boh1) != hash(boh3)
class TestBusinessOpeningHoursGetOpeningHoursForDayWithoutRequest:
@pytest.fixture
def sample_opening_hours(self):
# Monday 8am-8:30pm (480-1230)
# Tuesday 24 hours (1440-2879)
# Sunday 12am-11:58pm (8640-10078)
intervals = [
BusinessOpeningHoursInterval(480, 1230), # Monday 8am-8:30pm
BusinessOpeningHoursInterval(1440, 2879), # Tuesday 24 hours
BusinessOpeningHoursInterval(8640, 10078), # Sunday 12am-11:58pm
]
return BusinessOpeningHours(time_zone_name="UTC", opening_hours=intervals)
def test_monday_opening_hours(self, sample_opening_hours):
# Test for Monday
test_date = dtm.date(2023, 11, 6) # Monday
time_zone = ZoneInfo("UTC")
result = sample_opening_hours.get_opening_hours_for_day(test_date, time_zone)
expected = (
(
dtm.datetime(2023, 11, 6, 8, 0, tzinfo=time_zone),
dtm.datetime(2023, 11, 6, 20, 30, tzinfo=time_zone),
),
)
assert result == expected
def test_tuesday_24_hours(self, sample_opening_hours):
# Test for Tuesday (24 hours)
test_date = dtm.date(2023, 11, 7) # Tuesday
time_zone = ZoneInfo("UTC")
result = sample_opening_hours.get_opening_hours_for_day(test_date, time_zone)
expected = (
(
dtm.datetime(2023, 11, 7, 0, 0, tzinfo=time_zone),
dtm.datetime(2023, 11, 7, 23, 59, tzinfo=time_zone),
),
)
assert result == expected
def test_sunday_opening_hours(self, sample_opening_hours):
# Test for Sunday
test_date = dtm.date(2023, 11, 12) # Sunday
time_zone = ZoneInfo("UTC")
result = sample_opening_hours.get_opening_hours_for_day(test_date, time_zone)
expected = (
(
dtm.datetime(2023, 11, 12, 0, 0, tzinfo=time_zone),
dtm.datetime(2023, 11, 12, 23, 58, tzinfo=time_zone),
),
)
assert result == expected
def test_day_with_no_opening_hours(self, sample_opening_hours):
# Test for Wednesday (no opening hours defined)
test_date = dtm.date(2023, 11, 8) # Wednesday
time_zone = ZoneInfo("UTC")
result = sample_opening_hours.get_opening_hours_for_day(test_date, time_zone)
assert result == ()
def test_multiple_intervals_same_day(self):
# Test with multiple intervals on the same day
intervals = [
# unsorted on purpose to check that the sorting works (even though this is
# currently undocumented behaviour)
BusinessOpeningHoursInterval(900, 1230), # Monday 3pm-8:30pm
BusinessOpeningHoursInterval(480, 720), # Monday 8am-12pm
]
opening_hours = BusinessOpeningHours(time_zone_name="UTC", opening_hours=intervals)
test_date = dtm.date(2023, 11, 6) # Monday
time_zone = ZoneInfo("UTC")
result = opening_hours.get_opening_hours_for_day(test_date, time_zone)
expected = (
(
dtm.datetime(2023, 11, 6, 8, 0, tzinfo=time_zone),
dtm.datetime(2023, 11, 6, 12, 0, tzinfo=time_zone),
),
(
dtm.datetime(2023, 11, 6, 15, 0, tzinfo=time_zone),
dtm.datetime(2023, 11, 6, 20, 30, tzinfo=time_zone),
),
)
assert result == expected
@pytest.mark.parametrize("input_type", [str, ZoneInfo])
def test_timezone_conversion(self, sample_opening_hours, input_type):
# Test that timezone is properly applied
test_date = dtm.date(2023, 11, 6) # Monday
time_zone = input_type("America/New_York")
zone_info = ZoneInfo("America/New_York")
result = sample_opening_hours.get_opening_hours_for_day(test_date, time_zone)
expected = (
(
dtm.datetime(2023, 11, 6, 3, 0, tzinfo=zone_info),
dtm.datetime(2023, 11, 6, 15, 30, tzinfo=zone_info),
),
)
assert result == expected
assert result[0][0].tzinfo == zone_info
assert result[0][1].tzinfo == zone_info
def test_timezone_conversation_changing_date(self):
# test for the edge case where the returned time is on a different date in the target
# timezone than in the business timezone
intervals = [
BusinessOpeningHoursInterval(60, 120), # Monday 1am-2am UTC
]
opening_hours = BusinessOpeningHours(time_zone_name="UTC", opening_hours=intervals)
test_date = dtm.date(2023, 11, 6) # Monday
time_zone = ZoneInfo("America/New_York") # UTC-5, so 1am UTC is 8pm previous day
result = opening_hours.get_opening_hours_for_day(test_date, time_zone)
expected = (
(
dtm.datetime(2023, 11, 5, 20, 0, tzinfo=time_zone),
dtm.datetime(2023, 11, 5, 21, 0, tzinfo=time_zone),
),
)
assert result == expected
def test_no_timezone_provided(self, sample_opening_hours):
# Test when no timezone is provided
test_date = dtm.date(2023, 11, 6) # Monday
result = sample_opening_hours.get_opening_hours_for_day(test_date)
expected = (
(
dtm.datetime(
2023,
11,
6,
8,
0,
tzinfo=ZoneInfo(sample_opening_hours.time_zone_name),
),
dtm.datetime(
2023,
11,
6,
20,
30,
tzinfo=ZoneInfo(sample_opening_hours.time_zone_name),
),
),
)
assert result == expected
class TestBusinessOpeningHoursIsOpenWithoutRequest:
@pytest.fixture
def sample_opening_hours(self):
# Monday 8am-8:30pm (480-1230)
# Tuesday 24 hours (1440-2879)
# Sunday 12am-11:59pm (8640-10079)
intervals = [
BusinessOpeningHoursInterval(480, 1230), # Monday 8am-8:30pm UTC
BusinessOpeningHoursInterval(1440, 2879), # Tuesday 24 hours UTC
BusinessOpeningHoursInterval(8640, 10079), # Sunday 12am-11:59pm UTC
]
return BusinessOpeningHours(time_zone_name="UTC", opening_hours=intervals)
def test_is_open_during_business_hours(self, sample_opening_hours):
# Monday 10am UTC (within 8am-8:30pm)
dt = dtm.datetime(2023, 11, 6, 10, 0, tzinfo=ZoneInfo("UTC"))
assert sample_opening_hours.is_open(dt) is True
def test_is_open_at_opening_time(self, sample_opening_hours):
# Monday exactly 8am UTC
dt = dtm.datetime(2023, 11, 6, 8, 0, tzinfo=ZoneInfo("UTC"))
assert sample_opening_hours.is_open(dt) is True
def test_is_closed_at_closing_time(self, sample_opening_hours):
# Monday exactly 8:30pm UTC (closing time is exclusive)
dt = dtm.datetime(2023, 11, 6, 20, 30, tzinfo=ZoneInfo("UTC"))
assert sample_opening_hours.is_open(dt) is False
def test_is_closed_outside_business_hours(self, sample_opening_hours):
# Monday 7am UTC (before opening)
dt = dtm.datetime(2023, 11, 6, 7, 0, tzinfo=ZoneInfo("UTC"))
assert sample_opening_hours.is_open(dt) is False
def test_is_open_24h_day(self, sample_opening_hours):
# Tuesday 3am UTC (24h opening)
dt = dtm.datetime(2023, 11, 7, 3, 0, tzinfo=ZoneInfo("UTC"))
assert sample_opening_hours.is_open(dt) is True
def test_is_closed_on_day_with_no_hours(self, sample_opening_hours):
# Wednesday (no opening hours)
dt = dtm.datetime(2023, 11, 8, 12, 0, tzinfo=ZoneInfo("UTC"))
assert sample_opening_hours.is_open(dt) is False
def test_timezone_conversion(self, sample_opening_hours):
# Monday 5am EDT is 10am UTC (should be open)
dt = dtm.datetime(2023, 11, 6, 5, 0, tzinfo=ZoneInfo("America/New_York"))
assert sample_opening_hours.is_open(dt) is True
# Monday 2am EDT is 7am UTC (should be closed)
dt = dtm.datetime(2023, 11, 6, 2, 0, tzinfo=ZoneInfo("America/New_York"))
assert sample_opening_hours.is_open(dt) is False
def test_naive_datetime_uses_business_timezone(self, sample_opening_hours):
# Naive datetime - should be interpreted as UTC (business timezone)
dt = dtm.datetime(2023, 11, 6, 10, 0) # 10am naive
assert sample_opening_hours.is_open(dt) is True
def test_boundary_conditions(self, sample_opening_hours):
# Sunday 11:58pm UTC (should be open)
dt = dtm.datetime(2023, 11, 12, 23, 58, tzinfo=ZoneInfo("UTC"))
assert sample_opening_hours.is_open(dt) is True
# Sunday 11:59pm UTC (should be closed)
dt = dtm.datetime(2023, 11, 12, 23, 59, tzinfo=ZoneInfo("UTC"))
assert sample_opening_hours.is_open(dt) is False
+17 -17
View File
@@ -42,23 +42,23 @@ from tests.auxil.slots import mro_slots
class TestErrors:
def test_telegram_error(self):
with pytest.raises(TelegramError, match="^test message$"):
with pytest.raises(TelegramError, match=r"^test message$"):
raise TelegramError("test message")
with pytest.raises(TelegramError, match="^Test message$"):
with pytest.raises(TelegramError, match=r"^Test message$"):
raise TelegramError("Error: test message")
with pytest.raises(TelegramError, match="^Test message$"):
with pytest.raises(TelegramError, match=r"^Test message$"):
raise TelegramError("[Error]: test message")
with pytest.raises(TelegramError, match="^Test message$"):
with pytest.raises(TelegramError, match=r"^Test message$"):
raise TelegramError("Bad Request: test message")
def test_unauthorized(self):
with pytest.raises(Forbidden, match="test message"):
raise Forbidden("test message")
with pytest.raises(Forbidden, match="^Test message$"):
with pytest.raises(Forbidden, match=r"^Test message$"):
raise Forbidden("Error: test message")
with pytest.raises(Forbidden, match="^Test message$"):
with pytest.raises(Forbidden, match=r"^Test message$"):
raise Forbidden("[Error]: test message")
with pytest.raises(Forbidden, match="^Test message$"):
with pytest.raises(Forbidden, match=r"^Test message$"):
raise Forbidden("Bad Request: test message")
def test_invalid_token(self):
@@ -68,25 +68,25 @@ class TestErrors:
def test_network_error(self):
with pytest.raises(NetworkError, match="test message"):
raise NetworkError("test message")
with pytest.raises(NetworkError, match="^Test message$"):
with pytest.raises(NetworkError, match=r"^Test message$"):
raise NetworkError("Error: test message")
with pytest.raises(NetworkError, match="^Test message$"):
with pytest.raises(NetworkError, match=r"^Test message$"):
raise NetworkError("[Error]: test message")
with pytest.raises(NetworkError, match="^Test message$"):
with pytest.raises(NetworkError, match=r"^Test message$"):
raise NetworkError("Bad Request: test message")
def test_bad_request(self):
with pytest.raises(BadRequest, match="test message"):
raise BadRequest("test message")
with pytest.raises(BadRequest, match="^Test message$"):
with pytest.raises(BadRequest, match=r"^Test message$"):
raise BadRequest("Error: test message")
with pytest.raises(BadRequest, match="^Test message$"):
with pytest.raises(BadRequest, match=r"^Test message$"):
raise BadRequest("[Error]: test message")
with pytest.raises(BadRequest, match="^Test message$"):
with pytest.raises(BadRequest, match=r"^Test message$"):
raise BadRequest("Bad Request: test message")
def test_timed_out(self):
with pytest.raises(TimedOut, match="^Timed out$"):
with pytest.raises(TimedOut, match=r"^Timed out$"):
raise TimedOut
def test_chat_migrated(self):
@@ -97,11 +97,11 @@ class TestErrors:
@pytest.mark.parametrize("retry_after", [12, dtm.timedelta(seconds=12)])
def test_retry_after(self, PTB_TIMEDELTA, retry_after):
if PTB_TIMEDELTA:
with pytest.raises(RetryAfter, match="Flood control exceeded. Retry in 0:00:12"):
with pytest.raises(RetryAfter, match="Flood control exceeded\\. Retry in 0:00:12"):
raise (exception := RetryAfter(retry_after))
assert type(exception.retry_after) is dtm.timedelta
else:
with pytest.raises(RetryAfter, match="Flood control exceeded. Retry in 12 seconds"):
with pytest.raises(RetryAfter, match="Flood control exceeded\\. Retry in 12 seconds"):
raise (exception := RetryAfter(retry_after))
assert type(exception.retry_after) is int
@@ -118,7 +118,7 @@ class TestErrors:
assert type(retry_after) is int
def test_conflict(self):
with pytest.raises(Conflict, match="Something something."):
with pytest.raises(Conflict, match="Something something\\."):
raise Conflict("Something something.")
@pytest.mark.parametrize(
+4 -4
View File
@@ -603,13 +603,13 @@ class TestMessageWithoutRequest(MessageTestBase):
"""Used in testing reply_* below. Makes sure that do_quote is handled correctly"""
with pytest.raises(
ValueError,
match="`reply_to_message_id` and `reply_parameters` are mutually exclusive.",
match="`reply_to_message_id` and `reply_parameters` are mutually exclusive\\.",
):
await method(*args, reply_to_message_id=42, reply_parameters=42)
with pytest.raises(
ValueError,
match="`allow_sending_without_reply` and `reply_parameters` are mutually exclusive.",
match="`allow_sending_without_reply` and `reply_parameters` are mutually exclusive\\.",
):
await method(*args, allow_sending_without_reply=True, reply_parameters=42)
@@ -1463,7 +1463,7 @@ class TestMessageWithoutRequest(MessageTestBase):
message.text = "AA"
with pytest.raises(
ValueError,
match="You requested the 5-th occurrence of 'A', but this text appears only 2 times.",
match="You requested the 5-th occurrence of 'A', but this text appears only 2 times",
):
message.compute_quote_position_and_entities("A", 5)
@@ -1472,7 +1472,7 @@ class TestMessageWithoutRequest(MessageTestBase):
message.caption = None
with pytest.raises(
RuntimeError,
match="This message has neither text nor caption.",
match="This message has neither text nor caption\\.",
):
message.compute_quote_position_and_entities("A", 5)
Generated
+472 -509
View File
File diff suppressed because it is too large Load Diff