Compare commits

...

46 Commits

Author SHA1 Message Date
Beverly Nguyen
dc14da9638 3110 dashboard design feedback & permission changes (#3142)
* update org dashboard

* allowing org admins to access serviecs within an org

* fix test
2025-12-08 20:26:00 +00:00
Alex Janousek
957c3ee1e9 Fix dependabot prs (#3151)
* Bump terser-webpack-plugin from 5.3.14 to 5.3.15

Bumps [terser-webpack-plugin](https://github.com/webpack/terser-webpack-plugin) from 5.3.14 to 5.3.15.
- [Release notes](https://github.com/webpack/terser-webpack-plugin/releases)
- [Changelog](https://github.com/webpack/terser-webpack-plugin/blob/main/CHANGELOG.md)
- [Commits](https://github.com/webpack/terser-webpack-plugin/compare/v5.3.14...v5.3.15)

---
updated-dependencies:
- dependency-name: terser-webpack-plugin
  dependency-version: 5.3.15
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Bump moto from 5.1.17 to 5.1.18

Bumps [moto](https://github.com/getmoto/moto) from 5.1.17 to 5.1.18.
- [Release notes](https://github.com/getmoto/moto/releases)
- [Changelog](https://github.com/getmoto/moto/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getmoto/moto/compare/5.1.17...5.1.18)

---
updated-dependencies:
- dependency-name: moto
  dependency-version: 5.1.18
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Bump pip-audit from 2.9.0 to 2.10.0

Bumps [pip-audit](https://github.com/pypa/pip-audit) from 2.9.0 to 2.10.0.
- [Release notes](https://github.com/pypa/pip-audit/releases)
- [Changelog](https://github.com/pypa/pip-audit/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pypa/pip-audit/compare/v2.9.0...v2.10.0)

---
updated-dependencies:
- dependency-name: pip-audit
  dependency-version: 2.10.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-08 10:15:21 -08:00
Beverly Nguyen
a81c9d0ff0 Remove SMS Total Message Limit from Org Dash (#3144)
* Remove SMS Total Message Limit from Org Dash

* update text

* pytest
2025-12-08 12:12:19 -05:00
dependabot[bot]
cd5aeceb43 Bump hypothesis from 6.148.6 to 6.148.7 (#3148)
Bumps [hypothesis](https://github.com/HypothesisWorks/hypothesis) from 6.148.6 to 6.148.7.
- [Release notes](https://github.com/HypothesisWorks/hypothesis/releases)
- [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-python-6.148.6...hypothesis-python-6.148.7)

---
updated-dependencies:
- dependency-name: hypothesis
  dependency-version: 6.148.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-08 15:21:00 +00:00
dependabot[bot]
b2235d6490 Bump phonenumbers from 9.0.19 to 9.0.20 (#3149)
Bumps [phonenumbers](https://github.com/daviddrysdale/python-phonenumbers) from 9.0.19 to 9.0.20.
- [Commits](https://github.com/daviddrysdale/python-phonenumbers/compare/v9.0.19...v9.0.20)

---
updated-dependencies:
- dependency-name: phonenumbers
  dependency-version: 9.0.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-08 15:13:10 +00:00
dependabot[bot]
22af71718b Bump urllib3 from 2.5.0 to 2.6.0 (#3146)
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.5.0 to 2.6.0.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.5.0...2.6.0)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-version: 2.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-08 10:05:27 -05:00
dependabot[bot]
0f9e61b831 Bump hypothesis from 6.148.5 to 6.148.6 (#3145)
Bumps [hypothesis](https://github.com/HypothesisWorks/hypothesis) from 6.148.5 to 6.148.6.
- [Release notes](https://github.com/HypothesisWorks/hypothesis/releases)
- [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-python-6.148.5...hypothesis-python-6.148.6)

---
updated-dependencies:
- dependency-name: hypothesis
  dependency-version: 6.148.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-05 09:34:47 -05:00
dependabot[bot]
612f585cbf Bump beautifulsoup4 from 4.14.2 to 4.14.3 (#3137)
Bumps [beautifulsoup4](https://www.crummy.com/software/BeautifulSoup/bs4/) from 4.14.2 to 4.14.3.

---
updated-dependencies:
- dependency-name: beautifulsoup4
  dependency-version: 4.14.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-02 15:54:20 +00:00
dependabot[bot]
0c9626ff4d Bump flake8-bugbear from 25.10.21 to 25.11.29 (#3141)
Bumps [flake8-bugbear](https://github.com/PyCQA/flake8-bugbear) from 25.10.21 to 25.11.29.
- [Release notes](https://github.com/PyCQA/flake8-bugbear/releases)
- [Commits](https://github.com/PyCQA/flake8-bugbear/compare/25.10.21...25.11.29)

---
updated-dependencies:
- dependency-name: flake8-bugbear
  dependency-version: 25.11.29
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-02 15:46:54 +00:00
dependabot[bot]
9bc6290944 Bump hypothesis from 6.148.3 to 6.148.5 (#3138)
Bumps [hypothesis](https://github.com/HypothesisWorks/hypothesis) from 6.148.3 to 6.148.5.
- [Release notes](https://github.com/HypothesisWorks/hypothesis/releases)
- [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-python-6.148.3...hypothesis-python-6.148.5)

---
updated-dependencies:
- dependency-name: hypothesis
  dependency-version: 6.148.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-02 15:39:36 +00:00
dependabot[bot]
0d9061287c Bump werkzeug from 3.1.3 to 3.1.4 (#3143)
Bumps [werkzeug](https://github.com/pallets/werkzeug) from 3.1.3 to 3.1.4.
- [Release notes](https://github.com/pallets/werkzeug/releases)
- [Changelog](https://github.com/pallets/werkzeug/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/werkzeug/compare/3.1.3...3.1.4)

---
updated-dependencies:
- dependency-name: werkzeug
  dependency-version: 3.1.4
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-02 10:32:12 -05:00
dependabot[bot]
c5b3896b8d Bump hypothesis from 6.148.2 to 6.148.3 (#3136)
Bumps [hypothesis](https://github.com/HypothesisWorks/hypothesis) from 6.148.2 to 6.148.3.
- [Release notes](https://github.com/HypothesisWorks/hypothesis/releases)
- [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-python-6.148.2...hypothesis-python-6.148.3)

---
updated-dependencies:
- dependency-name: hypothesis
  dependency-version: 6.148.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-28 21:56:14 -05:00
dependabot[bot]
f887a19733 Bump playwright from 1.56.1 to 1.57.0 (#3134)
Bumps [playwright](https://github.com/microsoft/playwright) from 1.56.1 to 1.57.0.
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.56.1...v1.57.0)

---
updated-dependencies:
- dependency-name: playwright
  dependency-version: 1.57.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-26 10:23:45 -05:00
dependabot[bot]
2b5d00ee23 Bump pytest from 8.4.2 to 9.0.1 (#3130)
Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.4.2 to 9.0.1.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/8.4.2...9.0.1)

---
updated-dependencies:
- dependency-name: pytest
  dependency-version: 9.0.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-25 00:19:55 +00:00
dependabot[bot]
62b8aa26cc Bump pre-commit from 4.4.0 to 4.5.0 (#3132)
Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 4.4.0 to 4.5.0.
- [Release notes](https://github.com/pre-commit/pre-commit/releases)
- [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pre-commit/pre-commit/compare/v4.4.0...v4.5.0)

---
updated-dependencies:
- dependency-name: pre-commit
  dependency-version: 4.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-25 00:12:11 +00:00
dependabot[bot]
149258347f Bump bandit from 1.9.1 to 1.9.2 (#3131)
Bumps [bandit](https://github.com/PyCQA/bandit) from 1.9.1 to 1.9.2.
- [Release notes](https://github.com/PyCQA/bandit/releases)
- [Commits](https://github.com/PyCQA/bandit/compare/1.9.1...1.9.2)

---
updated-dependencies:
- dependency-name: bandit
  dependency-version: 1.9.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-24 19:07:14 -05:00
dependabot[bot]
325d9acc8f Bump pytest-playwright from 0.7.1 to 0.7.2 (#3133)
Bumps [pytest-playwright](https://github.com/microsoft/playwright-pytest) from 0.7.1 to 0.7.2.
- [Release notes](https://github.com/microsoft/playwright-pytest/releases)
- [Commits](https://github.com/microsoft/playwright-pytest/compare/v0.7.1...v0.7.2)

---
updated-dependencies:
- dependency-name: pytest-playwright
  dependency-version: 0.7.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-24 19:04:02 -05:00
Alex Janousek
2c51e476ea Security update (#3129) 2025-11-24 13:30:30 -08:00
dependabot[bot]
31a46b7485 Bump exceptiongroup from 1.3.0 to 1.3.1 (#3128)
Bumps [exceptiongroup](https://github.com/agronholm/exceptiongroup) from 1.3.0 to 1.3.1.
- [Release notes](https://github.com/agronholm/exceptiongroup/releases)
- [Changelog](https://github.com/agronholm/exceptiongroup/blob/main/CHANGES.rst)
- [Commits](https://github.com/agronholm/exceptiongroup/compare/1.3.0...1.3.1)

---
updated-dependencies:
- dependency-name: exceptiongroup
  dependency-version: 1.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-22 08:48:21 -05:00
dependabot[bot]
8b203666df Bump phonenumbers from 9.0.18 to 9.0.19 (#3127)
Bumps [phonenumbers](https://github.com/daviddrysdale/python-phonenumbers) from 9.0.18 to 9.0.19.
- [Commits](https://github.com/daviddrysdale/python-phonenumbers/compare/v9.0.18...v9.0.19)

---
updated-dependencies:
- dependency-name: phonenumbers
  dependency-version: 9.0.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-21 08:09:11 -05:00
Beverly Nguyen
0374e46e8c Merge pull request #3125 from GSA/3080-bring-up-jest-coverage
bring up jest and pytest coverage
2025-11-20 10:00:20 -08:00
dependabot[bot]
770f0203cf Bump redis from 7.0.1 to 7.1.0 (#3122)
Bumps [redis](https://github.com/redis/redis-py) from 7.0.1 to 7.1.0.
- [Release notes](https://github.com/redis/redis-py/releases)
- [Changelog](https://github.com/redis/redis-py/blob/master/CHANGES)
- [Commits](https://github.com/redis/redis-py/compare/v7.0.1...v7.1.0)

---
updated-dependencies:
- dependency-name: redis
  dependency-version: 7.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-20 12:46:45 -05:00
dependabot[bot]
719aa01f0c Bump sass from 1.94.1 to 1.94.2 (#3123)
Bumps [sass](https://github.com/sass/dart-sass) from 1.94.1 to 1.94.2.
- [Release notes](https://github.com/sass/dart-sass/releases)
- [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/dart-sass/compare/1.94.1...1.94.2)

---
updated-dependencies:
- dependency-name: sass
  dependency-version: 1.94.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-20 12:45:40 -05:00
Beverly Nguyen
23a55bcf96 test coverage 2025-11-19 19:59:57 -08:00
Beverly Nguyen
4d8cc358b4 org test 2025-11-19 19:55:24 -08:00
Beverly Nguyen
766a656311 improve test coverage 2025-11-19 19:34:20 -08:00
dependabot[bot]
567f80ce7d Bump bandit from 1.8.6 to 1.9.1 (#3105)
Bumps [bandit](https://github.com/PyCQA/bandit) from 1.8.6 to 1.9.1.
- [Release notes](https://github.com/PyCQA/bandit/releases)
- [Commits](https://github.com/PyCQA/bandit/compare/1.8.6...1.9.1)

---
updated-dependencies:
- dependency-name: bandit
  dependency-version: 1.9.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-19 12:53:56 -05:00
dependabot[bot]
d603f8e8fd Bump sass from 1.94.0 to 1.94.1 (#3106)
Bumps [sass](https://github.com/sass/dart-sass) from 1.94.0 to 1.94.1.
- [Release notes](https://github.com/sass/dart-sass/releases)
- [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/dart-sass/compare/1.94.0...1.94.1)

---
updated-dependencies:
- dependency-name: sass
  dependency-version: 1.94.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-19 12:52:38 -05:00
dependabot[bot]
227839e7ca Bump webpack from 5.102.1 to 5.103.0 (#3107)
Bumps [webpack](https://github.com/webpack/webpack) from 5.102.1 to 5.103.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.102.1...v5.103.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-version: 5.103.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-19 12:51:33 -05:00
dependabot[bot]
b2ab3f6361 Bump hypothesis from 6.148.1 to 6.148.2 (#3108)
Bumps [hypothesis](https://github.com/HypothesisWorks/hypothesis) from 6.148.1 to 6.148.2.
- [Release notes](https://github.com/HypothesisWorks/hypothesis/releases)
- [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-python-6.148.1...hypothesis-python-6.148.2)

---
updated-dependencies:
- dependency-name: hypothesis
  dependency-version: 6.148.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-19 12:50:27 -05:00
dependabot[bot]
f63103832e Bump coverage from 7.11.3 to 7.12.0 (#3109)
Bumps [coverage](https://github.com/coveragepy/coveragepy) from 7.11.3 to 7.12.0.
- [Release notes](https://github.com/coveragepy/coveragepy/releases)
- [Changelog](https://github.com/coveragepy/coveragepy/blob/main/CHANGES.rst)
- [Commits](https://github.com/coveragepy/coveragepy/compare/7.11.3...7.12.0)

---
updated-dependencies:
- dependency-name: coverage
  dependency-version: 7.12.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-19 12:49:22 -05:00
dependabot[bot]
f315bd4f47 Bump click from 8.3.0 to 8.3.1 (#3102)
Bumps [click](https://github.com/pallets/click) from 8.3.0 to 8.3.1.
- [Release notes](https://github.com/pallets/click/releases)
- [Changelog](https://github.com/pallets/click/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/click/compare/8.3.0...8.3.1)

---
updated-dependencies:
- dependency-name: click
  dependency-version: 8.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-18 14:43:56 +00:00
dependabot[bot]
a552d2d046 Bump js-yaml (#3100)
Bumps  and [js-yaml](https://github.com/nodeca/js-yaml). These dependencies needed to be updated together.

Updates `js-yaml` from 4.1.0 to 4.1.1
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1)

Updates `js-yaml` from 3.14.1 to 3.14.2
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.1.1
  dependency-type: indirect
- dependency-name: js-yaml
  dependency-version: 3.14.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-18 09:39:37 -05:00
dependabot[bot]
0e73bef2ae Bump hypothesis from 6.147.0 to 6.148.1 (#3101)
Bumps [hypothesis](https://github.com/HypothesisWorks/hypothesis) from 6.147.0 to 6.148.1.
- [Release notes](https://github.com/HypothesisWorks/hypothesis/releases)
- [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-python-6.147.0...hypothesis-python-6.148.1)

---
updated-dependencies:
- dependency-name: hypothesis
  dependency-version: 6.148.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-18 14:36:41 +00:00
dependabot[bot]
39f1aa5b6d Bump numpy from 2.3.4 to 2.3.5 (#3103)
Bumps [numpy](https://github.com/numpy/numpy) from 2.3.4 to 2.3.5.
- [Release notes](https://github.com/numpy/numpy/releases)
- [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst)
- [Commits](https://github.com/numpy/numpy/compare/v2.3.4...v2.3.5)

---
updated-dependencies:
- dependency-name: numpy
  dependency-version: 2.3.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-18 09:29:21 -05:00
dependabot[bot]
5bfb46dcfd Bump moto from 5.1.16 to 5.1.17 (#3104)
Bumps [moto](https://github.com/getmoto/moto) from 5.1.16 to 5.1.17.
- [Release notes](https://github.com/getmoto/moto/releases)
- [Changelog](https://github.com/getmoto/moto/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getmoto/moto/compare/5.1.16...5.1.17)

---
updated-dependencies:
- dependency-name: moto
  dependency-version: 5.1.17
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-18 09:25:56 -05:00
dependabot[bot]
d900264678 Bump cachetools from 6.2.1 to 6.2.2 (#3099)
Bumps [cachetools](https://github.com/tkem/cachetools) from 6.2.1 to 6.2.2.
- [Changelog](https://github.com/tkem/cachetools/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/tkem/cachetools/compare/v6.2.1...v6.2.2)

---
updated-dependencies:
- dependency-name: cachetools
  dependency-version: 6.2.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-14 07:37:47 -05:00
ccostino
fe6e7d8fba Merge pull request #3098 from GSA/update-qa-checklist
Update QA checklist
2025-11-13 17:41:54 -05:00
Beverly Nguyen
04b9edf9c2 Update QA checklist 2025-11-13 14:15:21 -08:00
Beverly Nguyen
305db60c07 Fix Nav Highlighting and Styling (#3097) 2025-11-13 15:38:14 -05:00
ccostino
66379f7966 Merge pull request #3093 from GSA/3088-org-dashboard---users-should-be-able-to-delete-service
add js delete function and css
2025-11-13 13:49:47 -05:00
ccostino
a11d47d0c3 Merge pull request #3096 from GSA/dependabot/pip/certifi-2025.11.12
Bump certifi from 2025.10.5 to 2025.11.12
2025-11-13 13:37:00 -05:00
dependabot[bot]
8c01c39e7c Bump certifi from 2025.10.5 to 2025.11.12
Bumps [certifi](https://github.com/certifi/python-certifi) from 2025.10.5 to 2025.11.12.
- [Commits](https://github.com/certifi/python-certifi/compare/2025.10.05...2025.11.12)

---
updated-dependencies:
- dependency-name: certifi
  dependency-version: 2025.11.12
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-12 23:08:22 +00:00
Beverly Nguyen
0f995ce392 adding back css that was removed 2025-11-12 13:36:52 -08:00
Beverly Nguyen
77d9bfb870 added encoded for defense 2025-11-12 11:03:51 -08:00
Beverly Nguyen
63d65c0305 add js delete function 2025-11-12 10:57:32 -08:00
33 changed files with 2797 additions and 3685 deletions

View File

@@ -83,6 +83,25 @@ body:
- label: Clicking on “Remove this team member” prompts a confirmation
- label: Clicking the confirmation removes the team member
- type: checkboxes
id: org_dashboard
attributes:
label: Organization Dashboard
options:
- label: Dashboard navigation item is highlighted when viewing organization dashboard
- label: Clicking "Create new service" button shows service creation form
- label: Clicking "Add org admin" button shows user invitation form
- label: Clicking "Manage" on a service shows the manage service form
- label: In manage service form, service name, primary contact, and status can be edited
- label: Clicking "Save Changes" shows confirmation modal
- label: Confirming changes updates the service and shows success message
- label: Updated service row is highlighted in the services table
- label: The "Delete Service" button only appears for trial services or platform admins
- label: Clicking "Delete Service" shows confirmation modal with service name
- label: Confirming deletion suspends the service and shows success message
- label: Deleted service is removed from the services table
- label: Cancel buttons on all forms return to dashboard without making changes
- type: checkboxes
id: sign_out
attributes:

View File

@@ -57,9 +57,41 @@
}
}
function initDeleteServiceConfirmation() {
var deleteButtons = document.querySelectorAll('[data-open-modal="confirmDeleteModal"]');
var confirmDeleteButton = document.getElementById('delete-service-confirm-btn');
var deleteServiceNameDisplay = document.getElementById('delete-service-name-display');
var deleteForm = document.getElementById('delete-service-form');
if (deleteButtons.length > 0 && confirmDeleteButton && deleteForm) {
var currentServiceId = null;
deleteButtons.forEach(function(button) {
button.addEventListener('click', function() {
currentServiceId = button.getAttribute('data-service-id');
var serviceName = button.getAttribute('data-service-name');
if (deleteServiceNameDisplay && serviceName) {
deleteServiceNameDisplay.textContent = serviceName;
}
});
});
confirmDeleteButton.addEventListener('click', function() {
if (currentServiceId) {
var orgId = window.location.pathname.split('/')[2];
deleteForm.action = '/organizations/' + encodeURIComponent(orgId) + '?action=delete-service&service_id=' + encodeURIComponent(currentServiceId);
deleteForm.submit();
}
});
}
}
document.addEventListener('DOMContentLoaded', function() {
initForms();
initEditServiceConfirmation();
initDeleteServiceConfirmation();
});
window.OrganizationDashboard = {

View File

@@ -48,3 +48,8 @@
display: inline-flex;
justify-content: center;
}
.button-not-clickable {
cursor: default;
pointer-events: none;
}

View File

@@ -47,3 +47,26 @@ td.table-empty-message {
box-shadow: none;
}
}
.is-highlighted {
td {
background-color: color('blue-cool-5v');
animation: fadeHighlight 3s ease-out forwards;
}
}
@keyframes fadeHighlight {
0% {
background-color: color('blue-cool-10v');
}
50% {
background-color: color('blue-cool-5v');
}
100% {
background-color: transparent;
}
}
.job-table {
width: 100%;
}

View File

@@ -123,7 +123,7 @@ def get_filtered_jobs(service_id, page):
@main.route("/activity/services/<uuid:service_id>")
@user_has_permissions(ServicePermission.VIEW_ACTIVITY)
@user_has_permissions(ServicePermission.VIEW_ACTIVITY, allow_org_user=True)
def all_jobs_activity(service_id):
service_data_retention_days = 8
page = get_page_from_request()

View File

@@ -23,7 +23,7 @@ dummy_bearer_token = "bearer_token_set" # nosec B105 - this is not a real token
@main.route("/services/<uuid:service_id>/api")
@user_has_permissions("manage_api_keys")
@user_has_permissions("manage_api_keys", allow_org_user=True)
def api_integration(service_id):
callbacks_link = (
".api_callbacks"
@@ -40,7 +40,7 @@ def api_integration(service_id):
@main.route("/services/<uuid:service_id>/api/documentation")
@user_has_permissions("manage_api_keys")
@user_has_permissions("manage_api_keys", allow_org_user=True)
def api_documentation(service_id):
return redirect(url_for(".documentation"), code=301)
@@ -51,7 +51,7 @@ def api_documentation(service_id):
endpoint="old_guest_list",
)
@main.route("/services/<uuid:service_id>/api/guest-list", methods=["GET", "POST"])
@user_has_permissions("manage_api_keys")
@user_has_permissions("manage_api_keys", allow_org_user=True)
def guest_list(service_id):
form = GuestList()
if form.validate_on_submit():
@@ -70,7 +70,7 @@ def guest_list(service_id):
@main.route("/services/<uuid:service_id>/api/keys")
@user_has_permissions("manage_api_keys")
@user_has_permissions("manage_api_keys", allow_org_user=True)
def api_keys(service_id):
return render_template(
"views/api/keys.html",
@@ -78,7 +78,7 @@ def api_keys(service_id):
@main.route("/services/<uuid:service_id>/api/keys/create", methods=["GET", "POST"])
@user_has_permissions("manage_api_keys", restrict_admin_usage=True)
@user_has_permissions("manage_api_keys", restrict_admin_usage=True, allow_org_user=True)
def create_api_key(service_id):
form = CreateKeyForm(current_service.api_keys)
form.key_type.choices = [
@@ -118,7 +118,7 @@ def create_api_key(service_id):
@main.route(
"/services/<uuid:service_id>/api/keys/revoke/<uuid:key_id>", methods=["GET", "POST"]
)
@user_has_permissions("manage_api_keys")
@user_has_permissions("manage_api_keys", allow_org_user=True)
def revoke_api_key(service_id, key_id):
key_name = current_service.get_api_key(key_id)["name"]
if request.method == "GET":
@@ -161,7 +161,7 @@ def check_token_against_dummy_bearer(token):
@main.route("/services/<uuid:service_id>/api/callbacks", methods=["GET"])
@user_has_permissions("manage_api_keys")
@user_has_permissions("manage_api_keys", allow_org_user=True)
def api_callbacks(service_id):
if not current_service.has_permission(ServicePermission.INBOUND_SMS):
return redirect(url_for(".delivery_status_callback", service_id=service_id))
@@ -192,7 +192,7 @@ def get_delivery_status_callback_details():
"/services/<uuid:service_id>/api/callbacks/delivery-status-callback",
methods=["GET", "POST"],
)
@user_has_permissions("manage_api_keys")
@user_has_permissions("manage_api_keys", allow_org_user=True)
def delivery_status_callback(service_id):
delivery_status_callback = get_delivery_status_callback_details()
back_link = (
@@ -259,7 +259,7 @@ def get_received_text_messages_callback():
"/services/<uuid:service_id>/api/callbacks/received-text-messages-callback",
methods=["GET", "POST"],
)
@user_has_permissions("manage_api_keys")
@user_has_permissions("manage_api_keys", allow_org_user=True)
def received_text_messages_callback(service_id):
if not current_service.has_permission(ServicePermission.INBOUND_SMS):
return redirect(url_for(".api_integration", service_id=service_id))

View File

@@ -13,7 +13,7 @@ from notifications_utils.template import SMSPreviewTemplate
@main.route("/services/<uuid:service_id>/conversation/<uuid:notification_id>")
@user_has_permissions(ServicePermission.VIEW_ACTIVITY)
@user_has_permissions(ServicePermission.VIEW_ACTIVITY, allow_org_user=True)
def conversation(service_id, notification_id):
user_number = get_user_number(service_id, notification_id)
@@ -31,7 +31,7 @@ def conversation(service_id, notification_id):
@main.route("/services/<uuid:service_id>/conversation/<uuid:notification_id>.json")
@user_has_permissions(ServicePermission.VIEW_ACTIVITY)
@user_has_permissions(ServicePermission.VIEW_ACTIVITY, allow_org_user=True)
def conversation_updates(service_id, notification_id):
return jsonify(
get_conversation_partials(
@@ -46,7 +46,7 @@ def conversation_updates(service_id, notification_id):
@main.route(
"/services/<uuid:service_id>/conversation/<uuid:notification_id>/reply-with/from-folder/<uuid:from_folder>"
)
@user_has_permissions(ServicePermission.SEND_MESSAGES)
@user_has_permissions(ServicePermission.SEND_MESSAGES, allow_org_user=True)
def conversation_reply(
service_id,
notification_id,
@@ -70,7 +70,7 @@ def conversation_reply(
@main.route(
"/services/<uuid:service_id>/conversation/<uuid:notification_id>/reply-with/<uuid:template_id>"
)
@user_has_permissions(ServicePermission.SEND_MESSAGES)
@user_has_permissions(ServicePermission.SEND_MESSAGES, allow_org_user=True)
def conversation_reply_with_template(
service_id,
notification_id,

View File

@@ -24,13 +24,13 @@ from app.utils.user import user_has_permissions
@main.route("/services/<uuid:service_id>/dashboard")
@user_has_permissions(ServicePermission.VIEW_ACTIVITY, ServicePermission.SEND_MESSAGES)
@user_has_permissions(ServicePermission.VIEW_ACTIVITY, ServicePermission.SEND_MESSAGES, allow_org_user=True)
def old_service_dashboard(service_id):
return redirect(url_for(".service_dashboard", service_id=service_id))
@main.route("/services/<uuid:service_id>")
@user_has_permissions()
@user_has_permissions(allow_org_user=True)
def service_dashboard(service_id):
if session.get("invited_user_id"):
@@ -84,7 +84,7 @@ def job_is_finished(job_dict):
@main.route("/services/<uuid:service_id>/daily-stats.json")
@user_has_permissions()
@user_has_permissions(allow_org_user=True)
def get_daily_stats(service_id):
date_range = get_stats_date_range()
days = date_range["days"]
@@ -148,7 +148,7 @@ def get_local_daily_stats_for_last_x_days(stats_utc, user_timezone, days):
@main.route("/services/<uuid:service_id>/daily-stats-by-user.json")
@user_has_permissions()
@user_has_permissions(allow_org_user=True)
def get_daily_stats_by_user(service_id):
date_range = get_stats_date_range()
days = date_range["days"]
@@ -166,7 +166,7 @@ def get_daily_stats_by_user(service_id):
@main.route("/services/<uuid:service_id>/template-usage")
@user_has_permissions(ServicePermission.VIEW_ACTIVITY)
@user_has_permissions(ServicePermission.VIEW_ACTIVITY, allow_org_user=True)
def template_usage(service_id):
year, current_financial_year = requested_and_current_financial_year(request)
stats = template_statistics_client.get_monthly_template_usage_for_service(

View File

@@ -11,7 +11,7 @@ from app.utils.user import user_has_permissions
@main.route("/services/<uuid:service_id>/history")
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def history(service_id):
events = _get_events(current_service.id, request.args.get("selected"))

View File

@@ -26,7 +26,7 @@ from notifications_utils.template import EmailPreviewTemplate, SMSBodyPreviewTem
@main.route("/services/<uuid:service_id>/jobs")
@user_has_permissions()
@user_has_permissions(allow_org_user=True)
def view_jobs(service_id):
return redirect(
url_for(
@@ -37,7 +37,7 @@ def view_jobs(service_id):
@main.route("/services/<uuid:service_id>/jobs/<uuid:job_id>")
@user_has_permissions()
@user_has_permissions(allow_org_user=True)
def view_job(service_id, job_id):
job = Job.from_id(job_id, service_id=current_service.id)
if job.cancelled:
@@ -83,7 +83,7 @@ def view_job(service_id, job_id):
@main.route("/services/<uuid:service_id>/jobs/<uuid:job_id>.csv")
@user_has_permissions(ServicePermission.VIEW_ACTIVITY)
@user_has_permissions(ServicePermission.VIEW_ACTIVITY, allow_org_user=True)
def view_job_csv(service_id, job_id):
job = Job.from_id(job_id, service_id=service_id)
filter_args = parse_filter_args(request.args)
@@ -111,14 +111,14 @@ def view_job_csv(service_id, job_id):
@main.route("/services/<uuid:service_id>/jobs/<uuid:job_id>", methods=["POST"])
@user_has_permissions(ServicePermission.SEND_MESSAGES)
@user_has_permissions(ServicePermission.SEND_MESSAGES, allow_org_user=True)
def cancel_job(service_id, job_id):
Job.from_id(job_id, service_id=service_id).cancel()
return redirect(url_for("main.service_dashboard", service_id=service_id))
@main.route("/services/<uuid:service_id>/jobs/<uuid:job_id>/status.json")
@user_has_permissions()
@user_has_permissions(allow_org_user=True)
def view_job_status_poll(service_id, job_id):
from app.notify_client.job_api_client import job_api_client
@@ -147,7 +147,7 @@ def view_job_status_poll(service_id, job_id):
@main.route("/services/<uuid:service_id>/jobs/<uuid:job_id>/notifications-table")
@user_has_permissions()
@user_has_permissions(allow_org_user=True)
def view_job_notifications_table(service_id, job_id):
"""Endpoint that returns only the notifications table HTML fragment."""
job = Job.from_id(job_id, service_id=current_service.id)

View File

@@ -44,7 +44,7 @@ def manage_users(service_id):
@main.route(
"/services/<uuid:service_id>/users/invite/<uuid:user_id>", methods=["GET", "POST"]
)
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def invite_user(service_id, user_id=None):
form_class = InviteUserForm
form = form_class(
@@ -116,7 +116,7 @@ def invite_user(service_id, user_id=None):
@main.route("/services/<uuid:service_id>/users/<uuid:user_id>", methods=["GET", "POST"])
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def edit_user_permissions(service_id, user_id):
service_has_email_auth = current_service.has_permission(
ServicePermission.EMAIL_AUTH
@@ -169,7 +169,7 @@ def edit_user_permissions(service_id, user_id):
@main.route("/services/<uuid:service_id>/users/<uuid:user_id>/delete", methods=["POST"])
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def remove_user_from_service(service_id, user_id):
try:
service_api_client.remove_user_from_service(service_id, user_id)
@@ -192,7 +192,7 @@ def remove_user_from_service(service_id, user_id):
"/services/<uuid:service_id>/users/<uuid:user_id>/edit-email",
methods=["GET", "POST"],
)
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def edit_user_email(service_id, user_id):
user = current_service.get_team_member(user_id)
user_email = user.email_address
@@ -225,7 +225,7 @@ def edit_user_email(service_id, user_id):
"/services/<uuid:service_id>/users/<uuid:user_id>/edit-email/confirm",
methods=["GET", "POST"],
)
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def confirm_edit_user_email(service_id, user_id):
user = current_service.get_team_member(user_id)
session_key = "team_member_email_change-{}".format(user_id)
@@ -263,7 +263,7 @@ def confirm_edit_user_email(service_id, user_id):
"/services/<uuid:service_id>/users/<uuid:user_id>/edit-mobile-number",
methods=["GET", "POST"],
)
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def edit_user_mobile_number(service_id, user_id):
user = current_service.get_team_member(user_id)
user_mobile_number = redact_mobile_number(user.mobile_number)
@@ -293,7 +293,7 @@ def edit_user_mobile_number(service_id, user_id):
"/services/<uuid:service_id>/users/<uuid:user_id>/edit-mobile-number/confirm",
methods=["GET", "POST"],
)
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def confirm_edit_user_mobile_number(service_id, user_id):
user = current_service.get_team_member(user_id)
if "team_member_mobile_change" in session:
@@ -331,7 +331,7 @@ def confirm_edit_user_mobile_number(service_id, user_id):
"/services/<uuid:service_id>/cancel-invited-user/<uuid:invited_user_id>",
methods=["GET"],
)
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def cancel_invited_user(service_id, invited_user_id):
current_service.cancel_invite(invited_user_id)
@@ -350,7 +350,7 @@ def cancel_invited_user(service_id, invited_user_id):
"/services/<uuid:service_id>/resend-invite/<uuid:invited_user_id>",
methods=["GET"],
)
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def resend_invite(service_id, invited_user_id):
current_service.resend_invite(invited_user_id)

View File

@@ -35,7 +35,7 @@ from notifications_utils.s3 import S3ObjectNotFound
@main.route("/services/<uuid:service_id>/notification/<uuid:notification_id>")
@user_has_permissions(ServicePermission.VIEW_ACTIVITY, ServicePermission.SEND_MESSAGES)
@user_has_permissions(ServicePermission.VIEW_ACTIVITY, ServicePermission.SEND_MESSAGES, allow_org_user=True)
def view_notification(service_id, notification_id, error_message=None):
if error_message:
flash(error_message)
@@ -108,7 +108,7 @@ def view_notification(service_id, notification_id, error_message=None):
@main.route("/services/<uuid:service_id>/notification/<uuid:notification_id>.json")
@user_has_permissions(ServicePermission.VIEW_ACTIVITY, ServicePermission.SEND_MESSAGES)
@user_has_permissions(ServicePermission.VIEW_ACTIVITY, ServicePermission.SEND_MESSAGES, allow_org_user=True)
def view_notification_updates(service_id, notification_id):
return jsonify(
**get_single_notification_partials(
@@ -149,7 +149,7 @@ PERIOD_TO_S3_FILENAME = {
@main.route("/services/<uuid:service_id>/download-notifications.csv")
@user_has_permissions(ServicePermission.VIEW_ACTIVITY)
@user_has_permissions(ServicePermission.VIEW_ACTIVITY, allow_org_user=True)
def download_notifications_csv(service_id):
set_timezone()
filter_args = parse_filter_args(request.args)

View File

@@ -87,18 +87,14 @@ def add_organization():
return render_template("views/organizations/add-organization.html", form=form)
def get_organization_message_allowance(org_id):
def get_organization_messages_sent(org_id):
try:
message_usage = organizations_client.get_organization_message_usage(org_id)
except Exception as e:
current_app.logger.error(f"Error fetching organization message usage: {e}")
message_usage = {}
return {
"messages_sent": message_usage.get("messages_sent", 0),
"messages_remaining": message_usage.get("messages_remaining", 0),
"total_message_limit": message_usage.get("total_message_limit", 0),
}
return message_usage.get("messages_sent", 0)
def _handle_create_service(org_id):
@@ -210,9 +206,7 @@ def _handle_delete_service(org_id, service_id):
cached_service_user_ids = [user.id for user in service.active_users]
service_api_client.archive_service(service_id, cached_service_user_ids)
create_archive_service_event(
service_id=service_id, archived_by_id=current_user.id
)
create_archive_service_event(service_id=service_id, archived_by_id=current_user.id)
cache.redis_client.delete("organizations")
@@ -247,10 +241,10 @@ def get_services_dashboard_data(organization, year):
if sms_sent > 0 or sms_remainder > 0:
if sms_cost > 0:
usage_parts.append(
f"{sms_sent:,} sms ({sms_remainder:,} remaining, ${sms_cost:,.2f})"
f"{sms_sent:,} sms ({sms_remainder:,} message parts remaining, ${sms_cost:,.2f})"
)
else:
usage_parts.append(f"{sms_sent:,} sms ({sms_remainder:,} remaining)")
usage_parts.append(f"{sms_sent:,} sms ({sms_remainder:,} message parts remaining)")
service["usage"] = ", ".join(usage_parts) if usage_parts else "No usage"
@@ -292,7 +286,7 @@ def organization_dashboard(org_id):
elif action == "delete-service" and service_id:
return _handle_delete_service(org_id, service_id)
message_allowance = get_organization_message_allowance(org_id)
messages_sent = get_organization_messages_sent(org_id)
return render_template(
"views/organizations/organization/index.html",
@@ -307,7 +301,7 @@ def organization_dashboard(org_id):
edit_service_data=edit_service_data,
new_service_id=session.pop("new_service_id", None),
updated_service_id=session.pop("updated_service_id", None),
**message_allowance,
messages_sent=messages_sent,
)

View File

@@ -95,7 +95,7 @@ def get_example_csv_rows(template, use_example_as_example=True, submitted_fields
@main.route(
"/services/<uuid:service_id>/send/<uuid:template_id>/csv", methods=["GET", "POST"]
)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True, allow_org_user=True)
def send_messages(service_id, template_id):
notification_count = service_api_client.get_notification_count(service_id)
remaining_messages = current_service.message_limit - notification_count
@@ -189,7 +189,7 @@ def send_messages(service_id, template_id):
@main.route("/services/<uuid:service_id>/send/<uuid:template_id>.csv", methods=["GET"])
@user_has_permissions(
ServicePermission.SEND_MESSAGES, ServicePermission.MANAGE_TEMPLATES
ServicePermission.SEND_MESSAGES, ServicePermission.MANAGE_TEMPLATES, allow_org_user=True
)
def get_example_csv(service_id, template_id):
template = get_template(
@@ -215,7 +215,7 @@ def get_example_csv(service_id, template_id):
"/services/<uuid:service_id>/send/<uuid:template_id>/set-sender",
methods=["GET", "POST"],
)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True, allow_org_user=True)
def set_sender(service_id, template_id):
session["sender_id"] = None
redirect_to_one_off = redirect(
@@ -344,7 +344,7 @@ def get_sender_details(service_id, template_type):
@main.route("/services/<uuid:service_id>/send/<uuid:template_id>/one-off")
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True, allow_org_user=True)
def send_one_off(service_id, template_id):
session["recipient"] = None
session["placeholders"] = {}
@@ -388,7 +388,7 @@ def get_notification_check_endpoint(service_id, template):
"/services/<uuid:service_id>/send/<uuid:template_id>/one-off/step-<int:step_index>",
methods=["GET", "POST"],
)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True, allow_org_user=True)
def send_one_off_step(service_id, template_id, step_index):
if {"recipient", "placeholders"} - set(session.keys()):
return redirect(
@@ -650,7 +650,7 @@ def _check_messages(service_id, template_id, upload_id, preview_row, **kwargs):
"/services/<uuid:service_id>/<uuid:template_id>/check/<uuid:upload_id>/row-<int:row_index>",
methods=["GET"],
)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True, allow_org_user=True)
def check_messages(service_id, template_id, upload_id, row_index=2):
data = _check_messages(service_id, template_id, upload_id, row_index)
data["allowed_file_extensions"] = Spreadsheet.ALLOWED_FILE_EXTENSIONS
@@ -694,7 +694,7 @@ def check_messages(service_id, template_id, upload_id, row_index=2):
"/services/<uuid:service_id>/<uuid:template_id>/check/<uuid:upload_id>/preview/row-<int:row_index>",
methods=["POST"],
)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True, allow_org_user=True)
def preview_job(service_id, template_id, upload_id, row_index=2):
session["scheduled_for"] = request.form.get("scheduled_for", "")
data = _check_messages(
@@ -709,7 +709,7 @@ def preview_job(service_id, template_id, upload_id, row_index=2):
@main.route("/services/<uuid:service_id>/start-job/<uuid:upload_id>", methods=["POST"])
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True, allow_org_user=True)
def start_job(service_id, upload_id):
scheduled_for = session.pop("scheduled_for", None)
job_api_client.create_job(
@@ -862,7 +862,7 @@ def get_skip_link(step_index, template):
"/services/<uuid:service_id>/template/<uuid:template_id>/one-off/send-to-myself",
methods=["GET"],
)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True, allow_org_user=True)
def send_one_off_to_myself(service_id, template_id):
current_app.logger.info("Send one off to myself")
try:
@@ -899,7 +899,7 @@ def send_one_off_to_myself(service_id, template_id):
"/services/<uuid:service_id>/template/<uuid:template_id>/notification/check",
methods=["GET"],
)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True, allow_org_user=True)
def check_notification(service_id, template_id):
return render_template(
"views/notifications/check.html",
@@ -981,7 +981,7 @@ def get_template_error_dict(exception):
"/services/<uuid:service_id>/template/<uuid:template_id>/notification/check/preview",
methods=["POST"],
)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True, allow_org_user=True)
def preview_notification(service_id, template_id):
recipient = get_recipient()
if not recipient:
@@ -1013,7 +1013,7 @@ def preview_notification(service_id, template_id):
"/services/<uuid:service_id>/template/<uuid:template_id>/notification/check",
methods=["POST"],
)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True, allow_org_user=True)
def send_notification(service_id, template_id):
recipient = get_recipient()

View File

@@ -70,7 +70,7 @@ PLATFORM_ADMIN_SERVICE_PERMISSIONS = OrderedDict(
@main.route("/services/<uuid:service_id>/service-settings")
@user_has_permissions(ServicePermission.MANAGE_SERVICE, "manage_api_keys")
@user_has_permissions(ServicePermission.MANAGE_SERVICE, "manage_api_keys", allow_org_user=True)
def service_settings(service_id):
return render_template(
"views/service-settings.html",
@@ -81,7 +81,7 @@ def service_settings(service_id):
@main.route(
"/services/<uuid:service_id>/service-settings/name", methods=["GET", "POST"]
)
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def service_name_change(service_id):
form = RenameServiceForm(name=current_service.name)
@@ -195,7 +195,7 @@ def service_set_permission(service_id, permission):
@main.route(
"/services/<uuid:service_id>/service-settings/archive", methods=["GET", "POST"]
)
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def archive_service(service_id):
if not current_service.active or not (
current_service.trial_mode or current_user.platform_admin
@@ -268,7 +268,7 @@ def resume_service(service_id):
"/services/<uuid:service_id>/service-settings/send-files-by-email",
methods=["GET", "POST"],
)
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def send_files_by_email_contact_details(service_id):
form = ServiceContactDetailsForm()
contact_details = None
@@ -298,7 +298,7 @@ def send_files_by_email_contact_details(service_id):
@main.route(
"/services/<uuid:service_id>/service-settings/set-reply-to-email", methods=["GET"]
)
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def service_set_reply_to_email(service_id):
return redirect(url_for(".service_email_reply_to", service_id=service_id))
@@ -306,7 +306,7 @@ def service_set_reply_to_email(service_id):
@main.route(
"/services/<uuid:service_id>/service-settings/email-reply-to", methods=["GET"]
)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, "manage_api_keys")
@user_has_permissions(ServicePermission.MANAGE_SERVICE, "manage_api_keys", allow_org_user=True)
def service_email_reply_to(service_id):
return render_template("views/service-settings/email_reply_to.html")
@@ -315,7 +315,7 @@ def service_email_reply_to(service_id):
"/services/<uuid:service_id>/service-settings/email-reply-to/add",
methods=["GET", "POST"],
)
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def service_add_email_reply_to(service_id):
form = ServiceReplyToEmailForm()
first_email_address = current_service.count_email_reply_to_addresses == 0
@@ -359,7 +359,7 @@ def service_add_email_reply_to(service_id):
"/services/<uuid:service_id>/service-settings/email-reply-to/<uuid:notification_id>/verify",
methods=["GET", "POST"],
)
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def service_verify_reply_to_address(service_id, notification_id):
replace = request.args.get("replace", False)
is_default = request.args.get("is_default", False)
@@ -379,7 +379,7 @@ def service_verify_reply_to_address(service_id, notification_id):
@main.route(
"/services/<uuid:service_id>/service-settings/email-reply-to/<uuid:notification_id>/verify.json"
)
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def service_verify_reply_to_address_updates(service_id, notification_id):
return jsonify(
**get_service_verify_reply_to_address_partials(service_id, notification_id)
@@ -456,7 +456,7 @@ def get_service_verify_reply_to_address_partials(service_id, notification_id):
methods=["GET"],
endpoint="service_confirm_delete_email_reply_to",
)
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def service_edit_email_reply_to(service_id, reply_to_email_id):
form = ServiceReplyToEmailForm()
reply_to_email_address = current_service.get_email_reply_to_address(
@@ -525,7 +525,7 @@ def service_edit_email_reply_to(service_id, reply_to_email_id):
"/services/<uuid:service_id>/service-settings/email-reply-to/<uuid:reply_to_email_id>/delete",
methods=["POST"],
)
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def service_delete_email_reply_to(service_id, reply_to_email_id):
service_api_client.delete_reply_to_email_address(
service_id=current_service.id,
@@ -538,7 +538,7 @@ def service_delete_email_reply_to(service_id, reply_to_email_id):
"/services/<uuid:service_id>/service-settings/set-inbound-number",
methods=["GET", "POST"],
)
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def service_set_inbound_number(service_id):
available_inbound_numbers = (
inbound_number_client.get_available_inbound_sms_numbers()
@@ -571,7 +571,7 @@ def service_set_inbound_number(service_id):
@main.route(
"/services/<uuid:service_id>/service-settings/sms-prefix", methods=["GET", "POST"]
)
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def service_set_sms_prefix(service_id):
form = SMSPrefixForm(enabled=current_service.prefix_sms)
@@ -590,7 +590,7 @@ def service_set_sms_prefix(service_id):
"/services/<uuid:service_id>/service-settings/set-international-sms",
methods=["GET", "POST"],
)
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def service_set_international_sms(service_id):
form = ServiceOnOffSettingForm(
"Send text messages to international phone numbers",
@@ -611,7 +611,7 @@ def service_set_international_sms(service_id):
@main.route(
"/services/<uuid:service_id>/service-settings/set-inbound-sms", methods=["GET"]
)
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def service_set_inbound_sms(service_id):
return render_template(
"views/service-settings/set-inbound-sms.html",
@@ -622,7 +622,7 @@ def service_set_inbound_sms(service_id):
"/services/<uuid:service_id>/service-settings/set-<channel>",
methods=["GET", "POST"],
)
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def service_set_channel(service_id, channel):
if channel not in {"email", "sms"}:
abort(404)
@@ -647,7 +647,7 @@ def service_set_channel(service_id, channel):
@main.route(
"/services/<uuid:service_id>/service-settings/set-auth-type", methods=["GET"]
)
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def service_set_auth_type(service_id):
return render_template(
"views/service-settings/set-auth-type.html",
@@ -655,7 +655,7 @@ def service_set_auth_type(service_id):
@main.route("/services/<uuid:service_id>/service-settings/sms-sender", methods=["GET"])
@user_has_permissions(ServicePermission.MANAGE_SERVICE, "manage_api_keys")
@user_has_permissions(ServicePermission.MANAGE_SERVICE, "manage_api_keys", allow_org_user=True)
def service_sms_senders(service_id):
return render_template(
"views/service-settings/sms-senders.html",
@@ -666,7 +666,7 @@ def service_sms_senders(service_id):
"/services/<uuid:service_id>/service-settings/sms-sender/add",
methods=["GET", "POST"],
)
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def service_add_sms_sender(service_id):
form = ServiceSmsSenderForm()
first_sms_sender = current_service.count_sms_senders == 0
@@ -694,7 +694,7 @@ def service_add_sms_sender(service_id):
methods=["GET"],
endpoint="service_confirm_delete_sms_sender",
)
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def service_edit_sms_sender(service_id, sms_sender_id):
sms_sender = current_service.get_sms_sender(sms_sender_id)
is_inbound_number = sms_sender["inbound_number_id"]
@@ -732,7 +732,7 @@ def service_edit_sms_sender(service_id, sms_sender_id):
"/services/<uuid:service_id>/service-settings/sms-sender/<uuid:sms_sender_id>/delete",
methods=["POST"],
)
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def service_delete_sms_sender(service_id, sms_sender_id):
service_api_client.delete_sms_sender(
service_id=current_service.id,

View File

@@ -40,7 +40,7 @@ form_objects = {
@main.route("/services/<uuid:service_id>/templates/<uuid:template_id>")
@user_has_permissions()
@user_has_permissions(allow_org_user=True)
def view_template(service_id, template_id):
template = current_service.get_template(template_id)
template_folder = current_service.get_template_folder(template["folder"])
@@ -82,7 +82,7 @@ def view_template(service_id, template_id):
"/services/<uuid:service_id>/templates/<template_type:template_type>/folders/<uuid:template_folder_id>",
methods=["GET", "POST"],
)
@user_has_permissions()
@user_has_permissions(allow_org_user=True)
def choose_template(service_id, template_type="all", template_folder_id=None):
template_folder = current_service.get_template_folder(template_folder_id)
@@ -244,7 +244,7 @@ def _view_template_version(service_id, template_id, version):
@main.route(
"/services/<uuid:service_id>/templates/<uuid:template_id>/version/<int:version>"
)
@user_has_permissions()
@user_has_permissions(allow_org_user=True)
def view_template_version(service_id, template_id, version):
return render_template(
"views/templates/template_history.html",
@@ -291,7 +291,7 @@ def _add_template_by_type(template_type, template_folder_id):
@main.route(
"/services/<uuid:service_id>/templates/copy/from-service/<uuid:from_service>/from-folder/<uuid:from_folder>"
)
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES)
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES, allow_org_user=True)
def choose_template_to_copy(
service_id,
from_service=None,
@@ -323,7 +323,7 @@ def choose_template_to_copy(
"/services/<uuid:service_id>/templates/copy/<uuid:template_id>",
methods=["GET", "POST"],
)
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES)
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES, allow_org_user=True)
def copy_template(service_id, template_id):
from_service = request.args.get("from_service")
@@ -380,7 +380,7 @@ def _get_template_copy_name(template, existing_templates):
"<template_type:notification_type>/<return_to>/<uuid:template_id>"
)
)
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES)
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES, allow_org_user=True)
def action_blocked(service_id, notification_type, return_to, template_id=None):
back_link = {
"add_new_template": partial(
@@ -412,7 +412,7 @@ def action_blocked(service_id, notification_type, return_to, template_id=None):
"/services/<uuid:service_id>/templates/folders/<uuid:template_folder_id>/manage",
methods=["GET", "POST"],
)
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES)
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES, allow_org_user=True)
def manage_template_folder(service_id, template_folder_id):
template_folder = current_service.get_template_folder_with_user_permission_or_403(
template_folder_id, current_user
@@ -462,7 +462,7 @@ def manage_template_folder(service_id, template_folder_id):
"/services/<uuid:service_id>/templates/folders/<uuid:template_folder_id>/delete",
methods=["GET", "POST"],
)
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES)
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES, allow_org_user=True)
def delete_template_folder(service_id, template_folder_id):
template_folder = current_service.get_template_folder_with_user_permission_or_403(
template_folder_id, current_user
@@ -527,7 +527,7 @@ def delete_template_folder(service_id, template_folder_id):
"/services/<uuid:service_id>/templates/folders/<uuid:template_folder_id>/add-<template_type:template_type>",
methods=["GET", "POST"],
)
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES)
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES, allow_org_user=True)
def add_service_template(service_id, template_type, template_folder_id=None):
if template_type not in current_service.available_template_types:
return redirect(
@@ -591,7 +591,7 @@ def abort_403_if_not_admin_user():
"/services/<uuid:service_id>/templates/<uuid:template_id>/edit",
methods=["GET", "POST"],
)
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES)
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES, allow_org_user=True)
def edit_service_template(service_id, template_id):
template = current_service.get_template_with_user_permission_or_403(
template_id, current_user
@@ -679,7 +679,7 @@ def edit_service_template(service_id, template_id):
"/services/<uuid:service_id>/templates/count-<template_type:template_type>-length",
methods=["POST"],
)
@user_has_permissions()
@user_has_permissions(allow_org_user=True)
def count_content_length(service_id, template_type):
if template_type not in {"sms"}:
abort(404)
@@ -749,7 +749,7 @@ def _get_content_count_error_and_message_for_template(template):
"/services/<uuid:service_id>/templates/<uuid:template_id>/delete",
methods=["GET", "POST"],
)
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES)
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES, allow_org_user=True)
def delete_service_template(service_id, template_id):
template = current_service.get_template_with_user_permission_or_403(
template_id, current_user
@@ -807,7 +807,7 @@ def delete_service_template(service_id, template_id):
@main.route(
"/services/<uuid:service_id>/templates/<uuid:template_id>/redact", methods=["GET"]
)
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES)
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES, allow_org_user=True)
def confirm_redact_template(service_id, template_id):
template = current_service.get_template_with_user_permission_or_403(
template_id, current_user
@@ -828,7 +828,7 @@ def confirm_redact_template(service_id, template_id):
@main.route(
"/services/<uuid:service_id>/templates/<uuid:template_id>/redact", methods=["POST"]
)
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES)
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES, allow_org_user=True)
def redact_template(service_id, template_id):
service_api_client.redact_service_template(service_id, template_id)
@@ -847,7 +847,7 @@ def redact_template(service_id, template_id):
@main.route("/services/<uuid:service_id>/templates/<uuid:template_id>/versions")
@user_has_permissions(ServicePermission.VIEW_ACTIVITY)
@user_has_permissions(ServicePermission.VIEW_ACTIVITY, allow_org_user=True)
def view_template_versions(service_id, template_id):
return render_template(
"views/templates/choose_history.html",
@@ -867,7 +867,7 @@ def view_template_versions(service_id, template_id):
"/services/<uuid:service_id>/templates/<uuid:template_id>/set-template-sender",
methods=["GET", "POST"],
)
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES)
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES, allow_org_user=True)
def set_template_sender(service_id, template_id):
template = current_service.get_template_with_user_permission_or_403(
template_id, current_user

View File

@@ -15,7 +15,7 @@ from app.utils.user import user_has_permissions
@main.route("/services/<uuid:service_id>/tour/<uuid:template_id>")
@user_has_permissions(ServicePermission.SEND_MESSAGES)
@user_has_permissions(ServicePermission.SEND_MESSAGES, allow_org_user=True)
def begin_tour(service_id, template_id):
db_template = current_service.get_template_with_user_permission_or_403(
template_id, current_user
@@ -48,7 +48,7 @@ def begin_tour(service_id, template_id):
"/services/<uuid:service_id>/tour/<uuid:template_id>/step-<int:step_index>",
methods=["GET", "POST"],
)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True, allow_org_user=True)
def tour_step(service_id, template_id, step_index):
db_template = current_service.get_template_with_user_permission_or_403(
template_id, current_user
@@ -167,7 +167,7 @@ def _get_tour_step_back_link(service_id, template_id, step_index):
@main.route(
"/services/<uuid:service_id>/tour/<uuid:template_id>/check", methods=["GET"]
)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True, allow_org_user=True)
def check_tour_notification(service_id, template_id):
db_template = current_service.get_template_with_user_permission_or_403(
template_id, current_user
@@ -224,7 +224,7 @@ def check_tour_notification(service_id, template_id):
@main.route("/services/<uuid:service_id>/end-tour/<uuid:example_template_id>")
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES)
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES, allow_org_user=True)
def go_to_dashboard_after_tour(service_id, example_template_id):
service_api_client.delete_service_template(service_id, example_template_id)

View File

@@ -11,7 +11,7 @@ MAX_FILE_UPLOAD_SIZE = 2 * 1024 * 1024 # 2MB
@main.route("/services/<uuid:service_id>/uploads")
@user_has_permissions()
@user_has_permissions(allow_org_user=True)
def uploads(service_id):
# No tests have been written, this has been quickly prepared for user research.
# It's also very like that a new view will be created to show uploads.

View File

@@ -7,12 +7,12 @@
<ul class="usa-sidenav">
{% if current_user.has_permissions() %}
{% if current_user.has_permissions(ServicePermission.VIEW_ACTIVITY) %}
<li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('dashboard') }}" href="{{ url_for('.service_dashboard', service_id=current_service.id) }}">Dashboard</a></li>
<li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('activity') }}" href="{{ url_for('.all_jobs_activity', service_id=current_service.id) }}">Activity</a></li>
<li class="usa-sidenav__item"><a class="usa-link {{ main_navigation.is_selected('dashboard') }}" href="{{ url_for('.service_dashboard', service_id=current_service.id) }}">Dashboard</a></li>
<li class="usa-sidenav__item"><a class="usa-link {{ main_navigation.is_selected('activity') }}" href="{{ url_for('.all_jobs_activity', service_id=current_service.id) }}">Activity</a></li>
{% endif %}
{% elif current_user.has_permissions(allow_org_user=True) %}
<li class="usa-sidenav__item"><a class="usa-link{{ main_navigation.is_selected('usage') }}" href="{{ url_for('.usage', service_id=current_service.id) }}">Usage</a></li>
<li class="usa-sidenav__item"><a class="usa-link{{ main_navigation.is_selected('team-members') }}" href="{{ url_for('.manage_users', service_id=current_service.id) }}">Team members</a></li>
<li class="usa-sidenav__item"><a class="usa-link {{ main_navigation.is_selected('usage') }}" href="{{ url_for('.usage', service_id=current_service.id) }}">Usage</a></li>
<li class="usa-sidenav__item"><a class="usa-link {{ main_navigation.is_selected('team-members') }}" href="{{ url_for('.manage_users', service_id=current_service.id) }}">Team members</a></li>
{% endif %}
</ul>
</nav>

View File

@@ -1,14 +1,14 @@
<nav id="nav-org-nav" aria-label="Organization navigation" class="nav margin-bottom-4">
<ul class="usa-sidenav">
{% if config.ORGANIZATION_DASHBOARD_ENABLED %}
<li class="usa-sidenav__item"><a class="usa-link{{ org_navigation.is_selected('dashboard') }}" href="{{ url_for('.organization_dashboard', org_id=current_org.id) }}">Dashboard</a></li>
<li class="usa-sidenav__item"><a class="usa-link {{ org_navigation.is_selected('dashboard') }}" href="{{ url_for('.organization_dashboard', org_id=current_org.id) }}">Dashboard</a></li>
{% endif %}
<li class="usa-sidenav__item"><a class="usa-link{{ org_navigation.is_selected('usage') }}" href="{{ url_for('.organization_usage', org_id=current_org.id) }}">Usage</a></li>
<li class="usa-sidenav__item"><a class="usa-link{{ org_navigation.is_selected('team-members') }}" href="{{ url_for('.manage_org_users', org_id=current_org.id) }}">Team members</a></li>
<li class="usa-sidenav__item"><a class="usa-link {{ org_navigation.is_selected('usage') }}" href="{{ url_for('.organization_usage', org_id=current_org.id) }}">Usage</a></li>
<li class="usa-sidenav__item"><a class="usa-link {{ org_navigation.is_selected('team-members') }}" href="{{ url_for('.manage_org_users', org_id=current_org.id) }}">Org admins</a></li>
{% if current_user.platform_admin %}
<li class="usa-sidenav__item"><a class="usa-link{{ org_navigation.is_selected('settings') }}" href="{{ url_for('.organization_settings', org_id=current_org.id) }}">Settings</a></li>
<li class="usa-sidenav__item"><a class="usa-link{{ org_navigation.is_selected('trial-services') }}" href="{{ url_for('.organization_trial_mode_services', org_id=current_org.id) }}">Trial mode services</a></li>
<li class="usa-sidenav__item"><a class="usa-link{{ org_navigation.is_selected('billing') }}" href="{{ url_for('.organization_billing', org_id=current_org.id) }}">Billing</a></li>
<li class="usa-sidenav__item"><a class="usa-link {{ org_navigation.is_selected('settings') }}" href="{{ url_for('.organization_settings', org_id=current_org.id) }}">Settings</a></li>
<li class="usa-sidenav__item"><a class="usa-link {{ org_navigation.is_selected('trial-services') }}" href="{{ url_for('.organization_trial_mode_services', org_id=current_org.id) }}">Trial mode services</a></li>
<li class="usa-sidenav__item"><a class="usa-link {{ org_navigation.is_selected('billing') }}" href="{{ url_for('.organization_billing', org_id=current_org.id) }}">Billing</a></li>
{% endif %}
</ul>
</nav>

View File

@@ -3,14 +3,12 @@
{% else %}
<nav class="nav">
<ul class="usa-sidenav">
{# {% if current_user.has_permissions() %} #}
<li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('settings') }}"
<li class="usa-sidenav__item"><a class="usa-link {{ main_navigation.is_selected('settings') }}"
href="{{ url_for('main.service_settings', service_id=current_service.id) }}">General</a></li>
<li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('user-profile') }}"
<li class="usa-sidenav__item"><a class="usa-link {{ main_navigation.is_selected('user-profile') }}"
href="{{ url_for('main.user_profile', service_id=current_service.id) }}">User profile</a></li>
<li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('team-members') }}"
<li class="usa-sidenav__item"><a class="usa-link {{ main_navigation.is_selected('team-members') }}"
href="{{ url_for('main.manage_users', service_id=current_service.id) }}">Team members</a></li>
{# {% endif %} #}
</ul>
</nav>
{% endif %}

View File

@@ -4,7 +4,7 @@
itemscope
itemtype="http://schema.org/ListItem"
>
<a class="{% if has_subnav %}parent-link {% endif %}{% if item['link'] == request.endpoint %} usa-current {% endif %}" href="{{ url_for(item['link']) }}" itemprop="item">
<a class="usa-link {% if has_subnav %}parent-link {% endif %}{% if item['link'] == request.endpoint %}usa-current {% endif %}" href="{{ url_for(item['link']) }}" itemprop="item">
<span itemprop="name">{{item['name']}}</span>
</a>
{% if caller %}

View File

@@ -9,23 +9,21 @@
{{ page_header('Organization Dashboard', size='large') }}
<div id="totalMessageChartContainer" data-messages-sent="{{ messages_sent|default(0) }}" data-messages-remaining="{{ messages_remaining|default(0) }}" data-total-message-limit="{{ total_message_limit|default(0) }}">
<div class="margin-bottom-3">
<div class="grid-row flex-align-center">
<h2 id="chartTitle" class="margin-right-1 margin-y-0">Overall {{ selected_year }} Total Message Allowance</h2>
<h2 id="chartTitle" class="margin-right-1 margin-y-0">Overall {{ selected_year }} Messages Sent</h2>
<button
type="button"
class="usa-tooltip usa-tooltip__information margin-right-0"
data-position="top"
title="Combined totals across all services in {{ current_org.name }}: pending, failed, or delivered"
title="Total messages sent across all services in {{ current_org.name }}"
>
<span class="usa-sr-only">More information</span>
<img src="{{ asset_url('img/usa-icons/info.svg') }}" alt="" class="usa-icon" role="presentation" />
</button>
</div>
<svg id="totalMessageChart"></svg>
<div id="message"></div>
<div class="font-sans-xl text-primary-darker margin-top-1">{{ "{:,}".format(messages_sent|default(0)) }} sent</div>
</div>
<div id="totalMessageTable" class="margin-bottom-3"></div>
<div class="grid-row margin-bottom-3 maxw-tablet">
<div class="grid-col-12 tablet:grid-col-6 margin-bottom-2 tablet:margin-bottom-0 tablet:padding-right-1">
<div class="usa-summary-box height-full" role="region">
@@ -39,7 +37,7 @@
<div class="text-ink font-body-2xs line-height-sans-3">
<span class="text-bold">{{ live_services }}</span> Live
<span class="margin-left-1 text-bold">{{ trial_services }}</span> Trial
<span class="margin-left-1 text-bold">{{ suspended_services }}</span> Suspended
<span class="margin-left-1 text-bold">{{ suspended_services }}</span> Archived
</div>
</div>
</div>
@@ -70,7 +68,7 @@
</div>
{% if create_service_form %}
<div id="create-service-form" class="padding-3 radius-md margin-bottom-3">
<div id="create-service-form" class="padding-3 radius-md margin-bottom-3 bg-gray-5">
<h3 class="margin-top-0">Create a new service</h3>
<form method="post" action="{{ url_for('.organization_dashboard', org_id=current_org.id, action='create-service') }}">
<input type="hidden" name="csrf_token" value="{{ create_service_form.csrf_token._value() }}"/>
@@ -142,6 +140,9 @@
{% endif %}
</div>
</form>
<div class="border-top-1px border-base-lighter margin-top-3 padding-top-3">
<a href="{{ url_for('main.invite_user', service_id=edit_service_data.id) }}" class="usa-button usa-button--outline">Invite team member</a>
</div>
</div>
<div
@@ -238,7 +239,7 @@
</h3>
<div id="what-is-service-content" class="usa-accordion__content usa-prose" hidden>
<p>
When you join Notify, you're added to a service. This is your organization's workspace for sending text messages and emails. Within your service, you can:
Services are your organization's workspaces for sending text messages and emails. If you have multiple teams using Notify for different purposes, it is helpful to set up a separate Service for each team, with its own managers and templates. There is no limit to the number of Services you can create, and messages sent by all of your Services contribute to your overall messaging usage. Within each Service, you can:
</p>
<ol class="usa-list">
<li>Create and edit message templates</li>
@@ -246,7 +247,8 @@
<li>View message status and history</li>
<li>Manage team members and permissions</li>
<li>Track usage and delivery statistics</li>
<li>If you work for multiple organizations, you may belong to multiple services and can switch between them.</li>
<li>Service members can be added to multiple services and can switch between them</li>
<li>Organizational administrators can always see all Services</li>
</ol>
</div>
</div>
@@ -275,7 +277,7 @@
<td><a href="{{ url_for('main.service_dashboard', service_id=service.id) }}" class="usa-link">{{ service.name }}</a></td>
<td>
{% if not service.active %}
Suspended
Archived
{% elif service.restricted %}
Trial
{% else %}

View File

@@ -5,7 +5,7 @@
{% from "components/components/button/macro.njk" import usaButton %}
{% block org_page_title %}
Team members
Organization administrators
{% endblock %}
{% block maincolumn_content %}
@@ -20,7 +20,7 @@
{% endif %}
<h1 class="font-body-2xl">
Team members
Organization administrators
</h1>
{% if show_search_box %}
@@ -66,7 +66,7 @@
<div class="js-stick-at-bottom-when-scrolling">
{{ usaButton({
"element": "a",
"text": "Invite team member",
"text": "Invite an organization administrator",
"href": url_for('.invite_org_user', org_id=current_org.id),
"classes": "usa-button"
}) }}

View File

@@ -15,7 +15,7 @@
{% block maincolumn_content %}
{{ page_header("Invite a team member") }}
{{ page_header("Invite an organization administrator") }}
{% call form_wrapper() %}
{{ form.email_address(
param_extensions={
@@ -26,11 +26,11 @@
<div class="margin-bottom-3">
<p class="form-label">
{{ current_org.name }} team members can:
{{ current_org.name }} organization administrators can:
</p>
<ul class="list list-bullet">
<li>see usage and team members for each service</li>
<li>invite other team members</li>
<li>access and manage all services</li>
<li>invite or remove other organization administrators</li>
</ul>
</div>

5277
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -27,7 +27,8 @@
"license": "CC0",
"homepage": "https://github.com/GSA/notifications-admin#readme",
"overrides": {
"graceful-fs": "^4.2.11"
"graceful-fs": "^4.2.11",
"glob@^10": "10.5.0"
},
"dependencies": {
"@rollup/plugin-commonjs": "^29.0.0",
@@ -37,7 +38,7 @@
"cbor-js": "0.1.0",
"ci-info": "^4.3.1",
"d3": "^7.9.0",
"playwright": "^1.56.1",
"playwright": "^1.57.0",
"python": "^0.0.4",
"query-command-supported": "1.0.0",
"sass-embedded": "^1.93.3",
@@ -60,10 +61,10 @@
"merge-stream": "^2.0.0",
"mini-css-extract-plugin": "^2.9.4",
"postcss-loader": "^8.2.0",
"sass": "^1.94.0",
"sass": "^1.94.2",
"sass-loader": "^16.0.6",
"terser-webpack-plugin": "^5.3.14",
"webpack": "^5.102.1",
"terser-webpack-plugin": "^5.3.15",
"webpack": "^5.103.0",
"webpack-cli": "^6.0.1"
}
}

558
poetry.lock generated
View File

@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand.
# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand.
[[package]]
name = "ago"
@@ -97,14 +97,14 @@ files = [
[[package]]
name = "bandit"
version = "1.8.6"
version = "1.9.2"
description = "Security oriented static analyser for python code."
optional = false
python-versions = ">=3.9"
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "bandit-1.8.6-py3-none-any.whl", hash = "sha256:3348e934d736fcdb68b6aa4030487097e23a501adf3e7827b63658df464dddd0"},
{file = "bandit-1.8.6.tar.gz", hash = "sha256:dbfe9c25fc6961c2078593de55fd19f2559f9e45b99f1272341f5b95dea4e56b"},
{file = "bandit-1.9.2-py3-none-any.whl", hash = "sha256:bda8d68610fc33a6e10b7a8f1d61d92c8f6c004051d5e946406be1fb1b16a868"},
{file = "bandit-1.9.2.tar.gz", hash = "sha256:32410415cd93bf9c8b91972159d5cf1e7f063a9146d70345641cd3877de348ce"},
]
[package.dependencies]
@@ -122,18 +122,18 @@ yaml = ["PyYAML"]
[[package]]
name = "beautifulsoup4"
version = "4.14.2"
version = "4.14.3"
description = "Screen-scraping library"
optional = false
python-versions = ">=3.7.0"
groups = ["main"]
files = [
{file = "beautifulsoup4-4.14.2-py3-none-any.whl", hash = "sha256:5ef6fa3a8cbece8488d66985560f97ed091e22bbc4e9c2338508a9d5de6d4515"},
{file = "beautifulsoup4-4.14.2.tar.gz", hash = "sha256:2a98ab9f944a11acee9cc848508ec28d9228abfd522ef0fad6a02a72e0ded69e"},
{file = "beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb"},
{file = "beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86"},
]
[package.dependencies]
soupsieve = ">1.2"
soupsieve = ">=1.6.1"
typing-extensions = ">=4.0.0"
[package.extras]
@@ -326,26 +326,26 @@ redis = ["redis (>=2.10.5)"]
[[package]]
name = "cachetools"
version = "6.2.1"
version = "6.2.2"
description = "Extensible memoizing collections and decorators"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "cachetools-6.2.1-py3-none-any.whl", hash = "sha256:09868944b6dde876dfd44e1d47e18484541eaf12f26f29b7af91b26cc892d701"},
{file = "cachetools-6.2.1.tar.gz", hash = "sha256:3f391e4bd8f8bf0931169baf7456cc822705f4e2a31f840d218f445b9a854201"},
{file = "cachetools-6.2.2-py3-none-any.whl", hash = "sha256:6c09c98183bf58560c97b2abfcedcbaf6a896a490f534b031b661d3723b45ace"},
{file = "cachetools-6.2.2.tar.gz", hash = "sha256:8e6d266b25e539df852251cfd6f990b4bc3a141db73b939058d809ebd2590fc6"},
]
[[package]]
name = "certifi"
version = "2025.10.5"
version = "2025.11.12"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.7"
groups = ["main", "dev"]
files = [
{file = "certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de"},
{file = "certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43"},
{file = "certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b"},
{file = "certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316"},
]
[[package]]
@@ -598,14 +598,14 @@ rapidfuzz = ">=3.0.0,<4.0.0"
[[package]]
name = "click"
version = "8.3.0"
version = "8.3.1"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.10"
groups = ["main", "dev"]
files = [
{file = "click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc"},
{file = "click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4"},
{file = "click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"},
{file = "click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a"},
]
[package.dependencies]
@@ -626,104 +626,104 @@ markers = {main = "platform_system == \"Windows\""}
[[package]]
name = "coverage"
version = "7.11.3"
version = "7.12.0"
description = "Code coverage measurement for Python"
optional = false
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "coverage-7.11.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0c986537abca9b064510f3fd104ba33e98d3036608c7f2f5537f869bc10e1ee5"},
{file = "coverage-7.11.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:28c5251b3ab1d23e66f1130ca0c419747edfbcb4690de19467cd616861507af7"},
{file = "coverage-7.11.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4f2bb4ee8dd40f9b2a80bb4adb2aecece9480ba1fa60d9382e8c8e0bd558e2eb"},
{file = "coverage-7.11.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e5f4bfac975a2138215a38bda599ef00162e4143541cf7dd186da10a7f8e69f1"},
{file = "coverage-7.11.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f4cbfff5cf01fa07464439a8510affc9df281535f41a1f5312fbd2b59b4ab5c"},
{file = "coverage-7.11.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:31663572f20bf3406d7ac00d6981c7bbbcec302539d26b5ac596ca499664de31"},
{file = "coverage-7.11.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9799bd6a910961cb666196b8583ed0ee125fa225c6fdee2cbf00232b861f29d2"},
{file = "coverage-7.11.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:097acc18bedf2c6e3144eaf09b5f6034926c3c9bb9e10574ffd0942717232507"},
{file = "coverage-7.11.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:6f033dec603eea88204589175782290a038b436105a8f3637a81c4359df27832"},
{file = "coverage-7.11.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dd9ca2d44ed8018c90efb72f237a2a140325a4c3339971364d758e78b175f58e"},
{file = "coverage-7.11.3-cp310-cp310-win32.whl", hash = "sha256:900580bc99c145e2561ea91a2d207e639171870d8a18756eb57db944a017d4bb"},
{file = "coverage-7.11.3-cp310-cp310-win_amd64.whl", hash = "sha256:c8be5bfcdc7832011b2652db29ed7672ce9d353dd19bce5272ca33dbcf60aaa8"},
{file = "coverage-7.11.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:200bb89fd2a8a07780eafcdff6463104dec459f3c838d980455cfa84f5e5e6e1"},
{file = "coverage-7.11.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8d264402fc179776d43e557e1ca4a7d953020d3ee95f7ec19cc2c9d769277f06"},
{file = "coverage-7.11.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:385977d94fc155f8731c895accdfcc3dd0d9dd9ef90d102969df95d3c637ab80"},
{file = "coverage-7.11.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0542ddf6107adbd2592f29da9f59f5d9cff7947b5bb4f734805085c327dcffaa"},
{file = "coverage-7.11.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d60bf4d7f886989ddf80e121a7f4d140d9eac91f1d2385ce8eb6bda93d563297"},
{file = "coverage-7.11.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0a3b6e32457535df0d41d2d895da46434706dd85dbaf53fbc0d3bd7d914b362"},
{file = "coverage-7.11.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:876a3ee7fd2613eb79602e4cdb39deb6b28c186e76124c3f29e580099ec21a87"},
{file = "coverage-7.11.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a730cd0824e8083989f304e97b3f884189efb48e2151e07f57e9e138ab104200"},
{file = "coverage-7.11.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:b5cd111d3ab7390be0c07ad839235d5ad54d2ca497b5f5db86896098a77180a4"},
{file = "coverage-7.11.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:074e6a5cd38e06671580b4d872c1a67955d4e69639e4b04e87fc03b494c1f060"},
{file = "coverage-7.11.3-cp311-cp311-win32.whl", hash = "sha256:86d27d2dd7c7c5a44710565933c7dc9cd70e65ef97142e260d16d555667deef7"},
{file = "coverage-7.11.3-cp311-cp311-win_amd64.whl", hash = "sha256:ca90ef33a152205fb6f2f0c1f3e55c50df4ef049bb0940ebba666edd4cdebc55"},
{file = "coverage-7.11.3-cp311-cp311-win_arm64.whl", hash = "sha256:56f909a40d68947ef726ce6a34eb38f0ed241ffbe55c5007c64e616663bcbafc"},
{file = "coverage-7.11.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5b771b59ac0dfb7f139f70c85b42717ef400a6790abb6475ebac1ecee8de782f"},
{file = "coverage-7.11.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:603c4414125fc9ae9000f17912dcfd3d3eb677d4e360b85206539240c96ea76e"},
{file = "coverage-7.11.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:77ffb3b7704eb7b9b3298a01fe4509cef70117a52d50bcba29cffc5f53dd326a"},
{file = "coverage-7.11.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4d4ca49f5ba432b0755ebb0fc3a56be944a19a16bb33802264bbc7311622c0d1"},
{file = "coverage-7.11.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:05fd3fb6edff0c98874d752013588836f458261e5eba587afe4c547bba544afd"},
{file = "coverage-7.11.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0e920567f8c3a3ce68ae5a42cf7c2dc4bb6cc389f18bff2235dd8c03fa405de5"},
{file = "coverage-7.11.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4bec8c7160688bd5a34e65c82984b25409563134d63285d8943d0599efbc448e"},
{file = "coverage-7.11.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:adb9b7b42c802bd8cb3927de8c1c26368ce50c8fdaa83a9d8551384d77537044"},
{file = "coverage-7.11.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c8f563b245b4ddb591e99f28e3cd140b85f114b38b7f95b2e42542f0603eb7d7"},
{file = "coverage-7.11.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e2a96fdc7643c9517a317553aca13b5cae9bad9a5f32f4654ce247ae4d321405"},
{file = "coverage-7.11.3-cp312-cp312-win32.whl", hash = "sha256:e8feeb5e8705835f0622af0fe7ff8d5cb388948454647086494d6c41ec142c2e"},
{file = "coverage-7.11.3-cp312-cp312-win_amd64.whl", hash = "sha256:abb903ffe46bd319d99979cdba350ae7016759bb69f47882242f7b93f3356055"},
{file = "coverage-7.11.3-cp312-cp312-win_arm64.whl", hash = "sha256:1451464fd855d9bd000c19b71bb7dafea9ab815741fb0bd9e813d9b671462d6f"},
{file = "coverage-7.11.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84b892e968164b7a0498ddc5746cdf4e985700b902128421bb5cec1080a6ee36"},
{file = "coverage-7.11.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f761dbcf45e9416ec4698e1a7649248005f0064ce3523a47402d1bff4af2779e"},
{file = "coverage-7.11.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1410bac9e98afd9623f53876fae7d8a5db9f5a0ac1c9e7c5188463cb4b3212e2"},
{file = "coverage-7.11.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:004cdcea3457c0ea3233622cd3464c1e32ebba9b41578421097402bee6461b63"},
{file = "coverage-7.11.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f067ada2c333609b52835ca4d4868645d3b63ac04fb2b9a658c55bba7f667d3"},
{file = "coverage-7.11.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:07bc7745c945a6d95676953e86ba7cebb9f11de7773951c387f4c07dc76d03f5"},
{file = "coverage-7.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8bba7e4743e37484ae17d5c3b8eb1ce78b564cb91b7ace2e2182b25f0f764cb5"},
{file = "coverage-7.11.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbffc22d80d86fbe456af9abb17f7a7766e7b2101f7edaacc3535501691563f7"},
{file = "coverage-7.11.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:0dba4da36730e384669e05b765a2c49f39514dd3012fcc0398dd66fba8d746d5"},
{file = "coverage-7.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ae12fe90b00b71a71b69f513773310782ce01d5f58d2ceb2b7c595ab9d222094"},
{file = "coverage-7.11.3-cp313-cp313-win32.whl", hash = "sha256:12d821de7408292530b0d241468b698bce18dd12ecaf45316149f53877885f8c"},
{file = "coverage-7.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:6bb599052a974bb6cedfa114f9778fedfad66854107cf81397ec87cb9b8fbcf2"},
{file = "coverage-7.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:bb9d7efdb063903b3fdf77caec7b77c3066885068bdc0d44bc1b0c171033f944"},
{file = "coverage-7.11.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:fb58da65e3339b3dbe266b607bb936efb983d86b00b03eb04c4ad5b442c58428"},
{file = "coverage-7.11.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8d16bbe566e16a71d123cd66382c1315fcd520c7573652a8074a8fe281b38c6a"},
{file = "coverage-7.11.3-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8258f10059b5ac837232c589a350a2df4a96406d6d5f2a09ec587cbdd539655"},
{file = "coverage-7.11.3-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4c5627429f7fbff4f4131cfdd6abd530734ef7761116811a707b88b7e205afd7"},
{file = "coverage-7.11.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:465695268414e149bab754c54b0c45c8ceda73dd4a5c3ba255500da13984b16d"},
{file = "coverage-7.11.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4ebcddfcdfb4c614233cff6e9a3967a09484114a8b2e4f2c7a62dc83676ba13f"},
{file = "coverage-7.11.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:13b2066303a1c1833c654d2af0455bb009b6e1727b3883c9964bc5c2f643c1d0"},
{file = "coverage-7.11.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d8750dd20362a1b80e3cf84f58013d4672f89663aee457ea59336df50fab6739"},
{file = "coverage-7.11.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ab6212e62ea0e1006531a2234e209607f360d98d18d532c2fa8e403c1afbdd71"},
{file = "coverage-7.11.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a6b17c2b5e0b9bb7702449200f93e2d04cb04b1414c41424c08aa1e5d352da76"},
{file = "coverage-7.11.3-cp313-cp313t-win32.whl", hash = "sha256:426559f105f644b69290ea414e154a0d320c3ad8a2bb75e62884731f69cf8e2c"},
{file = "coverage-7.11.3-cp313-cp313t-win_amd64.whl", hash = "sha256:90a96fcd824564eae6137ec2563bd061d49a32944858d4bdbae5c00fb10e76ac"},
{file = "coverage-7.11.3-cp313-cp313t-win_arm64.whl", hash = "sha256:1e33d0bebf895c7a0905fcfaff2b07ab900885fc78bba2a12291a2cfbab014cc"},
{file = "coverage-7.11.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fdc5255eb4815babcdf236fa1a806ccb546724c8a9b129fd1ea4a5448a0bf07c"},
{file = "coverage-7.11.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fe3425dc6021f906c6325d3c415e048e7cdb955505a94f1eb774dafc779ba203"},
{file = "coverage-7.11.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4ca5f876bf41b24378ee67c41d688155f0e54cdc720de8ef9ad6544005899240"},
{file = "coverage-7.11.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9061a3e3c92b27fd8036dafa26f25d95695b6aa2e4514ab16a254f297e664f83"},
{file = "coverage-7.11.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:abcea3b5f0dc44e1d01c27090bc32ce6ffb7aa665f884f1890710454113ea902"},
{file = "coverage-7.11.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:68c4eb92997dbaaf839ea13527be463178ac0ddd37a7ac636b8bc11a51af2428"},
{file = "coverage-7.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:149eccc85d48c8f06547534068c41d69a1a35322deaa4d69ba1561e2e9127e75"},
{file = "coverage-7.11.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:08c0bcf932e47795c49f0406054824b9d45671362dfc4269e0bc6e4bff010704"},
{file = "coverage-7.11.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:39764c6167c82d68a2d8c97c33dba45ec0ad9172570860e12191416f4f8e6e1b"},
{file = "coverage-7.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3224c7baf34e923ffc78cb45e793925539d640d42c96646db62dbd61bbcfa131"},
{file = "coverage-7.11.3-cp314-cp314-win32.whl", hash = "sha256:c713c1c528284d636cd37723b0b4c35c11190da6f932794e145fc40f8210a14a"},
{file = "coverage-7.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:c381a252317f63ca0179d2c7918e83b99a4ff3101e1b24849b999a00f9cd4f86"},
{file = "coverage-7.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:3e33a968672be1394eded257ec10d4acbb9af2ae263ba05a99ff901bb863557e"},
{file = "coverage-7.11.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f9c96a29c6d65bd36a91f5634fef800212dff69dacdb44345c4c9783943ab0df"},
{file = "coverage-7.11.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2ec27a7a991d229213c8070d31e3ecf44d005d96a9edc30c78eaeafaa421c001"},
{file = "coverage-7.11.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:72c8b494bd20ae1c58528b97c4a67d5cfeafcb3845c73542875ecd43924296de"},
{file = "coverage-7.11.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:60ca149a446da255d56c2a7a813b51a80d9497a62250532598d249b3cdb1a926"},
{file = "coverage-7.11.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb5069074db19a534de3859c43eec78e962d6d119f637c41c8e028c5ab3f59dd"},
{file = "coverage-7.11.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac5d5329c9c942bbe6295f4251b135d860ed9f86acd912d418dce186de7c19ac"},
{file = "coverage-7.11.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e22539b676fafba17f0a90ac725f029a309eb6e483f364c86dcadee060429d46"},
{file = "coverage-7.11.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2376e8a9c889016f25472c452389e98bc6e54a19570b107e27cde9d47f387b64"},
{file = "coverage-7.11.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4234914b8c67238a3c4af2bba648dc716aa029ca44d01f3d51536d44ac16854f"},
{file = "coverage-7.11.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f0b4101e2b3c6c352ff1f70b3a6fcc7c17c1ab1a91ccb7a33013cb0782af9820"},
{file = "coverage-7.11.3-cp314-cp314t-win32.whl", hash = "sha256:305716afb19133762e8cf62745c46c4853ad6f9eeba54a593e373289e24ea237"},
{file = "coverage-7.11.3-cp314-cp314t-win_amd64.whl", hash = "sha256:9245bd392572b9f799261c4c9e7216bafc9405537d0f4ce3ad93afe081a12dc9"},
{file = "coverage-7.11.3-cp314-cp314t-win_arm64.whl", hash = "sha256:9a1d577c20b4334e5e814c3d5fe07fa4a8c3ae42a601945e8d7940bab811d0bd"},
{file = "coverage-7.11.3-py3-none-any.whl", hash = "sha256:351511ae28e2509c8d8cae5311577ea7dd511ab8e746ffc8814a0896c3d33fbe"},
{file = "coverage-7.11.3.tar.gz", hash = "sha256:0f59387f5e6edbbffec2281affb71cdc85e0776c1745150a3ab9b6c1d016106b"},
{file = "coverage-7.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:32b75c2ba3f324ee37af3ccee5b30458038c50b349ad9b88cee85096132a575b"},
{file = "coverage-7.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cb2a1b6ab9fe833714a483a915de350abc624a37149649297624c8d57add089c"},
{file = "coverage-7.12.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5734b5d913c3755e72f70bf6cc37a0518d4f4745cde760c5d8e12005e62f9832"},
{file = "coverage-7.12.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b527a08cdf15753279b7afb2339a12073620b761d79b81cbe2cdebdb43d90daa"},
{file = "coverage-7.12.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9bb44c889fb68004e94cab71f6a021ec83eac9aeabdbb5a5a88821ec46e1da73"},
{file = "coverage-7.12.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4b59b501455535e2e5dde5881739897967b272ba25988c89145c12d772810ccb"},
{file = "coverage-7.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d8842f17095b9868a05837b7b1b73495293091bed870e099521ada176aa3e00e"},
{file = "coverage-7.12.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c5a6f20bf48b8866095c6820641e7ffbe23f2ac84a2efc218d91235e404c7777"},
{file = "coverage-7.12.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:5f3738279524e988d9da2893f307c2093815c623f8d05a8f79e3eff3a7a9e553"},
{file = "coverage-7.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0d68c1f7eabbc8abe582d11fa393ea483caf4f44b0af86881174769f185c94d"},
{file = "coverage-7.12.0-cp310-cp310-win32.whl", hash = "sha256:7670d860e18b1e3ee5930b17a7d55ae6287ec6e55d9799982aa103a2cc1fa2ef"},
{file = "coverage-7.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:f999813dddeb2a56aab5841e687b68169da0d3f6fc78ccf50952fa2463746022"},
{file = "coverage-7.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa124a3683d2af98bd9d9c2bfa7a5076ca7e5ab09fdb96b81fa7d89376ae928f"},
{file = "coverage-7.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d93fbf446c31c0140208dcd07c5d882029832e8ed7891a39d6d44bd65f2316c3"},
{file = "coverage-7.12.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:52ca620260bd8cd6027317bdd8b8ba929be1d741764ee765b42c4d79a408601e"},
{file = "coverage-7.12.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f3433ffd541380f3a0e423cff0f4926d55b0cc8c1d160fdc3be24a4c03aa65f7"},
{file = "coverage-7.12.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7bbb321d4adc9f65e402c677cd1c8e4c2d0105d3ce285b51b4d87f1d5db5245"},
{file = "coverage-7.12.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22a7aade354a72dff3b59c577bfd18d6945c61f97393bc5fb7bd293a4237024b"},
{file = "coverage-7.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3ff651dcd36d2fea66877cd4a82de478004c59b849945446acb5baf9379a1b64"},
{file = "coverage-7.12.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:31b8b2e38391a56e3cea39d22a23faaa7c3fc911751756ef6d2621d2a9daf742"},
{file = "coverage-7.12.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:297bc2da28440f5ae51c845a47c8175a4db0553a53827886e4fb25c66633000c"},
{file = "coverage-7.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ff7651cc01a246908eac162a6a86fc0dbab6de1ad165dfb9a1e2ec660b44984"},
{file = "coverage-7.12.0-cp311-cp311-win32.whl", hash = "sha256:313672140638b6ddb2c6455ddeda41c6a0b208298034544cfca138978c6baed6"},
{file = "coverage-7.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:a1783ed5bd0d5938d4435014626568dc7f93e3cb99bc59188cc18857c47aa3c4"},
{file = "coverage-7.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:4648158fd8dd9381b5847622df1c90ff314efbfc1df4550092ab6013c238a5fc"},
{file = "coverage-7.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:29644c928772c78512b48e14156b81255000dcfd4817574ff69def189bcb3647"},
{file = "coverage-7.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8638cbb002eaa5d7c8d04da667813ce1067080b9a91099801a0053086e52b736"},
{file = "coverage-7.12.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:083631eeff5eb9992c923e14b810a179798bb598e6a0dd60586819fc23be6e60"},
{file = "coverage-7.12.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:99d5415c73ca12d558e07776bd957c4222c687b9f1d26fa0e1b57e3598bdcde8"},
{file = "coverage-7.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e949ebf60c717c3df63adb4a1a366c096c8d7fd8472608cd09359e1bd48ef59f"},
{file = "coverage-7.12.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d907ddccbca819afa2cd014bc69983b146cca2735a0b1e6259b2a6c10be1e70"},
{file = "coverage-7.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b1518ecbad4e6173f4c6e6c4a46e49555ea5679bf3feda5edb1b935c7c44e8a0"},
{file = "coverage-7.12.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51777647a749abdf6f6fd8c7cffab12de68ab93aab15efc72fbbb83036c2a068"},
{file = "coverage-7.12.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:42435d46d6461a3b305cdfcad7cdd3248787771f53fe18305548cba474e6523b"},
{file = "coverage-7.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5bcead88c8423e1855e64b8057d0544e33e4080b95b240c2a355334bb7ced937"},
{file = "coverage-7.12.0-cp312-cp312-win32.whl", hash = "sha256:dcbb630ab034e86d2a0f79aefd2be07e583202f41e037602d438c80044957baa"},
{file = "coverage-7.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:2fd8354ed5d69775ac42986a691fbf68b4084278710cee9d7c3eaa0c28fa982a"},
{file = "coverage-7.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:737c3814903be30695b2de20d22bcc5428fdae305c61ba44cdc8b3252984c49c"},
{file = "coverage-7.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47324fffca8d8eae7e185b5bb20c14645f23350f870c1649003618ea91a78941"},
{file = "coverage-7.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ccf3b2ede91decd2fb53ec73c1f949c3e034129d1e0b07798ff1d02ea0c8fa4a"},
{file = "coverage-7.12.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b365adc70a6936c6b0582dc38746b33b2454148c02349345412c6e743efb646d"},
{file = "coverage-7.12.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bc13baf85cd8a4cfcf4a35c7bc9d795837ad809775f782f697bf630b7e200211"},
{file = "coverage-7.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:099d11698385d572ceafb3288a5b80fe1fc58bf665b3f9d362389de488361d3d"},
{file = "coverage-7.12.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:473dc45d69694069adb7680c405fb1e81f60b2aff42c81e2f2c3feaf544d878c"},
{file = "coverage-7.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:583f9adbefd278e9de33c33d6846aa8f5d164fa49b47144180a0e037f0688bb9"},
{file = "coverage-7.12.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b2089cc445f2dc0af6f801f0d1355c025b76c24481935303cf1af28f636688f0"},
{file = "coverage-7.12.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:950411f1eb5d579999c5f66c62a40961f126fc71e5e14419f004471957b51508"},
{file = "coverage-7.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b1aab7302a87bafebfe76b12af681b56ff446dc6f32ed178ff9c092ca776e6bc"},
{file = "coverage-7.12.0-cp313-cp313-win32.whl", hash = "sha256:d7e0d0303c13b54db495eb636bc2465b2fb8475d4c8bcec8fe4b5ca454dfbae8"},
{file = "coverage-7.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:ce61969812d6a98a981d147d9ac583a36ac7db7766f2e64a9d4d059c2fe29d07"},
{file = "coverage-7.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bcec6f47e4cb8a4c2dc91ce507f6eefc6a1b10f58df32cdc61dff65455031dfc"},
{file = "coverage-7.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:459443346509476170d553035e4a3eed7b860f4fe5242f02de1010501956ce87"},
{file = "coverage-7.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:04a79245ab2b7a61688958f7a855275997134bc84f4a03bc240cf64ff132abf6"},
{file = "coverage-7.12.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:09a86acaaa8455f13d6a99221d9654df249b33937b4e212b4e5a822065f12aa7"},
{file = "coverage-7.12.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:907e0df1b71ba77463687a74149c6122c3f6aac56c2510a5d906b2f368208560"},
{file = "coverage-7.12.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b57e2d0ddd5f0582bae5437c04ee71c46cd908e7bc5d4d0391f9a41e812dd12"},
{file = "coverage-7.12.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:58c1c6aa677f3a1411fe6fb28ec3a942e4f665df036a3608816e0847fad23296"},
{file = "coverage-7.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4c589361263ab2953e3c4cd2a94db94c4ad4a8e572776ecfbad2389c626e4507"},
{file = "coverage-7.12.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:91b810a163ccad2e43b1faa11d70d3cf4b6f3d83f9fd5f2df82a32d47b648e0d"},
{file = "coverage-7.12.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:40c867af715f22592e0d0fb533a33a71ec9e0f73a6945f722a0c85c8c1cbe3a2"},
{file = "coverage-7.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:68b0d0a2d84f333de875666259dadf28cc67858bc8fd8b3f1eae84d3c2bec455"},
{file = "coverage-7.12.0-cp313-cp313t-win32.whl", hash = "sha256:73f9e7fbd51a221818fd11b7090eaa835a353ddd59c236c57b2199486b116c6d"},
{file = "coverage-7.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:24cff9d1f5743f67db7ba46ff284018a6e9aeb649b67aa1e70c396aa1b7cb23c"},
{file = "coverage-7.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:c87395744f5c77c866d0f5a43d97cc39e17c7f1cb0115e54a2fe67ca75c5d14d"},
{file = "coverage-7.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a1c59b7dc169809a88b21a936eccf71c3895a78f5592051b1af8f4d59c2b4f92"},
{file = "coverage-7.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8787b0f982e020adb732b9f051f3e49dd5054cebbc3f3432061278512a2b1360"},
{file = "coverage-7.12.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5ea5a9f7dc8877455b13dd1effd3202e0bca72f6f3ab09f9036b1bcf728f69ac"},
{file = "coverage-7.12.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fdba9f15849534594f60b47c9a30bc70409b54947319a7c4fd0e8e3d8d2f355d"},
{file = "coverage-7.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a00594770eb715854fb1c57e0dea08cce6720cfbc531accdb9850d7c7770396c"},
{file = "coverage-7.12.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5560c7e0d82b42eb1951e4f68f071f8017c824ebfd5a6ebe42c60ac16c6c2434"},
{file = "coverage-7.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2e26b481c9159c2773a37947a9718cfdc58893029cdfb177531793e375cfc"},
{file = "coverage-7.12.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6e1a8c066dabcde56d5d9fed6a66bc19a2883a3fe051f0c397a41fc42aedd4cc"},
{file = "coverage-7.12.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f7ba9da4726e446d8dd8aae5a6cd872511184a5d861de80a86ef970b5dacce3e"},
{file = "coverage-7.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e0f483ab4f749039894abaf80c2f9e7ed77bbf3c737517fb88c8e8e305896a17"},
{file = "coverage-7.12.0-cp314-cp314-win32.whl", hash = "sha256:76336c19a9ef4a94b2f8dc79f8ac2da3f193f625bb5d6f51a328cd19bfc19933"},
{file = "coverage-7.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:7c1059b600aec6ef090721f8f633f60ed70afaffe8ecab85b59df748f24b31fe"},
{file = "coverage-7.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:172cf3a34bfef42611963e2b661302a8931f44df31629e5b1050567d6b90287d"},
{file = "coverage-7.12.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:aa7d48520a32cb21c7a9b31f81799e8eaec7239db36c3b670be0fa2403828d1d"},
{file = "coverage-7.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:90d58ac63bc85e0fb919f14d09d6caa63f35a5512a2205284b7816cafd21bb03"},
{file = "coverage-7.12.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ca8ecfa283764fdda3eae1bdb6afe58bf78c2c3ec2b2edcb05a671f0bba7b3f9"},
{file = "coverage-7.12.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:874fe69a0785d96bd066059cd4368022cebbec1a8958f224f0016979183916e6"},
{file = "coverage-7.12.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b3c889c0b8b283a24d721a9eabc8ccafcfc3aebf167e4cd0d0e23bf8ec4e339"},
{file = "coverage-7.12.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8bb5b894b3ec09dcd6d3743229dc7f2c42ef7787dc40596ae04c0edda487371e"},
{file = "coverage-7.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:79a44421cd5fba96aa57b5e3b5a4d3274c449d4c622e8f76882d76635501fd13"},
{file = "coverage-7.12.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:33baadc0efd5c7294f436a632566ccc1f72c867f82833eb59820ee37dc811c6f"},
{file = "coverage-7.12.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c406a71f544800ef7e9e0000af706b88465f3573ae8b8de37e5f96c59f689ad1"},
{file = "coverage-7.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e71bba6a40883b00c6d571599b4627f50c360b3d0d02bfc658168936be74027b"},
{file = "coverage-7.12.0-cp314-cp314t-win32.whl", hash = "sha256:9157a5e233c40ce6613dead4c131a006adfda70e557b6856b97aceed01b0e27a"},
{file = "coverage-7.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e84da3a0fd233aeec797b981c51af1cabac74f9bd67be42458365b30d11b5291"},
{file = "coverage-7.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:01d24af36fedda51c2b1aca56e4330a3710f83b02a5ff3743a6b015ffa7c9384"},
{file = "coverage-7.12.0-py3-none-any.whl", hash = "sha256:159d50c0b12e060b15ed3d39f87ed43d4f7f7ad40b8a534f4dd331adbb51104a"},
{file = "coverage-7.12.0.tar.gz", hash = "sha256:fc11e0a4e372cb5f282f16ef90d4a585034050ccda536451901abfb19a57f40c"},
]
[package.extras]
@@ -978,14 +978,14 @@ files = [
[[package]]
name = "exceptiongroup"
version = "1.3.0"
version = "1.3.1"
description = "Backport of PEP 654 (exception groups)"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"},
{file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"},
{file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"},
{file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"},
]
[package.extras]
@@ -1067,14 +1067,14 @@ pyflakes = ">=3.4.0,<3.5.0"
[[package]]
name = "flake8-bugbear"
version = "25.10.21"
version = "25.11.29"
description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle."
optional = false
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "flake8_bugbear-25.10.21-py3-none-any.whl", hash = "sha256:f1c5654f9d9d3e62e90da1f0335551fdbc565c51749713177dbcfb9edb105405"},
{file = "flake8_bugbear-25.10.21.tar.gz", hash = "sha256:2876afcaed8bfb3464cf33e3ec42cc3bec0a004165b84400dc3392b0547c2714"},
{file = "flake8_bugbear-25.11.29-py3-none-any.whl", hash = "sha256:9bf15e2970e736d2340da4c0a70493db964061c9c38f708cfe1f7b2d87392298"},
{file = "flake8_bugbear-25.11.29.tar.gz", hash = "sha256:b5d06710f3d26e595541ad303ad4d5cb52578bd4bccbb2c2c0b2c72e243dafc8"},
]
[package.dependencies]
@@ -1342,6 +1342,8 @@ files = [
{file = "greenlet-3.2.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d"},
{file = "greenlet-3.2.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5"},
{file = "greenlet-3.2.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8854167e06950ca75b898b104b63cc646573aa5fef1353d4508ecdd1ee76254f"},
{file = "greenlet-3.2.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f47617f698838ba98f4ff4189aef02e7343952df3a615f847bb575c3feb177a7"},
{file = "greenlet-3.2.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af41be48a4f60429d5cad9d22175217805098a9ef7c40bfef44f7669fb9d74d8"},
{file = "greenlet-3.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:73f49b5368b5359d04e18d15828eecc1806033db5233397748f4ca813ff1056c"},
{file = "greenlet-3.2.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2"},
{file = "greenlet-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246"},
@@ -1351,6 +1353,8 @@ files = [
{file = "greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8"},
{file = "greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52"},
{file = "greenlet-3.2.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa"},
{file = "greenlet-3.2.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9c6de1940a7d828635fbd254d69db79e54619f165ee7ce32fda763a9cb6a58c"},
{file = "greenlet-3.2.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03c5136e7be905045160b1b9fdca93dd6727b180feeafda6818e6496434ed8c5"},
{file = "greenlet-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9"},
{file = "greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd"},
{file = "greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb"},
@@ -1360,6 +1364,8 @@ files = [
{file = "greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0"},
{file = "greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0"},
{file = "greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f"},
{file = "greenlet-3.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee7a6ec486883397d70eec05059353b8e83eca9168b9f3f9a361971e77e0bcd0"},
{file = "greenlet-3.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:326d234cbf337c9c3def0676412eb7040a35a768efc92504b947b3e9cfc7543d"},
{file = "greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02"},
{file = "greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31"},
{file = "greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945"},
@@ -1369,6 +1375,8 @@ files = [
{file = "greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671"},
{file = "greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b"},
{file = "greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae"},
{file = "greenlet-3.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e343822feb58ac4d0a1211bd9399de2b3a04963ddeec21530fc426cc121f19b"},
{file = "greenlet-3.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca7f6f1f2649b89ce02f6f229d7c19f680a6238af656f61e0115b24857917929"},
{file = "greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b"},
{file = "greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0"},
{file = "greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f"},
@@ -1376,6 +1384,8 @@ files = [
{file = "greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1"},
{file = "greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735"},
{file = "greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337"},
{file = "greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269"},
{file = "greenlet-3.2.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:015d48959d4add5d6c9f6c5210ee3803a830dce46356e3bc326d6776bde54681"},
{file = "greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01"},
{file = "greenlet-3.2.4-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:b6a7c19cf0d2742d0809a4c05975db036fdff50cd294a93632d6a310bf9ac02c"},
{file = "greenlet-3.2.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:27890167f55d2387576d1f41d9487ef171849ea0359ce1510ca6e06c8bece11d"},
@@ -1385,6 +1395,8 @@ files = [
{file = "greenlet-3.2.4-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9913f1a30e4526f432991f89ae263459b1c64d1608c0d22a5c79c287b3c70df"},
{file = "greenlet-3.2.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b90654e092f928f110e0007f572007c9727b5265f7632c2fa7415b4689351594"},
{file = "greenlet-3.2.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:81701fd84f26330f0d5f4944d4e92e61afe6319dcd9775e39396e39d7c3e5f98"},
{file = "greenlet-3.2.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:28a3c6b7cd72a96f61b0e4b2a36f681025b60ae4779cc73c1535eb5f29560b10"},
{file = "greenlet-3.2.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:52206cd642670b0b320a1fd1cbfd95bca0e043179c1d8a045f2c6109dfe973be"},
{file = "greenlet-3.2.4-cp39-cp39-win32.whl", hash = "sha256:65458b409c1ed459ea899e939f0e1cdb14f58dbc803f2f93c5eab5694d32671b"},
{file = "greenlet-3.2.4-cp39-cp39-win_amd64.whl", hash = "sha256:d2e685ade4dafd447ede19c31277a224a239a0a1a4eca4e6390efedf20260cfb"},
{file = "greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d"},
@@ -1494,24 +1506,24 @@ tests = ["freezegun", "pytest", "pytest-cov"]
[[package]]
name = "hypothesis"
version = "6.147.0"
description = "A library for property-based testing"
version = "6.148.7"
description = "The property-based testing library for Python"
optional = false
python-versions = ">=3.10"
groups = ["main"]
files = [
{file = "hypothesis-6.147.0-py3-none-any.whl", hash = "sha256:de588807b6da33550d32f47bcd42b1a86d061df85673aa73e6443680249d185e"},
{file = "hypothesis-6.147.0.tar.gz", hash = "sha256:72e6004ea3bd1460bdb4640b6389df23b87ba7a4851893fd84d1375635d3e507"},
{file = "hypothesis-6.148.7-py3-none-any.whl", hash = "sha256:94dbd58ebf259afa3bafb1d3bf5761ac1bde6f1477de494798cbf7960aabbdee"},
{file = "hypothesis-6.148.7.tar.gz", hash = "sha256:b96e817e715c5b1a278411e3b9baf6d599d5b12207ba25e41a8f066929f6c2a6"},
]
[package.dependencies]
sortedcontainers = ">=2.1.0,<3.0.0"
[package.extras]
all = ["black (>=20.8b0)", "click (>=7.0)", "crosshair-tool (>=0.0.97)", "django (>=4.2)", "dpcontracts (>=0.4)", "hypothesis-crosshair (>=0.0.25)", "lark (>=0.10.1)", "libcst (>=0.3.16)", "numpy (>=1.21.6)", "pandas (>=1.1)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2025.2) ; sys_platform == \"win32\" or sys_platform == \"emscripten\"", "watchdog (>=4.0.0)"]
all = ["black (>=20.8b0)", "click (>=7.0)", "crosshair-tool (>=0.0.99)", "django (>=4.2)", "dpcontracts (>=0.4)", "hypothesis-crosshair (>=0.0.26)", "lark (>=0.10.1)", "libcst (>=0.3.16)", "numpy (>=1.21.6)", "pandas (>=1.1)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2025.2) ; sys_platform == \"win32\" or sys_platform == \"emscripten\"", "watchdog (>=4.0.0)"]
cli = ["black (>=20.8b0)", "click (>=7.0)", "rich (>=9.0.0)"]
codemods = ["libcst (>=0.3.16)"]
crosshair = ["crosshair-tool (>=0.0.97)", "hypothesis-crosshair (>=0.0.25)"]
crosshair = ["crosshair-tool (>=0.0.99)", "hypothesis-crosshair (>=0.0.26)"]
dateutil = ["python-dateutil (>=1.4)"]
django = ["django (>=4.2)"]
dpcontracts = ["dpcontracts (>=0.4)"]
@@ -2132,14 +2144,14 @@ files = [
[[package]]
name = "moto"
version = "5.1.16"
version = "5.1.18"
description = "A library that allows you to easily mock out tests based on AWS infrastructure"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "moto-5.1.16-py3-none-any.whl", hash = "sha256:8e6186f20b3aa91755d186e47701fe7e47f74e625c36fdf3bd7747da68468b19"},
{file = "moto-5.1.16.tar.gz", hash = "sha256:792045b345d16a8aa09068ad4a7656894e707c796f0799b438fffb738e8fae7c"},
{file = "moto-5.1.18-py3-none-any.whl", hash = "sha256:b65aa8fc9032c5c574415451e14fd7da4e43fd50b8bdcb5f10289ad382c25bcf"},
{file = "moto-5.1.18.tar.gz", hash = "sha256:45298ef7b88561b839f6fe3e9da2a6e2ecd10283c7bf3daf43a07a97465885f9"},
]
[package.dependencies]
@@ -2313,86 +2325,86 @@ files = [
[[package]]
name = "numpy"
version = "2.3.4"
version = "2.3.5"
description = "Fundamental package for array computing in Python"
optional = false
python-versions = ">=3.11"
groups = ["main"]
files = [
{file = "numpy-2.3.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e78aecd2800b32e8347ce49316d3eaf04aed849cd5b38e0af39f829a4e59f5eb"},
{file = "numpy-2.3.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7fd09cc5d65bda1e79432859c40978010622112e9194e581e3415a3eccc7f43f"},
{file = "numpy-2.3.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1b219560ae2c1de48ead517d085bc2d05b9433f8e49d0955c82e8cd37bd7bf36"},
{file = "numpy-2.3.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:bafa7d87d4c99752d07815ed7a2c0964f8ab311eb8168f41b910bd01d15b6032"},
{file = "numpy-2.3.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36dc13af226aeab72b7abad501d370d606326a0029b9f435eacb3b8c94b8a8b7"},
{file = "numpy-2.3.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7b2f9a18b5ff9824a6af80de4f37f4ec3c2aab05ef08f51c77a093f5b89adda"},
{file = "numpy-2.3.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9984bd645a8db6ca15d850ff996856d8762c51a2239225288f08f9050ca240a0"},
{file = "numpy-2.3.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:64c5825affc76942973a70acf438a8ab618dbd692b84cd5ec40a0a0509edc09a"},
{file = "numpy-2.3.4-cp311-cp311-win32.whl", hash = "sha256:ed759bf7a70342f7817d88376eb7142fab9fef8320d6019ef87fae05a99874e1"},
{file = "numpy-2.3.4-cp311-cp311-win_amd64.whl", hash = "sha256:faba246fb30ea2a526c2e9645f61612341de1a83fb1e0c5edf4ddda5a9c10996"},
{file = "numpy-2.3.4-cp311-cp311-win_arm64.whl", hash = "sha256:4c01835e718bcebe80394fd0ac66c07cbb90147ebbdad3dcecd3f25de2ae7e2c"},
{file = "numpy-2.3.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ef1b5a3e808bc40827b5fa2c8196151a4c5abe110e1726949d7abddfe5c7ae11"},
{file = "numpy-2.3.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2f91f496a87235c6aaf6d3f3d89b17dba64996abadccb289f48456cff931ca9"},
{file = "numpy-2.3.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f77e5b3d3da652b474cc80a14084927a5e86a5eccf54ca8ca5cbd697bf7f2667"},
{file = "numpy-2.3.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:8ab1c5f5ee40d6e01cbe96de5863e39b215a4d24e7d007cad56c7184fdf4aeef"},
{file = "numpy-2.3.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77b84453f3adcb994ddbd0d1c5d11db2d6bda1a2b7fd5ac5bd4649d6f5dc682e"},
{file = "numpy-2.3.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4121c5beb58a7f9e6dfdee612cb24f4df5cd4db6e8261d7f4d7450a997a65d6a"},
{file = "numpy-2.3.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:65611ecbb00ac9846efe04db15cbe6186f562f6bb7e5e05f077e53a599225d16"},
{file = "numpy-2.3.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dabc42f9c6577bcc13001b8810d300fe814b4cfbe8a92c873f269484594f9786"},
{file = "numpy-2.3.4-cp312-cp312-win32.whl", hash = "sha256:a49d797192a8d950ca59ee2d0337a4d804f713bb5c3c50e8db26d49666e351dc"},
{file = "numpy-2.3.4-cp312-cp312-win_amd64.whl", hash = "sha256:985f1e46358f06c2a09921e8921e2c98168ed4ae12ccd6e5e87a4f1857923f32"},
{file = "numpy-2.3.4-cp312-cp312-win_arm64.whl", hash = "sha256:4635239814149e06e2cb9db3dd584b2fa64316c96f10656983b8026a82e6e4db"},
{file = "numpy-2.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c090d4860032b857d94144d1a9976b8e36709e40386db289aaf6672de2a81966"},
{file = "numpy-2.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a13fc473b6db0be619e45f11f9e81260f7302f8d180c49a22b6e6120022596b3"},
{file = "numpy-2.3.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:3634093d0b428e6c32c3a69b78e554f0cd20ee420dcad5a9f3b2a63762ce4197"},
{file = "numpy-2.3.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:043885b4f7e6e232d7df4f51ffdef8c36320ee9d5f227b380ea636722c7ed12e"},
{file = "numpy-2.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ee6a571d1e4f0ea6d5f22d6e5fbd6ed1dc2b18542848e1e7301bd190500c9d7"},
{file = "numpy-2.3.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc8a63918b04b8571789688b2780ab2b4a33ab44bfe8ccea36d3eba51228c953"},
{file = "numpy-2.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:40cc556d5abbc54aabe2b1ae287042d7bdb80c08edede19f0c0afb36ae586f37"},
{file = "numpy-2.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ecb63014bb7f4ce653f8be7f1df8cbc6093a5a2811211770f6606cc92b5a78fd"},
{file = "numpy-2.3.4-cp313-cp313-win32.whl", hash = "sha256:e8370eb6925bb8c1c4264fec52b0384b44f675f191df91cbe0140ec9f0955646"},
{file = "numpy-2.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:56209416e81a7893036eea03abcb91c130643eb14233b2515c90dcac963fe99d"},
{file = "numpy-2.3.4-cp313-cp313-win_arm64.whl", hash = "sha256:a700a4031bc0fd6936e78a752eefb79092cecad2599ea9c8039c548bc097f9bc"},
{file = "numpy-2.3.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:86966db35c4040fdca64f0816a1c1dd8dbd027d90fca5a57e00e1ca4cd41b879"},
{file = "numpy-2.3.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:838f045478638b26c375ee96ea89464d38428c69170360b23a1a50fa4baa3562"},
{file = "numpy-2.3.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d7315ed1dab0286adca467377c8381cd748f3dc92235f22a7dfc42745644a96a"},
{file = "numpy-2.3.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:84f01a4d18b2cc4ade1814a08e5f3c907b079c847051d720fad15ce37aa930b6"},
{file = "numpy-2.3.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:817e719a868f0dacde4abdfc5c1910b301877970195db9ab6a5e2c4bd5b121f7"},
{file = "numpy-2.3.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85e071da78d92a214212cacea81c6da557cab307f2c34b5f85b628e94803f9c0"},
{file = "numpy-2.3.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2ec646892819370cf3558f518797f16597b4e4669894a2ba712caccc9da53f1f"},
{file = "numpy-2.3.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:035796aaaddfe2f9664b9a9372f089cfc88bd795a67bd1bfe15e6e770934cf64"},
{file = "numpy-2.3.4-cp313-cp313t-win32.whl", hash = "sha256:fea80f4f4cf83b54c3a051f2f727870ee51e22f0248d3114b8e755d160b38cfb"},
{file = "numpy-2.3.4-cp313-cp313t-win_amd64.whl", hash = "sha256:15eea9f306b98e0be91eb344a94c0e630689ef302e10c2ce5f7e11905c704f9c"},
{file = "numpy-2.3.4-cp313-cp313t-win_arm64.whl", hash = "sha256:b6c231c9c2fadbae4011ca5e7e83e12dc4a5072f1a1d85a0a7b3ed754d145a40"},
{file = "numpy-2.3.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:81c3e6d8c97295a7360d367f9f8553973651b76907988bb6066376bc2252f24e"},
{file = "numpy-2.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7c26b0b2bf58009ed1f38a641f3db4be8d960a417ca96d14e5b06df1506d41ff"},
{file = "numpy-2.3.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:62b2198c438058a20b6704351b35a1d7db881812d8512d67a69c9de1f18ca05f"},
{file = "numpy-2.3.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:9d729d60f8d53a7361707f4b68a9663c968882dd4f09e0d58c044c8bf5faee7b"},
{file = "numpy-2.3.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd0c630cf256b0a7fd9d0a11c9413b42fef5101219ce6ed5a09624f5a65392c7"},
{file = "numpy-2.3.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5e081bc082825f8b139f9e9fe42942cb4054524598aaeb177ff476cc76d09d2"},
{file = "numpy-2.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15fb27364ed84114438fff8aaf998c9e19adbeba08c0b75409f8c452a8692c52"},
{file = "numpy-2.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:85d9fb2d8cd998c84d13a79a09cc0c1091648e848e4e6249b0ccd7f6b487fa26"},
{file = "numpy-2.3.4-cp314-cp314-win32.whl", hash = "sha256:e73d63fd04e3a9d6bc187f5455d81abfad05660b212c8804bf3b407e984cd2bc"},
{file = "numpy-2.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:3da3491cee49cf16157e70f607c03a217ea6647b1cea4819c4f48e53d49139b9"},
{file = "numpy-2.3.4-cp314-cp314-win_arm64.whl", hash = "sha256:6d9cd732068e8288dbe2717177320723ccec4fb064123f0caf9bbd90ab5be868"},
{file = "numpy-2.3.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:22758999b256b595cf0b1d102b133bb61866ba5ceecf15f759623b64c020c9ec"},
{file = "numpy-2.3.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9cb177bc55b010b19798dc5497d540dea67fd13a8d9e882b2dae71de0cf09eb3"},
{file = "numpy-2.3.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0f2bcc76f1e05e5ab58893407c63d90b2029908fa41f9f1cc51eecce936c3365"},
{file = "numpy-2.3.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:8dc20bde86802df2ed8397a08d793da0ad7a5fd4ea3ac85d757bf5dd4ad7c252"},
{file = "numpy-2.3.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e199c087e2aa71c8f9ce1cb7a8e10677dc12457e7cc1be4798632da37c3e86e"},
{file = "numpy-2.3.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85597b2d25ddf655495e2363fe044b0ae999b75bc4d630dc0d886484b03a5eb0"},
{file = "numpy-2.3.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04a69abe45b49c5955923cf2c407843d1c85013b424ae8a560bba16c92fe44a0"},
{file = "numpy-2.3.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e1708fac43ef8b419c975926ce1eaf793b0c13b7356cfab6ab0dc34c0a02ac0f"},
{file = "numpy-2.3.4-cp314-cp314t-win32.whl", hash = "sha256:863e3b5f4d9915aaf1b8ec79ae560ad21f0b8d5e3adc31e73126491bb86dee1d"},
{file = "numpy-2.3.4-cp314-cp314t-win_amd64.whl", hash = "sha256:962064de37b9aef801d33bc579690f8bfe6c5e70e29b61783f60bcba838a14d6"},
{file = "numpy-2.3.4-cp314-cp314t-win_arm64.whl", hash = "sha256:8b5a9a39c45d852b62693d9b3f3e0fe052541f804296ff401a72a1b60edafb29"},
{file = "numpy-2.3.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6e274603039f924c0fe5cb73438fa9246699c78a6df1bd3decef9ae592ae1c05"},
{file = "numpy-2.3.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d149aee5c72176d9ddbc6803aef9c0f6d2ceeea7626574fc68518da5476fa346"},
{file = "numpy-2.3.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:6d34ed9db9e6395bb6cd33286035f73a59b058169733a9db9f85e650b88df37e"},
{file = "numpy-2.3.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:fdebe771ca06bb8d6abce84e51dca9f7921fe6ad34a0c914541b063e9a68928b"},
{file = "numpy-2.3.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e92defe6c08211eb77902253b14fe5b480ebc5112bc741fd5e9cd0608f847"},
{file = "numpy-2.3.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13b9062e4f5c7ee5c7e5be96f29ba71bc5a37fed3d1d77c37390ae00724d296d"},
{file = "numpy-2.3.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:81b3a59793523e552c4a96109dde028aa4448ae06ccac5a76ff6532a85558a7f"},
{file = "numpy-2.3.4.tar.gz", hash = "sha256:a7d018bfedb375a8d979ac758b120ba846a7fe764911a64465fd87b8729f4a6a"},
{file = "numpy-2.3.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de5672f4a7b200c15a4127042170a694d4df43c992948f5e1af57f0174beed10"},
{file = "numpy-2.3.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:acfd89508504a19ed06ef963ad544ec6664518c863436306153e13e94605c218"},
{file = "numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ffe22d2b05504f786c867c8395de703937f934272eb67586817b46188b4ded6d"},
{file = "numpy-2.3.5-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:872a5cf366aec6bb1147336480fef14c9164b154aeb6542327de4970282cd2f5"},
{file = "numpy-2.3.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3095bdb8dd297e5920b010e96134ed91d852d81d490e787beca7e35ae1d89cf7"},
{file = "numpy-2.3.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8cba086a43d54ca804ce711b2a940b16e452807acebe7852ff327f1ecd49b0d4"},
{file = "numpy-2.3.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6cf9b429b21df6b99f4dee7a1218b8b7ffbbe7df8764dc0bd60ce8a0708fed1e"},
{file = "numpy-2.3.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:396084a36abdb603546b119d96528c2f6263921c50df3c8fd7cb28873a237748"},
{file = "numpy-2.3.5-cp311-cp311-win32.whl", hash = "sha256:b0c7088a73aef3d687c4deef8452a3ac7c1be4e29ed8bf3b366c8111128ac60c"},
{file = "numpy-2.3.5-cp311-cp311-win_amd64.whl", hash = "sha256:a414504bef8945eae5f2d7cb7be2d4af77c5d1cb5e20b296c2c25b61dff2900c"},
{file = "numpy-2.3.5-cp311-cp311-win_arm64.whl", hash = "sha256:0cd00b7b36e35398fa2d16af7b907b65304ef8bb4817a550e06e5012929830fa"},
{file = "numpy-2.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74ae7b798248fe62021dbf3c914245ad45d1a6b0cb4a29ecb4b31d0bfbc4cc3e"},
{file = "numpy-2.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee3888d9ff7c14604052b2ca5535a30216aa0a58e948cdd3eeb8d3415f638769"},
{file = "numpy-2.3.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:612a95a17655e213502f60cfb9bf9408efdc9eb1d5f50535cc6eb365d11b42b5"},
{file = "numpy-2.3.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3101e5177d114a593d79dd79658650fe28b5a0d8abeb8ce6f437c0e6df5be1a4"},
{file = "numpy-2.3.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b973c57ff8e184109db042c842423ff4f60446239bd585a5131cc47f06f789d"},
{file = "numpy-2.3.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d8163f43acde9a73c2a33605353a4f1bc4798745a8b1d73183b28e5b435ae28"},
{file = "numpy-2.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:51c1e14eb1e154ebd80e860722f9e6ed6ec89714ad2db2d3aa33c31d7c12179b"},
{file = "numpy-2.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b46b4ec24f7293f23adcd2d146960559aaf8020213de8ad1909dba6c013bf89c"},
{file = "numpy-2.3.5-cp312-cp312-win32.whl", hash = "sha256:3997b5b3c9a771e157f9aae01dd579ee35ad7109be18db0e85dbdbe1de06e952"},
{file = "numpy-2.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:86945f2ee6d10cdfd67bcb4069c1662dd711f7e2a4343db5cecec06b87cf31aa"},
{file = "numpy-2.3.5-cp312-cp312-win_arm64.whl", hash = "sha256:f28620fe26bee16243be2b7b874da327312240a7cdc38b769a697578d2100013"},
{file = "numpy-2.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d0f23b44f57077c1ede8c5f26b30f706498b4862d3ff0a7298b8411dd2f043ff"},
{file = "numpy-2.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa5bc7c5d59d831d9773d1170acac7893ce3a5e130540605770ade83280e7188"},
{file = "numpy-2.3.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccc933afd4d20aad3c00bcef049cb40049f7f196e0397f1109dba6fed63267b0"},
{file = "numpy-2.3.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afaffc4393205524af9dfa400fa250143a6c3bc646c08c9f5e25a9f4b4d6a903"},
{file = "numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c75442b2209b8470d6d5d8b1c25714270686f14c749028d2199c54e29f20b4d"},
{file = "numpy-2.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e06aa0af8c0f05104d56450d6093ee639e15f24ecf62d417329d06e522e017"},
{file = "numpy-2.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed89927b86296067b4f81f108a2271d8926467a8868e554eaf370fc27fa3ccaf"},
{file = "numpy-2.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51c55fe3451421f3a6ef9a9c1439e82101c57a2c9eab9feb196a62b1a10b58ce"},
{file = "numpy-2.3.5-cp313-cp313-win32.whl", hash = "sha256:1978155dd49972084bd6ef388d66ab70f0c323ddee6f693d539376498720fb7e"},
{file = "numpy-2.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:00dc4e846108a382c5869e77c6ed514394bdeb3403461d25a829711041217d5b"},
{file = "numpy-2.3.5-cp313-cp313-win_arm64.whl", hash = "sha256:0472f11f6ec23a74a906a00b48a4dcf3849209696dff7c189714511268d103ae"},
{file = "numpy-2.3.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:414802f3b97f3c1eef41e530aaba3b3c1620649871d8cb38c6eaff034c2e16bd"},
{file = "numpy-2.3.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5ee6609ac3604fa7780e30a03e5e241a7956f8e2fcfe547d51e3afa5247ac47f"},
{file = "numpy-2.3.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:86d835afea1eaa143012a2d7a3f45a3adce2d7adc8b4961f0b362214d800846a"},
{file = "numpy-2.3.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:30bc11310e8153ca664b14c5f1b73e94bd0503681fcf136a163de856f3a50139"},
{file = "numpy-2.3.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1062fde1dcf469571705945b0f221b73928f34a20c904ffb45db101907c3454e"},
{file = "numpy-2.3.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce581db493ea1a96c0556360ede6607496e8bf9b3a8efa66e06477267bc831e9"},
{file = "numpy-2.3.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:cc8920d2ec5fa99875b670bb86ddeb21e295cb07aa331810d9e486e0b969d946"},
{file = "numpy-2.3.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9ee2197ef8c4f0dfe405d835f3b6a14f5fee7782b5de51ba06fb65fc9b36e9f1"},
{file = "numpy-2.3.5-cp313-cp313t-win32.whl", hash = "sha256:70b37199913c1bd300ff6e2693316c6f869c7ee16378faf10e4f5e3275b299c3"},
{file = "numpy-2.3.5-cp313-cp313t-win_amd64.whl", hash = "sha256:b501b5fa195cc9e24fe102f21ec0a44dffc231d2af79950b451e0d99cea02234"},
{file = "numpy-2.3.5-cp313-cp313t-win_arm64.whl", hash = "sha256:a80afd79f45f3c4a7d341f13acbe058d1ca8ac017c165d3fa0d3de6bc1a079d7"},
{file = "numpy-2.3.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:bf06bc2af43fa8d32d30fae16ad965663e966b1a3202ed407b84c989c3221e82"},
{file = "numpy-2.3.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:052e8c42e0c49d2575621c158934920524f6c5da05a1d3b9bab5d8e259e045f0"},
{file = "numpy-2.3.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:1ed1ec893cff7040a02c8aa1c8611b94d395590d553f6b53629a4461dc7f7b63"},
{file = "numpy-2.3.5-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:2dcd0808a421a482a080f89859a18beb0b3d1e905b81e617a188bd80422d62e9"},
{file = "numpy-2.3.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727fd05b57df37dc0bcf1a27767a3d9a78cbbc92822445f32cc3436ba797337b"},
{file = "numpy-2.3.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fffe29a1ef00883599d1dc2c51aa2e5d80afe49523c261a74933df395c15c520"},
{file = "numpy-2.3.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f7f0e05112916223d3f438f293abf0727e1181b5983f413dfa2fefc4098245c"},
{file = "numpy-2.3.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2e2eb32ddb9ccb817d620ac1d8dae7c3f641c1e5f55f531a33e8ab97960a75b8"},
{file = "numpy-2.3.5-cp314-cp314-win32.whl", hash = "sha256:66f85ce62c70b843bab1fb14a05d5737741e74e28c7b8b5a064de10142fad248"},
{file = "numpy-2.3.5-cp314-cp314-win_amd64.whl", hash = "sha256:e6a0bc88393d65807d751a614207b7129a310ca4fe76a74e5c7da5fa5671417e"},
{file = "numpy-2.3.5-cp314-cp314-win_arm64.whl", hash = "sha256:aeffcab3d4b43712bb7a60b65f6044d444e75e563ff6180af8f98dd4b905dfd2"},
{file = "numpy-2.3.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17531366a2e3a9e30762c000f2c43a9aaa05728712e25c11ce1dbe700c53ad41"},
{file = "numpy-2.3.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d21644de1b609825ede2f48be98dfde4656aefc713654eeee280e37cadc4e0ad"},
{file = "numpy-2.3.5-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c804e3a5aba5460c73955c955bdbd5c08c354954e9270a2c1565f62e866bdc39"},
{file = "numpy-2.3.5-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:cc0a57f895b96ec78969c34f682c602bf8da1a0270b09bc65673df2e7638ec20"},
{file = "numpy-2.3.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:900218e456384ea676e24ea6a0417f030a3b07306d29d7ad843957b40a9d8d52"},
{file = "numpy-2.3.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09a1bea522b25109bf8e6f3027bd810f7c1085c64a0c7ce050c1676ad0ba010b"},
{file = "numpy-2.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04822c00b5fd0323c8166d66c701dc31b7fbd252c100acd708c48f763968d6a3"},
{file = "numpy-2.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d6889ec4ec662a1a37eb4b4fb26b6100841804dac55bd9df579e326cdc146227"},
{file = "numpy-2.3.5-cp314-cp314t-win32.whl", hash = "sha256:93eebbcf1aafdf7e2ddd44c2923e2672e1010bddc014138b229e49725b4d6be5"},
{file = "numpy-2.3.5-cp314-cp314t-win_amd64.whl", hash = "sha256:c8a9958e88b65c3b27e22ca2a076311636850b612d6bbfb76e8d156aacde2aaf"},
{file = "numpy-2.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42"},
{file = "numpy-2.3.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f0963b55cdd70fad460fa4c1341f12f976bb26cb66021a5580329bd498988310"},
{file = "numpy-2.3.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f4255143f5160d0de972d28c8f9665d882b5f61309d8362fdd3e103cf7bf010c"},
{file = "numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:a4b9159734b326535f4dd01d947f919c6eefd2d9827466a696c44ced82dfbc18"},
{file = "numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2feae0d2c91d46e59fcd62784a3a83b3fb677fead592ce51b5a6fbb4f95965ff"},
{file = "numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffac52f28a7849ad7576293c0cb7b9f08304e8f7d738a8cb8a90ec4c55a998eb"},
{file = "numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63c0e9e7eea69588479ebf4a8a270d5ac22763cc5854e9a7eae952a3908103f7"},
{file = "numpy-2.3.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f16417ec91f12f814b10bafe79ef77e70113a2f5f7018640e7425ff979253425"},
{file = "numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0"},
]
[[package]]
@@ -2490,14 +2502,14 @@ install = ["zstandard (>=0.21.0)"]
[[package]]
name = "phonenumbers"
version = "9.0.18"
version = "9.0.20"
description = "Python version of Google's common library for parsing, formatting, storing and validating international phone numbers."
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "phonenumbers-9.0.18-py2.py3-none-any.whl", hash = "sha256:d3354454ac31c97f8a08121df97a7145b8dca641f734c6f1518a41c2f60c5764"},
{file = "phonenumbers-9.0.18.tar.gz", hash = "sha256:5537c61ba95b11b992c95e804da6e49193cc06b1224f632ade64631518a48ed1"},
{file = "phonenumbers-9.0.20-py2.py3-none-any.whl", hash = "sha256:03bf5dd14891891284ba96f803d0e5e7e11b9306a0ec4fdf25756ada39eacb86"},
{file = "phonenumbers-9.0.20.tar.gz", hash = "sha256:849788eec8e5a9737a99c8b906d18a62d9fced6497ba7033784b6a7e4c89bb2d"},
]
[[package]]
@@ -2529,32 +2541,34 @@ pip = "*"
[[package]]
name = "pip-audit"
version = "2.9.0"
version = "2.10.0"
description = "A tool for scanning Python environments for known vulnerabilities"
optional = false
python-versions = ">=3.9"
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "pip_audit-2.9.0-py3-none-any.whl", hash = "sha256:348b16e60895749a0839875d7cc27ebd692e1584ebe5d5cb145941c8e25a80bd"},
{file = "pip_audit-2.9.0.tar.gz", hash = "sha256:0b998410b58339d7a231e5aa004326a294e4c7c6295289cdc9d5e1ef07b1f44d"},
{file = "pip_audit-2.10.0-py3-none-any.whl", hash = "sha256:16e02093872fac97580303f0848fa3ad64f7ecf600736ea7835a2b24de49613f"},
{file = "pip_audit-2.10.0.tar.gz", hash = "sha256:427ea5bf61d1d06b98b1ae29b7feacc00288a2eced52c9c58ceed5253ef6c2a4"},
]
[package.dependencies]
CacheControl = {version = ">=0.13.0", extras = ["filecache"]}
cyclonedx-python-lib = ">=5,<10"
cyclonedx-python-lib = ">=5,<12"
packaging = ">=23.0.0"
pip-api = ">=0.0.28"
pip-requirements-parser = ">=32.0.0"
platformdirs = ">=4.2.0"
requests = ">=2.31.0"
rich = ">=12.4"
toml = ">=0.10"
tomli = ">=2.2.1"
tomli-w = ">=1.2.0"
[package.extras]
cov = ["coverage[toml] (>=7.0,!=7.3.3,<8.0)"]
dev = ["build", "pip-audit[doc,lint,test]"]
doc = ["pdoc"]
lint = ["interrogate (>=1.6,<2.0)", "mypy", "ruff (>=0.9,<1.0)", "types-requests", "types-toml"]
test = ["coverage[toml] (>=7.0,!=7.3.3,<8.0)", "pretend", "pytest", "pytest-cov"]
lint = ["interrogate (>=1.6,<2.0)", "mypy", "ruff (>=0.11)", "types-requests", "types-toml"]
test = ["pip-audit[cov]", "pretend", "pytest"]
[[package]]
name = "pip-requirements-parser"
@@ -2711,14 +2725,14 @@ python-dotenv = ">=0.10.0"
[[package]]
name = "pre-commit"
version = "4.4.0"
version = "4.5.0"
description = "A framework for managing and maintaining multi-language pre-commit hooks."
optional = false
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "pre_commit-4.4.0-py2.py3-none-any.whl", hash = "sha256:b35ea52957cbf83dcc5d8ee636cbead8624e3a15fbfa61a370e42158ac8a5813"},
{file = "pre_commit-4.4.0.tar.gz", hash = "sha256:f0233ebab440e9f17cabbb558706eb173d19ace965c68cdce2c081042b4fab15"},
{file = "pre_commit-4.5.0-py2.py3-none-any.whl", hash = "sha256:25e2ce09595174d9c97860a95609f9f852c0614ba602de3561e267547f2335e1"},
{file = "pre_commit-4.5.0.tar.gz", hash = "sha256:dc5a065e932b19fc1d4c653c6939068fe54325af8e741e74e88db4d28a4dd66b"},
]
[package.dependencies]
@@ -3047,20 +3061,20 @@ files = [
[[package]]
name = "pytest"
version = "8.4.2"
version = "9.0.1"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.9"
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"},
{file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"},
{file = "pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad"},
{file = "pytest-9.0.1.tar.gz", hash = "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8"},
]
[package.dependencies]
colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""}
iniconfig = ">=1"
packaging = ">=20"
iniconfig = ">=1.0.1"
packaging = ">=22"
pluggy = ">=1.5,<2"
pygments = ">=2.7.2"
@@ -3124,19 +3138,19 @@ dev = ["pre-commit", "pytest-asyncio", "tox"]
[[package]]
name = "pytest-playwright"
version = "0.7.1"
version = "0.7.2"
description = "A pytest wrapper with fixtures for Playwright to automate web browsers"
optional = false
python-versions = ">=3.9"
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "pytest_playwright-0.7.1-py3-none-any.whl", hash = "sha256:fcc46510fb75f8eba6df3bc8e84e4e902483d92be98075f20b9d160651a36d90"},
{file = "pytest_playwright-0.7.1.tar.gz", hash = "sha256:94b551b2677ecdc16284fcd6a4f0045eafda47a60e74410f3fe4d8260e12cabf"},
{file = "pytest_playwright-0.7.2-py3-none-any.whl", hash = "sha256:8084e015b2b3ecff483c2160f1c8219b38b66c0d4578b23c0f700d1b0240ea38"},
{file = "pytest_playwright-0.7.2.tar.gz", hash = "sha256:247b61123b28c7e8febb993a187a07e54f14a9aa04edc166f7a976d88f04c770"},
]
[package.dependencies]
playwright = ">=1.18"
pytest = ">=6.2.4,<9.0.0"
pytest = ">=6.2.4,<10.0.0"
pytest-base-url = ">=1.0.0,<3.0.0"
python-slugify = ">=6.0.0,<9.0.0"
@@ -3461,14 +3475,14 @@ all = ["numpy"]
[[package]]
name = "redis"
version = "7.0.1"
version = "7.1.0"
description = "Python client for Redis database and key-value store"
optional = false
python-versions = ">=3.9"
python-versions = ">=3.10"
groups = ["main"]
files = [
{file = "redis-7.0.1-py3-none-any.whl", hash = "sha256:4977af3c7d67f8f0eb8b6fec0dafc9605db9343142f634041fb0235f67c0588a"},
{file = "redis-7.0.1.tar.gz", hash = "sha256:c949df947dca995dc68fdf5a7863950bf6df24f8d6022394585acc98e81624f1"},
{file = "redis-7.1.0-py3-none-any.whl", hash = "sha256:23c52b208f92b56103e17c5d06bdc1a6c2c0b3106583985a76a18f83b265de2b"},
{file = "redis-7.1.0.tar.gz", hash = "sha256:b1cc3cfa5a2cb9c2ab3ba700864fb0ad75617b41f01352ce5779dabf6d5f9c3c"},
]
[package.extras]
@@ -3965,15 +3979,67 @@ files = [
]
[[package]]
name = "toml"
version = "0.10.2"
description = "Python Library for Tom's Obvious, Minimal Language"
name = "tomli"
version = "2.3.0"
description = "A lil' TOML parser"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
{file = "tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45"},
{file = "tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba"},
{file = "tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf"},
{file = "tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441"},
{file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845"},
{file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c"},
{file = "tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456"},
{file = "tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be"},
{file = "tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac"},
{file = "tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22"},
{file = "tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f"},
{file = "tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52"},
{file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8"},
{file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6"},
{file = "tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876"},
{file = "tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878"},
{file = "tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b"},
{file = "tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae"},
{file = "tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b"},
{file = "tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf"},
{file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f"},
{file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05"},
{file = "tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606"},
{file = "tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999"},
{file = "tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e"},
{file = "tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3"},
{file = "tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc"},
{file = "tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0"},
{file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879"},
{file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005"},
{file = "tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463"},
{file = "tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8"},
{file = "tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77"},
{file = "tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf"},
{file = "tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530"},
{file = "tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b"},
{file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67"},
{file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f"},
{file = "tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0"},
{file = "tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba"},
{file = "tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b"},
{file = "tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549"},
]
[[package]]
name = "tomli-w"
version = "1.2.0"
description = "A lil' TOML writer"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90"},
{file = "tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021"},
]
[[package]]
@@ -4014,21 +4080,21 @@ files = [
[[package]]
name = "urllib3"
version = "2.5.0"
version = "2.6.0"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
python-versions = ">=3.9"
groups = ["main", "dev"]
files = [
{file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"},
{file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"},
{file = "urllib3-2.6.0-py3-none-any.whl", hash = "sha256:c90f7a39f716c572c4e3e58509581ebd83f9b59cced005b7db7ad2d22b0db99f"},
{file = "urllib3-2.6.0.tar.gz", hash = "sha256:cb9bcef5a4b345d5da5d145dc3e30834f58e8018828cbc724d30b4cb7d4d49f1"},
]
[package.extras]
brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""]
brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""]
h2 = ["h2 (>=4,<5)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
zstd = ["zstandard (>=0.18.0)"]
zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""]
[[package]]
name = "virtualenv"
@@ -4077,18 +4143,18 @@ files = [
[[package]]
name = "werkzeug"
version = "3.1.3"
version = "3.1.4"
description = "The comprehensive WSGI web application library."
optional = false
python-versions = ">=3.9"
groups = ["main", "dev"]
files = [
{file = "werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e"},
{file = "werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746"},
{file = "werkzeug-3.1.4-py3-none-any.whl", hash = "sha256:2ad50fb9ed09cc3af22c54698351027ace879a0b60a3b5edf5730b2f7d876905"},
{file = "werkzeug-3.1.4.tar.gz", hash = "sha256:cd3cd98b1b92dc3b7b3995038826c68097dcb16f9baa63abe35f20eafeb9fe5e"},
]
[package.dependencies]
MarkupSafe = ">=2.1.1"
markupsafe = ">=2.1.1"
[package.extras]
watchdog = ["watchdog (>=2.3)"]
@@ -4428,4 +4494,4 @@ cffi = ["cffi (>=1.17) ; python_version >= \"3.13\" and platform_python_implemen
[metadata]
lock-version = "2.1"
python-versions = "^3.13.2"
content-hash = "1335fddbebfbc7063b8472df2456478073b2adff3a242f963cac1a65b7d86110"
content-hash = "e4982f084acb029455079bb568d3eb91542d22508faadb37c763cf7d8c75d3c6"

View File

@@ -10,17 +10,17 @@ package-mode = false
axe-core-python = "^0.1.0"
python = "^3.13.2"
ago = "~=0.1.0"
beautifulsoup4 = "^4.14.2"
beautifulsoup4 = "^4.14.3"
blinker = "~=1.8"
gevent = "==25.9.1"
exceptiongroup = "==1.3.0"
exceptiongroup = "==1.3.1"
flask = "~=3.1"
flask-basicauth = "~=0.2"
flask-login = "^0.6"
flask-talisman = "*"
flask-wtf = "^1.2"
gunicorn = {version = "==23.0.0", extras = ["gevent"]}
hypothesis = "^6.147.0"
hypothesis = "^6.148.7"
humanize = "~=4.14"
itsdangerous = "~=2.2"
jinja2 = "^3.1.6"
@@ -34,7 +34,7 @@ openpyxl = "==3.0.10"
pyproj = "==3.7.2"
python-dotenv = "==1.2.1"
rtreelib = "==0.2.0"
werkzeug = "^3.1.3"
werkzeug = "^3.1.4"
wtforms = "~=3.1"
markdown = "^3.10"
mistune = "^3.1.4"
@@ -42,33 +42,33 @@ async-timeout = "^5.0.1"
bleach = "^6.3.0"
boto3 = "^1.35.99"
botocore = "^1.35.99"
cachetools = "^6.2.1"
cachetools = "^6.2.2"
cffi = "^2.0.0"
cryptography = "^46.0.3"
flask-redis = "^0.4.0"
geojson = "^3.1.0"
jmespath = "^1.0.1"
numpy = "^2.3.4"
numpy = "^2.3.5"
ordered-set = "^4.1.0"
phonenumbers = "^9.0.18"
phonenumbers = "^9.0.20"
pycparser = "^2.23"
pyjwt = "^2.10.1"
python-json-logger = "^4.0.0"
redis = "^7.0.1"
redis = "^7.1.0"
regex = "^2025.11.3"
s3transfer = "^0.10.2"
shapely = "^2.1.2"
smartypants = "^2.0.1"
certifi = "^2025.10.5"
certifi = "^2025.11.12"
charset-normalizer = "^3.4.4"
click = "^8.3.0"
click = "^8.3.1"
idna = "^3.11"
markupsafe = "^3.0.3"
python-dateutil = "^2.9.0.post0"
pyyaml = "^6.0.3"
requests = "^2.32.5"
six = "^1.16.0"
urllib3 = "^2.5.0"
urllib3 = "^2.6.0"
webencodings = "^0.5.1"
virtualenv = "<20.36"
@@ -81,18 +81,18 @@ coverage = "*"
detect-secrets = "^1.5.0"
freezegun = "^1.5.5"
flake8 = "^7.3.0"
flake8-bugbear = "^25.10.21"
flake8-bugbear = "^25.11.29"
flake8-print = "^5.0.0"
flake8-pytest-style = "^2.2.0"
isort = "^7.0.0"
jinja2-cli = {version = "==0.8.2", extras = ["yaml"]}
moto = "*"
pip-audit = "*"
pre-commit = "^4.4.0"
pytest = "^8.4.2"
pre-commit = "^4.5.0"
pytest = "^9.0.1"
pytest-env = "^1.2.0"
pytest-mock = "^3.15.1"
pytest-playwright = "^0.7.1"
pytest-playwright = "^0.7.2"
pytest-xdist = "^3.8.0"
radon = "^6.0.1"
requests-mock = "^1.11.0"

View File

@@ -81,7 +81,7 @@ def test_cancel_invited_org_user_cancels_user_invitations(
invited_user_id=sample_org_invite["id"],
_follow_redirects=True,
)
assert normalize_spaces(page.h1.text) == "Team members"
assert normalize_spaces(page.h1.text) == "Organization administrators"
flash_banner = normalize_spaces(
page.select_one(".usa-alert--success .usa-alert__text").text
)

View File

@@ -839,7 +839,7 @@ def test_edit_organization_user_shows_the_delete_confirmation_banner(
user_id=active_user_with_permissions["id"],
)
assert normalize_spaces(page.h1) == "Team members"
assert normalize_spaces(page.h1) == "Organization administrators"
banner = page.select_one(".usa-alert--error")
banner_text = banner.select_one(".usa-alert__text")
@@ -1521,7 +1521,7 @@ def test_organization_billing_page_not_accessible_if_not_platform_admin(
)
def test_organization_dashboard_shows_message_usage(
def test_organization_dashboard_shows_messages_sent(
client_request,
mock_get_organization,
mocker,
@@ -1531,8 +1531,6 @@ def test_organization_dashboard_shows_message_usage(
"app.organizations_client.get_organization_message_usage",
return_value={
"messages_sent": 1000,
"messages_remaining": 2000,
"total_message_limit": 3000,
},
)
mocker.patch(
@@ -1553,11 +1551,7 @@ def test_organization_dashboard_shows_message_usage(
mock_message_usage.assert_called_once_with(ORGANISATION_ID)
assert normalize_spaces(page.select_one("h1").text) == "Organization Dashboard"
chart_container = page.select_one("#totalMessageChartContainer")
assert chart_container["data-messages-sent"] == "1000"
assert chart_container["data-messages-remaining"] == "2000"
assert chart_container["data-total-message-limit"] == "3000"
assert "1,000 sent" in page.text
def test_organization_dashboard_shows_service_counts(
@@ -1622,7 +1616,7 @@ def test_organization_dashboard_shows_service_counts(
assert "3" in normalize_spaces(service_box.text)
assert "1 Live" in normalize_spaces(service_box.text)
assert "1 Trial" in normalize_spaces(service_box.text)
assert "1 Suspended" in normalize_spaces(service_box.text)
assert "1 Archived" in normalize_spaces(service_box.text)
def test_organization_dashboard_services_table(
@@ -1699,7 +1693,7 @@ def test_organization_dashboard_services_table(
assert normalize_spaces(first_row_cells[1].text) == "Live"
assert "1,500 emails" in normalize_spaces(first_row_cells[2].text)
assert "500 sms" in normalize_spaces(first_row_cells[2].text)
assert "249,500 remaining" in normalize_spaces(first_row_cells[2].text)
assert "249,500 message parts remaining" in normalize_spaces(first_row_cells[2].text)
assert normalize_spaces(first_row_cells[3].text) == "N/A"
assert normalize_spaces(first_row_cells[4].text) == "Welcome SMS"
@@ -1708,7 +1702,7 @@ def test_organization_dashboard_services_table(
assert normalize_spaces(second_row_cells[1].text) == "Trial"
assert "250 emails" in normalize_spaces(second_row_cells[2].text)
assert "100 sms" in normalize_spaces(second_row_cells[2].text)
assert "249,900 remaining" in normalize_spaces(second_row_cells[2].text)
assert "249,900 message parts remaining" in normalize_spaces(second_row_cells[2].text)
assert normalize_spaces(second_row_cells[3].text) == "N/A"
assert normalize_spaces(second_row_cells[4].text) == "Reminder SMS"

View File

@@ -52,6 +52,9 @@ def test_all_activity(
"app.job_api_client.get_page_of_jobs", return_value=MOCK_JOBS
)
mocker.patch("app.job_api_client.get_immediate_jobs", return_value=[])
mock_s3_obj = mocker.Mock()
mock_s3_obj.content_length = 0
mocker.patch("app.s3_client.get_s3_object", return_value=mock_s3_obj)
mocker.patch("app.s3_client.check_s3_file_exists", return_value=False)
mocker.patch("app.s3_client.s3_csv_client.get_csv_upload", return_value=None)
@@ -140,6 +143,9 @@ def test_all_activity_no_jobs(client_request, mocker):
},
)
mocker.patch("app.job_api_client.get_immediate_jobs", return_value=[])
mock_s3_obj = mocker.Mock()
mock_s3_obj.content_length = 0
mocker.patch("app.s3_client.get_s3_object", return_value=mock_s3_obj)
mocker.patch("app.s3_client.check_s3_file_exists", return_value=False)
mocker.patch("app.s3_client.s3_csv_client.get_csv_upload", return_value=None)
response = client_request.get_response(
@@ -195,6 +201,9 @@ def test_all_activity_pagination(client_request, mocker):
},
)
mocker.patch("app.job_api_client.get_immediate_jobs", return_value=[])
mock_s3_obj = mocker.Mock()
mock_s3_obj.content_length = 0
mocker.patch("app.s3_client.get_s3_object", return_value=mock_s3_obj)
mocker.patch("app.s3_client.check_s3_file_exists", return_value=False)
mocker.patch("app.s3_client.s3_csv_client.get_csv_upload", return_value=None)
@@ -234,6 +243,9 @@ def test_all_activity_filters(client_request, mocker, filter_type, expected_limi
"app.job_api_client.get_page_of_jobs", return_value=MOCK_JOBS
)
mocker.patch("app.job_api_client.get_immediate_jobs", return_value=[])
mock_s3_obj = mocker.Mock()
mock_s3_obj.content_length = 0
mocker.patch("app.s3_client.get_s3_object", return_value=mock_s3_obj)
mocker.patch("app.s3_client.check_s3_file_exists", return_value=False)
mocker.patch("app.s3_client.s3_csv_client.get_csv_upload", return_value=None)

View File

@@ -0,0 +1,223 @@
/**
* @jest-environment jsdom
*/
describe('organizationDashboard.js', () => {
let originalLocation;
beforeEach(() => {
jest.resetModules();
jest.useFakeTimers();
originalLocation = window.location;
Element.prototype.scrollIntoView = jest.fn();
window.requestAnimationFrame = jest.fn((cb) => setTimeout(cb, 0));
});
afterEach(() => {
jest.useRealTimers();
document.body.innerHTML = '';
});
describe('Delete Service Confirmation', () => {
beforeEach(() => {
document.body.innerHTML = `
<button
data-open-modal="confirmDeleteModal"
data-service-id="service-123"
data-service-name="Test Service">
Delete
</button>
<div id="delete-service-name-display"></div>
<button id="delete-service-confirm-btn">Confirm Delete</button>
<form id="delete-service-form"></form>
`;
require('../../app/assets/javascripts/organizationDashboard.js');
document.dispatchEvent(new Event('DOMContentLoaded'));
});
test('sets service name when delete button clicked', () => {
const deleteButton = document.querySelector('[data-service-id="service-123"]');
const nameDisplay = document.getElementById('delete-service-name-display');
deleteButton.click();
expect(nameDisplay.textContent).toBe('Test Service');
});
test('does not update name display if missing', () => {
document.getElementById('delete-service-name-display').remove();
const deleteButton = document.querySelector('[data-service-id="service-123"]');
expect(() => deleteButton.click()).not.toThrow();
});
test('does nothing if required elements missing', () => {
document.body.innerHTML = '<div>No delete elements</div>';
require('../../app/assets/javascripts/organizationDashboard.js');
expect(() => document.dispatchEvent(new Event('DOMContentLoaded'))).not.toThrow();
});
});
describe('Edit Service Confirmation', () => {
beforeEach(() => {
document.body.innerHTML = `
<button id="edit-service-confirm-btn">Confirm Edit</button>
<form id="edit-service-form"></form>
`;
require('../../app/assets/javascripts/organizationDashboard.js');
document.dispatchEvent(new Event('DOMContentLoaded'));
});
test('submits edit form when confirm button clicked', () => {
const confirmButton = document.getElementById('edit-service-confirm-btn');
const editForm = document.getElementById('edit-service-form');
editForm.submit = jest.fn();
confirmButton.click();
expect(editForm.submit).toHaveBeenCalled();
});
test('does not throw if form missing when confirm clicked', () => {
document.getElementById('edit-service-form').remove();
const confirmButton = document.getElementById('edit-service-confirm-btn');
expect(() => confirmButton.click()).not.toThrow();
});
test('does nothing if confirm button missing', () => {
document.body.innerHTML = '<form id="edit-service-form"></form>';
require('../../app/assets/javascripts/organizationDashboard.js');
expect(() => document.dispatchEvent(new Event('DOMContentLoaded'))).not.toThrow();
});
});
describe('Form Initialization', () => {
test('scrolls to and focuses create service form', () => {
document.body.innerHTML = `
<form id="create-service-form">
<input type="text" name="service-name" />
</form>
`;
const form = document.getElementById('create-service-form');
const input = form.querySelector('input[type="text"]');
require('../../app/assets/javascripts/organizationDashboard.js');
document.dispatchEvent(new Event('DOMContentLoaded'));
jest.runAllTimers();
expect(form.scrollIntoView).toHaveBeenCalled();
expect(document.activeElement).toBe(input);
});
test('scrolls to and focuses invite user form', () => {
document.body.innerHTML = `
<form id="invite-user-form">
<input type="email" name="email" />
</form>
`;
const form = document.getElementById('invite-user-form');
const input = form.querySelector('input[type="email"]');
require('../../app/assets/javascripts/organizationDashboard.js');
document.dispatchEvent(new Event('DOMContentLoaded'));
jest.runAllTimers();
expect(form.scrollIntoView).toHaveBeenCalled();
expect(document.activeElement).toBe(input);
});
test('scrolls to and focuses edit service form', () => {
document.body.innerHTML = `
<form id="edit-service-form">
<input type="text" name="service-name" />
</form>
`;
const form = document.getElementById('edit-service-form');
const input = form.querySelector('input[type="text"]');
require('../../app/assets/javascripts/organizationDashboard.js');
document.dispatchEvent(new Event('DOMContentLoaded'));
jest.runAllTimers();
expect(form.scrollIntoView).toHaveBeenCalled();
expect(document.activeElement).toBe(input);
});
test('does nothing when no relevant forms present', () => {
document.body.innerHTML = '<div>No forms here</div>';
require('../../app/assets/javascripts/organizationDashboard.js');
document.dispatchEvent(new Event('DOMContentLoaded'));
jest.runAllTimers();
expect(Element.prototype.scrollIntoView).not.toHaveBeenCalled();
});
});
describe('Service Highlighting', () => {
test('scrolls to and removes highlight from service', () => {
document.body.innerHTML = `
<div id="service-test-123" class="is-highlighted"></div>
`;
const serviceRow = document.getElementById('service-test-123');
require('../../app/assets/javascripts/organizationDashboard.js');
document.dispatchEvent(new Event('DOMContentLoaded'));
window.OrganizationDashboard.highlightAndScrollToService('test-123');
jest.advanceTimersByTime(300);
expect(serviceRow.scrollIntoView).toHaveBeenCalled();
expect(serviceRow.classList.contains('is-highlighted')).toBe(true);
jest.advanceTimersByTime(3100);
expect(serviceRow.classList.contains('is-highlighted')).toBe(false);
});
test('removes class attribute when empty after highlight removal', () => {
document.body.innerHTML = `
<div id="service-test-123" class="is-highlighted"></div>
`;
const serviceRow = document.getElementById('service-test-123');
require('../../app/assets/javascripts/organizationDashboard.js');
document.dispatchEvent(new Event('DOMContentLoaded'));
window.OrganizationDashboard.highlightAndScrollToService('test-123');
jest.advanceTimersByTime(3400);
expect(serviceRow.hasAttribute('class')).toBe(false);
});
test('handles missing service element gracefully', () => {
document.body.innerHTML = '<div>No service here</div>';
require('../../app/assets/javascripts/organizationDashboard.js');
document.dispatchEvent(new Event('DOMContentLoaded'));
expect(() => {
window.OrganizationDashboard.highlightAndScrollToService('nonexistent');
jest.runAllTimers();
}).not.toThrow();
});
});
});