Compare commits

..

119 commits

Author SHA1 Message Date
f30055189b
chore(deps): update actions/setup-uv digest to d4b2f3b
All checks were successful
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 10m1s
Actions / Build Documentation (MkDocs) (pull_request) Successful in 10m49s
Actions / Build Documentation (MkDocs) (push) Successful in 10m15s
Actions / Lint Code (Ruff & Pylint) (push) Successful in 12m34s
2025-04-16 22:02:27 +00:00
89c1495155
chore(deps): update catthehacker/ubuntu:act-latest docker digest to cd83756
All checks were successful
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 1m35s
Actions / Build Documentation (MkDocs) (pull_request) Successful in 1m41s
Actions / Lint Code (Ruff & Pylint) (push) Successful in 55s
Actions / Build Documentation (MkDocs) (push) Successful in 1m6s
2025-04-08 13:35:04 +00:00
d3f9131afc
chore(deps): update catthehacker/ubuntu:act-latest docker digest to 0999d0b
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 33s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 37s
Actions / Build Documentation (MkDocs) (push) Successful in 32s
Actions / Lint Code (Ruff & Pylint) (push) Successful in 36s
2025-04-01 13:41:17 +00:00
0972be185f
chore(deps): update actions/setup-uv digest to 0c5e2b8
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 33s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 37s
Actions / Build Documentation (MkDocs) (push) Successful in 33s
Actions / Lint Code (Ruff & Pylint) (push) Successful in 37s
2025-03-31 00:41:12 +00:00
c5c4c53d5d
chore(deps): update dependency mkdocs-material to v9.6.10 (#85)
All checks were successful
Actions / Lint Code (Ruff & Pylint) (push) Successful in 52s
Actions / Build Documentation (MkDocs) (push) Successful in 1m3s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [mkdocs-material](https://github.com/squidfunk/mkdocs-material) ([changelog](https://squidfunk.github.io/mkdocs-material/changelog/)) | dependency-groups | patch | `==9.6.9` -> `==9.6.10` |

---

### Release Notes

<details>
<summary>squidfunk/mkdocs-material (mkdocs-material)</summary>

### [`v9.6.10`](https://github.com/squidfunk/mkdocs-material/releases/tag/9.6.10): mkdocs-material-9.6.10

[Compare Source](https://github.com/squidfunk/mkdocs-material/compare/9.6.9...9.6.10)

This version is a pure refactoring release, and does not contain new features or bug fixes. It strives to improve the compatibility of our templates with alternative Jinja-like template engines that we're currently exploring, including [minijinja].

Additionally, it replaces several instances of Python function invocations with idiomatic use of template filters. All instances where variables have been mutated inside templates have been replaced. Most changes have been made in partials, and only a few in blocks, and all of them are fully backward compatible, so no changes to overrides are necessary.

Note that this release does not replace the Jinja template engine with minijinja. However, our templates are now 99% compatible with minijinja, which means we can explore alternative Jinja-compatible implementations. Additionally, immutability and removal of almost all Python function invocations means much more idiomatic templating.

[minijinja]: https://github.com/mitsuhiko/minijinja

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMTEuMCIsInVwZGF0ZWRJblZlciI6IjM5LjIxMS4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: #85
Co-authored-by: Renovate <renovate@csw.im>
Co-committed-by: Renovate <renovate@csw.im>
2025-03-30 20:06:49 -04:00
f12f229b6e
chore(deps): lock file maintenance (#86)
All checks were successful
Actions / Build Documentation (MkDocs) (push) Successful in 1m7s
Actions / Lint Code (Ruff & Pylint) (push) Successful in 39s
This PR contains the following updates:

| Update | Change |
|---|---|
| lockFileMaintenance | All locks refreshed |

🔧 This Pull Request updates lock files to use the latest dependency versions.

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on monday" (UTC), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://github.com/renovatebot/renovate/discussions) if that's undesired.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMTEuMCIsInVwZGF0ZWRJblZlciI6IjM5LjIxMS4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: #86
Co-authored-by: Renovate <renovate@csw.im>
Co-committed-by: Renovate <renovate@csw.im>
2025-03-30 20:04:43 -04:00
050e66be7a
chore(deps): update catthehacker/ubuntu:act-latest docker digest to 0199164
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 34s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 37s
Actions / Build Documentation (MkDocs) (push) Successful in 34s
Actions / Lint Code (Ruff & Pylint) (push) Successful in 38s
2025-03-29 13:31:15 +00:00
a491d8e5fc
fix(deps): update dependency pydantic to v2.11.1 (#80)
All checks were successful
Actions / Lint Code (Ruff & Pylint) (push) Successful in 54s
Actions / Build Documentation (MkDocs) (push) Successful in 1m5s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [pydantic](https://github.com/pydantic/pydantic) ([changelog](https://docs.pydantic.dev/latest/changelog/)) | project.dependencies | minor | `==2.10.6` -> `==2.11.1` |

---

### Release Notes

<details>
<summary>pydantic/pydantic (pydantic)</summary>

### [`v2.11.1`](https://github.com/pydantic/pydantic/blob/HEAD/HISTORY.md#v2111-2025-03-28)

[Compare Source](https://github.com/pydantic/pydantic/compare/v2.11.0...v2.11.1)

[GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.11.1)

##### What's Changed

##### Fixes

-   Do not override `'definitions-ref'` schemas containing serialization schemas or metadata by [@&#8203;Viicos](https://github.com/Viicos) in [#&#8203;11644](https://github.com/pydantic/pydantic/pull/11644)

### [`v2.11.0`](https://github.com/pydantic/pydantic/blob/HEAD/HISTORY.md#v2110-2025-03-27)

[Compare Source](https://github.com/pydantic/pydantic/compare/v2.10.6...v2.11.0)

[GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.11.0)

##### What's Changed

Pydantic v2.11 is a version strongly focused on build time performance of Pydantic models (and core schema generation in general).
See the [blog post](https://pydantic.dev/articles/pydantic-v2-11-release) for more details.

##### Packaging

-   Bump `pydantic-core` to v2.33.0 by [@&#8203;Viicos](https://github.com/Viicos) in [#&#8203;11631](https://github.com/pydantic/pydantic/pull/11631)

##### New Features

-   Add `encoded_string()` method to the URL types by [@&#8203;YassinNouh21](https://github.com/YassinNouh21) in [#&#8203;11580](https://github.com/pydantic/pydantic/pull/11580)
-   Add support for `defer_build` with `@validate_call` decorator by [@&#8203;Viicos](https://github.com/Viicos) in [#&#8203;11584](https://github.com/pydantic/pydantic/pull/11584)
-   Allow `@with_config` decorator to be used with keyword arguments by [@&#8203;Viicos](https://github.com/Viicos) in [#&#8203;11608](https://github.com/pydantic/pydantic/pull/11608)
-   Simplify customization of default value inclusion in JSON Schema generation by [@&#8203;Viicos](https://github.com/Viicos) in [#&#8203;11634](https://github.com/pydantic/pydantic/pull/11634)
-   Add `generate_arguments_schema()` function by [@&#8203;Viicos](https://github.com/Viicos) in [#&#8203;11572](https://github.com/pydantic/pydantic/pull/11572)

##### Fixes

-   Allow generic typed dictionaries to be used for unpacked variadic keyword parameters by [@&#8203;Viicos](https://github.com/Viicos) in [#&#8203;11571](https://github.com/pydantic/pydantic/pull/11571)
-   Fix runtime error when computing model string representation involving cached properties and self-referenced models by [@&#8203;Viicos](https://github.com/Viicos) in [#&#8203;11579](https://github.com/pydantic/pydantic/pull/11579)
-   Preserve other steps when using the ellipsis in the pipeline API by [@&#8203;Viicos](https://github.com/Viicos) in [#&#8203;11626](https://github.com/pydantic/pydantic/pull/11626)
-   Fix deferred discriminator application logic by [@&#8203;Viicos](https://github.com/Viicos) in [#&#8203;11591](https://github.com/pydantic/pydantic/pull/11591)

##### New Contributors

-   [@&#8203;cmenon12](https://github.com/cmenon12) made their first contribution in [#&#8203;11562](https://github.com/pydantic/pydantic/pull/11562)
-   [@&#8203;Jeukoh](https://github.com/Jeukoh) made their first contribution in [#&#8203;11611](https://github.com/pydantic/pydantic/pull/11611)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMTEuMCIsInVwZGF0ZWRJblZlciI6IjM5LjIxMS4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: #80
Co-authored-by: Renovate <renovate@csw.im>
Co-committed-by: Renovate <renovate@csw.im>
2025-03-29 08:25:06 -04:00
aa20b5dcc0
fix(deps): update dependency markdownify to v1 (#81)
Some checks failed
Actions / Lint Code (Ruff & Pylint) (push) Has been cancelled
Actions / Build Documentation (MkDocs) (push) Has been cancelled
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [markdownify](https://github.com/matthewwithanm/python-markdownify) | project.dependencies | major | `==0.14.1` -> `==1.1.0` |

---

### Release Notes

<details>
<summary>matthewwithanm/python-markdownify (markdownify)</summary>

### [`v1.1.0`](https://github.com/matthewwithanm/python-markdownify/releases/tag/1.1.0)

[Compare Source](https://github.com/matthewwithanm/python-markdownify/compare/1.0.0...1.1.0)

#### What's Changed

-   Support `video` tag with `poster` attribute by [@&#8203;itmammoth](https://github.com/itmammoth) in https://github.com/matthewwithanm/python-markdownify/pull/189
-   Add missing newlines for definition lists by [@&#8203;chrispy-snps](https://github.com/chrispy-snps) in https://github.com/matthewwithanm/python-markdownify/pull/200
-   In inline contexts, resolve `<br/>` to a space instead of an empty string by [@&#8203;chrispy-snps](https://github.com/chrispy-snps) in https://github.com/matthewwithanm/python-markdownify/pull/202
-   Generalize `colspan` handling to handle missing header rows by [@&#8203;sbrown61](https://github.com/sbrown61) in https://github.com/matthewwithanm/python-markdownify/pull/203

#### New Contributors

-   [@&#8203;itmammoth](https://github.com/itmammoth) made their first contribution in https://github.com/matthewwithanm/python-markdownify/pull/189
-   [@&#8203;sbrown61](https://github.com/sbrown61) made their first contribution in https://github.com/matthewwithanm/python-markdownify/pull/203

**Full Changelog**: https://github.com/matthewwithanm/python-markdownify/compare/1.0.0...1.1.0

### [`v1.0.0`](https://github.com/matthewwithanm/python-markdownify/releases/tag/1.0.0)

[Compare Source](https://github.com/matthewwithanm/python-markdownify/compare/0.14.1...1.0.0)

#### Breaking Changes

If you are using custom tag conversion functions (`convert_*()`), note that the function interface has changed. See [#&#8203;191](https://github.com/matthewwithanm/python-markdownify/issues/191) for details.

#### What's Changed

-   Do not construct Markdown links in code spans and code blocks by [@&#8203;chrispy-snps](https://github.com/chrispy-snps) in https://github.com/matthewwithanm/python-markdownify/pull/165
-   Insert a blank line between table caption, table content by [@&#8203;chrispy-snps](https://github.com/chrispy-snps) in https://github.com/matthewwithanm/python-markdownify/pull/167
-   Allow a `wrap_width` value of `None` for unlimited line lengths by [@&#8203;chrispy-snps](https://github.com/chrispy-snps) in https://github.com/matthewwithanm/python-markdownify/pull/169
-   Optimize empty-line handling for `<li>` and `<blockquote>` content by [@&#8203;chrispy-snps](https://github.com/chrispy-snps) in https://github.com/matthewwithanm/python-markdownify/pull/171
-   Support HTML definition lists (`<dl>`, `<dt>`, and `<dd>`) by [@&#8203;chrispy-snps](https://github.com/chrispy-snps) in https://github.com/matthewwithanm/python-markdownify/pull/173
-   Add a new `table_infer_header` configuration option to control table header row inference by [@&#8203;SomeBottle](https://github.com/SomeBottle) in https://github.com/matthewwithanm/python-markdownify/pull/161
-   For `convert_*` functions, allow for tags with special characters in their name (like "subtag-name") by [@&#8203;Fess-AKA-DeadMonk](https://github.com/Fess-AKA-DeadMonk) in https://github.com/matthewwithanm/python-markdownify/pull/136
-   Code simplification to remove the `children_only` parameter by [@&#8203;chrispy-snps](https://github.com/chrispy-snps) in https://github.com/matthewwithanm/python-markdownify/pull/174
-   Add blank line before ATX-style headings to avoid ambiguity by [@&#8203;chrispy-snps](https://github.com/chrispy-snps) in https://github.com/matthewwithanm/python-markdownify/pull/178
-   Add blank line before/after preformatted blocks by [@&#8203;chrispy-snps](https://github.com/chrispy-snps) in https://github.com/matthewwithanm/python-markdownify/pull/179
-   Remove superfluous leading/trailing whitespace by [@&#8203;chrispy-snps](https://github.com/chrispy-snps) in https://github.com/matthewwithanm/python-markdownify/pull/181
-   Simplify computation of `convert_children_as_inline` variable by [@&#8203;chrispy-snps](https://github.com/chrispy-snps) in https://github.com/matthewwithanm/python-markdownify/pull/182
-   When computing `<ol>`/`<li>` numbering, ignore non-`<li>` previous siblings by [@&#8203;chrispy-snps](https://github.com/chrispy-snps) in https://github.com/matthewwithanm/python-markdownify/pull/183
-   Make conversion non-destructive to soup; improve div/article/section handling by [@&#8203;chrispy-snps](https://github.com/chrispy-snps) in https://github.com/matthewwithanm/python-markdownify/pull/184
-   Propagate parent tag context downward to improve runtime by [@&#8203;chrispy-snps](https://github.com/chrispy-snps) in https://github.com/matthewwithanm/python-markdownify/pull/191
-   Avoid stripping nonbreaking spaces by [@&#8203;jsm28](https://github.com/jsm28) in https://github.com/matthewwithanm/python-markdownify/pull/188
-   Escape right square brackets by [@&#8203;jsm28](https://github.com/jsm28) in https://github.com/matthewwithanm/python-markdownify/pull/187
-   Rename regex pattern variables by [@&#8203;chrispy-snps](https://github.com/chrispy-snps) in https://github.com/matthewwithanm/python-markdownify/pull/195
-   Use a conversion function cache to improve runtime by [@&#8203;chrispy-snps](https://github.com/chrispy-snps) in https://github.com/matthewwithanm/python-markdownify/pull/196
-   Use compiled regex patterns for escaping to improve runtime by [@&#8203;chrispy-snps](https://github.com/chrispy-snps) in https://github.com/matthewwithanm/python-markdownify/pull/194

#### New Contributors

-   [@&#8203;SomeBottle](https://github.com/SomeBottle) made their first contribution in https://github.com/matthewwithanm/python-markdownify/pull/161
-   [@&#8203;Fess-AKA-DeadMonk](https://github.com/Fess-AKA-DeadMonk) made their first contribution in https://github.com/matthewwithanm/python-markdownify/pull/136

**Full Changelog**: https://github.com/matthewwithanm/python-markdownify/compare/0.14.1...1.0.0

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMTEuMCIsInVwZGF0ZWRJblZlciI6IjM5LjIxMS4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: #81
Co-authored-by: Renovate <renovate@csw.im>
Co-committed-by: Renovate <renovate@csw.im>
2025-03-29 08:24:56 -04:00
6b3398c3d0
fix(deps): update dependency aiosqlite to v0.21.0 (#78)
Some checks failed
Actions / Lint Code (Ruff & Pylint) (push) Has been cancelled
Actions / Build Documentation (MkDocs) (push) Has been cancelled
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [aiosqlite](https://github.com/omnilib/aiosqlite) | project.dependencies | minor | `==0.20.0` -> `==0.21.0` |

---

### Release Notes

<details>
<summary>omnilib/aiosqlite (aiosqlite)</summary>

### [`v0.21.0`](https://github.com/omnilib/aiosqlite/blob/HEAD/CHANGELOG.md#v0210)

[Compare Source](https://github.com/omnilib/aiosqlite/compare/v0.20.0...v0.21.0)

Maintenance release

-   Fix: close connection correctly when BaseException raised in connection ([#&#8203;317](https://github.com/omnilib/aiosqlite/issues/317))
-   Metadata improvements
-   Tested and supported on Python 3.13
-   Drop support for Python 3.8
-   Drop testing on PyPy

```text
$ git shortlog -s v0.20.0...v0.21.0
     6	Amethyst Reese
     1	Gabriel
     1	Stanley Kudrow
    11	dependabot[bot]
```

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMTEuMCIsInVwZGF0ZWRJblZlciI6IjM5LjIxMS4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: #78
Co-authored-by: Renovate <renovate@csw.im>
Co-committed-by: Renovate <renovate@csw.im>
2025-03-29 08:24:45 -04:00
1701540fa7
chore(deps): update dependency mkdocs-git-revision-date-localized-plugin to v1.4.5 (#74)
Some checks failed
Actions / Lint Code (Ruff & Pylint) (push) Has been cancelled
Actions / Build Documentation (MkDocs) (push) Has been cancelled
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [mkdocs-git-revision-date-localized-plugin](https://github.com/timvink/mkdocs-git-revision-date-localized-plugin) | dependency-groups | minor | `==1.3.0` -> `==1.4.5` |

---

### Release Notes

<details>
<summary>timvink/mkdocs-git-revision-date-localized-plugin (mkdocs-git-revision-date-localized-plugin)</summary>

### [`v1.4.5`](https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/releases/tag/v1.4.5): revision-date-localized v1.4.5

[Compare Source](https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/compare/v1.4.4...v1.4.5)

#### What's Changed

-   Fix monorepo compability for techdocs by [@&#8203;timo-reymann](https://github.com/timo-reymann) in https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/pull/174

#### New Contributors

-   [@&#8203;timo-reymann](https://github.com/timo-reymann) made their first contribution in https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/pull/174

**Full Changelog**: https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/compare/v1.4.4...v1.4.5

### [`v1.4.4`](https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/releases/tag/v1.4.4)

[Compare Source](https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/compare/v1.4.2...v1.4.4)

#### What's Changed

-   Fix regression causing verbose output by [@&#8203;timvink](https://github.com/timvink) in https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/pull/171

**Full Changelog**: https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/compare/v1.4.3...v1.4.4

### [`v1.4.2`](https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/compare/v1.4.1...v1.4.2)

[Compare Source](https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/compare/v1.4.1...v1.4.2)

### [`v1.4.1`](https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/releases/tag/v1.4.1): revision-date-localized v1.4.1

[Compare Source](https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/compare/v1.4.0...v1.4.1)

#### What's Changed

-   Fix monorepo compatibility by [@&#8203;timvink](https://github.com/timvink) in https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/pull/168

**Full Changelog**: https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/compare/v1.4.0...v1.4.1

### [`v1.4.0`](https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/releases/tag/v1.4.0): revision-date-localized v1.4.0

[Compare Source](https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/compare/v1.3.0...v1.4.0)

#### What's Changed

##### New features

-   New option `enable_parallel_processing` (default: True) for a 2-5x speedup by [@&#8203;kunickiaj](https://github.com/kunickiaj) in https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/pull/116, and by [@&#8203;timvink](https://github.com/timvink) in https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/pull/166
-   New option `ignored_commits_file` enabling you to to ignore specific commits by [@&#8203;allanlw](https://github.com/allanlw) in [#&#8203;114](https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/issues/114), and [@&#8203;timvink](https://github.com/timvink) in https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/pull/157
-   Support 5 letter locale settings by [@&#8203;timvink](https://github.com/timvink) in https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/pull/165
-   Git hash and git tag information now also exposed to developers by [@&#8203;timvink](https://github.com/timvink) in https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/pull/162
-   Enable seeing datetime when hovering over element by adding datetime string to span element as title by [@&#8203;mschoettle](https://github.com/mschoettle) in https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/pull/152

##### Enhancements

-   Catch errors explicitly by [@&#8203;timvink](https://github.com/timvink) in https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/pull/159
-   Raise error when dubious git ownership is at fault by [@&#8203;timvink](https://github.com/timvink) in https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/pull/161
-   Ensure creation date is never later than revision date by [@&#8203;timvink](https://github.com/timvink) in https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/pull/163
-   When using `type: custom`, the `locale` is now properly respective by [@&#8203;timvink](https://github.com/timvink) in https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/pull/165

##### Documentation

-   fix: broken link by [@&#8203;sheeeng](https://github.com/sheeeng) in https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/pull/153
-   Add tips to speed up builds by [@&#8203;timvink](https://github.com/timvink) in https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/pull/160

#### New Contributors

-   [@&#8203;sheeeng](https://github.com/sheeeng) made their first contribution in https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/pull/153
-   [@&#8203;mschoettle](https://github.com/mschoettle) made their first contribution in https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/pull/152

**Full Changelog**: https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/compare/v1.3.0...v1.4.0

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMTEuMCIsInVwZGF0ZWRJblZlciI6IjM5LjIxMS4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: #74
Co-authored-by: Renovate <renovate@csw.im>
Co-committed-by: Renovate <renovate@csw.im>
2025-03-29 08:24:14 -04:00
3d09f36b56
fix(deps): update dependency websockets to v15 (#83)
All checks were successful
Actions / Lint Code (Ruff & Pylint) (push) Successful in 55s
Actions / Build Documentation (MkDocs) (push) Successful in 1m7s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [websockets](https://github.com/python-websockets/websockets) ([changelog](https://websockets.readthedocs.io/en/stable/project/changelog.html)) | project.dependencies | major | `==14.2` -> `==15.0.1` |

---

### Release Notes

<details>
<summary>python-websockets/websockets (websockets)</summary>

### [`v15.0.1`](https://github.com/python-websockets/websockets/releases/tag/15.0.1)

[Compare Source](https://github.com/python-websockets/websockets/compare/15.0...15.0.1)

See https://websockets.readthedocs.io/en/stable/project/changelog.html for details.

### [`v15.0`](https://github.com/python-websockets/websockets/releases/tag/15.0)

[Compare Source](https://github.com/python-websockets/websockets/compare/14.2...15.0)

See https://websockets.readthedocs.io/en/stable/project/changelog.html for details.

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMTEuMCIsInVwZGF0ZWRJblZlciI6IjM5LjIxMS4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: #83
Co-authored-by: Renovate <renovate@csw.im>
Co-committed-by: Renovate <renovate@csw.im>
2025-03-29 08:20:45 -04:00
19536a2351
fix(deps): update dependency beautifulsoup4 to v4.13.3 (#79)
Some checks failed
Actions / Lint Code (Ruff & Pylint) (push) Has been cancelled
Actions / Build Documentation (MkDocs) (push) Has been cancelled
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| beautifulsoup4 ([changelog](https://git.launchpad.net/beautifulsoup/tree/CHANGELOG)) | project.dependencies | minor | `==4.12.3` -> `==4.13.3` |

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMTEuMCIsInVwZGF0ZWRJblZlciI6IjM5LjIxMS4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: #79
Co-authored-by: Renovate <renovate@csw.im>
Co-committed-by: Renovate <renovate@csw.im>
2025-03-29 08:20:26 -04:00
4a41bbc2b2
chore(deps): update dependency ruff to v0.11.2 (#77)
Some checks failed
Actions / Build Documentation (MkDocs) (push) Has been cancelled
Actions / Lint Code (Ruff & Pylint) (push) Has been cancelled
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [ruff](https://docs.astral.sh/ruff) ([source](https://github.com/astral-sh/ruff), [changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)) | tool.uv.dev-dependencies | minor | `==0.9.3` -> `==0.11.2` |

---

### Release Notes

<details>
<summary>astral-sh/ruff (ruff)</summary>

### [`v0.11.2`](https://github.com/astral-sh/ruff/blob/HEAD/CHANGELOG.md#0112)

[Compare Source](https://github.com/astral-sh/ruff/compare/0.11.1...0.11.2)

##### Preview features

-   \[syntax-errors] Fix false-positive syntax errors emitted for annotations on variadic parameters before Python 3.11 ([#&#8203;16878](https://github.com/astral-sh/ruff/pull/16878))

### [`v0.11.1`](https://github.com/astral-sh/ruff/blob/HEAD/CHANGELOG.md#0111)

[Compare Source](https://github.com/astral-sh/ruff/compare/0.11.0...0.11.1)

##### Preview features

-   \[`airflow`] Add `chain`, `chain_linear` and `cross_downstream` for `AIR302` ([#&#8203;16647](https://github.com/astral-sh/ruff/pull/16647))
-   \[syntax-errors] Improve error message and range for pre-PEP-614 decorator syntax errors ([#&#8203;16581](https://github.com/astral-sh/ruff/pull/16581))
-   \[syntax-errors] PEP 701 f-strings before Python 3.12 ([#&#8203;16543](https://github.com/astral-sh/ruff/pull/16543))
-   \[syntax-errors] Parenthesized context managers before Python 3.9 ([#&#8203;16523](https://github.com/astral-sh/ruff/pull/16523))
-   \[syntax-errors] Star annotations before Python 3.11 ([#&#8203;16545](https://github.com/astral-sh/ruff/pull/16545))
-   \[syntax-errors] Star expression in index before Python 3.11 ([#&#8203;16544](https://github.com/astral-sh/ruff/pull/16544))
-   \[syntax-errors] Unparenthesized assignment expressions in sets and indexes ([#&#8203;16404](https://github.com/astral-sh/ruff/pull/16404))

##### Bug fixes

-   Server: Allow `FixAll` action in presence of version-specific syntax errors ([#&#8203;16848](https://github.com/astral-sh/ruff/pull/16848))
-   \[`flake8-bandit`] Allow raw strings in `suspicious-mark-safe-usage` (`S308`) [#&#8203;16702](https://github.com/astral-sh/ruff/issues/16702) ([#&#8203;16770](https://github.com/astral-sh/ruff/pull/16770))
-   \[`refurb`] Avoid panicking `unwrap` in `verbose-decimal-constructor` (`FURB157`) ([#&#8203;16777](https://github.com/astral-sh/ruff/pull/16777))
-   \[`refurb`] Fix starred expressions fix (`FURB161`) ([#&#8203;16550](https://github.com/astral-sh/ruff/pull/16550))
-   Fix `--statistics` reporting for unsafe fixes ([#&#8203;16756](https://github.com/astral-sh/ruff/pull/16756))

##### Rule changes

-   \[`flake8-executables`] Allow `uv run` in shebang line for `shebang-missing-python` (`EXE003`) ([#&#8203;16849](https://github.com/astral-sh/ruff/pull/16849),[#&#8203;16855](https://github.com/astral-sh/ruff/pull/16855))

##### CLI

-   Add `--exit-non-zero-on-format` ([#&#8203;16009](https://github.com/astral-sh/ruff/pull/16009))

##### Documentation

-   Update Ruff tutorial to avoid non-existent fix in `__init__.py` ([#&#8203;16818](https://github.com/astral-sh/ruff/pull/16818))
-   \[`flake8-gettext`] Swap `format-` and `printf-in-get-text-func-call` examples (`INT002`, `INT003`) ([#&#8203;16769](https://github.com/astral-sh/ruff/pull/16769))

### [`v0.11.0`](https://github.com/astral-sh/ruff/blob/HEAD/CHANGELOG.md#0110)

[Compare Source](https://github.com/astral-sh/ruff/compare/0.10.0...0.11.0)

This is a follow-up to release 0.10.0. Because of a mistake in the release process, the `requires-python` inference changes were not included in that release. Ruff 0.11.0 now includes this change as well as the stabilization of the preview behavior for `PGH004`.

##### Breaking changes

-   **Changes to how the Python version is inferred when a `target-version` is not specified** ([#&#8203;16319](https://github.com/astral-sh/ruff/pull/16319))

    In previous versions of Ruff, you could specify your Python version with:

    -   The `target-version` option in a `ruff.toml` file or the `[tool.ruff]` section of a pyproject.toml file.
    -   The `project.requires-python` field in a `pyproject.toml` file with a `[tool.ruff]` section.

    These options worked well in most cases, and are still recommended for fine control of the Python version. However, because of the way Ruff discovers config files, `pyproject.toml` files without a `[tool.ruff]` section would be ignored, including the `requires-python` setting. Ruff would then use the default Python version (3.9 as of this writing) instead, which is surprising when you've attempted to request another version.

    In v0.10, config discovery has been updated to address this issue:

    -   If Ruff finds a `ruff.toml` file without a `target-version`, it will check
        for a `pyproject.toml` file in the same directory and respect its
        `requires-python` version, even if it does not contain a `[tool.ruff]`
        section.
    -   If Ruff finds a user-level configuration, the `requires-python` field of the closest `pyproject.toml` in a parent directory will take precedence.
    -   If there is no config file (`ruff.toml`or `pyproject.toml` with a
        `[tool.ruff]` section) in the directory of the file being checked, Ruff will
        search for the closest `pyproject.toml` in the parent directories and use its
        `requires-python` setting.

##### Stabilization

The following behaviors have been stabilized:

-   [`blanket-noqa`](https://docs.astral.sh/ruff/rules/blanket-noqa/) (`PGH004`): Also detect blanked file-level noqa comments (and not just line level comments).

##### Preview features

-   \[syntax-errors] Tuple unpacking in `for` statement iterator clause before Python 3.9 ([#&#8203;16558](https://github.com/astral-sh/ruff/pull/16558))

### [`v0.10.0`](https://github.com/astral-sh/ruff/blob/HEAD/CHANGELOG.md#0100)

[Compare Source](https://github.com/astral-sh/ruff/compare/0.9.10...0.10.0)

Check out the [blog post](https://astral.sh/blog/ruff-v0.10.0) for a migration guide and overview of the changes!

##### Breaking changes

See also, the "Remapped rules" section which may result in disabled rules.

-   **Changes to how the Python version is inferred when a `target-version` is not specified** ([#&#8203;16319](https://github.com/astral-sh/ruff/pull/16319))

    Because of a mistake in the release process, the `requires-python` inference changes are not included in this release and instead shipped as part of 0.11.0.
    You can find a description of this change in the 0.11.0 section.

-   **Updated `TYPE_CHECKING` behavior** ([#&#8203;16669](https://github.com/astral-sh/ruff/pull/16669))

    Previously, Ruff only recognized typechecking blocks that tested the `typing.TYPE_CHECKING` symbol. Now, Ruff recognizes any local variable named `TYPE_CHECKING`. This release also removes support for the legacy `if 0:` and `if False:` typechecking checks. Use a local `TYPE_CHECKING` variable instead.

-   **More robust noqa parsing** ([#&#8203;16483](https://github.com/astral-sh/ruff/pull/16483))

    The syntax for both file-level and in-line suppression comments has been unified and made more robust to certain errors. In most cases, this will result in more suppression comments being read by Ruff, but there are a few instances where previously read comments will now log an error to the user instead. Please refer to the documentation on [*Error suppression*](https://docs.astral.sh/ruff/linter/#error-suppression) for the full specification.

-   **Avoid unnecessary parentheses around with statements with a single context manager and a trailing comment** ([#&#8203;14005](https://github.com/astral-sh/ruff/pull/14005))

    This change fixes a bug in the formatter where it introduced unnecessary parentheses around with statements with a single context manager and a trailing comment. This change may result in a change in formatting for some users.

-   **Bump alpine default tag to 3.21 for derived Docker images** ([#&#8203;16456](https://github.com/astral-sh/ruff/pull/16456))

    Alpine 3.21 was released in Dec 2024 and is used in the official Alpine-based Python images. Now the ruff:alpine image will use 3.21 instead of 3.20 and ruff:alpine3.20 will no longer be updated.

##### Deprecated Rules

The following rules have been deprecated:

-   [`non-pep604-isinstance`](https://docs.astral.sh/ruff/rules/non-pep604-isinstance/) (`UP038`)
-   [`suspicious-xmle-tree-usage`](https://docs.astral.sh/ruff/rules/suspicious-xmle-tree-usage/) (`S320`)

##### Remapped rules

The following rules have been remapped to new rule codes:

-   \[`unsafe-markup-use`]: `RUF035` to `S704`

##### Stabilization

The following rules have been stabilized and are no longer in preview:

-   [`batched-without-explicit-strict`](https://docs.astral.sh/ruff/rules/batched-without-explicit-strict) (`B911`)
-   [`unnecessary-dict-comprehension-for-iterable`](https://docs.astral.sh/ruff/rules/unnecessary-dict-comprehension-for-iterable) (`C420`)
-   [`datetime-min-max`](https://docs.astral.sh/ruff/rules/datetime-min-max) (`DTZ901`)
-   [`fast-api-unused-path-parameter`](https://docs.astral.sh/ruff/rules/fast-api-unused-path-parameter) (`FAST003`)
-   [`root-logger-call`](https://docs.astral.sh/ruff/rules/root-logger-call) (`LOG015`)
-   [`len-test`](https://docs.astral.sh/ruff/rules/len-test) (`PLC1802`)
-   [`shallow-copy-environ`](https://docs.astral.sh/ruff/rules/shallow-copy-environ) (`PLW1507`)
-   [`os-listdir`](https://docs.astral.sh/ruff/rules/os-listdir) (`PTH208`)
-   [`invalid-pathlib-with-suffix`](https://docs.astral.sh/ruff/rules/invalid-pathlib-with-suffix) (`PTH210`)
-   [`invalid-assert-message-literal-argument`](https://docs.astral.sh/ruff/rules/invalid-assert-message-literal-argument) (`RUF040`)
-   [`unnecessary-nested-literal`](https://docs.astral.sh/ruff/rules/unnecessary-nested-literal) (`RUF041`)
-   [`unnecessary-cast-to-int`](https://docs.astral.sh/ruff/rules/unnecessary-cast-to-int) (`RUF046`)
-   [`map-int-version-parsing`](https://docs.astral.sh/ruff/rules/map-int-version-parsing) (`RUF048`)
-   [`if-key-in-dict-del`](https://docs.astral.sh/ruff/rules/if-key-in-dict-del) (`RUF051`)
-   [`unsafe-markup-use`](https://docs.astral.sh/ruff/rules/unsafe-markup-use) (`S704`). This rule has also been renamed from `RUF035`.
-   [`split-static-string`](https://docs.astral.sh/ruff/rules/split-static-string) (`SIM905`)
-   [`runtime-cast-value`](https://docs.astral.sh/ruff/rules/runtime-cast-value) (`TC006`)
-   [`unquoted-type-alias`](https://docs.astral.sh/ruff/rules/unquoted-type-alias) (`TC007`)
-   [`non-pep646-unpack`](https://docs.astral.sh/ruff/rules/non-pep646-unpack) (`UP044`)

The following behaviors have been stabilized:

-   [`bad-staticmethod-argument`](https://docs.astral.sh/ruff/rules/bad-staticmethod-argument/) (`PLW0211`) [`invalid-first-argument-name-for-class-method`](https://docs.astral.sh/ruff/rules/invalid-first-argument-name-for-class-method/) (`N804`): `__new__` methods are now no longer flagged by `invalid-first-argument-name-for-class-method` (`N804`) but instead by `bad-staticmethod-argument` (`PLW0211`)
-   [`bad-str-strip-call`](https://docs.astral.sh/ruff/rules/bad-str-strip-call/) (`PLE1310`): The rule now applies to objects which are known to have type `str` or `bytes`.
-   [`custom-type-var-for-self`](https://docs.astral.sh/ruff/rules/custom-type-var-for-self/) (`PYI019`): More accurate detection of custom `TypeVars` replaceable by `Self`. The range of the diagnostic is now the full function header rather than just the return annotation.
-   [`invalid-argument-name`](https://docs.astral.sh/ruff/rules/invalid-argument-name/) (`N803`): Ignore argument names of functions decorated with `typing.override`
-   [`invalid-envvar-default`](https://docs.astral.sh/ruff/rules/invalid-envvar-default/) (`PLW1508`): Detect default value arguments to `os.environ.get` with invalid type.
-   [`pytest-raises-with-multiple-statements`](https://docs.astral.sh/ruff/rules/pytest-raises-with-multiple-statements/) (`PT012`) [`pytest-warns-with-multiple-statements`](https://docs.astral.sh/ruff/rules/pytest-warns-with-multiple-statements/) (`PT031`): Allow `for` statements with an empty body in `pytest.raises` and `pytest.warns` `with` statements.
-   [`redundant-open-modes`](https://docs.astral.sh/ruff/rules/redundant-open-modes/) (`UP015`): The diagnostic range is now the range of the redundant mode argument where it previously was the range of the entire open call. You may have to replace your `noqa` comments when suppressing `UP015`.
-   [`stdlib-module-shadowing`](https://docs.astral.sh/ruff/rules/stdlib-module-shadowing/) (`A005`): Changes the default value of `lint.flake8-builtins.strict-checking` from `true` to `false`.
-   [`type-none-comparison`](https://docs.astral.sh/ruff/rules/type-none-comparison/) (`FURB169`): Now also recognizes `type(expr) is type(None)` comparisons where `expr` isn't a name expression.

The following fixes or improvements to fixes have been stabilized:

-   [`repeated-equality-comparison`](https://docs.astral.sh/ruff/rules/repeated-equality-comparison/) (`PLR1714`) ([#&#8203;16685](https://github.com/astral-sh/ruff/pull/16685))
-   [`needless-bool`](https://docs.astral.sh/ruff/rules/needless-bool/) (`SIM103`) ([#&#8203;16684](https://github.com/astral-sh/ruff/pull/16684))
-   [`unused-private-type-var`](https://docs.astral.sh/ruff/rules/unused-private-type-var/) (`PYI018`) ([#&#8203;16682](https://github.com/astral-sh/ruff/pull/16682))

##### Server

-   Remove logging output for `ruff.printDebugInformation` ([#&#8203;16617](https://github.com/astral-sh/ruff/pull/16617))

##### Configuration

-   \[`flake8-builtins`] Deprecate the `builtins-` prefixed options in favor of the unprefixed options (e.g. `builtins-allowed-modules` is now deprecated in favor of `allowed-modules`) ([#&#8203;16092](https://github.com/astral-sh/ruff/pull/16092))

##### Bug fixes

-   \[flake8-bandit] Fix mixed-case hash algorithm names (S324) ([#&#8203;16552](https://github.com/astral-sh/ruff/pull/16552))

##### CLI

-   \[ruff] Fix `last_tag`/`commits_since_last_tag` for `version` command ([#&#8203;16686](https://github.com/astral-sh/ruff/pull/16686))

### [`v0.9.10`](https://github.com/astral-sh/ruff/blob/HEAD/CHANGELOG.md#0910)

[Compare Source](https://github.com/astral-sh/ruff/compare/0.9.9...0.9.10)

##### Preview features

-   \[`ruff`] Add new rule `RUF059`: Unused unpacked assignment ([#&#8203;16449](https://github.com/astral-sh/ruff/pull/16449))
-   \[`syntax-errors`] Detect assignment expressions before Python 3.8 ([#&#8203;16383](https://github.com/astral-sh/ruff/pull/16383))
-   \[`syntax-errors`] Named expressions in decorators before Python 3.9 ([#&#8203;16386](https://github.com/astral-sh/ruff/pull/16386))
-   \[`syntax-errors`] Parenthesized keyword argument names after Python 3.8 ([#&#8203;16482](https://github.com/astral-sh/ruff/pull/16482))
-   \[`syntax-errors`] Positional-only parameters before Python 3.8 ([#&#8203;16481](https://github.com/astral-sh/ruff/pull/16481))
-   \[`syntax-errors`] Tuple unpacking in `return` and `yield` before Python 3.8 ([#&#8203;16485](https://github.com/astral-sh/ruff/pull/16485))
-   \[`syntax-errors`] Type parameter defaults before Python 3.13 ([#&#8203;16447](https://github.com/astral-sh/ruff/pull/16447))
-   \[`syntax-errors`] Type parameter lists before Python 3.12 ([#&#8203;16479](https://github.com/astral-sh/ruff/pull/16479))
-   \[`syntax-errors`] `except*` before Python 3.11 ([#&#8203;16446](https://github.com/astral-sh/ruff/pull/16446))
-   \[`syntax-errors`] `type` statements before Python 3.12 ([#&#8203;16478](https://github.com/astral-sh/ruff/pull/16478))

##### Bug fixes

-   Escape template filenames in glob patterns in configuration ([#&#8203;16407](https://github.com/astral-sh/ruff/pull/16407))
-   \[`flake8-simplify`] Exempt unittest context methods for `SIM115` rule ([#&#8203;16439](https://github.com/astral-sh/ruff/pull/16439))
-   Formatter: Fix syntax error location in notebooks ([#&#8203;16499](https://github.com/astral-sh/ruff/pull/16499))
-   \[`pyupgrade`] Do not offer fix when at least one target is `global`/`nonlocal` (`UP028`) ([#&#8203;16451](https://github.com/astral-sh/ruff/pull/16451))
-   \[`flake8-builtins`] Ignore variables matching module attribute names (`A001`) ([#&#8203;16454](https://github.com/astral-sh/ruff/pull/16454))
-   \[`pylint`] Convert `code` keyword argument to a positional argument in fix for (`PLR1722`) ([#&#8203;16424](https://github.com/astral-sh/ruff/pull/16424))

##### CLI

-   Move rule code from `description` to `check_name` in GitLab output serializer ([#&#8203;16437](https://github.com/astral-sh/ruff/pull/16437))

##### Documentation

-   \[`pydocstyle`] Clarify that `D417` only checks docstrings with an arguments section ([#&#8203;16494](https://github.com/astral-sh/ruff/pull/16494))

### [`v0.9.9`](https://github.com/astral-sh/ruff/blob/HEAD/CHANGELOG.md#099)

[Compare Source](https://github.com/astral-sh/ruff/compare/0.9.8...0.9.9)

##### Preview features

-   Fix caching of unsupported-syntax errors ([#&#8203;16425](https://github.com/astral-sh/ruff/pull/16425))

##### Bug fixes

-   Only show unsupported-syntax errors in editors when preview mode is enabled ([#&#8203;16429](https://github.com/astral-sh/ruff/pull/16429))

### [`v0.9.8`](https://github.com/astral-sh/ruff/blob/HEAD/CHANGELOG.md#098)

[Compare Source](https://github.com/astral-sh/ruff/compare/0.9.7...0.9.8)

##### Preview features

-   Start detecting version-related syntax errors in the parser ([#&#8203;16090](https://github.com/astral-sh/ruff/pull/16090))

##### Rule changes

-   \[`pylint`] Mark fix unsafe (`PLW1507`) ([#&#8203;16343](https://github.com/astral-sh/ruff/pull/16343))
-   \[`pylint`] Catch `case np.nan`/`case math.nan` in `match` statements (`PLW0177`) ([#&#8203;16378](https://github.com/astral-sh/ruff/pull/16378))
-   \[`ruff`] Add more Pydantic models variants to the list of default copy semantics (`RUF012`) ([#&#8203;16291](https://github.com/astral-sh/ruff/pull/16291))

##### Server

-   Avoid indexing the project if `configurationPreference` is `editorOnly` ([#&#8203;16381](https://github.com/astral-sh/ruff/pull/16381))
-   Avoid unnecessary info at non-trace server log level ([#&#8203;16389](https://github.com/astral-sh/ruff/pull/16389))
-   Expand `ruff.configuration` to allow inline config ([#&#8203;16296](https://github.com/astral-sh/ruff/pull/16296))
-   Notify users for invalid client settings ([#&#8203;16361](https://github.com/astral-sh/ruff/pull/16361))

##### Configuration

-   Add `per-file-target-version` option ([#&#8203;16257](https://github.com/astral-sh/ruff/pull/16257))

##### Bug fixes

-   \[`refurb`] Do not consider docstring(s) (`FURB156`) ([#&#8203;16391](https://github.com/astral-sh/ruff/pull/16391))
-   \[`flake8-self`] Ignore attribute accesses on instance-like variables (`SLF001`) ([#&#8203;16149](https://github.com/astral-sh/ruff/pull/16149))
-   \[`pylint`] Fix false positives, add missing methods, and support positional-only parameters (`PLE0302`) ([#&#8203;16263](https://github.com/astral-sh/ruff/pull/16263))
-   \[`flake8-pyi`] Mark `PYI030` fix unsafe when comments are deleted ([#&#8203;16322](https://github.com/astral-sh/ruff/pull/16322))

##### Documentation

-   Fix example for `S611` ([#&#8203;16316](https://github.com/astral-sh/ruff/pull/16316))
-   Normalize inconsistent markdown headings in docstrings ([#&#8203;16364](https://github.com/astral-sh/ruff/pull/16364))
-   Document MSRV policy ([#&#8203;16384](https://github.com/astral-sh/ruff/pull/16384))

### [`v0.9.7`](https://github.com/astral-sh/ruff/blob/HEAD/CHANGELOG.md#097)

[Compare Source](https://github.com/astral-sh/ruff/compare/0.9.6...0.9.7)

##### Preview features

-   Consider `__new__` methods as special function type for enforcing class method or static method rules ([#&#8203;13305](https://github.com/astral-sh/ruff/pull/13305))
-   \[`airflow`] Improve the internal logic to differentiate deprecated symbols (`AIR303`) ([#&#8203;16013](https://github.com/astral-sh/ruff/pull/16013))
-   \[`refurb`] Manual timezone monkeypatching (`FURB162`) ([#&#8203;16113](https://github.com/astral-sh/ruff/pull/16113))
-   \[`ruff`] Implicit class variable in dataclass (`RUF045`) ([#&#8203;14349](https://github.com/astral-sh/ruff/pull/14349))
-   \[`ruff`] Skip singleton starred expressions for `incorrectly-parenthesized-tuple-in-subscript` (`RUF031`) ([#&#8203;16083](https://github.com/astral-sh/ruff/pull/16083))
-   \[`refurb`] Check for subclasses includes subscript expressions (`FURB189`) ([#&#8203;16155](https://github.com/astral-sh/ruff/pull/16155))

##### Rule changes

-   \[`flake8-debugger`] Also flag `sys.breakpointhook` and `sys.__breakpointhook__` (`T100`) ([#&#8203;16191](https://github.com/astral-sh/ruff/pull/16191))
-   \[`pycodestyle`] Exempt `site.addsitedir(...)` calls (`E402`) ([#&#8203;16251](https://github.com/astral-sh/ruff/pull/16251))

##### Formatter

-   Fix unstable formatting of trailing end-of-line comments of parenthesized attribute values ([#&#8203;16187](https://github.com/astral-sh/ruff/pull/16187))

##### Server

-   Fix handling of requests received after shutdown message ([#&#8203;16262](https://github.com/astral-sh/ruff/pull/16262))
-   Ignore `source.organizeImports.ruff` and `source.fixAll.ruff` code actions for a notebook cell ([#&#8203;16154](https://github.com/astral-sh/ruff/pull/16154))
-   Include document specific debug info for `ruff.printDebugInformation` ([#&#8203;16215](https://github.com/astral-sh/ruff/pull/16215))
-   Update server to return the debug info as string with `ruff.printDebugInformation` ([#&#8203;16214](https://github.com/astral-sh/ruff/pull/16214))

##### CLI

-   Warn on invalid `noqa` even when there are no diagnostics ([#&#8203;16178](https://github.com/astral-sh/ruff/pull/16178))
-   Better error messages while loading configuration `extend`s ([#&#8203;15658](https://github.com/astral-sh/ruff/pull/15658))

##### Bug fixes

-   \[`flake8-comprehensions`] Handle trailing comma in `C403` fix ([#&#8203;16110](https://github.com/astral-sh/ruff/pull/16110))
-   \[`flake8-pyi`] Avoid flagging `custom-typevar-for-self` on metaclass methods (`PYI019`) ([#&#8203;16141](https://github.com/astral-sh/ruff/pull/16141))
-   \[`pydocstyle`] Handle arguments with the same names as sections (`D417`) ([#&#8203;16011](https://github.com/astral-sh/ruff/pull/16011))
-   \[`pylint`] Correct ordering of arguments in fix for `if-stmt-min-max` (`PLR1730`) ([#&#8203;16080](https://github.com/astral-sh/ruff/pull/16080))
-   \[`pylint`] Do not offer fix for raw strings (`PLE251`) ([#&#8203;16132](https://github.com/astral-sh/ruff/pull/16132))
-   \[`pyupgrade`] Do not upgrade functional `TypedDicts` with private field names to the class-based syntax (`UP013`) ([#&#8203;16219](https://github.com/astral-sh/ruff/pull/16219))
-   \[`pyupgrade`] Handle micro version numbers correctly (`UP036`) ([#&#8203;16091](https://github.com/astral-sh/ruff/pull/16091))
-   \[`pyupgrade`] Unwrap unary expressions correctly (`UP018`) ([#&#8203;15919](https://github.com/astral-sh/ruff/pull/15919))
-   \[`refurb`] Correctly handle lengths of literal strings in `slice-to-remove-prefix-or-suffix` (`FURB188`) ([#&#8203;16237](https://github.com/astral-sh/ruff/pull/16237))
-   \[`ruff`] Skip `RUF001` diagnostics when visiting string type definitions ([#&#8203;16122](https://github.com/astral-sh/ruff/pull/16122))

##### Documentation

-   Add FAQ entry for `source.*` code actions in Notebook ([#&#8203;16212](https://github.com/astral-sh/ruff/pull/16212))
-   Add `SECURITY.md` ([#&#8203;16224](https://github.com/astral-sh/ruff/pull/16224))

### [`v0.9.6`](https://github.com/astral-sh/ruff/blob/HEAD/CHANGELOG.md#096)

[Compare Source](https://github.com/astral-sh/ruff/compare/0.9.5...0.9.6)

##### Preview features

-   \[`airflow`] Add `external_task.{ExternalTaskMarker, ExternalTaskSensor}` for `AIR302` ([#&#8203;16014](https://github.com/astral-sh/ruff/pull/16014))
-   \[`flake8-builtins`] Make strict module name comparison optional (`A005`) ([#&#8203;15951](https://github.com/astral-sh/ruff/pull/15951))
-   \[`flake8-pyi`] Extend fix to Python <= 3.9 for `redundant-none-literal` (`PYI061`) ([#&#8203;16044](https://github.com/astral-sh/ruff/pull/16044))
-   \[`pylint`] Also report when the object isn't a literal (`PLE1310`) ([#&#8203;15985](https://github.com/astral-sh/ruff/pull/15985))
-   \[`ruff`] Implement `indented-form-feed` (`RUF054`) ([#&#8203;16049](https://github.com/astral-sh/ruff/pull/16049))
-   \[`ruff`] Skip type definitions for `missing-f-string-syntax` (`RUF027`) ([#&#8203;16054](https://github.com/astral-sh/ruff/pull/16054))

##### Rule changes

-   \[`flake8-annotations`] Correct syntax for `typing.Union` in suggested return type fixes for `ANN20x` rules ([#&#8203;16025](https://github.com/astral-sh/ruff/pull/16025))
-   \[`flake8-builtins`] Match upstream module name comparison (`A005`) ([#&#8203;16006](https://github.com/astral-sh/ruff/pull/16006))
-   \[`flake8-comprehensions`] Detect overshadowed `list`/`set`/`dict`, ignore variadics and named expressions (`C417`) ([#&#8203;15955](https://github.com/astral-sh/ruff/pull/15955))
-   \[`flake8-pie`] Remove following comma correctly when the unpacked dictionary is empty (`PIE800`) ([#&#8203;16008](https://github.com/astral-sh/ruff/pull/16008))
-   \[`flake8-simplify`] Only trigger `SIM401` on known dictionaries ([#&#8203;15995](https://github.com/astral-sh/ruff/pull/15995))
-   \[`pylint`] Do not report calls when object type and argument type mismatch, remove custom escape handling logic (`PLE1310`) ([#&#8203;15984](https://github.com/astral-sh/ruff/pull/15984))
-   \[`pyupgrade`] Comments within parenthesized value ranges should not affect applicability (`UP040`) ([#&#8203;16027](https://github.com/astral-sh/ruff/pull/16027))
-   \[`pyupgrade`] Don't introduce invalid syntax when upgrading old-style type aliases with parenthesized multiline values (`UP040`) ([#&#8203;16026](https://github.com/astral-sh/ruff/pull/16026))
-   \[`pyupgrade`] Ensure we do not rename two type parameters to the same name (`UP049`) ([#&#8203;16038](https://github.com/astral-sh/ruff/pull/16038))
-   \[`pyupgrade`] \[`ruff`] Don't apply renamings if the new name is shadowed in a scope of one of the references to the binding (`UP049`, `RUF052`) ([#&#8203;16032](https://github.com/astral-sh/ruff/pull/16032))
-   \[`ruff`] Update `RUF009` to behave similar to `B008` and ignore attributes with immutable types ([#&#8203;16048](https://github.com/astral-sh/ruff/pull/16048))

##### Server

-   Root exclusions in the server to project root ([#&#8203;16043](https://github.com/astral-sh/ruff/pull/16043))

##### Bug fixes

-   \[`flake8-datetime`] Ignore `.replace()` calls while looking for `.astimezone` ([#&#8203;16050](https://github.com/astral-sh/ruff/pull/16050))
-   \[`flake8-type-checking`] Avoid `TC004` false positive where the runtime definition is provided by `__getattr__` ([#&#8203;16052](https://github.com/astral-sh/ruff/pull/16052))

##### Documentation

-   Improve `ruff-lsp` migration document ([#&#8203;16072](https://github.com/astral-sh/ruff/pull/16072))
-   Undeprecate `ruff.nativeServer` ([#&#8203;16039](https://github.com/astral-sh/ruff/pull/16039))

### [`v0.9.5`](https://github.com/astral-sh/ruff/blob/HEAD/CHANGELOG.md#095)

[Compare Source](https://github.com/astral-sh/ruff/compare/0.9.4...0.9.5)

##### Preview features

-   Recognize all symbols named `TYPE_CHECKING` for `in_type_checking_block` ([#&#8203;15719](https://github.com/astral-sh/ruff/pull/15719))
-   \[`flake8-comprehensions`] Handle builtins at top of file correctly for `unnecessary-dict-comprehension-for-iterable` (`C420`) ([#&#8203;15837](https://github.com/astral-sh/ruff/pull/15837))
-   \[`flake8-logging`] `.exception()` and `exc_info=` outside exception handlers (`LOG004`, `LOG014`) ([#&#8203;15799](https://github.com/astral-sh/ruff/pull/15799))
-   \[`flake8-pyi`] Fix incorrect behaviour of `custom-typevar-return-type` preview-mode autofix if `typing` was already imported (`PYI019`) ([#&#8203;15853](https://github.com/astral-sh/ruff/pull/15853))
-   \[`flake8-pyi`] Fix more complex cases (`PYI019`) ([#&#8203;15821](https://github.com/astral-sh/ruff/pull/15821))
-   \[`flake8-pyi`] Make `PYI019` autofixable for `.py` files in preview mode as well as stubs ([#&#8203;15889](https://github.com/astral-sh/ruff/pull/15889))
-   \[`flake8-pyi`] Remove type parameter correctly when it is the last (`PYI019`) ([#&#8203;15854](https://github.com/astral-sh/ruff/pull/15854))
-   \[`pylint`] Fix missing parens in unsafe fix for `unnecessary-dunder-call` (`PLC2801`) ([#&#8203;15762](https://github.com/astral-sh/ruff/pull/15762))
-   \[`pyupgrade`] Better messages and diagnostic range (`UP015`) ([#&#8203;15872](https://github.com/astral-sh/ruff/pull/15872))
-   \[`pyupgrade`] Rename private type parameters in PEP 695 generics (`UP049`) ([#&#8203;15862](https://github.com/astral-sh/ruff/pull/15862))
-   \[`refurb`] Also report non-name expressions (`FURB169`) ([#&#8203;15905](https://github.com/astral-sh/ruff/pull/15905))
-   \[`refurb`] Mark fix as unsafe if there are comments (`FURB171`) ([#&#8203;15832](https://github.com/astral-sh/ruff/pull/15832))
-   \[`ruff`] Classes with mixed type variable style (`RUF053`) ([#&#8203;15841](https://github.com/astral-sh/ruff/pull/15841))
-   \[`airflow`] `BashOperator` has been moved to `airflow.providers.standard.operators.bash.BashOperator` (`AIR302`) ([#&#8203;15922](https://github.com/astral-sh/ruff/pull/15922))
-   \[`flake8-pyi`] Add autofix for unused-private-type-var (`PYI018`) ([#&#8203;15999](https://github.com/astral-sh/ruff/pull/15999))
-   \[`flake8-pyi`] Significantly improve accuracy of `PYI019` if preview mode is enabled ([#&#8203;15888](https://github.com/astral-sh/ruff/pull/15888))

##### Rule changes

-   Preserve triple quotes and prefixes for strings ([#&#8203;15818](https://github.com/astral-sh/ruff/pull/15818))
-   \[`flake8-comprehensions`] Skip when `TypeError` present from too many (kw)args for `C410`,`C411`, and `C418` ([#&#8203;15838](https://github.com/astral-sh/ruff/pull/15838))
-   \[`flake8-pyi`] Rename `PYI019` and improve its diagnostic message ([#&#8203;15885](https://github.com/astral-sh/ruff/pull/15885))
-   \[`pep8-naming`] Ignore `@override` methods (`N803`) ([#&#8203;15954](https://github.com/astral-sh/ruff/pull/15954))
-   \[`pyupgrade`] Reuse replacement logic from `UP046` and `UP047` to preserve more comments (`UP040`) ([#&#8203;15840](https://github.com/astral-sh/ruff/pull/15840))
-   \[`ruff`] Analyze deferred annotations before enforcing `mutable-(data)class-default` and `function-call-in-dataclass-default-argument` (`RUF008`,`RUF009`,`RUF012`) ([#&#8203;15921](https://github.com/astral-sh/ruff/pull/15921))
-   \[`pycodestyle`] Exempt `sys.path += ...` calls (`E402`) ([#&#8203;15980](https://github.com/astral-sh/ruff/pull/15980))

##### Configuration

-   Config error only when `flake8-import-conventions` alias conflicts with `isort.required-imports` bound name ([#&#8203;15918](https://github.com/astral-sh/ruff/pull/15918))
-   Workaround Even Better TOML crash related to `allOf` ([#&#8203;15992](https://github.com/astral-sh/ruff/pull/15992))

##### Bug fixes

-   \[`flake8-comprehensions`] Unnecessary `list` comprehension (rewrite as a `set` comprehension) (`C403`) - Handle extraneous parentheses around list comprehension ([#&#8203;15877](https://github.com/astral-sh/ruff/pull/15877))
-   \[`flake8-comprehensions`] Handle trailing comma in fixes for `unnecessary-generator-list/set` (`C400`,`C401`) ([#&#8203;15929](https://github.com/astral-sh/ruff/pull/15929))
-   \[`flake8-pyi`] Fix several correctness issues with `custom-type-var-return-type` (`PYI019`) ([#&#8203;15851](https://github.com/astral-sh/ruff/pull/15851))
-   \[`pep8-naming`] Consider any number of leading underscore for `N801` ([#&#8203;15988](https://github.com/astral-sh/ruff/pull/15988))
-   \[`pyflakes`] Visit forward annotations in `TypeAliasType` as types (`F401`) ([#&#8203;15829](https://github.com/astral-sh/ruff/pull/15829))
-   \[`pylint`] Correct min/max auto-fix and suggestion for (`PL1730`) ([#&#8203;15930](https://github.com/astral-sh/ruff/pull/15930))
-   \[`refurb`] Handle unparenthesized tuples correctly (`FURB122`, `FURB142`) ([#&#8203;15953](https://github.com/astral-sh/ruff/pull/15953))
-   \[`refurb`] Avoid `None | None` as well as better detection and fix (`FURB168`) ([#&#8203;15779](https://github.com/astral-sh/ruff/pull/15779))

##### Documentation

-   Add deprecation warning for `ruff-lsp` related settings ([#&#8203;15850](https://github.com/astral-sh/ruff/pull/15850))
-   Docs (`linter.md`): clarify that Python files are always searched for in subdirectories ([#&#8203;15882](https://github.com/astral-sh/ruff/pull/15882))
-   Fix a typo in `non_pep695_generic_class.rs` ([#&#8203;15946](https://github.com/astral-sh/ruff/pull/15946))
-   Improve Docs: Pylint subcategories' codes ([#&#8203;15909](https://github.com/astral-sh/ruff/pull/15909))
-   Remove non-existing `lint.extendIgnore` editor setting ([#&#8203;15844](https://github.com/astral-sh/ruff/pull/15844))
-   Update black deviations ([#&#8203;15928](https://github.com/astral-sh/ruff/pull/15928))
-   Mention `UP049` in `UP046` and `UP047`, add `See also` section to `UP040` ([#&#8203;15956](https://github.com/astral-sh/ruff/pull/15956))
-   Add instance variable examples to `RUF012` ([#&#8203;15982](https://github.com/astral-sh/ruff/pull/15982))
-   Explain precedence for `ignore` and `select` config ([#&#8203;15883](https://github.com/astral-sh/ruff/pull/15883))

### [`v0.9.4`](https://github.com/astral-sh/ruff/blob/HEAD/CHANGELOG.md#094)

[Compare Source](https://github.com/astral-sh/ruff/compare/0.9.3...0.9.4)

##### Preview features

-   \[`airflow`] Extend airflow context parameter check for `BaseOperator.execute` (`AIR302`) ([#&#8203;15713](https://github.com/astral-sh/ruff/pull/15713))
-   \[`airflow`] Update `AIR302` to check for deprecated context keys ([#&#8203;15144](https://github.com/astral-sh/ruff/pull/15144))
-   \[`flake8-bandit`] Permit suspicious imports within stub files (`S4`) ([#&#8203;15822](https://github.com/astral-sh/ruff/pull/15822))
-   \[`pylint`] Do not trigger `PLR6201` on empty collections ([#&#8203;15732](https://github.com/astral-sh/ruff/pull/15732))
-   \[`refurb`] Do not emit diagnostic when loop variables are used outside loop body (`FURB122`) ([#&#8203;15757](https://github.com/astral-sh/ruff/pull/15757))
-   \[`ruff`] Add support for more `re` patterns (`RUF055`) ([#&#8203;15764](https://github.com/astral-sh/ruff/pull/15764))
-   \[`ruff`] Check for shadowed `map` before suggesting fix (`RUF058`) ([#&#8203;15790](https://github.com/astral-sh/ruff/pull/15790))
-   \[`ruff`] Do not emit diagnostic when all arguments to `zip()` are variadic (`RUF058`) ([#&#8203;15744](https://github.com/astral-sh/ruff/pull/15744))
-   \[`ruff`] Parenthesize fix when argument spans multiple lines for `unnecessary-round` (`RUF057`) ([#&#8203;15703](https://github.com/astral-sh/ruff/pull/15703))

##### Rule changes

-   Preserve quote style in generated code ([#&#8203;15726](https://github.com/astral-sh/ruff/pull/15726), [#&#8203;15778](https://github.com/astral-sh/ruff/pull/15778), [#&#8203;15794](https://github.com/astral-sh/ruff/pull/15794))
-   \[`flake8-bugbear`] Exempt `NewType` calls where the original type is immutable (`B008`) ([#&#8203;15765](https://github.com/astral-sh/ruff/pull/15765))
-   \[`pylint`] Honor banned top-level imports by `TID253` in `PLC0415`. ([#&#8203;15628](https://github.com/astral-sh/ruff/pull/15628))
-   \[`pyupgrade`] Ignore `is_typeddict` and `TypedDict` for `deprecated-import` (`UP035`) ([#&#8203;15800](https://github.com/astral-sh/ruff/pull/15800))

##### CLI

-   Fix formatter warning message for `flake8-quotes` option ([#&#8203;15788](https://github.com/astral-sh/ruff/pull/15788))
-   Implement tab autocomplete for `ruff config` ([#&#8203;15603](https://github.com/astral-sh/ruff/pull/15603))

##### Bug fixes

-   \[`flake8-comprehensions`] Do not emit `unnecessary-map` diagnostic when lambda has different arity (`C417`) ([#&#8203;15802](https://github.com/astral-sh/ruff/pull/15802))
-   \[`flake8-comprehensions`] Parenthesize `sorted` when needed for `unnecessary-call-around-sorted` (`C413`) ([#&#8203;15825](https://github.com/astral-sh/ruff/pull/15825))
-   \[`pyupgrade`] Handle end-of-line comments for `quoted-annotation` (`UP037`) ([#&#8203;15824](https://github.com/astral-sh/ruff/pull/15824))

##### Documentation

-   Add missing config docstrings ([#&#8203;15803](https://github.com/astral-sh/ruff/pull/15803))
-   Add references to `trio.run_process` and `anyio.run_process` ([#&#8203;15761](https://github.com/astral-sh/ruff/pull/15761))
-   Use `uv init --lib` in tutorial ([#&#8203;15718](https://github.com/astral-sh/ruff/pull/15718))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMTEuMCIsInVwZGF0ZWRJblZlciI6IjM5LjIxMS4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: #77
Co-authored-by: Renovate <renovate@csw.im>
Co-committed-by: Renovate <renovate@csw.im>
2025-03-29 08:18:55 -04:00
3c1acdca33
fix(deps): update dependency numpy to v2.2.4 (#71)
Some checks are pending
Actions / Lint Code (Ruff & Pylint) (push) Waiting to run
Actions / Build Documentation (MkDocs) (push) Waiting to run
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [numpy](https://github.com/numpy/numpy) ([changelog](https://numpy.org/doc/stable/release)) | project.dependencies | patch | `==2.2.2` -> `==2.2.4` |

---

### Release Notes

<details>
<summary>numpy/numpy (numpy)</summary>

### [`v2.2.4`](https://github.com/numpy/numpy/releases/tag/v2.2.4): 2.2.4 (Mar 16, 2025)

[Compare Source](https://github.com/numpy/numpy/compare/v2.2.3...v2.2.4)

### NumPy 2.2.4 Release Notes

NumPy 2.2.4 is a patch release that fixes bugs found after the 2.2.3
release. There are a large number of typing improvements, the rest of
the changes are the usual mix of bugfixes and platform maintenace.

This release supports Python versions 3.10-3.13.

#### Contributors

A total of 15 people contributed to this release. People with a "+" by
their names contributed a patch for the first time.

-   Abhishek Kumar
-   Andrej Zhilenkov
-   Andrew Nelson
-   Charles Harris
-   Giovanni Del Monte
-   Guan Ming(Wesley) Chiu +
-   Jonathan Albrecht +
-   Joren Hammudoglu
-   Mark Harfouche
-   Matthieu Darbois
-   Nathan Goldbaum
-   Pieter Eendebak
-   Sebastian Berg
-   Tyler Reddy
-   lvllvl +

#### Pull requests merged

A total of 17 pull requests were merged for this release.

-   [#&#8203;28333](https://github.com/numpy/numpy/pull/28333): MAINT: Prepare 2.2.x for further development.
-   [#&#8203;28348](https://github.com/numpy/numpy/pull/28348): TYP: fix positional- and keyword-only params in astype, cross...
-   [#&#8203;28377](https://github.com/numpy/numpy/pull/28377): MAINT: Update FreeBSD version and fix test failure
-   [#&#8203;28379](https://github.com/numpy/numpy/pull/28379): BUG: numpy.loadtxt reads only 50000 lines when skip_rows >= max_rows
-   [#&#8203;28385](https://github.com/numpy/numpy/pull/28385): BUG: Make np.nonzero threading safe
-   [#&#8203;28420](https://github.com/numpy/numpy/pull/28420): BUG: safer bincount casting (backport to 2.2.x)
-   [#&#8203;28422](https://github.com/numpy/numpy/pull/28422): BUG: Fix building on s390x with clang
-   [#&#8203;28423](https://github.com/numpy/numpy/pull/28423): CI: use QEMU 9.2.2 for Linux Qemu tests
-   [#&#8203;28424](https://github.com/numpy/numpy/pull/28424): BUG: skip legacy dtype multithreaded test on 32 bit runners
-   [#&#8203;28435](https://github.com/numpy/numpy/pull/28435): BUG: Fix searchsorted and CheckFromAny byte-swapping logic
-   [#&#8203;28449](https://github.com/numpy/numpy/pull/28449): BUG: sanity check `__array_interface__` number of dimensions
-   [#&#8203;28510](https://github.com/numpy/numpy/pull/28510): MAINT: Hide decorator from pytest traceback
-   [#&#8203;28512](https://github.com/numpy/numpy/pull/28512): TYP: Typing fixes backported from [#&#8203;28452](https://github.com/numpy/numpy/issues/28452), [#&#8203;28491](https://github.com/numpy/numpy/issues/28491), [#&#8203;28494](https://github.com/numpy/numpy/issues/28494)
-   [#&#8203;28521](https://github.com/numpy/numpy/pull/28521): TYP: Backport fixes from [#&#8203;28505](https://github.com/numpy/numpy/issues/28505), [#&#8203;28506](https://github.com/numpy/numpy/issues/28506), [#&#8203;28508](https://github.com/numpy/numpy/issues/28508), and [#&#8203;28511](https://github.com/numpy/numpy/issues/28511)
-   [#&#8203;28533](https://github.com/numpy/numpy/pull/28533): TYP: Backport typing fixes from main (2)
-   [#&#8203;28534](https://github.com/numpy/numpy/pull/28534): TYP: Backport typing fixes from main (3)
-   [#&#8203;28542](https://github.com/numpy/numpy/pull/28542): TYP: Backport typing fixes from main (4)

#### Checksums

##### MD5

    935928cbd2de140da097f6d5f4a01d72  numpy-2.2.4-cp310-cp310-macosx_10_9_x86_64.whl
    bf7fd01bb177885e920173b610c195d9  numpy-2.2.4-cp310-cp310-macosx_11_0_arm64.whl
    826e52cd898567a0c446113ab7a7b362  numpy-2.2.4-cp310-cp310-macosx_14_0_arm64.whl
    9982a91d7327aea541c24aff94d3e462  numpy-2.2.4-cp310-cp310-macosx_14_0_x86_64.whl
    5bdf5b63f4ee01fa808d13043b2a2275  numpy-2.2.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
    677b3031105e24eaee2e0e57d7c2a306  numpy-2.2.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
    d857867787fe1eb236670e7fdb25f414  numpy-2.2.4-cp310-cp310-musllinux_1_2_aarch64.whl
    a5aff3a7eb2923878e67fbe1cd04a9e9  numpy-2.2.4-cp310-cp310-musllinux_1_2_x86_64.whl
    e00bd3ac85d8f34b46b7f97a8278aeb3  numpy-2.2.4-cp310-cp310-win32.whl
    e5cb2a5d14bccee316bb73173be125ec  numpy-2.2.4-cp310-cp310-win_amd64.whl
    494f60d8e1c3500413bd093bb3f486ea  numpy-2.2.4-cp311-cp311-macosx_10_9_x86_64.whl
    a886a9f3e80a60ce6ba95b431578bbca  numpy-2.2.4-cp311-cp311-macosx_11_0_arm64.whl
    889f3b507bab9272d9b549780840a642  numpy-2.2.4-cp311-cp311-macosx_14_0_arm64.whl
    059788668d2c4e9aace4858e77c099ed  numpy-2.2.4-cp311-cp311-macosx_14_0_x86_64.whl
    db9ae978afb76a4bf79df0657a66aaeb  numpy-2.2.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
    e36963a4c177157dc7b0775c309fa5a8  numpy-2.2.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
    3603e683878b74f38e5617f04ff6a369  numpy-2.2.4-cp311-cp311-musllinux_1_2_aarch64.whl
    afbc410fb9b42b19f4f7c81c21d6777f  numpy-2.2.4-cp311-cp311-musllinux_1_2_x86_64.whl
    33ff8081378188894097942f80c33e26  numpy-2.2.4-cp311-cp311-win32.whl
    5b11fe8d26318d85e0bc577a654f6643  numpy-2.2.4-cp311-cp311-win_amd64.whl
    91121787f396d3e98210de8b617e5d48  numpy-2.2.4-cp312-cp312-macosx_10_13_x86_64.whl
    c524d1020b4652aacf4477d1628fa1ba  numpy-2.2.4-cp312-cp312-macosx_11_0_arm64.whl
    eb08f551bdd6772155bb39ac0da47479  numpy-2.2.4-cp312-cp312-macosx_14_0_arm64.whl
    7cb37fc9145d0ebbea5666b4f9ed1027  numpy-2.2.4-cp312-cp312-macosx_14_0_x86_64.whl
    c4452a5dc557c291904b5c51a4148237  numpy-2.2.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
    bd23a12ead870759f264160ab38b2c9d  numpy-2.2.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
    07b44109381985b48d1eef80feebc5ad  numpy-2.2.4-cp312-cp312-musllinux_1_2_aarch64.whl
    95f1a27d33106fa9f40ee0714681c840  numpy-2.2.4-cp312-cp312-musllinux_1_2_x86_64.whl
    507e550a55b19dedf267b58a487ba0bc  numpy-2.2.4-cp312-cp312-win32.whl
    be21ccbf8931e92ba1fdb2dc1250bf2a  numpy-2.2.4-cp312-cp312-win_amd64.whl
    e94003c2b65d81b00203711c5c42fb8e  numpy-2.2.4-cp313-cp313-macosx_10_13_x86_64.whl
    cf781fd5412ffd826e0436883452cc17  numpy-2.2.4-cp313-cp313-macosx_11_0_arm64.whl
    92c9a30386a64f2deddad1db742bd296  numpy-2.2.4-cp313-cp313-macosx_14_0_arm64.whl
    7fd16554fa0a15b7f99b1fabf1c4592c  numpy-2.2.4-cp313-cp313-macosx_14_0_x86_64.whl
    9293b0575a902b2d55c35567dee7679e  numpy-2.2.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
    9970699bd95e8a64a562b1e6328b83d0  numpy-2.2.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
    e8597c611a919a8e88229d6889c1f86e  numpy-2.2.4-cp313-cp313-musllinux_1_2_aarch64.whl
    329288501f012606605bdbed368e58e9  numpy-2.2.4-cp313-cp313-musllinux_1_2_x86_64.whl
    04bf8d0f6a9e279ab01df4ed0b4aeee1  numpy-2.2.4-cp313-cp313-win32.whl
    66801fe84a436b7ed3be6e0082b86917  numpy-2.2.4-cp313-cp313-win_amd64.whl
    3e2f31e01b45cd16a87b794477de3714  numpy-2.2.4-cp313-cp313t-macosx_10_13_x86_64.whl
    7504018213a3a8fea7173e2c1d0fcfd1  numpy-2.2.4-cp313-cp313t-macosx_11_0_arm64.whl
    e299021397c3cdb941b7ffe77cf0fefe  numpy-2.2.4-cp313-cp313t-macosx_14_0_arm64.whl
    1cc2731a246079bcab361179f38e7ccb  numpy-2.2.4-cp313-cp313t-macosx_14_0_x86_64.whl
    e6eccf936d25c9eda9df1a4d50ae2fdc  numpy-2.2.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
    ba825efd05cca6d56c3dca9f7f1f88e7  numpy-2.2.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
    369eebec47c9c27cb4841a13e9522167  numpy-2.2.4-cp313-cp313t-musllinux_1_2_aarch64.whl
    554dbfa52988d01f715cbe8d4da4b409  numpy-2.2.4-cp313-cp313t-musllinux_1_2_x86_64.whl
    811d25a008c68086c9382487e9a4127a  numpy-2.2.4-cp313-cp313t-win32.whl
    893fd2fdd42f386e300bee885bbb7778  numpy-2.2.4-cp313-cp313t-win_amd64.whl
    65e284546c5ee575eca0a3726c0a1d98  numpy-2.2.4-pp310-pypy310_pp73-macosx_10_15_x86_64.whl
    e4e73511eac8f1a10c6abbd6fa2fa0aa  numpy-2.2.4-pp310-pypy310_pp73-macosx_14_0_x86_64.whl
    a884ed5263b91fa87b5e3d14caf955a5  numpy-2.2.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
    7330087a6ad1527ae20a495e2fb3b357  numpy-2.2.4-pp310-pypy310_pp73-win_amd64.whl
    56232f4a69b03dd7a87a55fffc5f2ebc  numpy-2.2.4.tar.gz

##### SHA256

    8146f3550d627252269ac42ae660281d673eb6f8b32f113538e0cc2a9aed42b9  numpy-2.2.4-cp310-cp310-macosx_10_9_x86_64.whl
    e642d86b8f956098b564a45e6f6ce68a22c2c97a04f5acd3f221f57b8cb850ae  numpy-2.2.4-cp310-cp310-macosx_11_0_arm64.whl
    a84eda42bd12edc36eb5b53bbcc9b406820d3353f1994b6cfe453a33ff101775  numpy-2.2.4-cp310-cp310-macosx_14_0_arm64.whl
    4ba5054787e89c59c593a4169830ab362ac2bee8a969249dc56e5d7d20ff8df9  numpy-2.2.4-cp310-cp310-macosx_14_0_x86_64.whl
    7716e4a9b7af82c06a2543c53ca476fa0b57e4d760481273e09da04b74ee6ee2  numpy-2.2.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
    adf8c1d66f432ce577d0197dceaac2ac00c0759f573f28516246351c58a85020  numpy-2.2.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
    218f061d2faa73621fa23d6359442b0fc658d5b9a70801373625d958259eaca3  numpy-2.2.4-cp310-cp310-musllinux_1_2_aarch64.whl
    df2f57871a96bbc1b69733cd4c51dc33bea66146b8c63cacbfed73eec0883017  numpy-2.2.4-cp310-cp310-musllinux_1_2_x86_64.whl
    a0258ad1f44f138b791327961caedffbf9612bfa504ab9597157806faa95194a  numpy-2.2.4-cp310-cp310-win32.whl
    0d54974f9cf14acf49c60f0f7f4084b6579d24d439453d5fc5805d46a165b542  numpy-2.2.4-cp310-cp310-win_amd64.whl
    e9e0a277bb2eb5d8a7407e14688b85fd8ad628ee4e0c7930415687b6564207a4  numpy-2.2.4-cp311-cp311-macosx_10_9_x86_64.whl
    9eeea959168ea555e556b8188da5fa7831e21d91ce031e95ce23747b7609f8a4  numpy-2.2.4-cp311-cp311-macosx_11_0_arm64.whl
    bd3ad3b0a40e713fc68f99ecfd07124195333f1e689387c180813f0e94309d6f  numpy-2.2.4-cp311-cp311-macosx_14_0_arm64.whl
    cf28633d64294969c019c6df4ff37f5698e8326db68cc2b66576a51fad634880  numpy-2.2.4-cp311-cp311-macosx_14_0_x86_64.whl
    2fa8fa7697ad1646b5c93de1719965844e004fcad23c91228aca1cf0800044a1  numpy-2.2.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
    f4162988a360a29af158aeb4a2f4f09ffed6a969c9776f8f3bdee9b06a8ab7e5  numpy-2.2.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
    892c10d6a73e0f14935c31229e03325a7b3093fafd6ce0af704be7f894d95687  numpy-2.2.4-cp311-cp311-musllinux_1_2_aarch64.whl
    db1f1c22173ac1c58db249ae48aa7ead29f534b9a948bc56828337aa84a32ed6  numpy-2.2.4-cp311-cp311-musllinux_1_2_x86_64.whl
    ea2bb7e2ae9e37d96835b3576a4fa4b3a97592fbea8ef7c3587078b0068b8f09  numpy-2.2.4-cp311-cp311-win32.whl
    f7de08cbe5551911886d1ab60de58448c6df0f67d9feb7d1fb21e9875ef95e91  numpy-2.2.4-cp311-cp311-win_amd64.whl
    a7b9084668aa0f64e64bd00d27ba5146ef1c3a8835f3bd912e7a9e01326804c4  numpy-2.2.4-cp312-cp312-macosx_10_13_x86_64.whl
    dbe512c511956b893d2dacd007d955a3f03d555ae05cfa3ff1c1ff6df8851854  numpy-2.2.4-cp312-cp312-macosx_11_0_arm64.whl
    bb649f8b207ab07caebba230d851b579a3c8711a851d29efe15008e31bb4de24  numpy-2.2.4-cp312-cp312-macosx_14_0_arm64.whl
    f34dc300df798742b3d06515aa2a0aee20941c13579d7a2f2e10af01ae4901ee  numpy-2.2.4-cp312-cp312-macosx_14_0_x86_64.whl
    c3f7ac96b16955634e223b579a3e5798df59007ca43e8d451a0e6a50f6bfdfba  numpy-2.2.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
    4f92084defa704deadd4e0a5ab1dc52d8ac9e8a8ef617f3fbb853e79b0ea3592  numpy-2.2.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
    7a4e84a6283b36632e2a5b56e121961f6542ab886bc9e12f8f9818b3c266bfbb  numpy-2.2.4-cp312-cp312-musllinux_1_2_aarch64.whl
    11c43995255eb4127115956495f43e9343736edb7fcdb0d973defd9de14cd84f  numpy-2.2.4-cp312-cp312-musllinux_1_2_x86_64.whl
    65ef3468b53269eb5fdb3a5c09508c032b793da03251d5f8722b1194f1790c00  numpy-2.2.4-cp312-cp312-win32.whl
    2aad3c17ed2ff455b8eaafe06bcdae0062a1db77cb99f4b9cbb5f4ecb13c5146  numpy-2.2.4-cp312-cp312-win_amd64.whl
    1cf4e5c6a278d620dee9ddeb487dc6a860f9b199eadeecc567f777daace1e9e7  numpy-2.2.4-cp313-cp313-macosx_10_13_x86_64.whl
    1974afec0b479e50438fc3648974268f972e2d908ddb6d7fb634598cdb8260a0  numpy-2.2.4-cp313-cp313-macosx_11_0_arm64.whl
    79bd5f0a02aa16808fcbc79a9a376a147cc1045f7dfe44c6e7d53fa8b8a79392  numpy-2.2.4-cp313-cp313-macosx_14_0_arm64.whl
    3387dd7232804b341165cedcb90694565a6015433ee076c6754775e85d86f1fc  numpy-2.2.4-cp313-cp313-macosx_14_0_x86_64.whl
    6f527d8fdb0286fd2fd97a2a96c6be17ba4232da346931d967a0630050dfd298  numpy-2.2.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
    bce43e386c16898b91e162e5baaad90c4b06f9dcbe36282490032cec98dc8ae7  numpy-2.2.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
    31504f970f563d99f71a3512d0c01a645b692b12a63630d6aafa0939e52361e6  numpy-2.2.4-cp313-cp313-musllinux_1_2_aarch64.whl
    81413336ef121a6ba746892fad881a83351ee3e1e4011f52e97fba79233611fd  numpy-2.2.4-cp313-cp313-musllinux_1_2_x86_64.whl
    f486038e44caa08dbd97275a9a35a283a8f1d2f0ee60ac260a1790e76660833c  numpy-2.2.4-cp313-cp313-win32.whl
    207a2b8441cc8b6a2a78c9ddc64d00d20c303d79fba08c577752f080c4007ee3  numpy-2.2.4-cp313-cp313-win_amd64.whl
    8120575cb4882318c791f839a4fd66161a6fa46f3f0a5e613071aae35b5dd8f8  numpy-2.2.4-cp313-cp313t-macosx_10_13_x86_64.whl
    a761ba0fa886a7bb33c6c8f6f20213735cb19642c580a931c625ee377ee8bd39  numpy-2.2.4-cp313-cp313t-macosx_11_0_arm64.whl
    ac0280f1ba4a4bfff363a99a6aceed4f8e123f8a9b234c89140f5e894e452ecd  numpy-2.2.4-cp313-cp313t-macosx_14_0_arm64.whl
    879cf3a9a2b53a4672a168c21375166171bc3932b7e21f622201811c43cdd3b0  numpy-2.2.4-cp313-cp313t-macosx_14_0_x86_64.whl
    f05d4198c1bacc9124018109c5fba2f3201dbe7ab6e92ff100494f236209c960  numpy-2.2.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
    e2f085ce2e813a50dfd0e01fbfc0c12bbe5d2063d99f8b29da30e544fb6483b8  numpy-2.2.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
    92bda934a791c01d6d9d8e038363c50918ef7c40601552a58ac84c9613a665bc  numpy-2.2.4-cp313-cp313t-musllinux_1_2_aarch64.whl
    ee4d528022f4c5ff67332469e10efe06a267e32f4067dc76bb7e2cddf3cd25ff  numpy-2.2.4-cp313-cp313t-musllinux_1_2_x86_64.whl
    05c076d531e9998e7e694c36e8b349969c56eadd2cdcd07242958489d79a7286  numpy-2.2.4-cp313-cp313t-win32.whl
    188dcbca89834cc2e14eb2f106c96d6d46f200fe0200310fc29089657379c58d  numpy-2.2.4-cp313-cp313t-win_amd64.whl
    7051ee569db5fbac144335e0f3b9c2337e0c8d5c9fee015f259a5bd70772b7e8  numpy-2.2.4-pp310-pypy310_pp73-macosx_10_15_x86_64.whl
    ab2939cd5bec30a7430cbdb2287b63151b77cf9624de0532d629c9a1c59b1d5c  numpy-2.2.4-pp310-pypy310_pp73-macosx_14_0_x86_64.whl
    d0f35b19894a9e08639fd60a1ec1978cb7f5f7f1eace62f38dd36be8aecdef4d  numpy-2.2.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
    b4adfbbc64014976d2f91084915ca4e626fbf2057fb81af209c1a6d776d23e3d  numpy-2.2.4-pp310-pypy310_pp73-win_amd64.whl
    9ba03692a45d3eef66559efe1d1096c4b9b75c0986b5dff5530c378fb8331d4f  numpy-2.2.4.tar.gz

### [`v2.2.3`](https://github.com/numpy/numpy/releases/tag/v2.2.3): 2.2.3 (Feb 13, 2025)

[Compare Source](https://github.com/numpy/numpy/compare/v2.2.2...v2.2.3)

### NumPy 2.2.3 Release Notes

NumPy 2.2.3 is a patch release that fixes bugs found after the 2.2.2
release. The majority of the changes are typing improvements and fixes
for free threaded Python. Both of those areas are still under
development, so if you discover new problems, please report them.

This release supports Python versions 3.10-3.13.

#### Contributors

A total of 9 people contributed to this release. People with a "+" by
their names contributed a patch for the first time.

-   !amotzop
-   Charles Harris
-   Chris Sidebottom
-   Joren Hammudoglu
-   Matthew Brett
-   Nathan Goldbaum
-   Raghuveer Devulapalli
-   Sebastian Berg
-   Yakov Danishevsky +

#### Pull requests merged

A total of 21 pull requests were merged for this release.

-   [#&#8203;28185](https://github.com/numpy/numpy/pull/28185): MAINT: Prepare 2.2.x for further development
-   [#&#8203;28201](https://github.com/numpy/numpy/pull/28201): BUG: fix data race in a more minimal way on stable branch
-   [#&#8203;28208](https://github.com/numpy/numpy/pull/28208): BUG: Fix `from_float_positional` errors for huge pads
-   [#&#8203;28209](https://github.com/numpy/numpy/pull/28209): BUG: fix data race in np.repeat
-   [#&#8203;28212](https://github.com/numpy/numpy/pull/28212): MAINT: Use VQSORT_COMPILER_COMPATIBLE to determine if we should...
-   [#&#8203;28224](https://github.com/numpy/numpy/pull/28224): MAINT: update highway to latest
-   [#&#8203;28236](https://github.com/numpy/numpy/pull/28236): BUG: Add cpp atomic support ([#&#8203;28234](https://github.com/numpy/numpy/issues/28234))
-   [#&#8203;28237](https://github.com/numpy/numpy/pull/28237): BLD: Compile fix for clang-cl on WoA
-   [#&#8203;28243](https://github.com/numpy/numpy/pull/28243): TYP: Avoid upcasting `float64` in the set-ops
-   [#&#8203;28249](https://github.com/numpy/numpy/pull/28249): BLD: better fix for clang / ARM compiles
-   [#&#8203;28266](https://github.com/numpy/numpy/pull/28266): TYP: Fix `timedelta64.__divmod__` and `timedelta64.__mod__`...
-   [#&#8203;28274](https://github.com/numpy/numpy/pull/28274): TYP: Fixed missing typing information of set_printoptions
-   [#&#8203;28278](https://github.com/numpy/numpy/pull/28278): BUG: backport resource cleanup bugfix from [gh-28273](https://github.com/numpy/numpy/issues/28273)
-   [#&#8203;28282](https://github.com/numpy/numpy/pull/28282): BUG: fix incorrect bytes to stringdtype coercion
-   [#&#8203;28283](https://github.com/numpy/numpy/pull/28283): TYP: Fix scalar constructors
-   [#&#8203;28284](https://github.com/numpy/numpy/pull/28284): TYP: stub `numpy.matlib`
-   [#&#8203;28285](https://github.com/numpy/numpy/pull/28285): TYP: stub the missing `numpy.testing` modules
-   [#&#8203;28286](https://github.com/numpy/numpy/pull/28286): CI: Fix the github label for `TYP:` PR's and issues
-   [#&#8203;28305](https://github.com/numpy/numpy/pull/28305): TYP: Backport typing updates from main
-   [#&#8203;28321](https://github.com/numpy/numpy/pull/28321): BUG: fix race initializing legacy dtype casts
-   [#&#8203;28324](https://github.com/numpy/numpy/pull/28324): CI: update test_moderately_small_alpha

#### Checksums

##### MD5

    9cd8b5e358f89016f403a6c1a27e7e87  numpy-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl
    2818f5a9efcfc3bb6bf657137df26046  numpy-2.2.3-cp310-cp310-macosx_11_0_arm64.whl
    6d65c6a336cfb69fe4ddd756cad73d55  numpy-2.2.3-cp310-cp310-macosx_14_0_arm64.whl
    7f4cf33c634b33f633d4bf47f560a86d  numpy-2.2.3-cp310-cp310-macosx_14_0_x86_64.whl
    3c04024badd42bfcc68c14f106efa93f  numpy-2.2.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
    07658df1de0e1d3721de0aacff4313cd  numpy-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
    3e753fc4b7c879b29442ee9bab25eddd  numpy-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl
    d1811f1988d88b00825bc6e943d8e22d  numpy-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl
    b5fe91363c16001ea30cbd5befbb0555  numpy-2.2.3-cp310-cp310-win32.whl
    44dfe1df1640e4fe762bedad57cd7165  numpy-2.2.3-cp310-cp310-win_amd64.whl
    6156418f596620b00a3c221baef02476  numpy-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl
    97b925bac245aad1297d22ad3cfaa74c  numpy-2.2.3-cp311-cp311-macosx_11_0_arm64.whl
    3f05819fcb71df1d3093e5d1c041a4e9  numpy-2.2.3-cp311-cp311-macosx_14_0_arm64.whl
    f6763893ba9a5739fefa0929fd152db2  numpy-2.2.3-cp311-cp311-macosx_14_0_x86_64.whl
    e93cf6ed4e1a3f9a8009ee7f2fcb0da8  numpy-2.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
    851dcbcbe90212c385dcdac1614cca83  numpy-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
    9b27cf1d6319f70370f4b0af10c03f5c  numpy-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl
    28d20c95ff23d27ae639b4960df777ec  numpy-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl
    559fefe30c0043a088adeca90231b382  numpy-2.2.3-cp311-cp311-win32.whl
    5e32a1cc3dcfe729f675784a53e4d553  numpy-2.2.3-cp311-cp311-win_amd64.whl
    12134dcf62b2bca2eeebb7bbc45c2a71  numpy-2.2.3-cp312-cp312-macosx_10_13_x86_64.whl
    c72318236531d3ca61d229eaf96f7d04  numpy-2.2.3-cp312-cp312-macosx_11_0_arm64.whl
    1b807acc844c2ba5be7bc7586d4a3a6b  numpy-2.2.3-cp312-cp312-macosx_14_0_arm64.whl
    810d4908371bb2f08b0c7b16d3f05970  numpy-2.2.3-cp312-cp312-macosx_14_0_x86_64.whl
    bb918cedd0931cb68af9e77096dedf54  numpy-2.2.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
    92c6c6c5b22b207425b329f061bd18fa  numpy-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
    10d48fb9d86280db1afe7224b15a51af  numpy-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl
    a73da0434a971b21d8a9c0596015d629  numpy-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl
    c5f1e734c7d872e2f9af71d32e62d59c  numpy-2.2.3-cp312-cp312-win32.whl
    884c1a89844f539ab15b7016a43d231c  numpy-2.2.3-cp312-cp312-win_amd64.whl
    3a2de7f886cb756cf8d0375a36721926  numpy-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl
    c1fe5b6a9015c2877647419caa009be0  numpy-2.2.3-cp313-cp313-macosx_11_0_arm64.whl
    bb3f3a69219bbcdb719bbe38e4e69f79  numpy-2.2.3-cp313-cp313-macosx_14_0_arm64.whl
    8158c2e980a1cbfb4d98ff3a273bb2e9  numpy-2.2.3-cp313-cp313-macosx_14_0_x86_64.whl
    4d3d9b0c14db955e4b1aa1a1971d2def  numpy-2.2.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
    6575308269513900c94803258b89ac83  numpy-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
    945b91c2093fed2a1f34597fc66e5a35  numpy-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl
    c5867508607f75ed23426315a7ad86d7  numpy-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl
    5a1497c262d9aa52ce6859a12a54ebbc  numpy-2.2.3-cp313-cp313-win32.whl
    69c98e036d59eb74e4620c7649b5d7fc  numpy-2.2.3-cp313-cp313-win_amd64.whl
    2535d7c0f98ad848bcf1f48f7c358e41  numpy-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl
    aea9afa69d510ce905b2b8dbf0e33a11  numpy-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl
    cc5aceacd0a44a67cdd2cf8d5a446ca3  numpy-2.2.3-cp313-cp313t-macosx_14_0_arm64.whl
    32eb2ed1e734ea26c90f75b1f5616564  numpy-2.2.3-cp313-cp313t-macosx_14_0_x86_64.whl
    f1d85f322c3e85ef748c3e5594b94226  numpy-2.2.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
    7f24ce01ad5c352c76614a12fa5e2319  numpy-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
    62841d4b49c5a0cef2c2ba26a16f6959  numpy-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl
    d7b512f83999d05c47e55b931f2dcdfe  numpy-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl
    1dca2f20e0accc1741e5fb233ecf7dff  numpy-2.2.3-cp313-cp313t-win32.whl
    347b71f0db5b49a25ef1ed677e47999b  numpy-2.2.3-cp313-cp313t-win_amd64.whl
    3615d13c8c14c323aeda1c07d5a7fd55  numpy-2.2.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl
    f7d2ba950c5aa11c100bb6bf202d5799  numpy-2.2.3-pp310-pypy310_pp73-macosx_14_0_x86_64.whl
    b4336174c843c4943084e17945cd1165  numpy-2.2.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
    0d856a89e028c393f8125739c56591e0  numpy-2.2.3-pp310-pypy310_pp73-win_amd64.whl
    c6ee254bcdf1e2fdb13d87e0ee4166ba  numpy-2.2.3.tar.gz

##### SHA256

    cbc6472e01952d3d1b2772b720428f8b90e2deea8344e854df22b0618e9cce71  numpy-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl
    cdfe0c22692a30cd830c0755746473ae66c4a8f2e7bd508b35fb3b6a0813d787  numpy-2.2.3-cp310-cp310-macosx_11_0_arm64.whl
    e37242f5324ffd9f7ba5acf96d774f9276aa62a966c0bad8dae692deebec7716  numpy-2.2.3-cp310-cp310-macosx_14_0_arm64.whl
    95172a21038c9b423e68be78fd0be6e1b97674cde269b76fe269a5dfa6fadf0b  numpy-2.2.3-cp310-cp310-macosx_14_0_x86_64.whl
    d5b47c440210c5d1d67e1cf434124e0b5c395eee1f5806fdd89b553ed1acd0a3  numpy-2.2.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
    0391ea3622f5c51a2e29708877d56e3d276827ac5447d7f45e9bc4ade8923c52  numpy-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
    f6b3dfc7661f8842babd8ea07e9897fe3d9b69a1d7e5fbb743e4160f9387833b  numpy-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl
    1ad78ce7f18ce4e7df1b2ea4019b5817a2f6a8a16e34ff2775f646adce0a5027  numpy-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl
    5ebeb7ef54a7be11044c33a17b2624abe4307a75893c001a4800857956b41094  numpy-2.2.3-cp310-cp310-win32.whl
    596140185c7fa113563c67c2e894eabe0daea18cf8e33851738c19f70ce86aeb  numpy-2.2.3-cp310-cp310-win_amd64.whl
    16372619ee728ed67a2a606a614f56d3eabc5b86f8b615c79d01957062826ca8  numpy-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl
    5521a06a3148686d9269c53b09f7d399a5725c47bbb5b35747e1cb76326b714b  numpy-2.2.3-cp311-cp311-macosx_11_0_arm64.whl
    7c8dde0ca2f77828815fd1aedfdf52e59071a5bae30dac3b4da2a335c672149a  numpy-2.2.3-cp311-cp311-macosx_14_0_arm64.whl
    77974aba6c1bc26e3c205c2214f0d5b4305bdc719268b93e768ddb17e3fdd636  numpy-2.2.3-cp311-cp311-macosx_14_0_x86_64.whl
    d42f9c36d06440e34226e8bd65ff065ca0963aeecada587b937011efa02cdc9d  numpy-2.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
    f2712c5179f40af9ddc8f6727f2bd910ea0eb50206daea75f58ddd9fa3f715bb  numpy-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
    c8b0451d2ec95010d1db8ca733afc41f659f425b7f608af569711097fd6014e2  numpy-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl
    d9b4a8148c57ecac25a16b0e11798cbe88edf5237b0df99973687dd866f05e1b  numpy-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl
    1f45315b2dc58d8a3e7754fe4e38b6fce132dab284a92851e41b2b344f6441c5  numpy-2.2.3-cp311-cp311-win32.whl
    9f48ba6f6c13e5e49f3d3efb1b51c8193215c42ac82610a04624906a9270be6f  numpy-2.2.3-cp311-cp311-win_amd64.whl
    12c045f43b1d2915eca6b880a7f4a256f59d62df4f044788c8ba67709412128d  numpy-2.2.3-cp312-cp312-macosx_10_13_x86_64.whl
    87eed225fd415bbae787f93a457af7f5990b92a334e346f72070bf569b9c9c95  numpy-2.2.3-cp312-cp312-macosx_11_0_arm64.whl
    712a64103d97c404e87d4d7c47fb0c7ff9acccc625ca2002848e0d53288b90ea  numpy-2.2.3-cp312-cp312-macosx_14_0_arm64.whl
    a5ae282abe60a2db0fd407072aff4599c279bcd6e9a2475500fc35b00a57c532  numpy-2.2.3-cp312-cp312-macosx_14_0_x86_64.whl
    5266de33d4c3420973cf9ae3b98b54a2a6d53a559310e3236c4b2b06b9c07d4e  numpy-2.2.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
    3b787adbf04b0db1967798dba8da1af07e387908ed1553a0d6e74c084d1ceafe  numpy-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
    34c1b7e83f94f3b564b35f480f5652a47007dd91f7c839f404d03279cc8dd021  numpy-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl
    4d8335b5f1b6e2bce120d55fb17064b0262ff29b459e8493d1785c18ae2553b8  numpy-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl
    4d9828d25fb246bedd31e04c9e75714a4087211ac348cb39c8c5f99dbb6683fe  numpy-2.2.3-cp312-cp312-win32.whl
    83807d445817326b4bcdaaaf8e8e9f1753da04341eceec705c001ff342002e5d  numpy-2.2.3-cp312-cp312-win_amd64.whl
    7bfdb06b395385ea9b91bf55c1adf1b297c9fdb531552845ff1d3ea6e40d5aba  numpy-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl
    23c9f4edbf4c065fddb10a4f6e8b6a244342d95966a48820c614891e5059bb50  numpy-2.2.3-cp313-cp313-macosx_11_0_arm64.whl
    a0c03b6be48aaf92525cccf393265e02773be8fd9551a2f9adbe7db1fa2b60f1  numpy-2.2.3-cp313-cp313-macosx_14_0_arm64.whl
    2376e317111daa0a6739e50f7ee2a6353f768489102308b0d98fcf4a04f7f3b5  numpy-2.2.3-cp313-cp313-macosx_14_0_x86_64.whl
    8fb62fe3d206d72fe1cfe31c4a1106ad2b136fcc1606093aeab314f02930fdf2  numpy-2.2.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
    52659ad2534427dffcc36aac76bebdd02b67e3b7a619ac67543bc9bfe6b7cdb1  numpy-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
    1b416af7d0ed3271cad0f0a0d0bee0911ed7eba23e66f8424d9f3dfcdcae1304  numpy-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl
    1402da8e0f435991983d0a9708b779f95a8c98c6b18a171b9f1be09005e64d9d  numpy-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl
    136553f123ee2951bfcfbc264acd34a2fc2f29d7cdf610ce7daf672b6fbaa693  numpy-2.2.3-cp313-cp313-win32.whl
    5b732c8beef1d7bc2d9e476dbba20aaff6167bf205ad9aa8d30913859e82884b  numpy-2.2.3-cp313-cp313-win_amd64.whl
    435e7a933b9fda8126130b046975a968cc2d833b505475e588339e09f7672890  numpy-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl
    7678556eeb0152cbd1522b684dcd215250885993dd00adb93679ec3c0e6e091c  numpy-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl
    2e8da03bd561504d9b20e7a12340870dfc206c64ea59b4cfee9fceb95070ee94  numpy-2.2.3-cp313-cp313t-macosx_14_0_arm64.whl
    c9aa4496fd0e17e3843399f533d62857cef5900facf93e735ef65aa4bbc90ef0  numpy-2.2.3-cp313-cp313t-macosx_14_0_x86_64.whl
    f4ca91d61a4bf61b0f2228f24bbfa6a9facd5f8af03759fe2a655c50ae2c6610  numpy-2.2.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
    deaa09cd492e24fd9b15296844c0ad1b3c976da7907e1c1ed3a0ad21dded6f76  numpy-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
    246535e2f7496b7ac85deffe932896a3577be7af8fb7eebe7146444680297e9a  numpy-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl
    daf43a3d1ea699402c5a850e5313680ac355b4adc9770cd5cfc2940e7861f1bf  numpy-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl
    cf802eef1f0134afb81fef94020351be4fe1d6681aadf9c5e862af6602af64ef  numpy-2.2.3-cp313-cp313t-win32.whl
    aee2512827ceb6d7f517c8b85aa5d3923afe8fc7a57d028cffcd522f1c6fd082  numpy-2.2.3-cp313-cp313t-win_amd64.whl
    3c2ec8a0f51d60f1e9c0c5ab116b7fc104b165ada3f6c58abf881cb2eb16044d  numpy-2.2.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl
    ed2cf9ed4e8ebc3b754d398cba12f24359f018b416c380f577bbae112ca52fc9  numpy-2.2.3-pp310-pypy310_pp73-macosx_14_0_x86_64.whl
    39261798d208c3095ae4f7bc8eaeb3481ea8c6e03dc48028057d3cbdbdb8937e  numpy-2.2.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
    783145835458e60fa97afac25d511d00a1eca94d4a8f3ace9fe2043003c678e4  numpy-2.2.3-pp310-pypy310_pp73-win_amd64.whl
    dbdc15f0c81611925f382dfa97b3bd0bc2c1ce19d4fe50482cb0ddc12ba30020  numpy-2.2.3.tar.gz

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMTEuMCIsInVwZGF0ZWRJblZlciI6IjM5LjIxMS4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: #71
Co-authored-by: Renovate <renovate@csw.im>
Co-committed-by: Renovate <renovate@csw.im>
2025-03-29 08:18:43 -04:00
e1b97974ae
chore(deps): update dependency mkdocstrings to v0.29.0 (#76)
Some checks are pending
Actions / Lint Code (Ruff & Pylint) (push) Waiting to run
Actions / Build Documentation (MkDocs) (push) Waiting to run
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [mkdocstrings](https://github.com/mkdocstrings/mkdocstrings) ([changelog](https://mkdocstrings.github.io/changelog)) | dependency-groups | minor | `==0.27.0` -> `==0.29.0` |

---

### Release Notes

<details>
<summary>mkdocstrings/mkdocstrings (mkdocstrings)</summary>

### [`v0.29.0`](https://github.com/mkdocstrings/mkdocstrings/blob/HEAD/CHANGELOG.md#0290---2025-03-10)

[Compare Source](https://github.com/mkdocstrings/mkdocstrings/compare/0.28.3...0.29.0)

<small>[Compare with 0.28.3](https://github.com/mkdocstrings/mkdocstrings/compare/0.28.3...0.29.0)</small>

**This is the last version before v1!**

##### Build

-   Depend on MkDocs 1.6 ([11bc400](11bc400ab7) by Timothée Mazzucotelli).

##### Features

-   Support rendering backlinks through handlers ([d4c7b9c](d4c7b9c42f) by Timothée Mazzucotelli). [Issue-723](https://github.com/mkdocstrings/mkdocstrings/issues/723), [Issue-mkdocstrings-python-153](https://github.com/mkdocstrings/python/issues/153), [PR-739](https://github.com/mkdocstrings/mkdocstrings/pull/739)

##### Code Refactoring

-   Save and forward titles to autorefs ([f49fb29](f49fb29582) by Timothée Mazzucotelli).
-   Use a combined event (each split with a different priority) for `on_env` ([8d1dd75](8d1dd754b4) by Timothée Mazzucotelli).

### [`v0.28.3`](https://github.com/mkdocstrings/mkdocstrings/blob/HEAD/CHANGELOG.md#0283---2025-03-08)

[Compare Source](https://github.com/mkdocstrings/mkdocstrings/compare/0.28.2...0.28.3)

<small>[Compare with 0.28.2](https://github.com/mkdocstrings/mkdocstrings/compare/0.28.2...0.28.3)</small>

##### Deprecations

All public objects must now be imported from the top-level `mkdocstrings` module. Importing from submodules is deprecated, and will raise errors starting with v1. This should be the last deprecation before v1.

##### Build

-   Make `python` extra depend on latest mkdocstrings-python (1.16.2) ([ba9003e](ba9003e96c) by Timothée Mazzucotelli).

##### Code Refactoring

-   Finish exposing/hiding public/internal objects ([0723fc2](0723fc25fd) by Timothée Mazzucotelli).
-   Re-expose public API in the top-level `mkdocstrings` module ([e66e080](e66e08096d) by Timothée Mazzucotelli).
-   Move modules to internal folder ([23fe23f](23fe23f110) by Timothée Mazzucotelli).

### [`v0.28.2`](https://github.com/mkdocstrings/mkdocstrings/blob/HEAD/CHANGELOG.md#0282---2025-02-24)

[Compare Source](https://github.com/mkdocstrings/mkdocstrings/compare/0.28.1...0.28.2)

<small>[Compare with 0.28.1](https://github.com/mkdocstrings/mkdocstrings/compare/0.28.1...0.28.2)</small>

##### Build

-   Depend on mkdocs-autorefs >= 1.4 ([2c22bdc](2c22bdc49f) by Timothée Mazzucotelli).

### [`v0.28.1`](https://github.com/mkdocstrings/mkdocstrings/blob/HEAD/CHANGELOG.md#0281---2025-02-14)

[Compare Source](https://github.com/mkdocstrings/mkdocstrings/compare/0.28.0...0.28.1)

<small>[Compare with 0.28.0](https://github.com/mkdocstrings/mkdocstrings/compare/0.28.0...0.28.1)</small>

##### Bug Fixes

-   Renew MkDocs' `relpath` processor instead of using same instance ([4ab180d](4ab180d019) by Timothée Mazzucotelli). [Issue-mkdocs-3919](https://github.com/mkdocs/mkdocs/issues/3919)

### [`v0.28.0`](https://github.com/mkdocstrings/mkdocstrings/blob/HEAD/CHANGELOG.md#0280---2025-02-03)

[Compare Source](https://github.com/mkdocstrings/mkdocstrings/compare/0.27.0...0.28.0)

<small>[Compare with 0.27.0](https://github.com/mkdocstrings/mkdocstrings/compare/0.27.0...0.28.0)</small>

##### Breaking Changes

Although the following changes are "breaking" in terms of public API, we didn't find any public use of these classes and methods on GitHub.

-   `mkdocstrings.extension.AutoDocProcessor.__init__(parser)`: *Parameter was removed*
-   `mkdocstrings.extension.AutoDocProcessor.__init__(md)`: *Positional parameter was moved*
-   `mkdocstrings.extension.AutoDocProcessor.__init__(config)`: *Parameter was removed*
-   `mkdocstrings.extension.AutoDocProcessor.__init__(handlers)`: *Parameter kind was changed*: `positional or keyword` -> `keyword-only`
-   `mkdocstrings.extension.AutoDocProcessor.__init__(autorefs)`: *Parameter kind was changed*: `positional or keyword` -> `keyword-only`
-   `mkdocstrings.extension.MkdocstringsExtension.__init__(config)`: *Parameter was removed*
-   `mkdocstrings.extension.MkdocstringsExtension.__init__(handlers)`: *Positional parameter was moved*
-   `mkdocstrings.extension.MkdocstringsExtension.__init__(autorefs)`: *Positional parameter was moved*
-   `mkdocstrings.handlers.base.Handlers.__init__(config)`: *Parameter was removed*
-   `mkdocstrings.handlers.base.Handlers.__init__(theme)`: *Parameter was added as required*
-   `mkdocstrings.handlers.base.Handlers.__init__(default)`: *Parameter was added as required*
-   `mkdocstrings.handlers.base.Handlers.__init__(inventory_project)`: *Parameter was added as required*
-   `mkdocstrings.handlers.base.Handlers.__init__(tool_config)`: *Parameter was added as required*

Similarly, the following parameters were renamed, but the methods are only called from our own code, using positional arguments.

-   `mkdocstrings.handlers.base.BaseHandler.collect(config)`: *Parameter was renamed `options`*
-   `mkdocstrings.handlers.base.BaseHandler.render(config)`: *Parameter was renamed `options`*

Finally, the following method was removed, but this is again taken into account in our own code:

-   `mkdocstrings.handlers.base.BaseHandler.get_anchors`: *Public object was removed*

For these reasons, and because we're still in v0, we do not bump to v1 yet. See following deprecations.

##### Deprecations

*mkdocstrings* 0.28 will start emitting these deprecations warnings:

> The `handler` argument is deprecated. The handler name must be specified as a class attribute.

Previously, the `get_handler` function would pass a `handler` (name) argument to the handler constructor. This name must now be set on the handler's class directly.

```python
class MyHandler:
    name = "myhandler"
```

> The `domain` attribute must be specified as a class attribute.

The `domain` class attribute on handlers is now mandatory and cannot be an empty string.

```python
class MyHandler:
    domain = "mh"
```

> The `theme` argument must be passed as a keyword argument.

This argument could previously be passed as a positional argument (from the `get_handler` function), and must now be passed as a keyword argument.

> The `custom_templates` argument must be passed as a keyword argument.

Same as for `theme`, but with `custom_templates`.

> The `mdx` argument must be provided (as a keyword argument).

The `get_handler` function now receives a `mdx` argument, which it must forward to the handler constructor and then to the base handler, either explicitly or through `**kwargs`:

\=== "Explicitly"

    ```python
    def get_handler(..., mdx, ...):
        return MyHandler(..., mdx=mdx, ...)

    class MyHandler:
        def __init__(self, ..., mdx, ...):
            super().__init__(..., mdx=mdx, ...)
    ```

\=== "Through `**kwargs`"

    ```python
    def get_handler(..., **kwargs):
        return MyHandler(..., **kwargs)

    class MyHandler:
        def __init__(self, ..., **kwargs):
            super().__init__(**kwargs)
    ```

In the meantime we still retrieve this `mdx` value at a different moment, by reading it from the MkDocs configuration.

> The `mdx_config` argument must be provided (as a keyword argument).

Same as for `mdx`, but with `mdx_config`.

> mkdocstrings v1 will stop handling 'import' in handlers configuration. Instead your handler must define a `get_inventory_urls` method that returns a list of URLs to download.

Previously, mkdocstrings would pop the `import` key from a handler's configuration to download each item (URLs). Items could be strings, or dictionaries with a `url` key. Now mkdocstrings gives back control to handlers, which must store this inventory configuration within them, and expose it again through a `get_inventory_urls` method. This method returns a list of tuples: an URL, and a dictionary of options that will be passed again to their `load_inventory` method. Handlers have now full control over the "inventory" setting.

```python
from copy import deepcopy

def get_handler(..., handler_config, ...):
    return MyHandler(..., config=handler_config, ...)

class MyHandler:
    def __init__(self, ..., config, ...):
        self.config = config

    def get_inventory_urls(self):
        config = deepcopy(self.config["import"])
        return [(inv, {}) if isinstance(inv, str) else (inv.pop("url"), inv) for inv in config]
```

Changing the name of the key (for example from `import` to `inventories`) involves a change in user configuration, and both keys will have to be supported by your handler for some time.

```python
def get_handler(..., handler_config, ...):
    if "inventories" not in handler_config and "import" in handler_config:
        warn("The 'import' key is renamed 'inventories'", FutureWarning)
        handler_config["inventories"] = handler_config.pop("import")
    return MyHandler(..., config=handler_config, ...)
```

> Setting a fallback anchor function is deprecated and will be removed in a future release.

This comes from mkdocstrings and mkdocs-autorefs, and will disappear with mkdocstrings v0.28.

> mkdocstrings v1 will start using your handler's `get_options` method to build options instead of merging the global and local options (dictionaries).

Handlers must now store their own global options (in an instance attribute), and implement a `get_options` method that receives `local_options` (a dict) and returns combined options (dict or custom object). These combined options are then passed to `collect` and `render`, so that these methods can use them right away.

```python
def get_handler(..., handler_config, ...):
    return MyHandler(..., config=handler_config, ...)

class MyHandler:
    def __init__(self, ..., config, ...):
        self.config = config

    def get_options(local_options):
        return {**self.default_options, **self.config["options"], **local_options}
```

> The `update_env(md)` parameter is deprecated. Use `self.md` instead.

Handlers can remove the `md` parameter from their `update_env` method implementation, and use `self.md` instead, if they need it.

> No need to call `super().update_env()` anymore.

Handlers don't have to call the parent `update_env` method from their own implementation anymore, and can just drop the call.

> The `get_anchors` method is deprecated. Declare a `get_aliases` method instead, accepting a string (identifier) instead of a collected object.

Previously, handlers would implement a `get_anchors` method that received a data object (typed `CollectorItem`) to return aliases for this object. This forced mkdocstrings to collect this object through the handler's `collect` method, which then required some logic with "fallback config" as to prevent unwanted collection. mkdocstrings gives back control to handlers and now calls `get_aliases` instead, which accepts an `identifier` (string) and lets the handler decide how to return aliases for this identifier. For example, it can replicate previous behavior by calling its own `collect` method with its own "fallback config", or do something different (cache lookup, etc.).

```python
class MyHandler:
    def get_aliases(identifier):
        try:
            obj = self.collect(identifier, self.fallback_config)

### or obj = self._objects_cache[identifier]
        except CollectionError:  # or KeyError
            return ()
        return ...  # previous logic in `get_anchors`
```

> The `config_file_path` argument in `get_handler` functions is deprecated. Use `tool_config.get('config_file_path')` instead.

The `config_file_path` argument is now deprecated and only passed to `get_handler` functions if they accept it. If you used it to compute a "base directory", you can now use the `tool_config` argument instead, which is the configuration of the SSG tool in use (here MkDocs):

```python
base_dir = Path(tool_config.config_file_path or "./mkdocs.yml").parent
```

**Most of these warnings will disappear with the next version of mkdocstrings-python.**

##### Bug Fixes

-   Update handlers in JSON schema to be an object instead of an array ([3cf7d51](3cf7d51704) by Matthew Messinger). [Issue-733](https://github.com/mkdocstrings/mkdocstrings/issues/733), [PR-734](https://github.com/mkdocstrings/mkdocstrings/pull/734)
-   Fix broken table of contents when nesting autodoc instructions ([12c8f82](12c8f82e9a) by Timothée Mazzucotelli). [Issue-348](https://github.com/mkdocstrings/mkdocstrings/issues/348)

##### Code Refactoring

-   Pass `config_file_path` to `get_handler` if it expects it ([8c476ee](8c476ee0b8) by Timothée Mazzucotelli).
-   Give back inventory control to handlers ([b84653f](b84653f2b1) by Timothée Mazzucotelli). [Related-to-issue-719](https://github.com/mkdocstrings/mkdocstrings/issues/719)
-   Give back control to handlers on how they want to handle global/local options ([c00de7a](c00de7a42b) by Timothée Mazzucotelli). [Issue-719](https://github.com/mkdocstrings/mkdocstrings/issues/719)
-   Deprecate base handler's `get_anchors` method in favor of `get_aliases` method ([7a668f0](7a668f0f73) by Timothée Mazzucotelli).
-   Register all identifiers of rendered objects into autorefs ([434d8c7](434d8c7cd1) by Timothée Mazzucotelli).
-   Use mkdocs-get-deps' download utility to remove duplicated code ([bb87cd8](bb87cd833f) by Timothée Mazzucotelli).
-   Clean up data passed down from plugin to extension and handlers ([b8e8703](b8e87036e0) by Timothée Mazzucotelli). [PR-726](https://github.com/mkdocstrings/mkdocstrings/pull/726)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMTEuMCIsInVwZGF0ZWRJblZlciI6IjM5LjIxMS4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: #76
Co-authored-by: Renovate <renovate@csw.im>
Co-committed-by: Renovate <renovate@csw.im>
2025-03-29 08:18:33 -04:00
54e736e313
chore(deps): update dependency mkdocs-material to v9.6.9 (#75)
Some checks are pending
Actions / Lint Code (Ruff & Pylint) (push) Waiting to run
Actions / Build Documentation (MkDocs) (push) Waiting to run
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [mkdocs-material](https://github.com/squidfunk/mkdocs-material) ([changelog](https://squidfunk.github.io/mkdocs-material/changelog/)) | dependency-groups | minor | `==9.5.50` -> `==9.6.9` |

---

### Release Notes

<details>
<summary>squidfunk/mkdocs-material (mkdocs-material)</summary>

### [`v9.6.9`](https://github.com/squidfunk/mkdocs-material/releases/tag/9.6.9): mkdocs-material-9.6.9

[Compare Source](https://github.com/squidfunk/mkdocs-material/compare/9.6.8...9.6.9)

-   Updated Serbo-Croatian translations
-   Fixed [#&#8203;8086](https://github.com/squidfunk/mkdocs-material/issues/8086): Custom SVG icons containing hashes break rendering
-   Fixed [#&#8203;8067](https://github.com/squidfunk/mkdocs-material/issues/8067): Drawer has gap on right side in Firefox on some OSs

### [`v9.6.8`](https://github.com/squidfunk/mkdocs-material/releases/tag/9.6.8): mkdocs-material-9.6.8

[Compare Source](https://github.com/squidfunk/mkdocs-material/compare/9.6.7...9.6.8)

-   Added Welsh translations
-   Fixed [#&#8203;8076](https://github.com/squidfunk/mkdocs-material/issues/8076): Privacy plugin crashes if HTTP download fails

### [`v9.6.7`](https://github.com/squidfunk/mkdocs-material/releases/tag/9.6.7): mkdocs-material-9.6.7

[Compare Source](https://github.com/squidfunk/mkdocs-material/compare/9.6.6...9.6.7)

-   Fixed [#&#8203;8056](https://github.com/squidfunk/mkdocs-material/issues/8056): Error in backrefs implementation (9.6.6 regression)
-   Fixed [#&#8203;8054](https://github.com/squidfunk/mkdocs-material/issues/8054): Unescaped quotes in ARIA labels of table of contents

### [`v9.6.6`](https://github.com/squidfunk/mkdocs-material/releases/tag/9.6.6): mkdocs-material-9.6.6

[Compare Source](https://github.com/squidfunk/mkdocs-material/compare/9.6.5...9.6.6)

-   Fixed [#&#8203;8040](https://github.com/squidfunk/mkdocs-material/issues/8040): Privacy plugin not replacing exteral assets (9.6.5 regression)
-   Fixed [#&#8203;8031](https://github.com/squidfunk/mkdocs-material/issues/8031): Replace unmaintained `regex` package in search plugin

### [`v9.6.5`](https://github.com/squidfunk/mkdocs-material/releases/tag/9.6.5): mkdocs-material-9.6.5

[Compare Source](https://github.com/squidfunk/mkdocs-material/compare/9.6.4...9.6.5)

-   Fixed [#&#8203;8016](https://github.com/squidfunk/mkdocs-material/issues/8016): Tags listing not showing when when file name has spaces
-   Fixed [#&#8203;8012](https://github.com/squidfunk/mkdocs-material/issues/8012): Privacy plugin crashes if HTTP download fails

### [`v9.6.4`](https://github.com/squidfunk/mkdocs-material/releases/tag/9.6.4): mkdocs-material-9.6.4

[Compare Source](https://github.com/squidfunk/mkdocs-material/compare/9.6.3...9.6.4)

-   Fixed [#&#8203;7985](https://github.com/squidfunk/mkdocs-material/issues/7985): Blog content sometimes not stretching to full width
-   Fixed [#&#8203;7978](https://github.com/squidfunk/mkdocs-material/issues/7978): Navigation rendering bug in Safari 18.3

### [`v9.6.3`](https://github.com/squidfunk/mkdocs-material/releases/tag/9.6.3): mkdocs-material-9.6.3

[Compare Source](https://github.com/squidfunk/mkdocs-material/compare/9.6.2...9.6.3)

-   Fixed rendering of arrow heads in Mermaid.js class diagrams
-   Fixed [#&#8203;7960](https://github.com/squidfunk/mkdocs-material/issues/7960): Tags plugin crashes on numeric metadata titles

### [`v9.6.2`](https://github.com/squidfunk/mkdocs-material/releases/tag/9.6.2): mkdocs-material-9.6.2

[Compare Source](https://github.com/squidfunk/mkdocs-material/compare/9.6.1...9.6.2)

-   Fixed [#&#8203;7955](https://github.com/squidfunk/mkdocs-material/issues/7955): Excessively long words don't break on narrow screens
-   Fixed [#&#8203;7947](https://github.com/squidfunk/mkdocs-material/issues/7947): Scope setting interferes with outdated version banner

### [`v9.6.1`](https://github.com/squidfunk/mkdocs-material/releases/tag/9.6.1): mkdocs-material-9.6.1

[Compare Source](https://github.com/squidfunk/mkdocs-material/compare/9.6.0...9.6.1)

-   Fixed [#&#8203;7943](https://github.com/squidfunk/mkdocs-material/issues/7943): Tags plugin crashing due to merge error

### [`v9.6.0`](https://github.com/squidfunk/mkdocs-material/releases/tag/9.6.0): mkdocs-material-9.6.0

[Compare Source](https://github.com/squidfunk/mkdocs-material/compare/9.5.50...9.6.0)

-   Added meta plugin
-   Rewrite of the tags plugin
-   Added support for allow lists in tags plugin
-   Added support for and custom sorting in tags plugin
-   Added support for related links in blog plugin
-   Added support for custom index pages in blog plugin
-   Added support for navigation subtitles
-   Fixed [#&#8203;7924](https://github.com/squidfunk/mkdocs-material/issues/7924): Anchors might require two clicks when using instant navigation

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMTEuMCIsInVwZGF0ZWRJblZlciI6IjM5LjIxMS4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: #75
Co-authored-by: Renovate <renovate@csw.im>
Co-committed-by: Renovate <renovate@csw.im>
2025-03-29 08:17:43 -04:00
8868323e35
fix(deps): update dependency red-discordbot to v3.5.18 (#73)
Some checks are pending
Actions / Lint Code (Ruff & Pylint) (push) Waiting to run
Actions / Build Documentation (MkDocs) (push) Waiting to run
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [red-discordbot](https://github.com/Cog-Creators/Red-DiscordBot) ([changelog](https://docs.discord.red/en/stable/changelog.html)) | project.dependencies | patch | `==3.5.17` -> `==3.5.18` |

---

### Release Notes

<details>
<summary>Cog-Creators/Red-DiscordBot (red-discordbot)</summary>

### [`v3.5.18`](https://github.com/Cog-Creators/Red-DiscordBot/blob/HEAD/CHANGES.rst#Redbot-3518-2025-03-26)

[Compare Source](https://github.com/Cog-Creators/Red-DiscordBot/compare/3.5.17...3.5.18)

\==========================

| Thanks to all these amazing people that contributed to this release:
| :ghuser:`Jackenmen`, :ghuser:`Kreusada`

## Read before updating

\#. Information for Audio users that are using an external Lavalink instance (if you don't know what that is, you should skip this point):

    We've updated our default application.yml file and you should update your instance's ``application.yml`` accordingly.
    More specifically, we bumped the version of YT source plugin.
    `Download Red 3.5.18's default application.yml file <https://github.com/Cog-Creators/Red-DiscordBot/releases/download/3.5.18/Red-DiscordBot-3.5.18-default-lavalink-application.yml>`__

## End-user changelog

Changes

***

-   **Core - Dependencies** - Red's dependencies have been bumped (:issue:`6543`)

Fixes

***

-   |cool| **Cogs - Audio** - Fixed recent YT playback issues (:issue:`6542`)

***

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMTEuMCIsInVwZGF0ZWRJblZlciI6IjM5LjIxMS4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: #73
Co-authored-by: Renovate <renovate@csw.im>
Co-committed-by: Renovate <renovate@csw.im>
2025-03-29 08:17:27 -04:00
5d64248ce1
fix(deps): update dependency pip to v25.0.1 (#72)
Some checks are pending
Actions / Lint Code (Ruff & Pylint) (push) Waiting to run
Actions / Build Documentation (MkDocs) (push) Waiting to run
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [pip](https://github.com/pypa/pip) ([changelog](https://pip.pypa.io/en/stable/news/)) | project.dependencies | patch | `==25.0` -> `==25.0.1` |

---

### Release Notes

<details>
<summary>pypa/pip (pip)</summary>

### [`v25.0.1`](https://github.com/pypa/pip/compare/25.0...25.0.1)

[Compare Source](https://github.com/pypa/pip/compare/25.0...25.0.1)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMTEuMCIsInVwZGF0ZWRJblZlciI6IjM5LjIxMS4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: #72
Co-authored-by: Renovate <renovate@csw.im>
Co-committed-by: Renovate <renovate@csw.im>
2025-03-29 08:17:06 -04:00
293e3fbc4c
chore(deps): update dependency mkdocs-git-authors-plugin to v0.9.4 (#69)
All checks were successful
Actions / Lint Code (Ruff & Pylint) (push) Successful in 55s
Actions / Build Documentation (MkDocs) (push) Successful in 1m4s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [mkdocs-git-authors-plugin](https://github.com/timvink/mkdocs-git-authors-plugin) | dependency-groups | patch | `==0.9.2` -> `==0.9.4` |

---

### Release Notes

<details>
<summary>timvink/mkdocs-git-authors-plugin (mkdocs-git-authors-plugin)</summary>

### [`v0.9.4`](https://github.com/timvink/mkdocs-git-authors-plugin/releases/tag/v0.9.4): git-authors v0.9.4

[Compare Source](https://github.com/timvink/mkdocs-git-authors-plugin/compare/v0.9.3...v0.9.4)

#### What's Changed

-   Fix docs_dir git finding regression by [@&#8203;timvink](https://github.com/timvink) in https://github.com/timvink/mkdocs-git-authors-plugin/pull/106
-   Update Gitlab Build Instructions by [@&#8203;jmrtnz94](https://github.com/jmrtnz94) in https://github.com/timvink/mkdocs-git-authors-plugin/pull/105

#### New Contributors

-   [@&#8203;jmrtnz94](https://github.com/jmrtnz94) made their first contribution in https://github.com/timvink/mkdocs-git-authors-plugin/pull/105

**Full Changelog**: https://github.com/timvink/mkdocs-git-authors-plugin/compare/v0.9.3...v0.9.4

### [`v0.9.3`](https://github.com/timvink/mkdocs-git-authors-plugin/releases/tag/v0.9.3): git-authors v0.9.3

[Compare Source](https://github.com/timvink/mkdocs-git-authors-plugin/compare/v0.9.2...v0.9.3)

#### What's Changed

-   Ignore whitespace when determining authors by [@&#8203;timvink](https://github.com/timvink) in https://github.com/timvink/mkdocs-git-authors-plugin/pull/102
-   Support git inside documentation folder by [@&#8203;timvink](https://github.com/timvink) in https://github.com/timvink/mkdocs-git-authors-plugin/pull/103

**Full Changelog**: https://github.com/timvink/mkdocs-git-authors-plugin/compare/v0.9.2...v0.9.3

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMTEuMCIsInVwZGF0ZWRJblZlciI6IjM5LjIxMS4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: #69
Co-authored-by: Renovate <renovate@csw.im>
Co-committed-by: Renovate <renovate@csw.im>
2025-03-29 08:13:07 -04:00
e0137734b5
chore(deps): update dependency pylint to v3.3.6 (#70)
Some checks failed
Actions / Build Documentation (MkDocs) (push) Has been cancelled
Actions / Lint Code (Ruff & Pylint) (push) Has been cancelled
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [pylint](https://github.com/pylint-dev/pylint) ([changelog](https://pylint.readthedocs.io/en/latest/whatsnew/3/)) | tool.uv.dev-dependencies | patch | `==3.3.3` -> `==3.3.6` |

---

### Release Notes

<details>
<summary>pylint-dev/pylint (pylint)</summary>

### [`v3.3.6`](https://github.com/pylint-dev/pylint/releases/tag/v3.3.6)

[Compare Source](https://github.com/pylint-dev/pylint/compare/v3.3.5...v3.3.6)

## What's new in Pylint 3.3.6?

Release date: 2025-03-20

## False Positives Fixed

-   Fix a false positive for `used-before-assignment` when an inner function's return type
    annotation is a class defined at module scope.

    Closes [#&#8203;9391](https://github.com/pylint-dev/pylint/issues/9391)

### [`v3.3.5`](https://github.com/pylint-dev/pylint/releases/tag/v3.3.5)

[Compare Source](https://github.com/pylint-dev/pylint/compare/v3.3.4...v3.3.5)

## What's new in Pylint 3.3.5?

Release date: 2025-03-09

## False Positives Fixed

-   Fix false positives for `use-implicit-booleaness-not-comparison`, `use-implicit-booleaness-not-comparison-to-string`
    and `use-implicit-booleaness-not-comparison-to-zero` when chained comparisons are checked.

    Closes [#&#8203;10065](https://github.com/pylint-dev/pylint/issues/10065)

-   Fix a false positive for `invalid-getnewargs-ex-returned` when the tuple or dict has been assigned to a name.

    Closes [#&#8203;10208](https://github.com/pylint-dev/pylint/issues/10208)

-   Remove `getopt` and `optparse` from the list of deprecated modules.

    Closes [#&#8203;10211](https://github.com/pylint-dev/pylint/issues/10211)

## Other Bug Fixes

-   Fixed conditional import x.y causing false positive possibly-used-before-assignment.

    Closes [#&#8203;10081](https://github.com/pylint-dev/pylint/issues/10081)

-   Fix a crash when something besides a class is found in an except handler.

    Closes [#&#8203;10106](https://github.com/pylint-dev/pylint/issues/10106)

-   Fixed raising invalid-name when using camelCase for private methods with two leading underscores.

    Closes [#&#8203;10189](https://github.com/pylint-dev/pylint/issues/10189)

## Other Changes

-   Upload release assets to PyPI via Trusted Publishing.

    Closes [#&#8203;10256](https://github.com/pylint-dev/pylint/issues/10256)

### [`v3.3.4`](https://github.com/pylint-dev/pylint/releases/tag/v3.3.4)

[Compare Source](https://github.com/pylint-dev/pylint/compare/v3.3.3...v3.3.4)

## Other Bug Fixes

-   Fixes "skipped files" count calculation; the previous method was displaying an arbitrary number.

    Closes [#&#8203;10073](https://github.com/pylint-dev/pylint/issues/10073)

-   Fixes a crash that occurred when pylint was run in a container on a host with cgroupsv2 and restrictions on CPU usage.

    Closes [#&#8203;10103](https://github.com/pylint-dev/pylint/issues/10103)

-   Relaxed the requirements for isort so pylint can benefit from isort 6.

    Closes [#&#8203;10203](https://github.com/pylint-dev/pylint/issues/10203)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMTEuMCIsInVwZGF0ZWRJblZlciI6IjM5LjIxMS4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: #70
Co-authored-by: Renovate <renovate@csw.im>
Co-committed-by: Renovate <renovate@csw.im>
2025-03-29 08:12:59 -04:00
7562e1eff2
chore(repo): switch to exact versions, as opposed to lower bound version ranges
All checks were successful
Actions / Lint Code (Ruff & Pylint) (push) Successful in 55s
Actions / Build Documentation (MkDocs) (push) Successful in 1m7s
this is because of
https://github.com/renovatebot/renovate/discussions/34181
2025-03-29 07:09:51 -05:00
12b8dbdbdf
chore(repo): update readme
All checks were successful
Actions / Build Documentation (MkDocs) (push) Successful in 35s
Actions / Lint Code (Ruff & Pylint) (push) Successful in 40s
2025-03-28 10:37:49 -05:00
1c557f6f1f
fix(actions): correct meli url
All checks were successful
Actions / Build Documentation (MkDocs) (push) Successful in 33s
Actions / Lint Code (Ruff & Pylint) (push) Successful in 37s
2025-03-28 10:25:01 -05:00
2505dd0980
fix(aurora): more ruff fixes
All checks were successful
Actions / Build Documentation (MkDocs) (push) Successful in 32s
Actions / Lint Code (Ruff & Pylint) (push) Successful in 36s
2025-03-28 10:22:08 -05:00
4407a99b8e
style(pterodactyl): ruff reformat
All checks were successful
Actions / Build Documentation (MkDocs) (push) Successful in 33s
Actions / Lint Code (Ruff & Pylint) (push) Successful in 39s
2025-03-28 10:17:43 -05:00
df22f73d2e
style(aurora): reformat with ruff 2025-03-28 10:17:35 -05:00
ab22b791d1
fix(hotreload): pylint fixes
All checks were successful
Actions / Build Documentation (MkDocs) (push) Successful in 33s
Actions / Lint Code (Ruff & Pylint) (push) Successful in 36s
2025-03-28 10:16:26 -05:00
c0c84358ef
fix(backup): pylint fix 2025-03-28 10:14:30 -05:00
336e01456c
fix(aurora): bandaid fixes so pylint passes
these are not good changes in the slightest and are only here because
pylint is failing otherwise, you should really be using aurora v3
instead of v2 even though it's in development
2025-03-28 10:13:48 -05:00
cc1745a9d2
Remove the dev container (#68)
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 36s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 39s
I no longer need the dev container, as I use neovim where this isn't useful to me, and the nix flake is plenty good enough.

<!-- Create a new issue, if it doesn't exist yet -->

- [x] By submitting this pull request, I permit [cswimr](https://www.coastalcommits.com/cswimr) to license my work under
  the [Mozilla Public License Version 2.0](https://www.coastalcommits.com/cswimr/SeaCogs/src/branch/main/LICENSE).

Reviewed-on: #68
2025-03-28 11:05:00 -04:00
eefd5ece09
chore(deps): pin dependencies (#66)
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 35s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 37s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [actions/env](https://c.csw.im/actions/env) | action | pinDigest |  -> `1791216` |
| [actions/setup-uv](https://c.csw.im/actions/setup-uv) | action | pinDigest |  -> `2269511` |
| catthehacker/ubuntu | container | pinDigest |  -> `70d7485` |

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://github.com/renovatebot/renovate/discussions) if that's undesired.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xNjYuMCIsInVwZGF0ZWRJblZlciI6IjM5LjIxMS4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: #66
Co-authored-by: Renovate <renovate@csw.im>
Co-committed-by: Renovate <renovate@csw.im>
2025-03-28 10:54:31 -04:00
0503f3b78d
feat(backup): update to most recent red version
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 30s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 36s
2025-03-22 09:16:34 -04:00
4f01814896
fix(actions): switch back to local mirrored actions
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 33s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 37s
2025-03-19 15:37:53 -04:00
4661555aa6
fix(actions): use full urls for actions
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 40s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 43s
2025-03-19 13:57:15 -04:00
3e345cc3af
fix(actions): change actions urls
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 40s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 46s
2025-03-19 08:19:05 -04:00
4844f5892c
fix(actions): change the meli url
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 29s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 36s
2025-03-19 08:05:09 -04:00
1d502bbbe8
fix(actions): use the full github url
Some checks failed
Actions / Build Documentation (MkDocs) (push) Failing after 59s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 48s
2025-03-18 03:58:47 -05:00
8c4afd87c9
Revert "fix(actions): don't use uv run"
Some checks failed
Actions / Lint Code (Ruff & Pylint) (push) Failing after 50s
Actions / Build Documentation (MkDocs) (push) Failing after 59s
This reverts commit 091a23d610.
2025-03-18 03:57:07 -05:00
091a23d610
fix(actions): don't use uv run
Some checks failed
Actions / Lint Code (Ruff & Pylint) (push) Failing after 31s
Actions / Build Documentation (MkDocs) (push) Failing after 37s
`setup-uv` should be activating the virtual environment anyway, this is
unnecessary
2025-03-18 03:55:14 -05:00
d00776c006
fix(actions): don't prune the uv cachce
Some checks failed
Actions / Lint Code (Ruff & Pylint) (push) Failing after 52s
Actions / Build Documentation (MkDocs) (push) Failing after 58s
2025-03-18 03:47:03 -05:00
a19761b720
fix(actions): use a github token to prevent ratelimiting
Some checks failed
Actions / Lint Code (Ruff & Pylint) (push) Failing after 52s
Actions / Build Documentation (MkDocs) (push) Failing after 1m1s
2025-03-18 03:42:37 -05:00
6354559d3a
fix(actions): install the right dependencies
Some checks failed
Actions / Lint Code (Ruff & Pylint) (push) Failing after 7s
Actions / Build Documentation (MkDocs) (push) Failing after 7s
2025-03-18 03:24:48 -05:00
fd409d450f
fix(actions): install dependencies in a separate step
Some checks failed
Actions / Lint Code (Ruff & Pylint) (push) Failing after 50s
Actions / Build Documentation (MkDocs) (push) Failing after 47s
2025-03-18 03:20:08 -05:00
6cd44c1fc7
fix(actions): fix actions workflow & use the uv action to setup uv 2025-03-18 03:20:03 -05:00
15b7ef61b1
fix(pterodactyl): fix some incorrect function names
Some checks failed
Actions / Lint Code (Ruff & Pylint) (push) Failing after 0s
Actions / Build Documentation (MkDocs) (push) Failing after 0s
2025-02-21 15:16:06 -06:00
10b31c81b3
fix(pterodactyl): use the correct language for some codeblocks
Some checks are pending
Actions / Lint Code (Ruff & Pylint) (push) Waiting to run
Actions / Build Documentation (MkDocs) (push) Waiting to run
2025-02-21 11:58:07 -06:00
3fdb0836cf
chore(pterodactyl): bump version
Some checks are pending
Actions / Lint Code (Ruff & Pylint) (push) Waiting to run
Actions / Build Documentation (MkDocs) (push) Waiting to run
2025-02-21 10:27:10 -06:00
6bd2a2cc68
fix(pterodactyl): fix an attributeerror
Some checks are pending
Actions / Lint Code (Ruff & Pylint) (push) Waiting to run
Actions / Build Documentation (MkDocs) (push) Waiting to run
2025-02-21 10:03:07 -06:00
48c6289b2d
chore(deps): use py-dactyl from upstream instead of my fork
Some checks are pending
Actions / Lint Code (Ruff & Pylint) (push) Waiting to run
Actions / Build Documentation (MkDocs) (push) Waiting to run
2025-02-21 09:54:40 -06:00
346963fd4f
chore(tooling): fix editorconfig having the wrong indent size for python files
Some checks failed
Actions / Build Documentation (MkDocs) (push) Failing after 27s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 47s
2025-02-18 14:39:51 -06:00
d89c156744
chore(deps): update ghcr.io/astral-sh/uv docker tag to v0.5.30 (#53)
Some checks failed
Actions / Build Documentation (MkDocs) (push) Failing after 0s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 47s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [ghcr.io/astral-sh/uv](https://github.com/astral-sh/uv) | stage | patch | `0.5.24` -> `0.5.30` |

---

> ⚠️ **Warning**
>
> Some dependencies could not be looked up. Check the Dependency Dashboard for more information.

---

### Release Notes

<details>
<summary>astral-sh/uv (ghcr.io/astral-sh/uv)</summary>

### [`v0.5.30`](https://github.com/astral-sh/uv/blob/HEAD/CHANGELOG.md#0530)

[Compare Source](https://github.com/astral-sh/uv/compare/0.5.29...0.5.30)

##### Python

The managed PyPy distributions have been updated for PyPy v7.3.18, which includes:

-   PyPy3.10, which updates the standard library from Python 3.10.14 to 3.10.19
-   PyPy3.11, which adds beta support for Python 3.11.11

See the [PyPy release](https://pypy.org/posts/2025/02/pypy-v7318-release.html) for more details.

##### Enhancements

-   Add `uv sync --dry-run` ([#&#8203;11299](https://github.com/astral-sh/uv/pull/11299))
-   Ignore `#egg` fragment in HTML Simple API response ([#&#8203;11340](https://github.com/astral-sh/uv/pull/11340))

##### Configuration

-   Add `NO_BINARY` and `NO_BINARY_PACKAGE` environment variables ([#&#8203;11399](https://github.com/astral-sh/uv/pull/11399))

##### Performance

-   Avoid re-cloning name when populating ambiguous set ([#&#8203;11401](https://github.com/astral-sh/uv/pull/11401))
-   Optimize flattening in large workspaces ([#&#8203;11313](https://github.com/astral-sh/uv/pull/11313))

##### Bug fixes

-   Allow dynamic packages to be overloaded ([#&#8203;11400](https://github.com/astral-sh/uv/pull/11400))
-   Fix credential caching for index roots when URL ends in `simple/` ([#&#8203;11336](https://github.com/astral-sh/uv/pull/11336))
-   Fix marker merging for requirements.txt for psycopg ([#&#8203;11298](https://github.com/astral-sh/uv/pull/11298))
-   Set 777 permissions on locked files ([#&#8203;11328](https://github.com/astral-sh/uv/pull/11328))
-   Support extras in `@` requests for tools ([#&#8203;11335](https://github.com/astral-sh/uv/pull/11335))
-   Upgrade `astral-tokio-tar` to v0.5.1 ([#&#8203;11359](https://github.com/astral-sh/uv/pull/11359))
-   Avoid missing logging for no-op upgrade events ([#&#8203;11301](https://github.com/astral-sh/uv/pull/11301))
-   Use refined specifiers when logging narrowed Python range ([#&#8203;11334](https://github.com/astral-sh/uv/pull/11334))
-   Don't use popup-generating `eprintln` in trampoline warnings ([#&#8203;11295](https://github.com/astral-sh/uv/pull/11295))
-   Patch pkg-config files to be relocatable ([#&#8203;11291](https://github.com/astral-sh/uv/pull/11291))
-   Fix a case of duplicate `torch` packages when using conflicting extras ([#&#8203;11323](https://github.com/astral-sh/uv/pull/11323))

##### Documentation

-   Add docs for `uv tool install --editable` ([#&#8203;11280](https://github.com/astral-sh/uv/pull/11280))
-   Fix broken anchors in README and docs index ([#&#8203;11338](https://github.com/astral-sh/uv/pull/11338))

### [`v0.5.29`](https://github.com/astral-sh/uv/blob/HEAD/CHANGELOG.md#0529)

[Compare Source](https://github.com/astral-sh/uv/compare/0.5.28...0.5.29)

##### Enhancements

-   Add `--bare` option to `uv init` ([#&#8203;11192](https://github.com/astral-sh/uv/pull/11192))
-   Add support for respecting `VIRTUAL_ENV` in project commands via `--active` ([#&#8203;11189](https://github.com/astral-sh/uv/pull/11189))
-   Allow the project `VIRTUAL_ENV` warning to be silenced with `--no-active` ([#&#8203;11251](https://github.com/astral-sh/uv/pull/11251))

##### Python

The managed Python distributions have been updated, including:

-   CPython 3.12.9
-   CPython 3.13.2
-   pkg-config files are now relocatable

See the [`python-build-standalone` release notes](20250205) for more details.

##### Bug fixes

-   Always use base Python discovery logic for cached environments ([#&#8203;11254](https://github.com/astral-sh/uv/pull/11254))
-   Use a flock to avoid concurrent initialization of project environments ([#&#8203;11259](https://github.com/astral-sh/uv/pull/11259))
-   Fix handling of `--all-groups` and `--no-default-groups` flags ([#&#8203;11224](https://github.com/astral-sh/uv/pull/11224))

##### Documentation

-   Minor touchups to the Docker provenance docs ([#&#8203;11252](https://github.com/astral-sh/uv/pull/11252))
-   Move content from the `mkdocs.public.yml` into the template ([#&#8203;11246](https://github.com/astral-sh/uv/pull/11246))

### [`v0.5.28`](https://github.com/astral-sh/uv/blob/HEAD/CHANGELOG.md#0528)

[Compare Source](https://github.com/astral-sh/uv/compare/0.5.27...0.5.28)

##### Bug fixes

-   Allow discovering virtual environments from the first interpreter found on the `PATH` ([#&#8203;11218](https://github.com/astral-sh/uv/pull/11218))
-   Clear ephemeral overlays when running tools ([#&#8203;11141](https://github.com/astral-sh/uv/pull/11141))
-   Disable SSL in Git commands for `--allow-insecure-host` ([#&#8203;11210](https://github.com/astral-sh/uv/pull/11210))
-   Fix hardlinks in tar unpacking ([#&#8203;11221](https://github.com/astral-sh/uv/pull/11221))
-   Set base executable when returning virtual environment ([#&#8203;11209](https://github.com/astral-sh/uv/pull/11209))
-   Use base Python for cached environments ([#&#8203;11208](https://github.com/astral-sh/uv/pull/11208))

##### Documentation

-   Add documentation on verifying Docker image attestations ([#&#8203;11140](https://github.com/astral-sh/uv/pull/11140))
-   Add `last updated` to documentation ([#&#8203;11164](https://github.com/astral-sh/uv/pull/11164))

### [`v0.5.27`](https://github.com/astral-sh/uv/blob/HEAD/CHANGELOG.md#0527)

[Compare Source](https://github.com/astral-sh/uv/compare/0.5.26...0.5.27)

##### Enhancements

-   Avoid setting permissions during tar extraction ([#&#8203;11191](https://github.com/astral-sh/uv/pull/11191))
-   Remove warnings for missing lower bounds ([#&#8203;11195](https://github.com/astral-sh/uv/pull/11195))
-   Update PubGrub to set-based outdated priority tracking ([#&#8203;11169](https://github.com/astral-sh/uv/pull/11169))
-   Improve error messages for `uv pip install` with `--extra` or `--all-extras` and invalid sources ([#&#8203;11193](https://github.com/astral-sh/uv/pull/11193))
-   Sign Docker images using GitHub attestations ([#&#8203;8685](https://github.com/astral-sh/uv/pull/8685))

##### Preview features

-   Don't expand self-referential extras in the build backend ([#&#8203;11142](https://github.com/astral-sh/uv/pull/11142))

##### Performance

-   Filter discovered Python executables by source before querying ([#&#8203;11143](https://github.com/astral-sh/uv/pull/11143))
-   Optimize exclusion computation for markers ([#&#8203;11158](https://github.com/astral-sh/uv/pull/11158))
-   Use Astral-maintained `tokio-tar` fork ([#&#8203;11174](https://github.com/astral-sh/uv/pull/11174))
-   Remove unneeded `.clone()` ([#&#8203;11127](https://github.com/astral-sh/uv/pull/11127))

##### Bug fixes

-   Fix relative paths in bytecode compilation ([#&#8203;11177](https://github.com/astral-sh/uv/pull/11177))
-   Percent-decode URLs in canonical comparisons ([#&#8203;11088](https://github.com/astral-sh/uv/pull/11088))
-   Respect concurrency limits in parallel index fetch ([#&#8203;11182](https://github.com/astral-sh/uv/pull/11182))
-   Use wire JSON schema for conflict items ([#&#8203;11196](https://github.com/astral-sh/uv/pull/11196))
-   Use explicit `_GLibCVersion` tuple in uv-python crate ([#&#8203;11122](https://github.com/astral-sh/uv/pull/11122))

##### Documentation

-   Add Git SHA locking behavior to docs ([#&#8203;11125](https://github.com/astral-sh/uv/pull/11125))
-   Add best-practice flags to `pip install` example in troubleshooting guide ([#&#8203;11194](https://github.com/astral-sh/uv/pull/11194))
-   Set `VIRTUAL_ENV` in Jupyter kernels ([#&#8203;11155](https://github.com/astral-sh/uv/pull/11155))
-   Add instructions for deactivating an environment ([#&#8203;11200](https://github.com/astral-sh/uv/pull/11200))

### [`v0.5.26`](https://github.com/astral-sh/uv/blob/HEAD/CHANGELOG.md#0526)

[Compare Source](https://github.com/astral-sh/uv/compare/0.5.25...0.5.26)

##### Enhancements

-   Add support for `uvx python` ([#&#8203;11076](https://github.com/astral-sh/uv/pull/11076))
-   Allow `--no-dev --invert` in `uv tree` ([#&#8203;11068](https://github.com/astral-sh/uv/pull/11068))
-   Update `uv python install --reinstall` to reinstall all previous versions ([#&#8203;11072](https://github.com/astral-sh/uv/pull/11072))
-   Consistently write log messages with capitalized first word ([#&#8203;11111](https://github.com/astral-sh/uv/pull/11111))
-   Suggest `--build-backend` when `--backend` is passed to `uv init` ([#&#8203;10958](https://github.com/astral-sh/uv/pull/10958))
-   Improve retry trace message ([#&#8203;11108](https://github.com/astral-sh/uv/pull/11108))

##### Performance

-   Remove unnecessary UTF-8 conversion in hash parsing ([#&#8203;11110](https://github.com/astral-sh/uv/pull/11110))

##### Bug fixes

-   Ignore non-hash fragments in HTML API responses ([#&#8203;11107](https://github.com/astral-sh/uv/pull/11107))
-   Avoid resolving symbolic links when querying Python interpreters ([#&#8203;11083](https://github.com/astral-sh/uv/pull/11083))
-   Avoid sharing state between universal and non-universal resolves ([#&#8203;11051](https://github.com/astral-sh/uv/pull/11051))
-   Error when `--script` is passing a non-PEP 723 script ([#&#8203;11118](https://github.com/astral-sh/uv/pull/11118))
-   Make metadata deserialization failures non-fatal in the cache ([#&#8203;11105](https://github.com/astral-sh/uv/pull/11105))
-   Mark metadata as dynamic when reading from built wheel cache ([#&#8203;11046](https://github.com/astral-sh/uv/pull/11046))
-   Propagate credentials for `<index>/simple` to `<index>/...` endpoints ([#&#8203;11074](https://github.com/astral-sh/uv/pull/11074))
-   Fix conflicting extra bug during `uv sync` ([#&#8203;11075](https://github.com/astral-sh/uv/pull/11075))

##### Documentation

-   Add PyTorch XPU instructions to the PyTorch guide ([#&#8203;11109](https://github.com/astral-sh/uv/pull/11109))
-   Add docs for signal handling ([#&#8203;11041](https://github.com/astral-sh/uv/pull/11041))
-   Explain build frontend vs. build backend ([#&#8203;11094](https://github.com/astral-sh/uv/pull/11094))
-   Fix formatting of `RUST_LOG` documentation ([#&#8203;10053](https://github.com/astral-sh/uv/pull/10053))
-   Fix typo in `--no-deps` description ([#&#8203;11073](https://github.com/astral-sh/uv/pull/11073))
-   Reflow CLI documentation comments ([#&#8203;11040](https://github.com/astral-sh/uv/pull/11040))
-   Shorten "Using existing Python versions" nav item so it fits on one line ([#&#8203;11077](https://github.com/astral-sh/uv/pull/11077))
-   Some minor touch-ups to the Python install guide ([#&#8203;11116](https://github.com/astral-sh/uv/pull/11116))
-   Update Dependabot tracking issue link ([#&#8203;11054](https://github.com/astral-sh/uv/pull/11054))
-   Update documentation for running in a container ([#&#8203;11052](https://github.com/astral-sh/uv/pull/11052))
-   Upgrade PyTorch version in documentation ([#&#8203;11114](https://github.com/astral-sh/uv/pull/11114))
-   Use `sys_platform` in lieu of `platform_system` in PyTorch docs ([#&#8203;11113](https://github.com/astral-sh/uv/pull/11113))
-   Use positive (rather than negative) markers in PyTorch examples ([#&#8203;11112](https://github.com/astral-sh/uv/pull/11112))
-   Fix unnecessary backslashes in brackets ([#&#8203;11059](https://github.com/astral-sh/uv/pull/11059))
-   Suggest setting copy link mode in GitLab integration guide ([#&#8203;11067](https://github.com/astral-sh/uv/pull/11067))

### [`v0.5.25`](https://github.com/astral-sh/uv/blob/HEAD/CHANGELOG.md#0525)

[Compare Source](https://github.com/astral-sh/uv/compare/0.5.24...0.5.25)

##### Enhancements

-   Allow installation of manylinux wheels on loongarch64 ([#&#8203;10927](https://github.com/astral-sh/uv/pull/10927))
-   Allow optional `=` for editables in `requirements.txt` ([#&#8203;10954](https://github.com/astral-sh/uv/pull/10954))
-   Add Windows aarch64 to the release binaries ([#&#8203;10885](https://github.com/astral-sh/uv/pull/10885))

##### Bug fixes

-   Use spec-compliant (`128+n`) exit codes for `uv run` and `uv tool run` on Unix ([#&#8203;10781](https://github.com/astral-sh/uv/pull/10781))
-   Fix best-interpreter lookups when there is an invalid interpreter in the `PATH` ([#&#8203;11030](https://github.com/astral-sh/uv/pull/11030))
-   Guard against concurrent cache writes on Windows ([#&#8203;11007](https://github.com/astral-sh/uv/pull/11007))
-   Prioritize package preferences with greater package versions ([#&#8203;10963](https://github.com/astral-sh/uv/pull/10963))
-   Reject `--editable` flag on non-directory requirements ([#&#8203;10994](https://github.com/astral-sh/uv/pull/10994))
-   Respect `--no-sources` for `uv pip install` workspace discovery ([#&#8203;11003](https://github.com/astral-sh/uv/pull/11003))
-   Set `JEMALLOC_SYS_WITH_LG_PAGE=16` in ARM Docker builds ([#&#8203;10943](https://github.com/astral-sh/uv/pull/10943))
-   Update `riscv64` Python downloads to allow install on `riscv64gc` ([#&#8203;10937](https://github.com/astral-sh/uv/pull/10937))
-   Fix file persist retries on Windows ([#&#8203;11008](https://github.com/astral-sh/uv/pull/11008))
-   Fix incorrect error message when specifying `tool.uv.sources.(package).workspace` with other options ([#&#8203;11013](https://github.com/astral-sh/uv/pull/11013))
-   Improve SIGINT handling in `uv run` ([#&#8203;11009](https://github.com/astral-sh/uv/pull/11009))

##### Documentation

-   Add `SECURITY` policy ([#&#8203;11035](https://github.com/astral-sh/uv/pull/11035))
-   Add `Requires-Python` upper bound behavior to the docs ([#&#8203;10964](https://github.com/astral-sh/uv/pull/10964))
-   Add a troubleshooting section and reproducible example guide ([#&#8203;10947](https://github.com/astral-sh/uv/pull/10947))
-   Add documentation for `uv add -r` ([#&#8203;10926](https://github.com/astral-sh/uv/pull/10926))
-   Amend `requires-python` rules in resolver documentation ([#&#8203;10993](https://github.com/astral-sh/uv/pull/10993))
-   Reference workspaces in `--no-sources` documentation ([#&#8203;10995](https://github.com/astral-sh/uv/pull/10995))
-   Update documentation for activating virtual environments in different shell ([#&#8203;11000](https://github.com/astral-sh/uv/pull/11000))
-   Add Docker SHA pinning tip ([#&#8203;10955](https://github.com/astral-sh/uv/pull/10955))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xNDAuMCIsInVwZGF0ZWRJblZlciI6IjM5LjE2My4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: #53
Co-authored-by: Renovate <renovate@csw.im>
Co-committed-by: Renovate <renovate@csw.im>
2025-02-10 23:01:15 -05:00
e854abfb0e
feat(backup): update to most recent red version
Some checks failed
Actions / Build Documentation (MkDocs) (push) Failing after 0s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 51s
2025-02-08 19:00:45 -06:00
7ada16e999
chore(tooling): add .editorconfig 2025-02-08 19:00:36 -06:00
d649ca0f02
chore(repo): formatting
Some checks failed
Actions / Lint Code (Ruff & Pylint) (push) Failing after 0s
Actions / Build Documentation (MkDocs) (push) Failing after 0s
2025-02-07 16:07:50 -06:00
72dcc96fea
chore(tooling): remove comments 2025-02-07 16:07:44 -06:00
527c372fb0
chore(tooling): fix the nix flake
Some checks failed
Actions / Build Documentation (MkDocs) (push) Failing after 26s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 47s
2025-02-06 17:20:39 -06:00
e7714cd2df
fix(hotreload): fix typehint 2025-02-06 17:20:21 -06:00
9b96a15621
chore(repo): add schemas to repo.json files
Some checks failed
Actions / Lint Code (Ruff & Pylint) (push) Failing after 46s
Actions / Build Documentation (MkDocs) (push) Failing after 24s
2025-02-06 15:10:19 -06:00
7593aace00
chore(tooling): add dig to the nix flake
Some checks failed
Actions / Build Documentation (MkDocs) (push) Failing after 29s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 49s
2025-02-06 06:41:39 -06:00
e4f419ec7b
chore(repo): formatting
Some checks failed
Actions / Build Documentation (MkDocs) (push) Failing after 27s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 52s
2025-02-06 06:41:00 -06:00
1224d2b60f
chore(deps): update dependencies 2025-02-06 06:40:49 -06:00
999fd8e96f
chore(tooling): re-add nix flake and update deps
Some checks failed
Actions / Build Documentation (MkDocs) (push) Failing after 27s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 48s
2025-02-04 20:18:31 -06:00
2a5b924409
feat(repo): make all cogs pylance-typechecking compliant
Some checks failed
Actions / Lint Code (Ruff & Pylint) (push) Failing after 43s
Actions / Build Documentation (MkDocs) (push) Failing after 24s
at `basic` level, does not include Aurora as it's being rewritten in the `aurora/v3` branch
2025-02-01 16:57:45 +00:00
ea0b7937f8
chore(tooling): switch to zsh in the dev shell 2025-02-01 16:56:49 +00:00
034748b08e
chore(deps): update dependencies 2025-02-01 16:56:34 +00:00
89d5108ef2
chore(repo): update pull request template
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 39s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 45s
2025-01-29 23:34:52 +00:00
3eeb2f90a6
feat(hotreload): add pre-compilation
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 40s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 44s
this feature will detect syntax errors before reloading a cog and cancel the cog reload if it detects one. DOES NOT detect runtime/logic errors, only syntax errors.
2025-01-29 23:24:19 +00:00
7c2ff7681c
chore(deps): update code.forgejo.org/forgejo/runner docker tag to v6.2.1 (#56)
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 40s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 45s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [code.forgejo.org/forgejo/runner](https://forgejo.org) ([source](https://code.forgejo.org/forgejo/runner)) | stage | patch | `6.2.0` -> `6.2.1` |

---

> ⚠️ **Warning**
>
> Some dependencies could not be looked up. Check the Dependency Dashboard for more information.

---

### Release Notes

<details>
<summary>forgejo/runner (code.forgejo.org/forgejo/runner)</summary>

### [`v6.2.1`](https://code.forgejo.org/forgejo/runner/blob/HEAD/RELEASE-NOTES.md#621)

[Compare Source](https://code.forgejo.org/forgejo/runner/compare/v6.2.0...v6.2.1)

-   LXC [templates are updated if needed](https://code.forgejo.org/forgejo/act/pulls/102).

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xNDAuMCIsInVwZGF0ZWRJblZlciI6IjM5LjE0MC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: https://www.coastalcommits.com/cswimr/SeaCogs/pulls/56
Co-authored-by: Renovate <renovate@coastalcommits.com>
Co-committed-by: Renovate <renovate@coastalcommits.com>
2025-01-29 13:39:57 -05:00
1ca452669c
chore(deps): pin code.forgejo.org/forgejo/runner docker tag to 936c4fe (#52)
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 42s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 52s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [code.forgejo.org/forgejo/runner](https://forgejo.org) ([source](https://code.forgejo.org/forgejo/runner)) | stage | pinDigest |  -> `936c4fe` |

---

> ⚠️ **Warning**
>
> Some dependencies could not be looked up. Check the Dependency Dashboard for more information.

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xMzMuNCIsInVwZGF0ZWRJblZlciI6IjM5LjEzMy40IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: https://www.coastalcommits.com/cswimr/SeaCogs/pulls/52
Co-authored-by: Renovate <renovate@coastalcommits.com>
Co-committed-by: Renovate <renovate@coastalcommits.com>
2025-01-26 17:36:59 -05:00
666efaf893
chore(devcontainer): add DiD, forgejo runner, aliases
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 43s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 50s
2025-01-26 22:27:22 +00:00
289da84b76
fix(hotreload): use 3.8-compatible typehints
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 44s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 50s
2025-01-26 21:31:48 +00:00
5c6783452d
fix(hotreload): verify that a file path exists before creating an observer schedule for it
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 47s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 52s
2025-01-26 21:29:02 +00:00
70d75c688c
fix(hotreload): only import ObserverType for TYPE_CHECKING 2025-01-26 21:23:08 +00:00
76f0a4cdd4
chore(hotreload): support python 3.8+ instead of 3.10+
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 51s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 58s
2025-01-26 21:09:38 +00:00
19eca0b1b5
docs: fixed incorrect command instructions
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 42s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 48s
2025-01-26 15:33:34 +00:00
9e489a4c91
docs(hotreload): add docs
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 40s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 46s
2025-01-26 15:29:07 +00:00
1993da084d
docs(hotreload): add docs
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 43s
Actions / Lint Code (Ruff & Pylint) (push) Has been cancelled
2025-01-26 15:28:21 +00:00
b22d81f514
feat(hotreload): handle multiple observers
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 42s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 47s
2025-01-26 15:18:05 +00:00
ccf9389e13
feat(hotreload): Channel Notifications (#51)
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 41s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 47s
# Channel Notifications

This PR adds the ability for HotReload to send messages to a configurable discord channel when reloading a cog. Messages are only sent after the cog is reloaded to prevent slowdowns.
<!-- Create a new issue, if it doesn't exist yet -->

- [x] By submitting this pull request, I permit [cswimr](https://www.coastalcommits.com/cswimr) to license my work under
  the [Mozilla Public License Version 2.0](https://www.coastalcommits.com/cswimr/SeaCogs/src/branch/main/LICENSE).

Reviewed-on: https://www.coastalcommits.com/cswimr/SeaCogs/pulls/51
2025-01-26 10:06:14 -05:00
ff9e20be91
fix(pterodactyl): update pterodactyl for the new version of websockets & fix a few minor bugs
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 42s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 47s
2025-01-26 15:04:05 +00:00
d51e3f17e9
fix(antipolls): ruff fixes
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 42s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 48s
2025-01-26 14:22:12 +00:00
59d33ea87d
style(seautils): ruff fix
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 44s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 49s
2025-01-26 14:16:46 +00:00
65cfafc7b3
style(emojiinfo): ruff fix 2025-01-26 14:16:39 +00:00
ea5f51892a
style(repo): ruff fixes
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 42s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 47s
2025-01-26 14:13:37 +00:00
9df7f15bbe
chore(repo): enable even more ruff rules 2025-01-26 14:11:37 +00:00
4ad73ec6ee
fix(pterodactyl): ruff fix
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 43s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 47s
2025-01-26 14:10:48 +00:00
2543563af1
fix(hotreload): ruff fix 2025-01-26 14:10:48 +00:00
0b60a2df66
chore(repo): enable more ruff rules
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 44s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 49s
2025-01-26 14:03:28 +00:00
6f0fee8275
fix(backup): disable SLF001
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 45s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 52s
2025-01-26 13:42:57 +00:00
dbff8d0d2a
chore(repo): enable some more ruff rules 2025-01-26 13:42:48 +00:00
0c43e3d2c9
fix(backup): update for red 3.5.14 & reformat
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 44s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 51s
2025-01-26 13:21:28 +00:00
0f47a15291
fix(emojiinfo): ruff fixes & rename logger
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 43s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 49s
2025-01-26 13:18:04 +00:00
7f2a81e350
fix(bible): ruff fixes 2025-01-26 13:16:54 +00:00
5c5a2f28b2
chore(repo): reformat pyproject.toml and disable a stupid linting rule 2025-01-26 13:16:26 +00:00
d5a248733a
fix(pterodactyl): fix a ruff violation
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 44s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 51s
2025-01-26 13:09:15 +00:00
5decc51a36
chore(deps): update 2025-01-26 13:08:33 +00:00
41678ef3ed
chore(repo): enable more ruff linting rules 2025-01-26 13:08:28 +00:00
0492b30662
chore(repo): add two paths to gitignore 2025-01-26 13:08:02 +00:00
3063721b8d
chore(vscode): add some settings overrides 2025-01-26 13:07:46 +00:00
3d882625d2
style(seautils): reformat
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 36s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 40s
2025-01-26 01:54:02 +00:00
6c25d61dfd
fix(hotreload): use %s formatting in logging strings 2025-01-26 01:53:40 +00:00
6ffa81fdee
fix(bible): fix ruff errors 2025-01-26 01:53:09 +00:00
06e678f26f
style(repo): enable some more linting rules 2025-01-26 01:52:46 +00:00
e5210420cb
chore(vscode): force load hotreload in launch & debug configuration
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 35s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 40s
2025-01-26 01:08:06 +00:00
1677412b56
chore(hotreload): add disclaimer in install message that this cog doesn't add any commands
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 36s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 40s
2025-01-26 00:56:50 +00:00
451ecfe5d9
chore(tooling): disable too-many-positional-arguments
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 37s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 41s
2025-01-26 00:48:25 +00:00
8511b7b964
fix(bible): mark cog_unload as async 2025-01-26 00:46:21 +00:00
f233a7dec5
fix(hotreload): mark cog_load and cog_unload as async 2025-01-26 00:46:00 +00:00
18ba837467
fix(workflow): run on main branch and prs only, run docs build on prs
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 36s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 41s
2025-01-26 00:43:02 +00:00
2859f93501
fix(bible): close asyncio session after unloading the cog
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 35s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 40s
2025-01-26 00:39:09 +00:00
7d1a9cc01a
fix(hotreload): only add dest_package_name to the cogs_to_reload list if dest_package_name != src_package_name
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 34s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 40s
fix(hotreload): only add `dest_package_name` to the `cogs_to_reload` list if `dest_package_name != src_package_name`
2025-01-26 00:32:29 +00:00
5adc7a2c7b
feat(hotreload): Add more events (#50)
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 35s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 38s
# More HotReload Events
<!-- Create a new issue, if it doesn't exist yet -->
Currently, HotReload only supports file modification events. It should also support file moves, and some other event types.

- [x] By submitting this pull request, I permit cswimr to license my work under
  the [Mozilla Public License Version 2.0](https://www.coastalcommits.com/cswimr/SeaCogs/src/branch/main/LICENSE).

Reviewed-on: https://www.coastalcommits.com/cswimr/SeaCogs/pulls/50
2025-01-25 19:25:29 -05:00
5384809780
feat(hotreload): init (#49)
Some checks failed
Actions / Build Documentation (MkDocs) (push) Has been skipped
Actions / Lint Code (Ruff & Pylint) (push) Failing after 42s
Actions / Build Documentation (MkDocs) (pull_request) Has been skipped
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 41s
This pull request adds a cog that allows for automatic reloading of local cogs.

- [x] By submitting this pull request, I permit [cswimr](https://www.coastalcommits.com) to license my work under
  the [Mozilla Public License Version 2.0](https://www.coastalcommits.com/cswimr/SeaCogs/src/branch/main/LICENSE).

Reviewed-on: https://www.coastalcommits.com/cswimr/SeaCogs/pulls/49
2025-01-25 23:55:37 +00:00
78f036da48
feat(devcontainer): initialize redbot instance in postCreateCommand
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 37s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 41s
2025-01-25 22:50:35 +00:00
f831bfcdd5
feat(vscode): add --dev argument to redbot args
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 40s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 43s
2025-01-25 16:50:51 -05:00
9b60a8f01e
chore(vscode): add -vvv argument to launch & debug
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 37s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 41s
2025-01-25 15:47:05 -05:00
7369f5810a
fix(devcontainer): chown workspace directory
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 40s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 41s
for some reason, the default `.data` mount is not owned by the `vscode` user (on my system at least), so we `chown` it to ensure that `uv run redbot-setup` can write to the directory.
2025-01-25 14:43:02 -05:00
a8bb62dcf3
fix(devcontainer): ensure pip is installed in the devcontainer
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 37s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 41s
this is being done because Red-DiscordBot requires pip to be installed to function, but does not declare it as a dependency. this is only a problem when uv is being used to install dependencies in an environment where pip is not present. so, we ensure pip is present!
2025-01-25 14:39:24 -05:00
62 changed files with 1991 additions and 1861 deletions

View file

@ -1,34 +0,0 @@
FROM ghcr.io/astral-sh/uv:0.5.24@sha256:2381d6aa60c326b71fd40023f921a0a3b8f91b14d5db6b90402e65a635053709 AS uv
FROM python:3.11-slim@sha256:6ed5bff4d7d377e2a27d9285553b8c21cfccc4f00881de1b24c9bc8d90016e82 AS python
FROM mcr.microsoft.com/vscode/devcontainers/base:bookworm@sha256:6155a486f236fd5127b76af33086029d64f64cf49dd504accb6e5f949098eb7e
LABEL repository="www.coastalcommits.com/cswimr/SeaCogs"
LABEL maintainer="cswimr <seaswimmerthefsh@gmail.com>"
RUN apt-get update; \
apt-get install -y --no-install-recommends \
# Red-DiscordBot
build-essential \
git \
# PyNaCl
libsodium-dev \
# CFFI
libffi-dev \
# SSH repository support
openssh-client \
# Cog dependencies
# Audio
openjdk-17-jre-headless \
# PyLav
libaio1 \
libaio-dev \
# SeaUtils
dnsutils; \
apt-get clean; \
rm -rf /var/lib/apt/lists/*
COPY --from=uv --chown=vscode: /uv /uvx /bin/
COPY --from=python --chown=vscode: /usr/local /usr/local
RUN ln -s /usr/local/bin/python3.11 /usr/local/bin/python; \
python --version

View file

@ -1,35 +0,0 @@
{
"name": "Red-DiscordBot: SeaCogs",
"build": {
"context": "..",
"dockerfile": "Dockerfile"
},
"customizations": {
"vscode": {
"extensions": [
"charliermarsh.ruff",
"ms-azuretools.vscode-docker",
"ms-python.python",
"tekumara.typos-vscode",
"tamasfe.even-better-toml",
"redhat.vscode-yaml",
"DavidAnson.vscode-markdownlint",
"yy0931.vscode-sqlite3-editor",
"aaron-bond.better-comments",
"donjayamanne.githistory",
"eamodio.gitlens"
]
}
},
"containerEnv": {
"DISPLAY": "dummy",
"PYTHONUNBUFFERED": "True",
"UV_LINK_MODE": "copy",
"UV_PYTHON_PREFERENCE": "only-system",
"UV_PYTHON_DOWNLOADS": "never",
"PROJECT_DIR": "/workspaces/SeaCogs"
},
"mounts": ["source=seacogs-persistent-data,target=/workspaces/SeaCogs/.data,type=volume"],
"postCreateCommand": "uv sync --frozen",
"remoteUser": "vscode"
}

View file

@ -12,5 +12,5 @@ Aurora is a fully-featured moderation system. It is heavily inspired by Galactic
```bash
[p]repo add seacogs https://www.coastalcommits.com/cswimr/SeaCogs
[p]cog install seacogs aurora
[p]cog load aurora
[p]load aurora
```

View file

@ -7,7 +7,7 @@ Backup allows you to export a JSON list of all of your installed repositories an
```bash
[p]repo add seacogs https://www.coastalcommits.com/cswimr/SeaCogs
[p]cog install seacogs backup
[p]cog load backup
[p]load backup
```
## Version Compatibility

View file

@ -8,7 +8,7 @@ This cog does require an api key to work.
```bash
[p]repo add seacogs https://www.coastalcommits.com/cswimr/SeaCogs
[p]cog install seacogs bible
[p]cog load bible
[p]load bible
```
## Setup
@ -21,8 +21,9 @@ Then, you can use `[p]set api` to set the API key. Make sure your formatting mat
## Commands
### bible passage
- Usage: `[p]bible passage <book> <passage>`
- Aliases: `verse`
- Usage: `[p]bible passage <book> <passage>`
- Aliases: `verse`
Get a Bible passage.
@ -31,6 +32,7 @@ Example usage:
`[p]bible passage John 3:16-3:17`
### bible random
- Usage: `[p]bible random`
- Usage: `[p]bible random`
Get a random Bible verse.

View file

@ -7,7 +7,7 @@ EmojiInfo allows you to retrieve information about an emoji.
```bash
[p]repo add seacogs https://www.coastalcommits.com/cswimr/SeaCogs
[p]cog install seacogs emojiinfo
[p]cog load emojiinfo
[p]load emojiinfo
```
## Commands

26
.docs/hotreload.md Normal file
View file

@ -0,0 +1,26 @@
# HotReload
HotReload automatically reloads cogs in local cog paths on file change.
This is useful for development, as it allows you to make changes to your cogs and see the changes reflected in Discord immediately, without having to manually `[p]reload` the cog.
## Installation
```bash
[p]repo add seacogs https://www.coastalcommits.com/cswimr/SeaCogs
[p]cog install seacogs hotreload
[p]load hotreload
```
## Commands
### hotreload compile
Determines if the cog should try to compile a modified Python file before reloading the associated cog. Useful for catching syntax errors. Disabled by default.
### hotreload notifychannel
Set the channel where hotreload will send notifications when a cog is reloaded.
### hotreload list
Debugging command that shows the list of currently active observers. May be expanded in the future to show watched file paths.

View file

@ -7,7 +7,7 @@ Nerdify allows you to nerdify other people's text.
```bash
[p]repo add seacogs https://www.coastalcommits.com/cswimr/SeaCogs
[p]cog install seacogs nerdify
[p]cog load nerdify
[p]load nerdify
```
## Commands

View file

@ -12,5 +12,5 @@ Pterodactyl allows for connecting to a Pterodactyl server through websockets. It
```bash
[p]repo add seacogs https://www.coastalcommits.com/cswimr/SeaCogs
[p]cog install seacogs pterodactyl
[p]cog load aurora
[p]load pterodactyl
```

15
.editorconfig Normal file
View file

@ -0,0 +1,15 @@
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.py]
indent_size = 4
[*.md]
trim_trailing_whitespace = false

1
.envrc Normal file
View file

@ -0,0 +1 @@
use flake

View file

@ -2,5 +2,5 @@
<!-- Create a new issue, if it doesn't exist yet -->
- [ ] By submitting this pull request, I permit cswimr to license my work under
- [ ] By submitting this pull request, I permit [cswimr](https://www.coastalcommits.com/cswimr) to license my work under
the [Mozilla Public License Version 2.0](https://www.coastalcommits.com/cswimr/SeaCogs/src/branch/main/LICENSE).

View file

@ -12,6 +12,7 @@
too-many-locals,
too-many-public-methods,
too-many-statements,
too-many-positional-arguments,
arguments-differ,
too-many-return-statements,
import-outside-toplevel,

View file

@ -1,21 +1,28 @@
name: Actions
on:
push:
branches:
- main
pull_request:
jobs:
lint:
name: Lint Code (Ruff & Pylint)
runs-on: docker
container: www.coastalcommits.com/cswimr/actions:uv@sha256:211aaf7d9ac98087579ebf9fab87a9122f51b2697e3a3649ac9f4bd3b03b8e5d
container: catthehacker/ubuntu:act-latest@sha256:cd837565ef74f3d0f94e89ff6723292caa415306bddabfe566932ae892ca9334
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Install python
run: uv python install 3.11
- name: "Setup uv"
uses: actions/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5
with:
version: "latest"
enable-cache: true
prune-cache: false
github-token: ${{ secrets.GITHUBTOKEN }}
- name: Install dependencies
- name: "Install dependencies"
run: uv sync
- name: Analysing code with Ruff
@ -27,23 +34,27 @@ jobs:
docs:
name: Build Documentation (MkDocs)
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: docker
container: www.coastalcommits.com/cswimr/actions:docs@sha256:e405cd6b9b1182a570ddee32ed8dd1b2f899edc625d006c8b4b2f18c100e724f
container: catthehacker/ubuntu:act-latest@sha256:cd837565ef74f3d0f94e89ff6723292caa415306bddabfe566932ae892ca9334
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
fetch-depth: 0
- name: Install python
run: uv python install 3.11
- name: "Setup uv"
uses: actions/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5
with:
version: "latest"
enable-cache: true
prune-cache: false
github-token: ${{ secrets.GITHUBTOKEN }}
- name: Install dependencies
run: uv sync --no-dev --extra=documentation
run: uv sync --no-dev --group=documentation
- name: Set environment variables
uses: actions/env@v2
uses: actions/env@1791216cd180e6578dd1d67fb8d2852b883a5f53 # v2
- name: Build documentation
run: |
@ -63,13 +74,13 @@ jobs:
echo "${YELLOW}Deploying to ${BLUE}Meli ${YELLOW}on branch ${GREEN}$CI_ACTION_REF_NAME_SLUG${YELLOW}...\n"
npx -p "@getmeli/cli" meli upload ./site \
--url "https://pages.coastalcommits.com" \
--url "https://meli.csw.im" \
--site "${{ vars.MELI_SITE_ID }}" \
--token "${{ secrets.MELI_TOKEN }}" \
--release "$CI_ACTION_REF_NAME_SLUG/${{ env.GITHUB_SHA }}" \
--branch "$CI_ACTION_REF_NAME_SLUG"
echo "\n${YELLOW}Deployed to ${BLUE}Meli ${YELLOW}on branch ${GREEN}$CI_ACTION_REF_NAME_SLUG${YELLOW}!"
echo "${GREEN}https://$CI_ACTION_REF_NAME_SLUG.seacogs.coastalcommits.com/"
echo "${GREEN}https://$CI_ACTION_REF_NAME_SLUG.seacogs.csw.im/"
env:
GITEA_TOKEN: ${{ secrets.COASTALCOMMITSTOKEN }}

3
.gitignore vendored
View file

@ -3,3 +3,6 @@ site
.venv
.data
__pycache__
.mypy_cache/
.ruff_cache/
.direnv/

7
.vscode/launch.json vendored
View file

@ -1,15 +1,12 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Red-DiscordBot",
"name": "Python: Red-DiscordBot",
"type": "debugpy",
"request": "launch",
"module": "redbot",
"args": ["local"]
"args": ["local", "--dev", "-vvv", "--load-cogs=hotreload"]
}
]
}

24
.vscode/settings.json vendored
View file

@ -1,6 +1,5 @@
{
"[python]": {
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll": "explicit"
},
@ -8,5 +7,26 @@
},
"[json]": {
"editor.defaultFormatter": "vscode.json-language-features"
}
},
"[jsonc]": {
"editor.defaultFormatter": "vscode.json-language-features"
},
"files.exclude": {
"**/.git": true,
"**/__pycache__": true,
"**/.ruff_cache": true,
"**/.mypy_cache": true
},
"python.analysis.diagnosticSeverityOverrides": {
"reportAttributeAccessIssue": false, // disabled because `commands.group.command` is listed as Any / Unknown for some reason
"reportCallIssue": "information"
},
"python.analysis.diagnosticMode": "workspace",
"python.analysis.supportDocstringTemplate": true,
"python.analysis.typeCheckingMode": "basic",
"python.analysis.typeEvaluation.enableReachabilityAnalysis": true,
"python.analysis.typeEvaluation.strictDictionaryInference": true,
"python.analysis.typeEvaluation.strictListInference": true,
"python.analysis.typeEvaluation.strictSetInference": true,
"editor.formatOnSave": true,
}

View file

@ -1,27 +1,56 @@
# SeaCogs
[![Discord](https://img.shields.io/discord/1070058354925383681?logo=discord&color=%235661f6)](https://discord.gg/eMUMe77Yb8)
[![Documentation](https://img.shields.io/badge/docs-CoastalCommits%20Pages-3e83fd?logo=materialformkdocs)](https://seacogs.coastalcommits.com)
[![Documentation](https://img.shields.io/badge/docs-CoastalCommits%20Pages-3e83fd?logo=materialformkdocs)](https://seacogs.csw.im)
![Python Versions](https://img.shields.io/badge/python-3.10%20%7C%203.11-%233776ab?logo=python)
My assorted cogs for Red-DiscordBot.
## Development
## Developing
To get started with a development environment, first clone this repository.
You'll need some prerequisites before you can start working on my cogs.
[git](https://git-scm.com) - [uv](https://docs.astral.sh/uv)
Additionally, I recommend a code editor of some variety. [Visual Studio Code](https://code.visualstudio.com) is a good, beginner-friendly option.
```sh
git clone https://coastalcommits.com/cswimr/SeaCogs.git
### Installing Prerequisites
_This section of the guide only applies to Windows systems.
If you're on Linux, refer to the documentation of the projects listed above. I also offer a [Nix Flake](./flake.nix) that contains all of the required prerequisites, if you're a Nix user._
#### [`git`](https://git-scm.com)
You can download git from the [git download page](https://git-scm.com/downloads/win).
Alternatively, you can use `winget`:
```ps1
winget install --id=Git.Git -e --source=winget
```
Then, install Poetry.
#### [`uv`](https://docs.astral.sh/uv)
```sh
pip install poetry
You can install uv with the following Powershell command:
```ps1
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
```
Finally, use Poetry to create a virtual environment with all of the dependencies required by my cogs.
Alternatively, you can use `winget`:
```sh
poetry install
```ps1
winget install --id=astral-sh.uv -e
```
### Getting the Source Code
Once you have [`git`](https://git-scm.com) installed, you can use the `git clone` command to get a copy of the repository on your system.
```bash
git clone https://c.csw.im/cswimr/SeaCogs.git
```
Then, you can use `uv` to install the Python dependencies required for development.
```bash
uv sync --frozen
```

View file

@ -17,7 +17,7 @@ class AntiPolls(commands.Cog):
__author__ = ["[cswimr](https://www.coastalcommits.com/cswimr)"]
__git__ = "https://www.coastalcommits.com/cswimr/SeaCogs"
__version__ = "1.0.1"
__version__ = "1.0.3"
__documentation__ = "https://seacogs.coastalcommits.com/antipolls/"
def __init__(self, bot: Red):
@ -49,7 +49,7 @@ class AntiPolls(commands.Cog):
"""Nothing to delete."""
return
@commands.Cog.listener('on_message')
@commands.Cog.listener("on_message")
async def polls_listener(self, message: discord.Message) -> None:
if message.guild is None:
return self.logger.verbose("Message in direct messages ignored")
@ -62,13 +62,13 @@ class AntiPolls(commands.Cog):
guild_config = await self.config.guild(message.guild).all()
if guild_config['manage_messages'] is True and message.author.guild_permissions.manage_messages:
if guild_config["manage_messages"] is True and message.author.guild_permissions.manage_messages:
return self.logger.verbose("Message from user with Manage Messages permission ignored")
if message.channel.id in guild_config['channel_whitelist']:
if message.channel.id in guild_config["channel_whitelist"]:
return self.logger.verbose("Message in whitelisted channel %s ignored", message.channel.id)
if any(role.id in guild_config['role_whitelist'] for role in message.author.roles):
if any(role.id in guild_config["role_whitelist"] for role in message.author.roles):
return self.logger.verbose("Message from whitelisted role %s ignored", message.author.roles)
if not message.content and not message.embeds and not message.attachments and not message.stickers:
@ -80,9 +80,9 @@ class AntiPolls(commands.Cog):
return self.logger.error("Failed to delete message: %s", e)
return self.logger.trace("Deleted poll message %s", message.id)
self.logger.verbose("Message %s is not a poll, ignoring", message.id)
return self.logger.verbose("Message %s is not a poll, ignoring", message.id)
@commands.group(name="antipolls", aliases=["ap"])
@commands.group(name="antipolls", aliases=["ap"]) # type: ignore
@commands.guild_only()
@commands.admin_or_permissions(manage_guild=True)
async def antipolls(self, ctx: commands.Context) -> None:
@ -95,6 +95,8 @@ class AntiPolls(commands.Cog):
@antipolls_roles.command(name="add")
async def antipolls_roles_add(self, ctx: commands.Context, *roles: discord.Role) -> None:
"""Add roles to the whitelist."""
assert ctx.guild is not None # using `assert` here and in the rest of this file to satisfy typecheckers
# this is safe because the commands are part of a guild-only command group
async with self.config.guild(ctx.guild).role_whitelist() as role_whitelist:
role_whitelist: list
failed: list[discord.Role] = []
@ -110,6 +112,7 @@ class AntiPolls(commands.Cog):
@antipolls_roles.command(name="remove")
async def antipolls_roles_remove(self, ctx: commands.Context, *roles: discord.Role) -> None:
"""Remove roles from the whitelist."""
assert ctx.guild is not None
async with self.config.guild(ctx.guild).role_whitelist() as role_whitelist:
role_whitelist: list
failed: list[discord.Role] = []
@ -123,13 +126,14 @@ class AntiPolls(commands.Cog):
await ctx.send(f"The following roles were not in the whitelist: {humanize_list([role.mention for role in failed])}", delete_after=10)
@antipolls_roles.command(name="list")
async def antipolls_roles_list(self, ctx: commands.Context) -> None:
async def antipolls_roles_list(self, ctx: commands.Context) -> discord.Message:
"""List roles in the whitelist."""
assert ctx.guild is not None
role_whitelist = await self.config.guild(ctx.guild).role_whitelist()
if not role_whitelist:
return await ctx.send("No roles in the whitelist.")
roles = [ctx.guild.get_role(role) for role in role_whitelist]
await ctx.send(humanize_list([role.mention for role in roles]))
roles = [role for role in (ctx.guild.get_role(role) for role in role_whitelist) if role is not None]
return await ctx.send(humanize_list([role.mention for role in roles]))
@antipolls.group(name="channels")
async def antipolls_channels(self, ctx: commands.Context) -> None:
@ -138,6 +142,7 @@ class AntiPolls(commands.Cog):
@antipolls_channels.command(name="add")
async def antipolls_channels_add(self, ctx: commands.Context, *channels: discord.TextChannel) -> None:
"""Add channels to the whitelist."""
assert ctx.guild is not None
async with self.config.guild(ctx.guild).channel_whitelist() as channel_whitelist:
channel_whitelist: list
failed: list[discord.TextChannel] = []
@ -153,6 +158,7 @@ class AntiPolls(commands.Cog):
@antipolls_channels.command(name="remove")
async def antipolls_channels_remove(self, ctx: commands.Context, *channels: discord.TextChannel) -> None:
"""Remove channels from the whitelist."""
assert ctx.guild is not None
async with self.config.guild(ctx.guild).channel_whitelist() as channel_whitelist:
channel_whitelist: list
failed: list[discord.TextChannel] = []
@ -166,16 +172,21 @@ class AntiPolls(commands.Cog):
await ctx.send(f"The following channels were not in the whitelist: {humanize_list([channel.mention for channel in failed])}", delete_after=10)
@antipolls_channels.command(name="list")
async def antipolls_channels_list(self, ctx: commands.Context) -> None:
async def antipolls_channels_list(self, ctx: commands.Context) -> discord.Message:
"""List channels in the whitelist."""
assert ctx.guild is not None
channel_whitelist = await self.config.guild(ctx.guild).channel_whitelist()
if not channel_whitelist:
return await ctx.send("No channels in the whitelist.")
channels = [ctx.guild.get_channel(channel) for channel in channel_whitelist]
await ctx.send(humanize_list([channel.mention for channel in channels]))
channels = [channel for channel in (ctx.guild.get_channel(channel) for channel in channel_whitelist) if channel is not None]
for c in channels:
if not c:
channels.remove(c)
return await ctx.send(humanize_list([channel.mention for channel in channels]))
@antipolls.command(name="managemessages")
async def antipolls_managemessages(self, ctx: commands.Context, enabled: bool) -> None:
"""Toggle Manage Messages permission check."""
assert ctx.guild is not None
await self.config.guild(ctx.guild).manage_messages.set(enabled)
await ctx.tick()

View file

@ -1,17 +1,14 @@
{
"author" : ["cswimr"],
"install_msg" : "Thank you for installing AntiPolls!\nYou can find the source code of this cog [here](https://coastalcommits.com/cswimr/SeaCogs).",
"name" : "AntiPolls",
"short" : "AntiPolls deletes messages that contain polls.",
"description" : "AntiPolls deletes messages that contain polls, with a configurable per-guild role and channel whitelist and support for default Discord permissions (Manage Messages).",
"end_user_data_statement" : "This cog does not store any user data.",
"$schema": "https://raw.githubusercontent.com/Cog-Creators/Red-DiscordBot/refs/heads/V3/develop/schema/red_cog_repo.schema.json",
"author": ["cswimr"],
"install_msg": "Thank you for installing AntiPolls!\nYou can find the source code of this cog [here](https://coastalcommits.com/cswimr/SeaCogs).",
"name": "AntiPolls",
"short": "AntiPolls deletes messages that contain polls.",
"description": "AntiPolls deletes messages that contain polls, with a configurable per-guild role and channel whitelist and support for default Discord permissions (Manage Messages).",
"end_user_data_statement": "This cog does not store any user data.",
"hidden": true,
"disabled": false,
"min_bot_version": "3.5.0",
"min_python_version": [3, 10, 0],
"tags": [
"automod",
"automoderation",
"polls"
]
"tags": ["automod", "automoderation", "polls"]
}

View file

@ -40,7 +40,7 @@ class Aurora(commands.Cog):
This cog stores all of its data in an SQLite database."""
__author__ = ["cswimr"]
__version__ = "2.1.3"
__version__ = "2.1.5"
__documentation__ = "https://seacogs.coastalcommits.com/aurora/"
async def red_delete_data_for_user(self, *, requester, user_id: int):
@ -70,7 +70,8 @@ class Aurora(commands.Cog):
await config.user_from_id(user_id).clear()
else:
logger.warning(
"Invalid requester passed to red_delete_data_for_user: %s", requester
"Invalid requester passed to red_delete_data_for_user: %s",
requester,
)
def __init__(self, bot: Red):
@ -135,8 +136,7 @@ class Aurora(commands.Cog):
if await config.guild(entry.guild).ignore_other_bots() is True:
if entry.user.bot or entry.target.bot:
return
else:
if entry.user.id == self.bot.user.id:
elif entry.user.id == self.bot.user.id:
return
duration = "NULL"
@ -159,10 +159,10 @@ class Aurora(commands.Cog):
elif entry.action == discord.AuditLogAction.member_update:
if entry.after.timed_out_until is not None:
timed_out_until_aware = entry.after.timed_out_until.replace(
tzinfo=timezone.utc
tzinfo=timezone.utc,
)
duration_datetime = timed_out_until_aware - datetime.now(
tz=timezone.utc
tz=timezone.utc,
)
minutes = round(duration_datetime.total_seconds() / 60)
duration = timedelta(minutes=minutes)
@ -209,7 +209,7 @@ class Aurora(commands.Cog):
return
await interaction.response.send_message(
content=f"{target.mention} has recieved a note!\n**Reason** - `{reason}`"
content=f"{target.mention} has recieved a note!\n**Reason** - `{reason}`",
)
if silent is None:
@ -239,7 +239,7 @@ class Aurora(commands.Cog):
reason,
)
await interaction.edit_original_response(
content=f"{target.mention} has received a note! (Case `#{moderation_id:,}`)\n**Reason** - `{reason}`"
content=f"{target.mention} has received a note! (Case `#{moderation_id:,}`)\n**Reason** - `{reason}`",
)
await log(interaction, moderation_id)
@ -268,7 +268,7 @@ class Aurora(commands.Cog):
return
await interaction.response.send_message(
content=f"{target.mention} has been warned!\n**Reason** - `{reason}`"
content=f"{target.mention} has been warned!\n**Reason** - `{reason}`",
)
if silent is None:
@ -298,7 +298,7 @@ class Aurora(commands.Cog):
reason,
)
await interaction.edit_original_response(
content=f"{target.mention} has been warned! (Case `#{moderation_id:,}`)\n**Reason** - `{reason}`"
content=f"{target.mention} has been warned! (Case `#{moderation_id:,}`)\n**Reason** - `{reason}`",
)
await log(interaction, moderation_id)
@ -342,7 +342,8 @@ class Aurora(commands.Cog):
parsed_time = parse_timedelta(duration)
if parsed_time is None:
await interaction.response.send_message(
content=error("Please provide a valid duration!"), ephemeral=True
content=error("Please provide a valid duration!"),
ephemeral=True,
)
return
else:
@ -350,12 +351,15 @@ class Aurora(commands.Cog):
if role.id not in addrole_whitelist:
await interaction.response.send_message(
content=error("That role isn't whitelisted!"), ephemeral=True
content=error("That role isn't whitelisted!"),
ephemeral=True,
)
return
if not await check_moddable(
target, interaction, ["moderate_members", "manage_roles"]
target,
interaction,
["moderate_members", "manage_roles"],
):
return
@ -390,7 +394,7 @@ class Aurora(commands.Cog):
reason=f"Role added by {interaction.user.id}{' for ' + humanize_timedelta(timedelta=parsed_time) if parsed_time != 'NULL' else ''} for: {reason}",
)
response: discord.WebhookMessage = await interaction.followup.send(
content=f"{target.mention} has been given the {role.mention} role{' for ' + humanize_timedelta(timedelta=parsed_time) if parsed_time != 'NULL' else ''}!\n**Reason** - `{reason}`"
content=f"{target.mention} has been given the {role.mention} role{' for ' + humanize_timedelta(timedelta=parsed_time) if parsed_time != 'NULL' else ''}!\n**Reason** - `{reason}`",
)
moderation_id = await mysql_log(
@ -448,7 +452,8 @@ class Aurora(commands.Cog):
parsed_time = parse_timedelta(duration)
if parsed_time is None:
await interaction.response.send_message(
content=error("Please provide a valid duration!"), ephemeral=True
content=error("Please provide a valid duration!"),
ephemeral=True,
)
return
else:
@ -456,12 +461,15 @@ class Aurora(commands.Cog):
if role.id not in addrole_whitelist:
await interaction.response.send_message(
content=error("That role isn't whitelisted!"), ephemeral=True
content=error("That role isn't whitelisted!"),
ephemeral=True,
)
return
if not await check_moddable(
target, interaction, ["moderate_members", "manage_roles"]
target,
interaction,
["moderate_members", "manage_roles"],
):
return
@ -496,7 +504,7 @@ class Aurora(commands.Cog):
reason=f"Role removed by {interaction.user.id}{' for ' + humanize_timedelta(timedelta=parsed_time) if parsed_time != 'NULL' else ''} for: {reason}",
)
response: discord.WebhookMessage = await interaction.followup.send(
content=f"{target.mention} has had the {role.mention} role removed{' for ' + humanize_timedelta(timedelta=parsed_time) if parsed_time != 'NULL' else ''}!\n**Reason** - `{reason}`"
content=f"{target.mention} has had the {role.mention} role removed{' for ' + humanize_timedelta(timedelta=parsed_time) if parsed_time != 'NULL' else ''}!\n**Reason** - `{reason}`",
)
moderation_id = await mysql_log(
@ -553,21 +561,24 @@ class Aurora(commands.Cog):
parsed_time = parse_timedelta(duration, maximum=timedelta(days=28))
if parsed_time is None:
await interaction.response.send_message(
error("Please provide a valid duration!"), ephemeral=True
error("Please provide a valid duration!"),
ephemeral=True,
)
return
except commands.BadArgument:
await interaction.response.send_message(
error("Please provide a duration that is less than 28 days."), ephemeral=True
error("Please provide a duration that is less than 28 days."),
ephemeral=True,
)
return
await target.timeout(
parsed_time, reason=f"Muted by {interaction.user.id} for: {reason}"
parsed_time,
reason=f"Muted by {interaction.user.id} for: {reason}",
)
await interaction.response.send_message(
content=f"{target.mention} has been muted for {humanize_timedelta(timedelta=parsed_time)}!\n**Reason** - `{reason}`"
content=f"{target.mention} has been muted for {humanize_timedelta(timedelta=parsed_time)}!\n**Reason** - `{reason}`",
)
if silent is None:
@ -598,7 +609,7 @@ class Aurora(commands.Cog):
reason,
)
await interaction.edit_original_response(
content=f"{target.mention} has been muted for {humanize_timedelta(timedelta=parsed_time)}! (Case `#{moderation_id:,}`)\n**Reason** - `{reason}`"
content=f"{target.mention} has been muted for {humanize_timedelta(timedelta=parsed_time)}! (Case `#{moderation_id:,}`)\n**Reason** - `{reason}`",
)
await log(interaction, moderation_id)
@ -636,14 +647,15 @@ class Aurora(commands.Cog):
if reason:
await target.timeout(
None, reason=f"Unmuted by {interaction.user.id} for: {reason}"
None,
reason=f"Unmuted by {interaction.user.id} for: {reason}",
)
else:
await target.timeout(None, reason=f"Unbanned by {interaction.user.id}")
reason = "No reason given."
await interaction.response.send_message(
content=f"{target.mention} has been unmuted!\n**Reason** - `{reason}`"
content=f"{target.mention} has been unmuted!\n**Reason** - `{reason}`",
)
if silent is None:
@ -673,7 +685,7 @@ class Aurora(commands.Cog):
reason,
)
await interaction.edit_original_response(
content=f"{target.mention} has been unmuted! (Case `#{moderation_id:,}`)\n**Reason** - `{reason}`"
content=f"{target.mention} has been unmuted! (Case `#{moderation_id:,}`)\n**Reason** - `{reason}`",
)
await log(interaction, moderation_id)
@ -702,7 +714,7 @@ class Aurora(commands.Cog):
return
await interaction.response.send_message(
content=f"{target.mention} has been kicked!\n**Reason** - `{reason}`"
content=f"{target.mention} has been kicked!\n**Reason** - `{reason}`",
)
if silent is None:
@ -734,7 +746,7 @@ class Aurora(commands.Cog):
reason,
)
await interaction.edit_original_response(
content=f"{target.mention} has been kicked! (Case `#{moderation_id:,}`)\n**Reason** - `{reason}`"
content=f"{target.mention} has been kicked! (Case `#{moderation_id:,}`)\n**Reason** - `{reason}`",
)
await log(interaction, moderation_id)
@ -750,7 +762,7 @@ class Aurora(commands.Cog):
Choice(name="1 Day", value=86400),
Choice(name="3 Days", value=259200),
Choice(name="7 Days", value=604800),
]
],
)
async def ban(
self,
@ -786,7 +798,8 @@ class Aurora(commands.Cog):
try:
await interaction.guild.fetch_ban(target)
await interaction.response.send_message(
content=error(f"{target.mention} is already banned!"), ephemeral=True
content=error(f"{target.mention} is already banned!"),
ephemeral=True,
)
return
except discord.errors.NotFound:
@ -796,19 +809,21 @@ class Aurora(commands.Cog):
parsed_time = parse_relativedelta(duration)
if parsed_time is None:
await interaction.response.send_message(
content=error("Please provide a valid duration!"), ephemeral=True
content=error("Please provide a valid duration!"),
ephemeral=True,
)
return
try:
parsed_time = timedelta_from_relativedelta(parsed_time)
except ValueError:
await interaction.response.send_message(
content=error("Please provide a valid duration!"), ephemeral=True
content=error("Please provide a valid duration!"),
ephemeral=True,
)
return
await interaction.response.send_message(
content=f"{target.mention} has been banned for {humanize_timedelta(timedelta=parsed_time)}!\n**Reason** - `{reason}`"
content=f"{target.mention} has been banned for {humanize_timedelta(timedelta=parsed_time)}!\n**Reason** - `{reason}`",
)
try:
@ -842,7 +857,7 @@ class Aurora(commands.Cog):
reason,
)
await interaction.edit_original_response(
content=f"{target.mention} has been banned for {humanize_timedelta(timedelta=parsed_time)}! (Case `#{moderation_id}`)\n**Reason** - `{reason}`"
content=f"{target.mention} has been banned for {humanize_timedelta(timedelta=parsed_time)}! (Case `#{moderation_id}`)\n**Reason** - `{reason}`",
)
await log(interaction, moderation_id)
@ -850,14 +865,14 @@ class Aurora(commands.Cog):
await send_evidenceformat(interaction, case)
else:
await interaction.response.send_message(
content=f"{target.mention} has been banned!\n**Reason** - `{reason}`"
content=f"{target.mention} has been banned!\n**Reason** - `{reason}`",
)
if silent is None:
silent = not await config.guild(interaction.guild).dm_users()
if silent is False:
try:
embed = embed = await message_factory(
embed = await message_factory(
await self.bot.get_embed_color(interaction.channel),
guild=interaction.guild,
moderator=interaction.user,
@ -886,7 +901,7 @@ class Aurora(commands.Cog):
reason,
)
await interaction.edit_original_response(
content=f"{target.mention} has been banned! (Case `#{moderation_id:,}`)\n**Reason** - `{reason}`"
content=f"{target.mention} has been banned! (Case `#{moderation_id:,}`)\n**Reason** - `{reason}`",
)
await log(interaction, moderation_id)
@ -918,22 +933,25 @@ class Aurora(commands.Cog):
await interaction.guild.fetch_ban(target)
except discord.errors.NotFound:
await interaction.response.send_message(
content=error(f"{target.mention} is not banned!"), ephemeral=True
content=error(f"{target.mention} is not banned!"),
ephemeral=True,
)
return
if reason:
await interaction.guild.unban(
target, reason=f"Unbanned by {interaction.user.id} for: {reason}"
target,
reason=f"Unbanned by {interaction.user.id} for: {reason}",
)
else:
await interaction.guild.unban(
target, reason=f"Unbanned by {interaction.user.id}"
target,
reason=f"Unbanned by {interaction.user.id}",
)
reason = "No reason given."
await interaction.response.send_message(
content=f"{target.mention} has been unbanned!\n**Reason** - `{reason}`"
content=f"{target.mention} has been unbanned!\n**Reason** - `{reason}`",
)
if silent is None:
@ -963,7 +981,7 @@ class Aurora(commands.Cog):
reason,
)
await interaction.edit_original_response(
content=f"{target.mention} has been unbanned! (Case `#{moderation_id:,}`)\n**Reason** - `{reason}`"
content=f"{target.mention} has been unbanned! (Case `#{moderation_id:,}`)\n**Reason** - `{reason}`",
)
await log(interaction, moderation_id)
@ -1001,42 +1019,28 @@ class Aurora(commands.Cog):
export: bool
Exports the server's entire moderation history to a JSON file"""
if ephemeral is None:
ephemeral = (
await config.user(interaction.user).history_ephemeral()
or await config.guild(interaction.guild).history_ephemeral()
or False
)
ephemeral = await config.user(interaction.user).history_ephemeral() or await config.guild(interaction.guild).history_ephemeral() or False
if inline is None:
inline = (
await config.user(interaction.user).history_inline()
or await config.guild(interaction.guild).history_inline()
or False
)
inline = await config.user(interaction.user).history_inline() or await config.guild(interaction.guild).history_inline() or False
if pagesize is None:
if inline is True:
pagesize = (
await config.user(interaction.user).history_inline_pagesize()
or await config.guild(interaction.guild).history_inline_pagesize()
or 6
)
pagesize = await config.user(interaction.user).history_inline_pagesize() or await config.guild(interaction.guild).history_inline_pagesize() or 6
else:
pagesize = (
await config.user(interaction.user).history_pagesize()
or await config.guild(interaction.guild).history_pagesize()
or 5
)
pagesize = await config.user(interaction.user).history_pagesize() or await config.guild(interaction.guild).history_pagesize() or 5
await interaction.response.defer(ephemeral=ephemeral)
permissions = check_permissions(
interaction.client.user, ["embed_links"], interaction
interaction.client.user,
["embed_links"],
interaction,
)
if permissions:
await interaction.followup.send(
error(
f"I do not have the `{permissions}` permission, required for this action."
f"I do not have the `{permissions}` permission, required for this action.",
),
ephemeral=True,
)
@ -1061,18 +1065,15 @@ class Aurora(commands.Cog):
cases.append(case)
try:
filename = (
str(data_manager.cog_data_path(cog_instance=self))
+ str(os.sep)
+ f"moderation_{interaction.guild.id}.json"
)
filename = str(data_manager.cog_data_path(cog_instance=self)) + str(os.sep) + f"moderation_{interaction.guild.id}.json"
with open(filename, "w", encoding="utf-8") as f:
json.dump(cases, f, indent=2)
await interaction.followup.send(
file=discord.File(
filename, f"moderation_{interaction.guild.id}.json"
filename,
f"moderation_{interaction.guild.id}.json",
),
ephemeral=ephemeral,
)
@ -1081,7 +1082,7 @@ class Aurora(commands.Cog):
except json.JSONDecodeError as e:
await interaction.followup.send(
content=error(
"An error occured while exporting the moderation history.\nError:\n"
"An error occured while exporting the moderation history.\nError:\n",
)
+ box(e, "py"),
ephemeral=ephemeral,
@ -1127,7 +1128,7 @@ class Aurora(commands.Cog):
embed = discord.Embed(color=await self.bot.get_embed_color(interaction.channel))
embed.set_author(icon_url=interaction.guild.icon.url, name="Infraction History")
embed.set_footer(
text=f"Page {page:,}/{page_quantity:,} | {case_quantity:,} Results"
text=f"Page {page:,}/{page_quantity:,} | {case_quantity:,} Results",
)
memory_dict = {}
@ -1136,33 +1137,30 @@ class Aurora(commands.Cog):
if case["target_id"] not in memory_dict:
if case["target_type"] == "USER":
memory_dict[str(case["target_id"])] = await fetch_user_dict(
interaction.client, case["target_id"]
interaction.client,
case["target_id"],
)
elif case["target_type"] == "CHANNEL":
memory_dict[str(case["target_id"])] = await fetch_channel_dict(
interaction.guild, case["target_id"]
interaction.guild,
case["target_id"],
)
target_user = memory_dict[str(case["target_id"])]
if case["target_type"] == "USER":
target_name = (
f"`{target_user['name']}`"
if target_user["discriminator"] == "0"
else f"`{target_user['name']}#{target_user['discriminator']}`"
)
target_name = f"`{target_user['name']}`" if target_user["discriminator"] == "0" else f"`{target_user['name']}#{target_user['discriminator']}`"
elif case["target_type"] == "CHANNEL":
target_name = f"`{target_user['mention']}`"
else:
target_name = ""
if case["moderator_id"] not in memory_dict:
memory_dict[str(case["moderator_id"])] = await fetch_user_dict(
interaction.client, case["moderator_id"]
interaction.client,
case["moderator_id"],
)
moderator_user = memory_dict[str(case["moderator_id"])]
moderator_name = (
f"`{moderator_user['name']}`"
if moderator_user["discriminator"] == "0"
else f"`{moderator_user['name']}#{moderator_user['discriminator']}`"
)
moderator_name = f"`{moderator_user['name']}`" if moderator_user["discriminator"] == "0" else f"`{moderator_user['name']}#{moderator_user['discriminator']}`"
field_name = f"Case #{case['moderation_id']:,} ({str.title(case['moderation_type'])})"
field_value = f"**Target:** {target_name} ({target_user['id']})\n**Moderator:** {moderator_name} ({moderator_user['id']})"
@ -1177,20 +1175,15 @@ class Aurora(commands.Cog):
**{
unit: int(val)
for unit, val in zip(
["hours", "minutes", "seconds"], case["duration"].split(":")
["hours", "minutes", "seconds"],
case["duration"].split(":"),
)
}
)
duration_embed = (
f"{humanize_timedelta(timedelta=td)} | <t:{case['end_timestamp']}:R>"
if bool(case["expired"]) is False
else f"{humanize_timedelta(timedelta=td)} | Expired"
},
)
duration_embed = f"{humanize_timedelta(timedelta=td)} | <t:{case['end_timestamp']}:R>" if bool(case["expired"]) is False else f"{humanize_timedelta(timedelta=td)} | Expired"
field_value += f"\n**Duration:** {duration_embed}"
field_value += (
f"\n**Timestamp:** <t:{case['timestamp']}> | <t:{case['timestamp']}:R>"
)
field_value += f"\n**Timestamp:** <t:{case['timestamp']}> | <t:{case['timestamp']}:R>"
if case["role_id"] != "0":
role = interaction.guild.get_role(int(case["role_id"]))
@ -1208,7 +1201,10 @@ class Aurora(commands.Cog):
@app_commands.command(name="resolve")
async def resolve(
self, interaction: discord.Interaction, case: int, reason: str = None
self,
interaction: discord.Interaction,
case: int,
reason: str = None,
):
"""Resolve a specific case.
@ -1226,7 +1222,7 @@ class Aurora(commands.Cog):
if permissions:
await interaction.response.send_message(
error(
f"I do not have the `{permissions}` permission, required for this action."
f"I do not have the `{permissions}` permission, required for this action.",
),
ephemeral=True,
)
@ -1235,9 +1231,7 @@ class Aurora(commands.Cog):
database = connect()
cursor = database.cursor()
query_1 = (
f"SELECT * FROM moderation_{interaction.guild.id} WHERE moderation_id = ?;"
)
query_1 = f"SELECT * FROM moderation_{interaction.guild.id} WHERE moderation_id = ?;"
cursor.execute(query_1, (case,))
result_1 = cursor.fetchone()
if result_1 is None or case == 0:
@ -1253,7 +1247,7 @@ class Aurora(commands.Cog):
if result_2 is None:
await interaction.response.send_message(
content=error(
f"This moderation has already been resolved!\nUse `/case {case}` for more information."
f"This moderation has already been resolved!\nUse `/case {case}` for more information.",
),
ephemeral=True,
)
@ -1267,7 +1261,7 @@ class Aurora(commands.Cog):
if len(changes) > 25:
await interaction.response.send_message(
content=error(
"Due to limitations with Discord's embed system, you cannot edit a case more than 25 times."
"Due to limitations with Discord's embed system, you cannot edit a case more than 25 times.",
),
ephemeral=True,
)
@ -1279,7 +1273,7 @@ class Aurora(commands.Cog):
"timestamp": case_dict["timestamp"],
"reason": case_dict["reason"],
"user_id": case_dict["moderator_id"],
}
},
)
changes.append(
{
@ -1287,7 +1281,7 @@ class Aurora(commands.Cog):
"timestamp": int(time.time()),
"reason": reason,
"user_id": interaction.user.id,
}
},
)
if case_dict["moderation_type"] in ["UNMUTE", "UNBAN"]:
@ -1301,11 +1295,12 @@ class Aurora(commands.Cog):
if case_dict["moderation_type"] == "MUTE":
try:
member = await interaction.guild.fetch_member(
case_dict["target_id"]
case_dict["target_id"],
)
await member.timeout(
None, reason=f"Case #{case:,} resolved by {interaction.user.id}"
None,
reason=f"Case #{case:,} resolved by {interaction.user.id}",
)
except discord.NotFound:
pass
@ -1315,7 +1310,8 @@ class Aurora(commands.Cog):
user = await interaction.client.fetch_user(case_dict["target_id"])
await interaction.guild.unban(
user, reason=f"Case #{case} resolved by {interaction.user.id}"
user,
reason=f"Case #{case} resolved by {interaction.user.id}",
)
except discord.NotFound:
pass
@ -1340,7 +1336,8 @@ class Aurora(commands.Cog):
case_dict=await fetch_case(case, interaction.guild.id),
)
await interaction.response.send_message(
content=f"✅ Moderation #{case:,} resolved!", embed=embed
content=f"✅ Moderation #{case:,} resolved!",
embed=embed,
)
await log(interaction, case, resolved=True)
@ -1352,7 +1349,7 @@ class Aurora(commands.Cog):
export=[
Choice(name="Export as File", value="file"),
Choice(name="Export as Codeblock", value="codeblock"),
]
],
)
async def case(
self,
@ -1376,41 +1373,35 @@ class Aurora(commands.Cog):
export: bool
Export the case to a JSON file or codeblock"""
permissions = check_permissions(
interaction.client.user, ["embed_links"], interaction
interaction.client.user,
["embed_links"],
interaction,
)
if permissions:
await interaction.response.send_message(
error(
f"I do not have the `{permissions}` permission, required for this action."
f"I do not have the `{permissions}` permission, required for this action.",
),
ephemeral=True,
)
return
if ephemeral is None:
ephemeral = (
await config.user(interaction.user).history_ephemeral()
or await config.guild(interaction.guild).history_ephemeral()
or False
)
ephemeral = await config.user(interaction.user).history_ephemeral() or await config.guild(interaction.guild).history_ephemeral() or False
if case != 0:
case_dict = await fetch_case(case, interaction.guild.id)
if case_dict:
if export:
if export.value == "file" or len(str(case_dict)) > 1800:
filename = (
str(data_manager.cog_data_path(cog_instance=self))
+ str(os.sep)
+ f"moderation_{interaction.guild.id}_case_{case}.json"
)
filename = str(data_manager.cog_data_path(cog_instance=self)) + str(os.sep) + f"moderation_{interaction.guild.id}_case_{case}.json"
with open(filename, "w", encoding="utf-8") as f:
json.dump(case_dict, f, indent=2)
if export.value == "codeblock":
content = f"Case #{case:,} exported.\n" + warning(
"Case was too large to export as codeblock, so it has been uploaded as a `.json` file."
"Case was too large to export as codeblock, so it has been uploaded as a `.json` file.",
)
else:
content = f"Case #{case:,} exported."
@ -1427,34 +1418,41 @@ class Aurora(commands.Cog):
os.remove(filename)
return
await interaction.response.send_message(
content=box(json.dumps(case_dict, indent=2), 'json'),
content=box(json.dumps(case_dict, indent=2), "json"),
ephemeral=ephemeral,
)
return
if changes:
embed = await changes_factory(
interaction=interaction, case_dict=case_dict
interaction=interaction,
case_dict=case_dict,
)
await interaction.response.send_message(
embed=embed, ephemeral=ephemeral
embed=embed,
ephemeral=ephemeral,
)
elif evidenceformat:
content = await evidenceformat_factory(
interaction=interaction, case_dict=case_dict
interaction=interaction,
case_dict=case_dict,
)
await interaction.response.send_message(
content=content, ephemeral=ephemeral
content=content,
ephemeral=ephemeral,
)
else:
embed = await case_factory(
interaction=interaction, case_dict=case_dict
interaction=interaction,
case_dict=case_dict,
)
await interaction.response.send_message(
embed=embed, ephemeral=ephemeral
embed=embed,
ephemeral=ephemeral,
)
return
await interaction.response.send_message(
content=f"No case with case number `{case}` found.", ephemeral=True
content=f"No case with case number `{case}` found.",
ephemeral=True,
)
@app_commands.command(name="edit")
@ -1476,13 +1474,16 @@ class Aurora(commands.Cog):
duration: str
What is the new duration? Does not reapply the moderation if it has already expired.
"""
end_timestamp = None
permissions = check_permissions(
interaction.client.user, ["embed_links"], interaction
interaction.client.user,
["embed_links"],
interaction,
)
if permissions:
await interaction.response.send_message(
error(
f"I do not have the `{permissions}` permission, required for this action."
f"I do not have the `{permissions}` permission, required for this action.",
),
ephemeral=True,
)
@ -1496,26 +1497,25 @@ class Aurora(commands.Cog):
parsed_time = parse_timedelta(duration)
if parsed_time is None:
await interaction.response.send_message(
error("Please provide a valid duration!"), ephemeral=True
error("Please provide a valid duration!"),
ephemeral=True,
)
return
end_timestamp = case_dict["timestamp"] + parsed_time.total_seconds()
if case_dict["moderation_type"] == "MUTE":
if (
time.time() - case_dict["timestamp"]
) + parsed_time.total_seconds() > 2419200:
if (time.time() - case_dict["timestamp"]) + parsed_time.total_seconds() > 2419200:
await interaction.response.send_message(
error(
"Please provide a duration that is less than 28 days from the initial moderation."
)
"Please provide a duration that is less than 28 days from the initial moderation.",
),
)
return
try:
member = await interaction.guild.fetch_member(
case_dict["target_id"]
case_dict["target_id"],
)
await member.timeout(
@ -1529,7 +1529,7 @@ class Aurora(commands.Cog):
if len(changes) > 25:
await interaction.response.send_message(
content=error(
"Due to limitations with Discord's embed system, you cannot edit a case more than 25 times."
"Due to limitations with Discord's embed system, you cannot edit a case more than 25 times.",
),
ephemeral=True,
)
@ -1543,9 +1543,10 @@ class Aurora(commands.Cog):
"user_id": case_dict["moderator_id"],
"duration": case_dict["duration"],
"end_timestamp": case_dict["end_timestamp"],
}
},
)
if parsed_time:
assert end_timestamp is not None
changes.append(
{
"type": "EDIT",
@ -1554,7 +1555,7 @@ class Aurora(commands.Cog):
"user_id": interaction.user.id,
"duration": convert_timedelta_to_str(parsed_time),
"end_timestamp": end_timestamp,
}
},
)
else:
changes.append(
@ -1565,7 +1566,7 @@ class Aurora(commands.Cog):
"user_id": interaction.user.id,
"duration": case_dict["duration"],
"end_timestamp": case_dict["end_timestamp"],
}
},
)
database = connect()
@ -1602,7 +1603,8 @@ class Aurora(commands.Cog):
database.close()
return
await interaction.response.send_message(
content=error(f"No case with case number `{case}` found."), ephemeral=True
content=error(f"No case with case number `{case}` found."),
ephemeral=True,
)
@tasks.loop(minutes=1)
@ -1634,14 +1636,11 @@ class Aurora(commands.Cog):
unban_num = 0
for target_id, moderation_id in zip(target_ids, moderation_ids):
user: discord.User = await self.bot.fetch_user(target_id)
name = (
f"{user.name}#{user.discriminator}"
if user.discriminator != "0"
else user.name
)
name = f"{user.name}#{user.discriminator}" if user.discriminator != "0" else user.name
try:
await guild.unban(
user, reason=f"Automatic unban from case #{moderation_id}"
user,
reason=f"Automatic unban from case #{moderation_id}",
)
embed = await message_factory(
@ -1690,13 +1689,16 @@ class Aurora(commands.Cog):
role_ids = [row[2] for row in result]
for target_id, moderation_id, role_id in zip(
target_ids, moderation_ids, role_ids
target_ids,
moderation_ids,
role_ids,
):
try:
member = await guild.fetch_member(target_id)
await member.remove_roles(
Object(role_id), reason=f"Automatic role removal from case #{moderation_id}"
Object(role_id),
reason=f"Automatic role removal from case #{moderation_id}",
)
removerole_num = removerole_num + 1
@ -1725,13 +1727,16 @@ class Aurora(commands.Cog):
role_ids = [row[2] for row in result]
for target_id, moderation_id, role_id in zip(
target_ids, moderation_ids, role_ids
target_ids,
moderation_ids,
role_ids,
):
try:
member = await guild.fetch_member(target_id)
await member.add_roles(
Object(role_id), reason=f"Automatic role addition from case #{moderation_id}"
Object(role_id),
reason=f"Automatic role addition from case #{moderation_id}",
)
addrole_num = addrole_num + 1
@ -1827,15 +1832,11 @@ class Aurora(commands.Cog):
@commands.admin()
async def aurora_import_aurora(self, ctx: commands.Context):
"""Import moderation history from another bot using Aurora."""
if (
ctx.message.attachments
and ctx.message.attachments[0].content_type
== "application/json; charset=utf-8"
):
if ctx.message.attachments and ctx.message.attachments[0].content_type == "application/json; charset=utf-8":
message = await ctx.send(
warning(
"Are you sure you want to import moderations from another bot?\n**This will overwrite any moderations that already exist in this guild's moderation table.**\n*The import process will block the rest of your bot until it is complete.*"
)
"Are you sure you want to import moderations from another bot?\n**This will overwrite any moderations that already exist in this guild's moderation table.**\n*The import process will block the rest of your bot until it is complete.*",
),
)
await message.edit(view=ImportAuroraView(60, ctx, message))
else:
@ -1845,20 +1846,16 @@ class Aurora(commands.Cog):
@commands.admin()
async def aurora_import_galacticbot(self, ctx: commands.Context):
"""Import moderation history from GalacticBot."""
if (
ctx.message.attachments
and ctx.message.attachments[0].content_type
== "application/json; charset=utf-8"
):
if ctx.message.attachments and ctx.message.attachments[0].content_type == "application/json; charset=utf-8":
message = await ctx.send(
warning(
"Are you sure you want to import GalacticBot moderations?\n**This will overwrite any moderations that already exist in this guild's moderation table.**\n*The import process will block the rest of your bot until it is complete.*"
)
"Are you sure you want to import GalacticBot moderations?\n**This will overwrite any moderations that already exist in this guild's moderation table.**\n*The import process will block the rest of your bot until it is complete.*",
),
)
await message.edit(view=ImportGalacticBotView(60, ctx, message))
else:
await ctx.send(
error("Please provide a valid GalacticBot moderation export file.")
error("Please provide a valid GalacticBot moderation export file."),
)
@aurora.command(aliases=["tdc", "td", "timedeltaconvert"])

View file

@ -17,13 +17,9 @@ class ImportAuroraView(ui.View):
self.message: Message = message
@ui.button(label="Yes", style=ButtonStyle.success)
async def import_button_y(
self, interaction: Interaction, button: ui.Button
): # pylint: disable=unused-argument
async def import_button_y(self, interaction: Interaction, button: ui.Button): # pylint: disable=unused-argument
await self.message.delete()
await interaction.response.send_message(
"Deleting original table...", ephemeral=True
)
await interaction.response.send_message("Deleting original table...", ephemeral=True)
database = connect()
cursor = database.cursor()
@ -101,16 +97,10 @@ class ImportAuroraView(ui.View):
await interaction.edit_original_response(content="Import complete.")
if failed_cases:
await interaction.edit_original_response(
content="Import complete.\n"
+ warning("Failed to import the following cases:\n")
+ box(failed_cases)
)
await interaction.edit_original_response(content="Import complete.\n" + warning("Failed to import the following cases:\n") + box(failed_cases))
@ui.button(label="No", style=ButtonStyle.danger)
async def import_button_n(
self, interaction: Interaction, button: ui.Button
): # pylint: disable=unused-argument
async def import_button_n(self, interaction: Interaction, button: ui.Button): # pylint: disable=unused-argument
await self.message.edit(content="Import cancelled.", view=None)
await self.message.delete(10)
await self.ctx.message.delete(10)

View file

@ -16,13 +16,9 @@ class ImportGalacticBotView(ui.View):
self.message: Message = message
@ui.button(label="Yes", style=ButtonStyle.success)
async def import_button_y(
self, interaction: Interaction, button: ui.Button
): # pylint: disable=unused-argument
async def import_button_y(self, interaction: Interaction, button: ui.Button): # pylint: disable=unused-argument
await self.message.delete()
await interaction.response.send_message(
"Deleting original table...", ephemeral=True
)
await interaction.response.send_message("Deleting original table...", ephemeral=True)
database = connect()
cursor = database.cursor()
@ -92,9 +88,7 @@ class ImportGalacticBotView(ui.View):
if resolved_by is None:
resolved_by = "?"
if resolved_reason is None:
resolved_reason = (
"Could not get resolve reason during moderation import."
)
resolved_reason = "Could not get resolve reason during moderation import."
if resolved_timestamp is None:
resolved_timestamp = timestamp
changes = [
@ -142,16 +136,10 @@ class ImportGalacticBotView(ui.View):
await interaction.edit_original_response(content="Import complete.")
if failed_cases:
await interaction.edit_original_response(
content="Import complete.\n"
+ warning("Failed to import the following cases:\n")
+ box(failed_cases)
)
await interaction.edit_original_response(content="Import complete.\n" + warning("Failed to import the following cases:\n") + box(failed_cases))
@ui.button(label="No", style=ButtonStyle.danger)
async def import_button_n(
self, interaction: Interaction, button: ui.Button
): # pylint: disable=unused-argument
async def import_button_n(self, interaction: Interaction, button: ui.Button): # pylint: disable=unused-argument
await self.message.edit(content="Import cancelled.", view=None)
await self.message.delete(10)
await self.ctx.message.delete(10)

View file

@ -107,7 +107,11 @@ class Guild(ui.View):
await interaction.message.edit(embed=await guild_embed(self.ctx))
@ui.select(placeholder="History Pagesize", options=create_pagesize_options(), row=2)
async def pagesize(self, interaction: Interaction, select: ui.Select,):
async def pagesize(
self,
interaction: Interaction,
select: ui.Select,
):
if not interaction.user.guild_permissions.manage_guild and not interaction.user.guild_permissions.administrator:
await interaction.response.send_message("You must have the manage guild permission to change this setting.", ephemeral=True)
return
@ -119,7 +123,11 @@ class Guild(ui.View):
await interaction.message.edit(embed=await guild_embed(self.ctx))
@ui.select(placeholder="History Inline Pagesize", options=create_pagesize_options(), row=3)
async def inline_pagesize(self, interaction: Interaction, select: ui.Select,):
async def inline_pagesize(
self,
interaction: Interaction,
select: ui.Select,
):
if not interaction.user.guild_permissions.manage_guild and not interaction.user.guild_permissions.administrator:
await interaction.response.send_message("You must have the manage guild permission to change this setting.", ephemeral=True)
return

View file

@ -62,7 +62,11 @@ class Overrides(ui.View):
await interaction.message.edit(embed=await overrides_embed(self.ctx))
@ui.select(placeholder="Inline Pagesize", options=create_pagesize_options(), row=1)
async def inline_pagesize(self, interaction: Interaction, select: ui.Select,):
async def inline_pagesize(
self,
interaction: Interaction,
select: ui.Select,
):
if self.ctx.author != interaction.user:
await interaction.response.send_message("You cannot change this setting for other users.", ephemeral=True)
return
@ -74,7 +78,11 @@ class Overrides(ui.View):
await interaction.message.edit(embed=await overrides_embed(self.ctx))
@ui.select(placeholder="Pagesize", options=create_pagesize_options(), row=2)
async def pagesize(self, interaction: Interaction, select: ui.Select,):
async def pagesize(
self,
interaction: Interaction,
select: ui.Select,
):
if self.ctx.author != interaction.user:
await interaction.response.send_message("You cannot change this setting for other users.", ephemeral=True)
return

View file

@ -8,23 +8,18 @@ from discord import Guild
from redbot.core import data_manager
from .logger import logger
from .utils import (convert_timedelta_to_str, generate_dict,
get_next_case_number)
from .utils import convert_timedelta_to_str, generate_dict, get_next_case_number
def connect() -> sqlite3.Connection:
"""Connects to the SQLite database, and returns a connection object."""
try:
connection = sqlite3.connect(
database=data_manager.cog_data_path(raw_name="Aurora") / "aurora.db"
)
return connection
return sqlite3.connect(database=data_manager.cog_data_path(raw_name="Aurora") / "aurora.db")
except sqlite3.OperationalError as e:
logger.error("Unable to access the SQLite database!\nError:\n%s", e.msg)
raise ConnectionRefusedError(
f"Unable to access the SQLite Database!\n{e.msg}"
) from e
msg = f"Unable to access the SQLite Database!\n{e.msg}"
raise ConnectionRefusedError(msg) from e
async def create_guild_table(guild: Guild):

View file

@ -75,7 +75,9 @@ async def message_factory(
if await config.guild(guild).show_moderator() and moderator is not None:
embed.add_field(
name="Moderator", value=f"`{moderator.name} ({moderator.id})`", inline=False
name="Moderator",
value=f"`{moderator.name} ({moderator.id})`",
inline=False,
)
embed.add_field(name="Reason", value=f"`{reason}`", inline=False)
@ -94,7 +96,9 @@ async def message_factory(
async def log_factory(
interaction: Interaction, case_dict: dict, resolved: bool = False
interaction: Interaction,
case_dict: dict,
resolved: bool = False,
) -> Embed:
"""This function creates a log embed from set parameters, meant for moderation logging.
@ -103,14 +107,11 @@ async def log_factory(
case_dict (dict): The case dictionary.
resolved (bool, optional): Whether the case is resolved or not. Defaults to False.
"""
target_name = ""
if resolved:
if case_dict["target_type"] == "USER":
target_user = await fetch_user_dict(interaction.client, case_dict["target_id"])
target_name = (
f"`{target_user['name']}`"
if target_user["discriminator"] == "0"
else f"`{target_user['name']}#{target_user['discriminator']}`"
)
target_name = f"`{target_user['name']}`" if target_user["discriminator"] == "0" else f"`{target_user['name']}#{target_user['discriminator']}`"
elif case_dict["target_type"] == "CHANNEL":
target_user = await fetch_channel_dict(interaction.guild, case_dict["target_id"])
if target_user["mention"]:
@ -119,11 +120,7 @@ async def log_factory(
target_name = f"`{target_user['name']}`"
moderator_user = await fetch_user_dict(interaction.client, case_dict["moderator_id"])
moderator_name = (
f"`{moderator_user['name']}`"
if moderator_user["discriminator"] == "0"
else f"`{moderator_user['name']}#{moderator_user['discriminator']}`"
)
moderator_name = f"`{moderator_user['name']}`" if moderator_user["discriminator"] == "0" else f"`{moderator_user['name']}#{moderator_user['discriminator']}`"
embed = Embed(
title=f"📕 Case #{case_dict['moderation_id']:,} Resolved",
@ -140,40 +137,24 @@ async def log_factory(
["hours", "minutes", "seconds"],
case_dict["duration"].split(":"),
)
}
)
duration_embed = (
f"{humanize_timedelta(timedelta=td)} | <t:{case_dict['end_timestamp']}:R>"
if case_dict["expired"] == "0"
else str(humanize_timedelta(timedelta=td))
)
embed.description = (
embed.description
+ f"\n**Duration:** {duration_embed}\n**Expired:** {bool(case_dict['expired'])}"
},
)
duration_embed = f"{humanize_timedelta(timedelta=td)} | <t:{case_dict['end_timestamp']}:R>" if case_dict["expired"] == "0" else str(humanize_timedelta(timedelta=td))
embed.description = embed.description + f"\n**Duration:** {duration_embed}\n**Expired:** {bool(case_dict['expired'])}"
embed.add_field(name="Reason", value=box(case_dict["reason"]), inline=False)
resolved_user = await fetch_user_dict(interaction.client, case_dict["resolved_by"])
resolved_name = (
resolved_user["name"]
if resolved_user["discriminator"] == "0"
else f"{resolved_user['name']}#{resolved_user['discriminator']}"
)
resolved_name = resolved_user["name"] if resolved_user["discriminator"] == "0" else f"{resolved_user['name']}#{resolved_user['discriminator']}"
embed.add_field(
name="Resolve Reason",
value=f"Resolved by `{resolved_name}` ({resolved_user['id']}) for:\n"
+ box(case_dict["resolve_reason"]),
value=f"Resolved by `{resolved_name}` ({resolved_user['id']}) for:\n" + box(case_dict["resolve_reason"]),
inline=False,
)
else:
if case_dict["target_type"] == "USER":
target_user = await fetch_user_dict(interaction.client, case_dict["target_id"])
target_name = (
f"`{target_user['name']}`"
if target_user["discriminator"] == "0"
else f"`{target_user['name']}#{target_user['discriminator']}`"
)
target_name = f"`{target_user['name']}`" if target_user["discriminator"] == "0" else f"`{target_user['name']}#{target_user['discriminator']}`"
elif case_dict["target_type"] == "CHANNEL":
target_user = await fetch_channel_dict(interaction.guild, case_dict["target_id"])
if target_user["mention"]:
@ -182,11 +163,7 @@ async def log_factory(
target_name = f"`{target_user['name']}`"
moderator_user = await fetch_user_dict(interaction.client, case_dict["moderator_id"])
moderator_name = (
f"`{moderator_user['name']}`"
if moderator_user["discriminator"] == "0"
else f"`{moderator_user['name']}#{moderator_user['discriminator']}`"
)
moderator_name = f"`{moderator_user['name']}`" if moderator_user["discriminator"] == "0" else f"`{moderator_user['name']}#{moderator_user['discriminator']}`"
embed = Embed(
title=f"📕 Case #{case_dict['moderation_id']:,}",
@ -202,12 +179,9 @@ async def log_factory(
["hours", "minutes", "seconds"],
case_dict["duration"].split(":"),
)
}
)
embed.description = (
embed.description
+ f"\n**Duration:** {humanize_timedelta(timedelta=td)} | <t:{case_dict['end_timestamp']}:R>"
},
)
embed.description = embed.description + f"\n**Duration:** {humanize_timedelta(timedelta=td)} | <t:{case_dict['end_timestamp']}:R>"
embed.add_field(name="Reason", value=box(case_dict["reason"]), inline=False)
return embed
@ -220,13 +194,10 @@ async def case_factory(interaction: Interaction, case_dict: dict) -> Embed:
interaction (Interaction): The interaction object.
case_dict (dict): The case dictionary.
"""
target_name = ""
if case_dict["target_type"] == "USER":
target_user = await fetch_user_dict(interaction.client, case_dict["target_id"])
target_name = (
f"`{target_user['name']}`"
if target_user["discriminator"] == "0"
else f"`{target_user['name']}#{target_user['discriminator']}`"
)
target_name = f"`{target_user['name']}`" if target_user["discriminator"] == "0" else f"`{target_user['name']}#{target_user['discriminator']}`"
elif case_dict["target_type"] == "CHANNEL":
target_user = await fetch_channel_dict(interaction.guild, case_dict["target_id"])
if target_user["mention"]:
@ -235,11 +206,7 @@ async def case_factory(interaction: Interaction, case_dict: dict) -> Embed:
target_name = f"`{target_user['name']}`"
moderator_user = await fetch_user_dict(interaction.client, case_dict["moderator_id"])
moderator_name = (
f"`{moderator_user['name']}`"
if moderator_user["discriminator"] == "0"
else f"`{moderator_user['name']}#{moderator_user['discriminator']}`"
)
moderator_name = f"`{moderator_user['name']}`" if moderator_user["discriminator"] == "0" else f"`{moderator_user['name']}#{moderator_user['discriminator']}`"
embed = Embed(
title=f"📕 Case #{case_dict['moderation_id']:,}",
@ -252,41 +219,28 @@ async def case_factory(interaction: Interaction, case_dict: dict) -> Embed:
**{
unit: int(val)
for unit, val in zip(
["hours", "minutes", "seconds"], case_dict["duration"].split(":")
["hours", "minutes", "seconds"],
case_dict["duration"].split(":"),
)
}
)
duration_embed = (
f"{humanize_timedelta(timedelta=td)} | <t:{case_dict['end_timestamp']}:R>"
if bool(case_dict["expired"]) is False
else str(humanize_timedelta(timedelta=td))
},
)
duration_embed = f"{humanize_timedelta(timedelta=td)} | <t:{case_dict['end_timestamp']}:R>" if bool(case_dict["expired"]) is False else str(humanize_timedelta(timedelta=td))
embed.description += f"\n**Duration:** {duration_embed}\n**Expired:** {bool(case_dict['expired'])}"
embed.description += (
f"\n**Changes:** {len(case_dict['changes']) - 1}"
if case_dict["changes"]
else "\n**Changes:** 0"
)
embed.description += f"\n**Changes:** {len(case_dict['changes']) - 1}" if case_dict["changes"] else "\n**Changes:** 0"
if case_dict["role_id"]:
embed.description += f"\n**Role:** <@&{case_dict['role_id']}>"
if case_dict["metadata"]:
if case_dict["metadata"]["imported_from"]:
embed.description += (
f"\n**Imported From:** {case_dict['metadata']['imported_from']}"
)
embed.description += f"\n**Imported From:** {case_dict['metadata']['imported_from']}"
embed.add_field(name="Reason", value=box(case_dict["reason"]), inline=False)
if case_dict["resolved"] == 1:
resolved_user = await fetch_user_dict(interaction.client, case_dict["resolved_by"])
resolved_name = (
f"`{resolved_user['name']}`"
if resolved_user["discriminator"] == "0"
else f"`{resolved_user['name']}#{resolved_user['discriminator']}`"
)
resolved_name = f"`{resolved_user['name']}`" if resolved_user["discriminator"] == "0" else f"`{resolved_user['name']}#{resolved_user['discriminator']}`"
embed.add_field(
name="Resolve Reason",
value=f"Resolved by {resolved_name} ({resolved_user['id']}) for:\n{box(case_dict['resolve_reason'])}",
@ -314,15 +268,12 @@ async def changes_factory(interaction: Interaction, case_dict: dict) -> Embed:
for change in case_dict["changes"]:
if change["user_id"] not in memory_dict:
memory_dict[str(change["user_id"])] = await fetch_user_dict(
interaction.client, change["user_id"]
interaction.client,
change["user_id"],
)
user = memory_dict[str(change["user_id"])]
name = (
user["name"]
if user["discriminator"] == "0"
else f"{user['name']}#{user['discriminator']}"
)
name = user["name"] if user["discriminator"] == "0" else f"{user['name']}#{user['discriminator']}"
timestamp = f"<t:{change['timestamp']}> | <t:{change['timestamp']}:R>"
@ -360,24 +311,17 @@ async def evidenceformat_factory(interaction: Interaction, case_dict: dict) -> s
interaction (Interaction): The interaction object.
case_dict (dict): The case dictionary.
"""
target_name = ""
if case_dict["target_type"] == "USER":
target_user = await fetch_user_dict(interaction.client, case_dict["target_id"])
target_name = (
target_user["name"]
if target_user["discriminator"] == "0"
else f"{target_user['name']}#{target_user['discriminator']}"
)
target_name = target_user["name"] if target_user["discriminator"] == "0" else f"{target_user['name']}#{target_user['discriminator']}"
elif case_dict["target_type"] == "CHANNEL":
target_user = await fetch_channel_dict(interaction.guild, case_dict["target_id"])
target_name = target_user["name"]
moderator_user = await fetch_user_dict(interaction.client, case_dict["moderator_id"])
moderator_name = (
moderator_user["name"]
if moderator_user["discriminator"] == "0"
else f"{moderator_user['name']}#{moderator_user['discriminator']}"
)
moderator_name = moderator_user["name"] if moderator_user["discriminator"] == "0" else f"{moderator_user['name']}#{moderator_user['discriminator']}"
content = f"Case: {case_dict['moderation_id']:,} ({str.title(case_dict['moderation_type'])})\nTarget: {target_name} ({target_user['id']})\nModerator: {moderator_name} ({moderator_user['id']})"
@ -419,17 +363,11 @@ async def overrides_embed(ctx: commands.Context) -> Embed:
}
override_str = [
"- "
+ bold("Auto Evidence Format: ")
+ get_bool_emoji(override_settings["auto_evidenceformat"]),
"- " + bold("Auto Evidence Format: ") + get_bool_emoji(override_settings["auto_evidenceformat"]),
"- " + bold("Ephemeral: ") + get_bool_emoji(override_settings["ephemeral"]),
"- " + bold("History Inline: ") + get_bool_emoji(override_settings["inline"]),
"- "
+ bold("History Inline Pagesize: ")
+ get_pagesize_str(override_settings["inline_pagesize"]),
"- "
+ bold("History Pagesize: ")
+ get_pagesize_str(override_settings["pagesize"]),
"- " + bold("History Inline Pagesize: ") + get_pagesize_str(override_settings["inline_pagesize"]),
"- " + bold("History Pagesize: ") + get_pagesize_str(override_settings["pagesize"]),
]
override_str = "\n".join(override_str)
@ -451,7 +389,7 @@ async def guild_embed(ctx: commands.Context) -> Embed:
guild_settings = {
"show_moderator": await config.guild(ctx.guild).show_moderator(),
"use_discord_permissions": await config.guild(
ctx.guild
ctx.guild,
).use_discord_permissions(),
"ignore_modlog": await config.guild(ctx.guild).ignore_modlog(),
"ignore_other_bots": await config.guild(ctx.guild).ignore_other_bots(),
@ -461,7 +399,7 @@ async def guild_embed(ctx: commands.Context) -> Embed:
"history_inline": await config.guild(ctx.guild).history_inline(),
"history_pagesize": await config.guild(ctx.guild).history_pagesize(),
"history_inline_pagesize": await config.guild(
ctx.guild
ctx.guild,
).history_inline_pagesize(),
"auto_evidenceformat": await config.guild(ctx.guild).auto_evidenceformat(),
"respect_hierarchy": await config.guild(ctx.guild).respect_hierarchy(),
@ -474,37 +412,17 @@ async def guild_embed(ctx: commands.Context) -> Embed:
channel = channel.mention
guild_str = [
"- "
+ bold("Show Moderator: ")
+ get_bool_emoji(guild_settings["show_moderator"]),
"- "
+ bold("Use Discord Permissions: ")
+ get_bool_emoji(guild_settings["use_discord_permissions"]),
"- "
+ bold("Respect Hierarchy: ")
+ get_bool_emoji(guild_settings["respect_hierarchy"]),
"- "
+ bold("Ignore Modlog: ")
+ get_bool_emoji(guild_settings["ignore_modlog"]),
"- "
+ bold("Ignore Other Bots: ")
+ get_bool_emoji(guild_settings["ignore_other_bots"]),
"- " + bold("Show Moderator: ") + get_bool_emoji(guild_settings["show_moderator"]),
"- " + bold("Use Discord Permissions: ") + get_bool_emoji(guild_settings["use_discord_permissions"]),
"- " + bold("Respect Hierarchy: ") + get_bool_emoji(guild_settings["respect_hierarchy"]),
"- " + bold("Ignore Modlog: ") + get_bool_emoji(guild_settings["ignore_modlog"]),
"- " + bold("Ignore Other Bots: ") + get_bool_emoji(guild_settings["ignore_other_bots"]),
"- " + bold("DM Users: ") + get_bool_emoji(guild_settings["dm_users"]),
"- "
+ bold("Auto Evidence Format: ")
+ get_bool_emoji(guild_settings["auto_evidenceformat"]),
"- "
+ bold("Ephemeral: ")
+ get_bool_emoji(guild_settings["history_ephemeral"]),
"- "
+ bold("History Inline: ")
+ get_bool_emoji(guild_settings["history_inline"]),
"- "
+ bold("History Pagesize: ")
+ get_pagesize_str(guild_settings["history_pagesize"]),
"- "
+ bold("History Inline Pagesize: ")
+ get_pagesize_str(guild_settings["history_inline_pagesize"]),
"- " + bold("Auto Evidence Format: ") + get_bool_emoji(guild_settings["auto_evidenceformat"]),
"- " + bold("Ephemeral: ") + get_bool_emoji(guild_settings["history_ephemeral"]),
"- " + bold("History Inline: ") + get_bool_emoji(guild_settings["history_inline"]),
"- " + bold("History Pagesize: ") + get_pagesize_str(guild_settings["history_pagesize"]),
"- " + bold("History Inline Pagesize: ") + get_pagesize_str(guild_settings["history_inline_pagesize"]),
"- " + bold("Log Channel: ") + channel,
]
guild_str = "\n".join(guild_str)
@ -528,17 +446,21 @@ async def addrole_embed(ctx: commands.Context) -> Embed:
for role in whitelist:
evalulated_role = ctx.guild.get_role(role) or error(f"`{role}` (Not Found)")
if isinstance(evalulated_role, Role):
roles.append({
roles.append(
{
"id": evalulated_role.id,
"mention": evalulated_role.mention,
"position": evalulated_role.position
})
"position": evalulated_role.position,
},
)
else:
roles.append({
roles.append(
{
"id": role,
"mention": error(f"`{role}` (Not Found)"),
"position": 0
})
"position": 0,
},
)
if roles:
roles = sorted(roles, key=lambda x: x["position"], reverse=True)
@ -549,9 +471,7 @@ async def addrole_embed(ctx: commands.Context) -> Embed:
e = await _config(ctx)
e.title += ": Addrole Whitelist"
e.description = (
"Use the select menu below to manage this guild's addrole whitelist."
)
e.description = "Use the select menu below to manage this guild's addrole whitelist."
if len(whitelist_str) > 4000 and len(whitelist_str) < 5000:
lines = whitelist_str.split("\n")
@ -581,17 +501,21 @@ async def immune_embed(ctx: commands.Context) -> Embed:
for role in immune_roles:
evalulated_role = ctx.guild.get_role(role) or error(f"`{role}` (Not Found)")
if isinstance(evalulated_role, Role):
roles.append({
roles.append(
{
"id": evalulated_role.id,
"mention": evalulated_role.mention,
"position": evalulated_role.position
})
"position": evalulated_role.position,
},
)
else:
roles.append({
roles.append(
{
"id": role,
"mention": error(f"`{role}` (Not Found)"),
"position": 0
})
"position": 0,
},
)
if roles:
roles = sorted(roles, key=lambda x: x["position"], reverse=True)

View file

@ -32,24 +32,17 @@ def check_permissions(
raise (KeyError)
for permission in permissions:
if (
not getattr(resolved_permissions, permission, False)
and resolved_permissions.administrator is not True
):
if not getattr(resolved_permissions, permission, False) and resolved_permissions.administrator is not True:
return permission
return False
async def check_moddable(
target: Union[User, Member], interaction: Interaction, permissions: list
) -> bool:
async def check_moddable(target: Union[User, Member], interaction: Interaction, permissions: list) -> bool:
"""Checks if a moderator can moderate a target."""
if check_permissions(interaction.client.user, permissions, guild=interaction.guild):
await interaction.response.send_message(
error(
f"I do not have the `{permissions}` permission, required for this action."
),
error(f"I do not have the `{permissions}` permission, required for this action."),
ephemeral=True,
)
return False
@ -57,43 +50,30 @@ async def check_moddable(
if await config.guild(interaction.guild).use_discord_permissions() is True:
if check_permissions(interaction.user, permissions, guild=interaction.guild):
await interaction.response.send_message(
error(
f"You do not have the `{permissions}` permission, required for this action."
),
error(f"You do not have the `{permissions}` permission, required for this action."),
ephemeral=True,
)
return False
if interaction.user.id == target.id:
await interaction.response.send_message(
content="You cannot moderate yourself!", ephemeral=True
)
await interaction.response.send_message(content="You cannot moderate yourself!", ephemeral=True)
return False
if target.bot:
await interaction.response.send_message(
content="You cannot moderate bots!", ephemeral=True
)
await interaction.response.send_message(content="You cannot moderate bots!", ephemeral=True)
return False
if isinstance(target, Member):
if interaction.user.top_role <= target.top_role and await config.guild(interaction.guild).respect_hierarchy() is True:
await interaction.response.send_message(
content=error(
"You cannot moderate members with a higher role than you!"
),
content=error("You cannot moderate members with a higher role than you!"),
ephemeral=True,
)
return False
if (
interaction.guild.get_member(interaction.client.user.id).top_role
<= target.top_role
):
if interaction.guild.get_member(interaction.client.user.id).top_role <= target.top_role:
await interaction.response.send_message(
content=error(
"You cannot moderate members with a role higher than the bot!"
),
content=error("You cannot moderate members with a role higher than the bot!"),
ephemeral=True,
)
return False
@ -118,15 +98,13 @@ async def get_next_case_number(guild_id: str, cursor=None) -> int:
if not cursor:
database = connect()
cursor = database.cursor()
cursor.execute(
f"SELECT moderation_id FROM `moderation_{guild_id}` ORDER BY moderation_id DESC LIMIT 1"
)
cursor.execute(f"SELECT moderation_id FROM `moderation_{guild_id}` ORDER BY moderation_id DESC LIMIT 1")
result = cursor.fetchone()
return (result[0] + 1) if result else 1
def generate_dict(result) -> dict:
case = {
return {
"moderation_id": result[0],
"timestamp": result[1],
"moderation_type": result[2],
@ -144,7 +122,6 @@ def generate_dict(result) -> dict:
"changes": json.loads(result[14]),
"metadata": json.loads(result[15]),
}
return case
async def fetch_user_dict(client: commands.Bot, user_id: str) -> dict:
@ -171,7 +148,6 @@ async def fetch_user_dict(client: commands.Bot, user_id: str) -> dict:
"discriminator": "0",
}
return user_dict
@ -198,11 +174,9 @@ async def fetch_role_dict(guild: Guild, role_id: int) -> dict:
"""This function returns a dictionary containing either role information or a standard deleted role template."""
role = guild.get_role(int(role_id))
if not role:
role_dict = {"id": role_id, "name": "Deleted Role"}
pass
role_dict = {"id": role.id, "name": role.name}
return role_dict
return {"id": role.id, "name": role.name}
async def log(interaction: Interaction, moderation_id: int, resolved: bool = False) -> None:
@ -216,9 +190,7 @@ async def log(interaction: Interaction, moderation_id: int, resolved: bool = Fal
case = await fetch_case(moderation_id, interaction.guild.id)
if case:
embed = await log_factory(
interaction=interaction, case_dict=case, resolved=resolved
)
embed = await log_factory(interaction=interaction, case_dict=case, resolved=resolved)
try:
await logging_channel.send(embed=embed)
except Forbidden:
@ -229,11 +201,7 @@ async def send_evidenceformat(interaction: Interaction, case_dict: dict) -> None
"""This function sends an ephemeral message to the moderator who took the moderation action, with a pre-made codeblock for use in the mod-evidence channel."""
from .factory import evidenceformat_factory
send_evidence_bool = (
await config.user(interaction.user).auto_evidenceformat()
or await config.guild(interaction.guild).auto_evidenceformat()
or False
)
send_evidence_bool = await config.user(interaction.user).auto_evidenceformat() or await config.guild(interaction.guild).auto_evidenceformat() or False
if send_evidence_bool is False:
return
@ -274,24 +242,19 @@ def create_pagesize_options() -> list[SelectOption]:
label="Default",
value="default",
description="Reset the pagesize to the default value.",
),
)
)
for i in range(1, 21):
options.append(
SelectOption(
label=str(i),
value=str(i),
description=f"Set the pagesize to {i}.",
)
)
options.extend(SelectOption(label=str(i), value=str(i), description=f"Set the pagesize to {i}") for i in range(1, 21))
return options
def timedelta_from_relativedelta(relativedelta: rd) -> td:
"""Converts a relativedelta object to a timedelta object."""
now = datetime.now()
then = now - relativedelta
return now - then
def get_footer_image(coginstance: commands.Cog) -> File:
"""Returns the footer image for the embeds."""
image_path = data_manager.bundled_data_path(coginstance) / "arrow.png"

View file

@ -17,13 +17,16 @@ from redbot.core.bot import Red
from redbot.core.utils.chat_formatting import bold, error, humanize_list, text_to_file
# Disable Ruff & Pylint complaining about accessing private members
# That's kind of necessary for this cog to function because the Downloader cog has a limited public API
# ruff: noqa: SLF001 # Private member access
# pylint: disable=protected-access
class Backup(commands.Cog):
"""A utility to make reinstalling repositories and cogs after migrating the bot far easier."""
__author__ = ["[cswimr](https://www.coastalcommits.com/cswimr)"]
__git__ = "https://www.coastalcommits.com/cswimr/SeaCogs"
__version__ = "1.1.1"
__version__ = "1.1.4"
__documentation__ = "https://seacogs.coastalcommits.com/backup/"
def __init__(self, bot: Red):
@ -42,22 +45,18 @@ class Backup(commands.Cog):
]
return "\n".join(text)
@commands.group(autohelp=True)
@commands.group(autohelp=True) # type: ignore
@commands.is_owner()
async def backup(self, ctx: commands.Context):
async def backup(self, ctx: commands.Context) -> None:
"""Backup your installed cogs."""
@backup.command(name="export")
@commands.is_owner()
async def backup_export(self, ctx: commands.Context):
async def backup_export(self, ctx: commands.Context) -> None:
"""Export your installed repositories and cogs to a file."""
downloader = ctx.bot.get_cog("Downloader")
if downloader is None:
await ctx.send(
error(
f"You do not have the `Downloader` cog loaded. Please run `{ctx.prefix}load downloader` and try again."
)
)
await ctx.send(error(f"You do not have the `Downloader` cog loaded. Please run `{ctx.prefix}load downloader` and try again."))
return
all_repos = list(downloader._repo_manager.repos)
@ -78,7 +77,7 @@ class Backup(commands.Cog):
if cog.repo_name == repo.name:
cog_dict = {
"name": cog.name,
# "loaded": cog.name in ctx.bot.extensions.keys(),
# "loaded": cog.name in ctx.bot.extensions.keys(), # noqa: ERA001
# this functionality was planned but never implemented due to Red limitations
# and the possibility of restoration functionality being added to Core
"pinned": cog.pinned,
@ -88,30 +87,24 @@ class Backup(commands.Cog):
export_data.append(repo_dict)
await ctx.send(
file=text_to_file(json.dumps(export_data, indent=4), "backup.json")
)
await ctx.send(file=text_to_file(json.dumps(export_data, indent=4), "backup.json"))
@backup.command(name="import")
@commands.is_owner()
async def backup_import(self, ctx: commands.Context):
async def backup_import(self, ctx: commands.Context) -> None:
"""Import your installed repositories and cogs from an export file."""
try:
export = json.loads(await ctx.message.attachments[0].read())
except (json.JSONDecodeError, IndexError):
try:
export = json.loads(await ctx.message.reference.resolved.attachments[0].read())
export = json.loads(await ctx.message.reference.resolved.attachments[0].read()) # type: ignore - this is fine to let error because it gets handled
except (json.JSONDecodeError, IndexError, AttributeError):
await ctx.send(error("Please provide a valid JSON export file."))
return
downloader = ctx.bot.get_cog("Downloader")
if downloader is None:
await ctx.send(
error(
f"You do not have the `Downloader` cog loaded. Please run `{ctx.prefix}load downloader` and try again."
)
)
await ctx.send(error(f"You do not have the `Downloader` cog loaded. Please run `{ctx.prefix}load downloader` and try again."))
return
repo_s = []
@ -133,32 +126,20 @@ class Backup(commands.Cog):
repo_e.append("PyLav cogs are not supported.")
continue
if name.startswith(".") or name.endswith("."):
repo_e.append(
f"Invalid repository name: {name}\nRepository names cannot start or end with a dot."
)
repo_e.append(f"Invalid repository name: {name}\nRepository names cannot start or end with a dot.")
continue
if re.match(r"^[a-zA-Z0-9_\-\.]+$", name) is None:
repo_e.append(
f"Invalid repository name: {name}\nRepository names may only contain letters, numbers, underscores, hyphens, and dots."
)
repo_e.append(f"Invalid repository name: {name}\nRepository names may only contain letters, numbers, underscores, hyphens, and dots.")
continue
try:
repository = await downloader._repo_manager.add_repo(
url, name, branch
)
repo_s.append(
f"Added repository {name} from {url} on branch {branch}."
)
self.logger.debug(
"Added repository %s from %s on branch %s", name, url, branch
)
repository = await downloader._repo_manager.add_repo(url, name, branch)
repo_s.append(f"Added repository {name} from {url} on branch {branch}.")
self.logger.debug("Added repository %s from %s on branch %s", name, url, branch)
except errors.ExistingGitRepo:
repo_e.append(f"Repository {name} already exists.")
repository = downloader._repo_manager.get_repo(
name
)
repository = downloader._repo_manager.get_repo(name)
self.logger.debug("Repository %s already exists", name)
except errors.AuthenticationError as err:
@ -172,9 +153,7 @@ class Backup(commands.Cog):
continue
except errors.CloningError as err:
repo_e.append(
f"Cloning error while adding repository {name}. See logs for more information."
)
repo_e.append(f"Cloning error while adding repository {name}. See logs for more information.")
self.logger.exception(
"Something went wrong whilst cloning %s (to revision %s)",
url,
@ -184,9 +163,7 @@ class Backup(commands.Cog):
continue
except OSError:
repo_e.append(
f"OS error while adding repository {name}. See logs for more information."
)
repo_e.append(f"OS error while adding repository {name}. See logs for more information.")
self.logger.exception(
"Something went wrong trying to add repo %s under name %s",
url,
@ -206,23 +183,19 @@ class Backup(commands.Cog):
continue
cog_modules.append(cog_module)
for cog in set(cog.name for cog in cog_modules):
for cog in {cog.name for cog in cog_modules}:
poss_installed_path = (await downloader.cog_install_path()) / cog
if poss_installed_path.exists():
with contextlib.suppress(commands.ExtensionNotLoaded):
await ctx.bot.unload_extension(cog)
await ctx.bot.remove_loaded_package(cog)
await downloader._delete_cog(
poss_installed_path
)
await downloader._delete_cog(poss_installed_path)
uninstall_s.append(f"Uninstalled {cog}")
self.logger.debug("Uninstalled %s", cog)
else:
uninstall_e.append(f"Failed to uninstall {cog}")
self.logger.warning("Failed to uninstall %s", cog)
await downloader._remove_from_installed(
cog_modules
)
await downloader._remove_from_installed(cog_modules)
for cog in cogs:
cog_name = cog["name"]
@ -236,25 +209,15 @@ class Backup(commands.Cog):
if cog_name == "backup" and "cswimr/SeaCogs" in url:
continue
async with repository.checkout(
commit, exit_to_rev=repository.branch
):
cogs_c, message = (
await downloader._filter_incorrect_cogs_by_names(
repository, [cog_name]
)
)
async with repository.checkout(commit, exit_to_rev=repository.branch):
cogs_c, message = await downloader._filter_incorrect_cogs_by_names(repository, [cog_name])
if not cogs_c:
install_e.append(message)
self.logger.error(message)
continue
failed_reqs = await downloader._install_requirements(
cogs_c
)
failed_reqs = await downloader._install_requirements(cogs_c)
if failed_reqs:
install_e.append(
f"Failed to install {cog_name} due to missing requirements: {failed_reqs}"
)
install_e.append(f"Failed to install {cog_name} due to missing requirements: {failed_reqs}")
self.logger.error(
"Failed to install %s due to missing requirements: %s",
cog_name,
@ -262,51 +225,37 @@ class Backup(commands.Cog):
)
continue
installed_cogs, failed_cogs = await downloader._install_cogs(
cogs_c
)
installed_cogs, failed_cogs = await downloader._install_cogs(cogs_c)
if repository.available_libraries:
installed_libs, failed_libs = (
await repository.install_libraries(
installed_libs, failed_libs = await repository.install_libraries(
target_dir=downloader.SHAREDLIB_PATH,
req_target_dir=downloader.LIB_PATH,
)
)
else:
installed_libs = None
failed_libs = None
if cog_pinned:
for cog in installed_cogs:
for cog in installed_cogs: # noqa: PLW2901
cog.pinned = True
await downloader._save_to_installed(
installed_cogs + installed_libs
if installed_libs
else installed_cogs
)
await downloader._save_to_installed(installed_cogs + installed_libs if installed_libs else installed_cogs)
if installed_cogs:
installed_cog_name = installed_cogs[0].name
install_s.append(f"Installed {installed_cog_name}")
self.logger.debug("Installed %s", installed_cog_name)
if installed_libs:
for lib in installed_libs:
install_s.append(
f"Installed {lib.name} required for {cog_name}"
)
self.logger.debug(
"Installed %s required for %s", lib.name, cog_name
)
install_s.append(f"Installed {lib.name} required for {cog_name}")
self.logger.debug("Installed %s required for %s", lib.name, cog_name)
if failed_cogs:
failed_cog_name = failed_cogs[0].name
install_e.append(f"Failed to install {failed_cog_name}")
self.logger.error("Failed to install %s", failed_cog_name)
if failed_libs:
for lib in failed_libs:
install_e.append(
f"Failed to install {lib.name} required for {cog_name}"
)
install_e.append(f"Failed to install {lib.name} required for {cog_name}")
self.logger.error(
"Failed to install %s required for %s",
lib.name,

View file

@ -1,15 +1,22 @@
{
"author" : ["cswimr"],
"install_msg" : "Thank you for installing Backup!\nYou can find the source code of this cog [here](https://coastalcommits.com/cswimr/SeaCogs).",
"name" : "Backup",
"short" : "A utility to make reinstalling repositories and cogs after migrating the bot far easier.",
"description" : "A utility to make reinstalling repositories and cogs after migrating the bot far easier.",
"end_user_data_statement" : "This cog does not store end user data.",
"$schema": "https://raw.githubusercontent.com/Cog-Creators/Red-DiscordBot/refs/heads/V3/develop/schema/red_cog_repo.schema.json",
"author": [
"cswimr"
],
"install_msg": "Thank you for installing Backup!\nYou can find the source code of this cog [here](https://coastalcommits.com/cswimr/SeaCogs).",
"name": "Backup",
"short": "A utility to make reinstalling repositories and cogs after migrating the bot far easier.",
"description": "A utility to make reinstalling repositories and cogs after migrating the bot far easier.",
"end_user_data_statement": "This cog does not store end user data.",
"hidden": false,
"disabled": false,
"min_bot_version": "3.5.6",
"max_bot_version": "3.5.13",
"min_python_version": [3, 9, 0],
"max_bot_version": "3.5.17",
"min_python_version": [
3,
9,
0
],
"tags": [
"utility",
"backup",

View file

@ -6,6 +6,7 @@
# |_____/ \___|\__,_|___/ \_/\_/ |_|_| |_| |_|_| |_| |_|\___|_|
import random
from asyncio import create_task
from io import BytesIO
import aiohttp
@ -26,20 +27,21 @@ class Bible(commands.Cog):
__author__ = ["[cswimr](https://www.coastalcommits.com/cswimr)"]
__git__ = "https://www.coastalcommits.com/cswimr/SeaCogs"
__version__ = "1.1.1"
__version__ = "1.1.4"
__documentation__ = "https://seacogs.coastalcommits.com/pterodactyl/"
def __init__(self, bot: Red):
super().__init__()
self.bot = bot
self.session = aiohttp.ClientSession()
self.config = Config.get_conf(
self, identifier=481923957134912, force_registration=True
)
self.config = Config.get_conf(self, identifier=481923957134912, force_registration=True)
self.logger = getLogger("red.SeaCogs.Bible")
self.config.register_global(bible="de4e12af7f28f599-02")
self.config.register_user(bible=None)
async def cog_unload(self):
create_task(self.session.close())
def format_help_for_context(self, ctx: commands.Context) -> str:
pre_processed = super().format_help_for_context(ctx) or ""
n = "\n" if "\n\n" not in pre_processed else ""
@ -51,7 +53,6 @@ class Bible(commands.Cog):
]
return "\n".join(text)
def get_icon(self, color: Colour) -> File:
"""Get the docs.api.bible favicon with a given color."""
image_path = data_manager.bundled_data_path(self) / "api.bible-logo.png"
@ -70,9 +71,7 @@ class Bible(commands.Cog):
async def translate_book_name(self, bible_id: str, book_name: str) -> str:
"""Translate a book name to a book ID."""
book_name_list = [
w.lower() if w.lower() == "of" else w.title() for w in book_name.split()
]
book_name_list = [w.lower() if w.lower() == "of" else w.title() for w in book_name.split()]
book_name = " ".join(book_name_list)
books = await self._get_books(bible_id)
for book in books:
@ -92,20 +91,20 @@ class Bible(commands.Cog):
response.status,
)
if response.status == 401:
raise bible.errors.Unauthorized()
raise bible.errors.UnauthorizedError
if response.status == 403:
raise bible.errors.BibleAccessError()
raise bible.errors.BibleAccessError
if response.status == 503:
raise bible.errors.ServiceUnavailable()
raise bible.errors.ServiceUnavailableError
return Version(
bible_id,
data["data"]["abbreviation"],
data["data"]["language"]["name"],
data["data"]["abbreviationLocal"],
data["data"]["language"]["nameLocal"],
data["data"]["description"],
data["data"]["descriptionLocal"],
data["data"]["copyright"],
bible_id=bible_id,
abbreviation=data["data"]["abbreviation"],
language=data["data"]["language"]["name"],
abbreviation_local=data["data"]["abbreviationLocal"],
language_local=data["data"]["language"]["nameLocal"],
description=data["data"]["description"],
description_local=data["data"]["descriptionLocal"],
version_copyright=data["data"]["copyright"],
)
async def _get_passage(
@ -136,16 +135,17 @@ class Bible(commands.Cog):
response.status,
)
if response.status == 400:
raise bible.errors.InexplicableError()
raise bible.errors.InexplicableError
if response.status == 401:
raise bible.errors.Unauthorized()
raise bible.errors.UnauthorizedError
if response.status == 403:
raise bible.errors.BibleAccessError()
raise bible.errors.BibleAccessError
if response.status == 404:
raise bible.errors.NotFound()
raise bible.errors.NotFoundError
if response.status == 503:
raise bible.errors.ServiceUnavailable()
raise bible.errors.ServiceUnavailableError
assert self.bot.user is not None # bot will always be logged in
fums_url = "https://fums.api.bible/f3"
fums_params = {
"t": data["meta"]["fumsToken"],
@ -177,11 +177,11 @@ class Bible(commands.Cog):
response.status,
)
if response.status == 401:
raise bible.errors.Unauthorized()
raise bible.errors.UnauthorizedError
if response.status == 403:
raise bible.errors.BibleAccessError()
raise bible.errors.BibleAccessError
if response.status == 503:
raise bible.errors.ServiceUnavailable()
raise bible.errors.ServiceUnavailableError
return data["data"]
async def _get_chapters(self, bible_id: str, book_id: str) -> dict:
@ -196,11 +196,11 @@ class Bible(commands.Cog):
response.status,
)
if response.status == 401:
raise bible.errors.Unauthorized()
raise bible.errors.UnauthorizedError
if response.status == 403:
raise bible.errors.BibleAccessError()
raise bible.errors.BibleAccessError
if response.status == 503:
raise bible.errors.ServiceUnavailable()
raise bible.errors.ServiceUnavailableError
return data["data"]
async def _get_verses(self, bible_id: str, book_id: str, chapter: int) -> dict:
@ -215,11 +215,11 @@ class Bible(commands.Cog):
response.status,
)
if response.status == 401:
raise bible.errors.Unauthorized()
raise bible.errors.UnauthorizedError
if response.status == 403:
raise bible.errors.BibleAccessError()
raise bible.errors.BibleAccessError
if response.status == 503:
raise bible.errors.ServiceUnavailable()
raise bible.errors.ServiceUnavailableError
return data["data"]
@commands.group(autohelp=True)
@ -247,41 +247,34 @@ class Bible(commands.Cog):
from_verse, to_verse = passage.replace(":", ".").split("-")
if "." not in to_verse:
to_verse = f"{from_verse.split('.')[0]}.{to_verse}"
passage = await self._get_passage(
ctx, bible_id, f"{book_id}.{from_verse}-{book_id}.{to_verse}", True
)
retrieved_passage = await self._get_passage(ctx, bible_id, f"{book_id}.{from_verse}-{book_id}.{to_verse}", True)
else:
passage = await self._get_passage(
ctx, bible_id, f"{book_id}.{passage.replace(':', '.')}", False
)
retrieved_passage = await self._get_passage(ctx, bible_id, f"{book_id}.{passage.replace(':', '.')}", False)
except (
bible.errors.BibleAccessError,
bible.errors.NotFound,
bible.errors.NotFoundError,
bible.errors.InexplicableError,
bible.errors.ServiceUnavailable,
bible.errors.Unauthorized,
bible.errors.ServiceUnavailableError,
bible.errors.UnauthorizedError,
) as e:
await ctx.send(e.message)
return
if len(passage["content"]) > 4096:
if len(retrieved_passage["content"]) > 4096:
await ctx.send("The passage is too long to send.")
return
if await ctx.embed_requested():
icon = self.get_icon(await ctx.embed_color())
embed = Embed(
title=f"{passage['reference']}",
description=passage["content"].replace("", ""),
title=f"{retrieved_passage['reference']}",
description=retrieved_passage["content"].replace("", ""),
color=await ctx.embed_color(),
)
embed.set_footer(
text=f"{ctx.prefix}bible passage - Powered by API.Bible - {version.abbreviationLocal} ({version.languageLocal}, {version.descriptionLocal})",
icon_url="attachment://icon.png"
)
embed.set_footer(text=f"{ctx.prefix}bible passage - Powered by API.Bible - {version.abbreviation_local} ({version.language_local}, {version.description_local})", icon_url="attachment://icon.png")
await ctx.send(embed=embed, file=icon)
else:
await ctx.send(f"## {passage['reference']}\n{passage['content']}")
await ctx.send(f"## {retrieved_passage['reference']}\n{retrieved_passage['content']}")
@bible.command(name="random")
async def bible_random(self, ctx: commands.Context):
@ -302,10 +295,10 @@ class Bible(commands.Cog):
passage = await self._get_passage(ctx, bible_id, verse, False)
except (
bible.errors.BibleAccessError,
bible.errors.NotFound,
bible.errors.NotFoundError,
bible.errors.InexplicableError,
bible.errors.ServiceUnavailable,
bible.errors.Unauthorized,
bible.errors.ServiceUnavailableError,
bible.errors.UnauthorizedError,
) as e:
await ctx.send(e.message)
return
@ -317,10 +310,7 @@ class Bible(commands.Cog):
description=passage["content"].replace("", ""),
color=await ctx.embed_color(),
)
embed.set_footer(
text=f"{ctx.prefix}bible random - Powered by API.Bible - {version.abbreviationLocal} ({version.languageLocal}, {version.descriptionLocal})",
icon_url="attachment://icon.png"
)
embed.set_footer(text=f"{ctx.prefix}bible random - Powered by API.Bible - {version.abbreviation_local} ({version.language_local}, {version.description_local})", icon_url="attachment://icon.png")
await ctx.send(embed=embed, file=icon)
else:
await ctx.send(f"## {passage['reference']}\n{passage['content']}")

View file

@ -4,26 +4,22 @@ from redbot.core.utils.chat_formatting import error
class BibleAccessError(Exception):
def __init__(
self,
message: str = error(
"The provided API key cannot retrieve sections from the configured Bible. Please report this to the bot owner."
),
message: str = error("The provided API key cannot retrieve sections from the configured Bible. Please report this to the bot owner."),
):
super().__init__(message)
self.message = message
class Unauthorized(Exception):
class UnauthorizedError(Exception):
def __init__(
self,
message: str = error(
"The API key for API.Bible is missing or invalid. Please report this to the bot owner.\nIf you are the bot owner, please check the documentation [here](<https://seacogs.coastalcommits.com/bible/#setup>)."
),
message: str = error("The API key for API.Bible is missing or invalid. Please report this to the bot owner.\nIf you are the bot owner, please check the documentation [here](<https://seacogs.coastalcommits.com/bible/#setup>)."),
):
super().__init__(message)
self.message = message
class NotFound(Exception):
class NotFoundError(Exception):
def __init__(
self,
message: str = error("The requested passage was not found."),
@ -32,7 +28,7 @@ class NotFound(Exception):
self.message = message
class ServiceUnavailable(Exception):
class ServiceUnavailableError(Exception):
def __init__(
self,
message: str = error("The API.Bible service is currently unavailable."),
@ -44,9 +40,7 @@ class ServiceUnavailable(Exception):
class InexplicableError(Exception):
def __init__(
self,
message: str = error(
"An inexplicable 'Bad Request' error occurred. This error happens occassionally with the API.Bible service. Please try again. If the error persists, please report this to the bot owner."
),
message: str = error("An inexplicable 'Bad Request' error occurred. This error happens occasionally with the API.Bible service. Please try again. If the error persists, please report this to the bot owner."),
):
super().__init__(message)
self.message = message

View file

@ -1,18 +1,15 @@
{
"author" : ["cswimr"],
"install_msg" : "Thank you for installing Bible!\nThis cog requires setting an API key for API.Bible. Please read the [documentation](https://seacogs.coastalcommits.com/bible/#setup) for more information.\nYou can find the source code of this cog [here](https://coastalcommits.com/cswimr/SeaCogs).",
"name" : "Bible",
"short" : "Retrieve Bible verses from API.Bible.",
"description" : "Retrieve Bible verses from the API.Bible API. This cog requires an API.Bible api key.",
"end_user_data_statement" : "This cog does not store end user data, however it does send the following data to the API.Bible API:\n- The bot user's ID\n- The timestamp of the invoking message\n- The hashed user id of the invoking user",
"$schema": "https://raw.githubusercontent.com/Cog-Creators/Red-DiscordBot/refs/heads/V3/develop/schema/red_cog_repo.schema.json",
"author": ["cswimr"],
"install_msg": "Thank you for installing Bible!\nThis cog requires setting an API key for API.Bible. Please read the [documentation](https://seacogs.coastalcommits.com/bible/#setup) for more information.\nYou can find the source code of this cog [here](https://coastalcommits.com/cswimr/SeaCogs).",
"name": "Bible",
"short": "Retrieve Bible verses from API.Bible.",
"description": "Retrieve Bible verses from the API.Bible API. This cog requires an API.Bible api key.",
"end_user_data_statement": "This cog does not store end user data, however it does send the following data to the API.Bible API:\n- The bot user's ID\n- The timestamp of the invoking message\n- The hashed user id of the invoking user",
"hidden": false,
"disabled": false,
"min_bot_version": "3.5.0",
"min_python_version": [3, 10, 0],
"requirements": ["numpy", "pillow"],
"tags": [
"fun",
"utility",
"api"
]
"tags": ["fun", "utility", "api"]
}

View file

@ -4,23 +4,23 @@ class Version:
bible_id,
abbreviation,
language,
abbreviationLocal,
languageLocal,
abbreviation_local,
language_local,
description,
descriptionLocal,
description_local,
version_copyright,
):
self.bible_id = bible_id
self.abbreviation = abbreviation
self.language = language
self.abbreviationLocal = abbreviationLocal
self.languageLocal = languageLocal
self.abbreviation_local = abbreviation_local
self.language_local = language_local
self.description = description
self.descriptionLocal = descriptionLocal
self.description_local = description_local
self.copyright = version_copyright
def __str__(self):
return self.abbreviationLocal
return self.abbreviation_local
def __repr__(self):
return f'bible.models.Version("{self.bible_id}", "{self.abbreviation}", "{self.language}", "{self.abbreviationLocal}", "{self.languageLocal}", "{self.description}", "{self.descriptionLocal}", "{self.copyright}")'
return f'bible.models.Version("{self.bible_id}", "{self.abbreviation}", "{self.language}", "{self.abbreviation_local}", "{self.language_local}", "{self.description}", "{self.description_local}", "{self.copyright}")'

View file

@ -16,13 +16,13 @@ class EmojiInfo(commands.Cog):
__author__ = ["[cswimr](https://www.coastalcommits.com/cswimr)"]
__git__ = "https://www.coastalcommits.com/cswimr/SeaCogs"
__version__ = "1.0.1"
__version__ = "1.0.3"
__documentation__ = "https://seacogs.coastalcommits.com/emojiinfo/"
def __init__(self, bot: Red) -> None:
super().__init__()
self.bot: Red = bot
self.logger: RedTraceLogger = getLogger(name="red.SeaCogs.Emoji")
self.logger: RedTraceLogger = getLogger(name="red.SeaCogs.EmojiInfo")
def format_help_for_context(self, ctx: commands.Context) -> str:
pre_processed = super().format_help_for_context(ctx) or ""
@ -35,14 +35,12 @@ class EmojiInfo(commands.Cog):
]
return "\n".join(text)
async def fetch_twemoji(self, unicode_emoji) -> str:
base_url = "https://cdn.jsdelivr.net/gh/jdecked/twemoji@latest/assets/72x72/"
emoji_codepoint = "-".join([hex(ord(char))[2:] for char in unicode_emoji])
segments = emoji_codepoint.split("-")
valid_segments = [seg for seg in segments if len(seg) >= 4]
emoji_url = f"{base_url}{valid_segments[0]}.png"
return emoji_url
return f"{base_url}{valid_segments[0]}.png"
async def fetch_primary_color(self, emoji_url: str) -> discord.Color | None:
async with aiohttp.ClientSession() as session:
@ -51,8 +49,7 @@ class EmojiInfo(commands.Cog):
return None
image = await response.read()
dominant_color = ColorThief(io.BytesIO(image)).get_color(quality=1)
color = discord.Color.from_rgb(*dominant_color)
return color
return discord.Color.from_rgb(*dominant_color)
async def get_emoji_info(self, emoji: PartialEmoji) -> tuple[str, str]:
if emoji.is_unicode_emoji():
@ -72,59 +69,51 @@ class EmojiInfo(commands.Cog):
else:
emoji_id = ""
markdown = f"`{emoji}`"
name = f"{bold('Name:')} {emoji.aliases.pop(0)}\n"
name = f"{bold('Name:')} {emoji.aliases.pop(0) if emoji.aliases else emoji.name}\n"
aliases = f"{bold('Aliases:')} {', '.join(emoji.aliases)}\n" if emoji.aliases else ""
group = f"{bold('Group:')} {emoji.group}\n"
return (
f"{name}"
f"{emoji_id}"
f"{bold('Native:')} {emoji.is_unicode_emoji()}\n"
f"{group}"
f"{aliases}"
f"{bold('Animated:')} {emoji.animated}\n"
f"{bold('Markdown:')} {markdown}\n"
f"{bold('URL:')} [Click Here]({emoji_url})"
), emoji_url
return (f"{name}{emoji_id}{bold('Native:')} {emoji.is_unicode_emoji()}\n{group}{aliases}{bold('Animated:')} {emoji.animated}\n{bold('Markdown:')} {markdown}\n{bold('URL:')} [Click Here]({emoji_url})"), emoji_url
@app_commands.command(name="emoji")
@app_commands.describe(
emoji="What emoji would you like to get information on?",
ephemeral="Would you like the response to be hidden?"
)
@app_commands.describe(emoji="What emoji would you like to get information on?", ephemeral="Would you like the response to be hidden?")
async def emoji_slash(self, interaction: discord.Interaction, emoji: str, ephemeral: bool = True) -> None:
"""Retrieve information about an emoji."""
await interaction.response.defer(ephemeral=ephemeral)
try:
emoji: PartialEmoji = PartialEmoji.from_str(self, value=emoji)
string, emoji_url, = await self.get_emoji_info(emoji)
retrieved_emoji: PartialEmoji = PartialEmoji.from_str(self, value=emoji)
string, emoji_url = await self.get_emoji_info(retrieved_emoji)
self.logger.verbose(f"Emoji:\n{string}")
except (IndexError, UnboundLocalError):
return await interaction.followup.send("Please provide a valid emoji!")
assert isinstance(interaction.channel, discord.TextChannel)
if await self.bot.embed_requested(channel=interaction.channel):
embed = embed = discord.Embed(title="Emoji Information", description=string, color = await self.fetch_primary_color(emoji_url) or await self.bot.get_embed_color(interaction.channel))
embed = discord.Embed(title="Emoji Information", description=string, color=await self.fetch_primary_color(emoji_url) or await self.bot.get_embed_color(interaction.channel))
embed.set_thumbnail(url=emoji_url)
await interaction.followup.send(embed=embed)
else:
return None
await interaction.followup.send(content=string)
return None
@commands.command(name="emoji")
async def emoji(self, ctx: commands.Context, *, emoji: str) -> None:
"""Retrieve information about an emoji."""
try:
emoji: PartialEmoji = PartialEmoji.from_str(self, value=emoji)
string, emoji_url, = await self.get_emoji_info(emoji)
retrieved_emoji: PartialEmoji = PartialEmoji.from_str(self, value=emoji)
string, emoji_url = await self.get_emoji_info(retrieved_emoji)
self.logger.verbose(f"Emoji:\n{string}")
except (IndexError, UnboundLocalError):
return await ctx.send("Please provide a valid emoji!")
await ctx.send("Please provide a valid emoji!")
return
if await ctx.embed_requested():
embed = embed = discord.Embed(title="Emoji Information", description=string, color = await self.fetch_primary_color(emoji_url) or await ctx.embed_color)
embed = discord.Embed(title="Emoji Information", description=string, color=await self.fetch_primary_color(emoji_url) or await ctx.embed_color())
embed.set_thumbnail(url=emoji_url)
await ctx.send(embed=embed)
else:
return
await ctx.send(content=string)
return

View file

@ -1,16 +1,15 @@
{
"author" : ["cswimr"],
"install_msg" : "Thank you for installing Emoji!",
"name" : "Emoji",
"short" : "Retrieve information about emojis.",
"description" : "Retrieve information about emojis.",
"end_user_data_statement" : "This cog does not store end user data.",
"$schema": "https://raw.githubusercontent.com/Cog-Creators/Red-DiscordBot/refs/heads/V3/develop/schema/red_cog_repo.schema.json",
"author": ["cswimr"],
"install_msg": "Thank you for installing Emoji!",
"name": "Emoji",
"short": "Retrieve information about emojis.",
"description": "Retrieve information about emojis.",
"end_user_data_statement": "This cog does not store end user data.",
"hidden": false,
"disabled": false,
"min_bot_version": "3.5.0",
"min_python_version": [3, 10, 0],
"requirements": ["colorthief"],
"tags": [
"utility"
]
"tags": ["utility"]
}

View file

@ -39,7 +39,7 @@ class PartialEmoji(discord.PartialEmoji):
The group name of the emoji if it is a native emoji.
"""
def __init__(self, *, name: str, animated: bool = False, id: int | None = None, group: str | None = None, aliases: list | None = None) -> None: # pylint: disable=redefined-builtin
def __init__(self, *, name: str, animated: bool = False, id: int | None = None, group: str | None = None, aliases: list | None = None) -> None: # pylint: disable=redefined-builtin # noqa: A002
super().__init__(name=name, animated=animated, id=id)
self.group = group
self.aliases = aliases
@ -72,12 +72,12 @@ class PartialEmoji(discord.PartialEmoji):
match = cls._CUSTOM_EMOJI_RE.match(value)
if match is not None:
groups = match.groupdict()
animated = bool(groups['animated'])
emoji_id = int(groups['id'])
name = groups['name']
animated = bool(groups["animated"])
emoji_id = int(groups["id"])
name = groups["name"]
return cls(name=name, animated=animated, id=emoji_id)
path: data_manager.Path = data_manager.bundled_data_path(coginstance) / "emojis.json"
path = data_manager.bundled_data_path(coginstance) / "emojis.json"
with open(path, "r", encoding="UTF-8") as file:
emojis: dict = json.load(file)
emoji_aliases = []

25
flake.lock generated Normal file
View file

@ -0,0 +1,25 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1738680400,
"narHash": "sha256-ooLh+XW8jfa+91F1nhf9OF7qhuA/y1ChLx6lXDNeY5U=",
"rev": "799ba5bffed04ced7067a91798353d360788b30d",
"revCount": 747653,
"type": "tarball",
"url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.747653%2Brev-799ba5bffed04ced7067a91798353d360788b30d/0194d302-29da-7009-8f43-5b8a58825954/source.tar.gz"
},
"original": {
"type": "tarball",
"url": "https://flakehub.com/f/NixOS/nixpkgs/0.1.%2A.tar.gz"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

72
flake.nix Normal file
View file

@ -0,0 +1,72 @@
{
description = "SeaCogs Nix Flake";
inputs.nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0.1.*.tar.gz";
outputs =
{ self, nixpkgs }:
let
supportedSystems = [
"x86_64-linux"
"aarch64-linux"
"x86_64-darwin"
"aarch64-darwin"
];
forEachSupportedSystem =
f:
nixpkgs.lib.genAttrs supportedSystems (
system:
f {
pkgs = import nixpkgs { inherit system; };
lib = nixpkgs.lib;
}
);
in
{
devShells = forEachSupportedSystem (
{ pkgs, lib }:
let
myPython = pkgs.python311;
lib-path =
with pkgs;
lib.makeLibraryPath [
stdenv.cc.cc
# Red-DiscordBot dependencies
libffi
libsodium
# PyLav dependency
libaio
# Material for MkDocs dependency
cairo
];
in
{
default = pkgs.mkShell {
lib-path = lib-path;
packages = with pkgs; [
myPython
uv
ruff # the ruff pip package installs a dynamically linked binary that cannot run on NixOS
forgejo-runner
# Red-DiscordBot dependencies
git
jdk17
# Material for MkDocs dependencies
pngquant
# SeaCogs dependencies
dig
];
shellHook = # bash
''
export "LD_LIBRARY_PATH=$LD_LIBRARY_PATH:${lib-path}"
export "UV_PYTHON_PREFERENCE=only-system"
export "UV_PYTHON_DOWNLOADS=never"
uv sync --all-groups
source ./.venv/bin/activate
export "PYTHONPATH=`pwd`/.venv/${myPython.sitePackages}/:$PYTHONPATH"
export "PATH=${pkgs.ruff}/bin:$PATH"
'';
};
}
);
};
}

5
hotreload/__init__.py Normal file
View file

@ -0,0 +1,5 @@
from .hotreload import HotReload
async def setup(bot):
await bot.add_cog(HotReload(bot))

192
hotreload/hotreload.py Normal file
View file

@ -0,0 +1,192 @@
import py_compile
from asyncio import run_coroutine_threadsafe
from pathlib import Path
from tempfile import NamedTemporaryFile
from typing import Generator, List, Sequence
import discord
from red_commons.logging import RedTraceLogger, getLogger
from redbot.core import Config, checks, commands
from redbot.core.bot import Red
from redbot.core.core_commands import CoreLogic
from redbot.core.utils.chat_formatting import bold, box, humanize_list
from typing_extensions import override
from watchdog.events import FileSystemEvent, FileSystemMovedEvent, RegexMatchingEventHandler
from watchdog.observers import Observer
from watchdog.observers.api import BaseObserver
class HotReload(commands.Cog):
"""Automatically reload cogs in local cog paths on file change."""
__author__ = ["[cswimr](https://www.coastalcommits.com/cswimr)"]
__git__ = "https://www.coastalcommits.com/cswimr/SeaCogs"
__version__ = "1.4.2"
__documentation__ = "https://seacogs.coastalcommits.com/hotreload/"
def __init__(self, bot: Red) -> None:
super().__init__()
self.bot: Red = bot
self.config: Config = Config.get_conf(cog_instance=self, identifier=294518358420750336, force_registration=True)
self.logger: RedTraceLogger = getLogger(name="red.SeaCogs.HotReload")
self.observers: List[BaseObserver] = []
self.config.register_global(notify_channel=None, compile_before_reload=False)
watchdog_loggers = [getLogger(name="watchdog.observers.inotify_buffer")]
for watchdog_logger in watchdog_loggers:
watchdog_logger.setLevel("INFO") # SHUT UP!!!!
@override
async def cog_load(self) -> None:
"""Start the observer when the cog is loaded."""
_ = self.bot.loop.create_task(self.start_observer())
@override
async def cog_unload(self) -> None:
"""Stop the observer when the cog is unloaded."""
for observer in self.observers:
observer.stop()
observer.join()
self.logger.info("Stopped observer. No longer watching for file changes.")
@override
def format_help_for_context(self, ctx: commands.Context) -> str:
pre_processed = super().format_help_for_context(ctx) or ""
n = "\n" if "\n\n" not in pre_processed else ""
text = [
f"{pre_processed}{n}",
f"{bold('Cog Version:')} [{self.__version__}]({self.__git__})",
f"{bold('Author:')} {humanize_list(self.__author__)}",
f"{bold('Documentation:')} {self.__documentation__}",
]
return "\n".join(text)
# pylint: disable=protected-access
async def get_paths(self) -> Generator[Path, None, None]:
"""Retrieve user defined paths."""
cog_manager = self.bot._cog_mgr # noqa: SLF001 # We have to use this private method because there is no public API to get user defined paths
cog_paths = await cog_manager.user_defined_paths()
return (Path(path) for path in cog_paths)
async def start_observer(self) -> None:
"""Start the observer to watch for file changes."""
self.observers.append(Observer())
paths = await self.get_paths()
is_first = True
for observer in self.observers:
if not is_first:
observer.stop()
observer.join()
self.logger.debug("Stopped hanging observer.")
continue
for path in paths:
if not path.exists():
self.logger.warning("Path %s does not exist. Skipping.", path)
continue
self.logger.debug("Adding observer schedule for path %s.", path)
observer.schedule(event_handler=HotReloadHandler(cog=self, path=path), path=str(path), recursive=True)
observer.start()
self.logger.info("Started observer. Watching for file changes.")
is_first = False
@checks.is_owner()
@commands.group(name="hotreload")
async def hotreload_group(self, ctx: commands.Context) -> None:
"""HotReload configuration commands."""
@hotreload_group.command(name="notifychannel")
async def hotreload_notifychannel(self, ctx: commands.Context, channel: discord.TextChannel) -> None:
"""Set the channel to send notifications to."""
await self.config.notify_channel.set(channel.id)
await ctx.send(f"Notifications will be sent to {channel.mention}.")
@hotreload_group.command(name="compile") # type: ignore
async def hotreload_compile(self, ctx: commands.Context, compile_before_reload: bool) -> None:
"""Set whether to compile modified files before reloading."""
await self.config.compile_before_reload.set(compile_before_reload)
await ctx.send(f"I {'will' if compile_before_reload else 'will not'} compile modified files before hotreloading cogs.")
@hotreload_group.command(name="list") # type: ignore
async def hotreload_list(self, ctx: commands.Context) -> None:
"""List the currently active observers."""
if not self.observers:
await ctx.send("No observers are currently active.")
return
await ctx.send(f"Currently active observers (If there are more than one of these, report an issue): {box(humanize_list([str(o) for o in self.observers], style='unit'))}")
class HotReloadHandler(RegexMatchingEventHandler):
"""Handler for file changes."""
def __init__(self, cog: HotReload, path: Path) -> None:
super().__init__(regexes=[r".*\.py$"])
self.cog: HotReload = cog
self.path: Path = path
self.logger: RedTraceLogger = getLogger(name="red.SeaCogs.HotReload.Observer")
def on_any_event(self, event: FileSystemEvent) -> None:
"""Handle filesystem events."""
if event.is_directory:
return
allowed_events = ("moved", "deleted", "created", "modified")
if event.event_type not in allowed_events:
return
relative_src_path = Path(str(event.src_path)).relative_to(self.path)
src_package_name = relative_src_path.parts[0]
cogs_to_reload = [src_package_name]
if isinstance(event, FileSystemMovedEvent):
dest = f" to {event.dest_path}"
relative_dest_path = Path(str(event.dest_path)).relative_to(self.path)
dest_package_name = relative_dest_path.parts[0]
if dest_package_name != src_package_name:
cogs_to_reload.append(dest_package_name)
else:
dest = ""
self.logger.info("File %s has been %s%s.", event.src_path, event.event_type, dest)
run_coroutine_threadsafe(
coro=self.reload_cogs(
cog_names=cogs_to_reload,
paths=[Path(str(p)) for p in (event.src_path, getattr(event, "dest_path", None)) if p],
),
loop=self.cog.bot.loop,
)
# pylint: disable=protected-access
async def reload_cogs(self, cog_names: Sequence[str], paths: Sequence[Path]) -> None:
"""Reload modified cogs."""
if not self.compile_modified_files(cog_names, paths):
return
core_logic = CoreLogic(bot=self.cog.bot)
self.logger.info("Reloading cogs: %s", humanize_list(cog_names, style="unit"))
await core_logic._reload(pkg_names=cog_names) # noqa: SLF001 # We have to use this private method because there is no public API to reload other cogs
self.logger.info("Reloaded cogs: %s", humanize_list(cog_names, style="unit"))
channel = self.cog.bot.get_channel(await self.cog.config.notify_channel())
if channel and isinstance(channel, discord.TextChannel):
await channel.send(f"Reloaded cogs: {humanize_list(cog_names, style='unit')}")
def compile_modified_files(self, cog_names: Sequence[str], paths: Sequence[Path]) -> bool:
"""Compile modified files to ensure they are valid Python files."""
for path in paths:
if not path.exists() or path.suffix != ".py":
self.logger.debug("Path %s does not exist or does not point to a Python file. Skipping compilation step.", path)
continue
try:
with NamedTemporaryFile() as temp_file:
self.logger.debug("Attempting to compile %s", path)
py_compile.compile(file=str(path), cfile=temp_file.name, doraise=True)
self.logger.debug("Successfully compiled %s", path)
except py_compile.PyCompileError as e:
e.__suppress_context__ = True
self.logger.exception("%s failed to compile. Not reloading cogs %s.", path, humanize_list(cog_names, style="unit"))
return False
except OSError:
self.logger.exception("Failed to create tempfile for compilation step. Skipping.")
return True

15
hotreload/info.json Normal file
View file

@ -0,0 +1,15 @@
{
"$schema": "https://raw.githubusercontent.com/Cog-Creators/Red-DiscordBot/refs/heads/V3/develop/schema/red_cog_repo.schema.json",
"author": ["cswimr"],
"install_msg": "Thank you for installing HotReload! Please see the [documentation](https://seacogs.coastalcommits.com/hotreload) to get started.",
"name": "HotReload",
"short": "Automatically reload cogs in local cog paths on file change.",
"description": "Automatically reload cogs in local cog paths on file change.",
"end_user_data_statement": "This cog does not store end user data.",
"hidden": false,
"disabled": false,
"min_bot_version": "3.5.0",
"min_python_version": [3, 8, 0],
"requirements": ["watchdog"],
"tags": ["utility", "development"]
}

View file

@ -1,7 +1,6 @@
{
"author": [
"cswimr"
],
"$schema": "https://raw.githubusercontent.com/Cog-Creators/Red-DiscordBot/V3/develop/schema/red_cog_repo.schema.json",
"author": ["cswimr"],
"install_msg": "Thanks for installing my repo!\n\nIf you have any issues with any of the cogs, please create an issue [here](https://coastalcommits.com/cswimr/SeaCogs/issues) or join my [Discord Server](https://discord.gg/eMUMe77Yb8 ).",
"index_name": "sea-cogs",
"short": "Various cogs for Red, by cswimr",

View file

@ -1,8 +1,8 @@
site_name: SeaCogs Documentation
site_url: !ENV [SITE_URL, 'https://seacogs.coastalcommits.com']
site_url: !ENV [SITE_URL, "https://seacogs.coastalcommits.com"]
repo_name: CoastalCommits
repo_url: https://coastalcommits.com/cswimr/SeaCogs
edit_uri: !ENV [EDIT_URI, 'src/branch/main/.docs']
edit_uri: !ENV [EDIT_URI, "src/branch/main/.docs"]
copyright: Copyright &copy; 2023-2024, cswimr
docs_dir: .docs
@ -19,6 +19,7 @@ nav:
- Bible: bible.md
- Backup: backup.md
- EmojiInfo: emojiinfo.md
- HotReload: hotreload.md
- Nerdify: nerdify.md
- Pterodactyl:
- pterodactyl/index.md
@ -72,7 +73,7 @@ markdown_extensions:
theme:
name: material
palette:
- media: '(prefers-color-scheme: light)'
- media: "(prefers-color-scheme: light)"
scheme: default
primary: white
accent: light blue
@ -80,7 +81,7 @@ theme:
icon: material/toggle-switch
name: Switch to dark mode
- media: '(prefers-color-scheme: dark)'
- media: "(prefers-color-scheme: dark)"
scheme: slate
primary: black
accent: light blue

View file

@ -1,17 +1,14 @@
{
"author" : ["cswimr"],
"install_msg" : "Thank you for installing Nerdify!\nYou can find the source code of this cog [here](https://coastalcommits.com/cswimr/SeaCogs). Based off of PhasecoreX's [UwU](<https://github.com/PhasecoreX/PCXCogs/tree/master/uwu>) cog.",
"name" : "Nerdify",
"short" : "Nerdify your text!",
"description" : "Nerdify your text!",
"end_user_data_statement" : "This cog does not store end user data.",
"$schema": "https://raw.githubusercontent.com/Cog-Creators/Red-DiscordBot/refs/heads/V3/develop/schema/red_cog_repo.schema.json",
"author": ["cswimr"],
"install_msg": "Thank you for installing Nerdify!\nYou can find the source code of this cog [here](https://coastalcommits.com/cswimr/SeaCogs). Based off of PhasecoreX's [UwU](<https://github.com/PhasecoreX/PCXCogs/tree/master/uwu>) cog.",
"name": "Nerdify",
"short": "Nerdify your text!",
"description": "Nerdify your text!",
"end_user_data_statement": "This cog does not store end user data.",
"hidden": false,
"disabled": false,
"min_bot_version": "3.5.0",
"min_python_version": [3, 8, 0],
"tags": [
"fun",
"text",
"meme"
]
"tags": ["fun", "text", "meme"]
}

View file

@ -37,16 +37,20 @@ class Nerdify(commands.Cog):
]
return "\n".join(text)
@commands.command(aliases=["nerd"])
async def nerdify(
self, ctx: commands.Context, *, text: Optional[str] = None
self,
ctx: commands.Context,
*,
text: Optional[str] = None,
) -> None:
"""Nerdify the replied to message, previous message, or your own text."""
if not text:
if hasattr(ctx.message, "reference") and ctx.message.reference:
with suppress(
discord.Forbidden, discord.NotFound, discord.HTTPException
discord.Forbidden,
discord.NotFound,
discord.HTTPException,
):
message_id = ctx.message.reference.message_id
if message_id:
@ -62,7 +66,9 @@ class Nerdify(commands.Cog):
ctx.channel,
self.nerdify_text(text),
allowed_mentions=discord.AllowedMentions(
everyone=False, users=False, roles=False
everyone=False,
users=False,
roles=False,
),
)
@ -77,7 +83,10 @@ class Nerdify(commands.Cog):
return f'"{text}" 🤓'
async def type_message(
self, destination: discord.abc.Messageable, content: str, **kwargs: Any
self,
destination: discord.abc.Messageable,
content: str,
**kwargs: Any,
) -> Union[discord.Message, None]:
"""Simulate typing and sending a message to a destination.

View file

@ -2,28 +2,29 @@ from redbot.core import Config
config: Config = Config.get_conf(None, identifier=457581387213637448123567, cog_name="Pterodactyl", force_registration=True)
def register_config(config_obj: Config) -> None:
config_obj.register_global(
base_url=None,
server_id=None,
console_channel=None,
console_commands_enabled=False,
current_status='',
current_status="",
chat_regex=r"^\[\d{2}:\d{2}:\d{2}\sINFO\]: (?!\[(?:Server|Rcon)\])(?:<|\[)(\w+)(?:>|\]) (.*)",
server_regex=r"^\[\d{2}:\d{2}:\d{2} INFO\]:(?: \[Not Secure\])? \[(?:Server|Rcon)\] (.*)",
join_regex=r"^\[\d{2}:\d{2}:\d{2} INFO\]: ([^<\n]+) joined the game$",
leave_regex=r"^\[\d{2}:\d{2}:\d{2} INFO\]: ([^<\n]+) left the game$",
achievement_regex=r"^\[\d{2}:\d{2}:\d{2} INFO\]: (.*) has (made the advancement|completed the challenge) \[(.*)\]$",
chat_command='tellraw @a ["",{"text":".$N ","color":".$C","insertion":"<@.$I>","hoverEvent":{"action":"show_text","contents":"Shift click to mention this user inside Discord"}},{"text":"(DISCORD):","color":"blue","clickEvent":{"action":"open_url","value":".$V"},"hoverEvent":{"action":"show_text","contents":"Click to join the Discord Server"}},{"text":" .$M","color":"white"}]', # noqa: E501
topic='Server IP: .$H\nServer Players: .$P/.$M',
topic="Server IP: .$H\nServer Players: .$P/.$M",
topic_hostname=None,
topic_port=25565,
api_endpoint="minecraft",
chat_channel=None,
startup_msg='Server started!',
shutdown_msg='Server stopped!',
join_msg='Welcome to the server! 👋',
leave_msg='Goodbye! 👋',
startup_msg="Server started!",
shutdown_msg="Server stopped!",
join_msg="Welcome to the server! 👋",
leave_msg="Goodbye! 👋",
mask_ip=True,
invite=None,
regex_blacklist={},

View file

@ -1,19 +1,18 @@
{
"author" : ["cswimr"],
"install_msg" : "Thank you for installing Pterodactyl!\nYou can find the source code of this cog [here](https://coastalcommits.com/cswimr/SeaCogs).\nDocumentation can be found [here](https://seacogs.coastalcommits.com/pterodactyl ).",
"name" : "Pterodactyl",
"short" : "Interface with Pterodactyl through websockets.",
"description" : "Interface with Pterodactyl through websockets.",
"end_user_data_statement" : "This cog does not store end user data.",
"$schema": "https://raw.githubusercontent.com/Cog-Creators/Red-DiscordBot/refs/heads/V3/develop/schema/red_cog_repo.schema.json",
"author": ["cswimr"],
"install_msg": "Thank you for installing Pterodactyl!\nYou can find the source code of this cog [here](https://coastalcommits.com/cswimr/SeaCogs).\nDocumentation can be found [here](https://seacogs.coastalcommits.com/pterodactyl ).",
"name": "Pterodactyl",
"short": "Interface with Pterodactyl through websockets.",
"description": "Interface with Pterodactyl through websockets.",
"end_user_data_statement": "This cog does not store end user data.",
"hidden": false,
"disabled": false,
"min_bot_version": "3.5.0",
"min_python_version": [3, 8, 0],
"requirements": ["git+https://github.com/cswimr/pydactyl", "websockets"],
"tags": [
"pterodactyl",
"minecraft",
"server",
"management"
]
"min_python_version": [3, 10, 0],
"requirements": [
"git+https://github.com/iamkubi/pydactyl@v2.0.5",
"websockets"
],
"tags": ["pterodactyl", "minecraft", "server", "management"]
}

View file

@ -1,8 +1,8 @@
from red_commons import logging
from red_commons.logging import getLogger
logger = getLogger('red.SeaCogs.Pterodactyl')
websocket_logger = getLogger('red.SeaCogs.Pterodactyl.websocket')
logger = getLogger("red.SeaCogs.Pterodactyl")
websocket_logger = getLogger("red.SeaCogs.Pterodactyl.Websocket")
if logger.level >= logging.VERBOSE:
websocket_logger.setLevel(logging.logging.INFO)
elif logger.level < logging.VERBOSE:

View file

@ -2,9 +2,17 @@ import aiohttp
async def get_status(host: str, port: int = 25565) -> tuple[bool, dict]:
"""Get the status of a Minecraft server using the [mcsrvstat.us API](https://api.mcsrvstat.us).
Args:
host (str): The host of the server.
port (int, optional): The port to connect to. Defaults to 25565.
Returns:
A tuple containing a boolean and a dictionary. The boolean is True if the server is online, or False if it is offline. The dictionary contains the response from the API."""
async with aiohttp.ClientSession() as session:
async with session.get(f'https://api.mcsrvstat.us/2/{host}:{port}') as response:
response = await response.json()
if response['online']:
async with session.get(f"https://api.mcsrvstat.us/2/{host}:{port}") as response:
response = await response.json() # noqa: PLW2901
if response["online"]:
return (True, response)
return (False, response)

View file

@ -1,6 +1,6 @@
import asyncio
import json
from typing import Mapping, Optional, Tuple, Union
from typing import AsyncIterable, Iterable, Mapping, Optional, Tuple, Union
import discord
import websockets
@ -9,8 +9,9 @@ from pydactyl import PterodactylClient
from redbot.core import app_commands, commands
from redbot.core.app_commands import Choice
from redbot.core.bot import Red
from redbot.core.utils.chat_formatting import bold, box, error, humanize_list
from redbot.core.utils.chat_formatting import bold, box, humanize_list
from redbot.core.utils.views import ConfirmView
from typing_extensions import override
from pterodactyl import mcsrvstatus
from pterodactyl.config import config, register_config
@ -22,19 +23,20 @@ class Pterodactyl(commands.Cog):
__author__ = ["[cswimr](https://www.coastalcommits.com/cswimr)"]
__git__ = "https://www.coastalcommits.com/cswimr/SeaCogs"
__version__ = "2.0.4"
__version__ = "2.0.9"
__documentation__ = "https://seacogs.coastalcommits.com/pterodactyl/"
def __init__(self, bot: Red):
self.bot = bot
self.client: Optional[PterodactylClient] = None
self.task: Optional[asyncio.Task] = None
self.websocket: Optional[websockets.WebSocketClientProtocol] = None
self.websocket: Optional[websockets.ClientConnection] = None
self.retry_counter: int = 0
register_config(config)
self.task = self.get_task()
self.task = self._get_task()
self.update_topic.start()
@override
def format_help_for_context(self, ctx: commands.Context) -> str:
pre_processed = super().format_help_for_context(ctx) or ""
n = "\n" if "\n\n" not in pre_processed else ""
@ -46,47 +48,57 @@ class Pterodactyl(commands.Cog):
]
return "\n".join(text)
@override
async def cog_load(self) -> None:
pterodactyl_keys = await self.bot.get_shared_api_tokens("pterodactyl")
api_key = pterodactyl_keys.get("api_key")
if api_key is None:
self.task.cancel()
raise ValueError("Pterodactyl API key not set. Please set it using `[p]set api`.")
self.maybe_cancel_task()
logger.error("Pterodactyl API key not set. Please set it using `[p]set api`.")
return
base_url = await config.base_url()
if base_url is None:
self.task.cancel()
raise ValueError("Pterodactyl base URL not set. Please set it using `[p]pterodactyl config url`.")
self.maybe_cancel_task()
logger.error("Pterodactyl base URL not set. Please set it using `[p]pterodactyl config url`.")
return
server_id = await config.server_id()
if server_id is None:
self.task.cancel()
raise ValueError("Pterodactyl server ID not set. Please set it using `[p]pterodactyl config serverid`.")
self.maybe_cancel_task()
logger.error("Pterodactyl server ID not set. Please set it using `[p]pterodactyl config serverid`.")
return
self.client = PterodactylClient(base_url, api_key).client
@override
async def cog_unload(self) -> None:
self.update_topic.cancel()
self.task.cancel()
self.retry_counter = 0
await self.client._session.close() # pylint: disable=protected-access
self.maybe_cancel_task()
def get_task(self) -> asyncio.Task:
def maybe_cancel_task(self, reset_retry_counter: bool = True) -> None:
if self.task:
self.task.cancel()
if reset_retry_counter:
self.retry_counter = 0
def _get_task(self) -> asyncio.Task:
from pterodactyl.websocket import establish_websocket_connection
task = self.bot.loop.create_task(establish_websocket_connection(self), name="Pterodactyl Websocket Connection")
task.add_done_callback(self.error_callback)
task.add_done_callback(self._error_callback)
return task
def error_callback(self, fut) -> None: #NOTE - Thanks flame442 and zephyrkul for helping me figure this out
def _error_callback(self, fut) -> None: # NOTE Thanks flame442 and zephyrkul for helping me figure this out
try:
fut.result()
except asyncio.CancelledError:
logger.info("WebSocket task has been cancelled.")
except Exception as e: # pylint: disable=broad-exception-caught
logger.error("WebSocket task has failed: %s", e, exc_info=e)
self.task.cancel()
self.maybe_cancel_task(reset_retry_counter=False)
if self.retry_counter < 5:
self.retry_counter += 1
logger.info("Retrying in %s seconds...", 5 * self.retry_counter)
self.task = self.bot.loop.call_later(5 * self.retry_counter, self.get_task)
self.task = self.bot.loop.call_later(5 * self.retry_counter, self._get_task)
else:
logger.info("Retry limit reached. Stopping task.")
@ -97,9 +109,9 @@ class Pterodactyl(commands.Cog):
console = self.bot.get_channel(await config.console_channel())
chat = self.bot.get_channel(await config.chat_channel())
if console:
await console.edit(topic=topic)
await console.edit(topic=topic) # type: ignore
if chat:
await chat.edit(topic=topic)
await chat.edit(topic=topic) # type: ignore
@commands.Cog.listener()
async def on_message_without_command(self, message: discord.Message) -> None:
@ -110,13 +122,7 @@ class Pterodactyl(commands.Cog):
return
logger.debug("Received console command from %s: %s", message.author.id, message.content)
await message.channel.send(f"Received console command from {message.author.id}: {message.content[:1900]}", allowed_mentions=discord.AllowedMentions.none())
try:
await self.websocket.send(json.dumps({"event": "send command", "args": [message.content]}))
except websockets.exceptions.ConnectionClosed as e:
logger.error("WebSocket connection closed: %s", e)
self.task.cancel()
self.retry_counter = 0
self.task = self.get_task()
await self._send(json.dumps({"event": "send command", "args": [message.content]}))
if message.channel.id == await config.chat_channel() and message.author.bot is False:
logger.debug("Received chat message from %s: %s", message.author.id, message.content)
channel = self.bot.get_channel(await config.console_channel())
@ -124,13 +130,22 @@ class Pterodactyl(commands.Cog):
await channel.send(f"Received chat message from {message.author.id}: {message.content[:1900]}", allowed_mentions=discord.AllowedMentions.none())
msg = json.dumps({"event": "send command", "args": [await self.get_chat_command(message)]})
logger.debug("Sending chat message to server:\n%s", msg)
await self._send(message=msg)
async def _send(self, message: Union[websockets.Data, Iterable[websockets.Data], AsyncIterable[websockets.Data]], text: bool = False):
"""Send a message through the websocket connection. Restarts the websocket connection task if it is closed, and reinvokes itself."""
try:
await self.websocket.send(msg)
await self.websocket.send(message=message, text=text) # type: ignore - we want this to error if `self.websocket` is none
except websockets.exceptions.ConnectionClosed as e:
logger.error("WebSocket connection closed: %s", e)
self.task.cancel()
self.retry_counter = 0
self.task = self.get_task()
self.maybe_cancel_task()
self.task = self._get_task()
try:
await asyncio.wait_for(fut=self.task, timeout=60)
await self._send(message=message, text=text)
except asyncio.TimeoutError:
logger.error("Timeout while waiting for websocket connection")
raise
async def get_topic(self) -> str:
topic: str = await config.topic()
@ -141,23 +156,27 @@ class Pterodactyl(commands.Cog):
if await config.api_endpoint() == "minecraft":
status, response = await mcsrvstatus.get_status(await config.topic_hostname(), await config.topic_port())
if status:
placeholders.update({
"I": response['ip'],
"M": str(response['players']['max']),
"P": str(response['players']['online']),
"V": response['version'],
"D": response['motd']['clean'][0] if response['motd']['clean'] else "unset",
})
placeholders.update(
{
"I": response["ip"],
"M": str(response["players"]["max"]),
"P": str(response["players"]["online"]),
"V": response["version"],
"D": response["motd"]["clean"][0] if response["motd"]["clean"] else "unset",
},
)
else:
placeholders.update({
"I": response['ip'],
placeholders.update(
{
"I": response["ip"],
"M": "0",
"P": "0",
"V": "Server Offline",
"D": "Server Offline",
})
},
)
for key, value in placeholders.items():
topic = topic.replace('.$' + key, value)
topic = topic.replace(".$" + key, value)
return topic
async def get_chat_command(self, message: discord.Message) -> str:
@ -166,42 +185,45 @@ class Pterodactyl(commands.Cog):
"C": str(message.author.color),
"D": message.author.discriminator,
"I": str(message.author.id),
"M": message.content.replace('"','').replace("\n", " "),
"M": message.content.replace('"', "").replace("\n", " "),
"N": message.author.display_name,
"U": message.author.name,
"V": await config.invite() or "use [p]pterodactyl config invite to change me",
}
for key, value in placeholders.items():
command = command.replace('.$' + key, value)
command = command.replace(".$" + key, value)
return command
async def get_player_list(self) -> Optional[Tuple[str, list]]:
if await config.api_endpoint() == "minecraft":
status, response = await mcsrvstatus.get_status(await config.topic_hostname(), await config.topic_port())
if status and 'list' in response['players']:
output_str = '\n'.join(response['players']['list'])
return output_str, response['players']['list']
if status and "list" in response["players"]:
output_str = "\n".join(response["players"]["list"])
return output_str, response["players"]["list"]
return None
return None
async def get_player_list_embed(self, ctx: Union[commands.Context, discord.Interaction]) -> Optional[discord.Embed]:
player_list = await self.get_player_list()
if player_list:
if player_list and isinstance(ctx.channel, discord.abc.Messageable):
embed = discord.Embed(color=await self.bot.get_embed_color(ctx.channel), title="Players Online")
embed.description = player_list[0]
return embed
return None
async def power(self, ctx: Union[discord.Interaction, commands.Context], action: str, action_ing: str, warning: str = '') -> None:
async def power(self, ctx: Union[discord.Interaction, commands.Context], action: str, action_ing: str, warning: str = "") -> None:
if isinstance(ctx, discord.Interaction):
ctx = await self.bot.get_context(ctx)
current_status = await config.current_status()
if current_status == action_ing:
return await ctx.send(f"Server is already {action_ing}.", ephemeral=True)
await ctx.send(f"Server is already {action_ing}.", ephemeral=True)
return
if current_status in ["starting", "stopping"] and action != "kill":
return await ctx.send("Another power action is already in progress.", ephemeral=True)
await ctx.send("Another power action is already in progress.", ephemeral=True)
return
view = ConfirmView(ctx.author, disable_buttons=True)
@ -212,12 +234,13 @@ class Pterodactyl(commands.Cog):
if view.result is True:
await message.edit(content=f"Sending websocket command to {action} server...", view=None)
await self.websocket.send(json.dumps({"event": "set state", "args": [action]}))
await self._send(json.dumps({"event": "set state", "args": [action]}))
await message.edit(content=f"Server {action_ing}", view=None)
return
else:
await message.edit(content="Cancelled.", view=None)
return
async def send_command(self, ctx: Union[discord.Interaction, commands.Context], command: str):
channel = self.bot.get_channel(await config.console_channel())
@ -225,27 +248,19 @@ class Pterodactyl(commands.Cog):
ctx = await self.bot.get_context(ctx)
if channel:
await channel.send(f"Received console command from {ctx.author.id}: {command[:1900]}", allowed_mentions=discord.AllowedMentions.none())
try:
await self.websocket.send(json.dumps({"event": "send command", "args": [command]}))
await self._send(json.dumps({"event": "send command", "args": [command]}))
await ctx.send(f"Command sent to server. {box(command, 'json')}")
except websockets.exceptions.ConnectionClosed as e:
logger.error("WebSocket connection closed: %s", e)
await ctx.send(error("WebSocket connection closed."))
self.task.cancel()
self.retry_counter = 0
self.task = self.get_task()
@commands.Cog.listener()
async def on_red_api_tokens_update(self, service_name: str, api_tokens: Mapping[str,str]): # pylint: disable=unused-argument
async def on_red_api_tokens_update(self, service_name: str, api_tokens: Mapping[str, str]): # pylint: disable=unused-argument
if service_name == "pterodactyl":
logger.info("Configuration value set: api_key\nRestarting task...")
self.task.cancel()
self.retry_counter = 0
self.task = self.get_task()
self.maybe_cancel_task(reset_retry_counter=True)
self.task = self._get_task()
slash_pterodactyl = app_commands.Group(name="pterodactyl", description="Pterodactyl allows you to manage your Pterodactyl Panel from Discord.")
@slash_pterodactyl.command(name = "command", description = "Send a command to the server console.")
@slash_pterodactyl.command(name="command", description="Send a command to the server console.")
async def slash_pterodactyl_command(self, interaction: discord.Interaction, command: str) -> None:
"""Send a command to the server console.
@ -255,7 +270,7 @@ class Pterodactyl(commands.Cog):
The command to send to the server."""
return await self.send_command(interaction, command)
@slash_pterodactyl.command(name = "players", description = "Retrieve a list of players on the server.")
@slash_pterodactyl.command(name="players", description="Retrieve a list of players on the server.")
async def slash_pterodactyl_players(self, interaction: discord.Interaction) -> None:
"""Retrieve a list of players on the server."""
e = await self.get_player_list_embed(interaction)
@ -264,13 +279,8 @@ class Pterodactyl(commands.Cog):
else:
await interaction.response.send_message("No players online.", ephemeral=True)
@slash_pterodactyl.command(name = "power", description = "Send power actions to the server.")
@app_commands.choices(action=[
Choice(name="Start", value="start"),
Choice(name="Stop", value="stop"),
Choice(name="Restart", value="restart"),
Choice(name="⚠️ Kill ⚠️", value="kill")
])
@slash_pterodactyl.command(name="power", description="Send power actions to the server.")
@app_commands.choices(action=[Choice(name="Start", value="start"), Choice(name="Stop", value="stop"), Choice(name="Restart", value="restart"), Choice(name="⚠️ Kill ⚠️", value="kill")])
async def slash_pterodactyl_power(self, interaction: discord.Interaction, action: app_commands.Choice[str]) -> None:
"""Send power actions to the server.
@ -284,11 +294,11 @@ class Pterodactyl(commands.Cog):
return await self.power(interaction, action.value, "stopping...")
return await self.power(interaction, action.value, f"{action.value}ing...")
@commands.group(autohelp = True, name = "pterodactyl", aliases = ["ptero"])
@commands.group(autohelp=True, name="pterodactyl", aliases=["ptero"])
async def pterodactyl(self, ctx: commands.Context) -> None:
"""Pterodactyl allows you to manage your Pterodactyl Panel from Discord."""
@pterodactyl.command(name = "players", aliases=["list", "online", "playerlist", "who"])
@pterodactyl.command(name="players", aliases=["list", "online", "playerlist", "who"])
async def pterodactyl_players(self, ctx: commands.Context) -> None:
"""Retrieve a list of players on the server."""
e = await self.get_player_list_embed(ctx)
@ -297,43 +307,43 @@ class Pterodactyl(commands.Cog):
else:
await ctx.send("No players online.")
@pterodactyl.command(name = "command", aliases = ["cmd", "execute", "exec"])
@pterodactyl.command(name="command", aliases=["cmd", "execute", "exec"])
@commands.admin()
async def pterodactyl_command(self, ctx: commands.Context, *, command: str) -> None:
"""Send a command to the server console."""
return await self.send_command(ctx, command)
@pterodactyl.group(autohelp = True, name = "power")
@pterodactyl.group(autohelp=True, name="power")
@commands.admin()
async def pterodactyl_power(self, ctx: commands.Context) -> None:
"""Send power actions to the server."""
@pterodactyl_power.command(name = "start")
@pterodactyl_power.command(name="start")
async def pterodactyl_power_start(self, ctx: commands.Context) -> Optional[discord.Message]:
"""Start the server."""
return await self.power(ctx, "start", "starting...")
@pterodactyl_power.command(name = "stop")
@pterodactyl_power.command(name="stop")
async def pterodactyl_power_stop(self, ctx: commands.Context) -> Optional[discord.Message]:
"""Stop the server."""
return await self.power(ctx, "stop", "stopping...")
@pterodactyl_power.command(name = "restart")
@pterodactyl_power.command(name="restart")
async def pterodactyl_power_restart(self, ctx: commands.Context) -> Optional[discord.Message]:
"""Restart the server."""
return await self.power(ctx, "restart", "restarting...")
@pterodactyl_power.command(name = "kill")
@pterodactyl_power.command(name="kill")
async def pterodactyl_power_kill(self, ctx: commands.Context) -> Optional[discord.Message]:
"""Kill the server."""
return await self.power(ctx, "kill", "stopping... (forcefully killed)", warning="**⚠️ Forcefully killing the server process can corrupt data in some cases. ⚠️**\n")
@pterodactyl.group(autohelp = True, name = "config", aliases = ["settings", "set"])
@pterodactyl.group(autohelp=True, name="config", aliases=["settings", "set"])
@commands.is_owner()
async def pterodactyl_config(self, ctx: commands.Context) -> None:
"""Configure Pterodactyl settings."""
@pterodactyl_config.command(name = "url")
@pterodactyl_config.command(name="url")
async def pterodactyl_config_base_url(self, ctx: commands.Context, *, base_url: str) -> None:
"""Set the base URL of your Pterodactyl Panel.
@ -342,59 +352,57 @@ class Pterodactyl(commands.Cog):
await config.base_url.set(base_url)
await ctx.send(f"Base URL set to {base_url}")
logger.info("Configuration value set: base_url = %s\nRestarting task...", base_url)
self.task.cancel()
self.retry_counter = 0
self.task = self.get_task()
self.maybe_cancel_task(reset_retry_counter=True)
self.task = self._get_task()
@pterodactyl_config.command(name = "serverid")
@pterodactyl_config.command(name="serverid")
async def pterodactyl_config_server_id(self, ctx: commands.Context, *, server_id: str) -> None:
"""Set the ID of your server."""
await config.server_id.set(server_id)
await ctx.send(f"Server ID set to {server_id}")
logger.info("Configuration value set: server_id = %s\nRestarting task...", server_id)
self.task.cancel()
self.retry_counter = 0
self.task = self.get_task()
self.maybe_cancel_task(reset_retry_counter=True)
self.task = self._get_task()
@pterodactyl_config.group(name = "console")
@pterodactyl_config.group(name="console")
async def pterodactyl_config_console(self, ctx: commands.Context):
"""Configure console settings."""
@pterodactyl_config_console.command(name = "channel")
@pterodactyl_config_console.command(name="channel")
async def pterodactyl_config_console_channel(self, ctx: commands.Context, channel: discord.TextChannel) -> None:
"""Set the channel to send console output to."""
await config.console_channel.set(channel.id)
await ctx.send(f"Console channel set to {channel.mention}")
@pterodactyl_config_console.command(name = "commands")
@pterodactyl_config_console.command(name="commands")
async def pterodactyl_config_console_commands(self, ctx: commands.Context, enabled: bool) -> None:
"""Enable or disable console commands."""
await config.console_commands_enabled.set(enabled)
await ctx.send(f"Console commands set to {enabled}")
@pterodactyl_config.command(name = "invite")
@pterodactyl_config.command(name="invite")
async def pterodactyl_config_invite(self, ctx: commands.Context, invite: str) -> None:
"""Set the invite link for your server."""
await config.invite.set(invite)
await ctx.send(f"Invite link set to {invite}")
@pterodactyl_config.group(name = "topic")
@pterodactyl_config.group(name="topic")
async def pterodactyl_config_topic(self, ctx: commands.Context):
"""Set the topic for the console and chat channels."""
@pterodactyl_config_topic.command(name = "host", aliases = ["hostname", "ip"])
@pterodactyl_config_topic.command(name="host", aliases=["hostname", "ip"])
async def pterodactyl_config_topic_host(self, ctx: commands.Context, host: str) -> None:
"""Set the hostname or IP address of your server."""
await config.topic_hostname.set(host)
await ctx.send(f"Hostname/IP set to `{host}`")
@pterodactyl_config_topic.command(name = "port")
@pterodactyl_config_topic.command(name="port")
async def pterodactyl_config_topic_port(self, ctx: commands.Context, port: int) -> None:
"""Set the port of your server."""
await config.topic_port.set(port)
await ctx.send(f"Port set to `{port}`")
@pterodactyl_config_topic.command(name = "text")
@pterodactyl_config_topic.command(name="text")
async def pterodactyl_config_topic_text(self, ctx: commands.Context, *, text: str) -> None:
"""Set the text for the console and chat channels.
@ -408,19 +416,19 @@ class Pterodactyl(commands.Cog):
- `.$V` (version)
- `.$D` (description / Message of the Day)"""
await config.topic.set(text)
await ctx.send(f"Topic set to:\n{box(text, 'yaml')}")
await ctx.send(f"Topic set to:\n{box(text, 'markdown')}")
@pterodactyl_config.group(name = "chat")
@pterodactyl_config.group(name="chat")
async def pterodactyl_config_chat(self, ctx: commands.Context):
"""Configure chat settings."""
@pterodactyl_config_chat.command(name = "channel")
@pterodactyl_config_chat.command(name="channel")
async def pterodactyl_config_chat_channel(self, ctx: commands.Context, channel: discord.TextChannel) -> None:
"""Set the channel to send chat output to."""
await config.chat_channel.set(channel.id)
await ctx.send(f"Chat channel set to {channel.mention}")
@pterodactyl_config_chat.command(name = "command")
@pterodactyl_config_chat.command(name="command")
async def pterodactyl_config_chat_command(self, ctx: commands.Context, *, command: str) -> None:
"""Set the command that will be used to send messages from Discord.
@ -429,11 +437,11 @@ class Pterodactyl(commands.Cog):
await config.chat_command.set(command)
await ctx.send(f"Chat command set to:\n{box(command, 'json')}")
@pterodactyl_config.group(name = "regex")
@pterodactyl_config.group(name="regex")
async def pterodactyl_config_regex(self, ctx: commands.Context) -> None:
"""Set regex patterns."""
@pterodactyl_config_regex.command(name = "chat")
@pterodactyl_config_regex.command(name="chat")
async def pterodactyl_config_regex_chat(self, ctx: commands.Context, *, regex: str) -> None:
"""Set the regex pattern to match chat messages on the server.
@ -441,7 +449,7 @@ class Pterodactyl(commands.Cog):
await config.chat_regex.set(regex)
await ctx.send(f"Chat regex set to:\n{box(regex, 'regex')}")
@pterodactyl_config_regex.command(name = "server")
@pterodactyl_config_regex.command(name="server")
async def pterodactyl_config_regex_server(self, ctx: commands.Context, *, regex: str) -> None:
"""Set the regex pattern to match server messages on the server.
@ -449,7 +457,7 @@ class Pterodactyl(commands.Cog):
await config.server_regex.set(regex)
await ctx.send(f"Server regex set to:\n{box(regex, 'regex')}")
@pterodactyl_config_regex.command(name = "join")
@pterodactyl_config_regex.command(name="join")
async def pterodactyl_config_regex_join(self, ctx: commands.Context, *, regex: str) -> None:
"""Set the regex pattern to match join messages on the server.
@ -457,7 +465,7 @@ class Pterodactyl(commands.Cog):
await config.join_regex.set(regex)
await ctx.send(f"Join regex set to:\n{box(regex, 'regex')}")
@pterodactyl_config_regex.command(name = "leave")
@pterodactyl_config_regex.command(name="leave")
async def pterodactyl_config_regex_leave(self, ctx: commands.Context, *, regex: str) -> None:
"""Set the regex pattern to match leave messages on the server.
@ -465,7 +473,7 @@ class Pterodactyl(commands.Cog):
await config.leave_regex.set(regex)
await ctx.send(f"Leave regex set to:\n{box(regex, 'regex')}")
@pterodactyl_config_regex.command(name = "achievement")
@pterodactyl_config_regex.command(name="achievement")
async def pterodactyl_config_regex_achievement(self, ctx: commands.Context, *, regex: str) -> None:
"""Set the regex pattern to match achievement messages on the server.
@ -473,41 +481,41 @@ class Pterodactyl(commands.Cog):
await config.achievement_regex.set(regex)
await ctx.send(f"Achievement regex set to:\n{box(regex, 'regex')}")
@pterodactyl_config.group(name = "messages", aliases = ['msg', 'msgs', 'message'])
@pterodactyl_config.group(name="messages", aliases=["msg", "msgs", "message"])
async def pterodactyl_config_messages(self, ctx: commands.Context):
"""Configure message settings."""
@pterodactyl_config_messages.command(name = "startup")
@pterodactyl_config_messages.command(name="startup")
async def pterodactyl_config_messages_startup(self, ctx: commands.Context, *, message: str) -> None:
"""Set the message that will be sent when the server starts."""
await config.startup_msg.set(message)
await ctx.send(f"Startup message set to: {message}")
@pterodactyl_config_messages.command(name = "shutdown")
@pterodactyl_config_messages.command(name="shutdown")
async def pterodactyl_config_messages_shutdown(self, ctx: commands.Context, *, message: str) -> None:
"""Set the message that will be sent when the server stops."""
await config.shutdown_msg.set(message)
await ctx.send(f"Shutdown message set to: {message}")
@pterodactyl_config_messages.command(name = "join")
@pterodactyl_config_messages.command(name="join")
async def pterodactyl_config_messages_join(self, ctx: commands.Context, *, message: str) -> None:
"""Set the message that will be sent when a user joins the server. This is only shown in embeds."""
await config.join_msg.set(message)
await ctx.send(f"Join message set to: {message}")
@pterodactyl_config_messages.command(name = "leave")
@pterodactyl_config_messages.command(name="leave")
async def pterodactyl_config_messages_leave(self, ctx: commands.Context, *, message: str) -> None:
"""Set the message that will be sent when a user leaves the server. This is only shown in embeds."""
await config.leave_msg.set(message)
await ctx.send(f"Leave message set to: {message}")
@pterodactyl_config.command(name = "ip")
@pterodactyl_config.command(name="ip")
async def pterodactyl_config_mask_ip(self, ctx: commands.Context, mask: bool) -> None:
"""Mask the IP addresses of users in console messages."""
await config.mask_ip.set(mask)
await ctx.send(f"IP masking set to {mask}")
@pterodactyl_config.command(name = "api")
@pterodactyl_config.command(name="api")
async def pterodactyl_config_api(self, ctx: commands.Context, endpoint: str) -> None:
"""Set the API endpoint for retrieving user avatars.
@ -516,11 +524,14 @@ class Pterodactyl(commands.Cog):
await config.api_endpoint.set(endpoint)
await ctx.send(f"API endpoint set to {endpoint}")
@pterodactyl_config_regex.group(name = "blacklist", aliases = ['block', 'blocklist'],)
@pterodactyl_config_regex.group(
name="blacklist",
aliases=["block", "blocklist"],
)
async def pterodactyl_config_regex_blacklist(self, ctx: commands.Context):
"""Blacklist regex patterns."""
@pterodactyl_config_regex_blacklist.command(name = "add")
@pterodactyl_config_regex_blacklist.command(name="add")
async def pterodactyl_config_regex_blacklist_add(self, ctx: commands.Context, name: str, *, regex: str) -> None:
"""Add a regex pattern to the blacklist."""
async with config.regex_blacklist() as blacklist:
@ -538,7 +549,7 @@ class Pterodactyl(commands.Cog):
else:
await msg.edit(content="Cancelled.")
@pterodactyl_config_regex_blacklist.command(name = "remove")
@pterodactyl_config_regex_blacklist.command(name="remove")
async def pterodactyl_config_regex_blacklist_remove(self, ctx: commands.Context, name: str) -> None:
"""Remove a regex pattern from the blacklist."""
async with config.regex_blacklist() as blacklist:
@ -555,7 +566,7 @@ class Pterodactyl(commands.Cog):
else:
await ctx.send(f"Name `{name}` does not exist in the blacklist.")
@pterodactyl_config.command(name = 'view', aliases = ['show'])
@pterodactyl_config.command(name="view", aliases=["show"])
async def pterodactyl_config_view(self, ctx: commands.Context) -> None:
"""View the current configuration."""
base_url = await config.base_url()
@ -580,7 +591,7 @@ class Pterodactyl(commands.Cog):
topic_text = await config.topic()
topic_hostname = await config.topic_hostname()
topic_port = await config.topic_port()
embed = discord.Embed(color = await ctx.embed_color(), title="Pterodactyl Configuration")
embed = discord.Embed(color=await ctx.embed_color(), title="Pterodactyl Configuration")
embed.description = f"""**Base URL:** {base_url}
**Server ID:** `{server_id}`
**Console Channel:** <#{console_channel}>
@ -596,19 +607,19 @@ class Pterodactyl(commands.Cog):
**Topic Hostname:** `{topic_hostname}`
**Topic Port:** `{topic_port}`
**Topic Text:** {box(topic_text, 'yaml')}
**Topic Text:** {box(topic_text, "markdown")}
**Chat Command:** {box(chat_command, 'json')}
**Chat Regex:** {box(chat_regex, 're')}
**Server Regex:** {box(server_regex, 're')}
**Join Regex:** {box(join_regex, 're')}
**Leave Regex:** {box(leave_regex, 're')}
**Achievement Regex:** {box(achievement_regex, 're')}"""
**Chat Command:** {box(chat_command, "json")}
**Chat Regex:** {box(chat_regex, "regex")}
**Server Regex:** {box(server_regex, "regex")}
**Join Regex:** {box(join_regex, "regex")}
**Leave Regex:** {box(leave_regex, "regex")}
**Achievement Regex:** {box(achievement_regex, "regex")}"""
await ctx.send(embed=embed)
if not len(regex_blacklist) == 0:
regex_blacklist_embed = discord.Embed(color = await ctx.embed_color(), title="Regex Blacklist")
regex_blacklist_embed = discord.Embed(color=await ctx.embed_color(), title="Regex Blacklist")
for name, regex in regex_blacklist.items():
regex_blacklist_embed.add_field(name=name, value=box(regex, 're'), inline=False)
regex_blacklist_embed.add_field(name=name, value=box(regex, "regex"), inline=False)
await ctx.send(embed=regex_blacklist_embed)
def get_bool_str(self, inp: bool) -> str:

View file

@ -2,14 +2,14 @@
import json
import re
from pathlib import Path
from typing import Optional, Tuple, Union
from typing import Any, Optional, Tuple, Union
import aiohttp
import discord
import websockets
from pydactyl import PterodactylClient
from redbot.core.data_manager import bundled_data_path
from redbot.core.utils.chat_formatting import bold, pagify
from websockets.asyncio.client import connect
from pterodactyl.config import config
from pterodactyl.logger import logger, websocket_logger
@ -19,16 +19,16 @@ from pterodactyl.pterodactyl import Pterodactyl
async def establish_websocket_connection(coginstance: Pterodactyl) -> None:
await coginstance.bot.wait_until_red_ready()
base_url = await config.base_url()
base_url = base_url[:-1] if base_url.endswith('/') else base_url
base_url = base_url[:-1] if base_url.endswith("/") else base_url
logger.info("Establishing WebSocket connection")
websocket_credentials = await retrieve_websocket_credentials(coginstance)
async with websockets.connect(websocket_credentials['data']['socket'], origin=base_url, ping_timeout=60, logger=websocket_logger) as websocket:
async with connect(websocket_credentials["data"]["socket"], origin=base_url, ping_timeout=60, logger=websocket_logger) as websocket:
logger.info("WebSocket connection established")
auth_message = json.dumps({"event": "auth", "args": [websocket_credentials['data']['token']]})
auth_message = json.dumps({"event": "auth", "args": [websocket_credentials["data"]["token"]]})
await websocket.send(auth_message)
logger.info("Authentication message sent")
@ -36,29 +36,31 @@ async def establish_websocket_connection(coginstance: Pterodactyl) -> None:
while True: # pylint: disable=too-many-nested-blocks
message = json.loads(await websocket.recv())
if message['event'] in ('token expiring', 'token expired'):
if message["event"] in ("token expiring", "token expired"):
logger.info("Received token expiring/expired event. Refreshing token.")
websocket_credentials = await retrieve_websocket_credentials(coginstance)
auth_message = json.dumps({"event": "auth", "args": [websocket_credentials['data']['token']]})
auth_message = json.dumps({"event": "auth", "args": [websocket_credentials["data"]["token"]]})
await websocket.send(auth_message)
logger.info("Authentication message sent")
if message['event'] == 'auth success':
if message["event"] == "auth success":
logger.info("WebSocket authentication successful")
if message['event'] == 'console output' and await config.console_channel() is not None:
if message["event"] == "console output" and await config.console_channel() is not None:
regex_blacklist: dict = await config.regex_blacklist()
matches = [re.search(regex, message['args'][0]) for regex in regex_blacklist.values()]
matches = [re.search(regex, message["args"][0]) for regex in regex_blacklist.values()]
if await config.current_status() in ('running', '') and not any(matches):
content = remove_ansi_escape_codes(message['args'][0])
if await config.current_status() in ("running", "") and not any(matches):
content = remove_ansi_escape_codes(message["args"][0])
if await config.mask_ip() is True:
content = mask_ip(content)
console_channel = coginstance.bot.get_channel(await config.console_channel())
assert isinstance(console_channel, discord.abc.Messageable)
chat_channel = coginstance.bot.get_channel(await config.chat_channel())
assert isinstance(chat_channel, discord.abc.Messageable)
if console_channel is not None:
if content.startswith('['):
if content.startswith("["):
pagified_content = pagify(content, delims=[" ", "\n"])
for page in pagified_content:
await console_channel.send(content=page, allowed_mentions=discord.AllowedMentions.none())
@ -66,24 +68,24 @@ async def establish_websocket_connection(coginstance: Pterodactyl) -> None:
server_message = await check_if_server_message(content)
if server_message:
if chat_channel is not None:
await chat_channel.send(server_message if len(server_message) < 2000 else server_message[:1997] + '...', allowed_mentions=discord.AllowedMentions.none())
await chat_channel.send(server_message if len(server_message) < 2000 else server_message[:1997] + "...", allowed_mentions=discord.AllowedMentions.none())
chat_message = await check_if_chat_message(content)
if chat_message:
info = await get_info(chat_message['username'])
info = await get_info(chat_message["username"])
if info is not None:
await send_chat_discord(coginstance, chat_message['username'], chat_message['message'], info['data']['player']['avatar'])
await send_chat_discord(coginstance, chat_message["username"], chat_message["message"], info["data"]["player"]["avatar"])
else:
await send_chat_discord(coginstance, chat_message['username'], chat_message['message'], 'https://seafsh.cc/u/j3AzqQ.png')
await send_chat_discord(coginstance, chat_message["username"], chat_message["message"], "https://seafsh.cc/u/j3AzqQ.png")
join_message = await check_if_join_message(content)
if join_message:
if chat_channel is not None:
if coginstance.bot.embed_requested(chat_channel):
embed, img = await generate_join_leave_embed(coginstance=coginstance, username=join_message,join=True)
embed, img = await generate_join_leave_embed(coginstance=coginstance, username=join_message, join=True)
if img:
with open(img, 'rb') as file:
await chat_channel.send(embed=embed, file=file)
with open(img, "rb") as file:
await chat_channel.send(embed=embed, file=discord.File(fp=file))
else:
await chat_channel.send(embed=embed)
else:
@ -93,10 +95,10 @@ async def establish_websocket_connection(coginstance: Pterodactyl) -> None:
if leave_message:
if chat_channel is not None:
if coginstance.bot.embed_requested(chat_channel):
embed, img = await generate_join_leave_embed(coginstance=coginstance, username=leave_message,join=False)
embed, img = await generate_join_leave_embed(coginstance=coginstance, username=leave_message, join=False)
if img:
with open(img, 'rb') as file:
await chat_channel.send(embed=embed, file=file)
with open(img, "rb") as file:
await chat_channel.send(embed=embed, file=discord.File(fp=file))
else:
await chat_channel.send(embed=embed)
else:
@ -106,13 +108,17 @@ async def establish_websocket_connection(coginstance: Pterodactyl) -> None:
if achievement_message:
if chat_channel is not None:
if coginstance.bot.embed_requested(chat_channel):
await chat_channel.send(embed=await generate_achievement_embed(coginstance, achievement_message['username'], achievement_message['achievement'], achievement_message['challenge']))
embed, img = await generate_achievement_embed(coginstance, achievement_message["username"], achievement_message["achievement"], achievement_message["challenge"])
if img:
await chat_channel.send(embed=embed, file=discord.File(fp=img))
else:
await chat_channel.send(embed=embed)
else:
await chat_channel.send(f"{achievement_message['username']} has {'completed the challenge' if achievement_message['challenge'] else 'made the advancement'} {achievement_message['achievement']}")
if message['event'] == 'status':
if message["event"] == "status":
old_status = await config.current_status()
current_status = message['args'][0]
current_status = message["args"][0]
if old_status != current_status:
await config.current_status.set(current_status)
if await config.console_channel() is not None:
@ -120,81 +126,92 @@ async def establish_websocket_connection(coginstance: Pterodactyl) -> None:
if console is not None:
await console.send(f"Server status changed! `{current_status}`")
if await config.chat_channel() is not None:
if current_status == 'running' and await config.startup_msg() is not None:
if current_status == "running" and await config.startup_msg() is not None:
chat = coginstance.bot.get_channel(await config.chat_channel())
if chat is not None:
await chat.send(await config.startup_msg())
if current_status == 'stopping' and await config.shutdown_msg() is not None:
if current_status == "stopping" and await config.shutdown_msg() is not None:
chat = coginstance.bot.get_channel(await config.chat_channel())
if chat is not None:
await chat.send(await config.shutdown_msg())
async def retrieve_websocket_credentials(coginstance: Pterodactyl) -> Optional[dict]:
async def retrieve_websocket_credentials(coginstance: Pterodactyl) -> dict:
pterodactyl_keys = await coginstance.bot.get_shared_api_tokens("pterodactyl")
api_key = pterodactyl_keys.get("api_key")
if api_key is None:
coginstance.task.cancel()
coginstance.maybe_cancel_task()
raise ValueError("Pterodactyl API key not set. Please set it using `[p]set api`.")
base_url = await config.base_url()
if base_url is None:
coginstance.task.cancel()
coginstance.maybe_cancel_task()
raise ValueError("Pterodactyl base URL not set. Please set it using `[p]pterodactyl config url`.")
server_id = await config.server_id()
if server_id is None:
coginstance.task.cancel()
coginstance.maybe_cancel_task()
raise ValueError("Pterodactyl server ID not set. Please set it using `[p]pterodactyl config serverid`.")
client = PterodactylClient(base_url, api_key).client
coginstance.client = client
websocket_credentials = client.servers.get_websocket(server_id)
logger.debug("""Websocket connection details retrieved:
websocket_credentials: dict[str, Any] = client.servers.get_websocket(server_id)
if not websocket_credentials:
coginstance.maybe_cancel_task()
raise ValueError("Failed to retrieve websocket credentials. Please ensure the API details are correctly configured.")
logger.debug(
"""Websocket connection details retrieved:
Socket: %s
Token: %s...""",
websocket_credentials['data']['socket'],
websocket_credentials['data']['token'][:20]
websocket_credentials["data"]["socket"],
websocket_credentials["data"]["token"][:20],
)
return websocket_credentials
#NOTE - The token is truncated to prevent it from being logged in its entirety, for security reasons
# NOTE - The token is truncated to prevent it from being logged in its entirety, for security reasons
def remove_ansi_escape_codes(text: str) -> str:
ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
#NOTE - https://chat.openai.com/share/d92f9acf-d776-4fd6-a53f-b14ac15dd540
return ansi_escape.sub('', text)
ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
# NOTE - https://chat.openai.com/share/d92f9acf-d776-4fd6-a53f-b14ac15dd540
return ansi_escape.sub("", text)
async def check_if_server_message(text: str) -> Union[bool, str]:
async def check_if_server_message(text: str) -> Optional[str]:
regex = await config.server_regex()
match: Optional[re.Match[str]] = re.match(regex, text)
if match:
logger.trace("Message is a server message")
return match.group(1)
return False
return None
async def check_if_chat_message(text: str) -> Union[bool, dict]:
async def check_if_chat_message(text: str) -> Optional[dict]:
regex = await config.chat_regex()
match: Optional[re.Match[str]] = re.match(regex, text)
if match:
groups = {"username": match.group(1), "message": match.group(2)}
logger.trace("Message is a chat message\n%s", json.dumps(groups))
return groups
return False
return None
async def check_if_join_message(text: str) -> Union[bool, str]:
async def check_if_join_message(text: str) -> Optional[str]:
regex = await config.join_regex()
match: Optional[re.Match[str]] = re.match(regex, text)
if match:
logger.trace("Message is a join message")
return match.group(1)
return False
return None
async def check_if_leave_message(text: str) -> Union[bool, str]:
async def check_if_leave_message(text: str) -> Optional[str]:
regex = await config.leave_regex()
match: Optional[re.Match[str]] = re.match(regex, text)
if match:
logger.trace("Message is a leave message")
return match.group(1)
return False
return None
async def check_if_achievement_message(text: str) -> Union[bool, dict]:
async def check_if_achievement_message(text: str) -> Optional[dict]:
regex = await config.achievement_regex()
match: Optional[re.Match[str]] = re.match(regex, text)
if match:
@ -205,7 +222,8 @@ async def check_if_achievement_message(text: str) -> Union[bool, dict]:
groups["challenge"] = False
logger.trace("Message is an achievement message")
return groups
return False
return None
async def get_info(username: str) -> Optional[dict]:
logger.verbose("Retrieving player info for %s", username)
@ -218,6 +236,7 @@ async def get_info(username: str) -> Optional[dict]:
logger.warning("Failed to retrieve player info for %s: %s", username, response.status)
return None
async def send_chat_discord(coginstance: Pterodactyl, username: str, message: str, avatar_url: str) -> None:
logger.trace("Sending chat message to Discord")
channel = coginstance.bot.get_channel(await config.chat_channel())
@ -231,6 +250,7 @@ async def send_chat_discord(coginstance: Pterodactyl, username: str, message: st
else:
logger.warning("Chat channel not set. Skipping sending chat message to Discord")
async def generate_join_leave_embed(coginstance: Pterodactyl, username: str, join: bool) -> Tuple[discord.Embed, Optional[Union[str, Path]]]:
embed = discord.Embed()
embed.color = discord.Color.green() if join else discord.Color.red()
@ -238,30 +258,32 @@ async def generate_join_leave_embed(coginstance: Pterodactyl, username: str, joi
info = await get_info(username)
if info:
img = None
embed.set_author(name=username, icon_url=info['data']['player']['avatar'])
embed.set_author(name=username, icon_url=info["data"]["player"]["avatar"])
else:
img = bundled_data_path(coginstance) / "unknown.png"
embed.set_author(name=username, icon_url='attachment://unknown.png')
embed.set_author(name=username, icon_url="attachment://unknown.png")
embed.timestamp = discord.utils.utcnow()
return embed, img
async def generate_achievement_embed(coginstance: Pterodactyl, username: str, achievement: str, challenge: bool) -> Tuple[discord.Embed, Optional[Union[str, Path]]]:
embed = discord.Embed()
embed.color = discord.Color.from_str('#a800a7') if challenge else discord.Color.from_str('#54fb54')
embed.color = discord.Color.from_str("#a800a7") if challenge else discord.Color.from_str("#54fb54")
embed.description = f"{bold(username)} has {'completed the challenge' if challenge else 'made the advancement'} {bold(achievement)}"
info = await get_info(username)
if info:
img = None
embed.set_author(name=username, icon_url=info['data']['player']['avatar'])
embed.set_author(name=username, icon_url=info["data"]["player"]["avatar"])
else:
img = bundled_data_path(coginstance) / "unknown.png"
embed.set_author(name=username, icon_url='attachment://unknown.png')
embed.set_author(name=username, icon_url="attachment://unknown.png")
embed.timestamp = discord.utils.utcnow()
return embed, img
def mask_ip(string: str) -> str:
def check(match: re.Match[str]):
ip = match.group(0)
masked_ip = '.'.join(r'\*' * len(octet) for octet in ip.split('.'))
return masked_ip
return re.sub(r'\b(?:\d{1,3}\.){3}\d{1,3}\b', check, string)
return ".".join(r"\*" * len(octet) for octet in ip.split("."))
return re.sub(r"\b(?:\d{1,3}\.){3}\d{1,3}\b", check, string)

View file

@ -2,44 +2,45 @@
name = "seacogs"
version = "0.1.0"
description = "My assorted cogs for Red-DiscordBot."
authors = [{name = "cswimr", email = "seaswimmerthefsh@gmail.com"}]
license = {file="LICENSE"}
authors = [{ name = "cswimr", email = "seaswimmerthefsh@gmail.com" }]
license = { file = "LICENSE" }
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"aiosqlite>=0.20.0",
"beautifulsoup4>=4.12.3",
"colorthief>=0.2.1",
"markdownify>=0.13.1",
"numpy>=2.1.2",
"phx-class-registry>=5.0.0",
"pillow>=10.4.0",
"pip>=24.3.1",
"aiosqlite==0.21.0",
"beautifulsoup4==4.13.3",
"colorthief==0.2.1",
"markdownify==1.1.0",
"numpy==2.2.4",
"phx-class-registry==5.1.1",
"pillow==10.4.0",
"pip==25.0.1",
"py-dactyl",
"pydantic>=2.9.2",
"red-discordbot>=3.5.14",
"websockets>=13.1",
"pydantic==2.11.1",
"red-discordbot==3.5.18",
"watchdog==6.0.0",
"websockets==15.0.1",
]
[project.optional-dependencies]
[dependency-groups]
documentation = [
"mkdocs>=1.6.1",
"mkdocs-git-authors-plugin>=0.9.0",
"mkdocs-git-revision-date-localized-plugin>=1.2.9",
"mkdocs-material[imaging]>=9.5.40",
"mkdocstrings[python]>=0.26.1",
"mkdocs-redirects>=1.2.1",
"mkdocs==1.6.1",
"mkdocs-git-authors-plugin==0.9.4",
"mkdocs-git-revision-date-localized-plugin==1.4.5",
"mkdocs-material[imaging]==9.6.10",
"mkdocs-redirects==1.2.2",
"mkdocstrings[python]==0.29.0",
]
[tool.uv]
dev-dependencies = [
"pylint>=3.3.1",
"ruff>=0.6.9",
"sqlite-web>=0.6.4",
]
dev-dependencies = ["pylint==3.3.6", "ruff==0.11.2", "sqlite-web==0.6.4"]
[tool.uv.sources]
py-dactyl = { git = "https://github.com/cswimr/pydactyl" }
py-dactyl = { git = "https://github.com/iamkubi/pydactyl", tag = "v2.0.5" }
[tool.basedpyright]
typeCheckingMode = "basic"
reportAttributeAccessIssue = false # disabled because `commands.group.command` is listed as Any / Unknown for some reason
[tool.ruff]
# Exclude a variety of commonly ignored directories.
@ -83,8 +84,32 @@ target-version = "py311"
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
# McCabe complexity (`C901`) by default.
select = ["F", "W", "E", "C901"]
ignore = ["C901"]
select = [
"I",
"N",
"F",
"W",
"E",
"G",
"A",
"COM",
"INP",
"T20",
"PLC",
"PLE",
"PLW",
"PLR",
"LOG",
"SLF",
"ERA",
"FIX",
"PERF",
"C4",
"EM",
"RET",
"RSE",
]
ignore = ["PLR0911", "PLR0912", "PLR0915", "PLR2004", "PLR0913", "EM101"]
# Allow fix for all enabled rules (when `--fix`) is provided.
fixable = ["ALL"]

View file

@ -1,10 +1,11 @@
{
"author" : ["cswimr"],
"install_msg" : "Thank you for installing SeaUtils!\nYou can find the source code of this cog [here](https://coastalcommits.com/cswimr/SeaCogs).",
"name" : "SeaUtils",
"short" : "A collection of useful utilities.",
"description" : "A collection of useful utilities.",
"end_user_data_statement" : "This cog does not store end user data.",
"$schema": "https://raw.githubusercontent.com/Cog-Creators/Red-DiscordBot/refs/heads/V3/develop/schema/red_cog_repo.schema.json",
"author": ["cswimr"],
"install_msg": "Thank you for installing SeaUtils!\nYou can find the source code of this cog [here](https://coastalcommits.com/cswimr/SeaCogs).",
"name": "SeaUtils",
"short": "A collection of useful utilities.",
"description": "A collection of useful utilities.",
"end_user_data_statement": "This cog does not store end user data.",
"hidden": true,
"disabled": false,
"min_bot_version": "3.5.0",

View file

@ -29,18 +29,20 @@ from redbot.core.utils.views import SimpleMenu
def md(soup: BeautifulSoup, **options) -> Any | str:
return MarkdownConverter(**options).convert_soup(soup=soup)
def format_rfc_text(text: str, number: int) -> str:
one: str = re.sub(r"\(\.\/rfc(\d+)", r"(https://www.rfc-editor.org/rfc/rfc\1.html", text)
two: str = re.sub(r"\((#(?:section|page)-\d+(?:.\d+)?)\)", f"(https://www.rfc-editor.org/rfc/rfc{number}.html\1)", one)
three: str = re.sub(r"\n{3,}", "\n\n", two)
return three
class SeaUtils(commands.Cog):
"""A collection of random utilities."""
__author__ = ["[cswimr](https://www.coastalcommits.com/cswimr)"]
__git__ = "https://www.coastalcommits.com/cswimr/SeaCogs"
__version__ = "1.0.1"
__version__ = "1.0.2"
__documentation__ = "https://seacogs.coastalcommits.com/seautils/"
def __init__(self, bot: Red) -> None:
@ -57,7 +59,6 @@ class SeaUtils(commands.Cog):
]
return "\n".join(text)
def format_src(self, obj: Any) -> str:
"""A large portion of this code is repurposed from Zephyrkul's RTFS cog.
https://github.com/Zephyrkul/FluffyCogs/blob/master/rtfs/rtfs.py"""
@ -73,9 +74,9 @@ class SeaUtils(commands.Cog):
src = obj.function
return inspect.getsource(object=src)
@commands.command(aliases=["source", "src", "code", "showsource"])
@commands.command(aliases=["source", "src", "code", "showsource"]) # type: ignore
@commands.is_owner()
async def showcode(self, ctx: commands.Context, *, object: str) -> None: # pylint: disable=redefined-builtin
async def showcode(self, ctx: commands.Context, *, object: str) -> None: # pylint: disable=redefined-builtin # noqa: A002
"""Show the code for a particular object."""
try:
if object.startswith("/") and (obj := ctx.bot.tree.get_command(object[1:])):
@ -86,11 +87,7 @@ class SeaUtils(commands.Cog):
text = self.format_src(obj)
else:
raise AttributeError
temp_content = cf.pagify(
text=cleanup_code(text),
escape_mass_mentions=True,
page_length = 1977
)
temp_content = cf.pagify(text=cleanup_code(text), escape_mass_mentions=True, page_length=1977)
content = []
max_i = operator.length_hint(temp_content)
i = 1
@ -105,7 +102,7 @@ class SeaUtils(commands.Cog):
else:
await ctx.send(content="Object not found!", reference=ctx.message.to_reference(fail_if_not_exists=False))
@commands.command(name='dig', aliases=['dnslookup', 'nslookup'])
@commands.command(name="dig", aliases=["dnslookup", "nslookup"]) # type: ignore
@commands.is_owner()
async def dig(self, ctx: commands.Context, name: str, record_type: str | None = None, server: str | None = None, port: int = 53) -> None:
"""Retrieve DNS information for a domain.
@ -113,13 +110,13 @@ class SeaUtils(commands.Cog):
Uses `dig` to perform a DNS query. Will fall back to `nslookup` if `dig` is not installed on the system.
`nslookup` does not provide as much information as `dig`, so only the `name` parameter will be used if `nslookup` is used.
Will return the A, AAAA, and CNAME records for a domain by default. You can specify a different record type with the `type` parameter."""
command_opts: list[str | int] = ['dig']
query_types: list[str] = [record_type] if record_type else ['A', 'AAAA', 'CNAME']
command_opts: list[str] = ["dig"]
query_types: list[str] = [record_type] if record_type else ["A", "AAAA", "CNAME"]
if server:
command_opts.extend(['@', server])
command_opts.extend(["@", server])
for query_type in query_types:
command_opts.extend([name, query_type])
command_opts.extend(['-p', str(port), '+yaml'])
command_opts.extend(["-p", str(port), "+yaml"])
try:
process: Process = await asyncio.create_subprocess_exec(*command_opts, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
@ -128,22 +125,18 @@ class SeaUtils(commands.Cog):
await ctx.maybe_send_embed(message="An error was encountered!\n" + cf.box(text=stderr.decode()))
else:
data = yaml.safe_load(stdout.decode())
message_data: dict = data[0]['message']
response_data: dict = message_data['response_message_data']
message_data: dict = data[0]["message"]
response_data: dict = message_data["response_message_data"]
if ctx.embed_requested():
embed = Embed(
title="DNS Query Result",
color=await ctx.embed_color(),
timestamp=message_data['response_time']
)
embed.add_field(name="Response Address", value=message_data['response_address'], inline=True)
embed.add_field(name="Response Port", value=message_data['response_port'], inline=True)
embed.add_field(name="Query Address", value=message_data['query_address'], inline=True)
embed.add_field(name="Query Port", value=message_data['query_port'], inline=True)
embed.add_field(name="Status", value=response_data['status'], inline=True)
embed.add_field(name="Flags", value=response_data['flags'], inline=True)
embed = Embed(title="DNS Query Result", color=await ctx.embed_color(), timestamp=message_data["response_time"])
embed.add_field(name="Response Address", value=message_data["response_address"], inline=True)
embed.add_field(name="Response Port", value=message_data["response_port"], inline=True)
embed.add_field(name="Query Address", value=message_data["query_address"], inline=True)
embed.add_field(name="Query Port", value=message_data["query_port"], inline=True)
embed.add_field(name="Status", value=response_data["status"], inline=True)
embed.add_field(name="Flags", value=response_data["flags"], inline=True)
if response_data.get('status') != 'NOERROR':
if response_data.get("status") != "NOERROR":
embed.colour = Color.red()
embed.description = cf.error("Dig query did not return `NOERROR` status.")
@ -151,19 +144,19 @@ class SeaUtils(commands.Cog):
answers = []
authorities = []
for m in data:
response = m['message']['response_message_data']
if 'QUESTION_SECTION' in response:
for question in response['QUESTION_SECTION']:
response = m["message"]["response_message_data"]
if "QUESTION_SECTION" in response:
for question in response["QUESTION_SECTION"]:
if question not in questions:
questions.append(question)
if 'ANSWER_SECTION' in response:
for answer in response['ANSWER_SECTION']:
if "ANSWER_SECTION" in response:
for answer in response["ANSWER_SECTION"]:
if answer not in answers:
answers.append(answer)
if 'AUTHORITY_SECTION' in response:
for authority in response['AUTHORITY_SECTION']:
if "AUTHORITY_SECTION" in response:
for authority in response["AUTHORITY_SECTION"]:
if authority not in authorities:
authorities.append(authority)
@ -183,26 +176,22 @@ class SeaUtils(commands.Cog):
embed.add_field(name="Authority Section", value=f"{cf.box(text=authority_section, lang='prolog')}", inline=False)
await ctx.send(embed=embed)
else:
await ctx.send(content=cf.box(text=stdout, lang='yaml'))
except (FileNotFoundError):
await ctx.send(content=cf.box(text=str(stdout), lang="yaml"))
except FileNotFoundError:
try:
ns_process = await asyncio.create_subprocess_exec('nslookup', name, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
ns_process = await asyncio.create_subprocess_exec("nslookup", name, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
ns_stdout, ns_stderr = await ns_process.communicate()
if ns_stderr:
await ctx.maybe_send_embed(message="An error was encountered!\n" + cf.box(text=ns_stderr.decode()))
else:
warning = cf.warning("`dig` is not installed! Defaulting to `nslookup`.\nThis command provides more information when `dig` is installed on the system.\n")
if await ctx.embed_requested():
embed = Embed(
title="DNS Query Result",
color=await ctx.embed_color(),
timestamp=ctx.message.created_at
)
embed = Embed(title="DNS Query Result", color=await ctx.embed_color(), timestamp=ctx.message.created_at)
embed.description = warning + cf.box(text=ns_stdout.decode())
await ctx.send(embed=embed)
else:
await ctx.send(content = warning + cf.box(text=ns_stdout.decode()))
except (FileNotFoundError):
await ctx.send(content=warning + cf.box(text=ns_stdout.decode()))
except FileNotFoundError:
await ctx.maybe_send_embed(message=cf.error("Neither `dig` nor `nslookup` are installed on the system. Unable to resolve DNS query."))
@commands.command()
@ -217,38 +206,27 @@ class SeaUtils(commands.Cog):
async with session.get(url=url) as response:
if response.status == 200:
html = await response.text()
soup = BeautifulSoup(html, 'html.parser')
pre_tags = soup.find_all('pre')
content: list[Embed | str] = []
soup = BeautifulSoup(html, "html.parser")
pre_tags = soup.find_all("pre")
content: list[str | Embed] = []
for pre_tag in pre_tags:
text = format_rfc_text(md(pre_tag), number)
if len(text) > 4096:
pagified_text = cf.pagify(text, delims=["\n\n"], page_length=4096)
for page in pagified_text:
if await ctx.embed_requested():
embed = Embed(
title=f"RFC Document {number}",
url=datatracker_url,
description=page,
color=await ctx.embed_color()
)
embed = Embed(title=f"RFC Document {number}", url=datatracker_url, description=page, color=await ctx.embed_color())
content.append(embed)
else:
content.append(page)
else:
if await ctx.embed_requested():
embed = Embed(
title=f"RFC Document {number}",
url=datatracker_url,
description=text,
color=await ctx.embed_color()
)
elif await ctx.embed_requested():
embed = Embed(title=f"RFC Document {number}", url=datatracker_url, description=text, color=await ctx.embed_color())
content.append(embed)
else:
content.append(text)
if await ctx.embed_requested():
for embed in content:
embed.set_footer(text=f"Page {content.index(embed) + 1}/{len(content)}")
await SimpleMenu(pages=content, disable_after_timeout=True, timeout=300).start(ctx)
await SimpleMenu(pages=content, disable_after_timeout=True, timeout=300).start(ctx) # type: ignore
else:
await ctx.maybe_send_embed(message=cf.error(f"An error occurred while fetching RFC {number}. Status code: {response.status}."))

View file

@ -1,5 +0,0 @@
from .speedtest import Speedtest
async def setup(bot):
await bot.add_cog(Speedtest(bot))

View file

@ -1,14 +0,0 @@
{
"author" : ["SeaswimmerTheFsh (seasw.)"],
"install_msg" : "Thank you for installing Speedtest!\nYou can find the source code of this cog [here](https://coastalcommits.com/SeaswimmerTheFsh/SeaCogs).",
"name" : "Speedtest",
"short" : "A collection of useful utilities.",
"description" : "A collection of useful utilities.",
"end_user_data_statement" : "This cog does not store end user data.",
"hidden": true,
"disabled": false,
"min_bot_version": "3.5.0",
"min_python_version": [3, 10, 0],
"tags" : ["utility", "information"],
"requirements": ["pydantic"]
}

View file

@ -1,76 +0,0 @@
from datetime import datetime
from pydantic import BaseModel
class Speedtest(BaseModel):
type: str
timestamp: datetime
ping: "Ping"
download: "Bandwidth"
upload: "Bandwidth"
isp: str
interface: "Interface"
server: "Server"
result: "Result"
@classmethod
def from_json(cls, data: dict) -> "Speedtest":
return cls(
type=data["type"],
timestamp=datetime.fromisoformat(data["timestamp"]),
ping=Ping(**data["ping"]),
download=Bandwidth(**data["download"]),
upload=Bandwidth(**data["upload"]),
isp=data["isp"],
interface=Interface(**data["interface"]),
server=Server(**data["server"]),
result=Result(**data["result"])
)
class Bandwidth(BaseModel):
bandwidth: float
bytes: int
elapsed: int
latency: "Latency"
@property
def mbps(self) -> float:
return self.bandwidth / 1_000_000
class Latency(BaseModel):
iqm: float
low: float
high: float
jitter: float
class Interface(BaseModel):
internalIp: str
name: str
macAddr: str
isVpn: bool
externalIp: str
class Ping(BaseModel):
jitter: float
latency: float
low: float
high: float
class Server(BaseModel):
id: int
name: str
location: str
country: str
host: str
port: int
ip: str
class Result(BaseModel):
id: str
url: str
persisted: bool
@property
def image(self) -> str:
return self.url + ".png"

View file

@ -1,71 +0,0 @@
# _____ _
# / ____| (_)
# | (___ ___ __ _ _____ ___ _ __ ___ _ __ ___ ___ _ __
# \___ \ / _ \/ _` / __\ \ /\ / / | '_ ` _ \| '_ ` _ \ / _ \ '__|
# ____) | __/ (_| \__ \\ V V /| | | | | | | | | | | | __/ |
# |_____/ \___|\__,_|___/ \_/\_/ |_|_| |_| |_|_| |_| |_|\___|_|
import asyncio
import json
import subprocess
import discord
from redbot.core import commands
from redbot.core.bot import Red
from redbot.core.utils import chat_formatting as cf
from .models import Speedtest as sp
class Speedtest(commands.Cog):
"""A collection of random utilities."""
__author__ = ["SeaswimmerTheFsh"]
__version__ = "1.0.0"
def __init__(self, bot: Red):
self.bot = bot
def format_help_for_context(self, ctx: commands.Context) -> str:
pre_processed = super().format_help_for_context(ctx) or ""
n = "\n" if "\n\n" not in pre_processed else ""
text = [
f"{pre_processed}{n}",
f"Cog Version: **{self.__version__}**",
f"Author: {cf.humanize_list(self.__author__)}"
]
return "\n".join(text)
async def run_speedtest(self) -> str | sp:
try:
process = await asyncio.create_subprocess_exec(
"speedtest", "-f", "json", "--accept-license", "--accept-gdpr",
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
except FileNotFoundError:
return "Speedtest CLI is not installed."
stdout, stderr = await process.communicate()
if process.returncode != 0:
return stderr.decode("utf-8")
return sp.from_json(json.loads(stdout.decode("utf-8")))
@commands.command()
@commands.is_owner()
async def speedtest(self, ctx: commands.Context) -> None:
"""Run a speedtest."""
msg = await ctx.maybe_send_embed("Running speedtest...")
async with ctx.typing():
speedtest = await self.run_speedtest()
if await ctx.embed_requested():
if not isinstance(speedtest, sp):
await msg.edit(embed=discord.Embed(description=f"An error occurred! {speedtest}", color=discord.Colour.red()))
return
embed = discord.Embed(title="Speedtest Results", url=speedtest.result.url, color=await ctx.embed_color())
embed.set_image(url=speedtest.result.image)
await msg.edit(embed=embed)
else:
if not isinstance(speedtest, sp):
await msg.edit(content=f"An error occurred! \n`{speedtest}`")
return
await msg.edit(content=f"**[Result]({speedtest.result.url})**")

1074
uv.lock generated

File diff suppressed because it is too large Load diff