diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a5259f --- /dev/null +++ b/.gitignore @@ -0,0 +1,163 @@ +### Python template +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +.pdm-build/ \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..f721a08 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,8 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: . + hooks: + - id: ruff + args: [ --fix ] + + - id: ruff-format \ No newline at end of file diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/README.md b/README.md new file mode 100644 index 0000000..5271848 --- /dev/null +++ b/README.md @@ -0,0 +1,200 @@ +# Hachitool + +Hachitool is a set of utilities that make it easier to work with Python scripts in GitHub Actions. + +## Installation + +Hachitool can be installed persistently like any other Python package: + +```shell +pip install hachitool +``` + +### Inline scripts + +Hachitool can be ephemerally installed for inline Python scripts via [uv](https://docs.astral.sh/uv): + +```yaml +- uses: astral-sh/setup-uv@v3 + +- shell: uv run --with hachitool python {0} + run: | + import hachitool + + # Do stuff here +``` + +### External scripts + +Hachitool can be emphemerally installed for external scripts via uv and +[inline script metadata](https://packaging.python.org/en/latest/specifications/inline-script-metadata/#inline-script-metadata): + +```python +# script.py + +# /// script +# dependencies = [ +# "hachitool", +# ] +# /// + +import hachitool + +# Do stuff here +``` + +```yaml +# workflow.yml + +- uses: astral-sh/setup-uv@v3 + +- run: uv run script.py +``` + +## Usage + +### `hachitool.set_output` + +Set output for a step. Takes either: + +- a key as its first argument and a value as its second +- a set of key-value pairs as either a dictionary or keyword arguments + +```python +import hachitool + +# All of these are equivalent +hachitool.set_output("key", "value") +hachitool.set_output({"key": "value"}) +hachitool.set_output(key="value") +``` + +### `hachitool.set_env` + +Set environment variables. Takes either: + +- a key as its first argument and a value as its second +- a set of key-value pairs as either a dictionary or keyword arguments + +```python +import hachitool + +# All of these are equivalent +hachitool.set_env("key", "value") +hachitool.set_env({"key": "value"}) +hachitool.set_env(key="value") +``` + +### `hachitool.add_path` + +Append something to the system path. + +```python +import hachitool + +hachitool.add_path("/absolute/or/relative/path") +``` + +### `hachitool.summary` + +Add content to +the [step summary](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#adding-a-job-summary). +You can call this function multiple times; the result is cumulative. Content added to the summary cannot be removed. + +```python +import hachitool + +hachitool.summary("this is a summary") +``` + +### `hachitool.mask` + +[Mask a value](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#masking-a-value-in-a-log). + +```python +import hachitool + +hachitool.mask("super secret value") +``` + +### `hachitool.log` + +Print a message to the log. Takes the following arguments: + +| **Argument** | **Type** | **Description** | **Required?** | +|--------------|-----------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------|---------------| +| `level` | `"debug"` \| `"notice"` \| `"warning"` \| `"error"` | The log level of the message. | Yes | +| `message` | `str` | The message to print. | Yes | +| `file` | `str` | The path to a file to annotate with the message. | No | +| `line` | `int` \| `tuple[int, int]` | The line(s) of `file` to annotate with the message. A tuple will be interpreted as a pair of starting and ending lines. | No | +| `column` | `int` \| `tuple[int, int]` | The column(s) of `file` to annotate with the message. A tuple will be interpreted as a pair of starting and ending columns. | No | | + +`level` and `message` are the first and second positional arguments, respectively. +`file`, `line`, and `column` are keyword-only. + +```python +import hachitool + +hachitool.log("notice", "this is a notice message", file="main.py", line=1, column=6) + +# Using tuples for `line` and `column` +hachitool.log("notice", "this is a notic message", file="main.py", line=(1, 5), column=(6, 10)) +``` + +### `hachitool.debug`, `hachitool.notice`, `hachitool.warning`, `hachitool.error` + +Print a `debug`, `notice`, `warning`, or `error` message to the console, respectively. Takes the same arguments +as `hachitool.log` excpet for `level`. + +```python +import hachitool + +hachitool.debug("this is a debug message") +hachitool.notice("this is a notice message") +hachitool.warning("this is a warning message") +hachitool.error("this is an error message") +``` + +### `hachitool.fail` + +Optionally prints an error-level message, then fails the workflow. Takes an optional `exit_code` argument +that must be an integer greater than or equal to 1. Additionally takes all arguments of `hachitool.error`, +except `message` is optional. + +```python +import hachitool + +hachitool.fail("something went wrong", exit_code=1) +``` + +### `hachitool.log_group` + +Anything printed to the log inside this context manager will be printed as +an [expandable group](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#grouping-log-lines). + +Takes a mandatory `title` argument. + +```python +import hachitool + +with hachitool.log_group("group title"): + print("I'm part of a log group!") + print("me too!") + print("me three!") +``` + +### `hachitool.literal` + +Nothing printed to the log inside this context manager will be interpreted as a workflow command. + +```python +import hachitool + +with hachitool.literal(): + hachitool.mask("this doesn't work properly because GitHub won't interpret it as a workflow command") + hachitool.debug("neither does this") + hachitool.notice("or this") + hachitool.warning("or this") + hachitool.error("or this") + hachitool.fail("this will still fail the workflow but the error message won't print correctly") +``` \ No newline at end of file diff --git a/hachitool/__init__.py b/hachitool/__init__.py new file mode 100644 index 0000000..37cdf38 --- /dev/null +++ b/hachitool/__init__.py @@ -0,0 +1,2 @@ +from .commands import * # noqa: F403 +from .files import * # noqa: F403 diff --git a/hachitool/commands.py b/hachitool/commands.py new file mode 100644 index 0000000..01bdc06 --- /dev/null +++ b/hachitool/commands.py @@ -0,0 +1,85 @@ +import typing as t +import uuid +from contextlib import contextmanager + +import pydantic.alias_generators +from pydantic import Field, validate_call + +__all__ = ["log", "debug", "notice", "warning", "error", "fail", "mask", "log_group", "literal"] + + +class LogParams(t.TypedDict, total=False): + title: str + file: str + line: int | tuple[int, int] + column: int | tuple[int, int] + + +@validate_call +def log( + level: t.Literal["debug", "notice", "warning", "error"], + message: str, + **params: t.Unpack[LogParams], +): + + for key in ["line", "column"]: + if isinstance(params.get(key), tuple): + params[key], params[f"end_{key}"] = params[key] + + if params.get("column"): + params["col"] = params.pop("column") + + param_str = ",".join( + [f"{pydantic.alias_generators.to_camel(k)}={v}" for k, v in params.items()] + ) + + cmd = level + + if param_str: + cmd += " " + param_str + + print(f"::{cmd}::{message}") + + +def debug(message: str, **params): + log("debug", message, **params) + + +def notice(message: str, **params): + log("notice", message, **params) + + +def warning(message: str, **params): + log("warning", message, **params) + + +def error(message: str, **params): + log("error", message, **params) + + +def fail(message: str = None, /, exit_code: t.Annotated[int, Field(ge=1)] = 1, **params): + if message: + error(message, **params) + + exit(exit_code) + + +def mask(value: str): + print(f"::add-mask::{value}") + + +@contextmanager +def log_group(title: str): + print(f"::group::{title}") + yield + print("::endgroup::") + + +@contextmanager +def literal(): + signal = uuid.uuid4().hex + + print(f"::stop-commands::{signal}") + yield + print(f"::{signal}::") + diff --git a/hachitool/commands.pyi b/hachitool/commands.pyi new file mode 100644 index 0000000..44802b4 --- /dev/null +++ b/hachitool/commands.pyi @@ -0,0 +1,25 @@ +import typing as t + +def log(level: t.Literal["debug", "notice", "warning", "error"], message: str, /, title: str = None, file: str = None, line: int | tuple[int, int] = None, column: int | tuple[int, int] = None): + ... + +def debug(message: str, /, title: str = None, file: str = None, line: int | tuple[int, int] = None, column: int | tuple[int, int] = None): + ... + +def notice(message: str, /, title: str = None, file: str = None, line: int | tuple[int, int] = None, column: int | tuple[int, int] = None): + ... + +def warning(message: str, /, title: str = None, file: str = None, line: int | tuple[int, int] = None, column: int | tuple[int, int] = None): + ... + +def error(message: str, /, title: str = None, file: str = None, line: int | tuple[int, int] = None, column: int | tuple[int, int] = None): + ... + +def fail(message: str, /, exit_code: int = 1, title: str = None, file: str = None, line: int | tuple[int, int] = None, column: int | tuple[int, int] = None): + ... + +def mask(value: str): ... + +def log_group(title: str): ... + +def literal(): ... \ No newline at end of file diff --git a/hachitool/files.py b/hachitool/files.py new file mode 100644 index 0000000..78c3f23 --- /dev/null +++ b/hachitool/files.py @@ -0,0 +1,54 @@ +import os +from enum import StrEnum +from pathlib import Path +import typing as t + +from multimethod import multimethod +from pydantic import validate_call + +__all__ = ["set_output", "set_env", "add_path", "summary"] + + +class File(StrEnum): + OUTPUT = "OUTPUT" + ENV = "ENV" + PATH = "PATH" + SUMMARY = "STEP_SUMMARY" + + def write(self, content: str): + fp = Path(os.getenv("GITHUB_" + self.value)) + fp.open("a", newline="\n").write(content) + + +@multimethod +def _write_kv(file: File, key: t.Any, value: t.Any): + file.write(f"{key}={value}") + + +@multimethod +def _write_kv(file: File, data: dict): + for key, value in data.items(): + _write_kv(file, key, value) + + +@multimethod +def _write_kv(file: File, **kwargs): + _write_kv(file, kwargs) + + +def set_output(*args, **kwargs): + _write_kv(File.OUTPUT, *args, **kwargs) + + +def set_env(*args, **kwargs): + _write_kv(File.ENV, *args, **kwargs) + + +@validate_call +def add_path(path: Path): + File.PATH.write(str(path)) + + +@validate_call +def summary(content: str): + File.SUMMARY.write(content) diff --git a/hachitool/files.pyi b/hachitool/files.pyi new file mode 100644 index 0000000..faf27db --- /dev/null +++ b/hachitool/files.pyi @@ -0,0 +1,26 @@ +import os +import typing as t +from os import PathLike +from pathlib import Path + +@t.overload +def set_output(name: str, value: str): ... + +@t.overload +def set_output(data: dict): ... + +@t.overload +def set_output(**kwargs): ... + +@t.overload +def set_env(name: str, value: str): ... + +@t.overload +def set_env(data: dict): ... + +@t.overload +def set_env(**kwargs): ... + +def add_path(path: str | os.PathLike): ... + +def summary(content: str): ... \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..65c0807 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,17 @@ +[project] +name = "hachitool" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +authors = [ + { name = "celsius narhwal", email = "hello@celsiusnarhwal.dev" } +] +requires-python = ">=3.12" +dependencies = [ + "multimethod>=1.12", + "pydantic>=2.10.3", +] + +[build-system] +requires = ["pdm-backend"] +build-backend = "pdm.backend" diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..176d22a --- /dev/null +++ b/uv.lock @@ -0,0 +1,97 @@ +version = 1 +requires-python = ">=3.12" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "hachitool" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "multimethod" }, + { name = "pydantic" }, +] + +[package.metadata] +requires-dist = [ + { name = "multimethod", specifier = ">=1.12" }, + { name = "pydantic", specifier = ">=2.10.3" }, +] + +[[package]] +name = "multimethod" +version = "1.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/f3/930a6dc1d35b2ab65faffa2a75bbcc67f12d8227857188273783df4e5134/multimethod-1.12.tar.gz", hash = "sha256:8db8ef2a8d2a247e3570cc23317680892fdf903d84c8c1053667c8e8f7671a67", size = 17423 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/98/cff14d53a2f2f67d7fe8a4e235a383ee71aba6a1da12aeea24b325d0c72a/multimethod-1.12-py3-none-any.whl", hash = "sha256:fd0c473c43558908d97cc06e4d68e8f69202f167db46f7b4e4058893e7dbdf60", size = 10646 }, +] + +[[package]] +name = "pydantic" +version = "2.10.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/0f/27908242621b14e649a84e62b133de45f84c255eecb350ab02979844a788/pydantic-2.10.3.tar.gz", hash = "sha256:cb5ac360ce894ceacd69c403187900a02c4b20b693a9dd1d643e1effab9eadf9", size = 786486 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/51/72c18c55cf2f46ff4f91ebcc8f75aa30f7305f3d726be3f4ebffb4ae972b/pydantic-2.10.3-py3-none-any.whl", hash = "sha256:be04d85bbc7b65651c5f8e6b9976ed9c6f41782a55524cef079a34a0bb82144d", size = 456997 }, +] + +[[package]] +name = "pydantic-core" +version = "2.27.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/9f/7de1f19b6aea45aeb441838782d68352e71bfa98ee6fa048d5041991b33e/pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235", size = 412785 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/51/2e9b3788feb2aebff2aa9dfbf060ec739b38c05c46847601134cc1fed2ea/pydantic_core-2.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f", size = 1895239 }, + { url = "https://files.pythonhosted.org/packages/7b/9e/f8063952e4a7d0127f5d1181addef9377505dcce3be224263b25c4f0bfd9/pydantic_core-2.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02", size = 1805070 }, + { url = "https://files.pythonhosted.org/packages/2c/9d/e1d6c4561d262b52e41b17a7ef8301e2ba80b61e32e94520271029feb5d8/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c", size = 1828096 }, + { url = "https://files.pythonhosted.org/packages/be/65/80ff46de4266560baa4332ae3181fffc4488ea7d37282da1a62d10ab89a4/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac", size = 1857708 }, + { url = "https://files.pythonhosted.org/packages/d5/ca/3370074ad758b04d9562b12ecdb088597f4d9d13893a48a583fb47682cdf/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb", size = 2037751 }, + { url = "https://files.pythonhosted.org/packages/b1/e2/4ab72d93367194317b99d051947c071aef6e3eb95f7553eaa4208ecf9ba4/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529", size = 2733863 }, + { url = "https://files.pythonhosted.org/packages/8a/c6/8ae0831bf77f356bb73127ce5a95fe115b10f820ea480abbd72d3cc7ccf3/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35", size = 2161161 }, + { url = "https://files.pythonhosted.org/packages/f1/f4/b2fe73241da2429400fc27ddeaa43e35562f96cf5b67499b2de52b528cad/pydantic_core-2.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089", size = 1993294 }, + { url = "https://files.pythonhosted.org/packages/77/29/4bb008823a7f4cc05828198153f9753b3bd4c104d93b8e0b1bfe4e187540/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381", size = 2001468 }, + { url = "https://files.pythonhosted.org/packages/f2/a9/0eaceeba41b9fad851a4107e0cf999a34ae8f0d0d1f829e2574f3d8897b0/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb", size = 2091413 }, + { url = "https://files.pythonhosted.org/packages/d8/36/eb8697729725bc610fd73940f0d860d791dc2ad557faaefcbb3edbd2b349/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae", size = 2154735 }, + { url = "https://files.pythonhosted.org/packages/52/e5/4f0fbd5c5995cc70d3afed1b5c754055bb67908f55b5cb8000f7112749bf/pydantic_core-2.27.1-cp312-none-win32.whl", hash = "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c", size = 1833633 }, + { url = "https://files.pythonhosted.org/packages/ee/f2/c61486eee27cae5ac781305658779b4a6b45f9cc9d02c90cb21b940e82cc/pydantic_core-2.27.1-cp312-none-win_amd64.whl", hash = "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16", size = 1986973 }, + { url = "https://files.pythonhosted.org/packages/df/a6/e3f12ff25f250b02f7c51be89a294689d175ac76e1096c32bf278f29ca1e/pydantic_core-2.27.1-cp312-none-win_arm64.whl", hash = "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e", size = 1883215 }, + { url = "https://files.pythonhosted.org/packages/0f/d6/91cb99a3c59d7b072bded9959fbeab0a9613d5a4935773c0801f1764c156/pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073", size = 1895033 }, + { url = "https://files.pythonhosted.org/packages/07/42/d35033f81a28b27dedcade9e967e8a40981a765795c9ebae2045bcef05d3/pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08", size = 1807542 }, + { url = "https://files.pythonhosted.org/packages/41/c2/491b59e222ec7e72236e512108ecad532c7f4391a14e971c963f624f7569/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf", size = 1827854 }, + { url = "https://files.pythonhosted.org/packages/e3/f3/363652651779113189cefdbbb619b7b07b7a67ebb6840325117cc8cc3460/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737", size = 1857389 }, + { url = "https://files.pythonhosted.org/packages/5f/97/be804aed6b479af5a945daec7538d8bf358d668bdadde4c7888a2506bdfb/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2", size = 2037934 }, + { url = "https://files.pythonhosted.org/packages/42/01/295f0bd4abf58902917e342ddfe5f76cf66ffabfc57c2e23c7681a1a1197/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107", size = 2735176 }, + { url = "https://files.pythonhosted.org/packages/9d/a0/cd8e9c940ead89cc37812a1a9f310fef59ba2f0b22b4e417d84ab09fa970/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51", size = 2160720 }, + { url = "https://files.pythonhosted.org/packages/73/ae/9d0980e286627e0aeca4c352a60bd760331622c12d576e5ea4441ac7e15e/pydantic_core-2.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a", size = 1992972 }, + { url = "https://files.pythonhosted.org/packages/bf/ba/ae4480bc0292d54b85cfb954e9d6bd226982949f8316338677d56541b85f/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc", size = 2001477 }, + { url = "https://files.pythonhosted.org/packages/55/b7/e26adf48c2f943092ce54ae14c3c08d0d221ad34ce80b18a50de8ed2cba8/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960", size = 2091186 }, + { url = "https://files.pythonhosted.org/packages/ba/cc/8491fff5b608b3862eb36e7d29d36a1af1c945463ca4c5040bf46cc73f40/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23", size = 2154429 }, + { url = "https://files.pythonhosted.org/packages/78/d8/c080592d80edd3441ab7f88f865f51dae94a157fc64283c680e9f32cf6da/pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05", size = 1833713 }, + { url = "https://files.pythonhosted.org/packages/83/84/5ab82a9ee2538ac95a66e51f6838d6aba6e0a03a42aa185ad2fe404a4e8f/pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337", size = 1987897 }, + { url = "https://files.pythonhosted.org/packages/df/c3/b15fb833926d91d982fde29c0624c9f225da743c7af801dace0d4e187e71/pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5", size = 1882983 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +]