From ba17445e9dec85242f382089319c5c69f4a00c26 Mon Sep 17 00:00:00 2001 From: zml Date: Sat, 1 Feb 2025 20:20:40 -0800 Subject: [PATCH 01/10] chore: wire up ruff for formatting and linting ruff will be run in CI --- .github/workflows/check-format.yaml | 35 +++++++++++++++++++++++++++++ poetry.lock | 30 ++++++++++++++++++++++++- pyproject.toml | 9 ++++++++ 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/check-format.yaml diff --git a/.github/workflows/check-format.yaml b/.github/workflows/check-format.yaml new file mode 100644 index 0000000..baccf64 --- /dev/null +++ b/.github/workflows/check-format.yaml @@ -0,0 +1,35 @@ +name: "build" + +on: + push: + branches: "**" + tags-ignore: ["**"] + pull_request: + +permissions: + contents: "read" + checks: write + issues: write + pull-requests: write + +jobs: + build: + # Only run on PRs if the source branch is on someone else's repo + runs-on: ubuntu-latest + steps: + - name: "setup" + uses: "KyoriPowered/.github/.github/actions/setup-python-env@trunk" + - name: "setup / install reviewdog" + uses: "reviewdog/action-setup@v1.3.0" + with: + reviewdog_version: "latest" + - name: "setup / install deps" + id: "install" + run: "poetry install" + - name: "run ruff / apply format" + env: + REVIEWDOG_GITHUB_API_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + run: | + poetry run ruff format --diff | reviewdog -reporter=github-check -f=diff -f.diff.strip=0 + - name: "run ruff / check" + run: "poetry run ruff check --output-format=github" diff --git a/poetry.lock b/poetry.lock index dfee1f0..ffb7379 100644 --- a/poetry.lock +++ b/poetry.lock @@ -403,6 +403,34 @@ werkzeug = ">=3.0" [package.extras] dotenv = ["python-dotenv"] +[[package]] +name = "ruff" +version = "0.9.4" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "ruff-0.9.4-py3-none-linux_armv6l.whl", hash = "sha256:64e73d25b954f71ff100bb70f39f1ee09e880728efb4250c632ceed4e4cdf706"}, + {file = "ruff-0.9.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6ce6743ed64d9afab4fafeaea70d3631b4d4b28b592db21a5c2d1f0ef52934bf"}, + {file = "ruff-0.9.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:54499fb08408e32b57360f6f9de7157a5fec24ad79cb3f42ef2c3f3f728dfe2b"}, + {file = "ruff-0.9.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37c892540108314a6f01f105040b5106aeb829fa5fb0561d2dcaf71485021137"}, + {file = "ruff-0.9.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de9edf2ce4b9ddf43fd93e20ef635a900e25f622f87ed6e3047a664d0e8f810e"}, + {file = "ruff-0.9.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87c90c32357c74f11deb7fbb065126d91771b207bf9bfaaee01277ca59b574ec"}, + {file = "ruff-0.9.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:56acd6c694da3695a7461cc55775f3a409c3815ac467279dfa126061d84b314b"}, + {file = "ruff-0.9.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0c93e7d47ed951b9394cf352d6695b31498e68fd5782d6cbc282425655f687a"}, + {file = "ruff-0.9.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1d4c8772670aecf037d1bf7a07c39106574d143b26cfe5ed1787d2f31e800214"}, + {file = "ruff-0.9.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfc5f1d7afeda8d5d37660eeca6d389b142d7f2b5a1ab659d9214ebd0e025231"}, + {file = "ruff-0.9.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:faa935fc00ae854d8b638c16a5f1ce881bc3f67446957dd6f2af440a5fc8526b"}, + {file = "ruff-0.9.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a6c634fc6f5a0ceae1ab3e13c58183978185d131a29c425e4eaa9f40afe1e6d6"}, + {file = "ruff-0.9.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:433dedf6ddfdec7f1ac7575ec1eb9844fa60c4c8c2f8887a070672b8d353d34c"}, + {file = "ruff-0.9.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d612dbd0f3a919a8cc1d12037168bfa536862066808960e0cc901404b77968f0"}, + {file = "ruff-0.9.4-py3-none-win32.whl", hash = "sha256:db1192ddda2200671f9ef61d9597fcef89d934f5d1705e571a93a67fb13a4402"}, + {file = "ruff-0.9.4-py3-none-win_amd64.whl", hash = "sha256:05bebf4cdbe3ef75430d26c375773978950bbf4ee3c95ccb5448940dc092408e"}, + {file = "ruff-0.9.4-py3-none-win_arm64.whl", hash = "sha256:585792f1e81509e38ac5123492f8875fbc36f3ede8185af0a26df348e5154f41"}, + {file = "ruff-0.9.4.tar.gz", hash = "sha256:6907ee3529244bb0ed066683e075f09285b38dd5b4039370df6ff06041ca19e7"}, +] + [[package]] name = "sniffio" version = "1.3.1" @@ -517,4 +545,4 @@ h11 = ">=0.9.0,<1" [metadata] lock-version = "2.1" python-versions = ">= 3.12" -content-hash = "2862fc08a758d31345963fcecb68f6e3c5f8abddfbc4cb8a1458f77fc292b933" +content-hash = "c27ab978ee75c75079f7d75f98f0a5231757b092caca16abc8050f36f570cb4f" diff --git a/pyproject.toml b/pyproject.toml index bf1337a..3b2770c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,3 +20,12 @@ pydisgit = 'pydisgit:run_dev' [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" + +[tool.poetry.group.dev.dependencies] +ruff = "^0.9.4" + +[tool.ruff] +indent-width = 2 + +[tool.ruff.lint] +extend-select= ["I"] From 7a5c3b53ea789c86413e50ac02ce64f80b2add5c Mon Sep 17 00:00:00 2001 From: zml Date: Sat, 1 Feb 2025 20:28:44 -0800 Subject: [PATCH 02/10] fix naming in workflow --- .github/workflows/check-format.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/check-format.yaml b/.github/workflows/check-format.yaml index baccf64..ec80dec 100644 --- a/.github/workflows/check-format.yaml +++ b/.github/workflows/check-format.yaml @@ -1,4 +1,4 @@ -name: "build" +name: "check with ruff" on: push: @@ -8,12 +8,12 @@ on: permissions: contents: "read" - checks: write - issues: write - pull-requests: write + checks: "write" + issues: "write" + pull-requests: "write" jobs: - build: + check: # Only run on PRs if the source branch is on someone else's repo runs-on: ubuntu-latest steps: @@ -30,6 +30,6 @@ jobs: env: REVIEWDOG_GITHUB_API_TOKEN: "${{ secrets.GITHUB_TOKEN }}" run: | - poetry run ruff format --diff | reviewdog -reporter=github-check -f=diff -f.diff.strip=0 + poetry run ruff format --diff | reviewdog -reporter=github-check -f=diff -f.diff.strip=0 -name=ruff-format - name: "run ruff / check" run: "poetry run ruff check --output-format=github" From c6c2d94dd0a3ec977d14c28c49957c2254a6568b Mon Sep 17 00:00:00 2001 From: zml Date: Sat, 1 Feb 2025 20:29:27 -0800 Subject: [PATCH 03/10] chore: avoid duplicate workflow runs --- .github/workflows/check-format.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/check-format.yaml b/.github/workflows/check-format.yaml index ec80dec..3753807 100644 --- a/.github/workflows/check-format.yaml +++ b/.github/workflows/check-format.yaml @@ -15,6 +15,7 @@ permissions: jobs: check: # Only run on PRs if the source branch is on someone else's repo + if: "${{ github.event_name != 'pull_request' || github.repository != github.event.pull_request.head.repo.full_name }}" runs-on: ubuntu-latest steps: - name: "setup" From 1f8ea43d77bde58d8c65cf2e83e8d651e6f1b2b9 Mon Sep 17 00:00:00 2001 From: zml Date: Sat, 1 Feb 2025 20:33:30 -0800 Subject: [PATCH 04/10] chore: try to get output from reviewdog? --- .github/workflows/check-format.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/check-format.yaml b/.github/workflows/check-format.yaml index 3753807..b732077 100644 --- a/.github/workflows/check-format.yaml +++ b/.github/workflows/check-format.yaml @@ -31,6 +31,7 @@ jobs: env: REVIEWDOG_GITHUB_API_TOKEN: "${{ secrets.GITHUB_TOKEN }}" run: | - poetry run ruff format --diff | reviewdog -reporter=github-check -f=diff -f.diff.strip=0 -name=ruff-format + poetry run ruff format --diff | reviewdog -reporter=github-check -f=diff -f.diff.strip=0 -name=ruff-format -filter-mode=nofilter -fail-on-error - name: "run ruff / check" + if: "${{ always() && steps.install.conclusion == 'success' }}" run: "poetry run ruff check --output-format=github" From 33283bbca0c333ebddb6a0c1cde3215581eecb92 Mon Sep 17 00:00:00 2001 From: zml Date: Sat, 1 Feb 2025 20:39:19 -0800 Subject: [PATCH 05/10] try this? --- .github/workflows/check-format.yaml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/check-format.yaml b/.github/workflows/check-format.yaml index b732077..479441b 100644 --- a/.github/workflows/check-format.yaml +++ b/.github/workflows/check-format.yaml @@ -14,8 +14,6 @@ permissions: jobs: check: - # Only run on PRs if the source branch is on someone else's repo - if: "${{ github.event_name != 'pull_request' || github.repository != github.event.pull_request.head.repo.full_name }}" runs-on: ubuntu-latest steps: - name: "setup" @@ -31,7 +29,12 @@ jobs: env: REVIEWDOG_GITHUB_API_TOKEN: "${{ secrets.GITHUB_TOKEN }}" run: | - poetry run ruff format --diff | reviewdog -reporter=github-check -f=diff -f.diff.strip=0 -name=ruff-format -filter-mode=nofilter -fail-on-error + if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then + REPORTER="github-pr-review" + else + REPORTER="github-check" + fi + poetry run ruff format --diff | reviewdog -reporter=github-pr-revi -f=diff -f.diff.strip=0 -name=ruff-format -filter-mode=nofilter -fail-level=error - name: "run ruff / check" if: "${{ always() && steps.install.conclusion == 'success' }}" run: "poetry run ruff check --output-format=github" From f270f30281d24694629ce31d067a6371f779e306 Mon Sep 17 00:00:00 2001 From: zml Date: Sat, 1 Feb 2025 20:41:20 -0800 Subject: [PATCH 06/10] chore: ruff format --- src/pydisgit/__init__.py | 39 ++++-- src/pydisgit/conf.py | 27 ++-- src/pydisgit/handlers.py | 284 +++++++++++++++++++++------------------ src/pydisgit/hmac.py | 120 ++++++++++------- src/pydisgit/util.py | 4 +- src/pydisgit/webhook.py | 40 ++++-- 6 files changed, 295 insertions(+), 219 deletions(-) diff --git a/src/pydisgit/__init__.py b/src/pydisgit/__init__.py index 79c966e..3f01c82 100644 --- a/src/pydisgit/__init__.py +++ b/src/pydisgit/__init__.py @@ -18,21 +18,23 @@ app.asgi_app = HmacVerifyMiddleware(app.asgi_app, bound.github_webhook_secret) from .handlers import router as free_handler_router + handler_router = free_handler_router.bind(bound, app.logger) # http client + @app.before_serving async def setup_httpclient(): - app.http_client = AsyncClient( - headers={'User-Agent': 'pydisgit (kyori flavour)'} - ) + app.http_client = AsyncClient(headers={"User-Agent": "pydisgit (kyori flavour)"}) + @app.after_serving async def teardown_httpclient(): await app.http_client.aclose() -@app.get('/') + +@app.get("/") async def hello() -> str: """ root handler @@ -40,44 +42,53 @@ async def hello() -> str: return "begone foul beast", 400 -@app.post('//') +@app.post("//") async def gh_hook(hook_id: str, token: str) -> dict: event = request.headers["X-GitHub-Event"] if not event or not request.content_type: raise BadRequest("No event or content type") - #if (!(await validateRequest(request, env.githubWebhookSecret))) { + # if (!(await validateRequest(request, env.githubWebhookSecret))) { # return new Response('Invalid secret', { status: 403 }); - #} + # } if "application/json" in request.content_type: json = await request.json elif "application/x-www-form-urlencoded" in request.content_type: - json = json.loads((await request.form)['payload']) + json = json.loads((await request.form)["payload"]) else: raise BadRequest(f"Unknown content type {request.content_type}") embed = handler_router.process_request(event, json) if not embed: - return 'Webhook NO-OP', 200 + return "Webhook NO-OP", 200 - if app.config['DEBUG']: + if app.config["DEBUG"]: pprint.pprint(embed) pass # embed = await bound.buildDebugPaste(embed) http: AsyncClient = app.http_client - result = await http.post(f"https://discord.com/api/webhooks/{hook_id}/{token}", json = embed) + result = await http.post( + f"https://discord.com/api/webhooks/{hook_id}/{token}", json=embed + ) if result.status_code in (200, 204): result_text = "".join([await a async for a in result.aiter_text()]) - return {"message": f"We won! Webhook {hook_id} executed with token {token} :3, response: {result_text}"}, 200 + return { + "message": f"We won! Webhook {hook_id} executed with token {token} :3, response: {result_text}" + }, 200 else: - return Response(response = await result.aread(), status = result.status_code, content_type=result.headers['content-type'], headers=result.headers) + return Response( + response=await result.aread(), + status=result.status_code, + content_type=result.headers["content-type"], + headers=result.headers, + ) -@app.get('/health') +@app.get("/health") async def health_check() -> str: """ simple aliveness check diff --git a/src/pydisgit/conf.py b/src/pydisgit/conf.py index a8e8190..9d2f7cf 100644 --- a/src/pydisgit/conf.py +++ b/src/pydisgit/conf.py @@ -1,14 +1,17 @@ """ Configuration for pydisgit """ + from typing import Optional import logging import re + class Config: """ Raw environment from Workers """ + IGNORED_BRANCH_REGEX: str = "^$" IGNORED_BRANCHES: str = "" IGNORED_USERS: str = "" @@ -29,15 +32,17 @@ class BoundEnv: __ignored_users: list[str] __ignored_payloads: list[str] __pastegg_api_key: str - __github_webhook_secret: str; + __github_webhook_secret: str def __init__(self, env, logger): - self.__ignored_branch_pattern = re.compile(env['IGNORED_BRANCH_REGEX']) if 'IGNORED_BRANCH_REGEX' in env else None - self.__ignored_branches = env['IGNORED_BRANCHES'].split(",") - self.__ignored_users = env['IGNORED_USERS'].split(",") - self.__ignored_payloads = env['IGNORED_PAYLOADS'].split(",") - self.__pastegg_api_key = env['PASTE_GG_API_KEY'] - self.__github_webhook_secret = env['GITHUB_WEBHOOK_SECRET'] + self.__ignored_branch_pattern = ( + re.compile(env["IGNORED_BRANCH_REGEX"]) if "IGNORED_BRANCH_REGEX" in env else None + ) + self.__ignored_branches = env["IGNORED_BRANCHES"].split(",") + self.__ignored_users = env["IGNORED_USERS"].split(",") + self.__ignored_payloads = env["IGNORED_PAYLOADS"].split(",") + self.__pastegg_api_key = env["PASTE_GG_API_KEY"] + self.__github_webhook_secret = env["GITHUB_WEBHOOK_SECRET"] logger.info("Ignored branch pattern: %s", self.__ignored_branch_pattern) logger.info("Ignored branches: %s", self.__ignored_branches) @@ -45,9 +50,9 @@ def __init__(self, env, logger): logger.info("Ignored payloads: %s", self.__ignored_payloads) def ignored_branch(self, branch: str) -> bool: - return (self.__ignored_branch_pattern - and self.__ignored_branch_pattern.match(branch)) \ - or branch in self.__ignored_branches + return ( + self.__ignored_branch_pattern and self.__ignored_branch_pattern.match(branch) + ) or branch in self.__ignored_branches def ignored_user(self, user: str) -> bool: return user in self.__ignored_users @@ -61,6 +66,8 @@ def github_webhook_secret(self) -> str: async def build_debug_paste(self, embed: any) -> str: pass + + # embed = JSON.stringify({ # "files": [ # { diff --git a/src/pydisgit/handlers.py b/src/pydisgit/handlers.py index c82febe..7bafc05 100644 --- a/src/pydisgit/handlers.py +++ b/src/pydisgit/handlers.py @@ -1,34 +1,33 @@ """ GH event handlers """ + import logging from .conf import BoundEnv from .util import short_commit, truncate from .webhook import EmbedBody, Field, WebhookRouter -__slots__ = ['router'] +__slots__ = ["router"] logger = logging.getLogger(__name__) router = WebhookRouter() -@router.handler('ping') + +@router.handler("ping") def ping(zen, hook, repository, sender, organization=None) -> EmbedBody: - is_org = hook['type'] == 'Organization' - name = organization['login'] if is_org else repository['full_name'] + is_org = hook["type"] == "Organization" + name = organization["login"] if is_org else repository["full_name"] return EmbedBody( - f'[{name}] {hook['type']} hook ping received', - None, - sender, - 0xb8e98c, - zen + f"[{name}] {hook['type']} hook ping received", None, sender, 0xB8E98C, zen ) check_run_action = router.by_action("check_run") -@check_run_action('completed') + +@check_run_action("completed") # @router.filter(test = BoundEnv.ignored_branch, path = ['check_run', 'check_suite', 'head_branch']) # would this ever be nicer? than injecting the env as a parameter def check_completed(env: BoundEnv, check_run, repository, sender) -> EmbedBody: conclusion = check_run["conclusion"] @@ -46,32 +45,33 @@ def check_completed(env: BoundEnv, check_run, repository, sender) -> EmbedBody: if len(check_suite["pull_requests"]): pull = check_suite["pull_requests"][0] - if pull.url.startsWith(f'https://api.github.com/repos/{repository["full_name"]}'): - target = f'PR #{pull.number}' + if pull.url.startsWith(f"https://api.github.com/repos/{repository['full_name']}"): + target = f"PR #{pull.number}" - color = 0xaaaaaa + color = 0xAAAAAA status = "failed" match conclusion: case "success": - color = 0x00b32a + color = 0x00B32A status = "succeeded" case "failure" | "cancelled": - color = 0xff3b3b - status = "failed" if conclusion == "failure" else "cancelled" + color = 0xFF3B3B + status = "failed" if conclusion == "failure" else "cancelled" case "timed_out" | "action_required" | "stale": - color = 0xe4a723 + color = 0xE4A723 match conclusion: - case "timed_out": status = "timed out" - case "action_required": status = "requires action" - case _: status = "stale" + case "timed_out": + status = "timed out" + case "action_required": + status = "requires action" + case _: + status = "stale" case "neutral": status = "didn't run" case "skipped": status = "was skipped" - fields = [ - Field(name='Action Name', value = check_run["name"]) - ] + fields = [Field(name="Action Name", value=check_run["name"])] if "title" in output and output["title"]: fields.append(Field(name="Output Title", value=output["title"])) @@ -80,30 +80,32 @@ def check_completed(env: BoundEnv, check_run, repository, sender) -> EmbedBody: fields.append(Field(name="Output Summary", value=output["summary"])) return EmbedBody( - f"[{repository["full_name"]}] Actions check {status} on {target}", + f"[{repository['full_name']}] Actions check {status} on {target}", html_url, sender, color, - fields=fields + fields=fields, ) -commit_comment = router.by_action('commit_comment') -@commit_comment('created') +commit_comment = router.by_action("commit_comment") + + +@commit_comment("created") def commit_comment_created(env: BoundEnv, sender, comment, repository): if env.ignored_user(sender["login"]): return None return EmbedBody( - f"[{repository["full_name"]}] New comment on commit `{short_commit(comment["commit_id"])}`", + f"[{repository['full_name']}] New comment on commit `{short_commit(comment['commit_id'])}`", comment["html_url"], sender, 0x000001, - comment["body"] + comment["body"], ) -@router.handler('create') +@router.handler("create") def create_branch(env: BoundEnv, ref, ref_type, repository, sender): if env.ignored_user(sender["login"]): return None @@ -112,222 +114,224 @@ def create_branch(env: BoundEnv, ref, ref_type, repository, sender): return None return EmbedBody( - f"[{repository["full_name"]}] New {ref_type} created: {ref}", - None, - sender, - 0x000001 + f"[{repository['full_name']}] New {ref_type} created: {ref}", None, sender, 0x000001 ) -@router.handler('delete') +@router.handler("delete") def delete_branch(env: BoundEnv, ref, ref_type, repository, sender): if ref_type == "branch" and env.ignored_branch(ref): return None return EmbedBody( - f"[{repository["full_name"]}] {ref_type} deleted: {ref}", - None, - sender, - 0x000001 + f"[{repository['full_name']}] {ref_type} deleted: {ref}", None, sender, 0x000001 ) -discussion_action = router.by_action('discussion') +discussion_action = router.by_action("discussion") -@discussion_action('created') +@discussion_action("created") def discussion_created(env: BoundEnv, discussion, repository, sender): if env.ignored_user(sender.login): return None return EmbedBody( - f"[{repository["full_name"]}] New discussion: #{discussion["number"]} {discussion["title"]}", + f"[{repository['full_name']}] New discussion: #{discussion['number']} {discussion['title']}", discussion["html_url"], sender, - 0x9494ff, + 0x9494FF, discussion["body"], - f"Discussion Category: {discussion["category"]["name"]}" + f"Discussion Category: {discussion['category']['name']}", ) -discussion_comment_action = router.by_action('discussion_comment') +discussion_comment_action = router.by_action("discussion_comment") -@discussion_comment_action('created') +@discussion_comment_action("created") def discussion_comment_created(env: BoundEnv, discussion, comment, repository, sender): if env.ignored_user(sender["login"]): return None return EmbedBody( - f"[{repository["full_name"]}] New comment on discussion: #{discussion["number"]} {discussion["title"]}", + f"[{repository['full_name']}] New comment on discussion: #{discussion['number']} {discussion['title']}", comment["html_url"], sender, - 0x008a76, + 0x008A76, comment.body, - f"Discussion Category: {discussion["category"]["name"]}" + f"Discussion Category: {discussion['category']['name']}", ) @router.handler("fork") def fork(sender, repository, forkee): return EmbedBody( - f"[{repository["full_name"]}] Fork Created: {forkee["full_name"]}", + f"[{repository['full_name']}] Fork Created: {forkee['full_name']}", forkee["html_url"], sender, - 0xfcb900 + 0xFCB900, ) -issue_comment_action = router.by_action('issue_comment') -@issue_comment_action('created') +issue_comment_action = router.by_action("issue_comment") + + +@issue_comment_action("created") def issue_comment_created(env: BoundEnv, issue, comment, repository, sender): if env.ignored_user(sender["login"]): return None entity = "pull request" if "pull_request" in issue else "issue" return EmbedBody( - f"[{repository["full_name"]}] New comment on {entity}: #{issue["number"]} {issue["title"]}", + f"[{repository['full_name']}] New comment on {entity}: #{issue['number']} {issue['title']}", comment["html_url"], sender, - 0xad8b00, - comment["body"] + 0xAD8B00, + comment["body"], ) -issues_action = router.by_action('issues') +issues_action = router.by_action("issues") -@issues_action('opened') +@issues_action("opened") def issues_opened(env: BoundEnv, issue, repository, sender): if env.ignored_user(sender["login"]): return None return EmbedBody( - f"[{repository["full_name"]}] Issue opened: #{issue["number"]} {issue["title"]}", + f"[{repository['full_name']}] Issue opened: #{issue['number']} {issue['title']}", issue["html_url"], sender, - 0xff7d00, - issue["body"] + 0xFF7D00, + issue["body"], ) -@issues_action('reopened') +@issues_action("reopened") def issues_reopened(issue, repository, sender): return EmbedBody( - f"[{repository["full_name"]}] Issue reopened: #{issue["number"]} {issue["title"]}", + f"[{repository['full_name']}] Issue reopened: #{issue['number']} {issue['title']}", issue["html_url"], sender, - 0xff7d00 + 0xFF7D00, ) -@issues_action('closed') +@issues_action("closed") def issues_closed(issue, repository, sender): return EmbedBody( - f"[{repository["full_name"]}] Issue closed: #{issue["number"]} {issue["title"]}", + f"[{repository['full_name']}] Issue closed: #{issue['number']} {issue['title']}", issue["html_url"], sender, - 0xff482f + 0xFF482F, ) -package_action = router.by_action('package') +package_action = router.by_action("package") -@package_action('published') +@package_action("published") def package_published(sender, repository, package=None, registry_package=None): pkg = package if package else registry_package return EmbedBody( - f"[{repository["full_name"]}] Package Published: {pkg["namespace"]}/{pkg["name"]}", + f"[{repository['full_name']}] Package Published: {pkg['namespace']}/{pkg['name']}", pkg["package_version"]["html_url"], sender, - 0x009202 + 0x009202, ) -@package_action('updated') +@package_action("updated") def package_updated(sender, repository, package=None, registry_package=None): pkg = package if package else registry_package return EmbedBody( - f"[{repository["full_name"]}] Package Updated: {pkg["namespace"]}/{pkg["name"]}", + f"[{repository['full_name']}] Package Updated: {pkg['namespace']}/{pkg['name']}", pkg["package_version"]["html_url"], sender, - 0x9202 + 0x9202, ) -pull_request_action = router.by_action('pull_request') +pull_request_action = router.by_action("pull_request") -@pull_request_action('opened') +@pull_request_action("opened") def pull_request_opened(env: BoundEnv, pull_request, repository, sender): if env.ignored_user(sender["login"]): return None draft = pull_request["draft"] - color = 0xa7a7a7 if draft else 0x009202 + color = 0xA7A7A7 if draft else 0x009202 pr_type = "Draft pull request" if draft else "Pull request" return EmbedBody( - f"[{repository["full_name"]}] {pr_type} opened: #{pull_request["number"]} {pull_request["title"]}", + f"[{repository['full_name']}] {pr_type} opened: #{pull_request['number']} {pull_request['title']}", pull_request["html_url"], sender, - color + color, ) -@pull_request_action('closed') + +@pull_request_action("closed") def pull_request_closed(pull_request, repository, sender): merged = pull_request["merged"] - color = 0x8748ff if merged else 0xff293a + color = 0x8748FF if merged else 0xFF293A status = "merged" if merged else "closed" return EmbedBody( - f"[{repository["full_name"]}] Pull request {status}: #{pull_request["number"]} {pull_request["title"]}", + f"[{repository['full_name']}] Pull request {status}: #{pull_request['number']} {pull_request['title']}", pull_request["html_url"], sender, - color + color, ) -@pull_request_action('reopened') + +@pull_request_action("reopened") def pull_request_reopened(env: BoundEnv, pull_request, repository, sender): if env.ignored_user(sender["login"]): return None draft = pull_request["draft"] - color = 0xa7a7a7 if draft else 0x009202 + color = 0xA7A7A7 if draft else 0x009202 pr_type = "Draft pull request" if draft else "Pull request" return EmbedBody( - f"[{repository["full_name"]}] {pr_type} reopened: #{pull_request["number"]} {pull_request["title"]}", + f"[{repository['full_name']}] {pr_type} reopened: #{pull_request['number']} {pull_request['title']}", pull_request["html_url"], sender, - color + color, ) -@pull_request_action('converted_to_draft') + +@pull_request_action("converted_to_draft") def pull_request_converted_to_draft(pull_request, repository, sender): return EmbedBody( - f"[{repository["full_name"]}] Pull request marked as draft: #{pull_request["number"]} {pull_request["title"]}", + f"[{repository['full_name']}] Pull request marked as draft: #{pull_request['number']} {pull_request['title']}", pull_request["html_url"], sender, - 0xa7a7a7 + 0xA7A7A7, ) -@pull_request_action('ready_for_review') + +@pull_request_action("ready_for_review") def pull_request_ready_for_review(pull_request, repository, sender): return EmbedBody( - f"[{repository["full_name"]}] Pull request marked for review: #{pull_request["number"]} {pull_request["title"]}", + f"[{repository['full_name']}] Pull request marked for review: #{pull_request['number']} {pull_request['title']}", pull_request["html_url"], sender, - 0x009202 + 0x009202, ) -pull_request_review_action = router.by_action('pull_request_review') -@pull_request_review_action('submitted') -@pull_request_review_action('dismissed') +pull_request_review_action = router.by_action("pull_request_review") + + +@pull_request_review_action("submitted") +@pull_request_review_action("dismissed") def pull_request_review(pull_request, review, repository, action, sender): state = "reviewed" color = 7829367 @@ -344,27 +348,29 @@ def pull_request_review(pull_request, review, repository, action, sender): state = "review dismissed" return EmbedBody( - f"[{repository["full_name"]}] Pull request {state}: #{pull_request["number"]} {pull_request["title"]}", + f"[{repository['full_name']}] Pull request {state}: #{pull_request['number']} {pull_request['title']}", review["html_url"], sender, color, - review["body"] + review["body"], ) -pull_request_review_comment_action = router.by_action('pull_request_review_comment') +pull_request_review_comment_action = router.by_action("pull_request_review_comment") -@pull_request_review_comment_action('created') + +@pull_request_review_comment_action("created") def pull_request_review_comment_created(pull_request, comment, repository, sender): return EmbedBody( - f"[{repository["full_name"]}] Pull request review comment: #{pull_request["number"]} {pull_request["title"]}", + f"[{repository['full_name']}] Pull request review comment: #{pull_request['number']} {pull_request['title']}", comment["html_url"], sender, 0x777777, - comment["body"] + comment["body"], ) -@router.handler('push') + +@router.handler("push") def push(env: BoundEnv, commits, forced, after, repository, ref, compare, sender): branch = ref[11:] @@ -375,10 +381,10 @@ def push(env: BoundEnv, commits, forced, after, repository, ref, compare, sender if forced: return EmbedBody( - f"[{repository["full_name"]}] Branch {branch} was force-pushed to `{short_commit(after)}`", + f"[{repository['full_name']}] Branch {branch} was force-pushed to `{short_commit(after)}`", compare.replace("...", ".."), sender, - 0xff293a + 0xFF293A, ) amount = len(commits) @@ -389,7 +395,7 @@ def push(env: BoundEnv, commits, forced, after, repository, ref, compare, sender last_commit_url = "" for commit in commits: commit_url = commit["url"] - line = f"[`{short_commit(commit["id"])}`]({commit_url}) {truncate(commit["message"].split("\n")[0], 50)} - {commit["author"]["username"]}\n" + line = f"[`{short_commit(commit['id'])}`]({commit_url}) {truncate(commit['message'].split('\n')[0], 50)} - {commit['author']['username']}\n" if (len(description) + len(line)) >= 1500: break @@ -399,17 +405,19 @@ def push(env: BoundEnv, commits, forced, after, repository, ref, compare, sender commit_word = "commit" if amount == 1 else "commits" return EmbedBody( - f"[{repository["name"]}:{branch}] {amount} new {commit_word}", + f"[{repository['name']}:{branch}] {amount} new {commit_word}", last_commit_url if amount == 1 else compare, sender, - 0x5d62e4, - description + 0x5D62E4, + description, ) -release_action = router.by_action('release') -@release_action('released') -@release_action('prereleased') +release_action = router.by_action("release") + + +@release_action("released") +@release_action("prereleased") def release_released(release, repository, sender): if release["draft"]: return None @@ -419,50 +427,55 @@ def release_released(release, repository, sender): effective_name = release["tag_name"] return EmbedBody( - f"[{repository['full_name']}] New {'pre' if release["prerelease"] else ''}release published: {effective_name}", + f"[{repository['full_name']}] New {'pre' if release['prerelease'] else ''}release published: {effective_name}", release["html_url"], sender, - 0xde5de4, - release["body"] + 0xDE5DE4, + release["body"], ) -star_action = router.by_action('star') -@star_action('created') +star_action = router.by_action("star") + + +@star_action("created") def star_created(sender, repository): return EmbedBody( - f"[{repository["full_name"]}] New star added", + f"[{repository['full_name']}] New star added", repository["html_url"], sender, - 0xfcb900 + 0xFCB900, ) -deployment_action = router.by_action('deployment') -@deployment_action('created') +deployment_action = router.by_action("deployment") + + +@deployment_action("created") def deployment_created(deployment, repository, sender): web_url = deployment["payload"].get("web_url") if not web_url: web_url = "" return EmbedBody( - f"[{repository["full_name"]}] Deployment started for {deployment["description"]}", + f"[{repository['full_name']}] Deployment started for {deployment['description']}", web_url, sender, - 0xaa44b9 + 0xAA44B9, ) -@router.handler('deployment_status') + +@router.handler("deployment_status") def deployment_status(deployment, deployment_status, repository, sender): web_url = deployment["payload"].get("web_url") if not web_url: web_url = "" - color = 0xff3b3b + color = 0xFF3B3B term = "succeeded" match deployment_status["state"]: case "success": - color = 0x00b32a + color = 0x00B32A case "failure": term = "failed" case "error": @@ -471,12 +484,13 @@ def deployment_status(deployment, deployment_status, repository, sender): return None return EmbedBody( - f"[{repository["full_name"]}] Deployment for {deployment["description"]} {term}", + f"[{repository['full_name']}] Deployment for {deployment['description']} {term}", web_url, sender, - color + color, ) + @router.handler("gollum") def gollum(pages, sender, repository): # Pages is always an array with several "actions". @@ -493,7 +507,7 @@ def gollum(pages, sender, repository): edited += 1 # Wrap the title in a markdown with the link to the page. - title = f"[{page["title"]}](${page["html_url"]})" + title = f"[{page['title']}](${page['html_url']})" # Capitalize the first letter of the action, then prepend it to the title. titles.insert(0, f"{action[0].upper() + action[1:]}: {title}") @@ -510,18 +524,20 @@ def gollum(pages, sender, repository): case (1, 0): message = "A page was created" # Set the color to green. - color = 0x00b32a + color = 0x00B32A case (0, 1): message = "A page was edited" # Set the color to orange. - color = 0xfcb900 + color = 0xFCB900 case (a, b) if a > 0 and b > 0: - message = f"{created} page{"s" if created > 1 else ""} were created and {edited} {"were" if edited > 1 else "was"} edited" + message = f"{created} page{'s' if created > 1 else ''} were created and {edited} {'were' if edited > 1 else 'was'} edited" case _: - message = f"{max(created, edited)} pages were {"created" if created > 0 else "edited"}" + message = ( + f"{max(created, edited)} pages were {'created' if created > 0 else 'edited'}" + ) # Prepend the repository title to the message. - message = f"[{repository["full_name"]}] {message}" + message = f"[{repository['full_name']}] {message}" # Build the embed, with the sender as the author, the message as the title, and the edited pages as the description. return EmbedBody( diff --git a/src/pydisgit/hmac.py b/src/pydisgit/hmac.py index f2287ab..d6af9eb 100644 --- a/src/pydisgit/hmac.py +++ b/src/pydisgit/hmac.py @@ -9,64 +9,80 @@ logger = logging.getLogger(__name__) -class HmacVerifyMiddleware: - """ - Middleware to verify HMAC signatures inserted by GitHub. - """ - def __init__( - self, - app, - hmac_secret - ) -> None: - self.app = app - self.__hmac_secret = hmac_secret.encode() if hmac_secret else None - async def __call__(self, scope: Scope, receive: Callable, send: Callable) -> None: - if self.__hmac_secret is None or scope["type"] != "http" or len(scope["path"]) <= len("/health"): - await self.app(scope, receive, send) - return +class HmacVerifyMiddleware: + """ + Middleware to verify HMAC signatures inserted by GitHub. + """ - # processing an http connection - signature_header = None - for k, v in scope["headers"]: - if k.lower() == b"x-hub-signature-256": - signature_header = v - break + def __init__(self, app, hmac_secret) -> None: + self.app = app + self.__hmac_secret = hmac_secret.encode() if hmac_secret else None - if signature_header is None: - await self.__error_response__(receive, send, "No signature provided") - return + async def __call__(self, scope: Scope, receive: Callable, send: Callable) -> None: + if ( + self.__hmac_secret is None + or scope["type"] != "http" + or len(scope["path"]) <= len("/health") + ): + await self.app(scope, receive, send) + return - digest = hmac.new(key=self.__hmac_secret, digestmod='sha256') - await self.app(scope, self.recv_proxy(digest, signature_header[len("sha256="):], receive, send), send) + # processing an http connection + signature_header = None + for k, v in scope["headers"]: + if k.lower() == b"x-hub-signature-256": + signature_header = v + break + if signature_header is None: + await self.__error_response__(receive, send, "No signature provided") + return - def recv_proxy(self, digest: hmac.HMAC, expected: bytes, recv: Callable, send: Callable) -> Callable: - async def responder() -> dict: - response = await recv() - if response["type"] == "http.request": - digest.update(response["body"]) - if not response["more_body"]: - result = digest.hexdigest().encode() - if result != expected: - # send error response - logger.debug("Hash mismatch on request, got %s but expected %s", result, expected) - await self.__error_response__(recv, send, "Hash digest did not match expected") - return {"type": "http.disconnect"} - return response + digest = hmac.new(key=self.__hmac_secret, digestmod="sha256") + await self.app( + scope, + self.recv_proxy(digest, signature_header[len("sha256=") :], receive, send), + send, + ) - return responder + def recv_proxy( + self, digest: hmac.HMAC, expected: bytes, recv: Callable, send: Callable + ) -> Callable: + async def responder() -> dict: + response = await recv() + if response["type"] == "http.request": + digest.update(response["body"]) + if not response["more_body"]: + result = digest.hexdigest().encode() + if result != expected: + # send error response + logger.debug( + "Hash mismatch on request, got %s but expected %s", result, expected + ) + await self.__error_response__( + recv, send, "Hash digest did not match expected" + ) + return {"type": "http.disconnect"} + return response + return responder - async def __error_response__(self, recv: Callable, send: Callable, message: str) -> None: - bstr = message.encode() - await send({ - 'type': 'http.response.start', - 'status': 403, - 'headers': [(b'content-length', str(len(bstr)).encode())], - }) - await send({ - 'type': 'http.response.body', - 'body': bstr, - 'more_body': False, - }) + async def __error_response__( + self, recv: Callable, send: Callable, message: str + ) -> None: + bstr = message.encode() + await send( + { + "type": "http.response.start", + "status": 403, + "headers": [(b"content-length", str(len(bstr)).encode())], + } + ) + await send( + { + "type": "http.response.body", + "body": bstr, + "more_body": False, + } + ) diff --git a/src/pydisgit/util.py b/src/pydisgit/util.py index a2c1c59..01dfddd 100644 --- a/src/pydisgit/util.py +++ b/src/pydisgit/util.py @@ -14,7 +14,7 @@ def truncate(text: str, num: int) -> Optional[str]: if len(text) <= num: return text - return text[0:num - 3] + "..." + return text[0 : num - 3] + "..." def short_commit(hash: str) -> str: @@ -24,6 +24,8 @@ def short_commit(hash: str) -> str: # make into middleware async def validate_request(request, secret: str) -> bool: pass + + # signature_header = request.headers.get("X-Hub-Signature-256")?.substring("sha256=".length); # # if not signature_header: return False diff --git a/src/pydisgit/webhook.py b/src/pydisgit/webhook.py index c32681a..2b740c5 100644 --- a/src/pydisgit/webhook.py +++ b/src/pydisgit/webhook.py @@ -16,6 +16,7 @@ class Sender(NamedTuple): """ GH API Sender representation """ + login: str html_url: str avatar_url: str @@ -24,10 +25,12 @@ class Sender(NamedTuple): def from_json(cls, data: Any): return cls(data["login"], data["html_url"], data["avatar_url"]) + class Field(NamedTuple): """ Discord API field representation """ + name: str value: str inline: bool = True @@ -39,14 +42,16 @@ def to_json(self) -> Any: return { "name": self.name, "value": truncate(self.value, 1000), - "inline": self.inline + "inline": self.inline, } + @dataclass class EmbedBody: """ Discord API embed representation """ + title: str url: Optional[str] sender: Sender @@ -69,12 +74,14 @@ def to_json(self) -> Any: "author": { "name": truncate(self.sender.login, 255), "url": self.sender.html_url, - "icon_url": self.sender.avatar_url + "icon_url": self.sender.avatar_url, }, "color": self.color, "footer": { "text": truncate(self.footer, 255), - } if self.footer else None, + } + if self.footer + else None, "fields": [f.to_json() for f in self.fields], } ] @@ -83,8 +90,8 @@ def to_json(self) -> Any: type EventHandler = Callable[[BoundEnv, dict], Optional[EmbedBody]] -class BoundRouter: +class BoundRouter: def __init__(self, handlers: dict[str, EventHandler], env: BoundEnv, logger: Logger): self._handlers = handlers self._env = env @@ -104,13 +111,15 @@ def process_request(self, gh_hook_type: str, gh_data: dict) -> Optional[Any]: result = self._handlers[gh_hook_type](self._env, gh_data) if not result: - self._logger.debug("Produced no result for event type '%s' with payload '%s", gh_hook_type, gh_data) + self._logger.debug( + "Produced no result for event type '%s' with payload '%s", gh_hook_type, gh_data + ) return None return result.to_json() -class WebhookRouter(): +class WebhookRouter: __handlers: dict[str, EventHandler] = {} def bind(self, env: BoundEnv, logger: Logger) -> BoundRouter: @@ -121,7 +130,16 @@ def result(env, data): sig = inspect.signature(func) final_data = dict(data) # if we don't have a kwargs field, remove any extra attributes from the args map - if len([v for v in sig.parameters.values() if v.kind == inspect.Parameter.VAR_KEYWORD]) == 0: + if ( + len( + [ + v + for v in sig.parameters.values() + if v.kind == inspect.Parameter.VAR_KEYWORD + ] + ) + == 0 + ): for k in data.keys(): if k not in sig.parameters.keys(): del final_data[k] @@ -133,21 +151,26 @@ def result(env, data): # then call the actual handler return func(**final_data) + return result def handler(self, event: str) -> Callable: """ Decorator, function plus event name """ + def decorator(func): if event in self.__handlers: raise f"Already registered a handler for {event}!" self.__handlers[event] = self._wrap_func(func) return func + return decorator - def by_action(self, event: str) -> Callable[[str], Callable[[EventHandler], EventHandler]]: + def by_action( + self, event: str + ) -> Callable[[str], Callable[[EventHandler], EventHandler]]: """ Return a value that can be used as a decorator to dispatch handlers for ``event`` based on the provided action. @@ -168,6 +191,7 @@ def decorator(func: EventHandler) -> EventHandler: dispatchers[action] = self._wrap_func(func) return func + return decorator self.__handlers[event] = subhandler From 8578814665f0961db1dc390478a25b6e7426b119 Mon Sep 17 00:00:00 2001 From: zml Date: Sat, 1 Feb 2025 20:42:33 -0800 Subject: [PATCH 07/10] chore: ruff check --- src/pydisgit/__init__.py | 7 ++++--- src/pydisgit/conf.py | 3 +-- src/pydisgit/hmac.py | 1 + src/pydisgit/util.py | 4 +--- src/pydisgit/webhook.py | 4 ++-- 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/pydisgit/__init__.py b/src/pydisgit/__init__.py index 3f01c82..dc50228 100644 --- a/src/pydisgit/__init__.py +++ b/src/pydisgit/__init__.py @@ -1,9 +1,10 @@ -from httpx import AsyncClient import pprint + +from httpx import AsyncClient from quart import Quart, Response, request from werkzeug.exceptions import BadRequest -from .conf import Config, BoundEnv +from .conf import BoundEnv, Config from .hmac import HmacVerifyMiddleware Quart.__annotations__["http_client"] = AsyncClient @@ -17,7 +18,7 @@ bound = BoundEnv(app.config, app.logger) app.asgi_app = HmacVerifyMiddleware(app.asgi_app, bound.github_webhook_secret) -from .handlers import router as free_handler_router +from .handlers import router as free_handler_router # noqa: E402, I001 handler_router = free_handler_router.bind(bound, app.logger) diff --git a/src/pydisgit/conf.py b/src/pydisgit/conf.py index 9d2f7cf..f920350 100644 --- a/src/pydisgit/conf.py +++ b/src/pydisgit/conf.py @@ -2,9 +2,8 @@ Configuration for pydisgit """ -from typing import Optional -import logging import re +from typing import Optional class Config: diff --git a/src/pydisgit/hmac.py b/src/pydisgit/hmac.py index d6af9eb..9780874 100644 --- a/src/pydisgit/hmac.py +++ b/src/pydisgit/hmac.py @@ -5,6 +5,7 @@ import hmac import logging from collections.abc import Callable + from hypercorn.typing import Scope logger = logging.getLogger(__name__) diff --git a/src/pydisgit/util.py b/src/pydisgit/util.py index 01dfddd..6ac23f3 100644 --- a/src/pydisgit/util.py +++ b/src/pydisgit/util.py @@ -1,7 +1,5 @@ -from dataclasses import dataclass -from hmac import HMAC import re -from typing import NamedTuple, Optional +from typing import Optional __NEWLINE_REGEXP = re.compile(r"[\n|\r]*") diff --git a/src/pydisgit/webhook.py b/src/pydisgit/webhook.py index 2b740c5..016ea8c 100644 --- a/src/pydisgit/webhook.py +++ b/src/pydisgit/webhook.py @@ -2,11 +2,11 @@ Core logic for webhook handling """ +import inspect from collections.abc import Callable from dataclasses import dataclass, field from logging import Logger -from typing import Any, Optional, NamedTuple -import inspect +from typing import Any, NamedTuple, Optional from .conf import BoundEnv from .util import truncate From 2ce7fc705c9aa92087d76f507e8d86b83133ee62 Mon Sep 17 00:00:00 2001 From: zml Date: Sat, 1 Feb 2025 20:43:28 -0800 Subject: [PATCH 08/10] fix action variable use --- .github/workflows/check-format.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check-format.yaml b/.github/workflows/check-format.yaml index 479441b..89d75cf 100644 --- a/.github/workflows/check-format.yaml +++ b/.github/workflows/check-format.yaml @@ -34,7 +34,7 @@ jobs: else REPORTER="github-check" fi - poetry run ruff format --diff | reviewdog -reporter=github-pr-revi -f=diff -f.diff.strip=0 -name=ruff-format -filter-mode=nofilter -fail-level=error + poetry run ruff format --diff | reviewdog -reporter=$REPORTER -f=diff -f.diff.strip=0 -name=ruff-format -filter-mode=nofilter -fail-level=error - name: "run ruff / check" if: "${{ always() && steps.install.conclusion == 'success' }}" run: "poetry run ruff check --output-format=github" From c54b8f8b2cfd5b81f5ca77400cf27959bad4472a Mon Sep 17 00:00:00 2001 From: zml Date: Sat, 1 Feb 2025 20:44:56 -0800 Subject: [PATCH 09/10] fix format Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- src/pydisgit/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pydisgit/__init__.py b/src/pydisgit/__init__.py index dc50228..ae851ce 100644 --- a/src/pydisgit/__init__.py +++ b/src/pydisgit/__init__.py @@ -18,7 +18,7 @@ bound = BoundEnv(app.config, app.logger) app.asgi_app = HmacVerifyMiddleware(app.asgi_app, bound.github_webhook_secret) -from .handlers import router as free_handler_router # noqa: E402, I001 +from .handlers import router as free_handler_router # noqa: E402, I001 handler_router = free_handler_router.bind(bound, app.logger) From 318df8eca91a8d8b3a1d453ca2f67759afb13eec Mon Sep 17 00:00:00 2001 From: zml Date: Sat, 1 Feb 2025 21:03:42 -0800 Subject: [PATCH 10/10] configure pre-commit hooks --- .github/workflows/ci.yaml | 2 - .pre-commit-config.yaml | 20 +++ Dockerfile | 1 - README.md | 4 + etc/deployment/etc-sysconfig-pydisgit | 1 - poetry.lock | 190 +++++++++++++++++++++++++- pyproject.toml | 1 + 7 files changed, 214 insertions(+), 5 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 24362ee..b1a6c55 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -33,5 +33,3 @@ jobs: - name: "docker / push" if: "${{ github.event_name == 'push' && steps.setup.outputs.publishing_branch != ''}}" run: "docker push ghcr.io/kyoripowered/pydisgit" - - diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..7dffcab --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: +- repo: "https://github.com/pre-commit/pre-commit-hooks" + rev: "v5.0.0" + hooks: + - id: "trailing-whitespace" + - id: "end-of-file-fixer" + - id: "check-case-conflict" + - id: "check-yaml" +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: "v0.9.4" + hooks: + - id: "ruff" + args: [ "--fix" ] + - id: "ruff-format" +- repo: "https://github.com/python-poetry/poetry" + rev: "2.0.1" + hooks: + - id: "poetry-check" diff --git a/Dockerfile b/Dockerfile index 33af54c..abae3d1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,4 +30,3 @@ COPY --from=builder dist/ ./dist/ RUN pip install $(echo dist/*.whl) ENTRYPOINT [ "hypercorn", "asgi:pydisgit:app", "-k", "uvloop" ] - diff --git a/README.md b/README.md index 2bbbf30..ce3cfb8 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,10 @@ Some example unit files for deployment under Podman Quadlet with systemd socket We recommend choosing a webhook secret to prevent unauthorized users from exhausting the host server's available ratelimit space. +## contributing + +We welcome contributions! You'll need Python 3.12 or newer in your environment, and the [poetry](https://python-poetry.org) dependency manager installed. We also recommend installing and enabling [pre-commit](https://pre-commit.com/#install) to automatically resolve any formatting issues as you work on the project. We use the [ruff](https://docs.astral.sh/ruff) linter for code style, you may benefit from one of its editor plugins. + ## licensing pydisgit is released under the terms of the Apache Software License version 2.0. Thanks additionally go out to JRoy and all other contributors to upstream disgit for making it what it is today. diff --git a/etc/deployment/etc-sysconfig-pydisgit b/etc/deployment/etc-sysconfig-pydisgit index 02554f9..ed79cae 100644 --- a/etc/deployment/etc-sysconfig-pydisgit +++ b/etc/deployment/etc-sysconfig-pydisgit @@ -8,4 +8,3 @@ PYDISGIT_IGNORED_PAYLOADS='ping' # sekrit # PYDISGIT_GITHUB_WEBHOOK_SECRET='changeme' - diff --git a/poetry.lock b/poetry.lock index ffb7379..988979a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -58,6 +58,18 @@ files = [ {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, ] +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + [[package]] name = "click" version = "8.1.8" @@ -86,6 +98,35 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "distlib" +version = "0.3.9" +description = "Distribution utilities" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, + {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, +] + +[[package]] +name = "filelock" +version = "3.17.0" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338"}, + {file = "filelock-3.17.0.tar.gz", hash = "sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] +typing = ["typing-extensions (>=4.12.2)"] + [[package]] name = "flask" version = "3.1.0" @@ -233,6 +274,21 @@ files = [ {file = "hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08"}, ] +[[package]] +name = "identify" +version = "2.6.6" +description = "File identification library for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "identify-2.6.6-py2.py3-none-any.whl", hash = "sha256:cbd1810bce79f8b671ecb20f53ee0ae8e86ae84b557de31d89709dc2a48ba881"}, + {file = "identify-2.6.6.tar.gz", hash = "sha256:7bec12768ed44ea4761efb47806f0a41f86e7c0a5fdf5950d4648c90eca7e251"}, +] + +[package.extras] +license = ["ukkonen"] + [[package]] name = "idna" version = "3.10" @@ -349,6 +405,54 @@ files = [ {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, ] +[[package]] +name = "nodeenv" +version = "1.9.1" +description = "Node.js virtual environment builder" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +files = [ + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] + +[[package]] +name = "pre-commit" +version = "4.1.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pre_commit-4.1.0-py2.py3-none-any.whl", hash = "sha256:d29e7cb346295bcc1cc75fc3e92e343495e3ea0196c9ec6ba53f49f10ab6ae7b"}, + {file = "pre_commit-4.1.0.tar.gz", hash = "sha256:ae3f018575a588e30dfddfab9a05448bfbd6b73d78709617b5a2b853549716d4"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + [[package]] name = "priority" version = "2.0.0" @@ -376,6 +480,69 @@ files = [ [package.extras] cli = ["click (>=5.0)"] +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + [[package]] name = "quart" version = "0.20.0" @@ -509,6 +676,27 @@ dev = ["Cython (>=3.0,<4.0)", "setuptools (>=60)"] docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] test = ["aiohttp (>=3.10.5)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"] +[[package]] +name = "virtualenv" +version = "20.29.1" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "virtualenv-20.29.1-py3-none-any.whl", hash = "sha256:4e4cb403c0b0da39e13b46b1b2476e505cb0046b25f242bee80f62bf990b2779"}, + {file = "virtualenv-20.29.1.tar.gz", hash = "sha256:b8b8970138d32fb606192cb97f6cd4bb644fa486be9308fb9b63f81091b5dc35"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + [[package]] name = "werkzeug" version = "3.1.3" @@ -545,4 +733,4 @@ h11 = ">=0.9.0,<1" [metadata] lock-version = "2.1" python-versions = ">= 3.12" -content-hash = "c27ab978ee75c75079f7d75f98f0a5231757b092caca16abc8050f36f570cb4f" +content-hash = "b6054677ae3155ed9742fb728ea41caf705b14e1cc7d2ee47a0ea1a17cb8297a" diff --git a/pyproject.toml b/pyproject.toml index 3b2770c..e5b1cec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry.group.dev.dependencies] ruff = "^0.9.4" +pre-commit = "^4.1.0" [tool.ruff] indent-width = 2