Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ba76702423 | |||
| 787cf6e11a | |||
| 22605d8f8e | |||
| c310a72019 | |||
| e1a523ea67 |
@@ -1,2 +0,0 @@
|
||||
BasedOnStyle: Google
|
||||
IndentWidth: 4
|
||||
@@ -0,0 +1,72 @@
|
||||
name: Pling Release Package Upload
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "release*"
|
||||
|
||||
jobs:
|
||||
upload-to-pling:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
env:
|
||||
PLING_PROJECT_ID: ${{ secrets.PLING_PROJECT_ID }}
|
||||
PLING_USERNAME: ${{ secrets.PLING_USERNAME }}
|
||||
PLING_PASSWORD: ${{ secrets.PLING_PASSWORD }}
|
||||
PLING_BASE_URL: https://www.opendesktop.org
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
|
||||
- name: Install packaging dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends zip
|
||||
|
||||
- name: Validate required secrets
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for var in PLING_PROJECT_ID PLING_USERNAME PLING_PASSWORD; do
|
||||
if [ -z "${!var:-}" ]; then
|
||||
echo "::error::Missing required secret: $var"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync --frozen
|
||||
|
||||
- name: Build and package plasmoid
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
make package
|
||||
|
||||
widget_id="$(python -c 'import json; from pathlib import Path; m=json.loads(Path("package/metadata.json").read_text(encoding="utf-8")); print((m.get("KPlugin") or {}).get("Id") or m.get("Id") or "")')"
|
||||
if [ -z "$widget_id" ]; then
|
||||
echo "::error::Could not resolve widget id from package/metadata.json (expected KPlugin.Id)"
|
||||
exit 1
|
||||
fi
|
||||
artifact_path="build/${widget_id}.plasmoid"
|
||||
|
||||
if [ ! -f "$artifact_path" ]; then
|
||||
echo "::error::Expected plasmoid artifact was not created: $artifact_path"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PLASMOID_ARTIFACT=$artifact_path" >> "$GITHUB_ENV"
|
||||
echo "Packaged artifact: $artifact_path"
|
||||
|
||||
- name: Upload packaged extension to Pling
|
||||
run: uv run python scripts/pling_upload.py -f "$PLASMOID_ARTIFACT"
|
||||
+3
-1
@@ -15,7 +15,6 @@
|
||||
# Build artifacts
|
||||
build/
|
||||
*.plasmoid
|
||||
package/contents/ui/cppbridge/
|
||||
|
||||
# Temporary files
|
||||
*~
|
||||
@@ -25,3 +24,6 @@ package/contents/ui/cppbridge/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
|
||||
kde-6-widget-starter/
|
||||
@@ -6,9 +6,6 @@ METADATA_FILE := $(PACKAGE_DIR)/metadata.json
|
||||
WIDGET_ID := $(shell grep -oP '"Id": "\K[^"]+' $(METADATA_FILE))
|
||||
BUILD_DIR := build
|
||||
PLASMOID_FILE := $(WIDGET_ID).plasmoid
|
||||
CPP_DIR := cpp
|
||||
CPP_BUILD_DIR := $(BUILD_DIR)/cpp
|
||||
CPP_MODULE_DIR := $(PACKAGE_DIR)/contents/ui/cppbridge
|
||||
|
||||
# Default target
|
||||
.PHONY: help
|
||||
@@ -18,13 +15,11 @@ help:
|
||||
@echo "Usage:"
|
||||
@echo " make dev Run widget in plasmoidviewer from local package (MODE=panel|desktop|hidpi)"
|
||||
@echo " make watch Start Development Mode with Hot Reload"
|
||||
@echo " make cpp-build Build the C++ QML bridge module"
|
||||
@echo " make cpp-clean Remove C++ bridge build artifacts"
|
||||
@echo " make install Install/upgrade widget in local Plasma package store"
|
||||
@echo " make uninstall Remove widget from local system"
|
||||
@echo " make package Build .plasmoid file for distribution"
|
||||
@echo " make clean Remove build artifacts"
|
||||
@echo " make format Format QML, C++, JSON, YAML, and Markdown files"
|
||||
@echo " make format Format QML, JSON, YAML, and Markdown files"
|
||||
@echo " make lint Run non-mutating lint and formatting checks"
|
||||
@echo " make hooks-install Configure Git to use versioned hooks from .githooks/"
|
||||
|
||||
@@ -35,28 +30,11 @@ hooks-install:
|
||||
git config core.hooksPath .githooks
|
||||
@echo "Configured core.hooksPath=.githooks"
|
||||
|
||||
# C++ Bridge Build Targets
|
||||
# Generate CMake build files for the C++ QML bridge.
|
||||
.PHONY: cpp-configure
|
||||
cpp-configure:
|
||||
mkdir -p $(CPP_BUILD_DIR)
|
||||
cd $(CPP_DIR) && cmake --preset clang-default
|
||||
|
||||
# Build the C++ QML bridge module binaries.
|
||||
.PHONY: cpp-build
|
||||
cpp-build: cpp-configure
|
||||
cmake --build $(CPP_BUILD_DIR)
|
||||
|
||||
# Remove C++ bridge build output and copied module artifacts.
|
||||
.PHONY: cpp-clean
|
||||
cpp-clean:
|
||||
rm -rf $(CPP_BUILD_DIR) $(CPP_MODULE_DIR)
|
||||
|
||||
# Testing Targets
|
||||
# Run plasmoidviewer from the local package directory.
|
||||
# Optional MODE values: panel, desktop, hidpi (omit MODE for default).
|
||||
.PHONY: dev
|
||||
dev: cpp-build
|
||||
dev:
|
||||
@mode="$(MODE)"; \
|
||||
case "$$mode" in \
|
||||
""|default) \
|
||||
@@ -75,7 +53,7 @@ dev: cpp-build
|
||||
# Development Tools
|
||||
# Start file watcher for iterative development and reload workflow.
|
||||
.PHONY: watch
|
||||
watch: cpp-build
|
||||
watch:
|
||||
./scripts/watch.sh
|
||||
|
||||
# Format all QML files in the package directory in-place.
|
||||
@@ -84,12 +62,6 @@ format-qml:
|
||||
@command -v qmlformat >/dev/null 2>&1 || { echo "Missing required tool: qmlformat"; exit 1; }
|
||||
find $(PACKAGE_DIR) -name "*.qml" -type f -exec qmlformat -i {} +
|
||||
|
||||
# Format C/C++ source and header files with clang-format.
|
||||
.PHONY: format-cpp
|
||||
format-cpp:
|
||||
@command -v clang-format >/dev/null 2>&1 || { echo "Missing required tool: clang-format"; exit 1; }
|
||||
find $(CPP_DIR) -type f \( -name "*.c" -o -name "*.cc" -o -name "*.cpp" -o -name "*.h" -o -name "*.hh" -o -name "*.hpp" \) -exec clang-format -i {} +
|
||||
|
||||
# Format JSON/YAML/Markdown files with prettier.
|
||||
.PHONY: format-prettier
|
||||
format-prettier:
|
||||
@@ -102,7 +74,7 @@ format-prettier:
|
||||
|
||||
# Run all formatting passes.
|
||||
.PHONY: format
|
||||
format: format-qml format-cpp format-prettier
|
||||
format: format-qml format-prettier
|
||||
|
||||
# Linting
|
||||
# Check QML formatting without changing files.
|
||||
@@ -119,16 +91,10 @@ lint-qml-format:
|
||||
|
||||
# Run semantic QML lint checks.
|
||||
.PHONY: lint-qml
|
||||
lint-qml: cpp-build
|
||||
lint-qml:
|
||||
@command -v qmllint >/dev/null 2>&1 || { echo "Missing required tool: qmllint"; exit 1; }
|
||||
find $(PACKAGE_DIR) -name "*.qml" -type f -print0 | xargs -0 -r qmllint
|
||||
|
||||
# Check C/C++ formatting without changing files.
|
||||
.PHONY: lint-cpp
|
||||
lint-cpp:
|
||||
@command -v clang-format >/dev/null 2>&1 || { echo "Missing required tool: clang-format"; exit 1; }
|
||||
find $(CPP_DIR) -type f \( -name "*.c" -o -name "*.cc" -o -name "*.cpp" -o -name "*.h" -o -name "*.hh" -o -name "*.hpp" \) -print0 | xargs -0 -r clang-format --dry-run --Werror
|
||||
|
||||
# Check JSON/YAML/Markdown formatting without changing files.
|
||||
.PHONY: lint-prettier
|
||||
lint-prettier:
|
||||
@@ -141,12 +107,12 @@ lint-prettier:
|
||||
|
||||
# Run all lint and formatting checks.
|
||||
.PHONY: lint
|
||||
lint: lint-qml-format lint-qml lint-cpp lint-prettier
|
||||
lint: lint-qml-format lint-qml lint-prettier
|
||||
|
||||
# Installation
|
||||
# Install/upgrade widget in local Plasma package store.
|
||||
.PHONY: install
|
||||
install: cpp-build
|
||||
install:
|
||||
@if kpackagetool6 --type Plasma/Applet --show $(WIDGET_ID) >/dev/null 2>&1; then \
|
||||
echo "Upgrading existing package: $(WIDGET_ID)"; \
|
||||
kpackagetool6 --type Plasma/Applet --upgrade $(PACKAGE_DIR); \
|
||||
@@ -157,7 +123,7 @@ install: cpp-build
|
||||
|
||||
# Upgrade an already installed widget package with local changes.
|
||||
.PHONY: upgrade
|
||||
upgrade: cpp-build
|
||||
upgrade:
|
||||
kpackagetool6 --type Plasma/Applet --upgrade $(PACKAGE_DIR)
|
||||
|
||||
# Remove installed widget package from local system.
|
||||
@@ -173,7 +139,7 @@ uninstall:
|
||||
# Packaging
|
||||
# Create distributable .plasmoid archive from current package contents.
|
||||
.PHONY: package
|
||||
package: cpp-build
|
||||
package:
|
||||
mkdir -p $(BUILD_DIR)
|
||||
rm -f $(BUILD_DIR)/$(PLASMOID_FILE)
|
||||
|
||||
@@ -186,4 +152,4 @@ package: cpp-build
|
||||
# Delete all generated build artifacts for a clean workspace.
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf $(BUILD_DIR) $(CPP_MODULE_DIR)
|
||||
rm -rf $(BUILD_DIR)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# katoclock
|
||||
|
||||
Just a cat with a clock. Bootstrapped from my [kde-6-widget-starter](https://github.com/ruinivist/kde-6-widget-starter)
|
||||
Just a QML-only cat clock widget for KDE Plasma 6. Bootstrapped from my [kde-6-widget-starter](https://github.com/ruinivist/kde-6-widget-starter)
|
||||
|
||||
[Pling page](https://www.pling.com/p/2349620/)
|
||||
|
||||

|
||||
|
||||
# Install
|
||||
## Install
|
||||
|
||||
- clone the repo
|
||||
- `make install` from repo root
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.23)
|
||||
project(cppbridge LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
|
||||
find_package(Qt6 REQUIRED COMPONENTS Core Qml Quick)
|
||||
find_package(OpenCV 4 REQUIRED COMPONENTS core imgcodecs imgproc)
|
||||
qt_standard_project_setup(REQUIRES 6.5)
|
||||
|
||||
qt_add_qml_module(cppbridgeplugin
|
||||
URI cppbridge
|
||||
VERSION 1.0
|
||||
OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/../package/contents/ui/cppbridge"
|
||||
SOURCES
|
||||
src/clockimageprovider.hpp
|
||||
src/clockimageprovider.cpp
|
||||
src/config.hpp
|
||||
src/renderer.hpp
|
||||
src/renderer.cpp
|
||||
src/timebridge.h
|
||||
src/timebridge.cpp
|
||||
)
|
||||
|
||||
target_include_directories(cppbridgeplugin PRIVATE
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src"
|
||||
${OpenCV_INCLUDE_DIRS}
|
||||
)
|
||||
target_link_libraries(cppbridgeplugin PRIVATE
|
||||
Qt6::Core
|
||||
Qt6::Qml
|
||||
Qt6::Quick
|
||||
opencv_core
|
||||
opencv_imgcodecs
|
||||
opencv_imgproc
|
||||
)
|
||||
set_target_properties(cppbridgeplugin PROPERTIES
|
||||
CXX_STANDARD 20
|
||||
CXX_STANDARD_REQUIRED ON
|
||||
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/../package/contents/ui/cppbridge"
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/../package/contents/ui/cppbridge"
|
||||
BUILD_WITH_INSTALL_RPATH TRUE
|
||||
INSTALL_RPATH "$ORIGIN"
|
||||
)
|
||||
|
||||
set_target_properties(cppbridgepluginplugin PROPERTIES
|
||||
BUILD_WITH_INSTALL_RPATH TRUE
|
||||
INSTALL_RPATH "$ORIGIN"
|
||||
)
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"version": 6,
|
||||
"cmakeMinimumRequired": {
|
||||
"major": 3,
|
||||
"minor": 23,
|
||||
"patch": 0
|
||||
},
|
||||
"configurePresets": [
|
||||
{
|
||||
"name": "clang-default",
|
||||
"displayName": "Clang Default",
|
||||
"description": "Configure build/cpp with clang/clang++ and export compile commands.",
|
||||
"generator": "Unix Makefiles",
|
||||
"binaryDir": "${sourceDir}/../build/cpp",
|
||||
"cacheVariables": {
|
||||
"CMAKE_C_COMPILER": "clang",
|
||||
"CMAKE_CXX_COMPILER": "clang++",
|
||||
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
|
||||
}
|
||||
}
|
||||
],
|
||||
"buildPresets": [
|
||||
{
|
||||
"name": "clang-default",
|
||||
"configurePreset": "clang-default"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
#include "clockimageprovider.hpp"
|
||||
|
||||
#include <QMutexLocker>
|
||||
|
||||
ClockImageProvider::ClockImageProvider()
|
||||
: QQuickImageProvider(QQuickImageProvider::Image) {}
|
||||
|
||||
QImage ClockImageProvider::requestImage(const QString&, QSize* size,
|
||||
const QSize& requestedSize) {
|
||||
QImage frame;
|
||||
{
|
||||
QMutexLocker locker(&mutex_);
|
||||
frame = image_;
|
||||
}
|
||||
|
||||
if (size != nullptr) {
|
||||
*size = frame.size();
|
||||
}
|
||||
|
||||
if (!frame.isNull() && requestedSize.width() > 0 &&
|
||||
requestedSize.height() > 0) {
|
||||
return frame.scaled(requestedSize, Qt::KeepAspectRatio,
|
||||
Qt::SmoothTransformation);
|
||||
}
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
void ClockImageProvider::setImage(const QImage& image) {
|
||||
QMutexLocker locker(&mutex_);
|
||||
image_ = image;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <QImage>
|
||||
#include <QMutex>
|
||||
#include <QQuickImageProvider>
|
||||
|
||||
class ClockImageProvider : public QQuickImageProvider {
|
||||
public:
|
||||
ClockImageProvider();
|
||||
|
||||
QImage requestImage(const QString& id, QSize* size,
|
||||
const QSize& requestedSize) override;
|
||||
|
||||
void setImage(const QImage& image);
|
||||
|
||||
private:
|
||||
QMutex mutex_;
|
||||
QImage image_;
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <opencv2/core.hpp>
|
||||
|
||||
// file full of magic numbers
|
||||
// most are hand tuned based on the cat image I have
|
||||
|
||||
namespace Config {
|
||||
const cv::Point2f CLOCK_CENTER(300.0f, 286.0f);
|
||||
|
||||
const double HAND_SCALE = 0.22;
|
||||
const double HOUR_HAND_SCALE = HAND_SCALE * 0.7;
|
||||
const double MINUTE_HAND_SCALE = HAND_SCALE * 0.7;
|
||||
|
||||
const cv::Point2f HOUR_HAND_PIVOT_FRACTION(0.126f, 0.502f);
|
||||
const cv::Point2f MINUTE_HAND_PIVOT_FRACTION(0.101f, 0.488f);
|
||||
// Asset convention: unrotated hand points right (+X).
|
||||
// Clock convention: 12:00 points up (-Y), hence +90 degrees.
|
||||
const double HAND_BASE_ANGLE_OFFSET = 90.0;
|
||||
|
||||
} // namespace Config
|
||||
@@ -1,112 +0,0 @@
|
||||
#include "renderer.hpp"
|
||||
|
||||
#include <iostream>
|
||||
#include <opencv2/imgcodecs.hpp>
|
||||
#include <opencv2/imgproc.hpp>
|
||||
|
||||
#include "config.hpp"
|
||||
|
||||
Renderer::Renderer() {}
|
||||
|
||||
bool Renderer::load_assets(const std::string& bg_path,
|
||||
const std::string& hour_hand_path,
|
||||
const std::string& minute_hand_path) {
|
||||
background_ = cv::imread(bg_path, cv::IMREAD_UNCHANGED);
|
||||
if (background_.empty()) {
|
||||
std::cerr << "failed to load background: " << bg_path << std::endl;
|
||||
return false;
|
||||
}
|
||||
if (background_.channels() != 4) {
|
||||
std::cerr << "expected rgba png for background";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Load hands with alpha channel.
|
||||
hour_hand_ = cv::imread(hour_hand_path, cv::IMREAD_UNCHANGED);
|
||||
if (hour_hand_.empty()) {
|
||||
std::cerr << "failed to load hour hand: " << hour_hand_path
|
||||
<< std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
minute_hand_ = cv::imread(minute_hand_path, cv::IMREAD_UNCHANGED);
|
||||
if (minute_hand_.empty()) {
|
||||
std::cerr << "failed to load minute hand: " << minute_hand_path
|
||||
<< std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hour_hand_.channels() != 4 || minute_hand_.channels() != 4) {
|
||||
std::cerr << "expected rgba png for hands" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
cv::Mat Renderer::render(double hour_angle, double minute_angle) {
|
||||
auto clock_to_cv_rotation = [](double clock_angle) {
|
||||
return Config::HAND_BASE_ANGLE_OFFSET - clock_angle;
|
||||
};
|
||||
|
||||
cv::Mat warped_hour = transform_hand(
|
||||
hour_hand_, clock_to_cv_rotation(hour_angle), Config::HOUR_HAND_SCALE,
|
||||
Config::HOUR_HAND_PIVOT_FRACTION);
|
||||
cv::Mat warped_minute = transform_hand(
|
||||
minute_hand_, clock_to_cv_rotation(minute_angle),
|
||||
Config::MINUTE_HAND_SCALE, Config::MINUTE_HAND_PIVOT_FRACTION);
|
||||
|
||||
cv::Mat keyframe = background_.clone();
|
||||
|
||||
// Layer order matters: hour underneath minute.
|
||||
blend_overlay(keyframe, warped_hour);
|
||||
blend_overlay(keyframe, warped_minute);
|
||||
|
||||
return keyframe;
|
||||
}
|
||||
|
||||
cv::Mat Renderer::transform_hand(const cv::Mat& hand_sprite, double angle_deg,
|
||||
double scale,
|
||||
const cv::Point2f& pivot_fraction) {
|
||||
cv::Mat oriented_sprite;
|
||||
cv::flip(hand_sprite, oriented_sprite, 0);
|
||||
|
||||
if (scale <= 0.0) {
|
||||
return cv::Mat::zeros(background_.size(), CV_8UC4);
|
||||
}
|
||||
|
||||
cv::Point2f pivot(oriented_sprite.cols * pivot_fraction.x,
|
||||
oriented_sprite.rows * pivot_fraction.y);
|
||||
|
||||
cv::Mat rot_mat = cv::getRotationMatrix2D(pivot, angle_deg, scale);
|
||||
rot_mat.at<double>(0, 2) += Config::CLOCK_CENTER.x - pivot.x;
|
||||
rot_mat.at<double>(1, 2) += Config::CLOCK_CENTER.y - pivot.y;
|
||||
|
||||
cv::Mat overlay = cv::Mat::zeros(background_.size(), CV_8UC4);
|
||||
cv::warpAffine(oriented_sprite, overlay, rot_mat, background_.size(),
|
||||
cv::INTER_CUBIC, cv::BORDER_CONSTANT,
|
||||
cv::Scalar(0, 0, 0, 0));
|
||||
|
||||
return overlay;
|
||||
}
|
||||
|
||||
void Renderer::blend_overlay(cv::Mat& bg, const cv::Mat& fg) {
|
||||
// Blend hand RGB onto background RGB using hand alpha
|
||||
for (int y = 0; y < fg.rows; ++y) {
|
||||
const cv::Vec4b* fg_ptr = fg.ptr<cv::Vec4b>(y);
|
||||
cv::Vec4b* bg_ptr = bg.ptr<cv::Vec4b>(y);
|
||||
|
||||
for (int x = 0; x < fg.cols; ++x) {
|
||||
const cv::Vec4b& fg_pixel = fg_ptr[x];
|
||||
const double alpha = fg_pixel[3] / 255.0;
|
||||
|
||||
if (alpha > 0.0) {
|
||||
const cv::Vec4b bg_pixel = bg_ptr[x];
|
||||
for (int c = 0; c < 3; ++c) {
|
||||
bg_ptr[x][c] = static_cast<uchar>(
|
||||
fg_pixel[c] * alpha + bg_pixel[c] * (1.0 - alpha));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <opencv2/core.hpp>
|
||||
#include <string>
|
||||
|
||||
class Renderer {
|
||||
public:
|
||||
Renderer();
|
||||
|
||||
// Load assets from disk. Returns false if any fail to load.
|
||||
bool load_assets(const std::string& bg_path,
|
||||
const std::string& hour_hand_path,
|
||||
const std::string& minute_hand_path);
|
||||
|
||||
// Composites the hands onto the background at the given angles (degrees).
|
||||
// Returns the result image.
|
||||
cv::Mat render(double hour_angle, double minute_angle);
|
||||
|
||||
private:
|
||||
cv::Mat background_;
|
||||
cv::Mat hour_hand_;
|
||||
cv::Mat minute_hand_;
|
||||
|
||||
// Helpers
|
||||
cv::Mat transform_hand(const cv::Mat& hand_sprite, double angle_deg,
|
||||
double scale, const cv::Point2f& pivot_fraction);
|
||||
void blend_overlay(cv::Mat& bg, const cv::Mat& fg);
|
||||
};
|
||||
@@ -1,216 +0,0 @@
|
||||
#include "timebridge.h"
|
||||
|
||||
#include <QQmlEngine>
|
||||
#include <QTime>
|
||||
#include <QUuid>
|
||||
#include <opencv2/imgproc.hpp>
|
||||
|
||||
#include "clockimageprovider.hpp"
|
||||
#include "renderer.hpp"
|
||||
|
||||
namespace {
|
||||
constexpr int kMillisPerMinute = 60 * 1000;
|
||||
}
|
||||
|
||||
ClockBridge::ClockBridge(QObject* parent) : QObject(parent) {
|
||||
updateTimer_.setSingleShot(true);
|
||||
connect(&updateTimer_, &QTimer::timeout, this,
|
||||
&ClockBridge::onUpdateTimerTimeout);
|
||||
}
|
||||
|
||||
ClockBridge::~ClockBridge() {
|
||||
updateTimer_.stop();
|
||||
if (renderer_ != nullptr) {
|
||||
delete renderer_;
|
||||
renderer_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
QString ClockBridge::imageSource() const { return imageSource_; }
|
||||
|
||||
bool ClockBridge::ready() const { return ready_; }
|
||||
|
||||
QString ClockBridge::errorString() const { return errorString_; }
|
||||
|
||||
bool ClockBridge::initialize(const QUrl& bg, const QUrl& hour,
|
||||
const QUrl& minute) {
|
||||
updateTimer_.stop();
|
||||
initialized_ = false;
|
||||
setReady(false);
|
||||
|
||||
const QString bgPath = toLocalPath(bg);
|
||||
const QString hourPath = toLocalPath(hour);
|
||||
const QString minutePath = toLocalPath(minute);
|
||||
if (bgPath.isEmpty() || hourPath.isEmpty() || minutePath.isEmpty()) {
|
||||
setErrorString(QStringLiteral("Clock assets must be local file URLs."));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ensureImageProvider()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (renderer_ == nullptr) {
|
||||
renderer_ = new Renderer();
|
||||
}
|
||||
|
||||
if (!renderer_->load_assets(bgPath.toStdString(), hourPath.toStdString(),
|
||||
minutePath.toStdString())) {
|
||||
setErrorString(QStringLiteral("Failed to load clock assets."));
|
||||
return false;
|
||||
}
|
||||
|
||||
initialized_ = true;
|
||||
if (!renderForCurrentTime()) {
|
||||
initialized_ = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
scheduleNextMinuteTick();
|
||||
return true;
|
||||
}
|
||||
|
||||
void ClockBridge::refreshNow() {
|
||||
if (!initialized_) {
|
||||
setErrorString(QStringLiteral("ClockBridge is not initialized."));
|
||||
setReady(false);
|
||||
return;
|
||||
}
|
||||
|
||||
renderForCurrentTime();
|
||||
scheduleNextMinuteTick();
|
||||
}
|
||||
|
||||
void ClockBridge::onUpdateTimerTimeout() {
|
||||
if (!initialized_) {
|
||||
return;
|
||||
}
|
||||
|
||||
renderForCurrentTime();
|
||||
scheduleNextMinuteTick();
|
||||
}
|
||||
|
||||
bool ClockBridge::ensureImageProvider() {
|
||||
if (imageProvider_ != nullptr) {
|
||||
return true;
|
||||
}
|
||||
|
||||
QQmlEngine* engine = qmlEngine(this);
|
||||
if (engine == nullptr) {
|
||||
setErrorString(QStringLiteral("ClockBridge has no active QQmlEngine."));
|
||||
return false;
|
||||
}
|
||||
|
||||
providerId_ = QStringLiteral("catclock_%1")
|
||||
.arg(QUuid::createUuid().toString(QUuid::Id128));
|
||||
if (engine->imageProvider(providerId_) != nullptr) {
|
||||
setErrorString(QStringLiteral("Image provider ID collision."));
|
||||
return false;
|
||||
}
|
||||
|
||||
imageProvider_ = new ClockImageProvider();
|
||||
engine->addImageProvider(providerId_, imageProvider_);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ClockBridge::renderForCurrentTime() {
|
||||
if (renderer_ == nullptr || imageProvider_ == nullptr) {
|
||||
setErrorString(QStringLiteral("ClockBridge is not initialized."));
|
||||
setReady(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
const QTime now = QTime::currentTime();
|
||||
const int minute = now.minute();
|
||||
const int hour = now.hour();
|
||||
|
||||
const double minuteAngle = minute * 6.0;
|
||||
const double hourAngle = (hour % 12) * 30.0 + minute * 0.5;
|
||||
|
||||
cv::Mat frame = renderer_->render(hourAngle, minuteAngle);
|
||||
if (frame.empty()) {
|
||||
setErrorString(QStringLiteral("Renderer returned an empty frame."));
|
||||
setReady(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
cv::Mat rgbaFrame;
|
||||
cv::cvtColor(frame, rgbaFrame, cv::COLOR_BGRA2RGBA);
|
||||
QImage image(rgbaFrame.data, rgbaFrame.cols, rgbaFrame.rows,
|
||||
static_cast<int>(rgbaFrame.step), QImage::Format_RGBA8888);
|
||||
imageProvider_->setImage(image.copy());
|
||||
|
||||
setErrorString(QString());
|
||||
setReady(true);
|
||||
bumpFrameRevision();
|
||||
return true;
|
||||
}
|
||||
|
||||
void ClockBridge::scheduleNextMinuteTick() {
|
||||
if (!initialized_) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QTime now = QTime::currentTime();
|
||||
int delayMs = kMillisPerMinute - (now.second() * 1000 + now.msec());
|
||||
if (delayMs <= 0 || delayMs > kMillisPerMinute) {
|
||||
delayMs = kMillisPerMinute;
|
||||
}
|
||||
|
||||
updateTimer_.start(delayMs);
|
||||
}
|
||||
|
||||
void ClockBridge::setReady(bool value) {
|
||||
if (ready_ == value) {
|
||||
return;
|
||||
}
|
||||
ready_ = value;
|
||||
emit readyChanged();
|
||||
}
|
||||
|
||||
void ClockBridge::setErrorString(const QString& value) {
|
||||
if (errorString_ == value) {
|
||||
return;
|
||||
}
|
||||
errorString_ = value;
|
||||
emit errorStringChanged();
|
||||
}
|
||||
|
||||
QString ClockBridge::toLocalPath(const QUrl& url) const {
|
||||
if (url.isEmpty()) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
if (url.isLocalFile()) {
|
||||
return url.toLocalFile();
|
||||
}
|
||||
|
||||
if (url.scheme().isEmpty()) {
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString ClockBridge::buildImageSource() const {
|
||||
if (providerId_.isEmpty()) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
return QStringLiteral("image://%1/frame?rev=%2")
|
||||
.arg(providerId_)
|
||||
.arg(frameRevision_);
|
||||
}
|
||||
|
||||
// to signal image inddeed changed even if the path is same
|
||||
// we update revision
|
||||
void ClockBridge::bumpFrameRevision() {
|
||||
++frameRevision_;
|
||||
const QString nextSource = buildImageSource();
|
||||
if (imageSource_ == nextSource) {
|
||||
return;
|
||||
}
|
||||
|
||||
imageSource_ = nextSource;
|
||||
emit imageSourceChanged();
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <qqmlintegration.h>
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QTimer>
|
||||
#include <QUrl>
|
||||
|
||||
class ClockImageProvider;
|
||||
class Renderer;
|
||||
|
||||
class ClockBridge : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
Q_PROPERTY(QString imageSource READ imageSource NOTIFY imageSourceChanged)
|
||||
Q_PROPERTY(bool ready READ ready NOTIFY readyChanged)
|
||||
Q_PROPERTY(QString errorString READ errorString NOTIFY errorStringChanged)
|
||||
|
||||
public:
|
||||
explicit ClockBridge(QObject* parent = nullptr);
|
||||
~ClockBridge() override;
|
||||
|
||||
QString imageSource() const;
|
||||
bool ready() const;
|
||||
QString errorString() const;
|
||||
|
||||
Q_INVOKABLE bool initialize(const QUrl& bg, const QUrl& hour,
|
||||
const QUrl& minute);
|
||||
Q_INVOKABLE void refreshNow();
|
||||
|
||||
signals:
|
||||
void imageSourceChanged();
|
||||
void readyChanged();
|
||||
void errorStringChanged();
|
||||
|
||||
private slots:
|
||||
void onUpdateTimerTimeout();
|
||||
|
||||
private:
|
||||
bool ensureImageProvider();
|
||||
bool renderForCurrentTime();
|
||||
void scheduleNextMinuteTick();
|
||||
void setReady(bool value);
|
||||
void setErrorString(const QString& value);
|
||||
QString toLocalPath(const QUrl& url) const;
|
||||
QString buildImageSource() const;
|
||||
void bumpFrameRevision();
|
||||
|
||||
Renderer* renderer_ = nullptr;
|
||||
ClockImageProvider* imageProvider_ = nullptr;
|
||||
QString providerId_;
|
||||
QString imageSource_;
|
||||
QString errorString_;
|
||||
bool ready_ = false;
|
||||
bool initialized_ = false;
|
||||
int frameRevision_ = 0;
|
||||
QTimer updateTimer_;
|
||||
};
|
||||
@@ -1,56 +1,125 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import "cppbridge" as CppBridge
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.plasma.components as PlasmaComponents
|
||||
import org.kde.plasma.core as PlasmaCore
|
||||
import org.kde.plasma.plasmoid
|
||||
|
||||
PlasmoidItem {
|
||||
id: root
|
||||
|
||||
// Widget setup
|
||||
readonly property real clockWidth: 421
|
||||
readonly property real clockHeight: 600
|
||||
readonly property real clockCenterX: 300
|
||||
readonly property real clockCenterY: 286
|
||||
readonly property real handScale: 0.154
|
||||
property real hourAngle: 0
|
||||
property real minuteAngle: 0
|
||||
|
||||
function refreshTime() {
|
||||
const now = new Date();
|
||||
const minute = now.getMinutes();
|
||||
const hour = now.getHours();
|
||||
root.minuteAngle = minute * 6;
|
||||
root.hourAngle = (hour % 12) * 30 + minute * 0.5;
|
||||
let nextTick = 60000 - (now.getSeconds() * 1000 + now.getMilliseconds());
|
||||
if (nextTick <= 0 || nextTick > 60000)
|
||||
nextTick = 60000;
|
||||
|
||||
minuteTimer.interval = nextTick;
|
||||
minuteTimer.restart();
|
||||
}
|
||||
|
||||
width: Kirigami.Units.gridUnit * 20
|
||||
height: Kirigami.Units.gridUnit * 20
|
||||
Plasmoid.backgroundHints: PlasmaCore.Types.NoBackground
|
||||
toolTipMainText: "katoclock"
|
||||
Component.onCompleted: {
|
||||
const initialized = clockBridge.initialize(Qt.resolvedUrl("assets/background.png"), Qt.resolvedUrl("assets/hand_hour.png"), Qt.resolvedUrl("assets/hand_minute.png"));
|
||||
if (!initialized)
|
||||
console.warn(clockBridge.errorString);
|
||||
Component.onCompleted: refreshTime()
|
||||
|
||||
Timer {
|
||||
id: minuteTimer
|
||||
|
||||
repeat: false
|
||||
onTriggered: root.refreshTime()
|
||||
}
|
||||
|
||||
CppBridge.ClockBridge {
|
||||
id: clockBridge
|
||||
component ClockHand: Item {
|
||||
required property url imageSource
|
||||
required property real sourceWidth
|
||||
required property real sourceHeight
|
||||
required property real pivotFractionX
|
||||
required property real pivotFractionY
|
||||
required property real clockAngle
|
||||
readonly property real pivotX: width * pivotFractionX
|
||||
readonly property real pivotY: height * pivotFractionY
|
||||
|
||||
width: sourceWidth * root.handScale
|
||||
height: sourceHeight * root.handScale
|
||||
x: root.clockCenterX - pivotX
|
||||
y: root.clockCenterY - pivotY
|
||||
|
||||
Image {
|
||||
id: handImage
|
||||
|
||||
anchors.fill: parent
|
||||
source: imageSource
|
||||
smooth: true
|
||||
mipmap: true
|
||||
|
||||
transform: Scale {
|
||||
origin.x: handImage.width / 2
|
||||
origin.y: handImage.height / 2
|
||||
yScale: -1
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
transform: Rotation {
|
||||
origin.x: pivotX
|
||||
origin.y: pivotY
|
||||
angle: clockAngle - 90
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
component ClockImageView: Item {
|
||||
// asset image size -> 421x600, 1 is cropping artifact but I'll live with it
|
||||
implicitWidth: 421
|
||||
implicitHeight: 600
|
||||
implicitWidth: root.clockWidth
|
||||
implicitHeight: root.clockHeight
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
visible: clockBridge.ready
|
||||
source: clockBridge.imageSource
|
||||
fillMode: Image.PreserveAspectFit
|
||||
asynchronous: true
|
||||
cache: false
|
||||
}
|
||||
Item {
|
||||
width: root.clockWidth
|
||||
height: root.clockHeight
|
||||
anchors.centerIn: parent
|
||||
scale: Math.min(parent.width / width, parent.height / height)
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: Qt.resolvedUrl("assets/background.png")
|
||||
smooth: true
|
||||
mipmap: true
|
||||
}
|
||||
|
||||
ClockHand {
|
||||
imageSource: Qt.resolvedUrl("assets/hand_hour.png")
|
||||
sourceWidth: 227
|
||||
sourceHeight: 76
|
||||
pivotFractionX: 0.126
|
||||
pivotFractionY: 0.502
|
||||
clockAngle: root.hourAngle
|
||||
}
|
||||
|
||||
ClockHand {
|
||||
imageSource: Qt.resolvedUrl("assets/hand_minute.png")
|
||||
sourceWidth: 316
|
||||
sourceHeight: 78
|
||||
pivotFractionX: 0.101
|
||||
pivotFractionY: 0.488
|
||||
clockAngle: root.minuteAngle
|
||||
}
|
||||
|
||||
PlasmaComponents.Label {
|
||||
anchors.fill: parent
|
||||
visible: !clockBridge.ready
|
||||
text: clockBridge.errorString.length > 0 ? clockBridge.errorString : "Loading..."
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// full representation (popup or desktop widget)
|
||||
fullRepresentation: Item {
|
||||
Layout.minimumWidth: Kirigami.Units.gridUnit * 12
|
||||
Layout.minimumHeight: Kirigami.Units.gridUnit * 12
|
||||
@@ -64,7 +133,6 @@ PlasmoidItem {
|
||||
|
||||
}
|
||||
|
||||
// compact representation (icon in panel)
|
||||
compactRepresentation: Item {
|
||||
Layout.minimumWidth: Kirigami.Units.gridUnit * 6
|
||||
Layout.minimumHeight: Kirigami.Units.gridUnit * 6
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
[project]
|
||||
name = "katoclock"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"niquests>=3.18.5",
|
||||
]
|
||||
Executable
+818
@@ -0,0 +1,818 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Pling/OpenDesktop uploader.
|
||||
|
||||
Modes:
|
||||
- default: destructive overwrite upload (delete all product files, then upload files)
|
||||
- --dry-run: authenticate and validate upload endpoint discovery only
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from html.parser import HTMLParser
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable, Optional, Protocol, cast
|
||||
from urllib.parse import urljoin, urlparse
|
||||
|
||||
try:
|
||||
import niquests # pyright: ignore[reportMissingImports]
|
||||
except ImportError as exc: # pragma: no cover - import guard
|
||||
print(
|
||||
"ERROR: missing dependency 'niquests'. Install it first, e.g. 'uv add niquests'.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
raise SystemExit(2) from exc
|
||||
|
||||
|
||||
DEFAULT_BASE_URL = "https://www.opendesktop.org"
|
||||
LOGIN_PATH = "/login/"
|
||||
EDIT_PATH_TEMPLATE = "/p/{project_id}/edit"
|
||||
DEFAULT_TIMEOUT_SECONDS = 30.0
|
||||
DEFAULT_MAX_RETRIES = 2
|
||||
|
||||
REQUIRED_EDIT_DATA_ATTRS = (
|
||||
"data-addpploadfile-uri",
|
||||
"data-updatepploadfile-uri",
|
||||
"data-deletepploadfile-uri",
|
||||
"data-deletepploadfiles-uri",
|
||||
"data-product-id",
|
||||
"data-ppload-collection-id",
|
||||
)
|
||||
|
||||
SENSITIVE_KEYS = {
|
||||
"password",
|
||||
"csrf",
|
||||
"cookie",
|
||||
"cookies",
|
||||
"authorization",
|
||||
"token",
|
||||
"session",
|
||||
}
|
||||
|
||||
|
||||
class PlingUploaderError(RuntimeError):
|
||||
"""A non-secret, user-facing uploader failure."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LoginForm:
|
||||
action_url: str
|
||||
hidden_fields: dict[str, str]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RuntimeConfig:
|
||||
project_id: str
|
||||
base_url: str
|
||||
username: str
|
||||
password: str
|
||||
timeout: float
|
||||
max_retries: int
|
||||
dry_run: bool
|
||||
artifact_paths: list[Path]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EditContext:
|
||||
add_file_url: str
|
||||
update_file_url: str
|
||||
delete_file_url: str
|
||||
delete_all_files_url: str
|
||||
product_id: str
|
||||
collection_id: str
|
||||
file_server_upload_url: str
|
||||
file_server_client_id: str
|
||||
file_server_owner_id: str
|
||||
|
||||
|
||||
class ResponseLike(Protocol):
|
||||
status_code: int | None
|
||||
url: object
|
||||
text: str | None
|
||||
|
||||
def json(self) -> object: ...
|
||||
|
||||
|
||||
class SessionLike(Protocol):
|
||||
headers: dict[str, str]
|
||||
cookies: object
|
||||
|
||||
def request(
|
||||
self,
|
||||
method: str,
|
||||
url: str,
|
||||
*,
|
||||
timeout: float,
|
||||
**kwargs: Any,
|
||||
) -> ResponseLike: ...
|
||||
|
||||
|
||||
class _LoginFormParser(HTMLParser):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._inside_form = False
|
||||
self._form_action: Optional[str] = None
|
||||
self._form_has_auth_fields = False
|
||||
self._form_hidden_inputs: dict[str, str] = {}
|
||||
self.login_form: Optional[LoginForm] = None
|
||||
|
||||
def handle_starttag(self, tag: str, attrs: list[tuple[str, Optional[str]]]) -> None:
|
||||
attrs_dict = {k: (v or "") for k, v in attrs}
|
||||
|
||||
if tag == "form":
|
||||
self._inside_form = True
|
||||
self._form_action = attrs_dict.get("action") or LOGIN_PATH
|
||||
self._form_has_auth_fields = False
|
||||
self._form_hidden_inputs = {}
|
||||
return
|
||||
|
||||
if not self._inside_form or tag != "input":
|
||||
return
|
||||
|
||||
name = (attrs_dict.get("name") or "").strip()
|
||||
input_type = (attrs_dict.get("type") or "").strip().lower()
|
||||
value = attrs_dict.get("value") or ""
|
||||
|
||||
if name in {"email", "password"}:
|
||||
self._form_has_auth_fields = True
|
||||
|
||||
if input_type == "hidden" and name:
|
||||
self._form_hidden_inputs[name] = value
|
||||
|
||||
def handle_endtag(self, tag: str) -> None:
|
||||
if tag != "form" or not self._inside_form:
|
||||
return
|
||||
|
||||
if self._form_has_auth_fields:
|
||||
self.login_form = LoginForm(
|
||||
action_url=self._form_action or LOGIN_PATH,
|
||||
hidden_fields=dict(self._form_hidden_inputs),
|
||||
)
|
||||
|
||||
self._inside_form = False
|
||||
self._form_action = None
|
||||
self._form_has_auth_fields = False
|
||||
self._form_hidden_inputs = {}
|
||||
|
||||
|
||||
def _redact(key: str, value: object) -> object:
|
||||
lowered = key.lower()
|
||||
if any(secret in lowered for secret in SENSITIVE_KEYS):
|
||||
return "***"
|
||||
return value
|
||||
|
||||
|
||||
def log_event(level: str, event: str, **fields: object) -> None:
|
||||
payload: dict[str, object] = {
|
||||
"level": level.upper(),
|
||||
"event": event,
|
||||
}
|
||||
payload.update({k: _redact(k, v) for k, v in fields.items()})
|
||||
print(json.dumps(payload, ensure_ascii=True, sort_keys=True))
|
||||
|
||||
|
||||
def normalize_base_url(base_url: str) -> str:
|
||||
normalized = base_url.strip().rstrip("/")
|
||||
parsed = urlparse(normalized)
|
||||
if not parsed.scheme or not parsed.netloc:
|
||||
raise PlingUploaderError(f"Invalid --base-url: {base_url!r}")
|
||||
return normalized
|
||||
|
||||
|
||||
def _safe_response_context(response: ResponseLike) -> dict[str, object]:
|
||||
status_code: object = response.status_code
|
||||
if status_code is None:
|
||||
status_code = "unknown"
|
||||
url = response.url
|
||||
return {"status_code": status_code, "url": str(url)}
|
||||
|
||||
|
||||
def request_with_retries(
|
||||
session: SessionLike,
|
||||
method: str,
|
||||
url: str,
|
||||
*,
|
||||
timeout: float,
|
||||
max_retries: int,
|
||||
retry_on_statuses: Iterable[int] = (429, 500, 502, 503, 504),
|
||||
**kwargs: Any,
|
||||
) -> ResponseLike:
|
||||
attempt = 0
|
||||
last_error: Optional[Exception] = None
|
||||
|
||||
while attempt <= max_retries:
|
||||
attempt += 1
|
||||
try:
|
||||
response = session.request(method=method, url=url, timeout=timeout, **kwargs)
|
||||
except Exception as exc:
|
||||
last_error = exc
|
||||
log_event(
|
||||
"warning",
|
||||
"http_request_exception",
|
||||
method=method,
|
||||
url=url,
|
||||
attempt=attempt,
|
||||
max_retries=max_retries,
|
||||
error_type=type(exc).__name__,
|
||||
)
|
||||
if attempt > max_retries:
|
||||
break
|
||||
time.sleep(min(0.5 * attempt, 2.0))
|
||||
continue
|
||||
|
||||
status_code = response.status_code
|
||||
if isinstance(status_code, int) and status_code in retry_on_statuses and attempt <= max_retries:
|
||||
log_event(
|
||||
"warning",
|
||||
"http_retryable_status",
|
||||
method=method,
|
||||
url=url,
|
||||
attempt=attempt,
|
||||
status_code=status_code,
|
||||
)
|
||||
time.sleep(min(0.5 * attempt, 2.0))
|
||||
continue
|
||||
|
||||
return response
|
||||
|
||||
if last_error is not None:
|
||||
raise PlingUploaderError(
|
||||
f"HTTP request failed after retries: {method} {url} ({type(last_error).__name__})"
|
||||
) from last_error
|
||||
|
||||
raise PlingUploaderError(f"HTTP request failed after retries: {method} {url}")
|
||||
|
||||
|
||||
def parse_json_response(response: ResponseLike, *, context: str) -> dict[str, object]:
|
||||
try:
|
||||
data = response.json()
|
||||
except Exception as exc:
|
||||
raise PlingUploaderError(f"{context}: expected JSON response.") from exc
|
||||
|
||||
if not isinstance(data, dict):
|
||||
raise PlingUploaderError(f"{context}: response JSON shape is not an object.")
|
||||
|
||||
normalized: dict[str, object] = {}
|
||||
for key, value in data.items():
|
||||
normalized[str(key)] = value
|
||||
return normalized
|
||||
|
||||
|
||||
def get_response_text(response: ResponseLike, *, context: str) -> str:
|
||||
text = response.text
|
||||
if isinstance(text, str):
|
||||
return text
|
||||
raise PlingUploaderError(f"{context}: expected text response.")
|
||||
|
||||
|
||||
def parse_login_form(login_html: str, login_url: str) -> LoginForm:
|
||||
parser = _LoginFormParser()
|
||||
parser.feed(login_html)
|
||||
|
||||
if parser.login_form is None:
|
||||
raise PlingUploaderError("Could not find login form with email/password fields.")
|
||||
|
||||
action_url = urljoin(login_url, parser.login_form.action_url)
|
||||
hidden_fields = parser.login_form.hidden_fields
|
||||
|
||||
missing_keys = [k for k in ("csrf", "redirect_url", "twit") if k not in hidden_fields]
|
||||
if missing_keys:
|
||||
raise PlingUploaderError(
|
||||
f"Login form is missing expected hidden fields: {', '.join(missing_keys)}"
|
||||
)
|
||||
|
||||
return LoginForm(action_url=action_url, hidden_fields=hidden_fields)
|
||||
|
||||
|
||||
def extract_required_data_attrs(edit_html: str, project_id: str) -> dict[str, str]:
|
||||
attrs: dict[str, str] = {}
|
||||
for attr in REQUIRED_EDIT_DATA_ATTRS:
|
||||
match = re.search(rf"\b{re.escape(attr)}=[\"']([^\"']*)[\"']", edit_html)
|
||||
if match:
|
||||
attrs[attr] = match.group(1).strip()
|
||||
|
||||
missing = [attr for attr in REQUIRED_EDIT_DATA_ATTRS if attr not in attrs]
|
||||
if missing:
|
||||
raise PlingUploaderError(
|
||||
"Edit page is missing required upload data attributes: " + ", ".join(missing)
|
||||
)
|
||||
|
||||
if attrs.get("data-product-id", "") == "":
|
||||
attrs["data-product-id"] = project_id
|
||||
|
||||
return attrs
|
||||
|
||||
|
||||
def extract_js_string_var(html: str, var_name: str) -> Optional[str]:
|
||||
match = re.search(rf"\bvar\s+{re.escape(var_name)}\s*=\s*[\"']([^\"']+)[\"']", html)
|
||||
if match:
|
||||
return match.group(1).strip()
|
||||
return None
|
||||
|
||||
|
||||
def extract_form_append_literal(html: str, key: str) -> Optional[str]:
|
||||
match = re.search(
|
||||
rf"form\.append\([\"']{re.escape(key)}[\"'],\s*[\"']([^\"']+)[\"']\)",
|
||||
html,
|
||||
)
|
||||
if match:
|
||||
return match.group(1).strip()
|
||||
return None
|
||||
|
||||
|
||||
def looks_like_login_page(html: str) -> bool:
|
||||
lowered = html.lower()
|
||||
return 'name="email"' in lowered and 'name="password"' in lowered and "login" in lowered
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Pling/OpenDesktop uploader. Default mode deletes all files then uploads files."
|
||||
)
|
||||
)
|
||||
parser.add_argument(
|
||||
"-f",
|
||||
"--file",
|
||||
dest="files",
|
||||
action="append",
|
||||
default=[],
|
||||
help="File to upload in live mode. Repeat -f multiple times to upload multiple files.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--project-id",
|
||||
help="Pling/OpenDesktop project id (fallback: env PLING_PROJECT_ID)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--base-url",
|
||||
help=f"Base URL (fallback: env PLING_BASE_URL, then {DEFAULT_BASE_URL})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--username",
|
||||
help="Login username/email (fallback: env PLING_USERNAME)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--password",
|
||||
help="Login password (fallback: env PLING_PASSWORD)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--timeout",
|
||||
type=float,
|
||||
help=(
|
||||
"Per-request timeout seconds "
|
||||
f"(fallback: env PLING_TIMEOUT, then {DEFAULT_TIMEOUT_SECONDS})"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-retries",
|
||||
type=int,
|
||||
help=(
|
||||
"Retry count for transient errors "
|
||||
f"(fallback: env PLING_MAX_RETRIES, then {DEFAULT_MAX_RETRIES})"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Validate login and endpoint discovery only.",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def resolve_cli_or_env(
|
||||
*,
|
||||
cli_value: Optional[str],
|
||||
env_key: str,
|
||||
default: Optional[str] = None,
|
||||
) -> Optional[str]:
|
||||
if cli_value is not None and cli_value.strip() != "":
|
||||
return cli_value.strip()
|
||||
|
||||
env_value = os.getenv(env_key)
|
||||
if env_value is not None and env_value.strip() != "":
|
||||
return env_value.strip()
|
||||
|
||||
return default
|
||||
|
||||
|
||||
def resolve_float_cli_or_env(
|
||||
*,
|
||||
cli_value: Optional[float],
|
||||
env_key: str,
|
||||
default: float,
|
||||
) -> float:
|
||||
if cli_value is not None:
|
||||
return cli_value
|
||||
|
||||
env_value = os.getenv(env_key)
|
||||
if env_value is None or env_value.strip() == "":
|
||||
return default
|
||||
|
||||
try:
|
||||
return float(env_value)
|
||||
except ValueError as exc:
|
||||
raise PlingUploaderError(
|
||||
f"Invalid {env_key} value {env_value!r}; expected a number."
|
||||
) from exc
|
||||
|
||||
|
||||
def resolve_int_cli_or_env(
|
||||
*,
|
||||
cli_value: Optional[int],
|
||||
env_key: str,
|
||||
default: int,
|
||||
) -> int:
|
||||
if cli_value is not None:
|
||||
return cli_value
|
||||
|
||||
env_value = os.getenv(env_key)
|
||||
if env_value is None or env_value.strip() == "":
|
||||
return default
|
||||
|
||||
try:
|
||||
return int(env_value)
|
||||
except ValueError as exc:
|
||||
raise PlingUploaderError(
|
||||
f"Invalid {env_key} value {env_value!r}; expected an integer."
|
||||
) from exc
|
||||
|
||||
|
||||
def resolve_runtime_config(args: argparse.Namespace) -> RuntimeConfig:
|
||||
project_id = resolve_cli_or_env(cli_value=args.project_id, env_key="PLING_PROJECT_ID")
|
||||
if not project_id:
|
||||
raise PlingUploaderError("Missing project id. Set --project-id or env PLING_PROJECT_ID.")
|
||||
|
||||
username = resolve_cli_or_env(cli_value=args.username, env_key="PLING_USERNAME")
|
||||
password = resolve_cli_or_env(cli_value=args.password, env_key="PLING_PASSWORD")
|
||||
if not username or not password:
|
||||
raise PlingUploaderError(
|
||||
"Missing credentials. Set --username/--password or env PLING_USERNAME/PLING_PASSWORD."
|
||||
)
|
||||
|
||||
timeout = resolve_float_cli_or_env(
|
||||
cli_value=args.timeout,
|
||||
env_key="PLING_TIMEOUT",
|
||||
default=DEFAULT_TIMEOUT_SECONDS,
|
||||
)
|
||||
max_retries = resolve_int_cli_or_env(
|
||||
cli_value=args.max_retries,
|
||||
env_key="PLING_MAX_RETRIES",
|
||||
default=DEFAULT_MAX_RETRIES,
|
||||
)
|
||||
|
||||
if max_retries < 0:
|
||||
raise PlingUploaderError("--max-retries must be >= 0")
|
||||
if timeout <= 0:
|
||||
raise PlingUploaderError("--timeout must be > 0")
|
||||
|
||||
base_url = normalize_base_url(
|
||||
resolve_cli_or_env(
|
||||
cli_value=args.base_url,
|
||||
env_key="PLING_BASE_URL",
|
||||
default=DEFAULT_BASE_URL,
|
||||
)
|
||||
or DEFAULT_BASE_URL
|
||||
)
|
||||
|
||||
artifact_paths: list[Path] = []
|
||||
if not args.dry_run:
|
||||
cli_files = [entry.strip() for entry in args.files if entry and entry.strip()]
|
||||
env_files_raw = os.getenv("PLING_FILES", "")
|
||||
env_files = [entry.strip() for entry in env_files_raw.split(",") if entry.strip()]
|
||||
selected_files = cli_files if cli_files else env_files
|
||||
if not selected_files:
|
||||
raise PlingUploaderError(
|
||||
"Missing upload files. Pass -f <file> (repeatable) or set env PLING_FILES as comma-separated values."
|
||||
)
|
||||
|
||||
for file_entry in selected_files:
|
||||
artifact_path = Path(file_entry)
|
||||
if not artifact_path.exists() or not artifact_path.is_file():
|
||||
raise PlingUploaderError(
|
||||
f"Upload file does not exist or is not a file: {artifact_path}"
|
||||
)
|
||||
artifact_paths.append(artifact_path)
|
||||
|
||||
return RuntimeConfig(
|
||||
project_id=project_id,
|
||||
base_url=base_url,
|
||||
username=username,
|
||||
password=password,
|
||||
timeout=timeout,
|
||||
max_retries=max_retries,
|
||||
dry_run=args.dry_run,
|
||||
artifact_paths=artifact_paths,
|
||||
)
|
||||
|
||||
|
||||
def create_session(base_url: str) -> SessionLike:
|
||||
session = cast(SessionLike, cast(object, niquests.Session(disable_http2=True, disable_http3=True)))
|
||||
session.headers.update(
|
||||
{
|
||||
"User-Agent": (
|
||||
"Mozilla/5.0 (X11; Linux x86_64) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||
"Chrome/123.0.0.0 Safari/537.36"
|
||||
),
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
}
|
||||
)
|
||||
cookie_setter = getattr(session.cookies, "set", None)
|
||||
if callable(cookie_setter):
|
||||
cookie_setter("verified", "1", domain=urlparse(base_url).hostname)
|
||||
return session
|
||||
|
||||
|
||||
def discover_edit_context(
|
||||
session: SessionLike,
|
||||
config: RuntimeConfig,
|
||||
) -> tuple[str, EditContext]:
|
||||
login_url = urljoin(config.base_url + "/", LOGIN_PATH.lstrip("/"))
|
||||
edit_url = urljoin(
|
||||
config.base_url + "/",
|
||||
EDIT_PATH_TEMPLATE.format(project_id=config.project_id).lstrip("/"),
|
||||
)
|
||||
|
||||
log_event(
|
||||
"info",
|
||||
"session_start",
|
||||
base_url=config.base_url,
|
||||
project_id=config.project_id,
|
||||
dry_run=config.dry_run,
|
||||
)
|
||||
|
||||
login_page = request_with_retries(
|
||||
session,
|
||||
"GET",
|
||||
login_url,
|
||||
timeout=config.timeout,
|
||||
max_retries=config.max_retries,
|
||||
)
|
||||
log_event("info", "login_page_fetched", **_safe_response_context(login_page))
|
||||
|
||||
login_page_html = get_response_text(login_page, context="login page")
|
||||
login_form = parse_login_form(login_page_html, login_url)
|
||||
form_payload = dict(login_form.hidden_fields)
|
||||
form_payload.update({"email": config.username, "password": config.password})
|
||||
|
||||
auth_response = request_with_retries(
|
||||
session,
|
||||
"POST",
|
||||
login_form.action_url,
|
||||
data=form_payload,
|
||||
timeout=config.timeout,
|
||||
max_retries=config.max_retries,
|
||||
)
|
||||
log_event("info", "login_submitted", **_safe_response_context(auth_response))
|
||||
|
||||
auth_response_html = get_response_text(auth_response, context="login response")
|
||||
if "incorrect login" in auth_response_html.lower():
|
||||
raise PlingUploaderError("Authentication failed: incorrect username or password.")
|
||||
|
||||
edit_page = request_with_retries(
|
||||
session,
|
||||
"GET",
|
||||
edit_url,
|
||||
timeout=config.timeout,
|
||||
max_retries=config.max_retries,
|
||||
)
|
||||
log_event("info", "edit_page_fetched", **_safe_response_context(edit_page))
|
||||
|
||||
edit_page_html = get_response_text(edit_page, context="edit page")
|
||||
if looks_like_login_page(edit_page_html):
|
||||
raise PlingUploaderError(
|
||||
"Authentication appears unsuccessful: redirected back to login page."
|
||||
)
|
||||
|
||||
if str(edit_page.url).rstrip("/") == normalize_base_url(login_url).rstrip("/"):
|
||||
raise PlingUploaderError("Unable to access edit page; received login page URL.")
|
||||
|
||||
attrs = extract_required_data_attrs(edit_page_html, project_id=config.project_id)
|
||||
|
||||
resolved: dict[str, str] = {}
|
||||
for key, value in attrs.items():
|
||||
replaced = value.replace("@@project_id@@", config.project_id)
|
||||
if key.endswith("-uri"):
|
||||
resolved[key] = urljoin(config.base_url + "/", replaced.lstrip("/"))
|
||||
else:
|
||||
resolved[key] = replaced
|
||||
|
||||
client_id = extract_js_string_var(edit_page_html, "client_id")
|
||||
file_uri = extract_js_string_var(edit_page_html, "fileUri")
|
||||
owner_id = extract_form_append_literal(edit_page_html, "owner_id")
|
||||
|
||||
missing_runtime = []
|
||||
if not client_id:
|
||||
missing_runtime.append("client_id")
|
||||
if not file_uri:
|
||||
missing_runtime.append("fileUri")
|
||||
if not owner_id:
|
||||
missing_runtime.append("owner_id")
|
||||
|
||||
if missing_runtime:
|
||||
raise PlingUploaderError(
|
||||
"Edit page is missing required upload runtime values: "
|
||||
+ ", ".join(missing_runtime)
|
||||
)
|
||||
assert client_id is not None
|
||||
assert file_uri is not None
|
||||
assert owner_id is not None
|
||||
|
||||
context = EditContext(
|
||||
add_file_url=resolved["data-addpploadfile-uri"],
|
||||
update_file_url=resolved["data-updatepploadfile-uri"],
|
||||
delete_file_url=resolved["data-deletepploadfile-uri"],
|
||||
delete_all_files_url=resolved["data-deletepploadfiles-uri"],
|
||||
product_id=resolved["data-product-id"],
|
||||
collection_id=resolved["data-ppload-collection-id"],
|
||||
file_server_upload_url=file_uri,
|
||||
file_server_client_id=client_id,
|
||||
file_server_owner_id=owner_id,
|
||||
)
|
||||
|
||||
log_event("info", "endpoint_discovered", name="data-addpploadfile-uri", value=context.add_file_url)
|
||||
log_event(
|
||||
"info",
|
||||
"endpoint_discovered",
|
||||
name="data-deletepploadfiles-uri",
|
||||
value=context.delete_all_files_url,
|
||||
)
|
||||
log_event(
|
||||
"info",
|
||||
"endpoint_discovered",
|
||||
name="data-ppload-collection-id",
|
||||
value=context.collection_id,
|
||||
)
|
||||
log_event("info", "endpoint_discovered", name="data-product-id", value=context.product_id)
|
||||
|
||||
return edit_url, context
|
||||
|
||||
|
||||
def delete_all_existing_files(
|
||||
session: SessionLike,
|
||||
config: RuntimeConfig,
|
||||
edit_url: str,
|
||||
context: EditContext,
|
||||
) -> None:
|
||||
log_event(
|
||||
"warning",
|
||||
"delete_all_existing_files_start",
|
||||
message="Overwrite mode enabled: deleting all existing product files before upload.",
|
||||
)
|
||||
response = request_with_retries(
|
||||
session,
|
||||
"POST",
|
||||
context.delete_all_files_url,
|
||||
data={},
|
||||
headers={
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"Accept": "application/json",
|
||||
"Referer": edit_url,
|
||||
},
|
||||
timeout=config.timeout,
|
||||
max_retries=config.max_retries,
|
||||
)
|
||||
payload = parse_json_response(response, context="deletepploadfiles")
|
||||
status = payload.get("status")
|
||||
if status != "ok":
|
||||
raise PlingUploaderError(
|
||||
f"deletepploadfiles failed: status={status!r}"
|
||||
)
|
||||
log_event("info", "delete_all_existing_files_success")
|
||||
|
||||
|
||||
def upload_to_file_server(
|
||||
session: SessionLike,
|
||||
config: RuntimeConfig,
|
||||
context: EditContext,
|
||||
artifact_path: Path,
|
||||
) -> dict[str, object]:
|
||||
log_event("info", "file_server_upload_start", artifact=str(artifact_path))
|
||||
|
||||
with artifact_path.open("rb") as artifact_file:
|
||||
response = request_with_retries(
|
||||
session,
|
||||
"POST",
|
||||
context.file_server_upload_url,
|
||||
data={
|
||||
"client_id": context.file_server_client_id,
|
||||
"owner_id": context.file_server_owner_id,
|
||||
"format": "json",
|
||||
"collection_id": context.collection_id,
|
||||
"method": "post",
|
||||
},
|
||||
files={
|
||||
"file": (artifact_path.name, artifact_file),
|
||||
},
|
||||
timeout=max(config.timeout, 120.0),
|
||||
max_retries=config.max_retries,
|
||||
)
|
||||
|
||||
payload = parse_json_response(response, context="file server upload")
|
||||
status = payload.get("status")
|
||||
if status != "success":
|
||||
raise PlingUploaderError(f"File server upload failed: status={status!r}")
|
||||
|
||||
file_payload = payload.get("file")
|
||||
if not isinstance(file_payload, dict) or not file_payload:
|
||||
raise PlingUploaderError("File server upload succeeded but no file payload was returned.")
|
||||
|
||||
log_event(
|
||||
"info",
|
||||
"file_server_upload_success",
|
||||
file_id=file_payload.get("id", "unknown"),
|
||||
file_name=file_payload.get("name", "unknown"),
|
||||
)
|
||||
return file_payload
|
||||
|
||||
|
||||
def register_uploaded_file(
|
||||
session: SessionLike,
|
||||
config: RuntimeConfig,
|
||||
edit_url: str,
|
||||
context: EditContext,
|
||||
file_payload: dict[str, object],
|
||||
) -> dict[str, object]:
|
||||
response = request_with_retries(
|
||||
session,
|
||||
"POST",
|
||||
context.add_file_url,
|
||||
data=file_payload,
|
||||
headers={
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"Accept": "application/json",
|
||||
"Referer": edit_url,
|
||||
},
|
||||
timeout=config.timeout,
|
||||
max_retries=config.max_retries,
|
||||
)
|
||||
|
||||
payload = parse_json_response(response, context="addpploadfile")
|
||||
status = payload.get("status")
|
||||
if status != "ok":
|
||||
raise PlingUploaderError(f"addpploadfile failed: status={status!r}")
|
||||
|
||||
registered_file = payload.get("file")
|
||||
if not isinstance(registered_file, dict) or not registered_file:
|
||||
raise PlingUploaderError("addpploadfile succeeded but no file payload was returned.")
|
||||
|
||||
return registered_file
|
||||
|
||||
|
||||
def run_dry_run(config: RuntimeConfig) -> int:
|
||||
session = create_session(config.base_url)
|
||||
_edit_url, _context = discover_edit_context(session, config)
|
||||
log_event(
|
||||
"info",
|
||||
"dry_run_success",
|
||||
message="Dry-run completed. No upload/update/delete actions were performed.",
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def run_upload_mode(config: RuntimeConfig) -> int:
|
||||
session = create_session(config.base_url)
|
||||
edit_url, context = discover_edit_context(session, config)
|
||||
|
||||
delete_all_existing_files(session, config, edit_url, context)
|
||||
uploaded_file_ids: list[str] = []
|
||||
for artifact_path in config.artifact_paths:
|
||||
file_payload = upload_to_file_server(session, config, context, artifact_path)
|
||||
registered_file = register_uploaded_file(session, config, edit_url, context, file_payload)
|
||||
uploaded_file_id = str(registered_file.get("id", "unknown"))
|
||||
uploaded_file_name = str(registered_file.get("name", artifact_path.name))
|
||||
uploaded_file_ids.append(uploaded_file_id)
|
||||
log_event(
|
||||
"info",
|
||||
"file_registered",
|
||||
uploaded_file_id=uploaded_file_id,
|
||||
uploaded_file_name=uploaded_file_name,
|
||||
)
|
||||
|
||||
log_event("info", "upload_success", files_uploaded=len(uploaded_file_ids))
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
config = resolve_runtime_config(args)
|
||||
if config.dry_run:
|
||||
return run_dry_run(config)
|
||||
return run_upload_mode(config)
|
||||
except PlingUploaderError as exc:
|
||||
log_event("error", "upload_failed", message=str(exc))
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,230 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jh2"
|
||||
version = "5.0.13"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/47/b1/b2b7389b2e0ddac90a1aecbf4a761db8790de85dace7695c01173ed083cc/jh2-5.0.13.tar.gz", hash = "sha256:f8c78cffb3a35c4410513c3eb7989de36028c84277c04f07c97909dd94c23a75", size = 7322505, upload-time = "2026-05-29T05:21:43.516Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/96/c38d3555cc9bbb1c309c896e99dbb0b1f76308bed7f2d6226cdbeaeb3eee/jh2-5.0.13-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:2195557f5953ceee743d0eaab0b09ec537bbc1b9a2c6c1463106bfca9b03805f", size = 601454, upload-time = "2026-05-29T05:19:23.763Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/84/a3043ddaf636cec0f4c3aa179a9ef3a59db3d06f770ab6007133ca31bc5f/jh2-5.0.13-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e734f8a5cf002316cc81b80d8a91a0f7c62d631270abd913746644fb9ae288d", size = 384187, upload-time = "2026-05-29T05:19:25.504Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/8b/72dba3fb64bf01355a0bbd78cbe0a2635759d4a6f273c1b118760ca8ecf0/jh2-5.0.13-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ba5e545b3209e8aa5b6af88e7032814473e57d60d32d3fc6b6185453733a4b26", size = 389346, upload-time = "2026-05-29T05:19:26.926Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/2e/6f07e42de6101d4555f14be1e4f42a056c9a44d7ee8f745b3c188c96cbe0/jh2-5.0.13-cp313-cp313t-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2a37164eb8f2462268e6252db93b009cb685fa060b62ad217bf1b333b450c68d", size = 510278, upload-time = "2026-05-29T05:19:28.175Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/a5/85682dcc959682aede0f80206a16574c51af51ed26eb1e87252fd31c6ded/jh2-5.0.13-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b4eae2d5bc1a91033450fd4381c04f33db7d4c9cfd31046c4708c8c1323c06d0", size = 503090, upload-time = "2026-05-29T05:19:29.341Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/4c/a8a301d494c0a1c2859951d44b4fa54f3b2db91826c719ce5be8b662c5c8/jh2-5.0.13-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d0a10036cdf8bf1a36b07c10b69ad0d08ce31d99b8962e3d055ee417fe3423", size = 402843, upload-time = "2026-05-29T05:19:30.505Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/5f/5f9e0933f458a89005af3b2aa7d1c61cc3cc57c8a633fd54a95acff85121/jh2-5.0.13-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ad4d68340e38a8474a48c424e2929d3e2e82e35e4e8ff1188decb1985d1b4d7", size = 386327, upload-time = "2026-05-29T05:19:31.988Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/3d/353ec4a6763e8b6be8d162e6b23f7e1499e75b1bb934cee1e5b5fc062f1e/jh2-5.0.13-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3f8c5e81254b986f8abe9fd974e7ed595aeaead3f3066ce9ed95dfff236ceca9", size = 406978, upload-time = "2026-05-29T05:19:33.438Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/e5/cca58b58cb8bab1891f45da43ef91b498ff93d573a1a2836af11962ccfb8/jh2-5.0.13-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:da030729eb8ef386569e78c10e2429adb10026ce82989fdeb906b037314d66c7", size = 560030, upload-time = "2026-05-29T05:19:34.879Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/c9/c3391d8607781016daa1881d8d80f81e0cf0302ff06c86342c492279cf48/jh2-5.0.13-cp313-cp313t-musllinux_1_1_armv7l.whl", hash = "sha256:a5d2ceb2e9d42ac236f0da7b2f9e8237818d32a10d892b06f30f7160ee4365ae", size = 664407, upload-time = "2026-05-29T05:19:36.138Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/b4/3375744e2f33d097da241dfd5262c9997a87c2b948db2385fbf100c7dd12/jh2-5.0.13-cp313-cp313t-musllinux_1_1_i686.whl", hash = "sha256:1f4dffe1c6ed0d4b0d2d9a45d064622bb786cbf87db35eac469dd4c376ac6fb6", size = 625242, upload-time = "2026-05-29T05:19:37.93Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/2f/e33763b1f66817bd8c848777f05cf3781ad9ee1c642741458bfb66da1921/jh2-5.0.13-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:bdeb48657db508e8bc299744aa431798efdf090feb3959cf0123167c9251ebb0", size = 591124, upload-time = "2026-05-29T05:19:39.373Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/92/5a8de2f6936c7af77555299732795f4607d2cefb974ad485acf25b3f998b/jh2-5.0.13-cp313-cp313t-win32.whl", hash = "sha256:b41867d5decd7cd62e12cdacfc45c5d8f6eae872578ae01d78900174d5e47b82", size = 238101, upload-time = "2026-05-29T05:19:40.917Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/71/0f7ba74bb6681e60e871284772a216a50cfd63f4f62ef52edd13d8a7c057/jh2-5.0.13-cp313-cp313t-win_amd64.whl", hash = "sha256:62aee4bd128e17871980e4847cc4b94b67651de612bc96dd3d7ebf647c68a6e4", size = 245923, upload-time = "2026-05-29T05:19:42.338Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/dc/11434a6deee70630f5f38cc40813692e1faadd4259f242f4f810f35709eb/jh2-5.0.13-cp313-cp313t-win_arm64.whl", hash = "sha256:267aaa7a96f54452e1e07c7701799bc7817e1f3bb2de09301b2c492841e4c98e", size = 242596, upload-time = "2026-05-29T05:19:43.853Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/f8/e9e82b7b51947edbf762edb26eb9bcfadda10fc7526d092244528e9cea8c/jh2-5.0.13-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:1dd137086b13d55a53c4b7b8b75869c824060a4d35615e4f49180ae58c7783f5", size = 601736, upload-time = "2026-05-29T05:19:45.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/f4/e82b4c5797c20facb2f1b54f893a422f8ba44f83bc66da3d60d90a1c3064/jh2-5.0.13-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75d0b2bffb2e260a0f3578a11a4999a713c94d1bd8de7cfe692c840e20ae83cf", size = 383983, upload-time = "2026-05-29T05:19:46.687Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/df/f439704e7a884b5bd0181188b30217d71d48ce6bb9a9d7e8378d00ed3497/jh2-5.0.13-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f681e197bf6552df3bb7a3abfd676e81316b69e36e7263e72383477174032ca2", size = 389233, upload-time = "2026-05-29T05:19:47.973Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/9e/c2cbdb2261724edb83fa0bdb260f75ad9b091189ada1651608f8ade71906/jh2-5.0.13-cp314-cp314t-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:bde7e7ff0f17e87aca9ef23f66b80540a2de0ff8e2c26af55eb2e288a2a5f74f", size = 510268, upload-time = "2026-05-29T05:19:49.183Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/cf/65925f2aaea92e5f4a90a578b86a975bf6327ad53d82035a4d94f8f63580/jh2-5.0.13-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f8d364d53cc0854d910f2305726a8254acb527f13fa933dc213a591e21e7ffa", size = 503003, upload-time = "2026-05-29T05:19:50.699Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/fc/684ae4efe7568ec9f8ce427b6ecdc7387f870b3bbdcee75c370326685865/jh2-5.0.13-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b572daaafbd867bb5c5f324c918704ddd6d76af1cc7c2be483c3649b76ce7940", size = 402856, upload-time = "2026-05-29T05:19:52.004Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/41/009c8b95f471ca83667e63e87a845f634f7b100f9fa2c1cbfc090c142ec9/jh2-5.0.13-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0772ad66f346038fe05cb2928866ff1b21bcaa9ec3f14709fa1d419ed1dc287", size = 386592, upload-time = "2026-05-29T05:19:53.201Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/8b/5b49f66cadc6d89db5339b602c06a9567ef493c33b860324f24a96b390a4/jh2-5.0.13-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b1d8313b13acfdef19c5ff793fdf397bae3d8d75181f6eee62604f13e4ec20a8", size = 406930, upload-time = "2026-05-29T05:19:54.441Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/a4/7b997c7787ee6316c6b4f7bc0ff4d80c062ef3288038fbee870ed83f0946/jh2-5.0.13-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:99aedd20daacb32f377152c9103d8d0271f32d1932bd39a5ed2cd2eef97f7e07", size = 559781, upload-time = "2026-05-29T05:19:55.659Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/08/b75afc38215f961ba8d92af64a9e7cba57767829786484c5cb98fc1a1d2f/jh2-5.0.13-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:53b1df29ffe966cde1d2f0085b6acfcd237504627d406a51717ef3849ec88831", size = 664237, upload-time = "2026-05-29T05:19:57.031Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/92/0fad513d95376611b337c631abd0263e641b78b0adbe80f3cf561e614d6e/jh2-5.0.13-cp314-cp314t-musllinux_1_1_i686.whl", hash = "sha256:0ee6a5dd24a1dc0772a89b3498ed20f60167fbc633404b1e02895f04ae66c47c", size = 625249, upload-time = "2026-05-29T05:19:58.447Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/28/4f70df7bfaf1cdb560340400b46849cfa96e4b08d2430793501013d7ee8a/jh2-5.0.13-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:521f871864880cabe361060f070516a56a54abc3e96f07bee9ba8b061eada891", size = 591017, upload-time = "2026-05-29T05:19:59.839Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/58/ed891762aa68542fa7969c4cbb109817231740f342931165f60ea7a2a923/jh2-5.0.13-cp314-cp314t-win32.whl", hash = "sha256:5909e2273f8b6fcea93c95564aa0f366ff9261a544a7a5d640ea95341321cc37", size = 238041, upload-time = "2026-05-29T05:20:01.228Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/13/46ae2bf16baad7841526dba44668083f9de3e427027e4fa44a567cb778d6/jh2-5.0.13-cp314-cp314t-win_amd64.whl", hash = "sha256:2cc4923ac6f57753e0cb4aef054a04f5d5c744b274dabf863f695fa3c2fd75f9", size = 245861, upload-time = "2026-05-29T05:20:02.757Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/49/3652903fc82c0e5c82e9caa3445e3e514b168969c2dae3da15c46627661f/jh2-5.0.13-cp314-cp314t-win_arm64.whl", hash = "sha256:d3cf203de31371289d4c1e6b4d30c9f6de95b2bd2b300fe5468ff946299d0e48", size = 242699, upload-time = "2026-05-29T05:20:03.9Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/f3/8c151f371f7962902e6f1f494b50abc1b8be27305a9d12cb88cf049c5a80/jh2-5.0.13-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:3f54095cde010ff4e52f23dda92b31d3d3160c64f5f23fc6e181f8c7938afd33", size = 618581, upload-time = "2026-05-29T05:20:05.043Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/3f/a2d5458586c024e0076ae8182dcbb71450bd9d4dd65ffdaa659313461bd4/jh2-5.0.13-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f810dd02b4a9647eacbe4eab617111c0e3ffc68e923e3ff172948db601d2ba1e", size = 392776, upload-time = "2026-05-29T05:20:06.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/1f/cf205104cfcef4bd26aa2b736f36ddf3739b69ddaf14d23a5f7697673d74/jh2-5.0.13-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29c23f3937f257beb5cd5b17f8727404f1267ff25d26e54c0a35d5defd0c895e", size = 397943, upload-time = "2026-05-29T05:20:07.479Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/82/bd218132e5a081293cf3d7de5cf41d871f3f88808c23f6c6aebbe073fb5e/jh2-5.0.13-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c697b250a935030a128b45b41f507a4d33a082d8b8e12311c7e73fbb3970f057", size = 520351, upload-time = "2026-05-29T05:20:08.936Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/7c/78b564899237909ddf4103569be59ea31b6c00f8b4f1c178cc57c6e4e45d/jh2-5.0.13-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47d3203ed6fec39df4922764817979fad3a13d22cf750adc9cf9dc2b3399eacf", size = 511162, upload-time = "2026-05-29T05:20:10.239Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/3a/b59157363139436cb60620353aa8b687197c9a4ec1eb87952004a8843e6e/jh2-5.0.13-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5146468a76bbe8f4035f23a78813c5c433c70c0ca44eb0ba5558627a9e644b8", size = 411838, upload-time = "2026-05-29T05:20:11.515Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/a4/18b35000b00d9fb72003e0eacf66b3af828a0bb897c21a287a33d2025138/jh2-5.0.13-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85d3d338284fbfbe173243c3cc62e510c6fa0d6d023e67da2292bd7578c734e3", size = 395975, upload-time = "2026-05-29T05:20:13.157Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/42/f017b6b7b666e6ef615f7c64fd748201d0b20edeed094182c4f37f23a6e8/jh2-5.0.13-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:641f65f5414b8990251bd7609513a696831558f69d2ca77d65dc3f7087f31267", size = 417493, upload-time = "2026-05-29T05:20:14.394Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/fc/054443bd40a9827ade99508482185692530cf56a50d4ad8ed2aba761f11d/jh2-5.0.13-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:cf3dc6f7c14340241260bf63e5d6006b2ee7db45dafcb39c16fdf164ea0240a0", size = 568373, upload-time = "2026-05-29T05:20:15.914Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/48/dde093626bb976722c940053ceaa82407bd358e07fa6a1690590456d21b5/jh2-5.0.13-cp37-abi3-musllinux_1_1_armv7l.whl", hash = "sha256:fb8e37ecf37382c02af83da521df5a77561441bc4cbb93b00627e015b7bbf349", size = 673766, upload-time = "2026-05-29T05:20:17.136Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/94/edae999301bb28738e17cf54ecd50b7d7d9fd96d9536376bbeb9b8498526/jh2-5.0.13-cp37-abi3-musllinux_1_1_i686.whl", hash = "sha256:296ee44dfad08ccc3857daf433dfa4a555d5b45f2df39503ccdd90ff10b32943", size = 635045, upload-time = "2026-05-29T05:20:18.633Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/5b/d9fd0fc71e43c37c30d581affbb7371fa8177ce211b663457939bbed9e61/jh2-5.0.13-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:1a75eed545420a31ea356e8046b8d86fcf694234dd1035191734a03720fb8a44", size = 599870, upload-time = "2026-05-29T05:20:20.117Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/ec/66c17d8aec77d41c8f93145dd7faa7138b9f1bc370fa685008bf92d75f9f/jh2-5.0.13-cp37-abi3-win32.whl", hash = "sha256:4812710810010c428db04e9290a13117f7b7c5a3a663b46a0586fe101cf25c9d", size = 245231, upload-time = "2026-05-29T05:20:21.275Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/3c/107f16a84c8becef5281040fdb50681f06e4615bb4a754f211abe7d61a6f/jh2-5.0.13-cp37-abi3-win_amd64.whl", hash = "sha256:5d61bc0a62b4eee11039bf29d4bdbb26efcff760dfb9d8e0739740028aa52b0f", size = 252085, upload-time = "2026-05-29T05:20:22.426Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/7b/e8baeef7655449a6c9abee0165fcb1c63d12ec5432b8d810ff5910184690/jh2-5.0.13-cp37-abi3-win_arm64.whl", hash = "sha256:3f74dad9036e599eed51576492b00955237b86cf886f96844708a81cd0f5c710", size = 249139, upload-time = "2026-05-29T05:20:23.558Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/1e/be44f6e9ff5689fcdf9a2c0ed30880fd5da303676d89d6457a603450b7b1/jh2-5.0.13-py3-none-any.whl", hash = "sha256:2b311414989da48a155721fe47d0dc3e3b75360efd62d779aaff72d786be91cf", size = 98909, upload-time = "2026-05-29T05:21:42.086Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "katoclock"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "niquests" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "niquests", specifier = ">=3.18.5" }]
|
||||
|
||||
[[package]]
|
||||
name = "niquests"
|
||||
version = "3.18.8"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "urllib3-future" },
|
||||
{ name = "wassima", marker = "sys_platform != 'emscripten'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ae/3f/3cbfd81c507c4d58b57afc04d75b6feb00cbcdeba43852a7506100af2eae/niquests-3.18.8.tar.gz", hash = "sha256:06244ef4d1c93b067295b22c1d7a8938be948af1227d368c22acf531f61fdf57", size = 1028824, upload-time = "2026-05-10T05:29:15.663Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/2c/01d6978e16536922baaafad903adbc55880b11f23595f90a3b52478ce234/niquests-3.18.8-py3-none-any.whl", hash = "sha256:9b1a3a378277bfca4b2438877d0954e989e4d3f3f75da7bb83bf7204dfa5f1cf", size = 208878, upload-time = "2026-05-10T05:29:14.003Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "qh3"
|
||||
version = "1.8.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0a/83/8a57595b1c8ba57f70abcb731db647b3beabad52f76e282e1fc7f666c5a0/qh3-1.8.1.tar.gz", hash = "sha256:28d5d73903850d5733eb4c7f6cc3a81d40099f98204b5c8482330a1fb7ca92c8", size = 335758, upload-time = "2026-05-07T09:38:00.192Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/4f/6a464e814b7bc04809147030633751d791a08cce53609816867b9f40cd00/qh3-1.8.1-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:c708733fa1255542c57d6e8b253e226ab096eb5a5a8f0dfed477fb013f0688cb", size = 4412720, upload-time = "2026-05-07T09:34:33.338Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/5b/cec395ceb03bc07c739f14d6b784650a30504d240c261dac4ce2310b5517/qh3-1.8.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4e37418e7a0b606348f06dedc5742032ca589478e28e24e5c455005d527ec75", size = 2156307, upload-time = "2026-05-07T09:34:35.257Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/de/d717b17b1c267b6be327eed0639d2cb404688adb5c633c5f0e8ff5d8ac6c/qh3-1.8.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f7d947493288a3eee0294f589bcc564831d7fd4811019076f888d04c225421b4", size = 1850656, upload-time = "2026-05-07T09:34:37.203Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/92/3c0ebd8fb2bb7a98e906fa081cf9ba5c06d2e88a10105eda822ffc6ab304/qh3-1.8.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1063d1586dd4ad04c4ac467f31d5f0d6bfd31580b64dcb5b6f6cfb0cc0e49955", size = 2028377, upload-time = "2026-05-07T09:34:39.075Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/17/9f8c04b630c142172f91adedb8c53973afd0e1e1e73156688b2cb38c2eee/qh3-1.8.1-cp313-cp313t-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2ec8b3e339561c5fc2e8623b95afb746020528fe0f85b6e879d218dae96182a9", size = 2022675, upload-time = "2026-05-07T09:34:41.023Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/69/66294a85652b43679fee99c4b3df043aa45bf385cdcb8afebc2e9228c0f0/qh3-1.8.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42386c9371fcdac54db0f02f735f251b97a0a35aa23dac921a3c2cf2f7052bad", size = 2024210, upload-time = "2026-05-07T09:34:42.605Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/9c/b0396a856c398aacef94f000db47351487ccb61f73b2ca7fc7e3e89bdd73/qh3-1.8.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eed058868a9f5b37986421ad69712f218ebc08ee7ecada653e306a9d67e89880", size = 2087658, upload-time = "2026-05-07T09:34:44.405Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/df/0318b695f64f223d7c1d812b70a5ca6c6efa5a5c133a66fb1924adb94951/qh3-1.8.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:992753fe1631ae1088e1e4cba2cc3d88bb42834443a03f54f129aa584c41ad11", size = 2353324, upload-time = "2026-05-07T09:34:46.05Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/f2/18b05890fed464773ca89ca62c2c10be54574e6f5e05360035a7a2a04c71/qh3-1.8.1-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:da63c2cc1d358ad9be9cd630aa89498041bb292949fdf243364ac019ec6f685a", size = 2007199, upload-time = "2026-05-07T09:34:48.041Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/28/f69792686ea4aa76939c4c2b8b443dae46fc090e3297aee9424f5bb8a995/qh3-1.8.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:bb898db0154d0673b94d53e6b0345ec9a97932a0f730dd9140bfefa7a65eb234", size = 2343012, upload-time = "2026-05-07T09:34:49.746Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/61/915d5ce7692128ddc5da93ee2f891a6eb21a42d7453116d77352b6be66d4/qh3-1.8.1-cp313-cp313t-musllinux_1_1_armv7l.whl", hash = "sha256:0ebd0af3fe6208580fa68b4b0be8a40280ab295aaa5c6826447ade597129972d", size = 2120677, upload-time = "2026-05-07T09:34:51.883Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/63/d3d1fbb3247b6627de717e34b6805e2d4692fcb290147a16dd87e7be3761/qh3-1.8.1-cp313-cp313t-musllinux_1_1_i686.whl", hash = "sha256:f389d6e4cf522757ffbc1ca97976391cf0e7c13f8ea9842c358b9e959897cdd5", size = 2219803, upload-time = "2026-05-07T09:34:54Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/24/351bc4d48687e73e0359cef84ce9305df1da1e662e673a99c43113bbb294/qh3-1.8.1-cp313-cp313t-musllinux_1_1_riscv64.whl", hash = "sha256:96259508b1fff9ab5fe8e734e825992ef1aec7c5dfced4afb8839b15831e2082", size = 2117467, upload-time = "2026-05-07T09:34:55.553Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/01/0c93ceb0d25bf9b214c7088bd56fdb081373823b647283bf266c1638c29f/qh3-1.8.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:83bdb9963cde76a7bd3f7f2e3fc9e7df47e014d5f9d23cce99b213a4275018c5", size = 2580307, upload-time = "2026-05-07T09:34:57.876Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/fa/82ee54b8fb0e7b7f593d590ebfd06a7a1525adca4f561a361bcd9c25e8f4/qh3-1.8.1-cp313-cp313t-win32.whl", hash = "sha256:3d52a652758dc810c0541f86e6149675e78dc3119a93b8c3d052e65996858bdd", size = 1855354, upload-time = "2026-05-07T09:34:59.828Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/a6/28cd099b7031107589bd22c87bc2a15f5d55bfdba956162d1b0381324e60/qh3-1.8.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e74cb0677e532958d1cb105d3defe2dd6786b5a1b73e3bee392aa466310a3286", size = 2119773, upload-time = "2026-05-07T09:35:01.376Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/5b/3b6b3193c8126d02b0fc16543e3910f0a25d52124019a2e7a290e65f0154/qh3-1.8.1-cp313-cp313t-win_arm64.whl", hash = "sha256:583eabc49063174e74171451864e1c542ddbbb49b3c3f218b036acbb070fac21", size = 1955463, upload-time = "2026-05-07T09:35:03.24Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/23/c41c6e1aa5b0ca0d5fbb4af3d6071891306073ab1270d9feba7f54a5b01e/qh3-1.8.1-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:a246de889fd4a037123b41bbdc3ee5fd8eb8f9254595cf2d47e34d604cad5336", size = 4413625, upload-time = "2026-05-07T09:35:04.779Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/de/85f2c7de0903a82f7114a2fd79531e6b469054451318e4a41c01f8fda013/qh3-1.8.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5de3c63bc36ff8b4a6f95318646c9fc14aba12eef4bf95a16942a645bf710131", size = 2156690, upload-time = "2026-05-07T09:35:06.54Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/9b/9a034cb5f9e4c1073c0136cb5b3632dc84178b65eca9d3acfa736f244742/qh3-1.8.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bef5914389fd03864922fd16656ed4c086f854805485cb04013c1e9fed4ccad9", size = 1850747, upload-time = "2026-05-07T09:35:08.148Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/93/a54db8e3b208395b460b9f62b100216abe7a23fd5140cd17cb9bee1c2814/qh3-1.8.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bf8811c2648ad7e43b1537e01ab1ef49ead72ffd40662107340d3fdd610f9552", size = 2029003, upload-time = "2026-05-07T09:35:09.985Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/00/c2623d4c68e594b6c6ef654e318a7b6cc0f50bab3a12c43e3fe0a0d3aa4b/qh3-1.8.1-cp314-cp314t-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:528364da7fa7e58bff1ef49f714303594e78755d2ec0e9c69dc906aaad7e74e0", size = 2022979, upload-time = "2026-05-07T09:35:11.717Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/e7/0bd8a42a5b70fc0dfa3d12ec332d51eb177111a7bffd82a8932ff97dd2a8/qh3-1.8.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2df3ebda00d1707c190bc4f827343bf43f247a03d3caceae0ed549df9a23a136", size = 2024511, upload-time = "2026-05-07T09:35:13.701Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/59/2cc22875e9d35b94c24a1947c0c50fec09a38fa956416d48634cab38fa1e/qh3-1.8.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40909e358c44f48b49a878490ca3f4bddc3c0d76b23c90e172aa1a2c6464b097", size = 2088269, upload-time = "2026-05-07T09:35:15.577Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/f6/27dfa7a069fbc77e1fd6cd0775d749bbd4b1c21987bf18abe151497a4f77/qh3-1.8.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc9dbd73bd30dc3949c05ee0a658244201eb1932e6ed2312c5359a3c09567f11", size = 2354129, upload-time = "2026-05-07T09:35:17.411Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/ce/b766c0ce5ae3c2a026c75bedcfa53269785a01ccad501ac060611136e6ff/qh3-1.8.1-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:0d23a4b8c76a5fb4290b50319f6a77ecbb9572978b64f0cd014a26f12b769124", size = 2007563, upload-time = "2026-05-07T09:35:19.24Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/da/412b9e8bf6e9e73735d9036acfb2c2ce1bfe05fe1f88fdbcd249a8634072/qh3-1.8.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:f502271b4286c3ea788e76f2e7dba25d562b429ffc78169b7d23be87bfecfbea", size = 2343096, upload-time = "2026-05-07T09:35:21.086Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/19/0fbb63400412faf5ea109e47a105e22fc176fa1a17c54959516fd896c2e2/qh3-1.8.1-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:e0a42c5407b57a96c4408e72beba6ddad7e5540fe8fefeb0aae970525034be44", size = 2121245, upload-time = "2026-05-07T09:35:22.619Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/ec/754fd5c208095ba10eb68787b6c26bb8b39852848eca8d0b11000c699a02/qh3-1.8.1-cp314-cp314t-musllinux_1_1_i686.whl", hash = "sha256:6cc769dc57b831d895a7c6a260a6595483f96144d640783d771f55b814074c8b", size = 2220516, upload-time = "2026-05-07T09:35:24.397Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/f0/1a8381fe5f5d62908f2d7f82b1b0c935a44f441095c8eb890ba75c6e92c6/qh3-1.8.1-cp314-cp314t-musllinux_1_1_riscv64.whl", hash = "sha256:6fb0e6f1b895df5f507aa98cb30da4a2782a68c1890634d408f29caa8ba8168c", size = 2118169, upload-time = "2026-05-07T09:35:26.255Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/b9/da5a44c1ba1f7459c896f5bbc4efccb9840f20e2e82211a82add0b5503f6/qh3-1.8.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a411ca298f827a43dd8b6b6b9ebbf07667f57a9a4728874bbf191a500cd7958f", size = 2581300, upload-time = "2026-05-07T09:35:27.971Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/7a/cfb00853baafdedaf1aa7ef2f5155dc0dfec1443615a6c246f85cbdc691b/qh3-1.8.1-cp314-cp314t-win32.whl", hash = "sha256:899d2224771ab4f45306d6787aef3ce748b9350b74a2b4a46eb5f4f3a4ab142a", size = 1855050, upload-time = "2026-05-07T09:35:29.54Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/6e/e99f78e71e2b9af9f59c8e2ae8ccfa759559d5718477b9ad4152b95b9ee8/qh3-1.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:30fa62bd99537c62e73222e00b145bc7c249bd4c7f1c77124bfeb12f562b16e8", size = 2120017, upload-time = "2026-05-07T09:35:31.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/87/0abef3dab1a1df163f5655b7d90c6b9e4347d3e0a7eaacfac717a3dfc431/qh3-1.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:da34cfb55561d3f1bef686343df686f4fe149e775f250293d6c56be16594c7c8", size = 1955992, upload-time = "2026-05-07T09:35:33.122Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/5b/2ea75701d7e9fe81d60f4307cbd370d023879e7ad65c30af5dcf335f5ab0/qh3-1.8.1-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:576cfd6a657f3b5f574e601500461d586b2e7037fa24ea876f5f724a10f2f552", size = 4432070, upload-time = "2026-05-07T09:35:35.011Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/82/282a7250c84ae3ece859f97a2023e063255be7e70c5b99b08fca7ed59d99/qh3-1.8.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d09950baf910e3cb0053d546a8fd8bdb7dae5f4d217a88f51ffcf2dd7b1397d", size = 2161987, upload-time = "2026-05-07T09:35:36.802Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/74/1f1caa2c796252b11ec6356d04f02b0e949355a4b80fbd5e15dc5924f2b3/qh3-1.8.1-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8e75516f5c152ccb69f2290b75510f1ebb5766f07da19d9c0b3208c024979be4", size = 1853637, upload-time = "2026-05-07T09:35:38.51Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/ce/9b2a1d646257b97b0d28d324164a8a28060f62367e2575b95a3706ec0bbe/qh3-1.8.1-cp37-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a60fb2f1b363004a8ce7188cf6eb7867a2eddd552b16ae028b266a317528ca69", size = 2033035, upload-time = "2026-05-07T09:35:40.246Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/c3/95c439ab1d2b1b019685972539a38ffd1909a3095df0c339821b65d61503/qh3-1.8.1-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4ac855e16b5f31022fb4c1191cf9de17b65dd749aa0a2bceeddb1a066e8f5299", size = 2029467, upload-time = "2026-05-07T09:35:41.684Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/5e/dfffea7c99af15311d9fd1cdb34a973a9557cf6e046bd7e9d9f8f42a29fb/qh3-1.8.1-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5945d35737d45431543807794a7d06fa38d9440649b3467fb490b26bb81b645", size = 2031545, upload-time = "2026-05-07T09:35:43.508Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/6c/b4885d04791b516bf677d8d651febe4e533d75b61376f9d49cd6df7b6004/qh3-1.8.1-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:182cf18d37eb64d619c1d284381930d8cdf8c889de35a873d490cb555369daf5", size = 2092163, upload-time = "2026-05-07T09:35:45.387Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/96/c435fecb8d420ec2924c11fbc396ad8d9d7b4861ac4e48dfb8631fc1d3bf/qh3-1.8.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c23816f4f4e017528cb30eef697aa607578ad42d3111ae7552c73666021d5268", size = 2358583, upload-time = "2026-05-07T09:35:47.426Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/1f/84ae3135de408b2b3f40e1c49fc14b52b7f763ba814958bec60ce7bfe44d/qh3-1.8.1-cp37-abi3-manylinux_2_39_riscv64.whl", hash = "sha256:31669c33fe8c76cca42cf6d9650734904fc7df0a7a6b16e4e7dbc5fe58763ca6", size = 2010252, upload-time = "2026-05-07T09:35:49.076Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/14/faae208dfea35db41cf016c5622fd694b45f25fe09f29f683140de145529/qh3-1.8.1-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:8e4cfc6d8ae494f0ae052492f1fc51ad1a3ce7eb3f4ed3bc0276fc7e3f4ee1c5", size = 2350931, upload-time = "2026-05-07T09:35:50.701Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/85/f5f430cb2b8c400188dff1a0a09fa242122fb6e11983236f8fd45dd5d150/qh3-1.8.1-cp37-abi3-musllinux_1_1_armv7l.whl", hash = "sha256:8f716284e10890708e1911df8b60debca8eaae1e752298719f615df492c54c91", size = 2123379, upload-time = "2026-05-07T09:35:52.134Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/2c/882fcdb0b6f604490d737313671eee37c7e965a6bb13aafda51fa8be41ef/qh3-1.8.1-cp37-abi3-musllinux_1_1_i686.whl", hash = "sha256:58e2626ce9d8454c5a1a43cbdb616449221027be201b5d9bc1ba4be9d8aeee2a", size = 2224567, upload-time = "2026-05-07T09:35:53.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/9b/0eb23f70934580f43f9f658df717b38e2515bebea3d04b58c0f5bc78982e/qh3-1.8.1-cp37-abi3-musllinux_1_1_riscv64.whl", hash = "sha256:7132d3498a985f7588b5f9a6285aed813fa034f0c6f491a943c0f306d7ebb0eb", size = 2120849, upload-time = "2026-05-07T09:35:55.79Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/f3/6e16cf55f42aec85af2c2c5ae5fd46a2a8eefe80f1f6b669a1b5969a7172/qh3-1.8.1-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:2bd4b95d4c9e9e5a4df36867aa344feae8d7b5a511d56035603ccbbb41b00a84", size = 2585895, upload-time = "2026-05-07T09:35:57.745Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/1c/fb8b3ffe854642f9bcfe70d9fea2c16b2fe2fc7c1ec1c4dd4302269a827f/qh3-1.8.1-cp37-abi3-win32.whl", hash = "sha256:bbf293fb4a6aa3721c559e2a7f12cda259eb837b962ddc2fa2246172b0869355", size = 1865051, upload-time = "2026-05-07T09:35:59.228Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/e8/3f871ea3f1d4e17536f38410c5c333b4aa3c60f74350eb91799d482243de/qh3-1.8.1-cp37-abi3-win_amd64.whl", hash = "sha256:d4c53c82f7cef0a2a8714cc3c48fca970f496132542483ad1489fb8f2b9c70c5", size = 2129090, upload-time = "2026-05-07T09:36:00.836Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/17/8e0c85c5c37a1d0f3cd786d8a214721ac6ff375f81c8bc42b085afeaba91/qh3-1.8.1-cp37-abi3-win_arm64.whl", hash = "sha256:b663902cc74fe3f7f4f7d47ff7b42aed1d58172391b116e388e4557a76b3635b", size = 1965883, upload-time = "2026-05-07T09:36:02.54Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3-future"
|
||||
version = "2.21.900"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "h11" },
|
||||
{ name = "jh2" },
|
||||
{ name = "qh3", marker = "(platform_machine == 'AMD64' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_machine == 'ARM64' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_machine == 'arm64' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_machine == 'armv7l' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_machine == 'i686' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_machine == 'ppc64' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_machine == 'ppc64le' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_machine == 'riscv64' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_machine == 'riscv64gc' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_machine == 's390x' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_machine == 'x86' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_machine == 'AMD64' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'ARM64' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'aarch64' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'arm64' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'armv7l' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'i686' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'ppc64' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'ppc64le' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'riscv64' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'riscv64gc' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 's390x' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'x86' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'x86_64' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'AMD64' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (platform_machine == 'ARM64' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (platform_machine == 'aarch64' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (platform_machine == 'arm64' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (platform_machine == 'armv7l' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (platform_machine == 'i686' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (platform_machine == 'ppc64' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (platform_machine == 'ppc64le' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (platform_machine == 'riscv64' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (platform_machine == 'riscv64gc' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (platform_machine == 's390x' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (platform_machine == 'x86' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (platform_machine == 'x86_64' and platform_python_implementation == 'CPython' and sys_platform == 'win32')" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/66/7e/b744ef409c12ecdab9cc463268de218fc189d553a1a94ce7119b6c1a6d05/urllib3_future-2.21.900.tar.gz", hash = "sha256:370e83b864a67013d07fe69510a51120122de32feca02847205d4c44efc559c1", size = 1298456, upload-time = "2026-05-30T12:28:40.385Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/96/10/20cc2b1277d0ed5ec599a51d5e3d1595f5c653d2004a848c302eaf656a0f/urllib3_future-2.21.900-py3-none-any.whl", hash = "sha256:18673623d88ec8f54f7a9648a8c7075c98c07f78bf077a9a7d53fd0cf9f8ede7", size = 771289, upload-time = "2026-05-30T12:28:38.615Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wassima"
|
||||
version = "2.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/58/e0/10e0ff85dc4d48b5000aa76567dfacf33b9518c2fadd410e850e213c36b9/wassima-2.1.0.tar.gz", hash = "sha256:709f375c778d9be84568d802d1e1b62a2ed3942b1fa4c83830533a69cf36c243", size = 135950, upload-time = "2026-05-10T04:07:21.576Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/53/f9/077b0aacd337c31d3f97eef7acfa89bcd522360b56614f961af84db0a3ca/wassima-2.1.0-py3-none-any.whl", hash = "sha256:4e8bc4b439f91f4da24bd2260be662fcb42d1cc439d7678420237d314e55a854", size = 128092, upload-time = "2026-05-10T04:07:19.9Z" },
|
||||
]
|
||||
Reference in New Issue
Block a user