diff --git a/.basedpyright/baseline.json b/.basedpyright/baseline.json new file mode 100644 index 0000000..03b55fc --- /dev/null +++ b/.basedpyright/baseline.json @@ -0,0 +1,3 @@ +{ + "files": {} +} \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d84afbe --- /dev/null +++ b/Makefile @@ -0,0 +1,37 @@ +USER_GROUP := $(shell id -u):$(shell id -g) + +ifeq ($(OS),Windows_NT) + detected_OS := Windows +else + detected_OS := $(shell uname -s) +endif + +.PHONY: format +format: image + docker run --rm -u ${USER_GROUP} -v "$(CURDIR):/app" -w /app interuss/geospatial-utils uv run ruff format + docker run --rm -u ${USER_GROUP} -v "$(CURDIR):/app" -w /app interuss/geospatial-utils uv run ruff check --fix + docker run --rm -u ${USER_GROUP} -v "$(CURDIR):/app" -w /app interuss/geospatial-utils uv run basedpyright + + +.PHONY: lint +lint: shell-lint python-lint + + +.PHONY: python-lint +python-lint: image + docker run --rm -u ${USER_GROUP} -v "$(CURDIR):/app" -w /app interuss/geospatial-utils uv run ruff format --check || (echo "Linter didn't succeed. You can use the following command to fix python linter issues: make format" && exit 1) + docker run --rm -u ${USER_GROUP} -v "$(CURDIR):/app" -w /app interuss/geospatial-utils uv run ruff check || (echo "Linter didn't succeed. You can use the following command to fix python linter issues: make format" && exit 1) + shasum -b -a 256 .basedpyright/baseline.json > /tmp/baseline-before.hash + docker run --rm -u ${USER_GROUP} -v "$(CURDIR):/app" -w /app interuss/geospatial-utils uv run basedpyright || (echo "Typing check didn't succeed. Please fix issue and run make format to validate changes." && exit 1) + shasum -b -a 256 .basedpyright/baseline.json > /tmp/baseline-after.hash + diff /tmp/baseline-before.hash /tmp/baseline-after.hash || (echo "Basedpyright baseline changed, probably dues to issues that have been cleanup. Use the following command to update baseline: make format" && exit 1) + + +.PHONY: shell-lint +shell-lint: + find . -type f -name '*.sh' ! -path './.*' | xargs docker run --rm -v "$(CURDIR):/geospatial-utils" -w /geospatial-utils koalaman/shellcheck + + +.PHONY: image +image: + cd geospatial-utils && make image \ No newline at end of file diff --git a/geospatial-utils/Dockerfile b/geospatial-utils/Dockerfile new file mode 100644 index 0000000..2af6fd7 --- /dev/null +++ b/geospatial-utils/Dockerfile @@ -0,0 +1,68 @@ +# Dockerfile for interuss/geospatial-utils +# +# The image generated by this Dockerfile (via ./build.sh) includes the entire +# `geospatial-utils` folder contents in /app/geospatial-utils and has installed dependencies +# necessary to run any of the geospatial-utils tools. +# +# This image is intended to be built from the repository root context/folder. + +FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim +# Not -alpine because: https://stackoverflow.com/a/58028091/651139 + +# Install system tools +# openssl: Provides TLS tools +# curl: Useful debugging utility +# gcc: Required to build various packages +# ca-certificates: Needed to accurately validate TLS connections +RUN apt-get update --fix-missing && apt-get install -y make openssl curl libgeos-dev gcc g++ && apt-get install ca-certificates + +# Required to build in an ARM environment +# gevent: libffi-dev libssl-dev python3-dev build-essential +# lxml: libxml2-dev libxslt-dev +# h5py: pkg-config libhdf5-dev +RUN if [ "$(uname -m)" = "aarch64" ]; then \ + apt-get install -y libffi-dev libssl-dev python3-dev build-essential libxml2-dev libxslt-dev pkg-config libhdf5-dev \ +; fi + +RUN mkdir -p /app/ + +# Install dependencies +ENV UV_LINK_MODE=copy +ENV UV_PROJECT_ENVIRONMENT=/venv/ +ENV VIRTUAL_ENV=/venv/ +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=bind,source=./uv.lock,target=/app/uv.lock \ + --mount=type=bind,source=./pyproject.toml,target=/app/pyproject.toml \ + cd /app && \ + uv sync --locked --no-install-project --compile-bytecode + +# Ensure UV don't check dependencies or lock file, since we installed everything before +ENV UV_FROZEN=1 + +# Ensure the cache folder is present and writable by anyone +# (Some command run with limited privileges) +RUN mkdir -p /.cache/uv/ && chmod 777 /.cache/uv/ +# Also fix the root folder where python is downloaded +RUN find /root/ -type d -exec chmod a+rx {} + + +# Start in this folder +WORKDIR /app/geospatial-utils + +# Add core content from repo +ADD ./geospatial-utils /app/geospatial-utils + +# Discover `geospatial-utils` module in Python +ENV PYTHONPATH=/app + +# Add venv to path +ENV PATH="/venv/bin/:$PATH" + +# This image should be built by passing in `version` and `commit_hash` based on information from git (see `make image`) +# This version information becomes available in the environment variables specified below +ARG version +ARG commit_hash +ENV GEOSPATIAL_UTILS_VERSION=$version +ENV GIT_COMMIT_HASH=$commit_hash + +# No entry point maximizes flexibility in the use of this image +ENTRYPOINT [] \ No newline at end of file diff --git a/geospatial-utils/Makefile b/geospatial-utils/Makefile new file mode 100644 index 0000000..19d17a0 --- /dev/null +++ b/geospatial-utils/Makefile @@ -0,0 +1,3 @@ +image: ../uv.lock ../pyproject.toml $(shell find . -type f ! -name image ! -name *.pyc) + # Building image due to changes in the following files: $? + ./build.sh diff --git a/geospatial-utils/build.sh b/geospatial-utils/build.sh new file mode 100755 index 0000000..3bc42ed --- /dev/null +++ b/geospatial-utils/build.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +set -eo pipefail + +# Find and change to repo root directory +OS=$(uname) +if [[ "$OS" == "Darwin" ]]; then + # OSX uses BSD readlink + BASEDIR="$(dirname "$0")" +else + BASEDIR=$(readlink -e "$(dirname "$0")") +fi +cd "${BASEDIR}/.." || exit 1 + +TAG="${1:-interuss/geospatial-utils}" + +docker image build \ + -f geospatial-utils/Dockerfile \ + -t "${TAG}" \ + --build-arg version="$(scripts/git/version.sh geospatial-utils --long)" \ + --build-arg commit_hash="$(git rev-parse HEAD)" \ + . \ + || exit 1 +echo "File created by geospatial-utils/build.sh to keep track of the latest build run date time." > geospatial-utils/image \ No newline at end of file diff --git a/geospatial-utils/image b/geospatial-utils/image new file mode 100644 index 0000000..7a3e505 --- /dev/null +++ b/geospatial-utils/image @@ -0,0 +1 @@ +File created by geospatial-utils/build.sh to keep track of the latest build run date time. diff --git a/geospatial-utils/main.py b/geospatial-utils/main.py new file mode 100644 index 0000000..b025b9b --- /dev/null +++ b/geospatial-utils/main.py @@ -0,0 +1,13 @@ +import os + +from loguru import logger + +version = os.environ.get("GEOSPATIAL_UTILS_VERSION", "unknown") + + +def main(): + logger.info(f"Geospatial utils - {version}") + + +if __name__ == "__main__": + main() diff --git a/geospatial-utils/run_locally.sh b/geospatial-utils/run_locally.sh new file mode 100755 index 0000000..4839c8a --- /dev/null +++ b/geospatial-utils/run_locally.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash + +set -eo pipefail + +# Find and change to repo root directory +OS=$(uname) +if [[ "$OS" == "Darwin" ]]; then + # OSX uses BSD readlink + BASEDIR="$(dirname "$0")" +else + BASEDIR=$(readlink -e "$(dirname "$0")") +fi +cd "${BASEDIR}/.." || exit 1 + +( +cd geospatial-utils || exit 1 +make image +) + +# https://stackoverflow.com/a/9057392 +# shellcheck disable=SC2124 +OTHER_ARGS=${@:2} + +if [ "$CI" == "true" ]; then + docker_args="" +else + docker_args="-it" +fi + +# shellcheck disable=SC2086 +docker run ${docker_args} --name geospatial-utils \ + --rm \ + --network interop_ecosystem_network \ + --add-host=host.docker.internal:host-gateway \ + -u "$(id -u):$(id -g)" \ + -e PYTHONBUFFERED=1 \ + -w /app/geospatial-utils \ + interuss/geospatial-utils \ + uv run main.py $OTHER_ARGS \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f50a6ed --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,30 @@ +[project] +name = "geospatial-utils" +version = "0.1.0" +description = "Set of geospatial utilies" +readme = "README.md" +requires-python = "==3.13.5" +dependencies = [ + "basedpyright>=1.31.1", + "loguru>=0.7.3", + "ruff", +] + +[tool.ruff] +target-version = "py313" + +# Default + isort + pyupgrade +lint.select = [ + "E4", "E7", "E9", "F", "I", "UP" +] +extend-exclude = [ +] +line-length = 88 + +[tool.basedpyright] +typeCheckingMode = "standard" + +exclude = [ + "**/__pycache__", + "**/.*", +] diff --git a/scripts/git/upstream_owner.sh b/scripts/git/upstream_owner.sh new file mode 100755 index 0000000..39e53c5 --- /dev/null +++ b/scripts/git/upstream_owner.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +# This script attempts to print the organization of the upstream repository. + +# The following strategies will be used to determine the organization name: +# 1. If a remote named `origin` exists, the organization name will be extracted from the +# remote URL assuming the format below. +# 2. If a remote named `interuss` exists, the organization name will be extracted from the +# remote URL assuming the format below. +# 3. If the upstream of the current branch exists, the organization name will be set to the +# upstream repo name. +# 4. Otherwise, the default organization name of "unknown" will be printed. + +# The expected URL formats for remote URLs are: +# 1. git@github.com:interuss/geospatial-utils.git +# 2. git@github.com/interuss/geospatial-utils.git +# 3. https://github.com/interuss/geospatial-utils.git + +# Determine what remote this branch is tracking, in case `origin` and `interuss` don't exist +BACKUP_REPO="https://github.com/$(git rev-parse --abbrev-ref @\{upstream\} 2> /dev/null || echo unknown/_)" + +UPSTREAM_REPO=$(git remote get-url origin 2> /dev/null || git remote get-url interuss 2> /dev/null || echo "$BACKUP_REPO") +# Replace `:` by `/` to handle git@github.com:interuss/geospatial-utils.git remote reference. +UPSTREAM_REPO=${UPSTREAM_REPO//:/\/} +# Remove hostname part +UPSTREAM_OWNER=$(dirname "${UPSTREAM_REPO#*github.com/*}") + +echo "$UPSTREAM_OWNER" \ No newline at end of file diff --git a/scripts/git/version.sh b/scripts/git/version.sh new file mode 100755 index 0000000..c4549ff --- /dev/null +++ b/scripts/git/version.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash + +# This script prints the current version of a component in the repository based on the tags +# of the upstream repository (remote origin) matching the following convention: +# owner/component/version. Examples of values: +# - owner: interuss (automatically extracted from the remote origin url) +# - component: rid, scd, aux, uss_qualifier +# - version: v3.0.1[-hash][-dirty] +# - [-hash] (example: -8a493ef8 ) is added when commits have been added to the latest version tagged. +# - [-dirty] (example: -dirty) when the workspace is not clean. +# Only versions without [-hash] and without [-dirty] shall be released. + +if [[ $# == 0 ]]; then + echo "Usage: $0 [--long]" + echo "Print the component's version number. (ie v0.0.1)" + echo "[--long]: Print the component's version using the long format including the upstream owner (ie interuss/scd/v0.0.1)." + exit 1 +fi + +COMPONENT=${1} + +RELEASE_FORMAT=false +if [[ $2 == "--long" ]]; then + RELEASE_FORMAT=true +fi + +# Set working directory +cd "$(dirname "$0")" || exit 1 + +UPSTREAM_ORG=$(./upstream_owner.sh) + +# Look for the last tag of the component +LAST_VERSION_TAG=$(git describe --abbrev=1 --tags --match="${UPSTREAM_ORG}/${COMPONENT}/*" 2> /dev/null) +#echo "LAST_VERSION_TAG: $LAST_VERSION_TAG" + +# Store in LAST_VERSION the version of the tag (ie v0.0.1) +LAST_VERSION=${LAST_VERSION_TAG##*/} +#echo "LAST_VERSION: $LAST_VERSION" + +# Current commit +COMMIT=$(git rev-parse --short HEAD) + +# If no version was found, use default v0.0.0. +if [[ -z "$LAST_VERSION" ]]; then + LAST_VERSION="v0.0.0-$COMMIT" +# Check if there are some commits on top of the tag by checking if an abbrev part is present. +elif [[ "$LAST_VERSION" == *"-"* ]]; then + # Remove abbrev part + LAST_VERSION=${LAST_VERSION%%-*} + # Append the commit hash + LAST_VERSION=${LAST_VERSION}-${COMMIT} +fi + +# Set the dirty flag if the workspace is not clean. +DIRTY="" +if test -n "$(git status -s)"; then + DIRTY="-dirty" +fi + +if [[ "$RELEASE_FORMAT" == "true" ]]; then + echo "${UPSTREAM_ORG}"/"${COMPONENT}"/"${LAST_VERSION}""${DIRTY}" +else + echo "${LAST_VERSION}""${DIRTY}" +fi \ No newline at end of file diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..1945949 --- /dev/null +++ b/uv.lock @@ -0,0 +1,103 @@ +version = 1 +revision = 3 +requires-python = "==3.13.5" + +[[package]] +name = "basedpyright" +version = "1.31.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodejs-wheel-binaries" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/53/570b03ec0445a9b2cc69788482c1d12902a9b88a9b159e449c4c537c4e3a/basedpyright-1.31.4.tar.gz", hash = "sha256:2450deb16530f7c88c1a7da04530a079f9b0b18ae1c71cb6f812825b3b82d0b1", size = 22494467, upload-time = "2025-09-03T13:05:55.817Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/40/d1047a5addcade9291685d06ef42a63c1347517018bafd82747af9da0294/basedpyright-1.31.4-py3-none-any.whl", hash = "sha256:055e4a38024bd653be12d6216c1cfdbee49a1096d342b4d5f5b4560f7714b6fc", size = 11731440, upload-time = "2025-09-03T13:05:52.308Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "geospatial-utils" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "basedpyright" }, + { name = "loguru" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "basedpyright", specifier = ">=1.31.1" }, + { name = "loguru", specifier = ">=0.7.3" }, + { name = "ruff" }, +] + +[[package]] +name = "loguru" +version = "0.7.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "win32-setctime", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559, upload-time = "2024-12-06T11:20:56.608Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" }, +] + +[[package]] +name = "nodejs-wheel-binaries" +version = "22.18.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/6d/773e09de4a052cc75c129c3766a3cf77c36bff8504a38693b735f4a1eb55/nodejs_wheel_binaries-22.18.0-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:53b04495857755c5d5658f7ac969d84f25898fe0b0c1bdc41172e5e0ac6105ca", size = 50873051, upload-time = "2025-08-01T11:10:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/ae/fc/3d6fd4ad5d26c9acd46052190d6a8895dc5050297b03d9cce03def53df0d/nodejs_wheel_binaries-22.18.0-py2.py3-none-macosx_11_0_x86_64.whl", hash = "sha256:bd4d016257d4dfe604ed526c19bd4695fdc4f4cc32e8afc4738111447aa96d03", size = 51814481, upload-time = "2025-08-01T11:10:33.086Z" }, + { url = "https://files.pythonhosted.org/packages/10/f9/7be44809a861605f844077f9e731a117b669d5ca6846a7820e7dd82c9fad/nodejs_wheel_binaries-22.18.0-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3b125f94f3f5e8ab9560d3bd637497f02e45470aeea74cf6fe60afe751cfa5f", size = 57804907, upload-time = "2025-08-01T11:10:36.83Z" }, + { url = "https://files.pythonhosted.org/packages/e9/67/563e74a0dff653ec7ddee63dc49b3f37a20df39f23675cfc801d7e8e4bb7/nodejs_wheel_binaries-22.18.0-py2.py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78bbb81b6e67c15f04e2a9c6c220d7615fb46ae8f1ad388df0d66abac6bed5f8", size = 58335587, upload-time = "2025-08-01T11:10:40.716Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b1/ec45fefef60223dd40e7953e2ff087964e200d6ec2d04eae0171d6428679/nodejs_wheel_binaries-22.18.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5d3ea8b7f957ae16b73241451f6ce831d6478156f363cce75c7ea71cbe6c6f7", size = 59662356, upload-time = "2025-08-01T11:10:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ed/6de2c73499eebf49d0d20e0704f64566029a3441c48cd4f655d49befd28b/nodejs_wheel_binaries-22.18.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bcda35b07677039670102a6f9b78c2313fd526111d407cb7ffc2a4c243a48ef9", size = 60706806, upload-time = "2025-08-01T11:10:48.985Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f5/487434b1792c4f28c63876e4a896f2b6e953e2dc1f0b3940e912bd087755/nodejs_wheel_binaries-22.18.0-py2.py3-none-win_amd64.whl", hash = "sha256:0f55e72733f1df2f542dce07f35145ac2e125408b5e2051cac08e5320e41b4d1", size = 39998139, upload-time = "2025-08-01T11:10:52.676Z" }, +] + +[[package]] +name = "ruff" +version = "0.12.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/f0/e0965dd709b8cabe6356811c0ee8c096806bb57d20b5019eb4e48a117410/ruff-0.12.12.tar.gz", hash = "sha256:b86cd3415dbe31b3b46a71c598f4c4b2f550346d1ccf6326b347cc0c8fd063d6", size = 5359915, upload-time = "2025-09-04T16:50:18.273Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/79/8d3d687224d88367b51c7974cec1040c4b015772bfbeffac95face14c04a/ruff-0.12.12-py3-none-linux_armv6l.whl", hash = "sha256:de1c4b916d98ab289818e55ce481e2cacfaad7710b01d1f990c497edf217dafc", size = 12116602, upload-time = "2025-09-04T16:49:18.892Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c3/6e599657fe192462f94861a09aae935b869aea8a1da07f47d6eae471397c/ruff-0.12.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7acd6045e87fac75a0b0cdedacf9ab3e1ad9d929d149785903cff9bb69ad9727", size = 12868393, upload-time = "2025-09-04T16:49:23.043Z" }, + { url = "https://files.pythonhosted.org/packages/e8/d2/9e3e40d399abc95336b1843f52fc0daaceb672d0e3c9290a28ff1a96f79d/ruff-0.12.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:abf4073688d7d6da16611f2f126be86523a8ec4343d15d276c614bda8ec44edb", size = 12036967, upload-time = "2025-09-04T16:49:26.04Z" }, + { url = "https://files.pythonhosted.org/packages/e9/03/6816b2ed08836be272e87107d905f0908be5b4a40c14bfc91043e76631b8/ruff-0.12.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:968e77094b1d7a576992ac078557d1439df678a34c6fe02fd979f973af167577", size = 12276038, upload-time = "2025-09-04T16:49:29.056Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d5/707b92a61310edf358a389477eabd8af68f375c0ef858194be97ca5b6069/ruff-0.12.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42a67d16e5b1ffc6d21c5f67851e0e769517fb57a8ebad1d0781b30888aa704e", size = 11901110, upload-time = "2025-09-04T16:49:32.07Z" }, + { url = "https://files.pythonhosted.org/packages/9d/3d/f8b1038f4b9822e26ec3d5b49cf2bc313e3c1564cceb4c1a42820bf74853/ruff-0.12.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b216ec0a0674e4b1214dcc998a5088e54eaf39417327b19ffefba1c4a1e4971e", size = 13668352, upload-time = "2025-09-04T16:49:35.148Z" }, + { url = "https://files.pythonhosted.org/packages/98/0e/91421368ae6c4f3765dd41a150f760c5f725516028a6be30e58255e3c668/ruff-0.12.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:59f909c0fdd8f1dcdbfed0b9569b8bf428cf144bec87d9de298dcd4723f5bee8", size = 14638365, upload-time = "2025-09-04T16:49:38.892Z" }, + { url = "https://files.pythonhosted.org/packages/74/5d/88f3f06a142f58ecc8ecb0c2fe0b82343e2a2b04dcd098809f717cf74b6c/ruff-0.12.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ac93d87047e765336f0c18eacad51dad0c1c33c9df7484c40f98e1d773876f5", size = 14060812, upload-time = "2025-09-04T16:49:42.732Z" }, + { url = "https://files.pythonhosted.org/packages/13/fc/8962e7ddd2e81863d5c92400820f650b86f97ff919c59836fbc4c1a6d84c/ruff-0.12.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:01543c137fd3650d322922e8b14cc133b8ea734617c4891c5a9fccf4bfc9aa92", size = 13050208, upload-time = "2025-09-04T16:49:46.434Z" }, + { url = "https://files.pythonhosted.org/packages/53/06/8deb52d48a9a624fd37390555d9589e719eac568c020b27e96eed671f25f/ruff-0.12.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afc2fa864197634e549d87fb1e7b6feb01df0a80fd510d6489e1ce8c0b1cc45", size = 13311444, upload-time = "2025-09-04T16:49:49.931Z" }, + { url = "https://files.pythonhosted.org/packages/2a/81/de5a29af7eb8f341f8140867ffb93f82e4fde7256dadee79016ac87c2716/ruff-0.12.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0c0945246f5ad776cb8925e36af2438e66188d2b57d9cf2eed2c382c58b371e5", size = 13279474, upload-time = "2025-09-04T16:49:53.465Z" }, + { url = "https://files.pythonhosted.org/packages/7f/14/d9577fdeaf791737ada1b4f5c6b59c21c3326f3f683229096cccd7674e0c/ruff-0.12.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a0fbafe8c58e37aae28b84a80ba1817f2ea552e9450156018a478bf1fa80f4e4", size = 12070204, upload-time = "2025-09-04T16:49:56.882Z" }, + { url = "https://files.pythonhosted.org/packages/77/04/a910078284b47fad54506dc0af13839c418ff704e341c176f64e1127e461/ruff-0.12.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b9c456fb2fc8e1282affa932c9e40f5ec31ec9cbb66751a316bd131273b57c23", size = 11880347, upload-time = "2025-09-04T16:49:59.729Z" }, + { url = "https://files.pythonhosted.org/packages/df/58/30185fcb0e89f05e7ea82e5817b47798f7fa7179863f9d9ba6fd4fe1b098/ruff-0.12.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5f12856123b0ad0147d90b3961f5c90e7427f9acd4b40050705499c98983f489", size = 12891844, upload-time = "2025-09-04T16:50:02.591Z" }, + { url = "https://files.pythonhosted.org/packages/21/9c/28a8dacce4855e6703dcb8cdf6c1705d0b23dd01d60150786cd55aa93b16/ruff-0.12.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:26a1b5a2bf7dd2c47e3b46d077cd9c0fc3b93e6c6cc9ed750bd312ae9dc302ee", size = 13360687, upload-time = "2025-09-04T16:50:05.8Z" }, + { url = "https://files.pythonhosted.org/packages/c8/fa/05b6428a008e60f79546c943e54068316f32ec8ab5c4f73e4563934fbdc7/ruff-0.12.12-py3-none-win32.whl", hash = "sha256:173be2bfc142af07a01e3a759aba6f7791aa47acf3604f610b1c36db888df7b1", size = 12052870, upload-time = "2025-09-04T16:50:09.121Z" }, + { url = "https://files.pythonhosted.org/packages/85/60/d1e335417804df452589271818749d061b22772b87efda88354cf35cdb7a/ruff-0.12.12-py3-none-win_amd64.whl", hash = "sha256:e99620bf01884e5f38611934c09dd194eb665b0109104acae3ba6102b600fd0d", size = 13178016, upload-time = "2025-09-04T16:50:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/28/7e/61c42657f6e4614a4258f1c3b0c5b93adc4d1f8575f5229d1906b483099b/ruff-0.12.12-py3-none-win_arm64.whl", hash = "sha256:2a8199cab4ce4d72d158319b63370abf60991495fb733db96cd923a34c52d093", size = 12256762, upload-time = "2025-09-04T16:50:15.737Z" }, +] + +[[package]] +name = "win32-setctime" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867, upload-time = "2024-12-07T15:28:28.314Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" }, +]