|
4 | 4 | import json |
5 | 5 | import re |
6 | 6 | import subprocess |
| 7 | +from datetime import UTC, datetime |
| 8 | +from enum import StrEnum |
7 | 9 | from itertools import islice |
8 | 10 | from typing import TYPE_CHECKING, TypedDict, TypeVar |
9 | 11 |
|
|
13 | 15 | from ruff.__main__ import find_ruff_bin # type: ignore[import-untyped] |
14 | 16 |
|
15 | 17 | from byte.lib import settings |
16 | | -from byte.lib.common import pastebin |
| 18 | +from byte.lib.common.links import pastebin |
17 | 19 |
|
18 | 20 | if TYPE_CHECKING: |
19 | 21 | from collections.abc import Iterable |
|
22 | 24 | from discord.ext.commands import Context |
23 | 25 | from discord.ext.commands._types import Check |
24 | 26 |
|
| 27 | +__all__ = ( |
| 28 | + "BaseRuffRule", |
| 29 | + "RuffRule", |
| 30 | + "FormattedRuffRule", |
| 31 | + "PEP", |
| 32 | + "is_byte_dev", |
| 33 | + "linker", |
| 34 | + "mention_user", |
| 35 | + "mention_user_nickname", |
| 36 | + "mention_channel", |
| 37 | + "mention_role", |
| 38 | + "mention_slash_command", |
| 39 | + "mention_custom_emoji", |
| 40 | + "mention_custom_emoji_animated", |
| 41 | + "mention_timestamp", |
| 42 | + "mention_guild_navigation", |
| 43 | + "format_ruff_rule", |
| 44 | + "query_all_ruff_rules", |
| 45 | + "run_ruff_format", |
| 46 | + "paste", |
| 47 | + "chunk_sequence", |
| 48 | + "query_all_peps", |
| 49 | +) |
| 50 | + |
25 | 51 | _T = TypeVar("_T") |
26 | 52 |
|
27 | 53 |
|
28 | 54 | class BaseRuffRule(TypedDict): |
| 55 | + """Base Ruff rule data.""" |
| 56 | + |
29 | 57 | name: str |
30 | 58 | summary: str |
31 | 59 | fix: str |
32 | 60 | explanation: str |
33 | 61 |
|
34 | 62 |
|
35 | 63 | class RuffRule(BaseRuffRule): |
| 64 | + """Ruff rule data.""" |
| 65 | + |
36 | 66 | code: str |
37 | 67 | linter: str |
38 | 68 | message_formats: list[str] |
39 | 69 | preview: bool |
40 | 70 |
|
41 | 71 |
|
42 | 72 | class FormattedRuffRule(BaseRuffRule): |
| 73 | + """Formatted Ruff rule data.""" |
| 74 | + |
43 | 75 | rule_link: str |
44 | 76 | rule_anchor_link: str |
45 | 77 |
|
46 | 78 |
|
47 | | -__all__ = ( |
48 | | - "is_byte_dev", |
49 | | - "linker", |
50 | | - "mention_user", |
51 | | - "mention_user_nickname", |
52 | | - "mention_channel", |
53 | | - "mention_role", |
54 | | - "mention_slash_command", |
55 | | - "mention_custom_emoji", |
56 | | - "mention_custom_emoji_animated", |
57 | | - "mention_timestamp", |
58 | | - "mention_guild_navigation", |
59 | | - "format_ruff_rule", |
60 | | - "query_all_ruff_rules", |
61 | | - "run_ruff_format", |
62 | | - "paste", |
63 | | -) |
| 79 | +class PEPType(StrEnum): |
| 80 | + """Type of PEP. |
| 81 | +
|
| 82 | + Based off of `PEP Types in PEP1 <https://peps.python.org/#pep-types-key>`_. |
| 83 | + """ |
| 84 | + |
| 85 | + I = "Informational" # noqa: E741 |
| 86 | + P = "Process" |
| 87 | + S = "Standards Track" |
| 88 | + |
| 89 | + |
| 90 | +class PEPStatus(StrEnum): |
| 91 | + """Status of a PEP. |
| 92 | +
|
| 93 | + .. note:: ``Active`` and ``Accepted`` both traditionally use ``A``, |
| 94 | + but are differentiated here for clarity. |
| 95 | +
|
| 96 | + Based off of `PEP Status in PEP1 <https://peps.python.org/#pep-status-key>`_. |
| 97 | + """ |
| 98 | + |
| 99 | + A = "Active" |
| 100 | + AA = "Accepted" |
| 101 | + D = "Deferred" |
| 102 | + __ = "Draft" |
| 103 | + F = "Final" |
| 104 | + P = "Provisional" |
| 105 | + R = "Rejected" |
| 106 | + S = "Superseded" |
| 107 | + W = "Withdrawn" |
| 108 | + |
| 109 | + |
| 110 | +class PEPHistoryItem(TypedDict, total=False): |
| 111 | + """PEP history item. |
| 112 | +
|
| 113 | + Sometimes these include a list of ``datetime`` objects, |
| 114 | + other times they are a list of datetime and str |
| 115 | + because they contain a date and an rST link. |
| 116 | + """ |
| 117 | + |
| 118 | + date: str |
| 119 | + link: str |
| 120 | + |
| 121 | + |
| 122 | +class PEP(TypedDict): |
| 123 | + """PEP data. |
| 124 | +
|
| 125 | + Based off of the `PEPS API <https://peps.python.org/api/peps.json>`_. |
| 126 | + """ |
| 127 | + |
| 128 | + number: int |
| 129 | + title: str |
| 130 | + authors: list[str] | str |
| 131 | + discussions_to: str |
| 132 | + status: PEPStatus |
| 133 | + type: PEPType |
| 134 | + topic: str |
| 135 | + created: datetime |
| 136 | + python_version: list[float] | float |
| 137 | + post_history: list[str] |
| 138 | + resolution: str | None |
| 139 | + requires: str | None |
| 140 | + replaces: str | None |
| 141 | + superseded_by: str | None |
| 142 | + url: str |
64 | 143 |
|
65 | 144 |
|
66 | 145 | def is_byte_dev() -> Check[Any]: |
@@ -311,3 +390,55 @@ def chunk_sequence(sequence: Iterable[_T], size: int) -> Iterable[tuple[_T, ...] |
311 | 390 | _sequence = iter(sequence) |
312 | 391 | while chunk := tuple(islice(_sequence, size)): |
313 | 392 | yield chunk |
| 393 | + |
| 394 | + |
| 395 | +def format_resolution_link(resolution: str | None) -> str: |
| 396 | + """Formats the resolution URL into a markdown link. |
| 397 | +
|
| 398 | + Args: |
| 399 | + resolution (str): The resolution URL. |
| 400 | +
|
| 401 | + Returns: |
| 402 | + str: The formatted markdown link. |
| 403 | + """ |
| 404 | + if not resolution: |
| 405 | + return "N/A" |
| 406 | + if "discuss.python.org" in resolution: |
| 407 | + return f"[via Discussion Forum]({resolution})" |
| 408 | + if "mail.python.org" in resolution: |
| 409 | + return f"[via Mailist]({resolution})" |
| 410 | + return resolution |
| 411 | + |
| 412 | + |
| 413 | +async def query_all_peps() -> list[PEP]: |
| 414 | + """Query all PEPs from the PEPs Python.org API. |
| 415 | +
|
| 416 | + Returns: |
| 417 | + list[PEP]: All PEPs |
| 418 | + """ |
| 419 | + url = "https://peps.python.org/api/peps.json" |
| 420 | + async with httpx.AsyncClient() as client: |
| 421 | + response = await client.get(url) |
| 422 | + response.raise_for_status() |
| 423 | + data = response.json() |
| 424 | + |
| 425 | + return [ # type: ignore[reportReturnType] |
| 426 | + { |
| 427 | + "number": pep_info["number"], |
| 428 | + "title": pep_info["title"], |
| 429 | + "authors": pep_info["authors"].split(", "), |
| 430 | + "discussions_to": pep_info["discussions_to"], |
| 431 | + "status": PEPStatus(pep_info["status"]), |
| 432 | + "type": PEPType(pep_info["type"]), |
| 433 | + "topic": pep_info.get("topic", ""), |
| 434 | + "created": datetime.strptime(pep_info["created"], "%d-%b-%Y").replace(tzinfo=UTC).strftime("%Y-%m-%d"), |
| 435 | + "python_version": pep_info.get("python_version"), |
| 436 | + "post_history": pep_info.get("post_history", []), |
| 437 | + "resolution": format_resolution_link(pep_info.get("resolution", "N/A")), |
| 438 | + "requires": pep_info.get("requires"), |
| 439 | + "replaces": pep_info.get("replaces"), |
| 440 | + "superseded_by": pep_info.get("superseded_by"), |
| 441 | + "url": pep_info["url"], |
| 442 | + } |
| 443 | + for pep_info in data.values() |
| 444 | + ] |
0 commit comments