5 Commits

Author SHA1 Message Date
ruinivist ba76702423 fix: compatibility issue with Pling file upload server 2026-05-31 06:50:30 +05:30
ruinivist 787cf6e11a fix: disable HTTP/2 and HTTP/3 multiplexing in pling upload script
The Pling/OpenDesktop file upload servers (`files07.pling.com`)
do not properly handle HTTP/2 multiplexed file uploads from newer
HTTP clients like `niquests`, causing `ConnectionError` and `500` status
codes on push. Disabling HTTP/2 and HTTP/3 explicitly avoids these connection drops.
2026-05-31 01:11:48 +00:00
ruinivist 22605d8f8e docs: better readme 2026-05-31 05:58:54 +05:30
ruinivist c310a72019 ci: upload to pling on release 2026-05-31 00:23:41 +00:00
ruinivist e1a523ea67 refactor: move clock rendering to qml 2026-05-31 00:10:54 +00:00
19 changed files with 1239 additions and 644 deletions
-2
View File
@@ -1,2 +0,0 @@
BasedOnStyle: Google
IndentWidth: 4
-3
View File
@@ -1,3 +0,0 @@
CompileFlags:
# C++20
Add: [-std=c++20]
@@ -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
View File
@@ -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/
+10 -44
View File
@@ -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)
+2 -2
View File
@@ -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/)
![Screenshot from desktop](Example.png)
# Install
## Install
- clone the repo
- `make install` from repo root
-48
View File
@@ -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"
)
-28
View File
@@ -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"
}
]
}
-32
View File
@@ -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;
}
-19
View File
@@ -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_;
};
-21
View File
@@ -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
-112
View File
@@ -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));
}
}
}
}
}
-28
View File
@@ -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);
};
-216
View File
@@ -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();
}
-59
View File
@@ -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_;
};
+97 -29
View File
@@ -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
+7
View File
@@ -0,0 +1,7 @@
[project]
name = "katoclock"
version = "0.1.0"
requires-python = ">=3.13"
dependencies = [
"niquests>=3.18.5",
]
+818
View File
@@ -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())
Generated
+230
View File
@@ -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" },
]