BTHLABS-50: Safari Web extension

Co-authored-by: Tomek Wójcik <labs@tomekwojcik.pl>
Co-committed-by: Tomek Wójcik <labs@tomekwojcik.pl>
This commit is contained in:
2025-09-08 18:11:36 +00:00
committed by Tomek Wójcik
parent ffecf780ee
commit b6d02dbe78
184 changed files with 7536 additions and 163 deletions

2
services/extension/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules/
dist/

View File

@@ -0,0 +1,26 @@
ARG APP_USER_UID=1000
ARG APP_USER_GID=1000
ARG IMAGE_ID=development.00000000
FROM docker-hosted.nexus.bthlabs.pl/hotpocket/base:build-node-20250819-01 AS development
ARG APP_USER_UID
ARG APP_USER_GID
ARG IMAGE_ID
USER root
# COPY --chown=$APP_USER_UID:$APP_USER_GID extension/ops/bin/*.sh /srv/bin/
RUN chown -R ${APP_USER_UID}:${APP_USER_GID} /srv
USER app
VOLUME ["/srv/node_modules", "/srv/venv"]
FROM development AS ci
COPY --chown=$APP_USER_UID:$APP_USER_GID extension/ /srv/app/
COPY --chown=$APP_USER_UID:$APP_USER_GID packages/ /srv/packages/
COPY --chown=$APP_USER_UID:$APP_USER_GID tls/ /srv/tls/
RUN chown -R $APP_USER_UID:$APP_USER_GID /srv

View File

@@ -0,0 +1,3 @@
# HotPocket by BTHLabs
This repository contains the _HotPocket Extension_ project.

View File

@@ -0,0 +1,30 @@
{
"extension_name": {
"message": "Save to HotPocket",
"description": "The display name for the extension."
},
"extension_description": {
"message": "Save to HotPocket. Straight from the toolbar!",
"description": "Description of what the extension does."
},
"extension_description_Safari": {
"message": "Save to HotPocket. Straight from Safari's toolbar!",
"description": "Description of what the extension does."
},
"content_popup_content_success_title": {
"message": "Saved!",
"description": "Title of the success content popup."
},
"content_popup_content_success_message": {
"message": "Your link has been saved!",
"description": "Message of the success content popup."
},
"content_popup_content_error_title": {
"message": "Oops!",
"description": "Title of the error content popup."
},
"content_popup_content_error_message": {
"message": "HotPocket couldn't complete this operation.",
"description": "Title of the error content popup."
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 61 (89581) - https://sketch.com -->
<title>toolbar-icon</title>
<desc>Created with Sketch.</desc>
<g id="toolbar-icon" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="HotPocket-Unstyled" fill="#000000">
<g id="Group">
<g id="patch-fashion-svgrepo-com" fill-rule="nonzero">
<path d="M15.7333437,0 L0.26665625,0 C0.10665625,0 0,0.10665625 0,0.26665625 L0,11.4666562 C0,11.5733125 0.05334375,11.6533125 0.13334375,11.7066562 L7.8666875,15.9733125 C7.92,16 7.94665625,16 8,16 C8.05334375,16 8.08,16 8.13334375,15.9733437 L15.8666875,11.7066875 C15.9466875,11.6533438 16.0000312,11.5733438 16.0000312,11.4666875 L16.0000312,0.26665625 C16.0000312,0.10665625 15.8933437,0 15.7333437,0 Z M15.4666875,11.3066562 L8,15.4133437 L0.53334375,11.3066562 L0.53334375,0.53334375 L15.4666875,0.53334375 L15.4666875,11.3066562 L15.4666875,11.3066562 Z" id="Shape"></path>
<path d="M1.73334375,10.7466562 L2.77334375,11.3333125 C2.8266875,11.3599687 2.85334375,11.3599687 2.9066875,11.3599687 C3.01334375,11.3599687 3.09334375,11.306625 3.1466875,11.2533125 C3.2266875,11.1199687 3.17334375,10.9599687 3.04003125,10.8799687 L2.133375,10.3733125 L2.133375,9.3333125 C2.133375,9.1733125 2.02671875,9.06665625 1.86671875,9.06665625 C1.70671875,9.06665625 1.60006245,9.1733125 1.60006245,9.3333125 L1.60006245,10.5333125 C1.6,10.6133437 1.65334375,10.72 1.73334375,10.7466562 Z" id="Path"></path>
<path d="M1.86665625,7.81334375 C2.02665625,7.81334375 2.1333125,7.68 2.1333125,7.5466875 L2.1333125,4.85334375 C2.1333125,4.69334375 2.02665625,4.5866875 1.86665625,4.5866875 C1.70665625,4.5866875 1.6,4.69334375 1.6,4.85334375 L1.6,7.5466875 C1.6,7.70665625 1.70665625,7.81334375 1.86665625,7.81334375 Z" id="Path"></path>
<path d="M1.86665625,3.33334375 C2.02665625,3.33334375 2.1333125,3.2266875 2.1333125,3.0666875 L2.1333125,2.13334375 L3.06665625,2.13334375 C3.22665625,2.13334375 3.3333125,2.0266875 3.3333125,1.8666875 C3.3333125,1.7066875 3.22665625,1.60003125 3.06665625,1.60003125 L1.86665625,1.60003125 C1.70665625,1.60003125 1.6,1.7066875 1.6,1.8666875 L1.6,3.0666875 C1.6,3.22665625 1.70665625,3.33334375 1.86665625,3.33334375 Z" id="Path"></path>
<path d="M4.72,2.13334375 L7.17334375,2.13334375 C7.33334375,2.13334375 7.44,2.0266875 7.44,1.8666875 C7.44,1.7066875 7.33334375,1.60003125 7.17334375,1.60003125 L4.72,1.60003125 C4.56,1.60003125 4.45334375,1.7066875 4.45334375,1.8666875 C4.45334375,2.0266875 4.56,2.13334375 4.72,2.13334375 Z" id="Path"></path>
<path d="M8.82665625,2.13334375 L11.28,2.13334375 C11.44,2.13334375 11.5466562,2.0266875 11.5466562,1.8666875 C11.5466562,1.7066875 11.44,1.6 11.28,1.6 L8.82665625,1.6 C8.66665625,1.6 8.56,1.70665625 8.56,1.86665625 C8.56,2.02665625 8.66665625,2.13334375 8.82665625,2.13334375 Z" id="Path"></path>
<path d="M12.9333438,2.13334375 L13.8666875,2.13334375 L13.8666875,3.0666875 C13.8666875,3.2266875 13.9733438,3.33334375 14.1333438,3.33334375 C14.2933438,3.33334375 14.4,3.2266875 14.4,3.0666875 L14.4,1.8666875 C14.4,1.7066875 14.2933438,1.60003125 14.1333438,1.60003125 L12.9333438,1.60003125 C12.7733438,1.60003125 12.6666875,1.7066875 12.6666875,1.8666875 C12.6666875,2.0266875 12.7733438,2.13334375 12.9333438,2.13334375 Z" id="Path"></path>
<path d="M14.1333438,4.58665625 C13.9733438,4.58665625 13.8666875,4.6933125 13.8666875,4.8533125 L13.8666875,7.54665625 C13.8666875,7.70665625 13.9733438,7.8133125 14.1333438,7.8133125 C14.2933438,7.8133125 14.4,7.67996875 14.4,7.54665625 L14.4,4.8533125 C14.4,4.69334375 14.2933438,4.58665625 14.1333438,4.58665625 Z" id="Path"></path>
<path d="M14.1333437,9.06665625 C13.9733437,9.06665625 13.8666875,9.1733125 13.8666875,9.3333125 L13.8666875,10.3466563 L12.9600313,10.8533125 C12.8266875,10.9333125 12.773375,11.0933125 12.853375,11.2266563 C12.9067187,11.3066563 12.9867187,11.36 13.093375,11.36 C13.1200313,11.36 13.173375,11.36 13.2267187,11.36 L14.2667187,10.7733438 C14.3467187,10.72 14.4000625,10.64 14.4000625,10.5333438 L14.4000625,9.33334375 C14.4,9.17334375 14.2933437,9.06665625 14.1333437,9.06665625 Z" id="Path"></path>
<path d="M9.97334375,12.8266562 C10.0266875,12.9066562 10.1066875,12.96 10.2133437,12.96 C10.24,12.96 10.2933437,12.9333437 10.3466875,12.9333437 L12.0800312,11.9733437 C12.213375,11.8933437 12.2666875,11.7333437 12.1866875,11.6 C12.1066875,11.4666562 11.9466875,11.4133437 11.8133437,11.4933437 L10.08,12.4533437 C9.94665625,12.5333437 9.89334375,12.6933437 9.97334375,12.8266562 Z" id="Path"></path>
<path d="M6.72,13.1733438 C6.64,13.3066875 6.69334375,13.4666875 6.82665625,13.5466875 L7.86665625,14.1333438 C7.92,14.16 7.94665625,14.16 8,14.16 C8.05334375,14.16 8.08,14.16 8.13334375,14.1333438 L9.2,13.5733438 C9.33334375,13.4933438 9.38665625,13.3333438 9.30665625,13.2 C9.22665625,13.0666563 9.06665625,13.0133438 8.9333125,13.0933438 L8,13.6 L7.09334375,13.0666563 C6.96,12.9866563 6.8,13.04 6.72,13.1733438 Z" id="Path"></path>
<path d="M3.92,11.9733437 L5.65334375,12.9333438 C5.7066875,12.96 5.73334375,12.96 5.7866875,12.96 C5.89334375,12.96 5.97334375,12.9066563 6.0266875,12.8266563 C6.1066875,12.6933125 6.05334375,12.5333125 5.92003125,12.4533125 L4.1866875,11.4933125 C4.05334375,11.4133125 3.89334375,11.4666562 3.81334375,11.5999687 C3.73334375,11.7332812 3.78665625,11.8933437 3.92,11.9733437 Z" id="Path"></path>
</g>
<g id="pepper-hot-solid-svgrepo-com" transform="translate(3.480000, 1.560000)" fill-rule="nonzero">
<path d="M9.04,0.750430988 L9.04,0 C8.29241927,0 7.7093357,0.208127532 7.3083736,0.61558792 C7.07631531,0.851563196 6.95147381,1.113921 6.87950602,1.33963676 C6.68857201,1.20772487 6.4667945,1.12564648 6.22004877,1.12564648 C5.27125287,1.12564648 4.91288407,1.87900903 4.90407153,2.39199878 C4.90407153,2.47407716 4.90113427,2.5605527 4.89525937,2.64995943 C4.64263874,3.39599327 4.15208453,2.90205734 4.15208453,1.87607747 C2.94038683,3.16001799 4.24608291,3.63343451 3.50143944,4.37653716 C3.34281719,4.5348312 2.96388643,4.5963898 2.72742186,4.30618426 C2.41458324,3.93536554 3.05054127,3.32564036 2.448364,2.86981244 C2.55851844,3.55575327 2.02830895,3.41358149 1.84912436,3.26701294 C1.61559706,3.07647401 1.19113574,2.65728776 1.89612355,1.50086198 C1.17497968,1.82917553 0.740237198,2.49606254 0.768143059,3.00172395 C0.843048108,4.32816926 2.89779392,5.48019827 2.06796424,6.25408023 C1.64937782,6.64541835 0.826892043,6.1163056 1.14413656,5.25301692 C0.749049733,5.45381574 0.28493276,5.93162941 0.433273851,6.67473207 C0.536084761,7.1891874 1.28513412,8.34854475 0.0161560646,9.00517186 C0.0161560646,9.00517186 0.022030963,9.00663745 0.0323121292,9.00956901 L0,9.75560285 C0.0249686002,9.75706844 0.126310503,9.76 0.289339028,9.76 C1.34682074,9.76 5.00688244,9.59730881 7.25990579,7.44421687 C8.44076036,6.31563883 9.04,4.8206396 9.04,3.00172395 C9.04,2.48287147 8.80353543,1.50086198 7.91201951,1.50086198 C7.80186507,1.50086198 7.69905454,1.51551902 7.60211852,1.53896999 C7.64471182,1.41292085 7.71814786,1.26928385 7.84445836,1.14030353 C8.10001626,0.882342873 8.50097798,0.750430988 9.04,0.750430988 Z M7.91201951,2.25129297 C8.26745105,2.25129297 8.28801301,2.99586121 8.28801301,3.00172395 C8.28801301,4.6066497 7.76661568,5.91843833 6.7414461,6.90044745 C5.07738122,8.49218212 2.45570753,8.88645143 1.03251349,8.9802553 C1.37913249,8.9011081 1.79625028,8.73988288 2.25302372,8.43208873 C4.45317298,7.22876101 5.61052797,4.39119383 5.65312126,2.50192528 L5.65605852,2.50192528 L5.65605852,2.39932711 C5.65752715,2.31138598 5.69277655,1.87607747 6.22004877,1.87607747 C6.54169955,1.87607747 6.78403902,2.33190576 6.78403902,2.62650846 L7.53602601,2.62650846 C7.53602601,2.46528324 7.57568167,2.25129297 7.91201951,2.25129297 Z" id="Shape"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@@ -0,0 +1,23 @@
services:
extension-ci:
build:
context: ".."
dockerfile: "extension/Dockerfile"
target: "development"
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/extension:ci-local"
command: "echo 'NOOP'"
environment:
PYTHONBREAKPOINT: "ipdb.set_trace"
HOTPOCKET_PACKAGES_ENV: "${HOTPOCKET_EXTENSION_ENV:-docker}"
# REQUESTS_CA_BUNDLE: "/srv/tls/requests_ca_bundle.pem"
RUN_POETRY_INSTALL: "true"
RUN_YARN_INSTALL: "true"
SETUP_BACKEND: "true"
SETUP_FRONTEND: "true"
volumes:
- "extension_venv:/srv/venv"
- "extension_node_modules:/srv/node_modules"
- "../tls:/srv/tls"
restart: "no"
stdin_open: true
tty: true

View File

@@ -0,0 +1,29 @@
services:
extension-management:
build:
context: ".."
dockerfile: "extension/Dockerfile"
target: "development"
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/extension:local"
command: "echo 'NOOP'"
environment: &extension-env
PYTHONBREAKPOINT: "ipdb.set_trace"
HOTPOCKET_EXTENSION_ENV: "${HOTPOCKET_EXTENSION_ENV:-docker}"
REQUESTS_CA_BUNDLE: "/srv/tls/requests_ca_bundle.pem"
RUN_POETRY_INSTALL: "true"
RUN_YARN_INSTALL: "true"
SETUP_BACKEND: "true"
SETUP_FRONTEND: "true"
volumes:
- "extension_venv:/srv/venv"
- "extension_node_modules:/srv/node_modules"
- ".:/srv/app"
- "../packages:/srv/packages"
- "../tls:/srv/tls"
restart: "no"
stdin_open: true
tty: true
volumes:
extension_venv:
extension_node_modules:

View File

@@ -0,0 +1,74 @@
// eslint.config.js
import js from '@eslint/js';
import {defineConfig} from 'eslint/config';
import globals from 'globals';
export default defineConfig([
{
files: [
'eslint.config.js',
'src/**/*.js',
],
plugins: {
js,
},
extends: ['js/recommended'],
languageOptions: {
ecmaVersion: 2022,
sourceType: 'module',
globals: {
...globals.browser,
...globals.webextensions,
__HOTPOCKET_EXTENSION_ENV__: false,
__HOTPOCKET_EXTENSION_VERSION__: false,
__HOTPOCKET_EXTENSION_BASE_URL__: false,
},
},
rules: {
'no-undef': 'error',
'quotes': [
'error',
'single',
{'avoidEscape': true, 'allowTemplateLiterals': true},
],
'no-unused-vars': ['error', {'args': 'none'}],
'no-console': ['error', {'allow': ['warn', 'error', 'info']}],
'no-empty': ['error', {'allowEmptyCatch': true}],
'array-bracket-spacing': ['error', 'never'],
'block-spacing': ['error', 'always'],
'brace-style': ['error', '1tbs', {'allowSingleLine': true}],
'camelcase': ['error', {'properties': 'never'}],
'comma-dangle': ['error', 'always-multiline'],
'comma-spacing': ['error', {'before': false, 'after': true}],
'comma-style': ['error', 'last'],
'computed-property-spacing': ['error', 'never'],
'key-spacing': [
'error', {'beforeColon': false, 'afterColon': true, 'mode': 'strict'},
],
'keyword-spacing': ['error', { 'before': true, 'after': true }],
'linebreak-style': ['error', 'unix'],
'max-len': ['error', 120],
'no-multiple-empty-lines': 'error',
'no-spaced-func': 'error',
'no-trailing-spaces': 'error',
'no-unreachable': 'warn',
'no-whitespace-before-property': 'error',
'object-curly-spacing': 'off',
'one-var-declaration-per-line': ['error', 'always'],
'one-var': ['error', 'never'],
'semi-spacing': ['error', {'before': false, 'after': true}],
'semi': ['error', 'always'],
'space-before-function-paren': ['error', 'always'],
'space-before-blocks': ['error', 'always'],
'space-in-parens': ['error', 'never'],
'space-infix-ops': 'error',
'unicode-bom': ['error', 'never'],
'no-useless-escape': 'off',
'class-methods-use-this': 'off',
'no-invalid-this': 'off',
},
},
{
ignores: ['dist/**'],
},
]);

View File

@@ -0,0 +1,3 @@
run:
echo: true
pty: true

View File

@@ -0,0 +1,27 @@
{
"name": "hotpocket-extension",
"version": "1.0.2",
"description": "HotPocket Extension",
"main": "src/index.js",
"repository": "https://git.bthlabs.pl/tomekwojcik/hotpocket",
"author": "Tomek Wójcik <contact@bthlabs.pl>",
"license": "Apache-2.0",
"private": true,
"type": "module",
"scripts": {
"eslint": "npx eslint .",
"build:safari": "NODE_ENV=production HOTPOCKET_EXTENSION_TARGET=safari npx rollup -c rollup.config.js",
"dev:safari": "HOTPOCKET_EXTENSION_TARGET=safari npx rollup -c rollup.config.js",
"watch:safari": "HOTPOCKET_EXTENSION_TARGET=safari npx rollup -c rollup.config.js -w"
},
"devDependencies": {
"@eslint/js": "9.33.0",
"@rollup/plugin-replace": "6.0.2",
"@rollup/plugin-terser": "0.4.4",
"eslint": "9.33.0",
"globals": "14.0.0",
"rollup": "4.50.0",
"rollup-plugin-copy": "3.5.0",
"rollup-plugin-string": "3.0.0"
}
}

508
services/extension/poetry.lock generated Normal file
View File

@@ -0,0 +1,508 @@
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
[[package]]
name = "asttokens"
version = "3.0.0"
description = "Annotate AST trees with source code positions"
optional = false
python-versions = ">=3.8"
files = [
{file = "asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2"},
{file = "asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7"},
]
[package.extras]
astroid = ["astroid (>=2,<4)"]
test = ["astroid (>=2,<4)", "pytest", "pytest-cov", "pytest-xdist"]
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "decorator"
version = "5.2.1"
description = "Decorators for Humans"
optional = false
python-versions = ">=3.8"
files = [
{file = "decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a"},
{file = "decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360"},
]
[[package]]
name = "executing"
version = "2.2.1"
description = "Get the currently executing AST node of a frame, and other information"
optional = false
python-versions = ">=3.8"
files = [
{file = "executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017"},
{file = "executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4"},
]
[package.extras]
tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"]
[[package]]
name = "factory-boy"
version = "3.3.3"
description = "A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby."
optional = false
python-versions = ">=3.8"
files = [
{file = "factory_boy-3.3.3-py2.py3-none-any.whl", hash = "sha256:1c39e3289f7e667c4285433f305f8d506efc2fe9c73aaea4151ebd5cdea394fc"},
{file = "factory_boy-3.3.3.tar.gz", hash = "sha256:866862d226128dfac7f2b4160287e899daf54f2612778327dd03d0e2cb1e3d03"},
]
[package.dependencies]
Faker = ">=0.7.0"
[package.extras]
dev = ["Django", "Pillow", "SQLAlchemy", "coverage", "flake8", "isort", "mongoengine", "mongomock", "mypy", "tox", "wheel (>=0.32.0)", "zest.releaser[recommended]"]
doc = ["Sphinx", "sphinx-rtd-theme", "sphinxcontrib-spelling"]
[[package]]
name = "faker"
version = "37.6.0"
description = "Faker is a Python package that generates fake data for you."
optional = false
python-versions = ">=3.9"
files = [
{file = "faker-37.6.0-py3-none-any.whl", hash = "sha256:3c5209b23d7049d596a51db5d76403a0ccfea6fc294ffa2ecfef6a8843b1e6a7"},
{file = "faker-37.6.0.tar.gz", hash = "sha256:0f8cc34f30095184adf87c3c24c45b38b33ad81c35ef6eb0a3118f301143012c"},
]
[package.dependencies]
tzdata = "*"
[[package]]
name = "flake8"
version = "7.3.0"
description = "the modular source code checker: pep8 pyflakes and co"
optional = false
python-versions = ">=3.9"
files = [
{file = "flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e"},
{file = "flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872"},
]
[package.dependencies]
mccabe = ">=0.7.0,<0.8.0"
pycodestyle = ">=2.14.0,<2.15.0"
pyflakes = ">=3.4.0,<3.5.0"
[[package]]
name = "flake8-commas"
version = "4.0.0"
description = "Flake8 lint for trailing commas."
optional = false
python-versions = ">=3.8"
files = [
{file = "flake8_commas-4.0.0-py3-none-any.whl", hash = "sha256:cad476d71ba72e8b941a8508d5b9ffb6b03e50f7102982474f085ad0d674b685"},
{file = "flake8_commas-4.0.0.tar.gz", hash = "sha256:a68834b42a9a31c94ca790efe557a932c0eae21a3479c6b9a23c4dc077e3ea96"},
]
[package.dependencies]
flake8 = ">=5"
[[package]]
name = "hotpocket-workspace-tools"
version = "1.0.0.dev0"
description = "HotPocket Workspace Tools"
optional = false
python-versions = "^3.12"
files = []
develop = true
[package.source]
type = "directory"
url = "../packages/workspace_tools"
[[package]]
name = "invoke"
version = "2.2.0"
description = "Pythonic task execution"
optional = false
python-versions = ">=3.6"
files = [
{file = "invoke-2.2.0-py3-none-any.whl", hash = "sha256:6ea924cc53d4f78e3d98bc436b08069a03077e6f85ad1ddaa8a116d7dad15820"},
{file = "invoke-2.2.0.tar.gz", hash = "sha256:ee6cbb101af1a859c7fe84f2a264c059020b0cb7fe3535f9424300ab568f6bd5"},
]
[[package]]
name = "ipdb"
version = "0.13.13"
description = "IPython-enabled pdb"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
files = [
{file = "ipdb-0.13.13-py3-none-any.whl", hash = "sha256:45529994741c4ab6d2388bfa5d7b725c2cf7fe9deffabdb8a6113aa5ed449ed4"},
{file = "ipdb-0.13.13.tar.gz", hash = "sha256:e3ac6018ef05126d442af680aad863006ec19d02290561ac88b8b1c0b0cfc726"},
]
[package.dependencies]
decorator = {version = "*", markers = "python_version >= \"3.11\""}
ipython = {version = ">=7.31.1", markers = "python_version >= \"3.11\""}
[[package]]
name = "ipython"
version = "9.3.0"
description = "IPython: Productive Interactive Computing"
optional = false
python-versions = ">=3.11"
files = [
{file = "ipython-9.3.0-py3-none-any.whl", hash = "sha256:1a0b6dd9221a1f5dddf725b57ac0cb6fddc7b5f470576231ae9162b9b3455a04"},
{file = "ipython-9.3.0.tar.gz", hash = "sha256:79eb896f9f23f50ad16c3bc205f686f6e030ad246cc309c6279a242b14afe9d8"},
]
[package.dependencies]
colorama = {version = "*", markers = "sys_platform == \"win32\""}
decorator = "*"
ipython-pygments-lexers = "*"
jedi = ">=0.16"
matplotlib-inline = "*"
pexpect = {version = ">4.3", markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""}
prompt_toolkit = ">=3.0.41,<3.1.0"
pygments = ">=2.4.0"
stack_data = "*"
traitlets = ">=5.13.0"
[package.extras]
all = ["ipython[doc,matplotlib,test,test-extra]"]
black = ["black"]
doc = ["docrepr", "exceptiongroup", "intersphinx_registry", "ipykernel", "ipython[test]", "matplotlib", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinx_toml (==0.0.4)", "typing_extensions"]
matplotlib = ["matplotlib"]
test = ["packaging", "pytest", "pytest-asyncio (<0.22)", "testpath"]
test-extra = ["curio", "ipykernel", "ipython[test]", "jupyter_ai", "matplotlib (!=3.2.0)", "nbclient", "nbformat", "numpy (>=1.23)", "pandas", "trio"]
[[package]]
name = "ipython-pygments-lexers"
version = "1.1.1"
description = "Defines a variety of Pygments lexers for highlighting IPython code."
optional = false
python-versions = ">=3.8"
files = [
{file = "ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c"},
{file = "ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81"},
]
[package.dependencies]
pygments = "*"
[[package]]
name = "isort"
version = "6.0.1"
description = "A Python utility / library to sort Python imports."
optional = false
python-versions = ">=3.9.0"
files = [
{file = "isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615"},
{file = "isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450"},
]
[package.extras]
colors = ["colorama"]
plugins = ["setuptools"]
[[package]]
name = "jedi"
version = "0.19.2"
description = "An autocompletion tool for Python that can be used for text editors."
optional = false
python-versions = ">=3.6"
files = [
{file = "jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9"},
{file = "jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0"},
]
[package.dependencies]
parso = ">=0.8.4,<0.9.0"
[package.extras]
docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"]
qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"]
testing = ["Django", "attrs", "colorama", "docopt", "pytest (<9.0.0)"]
[[package]]
name = "matplotlib-inline"
version = "0.1.7"
description = "Inline Matplotlib backend for Jupyter"
optional = false
python-versions = ">=3.8"
files = [
{file = "matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"},
{file = "matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90"},
]
[package.dependencies]
traitlets = "*"
[[package]]
name = "mccabe"
version = "0.7.0"
description = "McCabe checker, plugin for flake8"
optional = false
python-versions = ">=3.6"
files = [
{file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
]
[[package]]
name = "mypy"
version = "1.16.1"
description = "Optional static typing for Python"
optional = false
python-versions = ">=3.9"
files = [
{file = "mypy-1.16.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b4f0fed1022a63c6fec38f28b7fc77fca47fd490445c69d0a66266c59dd0b88a"},
{file = "mypy-1.16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86042bbf9f5a05ea000d3203cf87aa9d0ccf9a01f73f71c58979eb9249f46d72"},
{file = "mypy-1.16.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ea7469ee5902c95542bea7ee545f7006508c65c8c54b06dc2c92676ce526f3ea"},
{file = "mypy-1.16.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:352025753ef6a83cb9e7f2427319bb7875d1fdda8439d1e23de12ab164179574"},
{file = "mypy-1.16.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff9fa5b16e4c1364eb89a4d16bcda9987f05d39604e1e6c35378a2987c1aac2d"},
{file = "mypy-1.16.1-cp310-cp310-win_amd64.whl", hash = "sha256:1256688e284632382f8f3b9e2123df7d279f603c561f099758e66dd6ed4e8bd6"},
{file = "mypy-1.16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:472e4e4c100062488ec643f6162dd0d5208e33e2f34544e1fc931372e806c0cc"},
{file = "mypy-1.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea16e2a7d2714277e349e24d19a782a663a34ed60864006e8585db08f8ad1782"},
{file = "mypy-1.16.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08e850ea22adc4d8a4014651575567b0318ede51e8e9fe7a68f25391af699507"},
{file = "mypy-1.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22d76a63a42619bfb90122889b903519149879ddbf2ba4251834727944c8baca"},
{file = "mypy-1.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2c7ce0662b6b9dc8f4ed86eb7a5d505ee3298c04b40ec13b30e572c0e5ae17c4"},
{file = "mypy-1.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:211287e98e05352a2e1d4e8759c5490925a7c784ddc84207f4714822f8cf99b6"},
{file = "mypy-1.16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:af4792433f09575d9eeca5c63d7d90ca4aeceda9d8355e136f80f8967639183d"},
{file = "mypy-1.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:66df38405fd8466ce3517eda1f6640611a0b8e70895e2a9462d1d4323c5eb4b9"},
{file = "mypy-1.16.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44e7acddb3c48bd2713994d098729494117803616e116032af192871aed80b79"},
{file = "mypy-1.16.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ab5eca37b50188163fa7c1b73c685ac66c4e9bdee4a85c9adac0e91d8895e15"},
{file = "mypy-1.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb6229b2c9086247e21a83c309754b9058b438704ad2f6807f0d8227f6ebdd"},
{file = "mypy-1.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:1f0435cf920e287ff68af3d10a118a73f212deb2ce087619eb4e648116d1fe9b"},
{file = "mypy-1.16.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ddc91eb318c8751c69ddb200a5937f1232ee8efb4e64e9f4bc475a33719de438"},
{file = "mypy-1.16.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:87ff2c13d58bdc4bbe7dc0dedfe622c0f04e2cb2a492269f3b418df2de05c536"},
{file = "mypy-1.16.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a7cfb0fe29fe5a9841b7c8ee6dffb52382c45acdf68f032145b75620acfbd6f"},
{file = "mypy-1.16.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:051e1677689c9d9578b9c7f4d206d763f9bbd95723cd1416fad50db49d52f359"},
{file = "mypy-1.16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d5d2309511cc56c021b4b4e462907c2b12f669b2dbeb68300110ec27723971be"},
{file = "mypy-1.16.1-cp313-cp313-win_amd64.whl", hash = "sha256:4f58ac32771341e38a853c5d0ec0dfe27e18e27da9cdb8bbc882d2249c71a3ee"},
{file = "mypy-1.16.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7fc688329af6a287567f45cc1cefb9db662defeb14625213a5b7da6e692e2069"},
{file = "mypy-1.16.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e198ab3f55924c03ead626ff424cad1732d0d391478dfbf7bb97b34602395da"},
{file = "mypy-1.16.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09aa4f91ada245f0a45dbc47e548fd94e0dd5a8433e0114917dc3b526912a30c"},
{file = "mypy-1.16.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13c7cd5b1cb2909aa318a90fd1b7e31f17c50b242953e7dd58345b2a814f6383"},
{file = "mypy-1.16.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:58e07fb958bc5d752a280da0e890c538f1515b79a65757bbdc54252ba82e0b40"},
{file = "mypy-1.16.1-cp39-cp39-win_amd64.whl", hash = "sha256:f895078594d918f93337a505f8add9bd654d1a24962b4c6ed9390e12531eb31b"},
{file = "mypy-1.16.1-py3-none-any.whl", hash = "sha256:5fc2ac4027d0ef28d6ba69a0343737a23c4d1b83672bf38d1fe237bdc0643b37"},
{file = "mypy-1.16.1.tar.gz", hash = "sha256:6bd00a0a2094841c5e47e7374bb42b83d64c527a502e3334e1173a0c24437bab"},
]
[package.dependencies]
mypy_extensions = ">=1.0.0"
pathspec = ">=0.9.0"
typing_extensions = ">=4.6.0"
[package.extras]
dmypy = ["psutil (>=4.0)"]
faster-cache = ["orjson"]
install-types = ["pip"]
mypyc = ["setuptools (>=50)"]
reports = ["lxml"]
[[package]]
name = "mypy-extensions"
version = "1.1.0"
description = "Type system extensions for programs checked with the mypy type checker."
optional = false
python-versions = ">=3.8"
files = [
{file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"},
{file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"},
]
[[package]]
name = "parso"
version = "0.8.5"
description = "A Python Parser"
optional = false
python-versions = ">=3.6"
files = [
{file = "parso-0.8.5-py2.py3-none-any.whl", hash = "sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887"},
{file = "parso-0.8.5.tar.gz", hash = "sha256:034d7354a9a018bdce352f48b2a8a450f05e9d6ee85db84764e9b6bd96dafe5a"},
]
[package.extras]
qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"]
testing = ["docopt", "pytest"]
[[package]]
name = "pathspec"
version = "0.12.1"
description = "Utility library for gitignore style pattern matching of file paths."
optional = false
python-versions = ">=3.8"
files = [
{file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
]
[[package]]
name = "pexpect"
version = "4.9.0"
description = "Pexpect allows easy control of interactive console applications."
optional = false
python-versions = "*"
files = [
{file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"},
{file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"},
]
[package.dependencies]
ptyprocess = ">=0.5"
[[package]]
name = "prompt-toolkit"
version = "3.0.52"
description = "Library for building powerful interactive command lines in Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955"},
{file = "prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855"},
]
[package.dependencies]
wcwidth = "*"
[[package]]
name = "ptyprocess"
version = "0.7.0"
description = "Run a subprocess in a pseudo terminal"
optional = false
python-versions = "*"
files = [
{file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"},
{file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"},
]
[[package]]
name = "pure-eval"
version = "0.2.3"
description = "Safely evaluate AST nodes without side effects"
optional = false
python-versions = "*"
files = [
{file = "pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0"},
{file = "pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42"},
]
[package.extras]
tests = ["pytest"]
[[package]]
name = "pycodestyle"
version = "2.14.0"
description = "Python style guide checker"
optional = false
python-versions = ">=3.9"
files = [
{file = "pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d"},
{file = "pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783"},
]
[[package]]
name = "pyflakes"
version = "3.4.0"
description = "passive checker of Python programs"
optional = false
python-versions = ">=3.9"
files = [
{file = "pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f"},
{file = "pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58"},
]
[[package]]
name = "pygments"
version = "2.19.2"
description = "Pygments is a syntax highlighting package written in Python."
optional = false
python-versions = ">=3.8"
files = [
{file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"},
{file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"},
]
[package.extras]
windows-terminal = ["colorama (>=0.4.6)"]
[[package]]
name = "stack-data"
version = "0.6.3"
description = "Extract data from python stack frames and tracebacks for informative displays"
optional = false
python-versions = "*"
files = [
{file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"},
{file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"},
]
[package.dependencies]
asttokens = ">=2.1.0"
executing = ">=1.2.0"
pure-eval = "*"
[package.extras]
tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"]
[[package]]
name = "traitlets"
version = "5.14.3"
description = "Traitlets Python configuration system"
optional = false
python-versions = ">=3.8"
files = [
{file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"},
{file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"},
]
[package.extras]
docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"]
test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"]
[[package]]
name = "typing-extensions"
version = "4.15.0"
description = "Backported and Experimental Type Hints for Python 3.9+"
optional = false
python-versions = ">=3.9"
files = [
{file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"},
{file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"},
]
[[package]]
name = "tzdata"
version = "2025.2"
description = "Provider of IANA time zone data"
optional = false
python-versions = ">=2"
files = [
{file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"},
{file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"},
]
[[package]]
name = "wcwidth"
version = "0.2.13"
description = "Measures the displayed width of unicode strings in a terminal"
optional = false
python-versions = "*"
files = [
{file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"},
{file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"},
]
[metadata]
lock-version = "2.0"
python-versions = "^3.12"
content-hash = "1c74b6d5928a0b1bde41962f4233af2eba2242324c3155b21093b2f46baf5cd0"

View File

@@ -0,0 +1,26 @@
[tool.poetry]
name = "hotpocket-extension"
version = "1.0.2"
description = "HotPocket Extension"
authors = ["Tomek Wójcik <contact@bthlabs.pl>"]
license = "Apache-2.0"
readme = "README.md"
package-mode = false
[tool.poetry.dependencies]
python = "^3.12"
[tool.poetry.group.dev.dependencies]
factory-boy = "3.3.3"
flake8 = "7.3.0"
flake8-commas = "4.0.0"
hotpocket-workspace-tools = {path = "../packages/workspace_tools", develop = true}
invoke = "2.2.0"
ipdb = "0.13.13"
ipython = "9.3.0"
isort = "6.0.1"
mypy = "1.16.1"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

View File

@@ -0,0 +1,125 @@
import replace from '@rollup/plugin-replace';
import copy from 'rollup-plugin-copy';
import {string} from 'rollup-plugin-string';
import packageJSON from './package.json' with {type: 'json'};
import manifestCommon from './src/manifest/common.json' with {type: 'json'};
import manifestSafari from './src/manifest/safari.json' with {type: 'json'};
const BANNER = `/*!
* HotPocket by BTHLabs (https://hotpocket.app/)
* Copyright 2025-present BTHLabs <contact@bthlabs.pl> (https://bthlabs.pl/)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/`;
const ENV = process.env['NODE_ENV'] || 'development';
const IS_PRODUCTION = (ENV === 'production');
const TARGET = process.env['HOTPOCKET_EXTENSION_TARGET'] || 'safari';
let BASE_URL = process.env['HOTPOCKET_EXTENSION_RPC_BASE_URL'] || null;
if (BASE_URL === null) {
if (IS_PRODUCTION) {
BASE_URL = 'https://my.hotpocket.app/';
} else {
BASE_URL = 'https://app.hotpocket.work.bthlabs.net/';
}
}
const PLUGINS = [
string({
include: /\.html$/,
}),
replace({
include: /\.js$/,
preventAssignment: true,
values: {
'__HOTPOCKET_EXTENSION_ENV__': JSON.stringify(ENV),
'__HOTPOCKET_EXTENSION_VERSION__': JSON.stringify(packageJSON.version),
'__HOTPOCKET_EXTENSION_BASE_URL__': JSON.stringify(BASE_URL),
},
}),
];
const manifestJsonOutputPlugin = () => {
return {
name: 'manifest-json',
renderChunk: async (code, chunk, outputOptions) => {
let result = {...manifestCommon};
if (TARGET === 'safari') {
result = {
...result,
...manifestSafari,
};
}
result.version = packageJSON.version;
return JSON.stringify(result, null, 2);
},
};
};
let OUTPUT_PATH = `dist/${TARGET}`;
if (TARGET === 'safari') {
OUTPUT_PATH = '../apple/Shared (Extension)/Resources';
}
export default [
{
input: `src/content/${TARGET}.js`,
output: {
file: `${OUTPUT_PATH}/content-bundle.js`,
format: 'iife',
banner: BANNER,
sourcemap: false,
},
plugins: [
...PLUGINS,
copy({
targets: [
{
src: 'assets/_locales',
dest: OUTPUT_PATH,
},
{
src: 'assets/images',
dest: OUTPUT_PATH,
},
],
}),
],
},
{
input: `src/background/${TARGET}.js`,
output: {
file: `${OUTPUT_PATH}/background-bundle.js`,
format: 'iife',
banner: BANNER,
sourcemap: false,
},
plugins: PLUGINS,
},
{
input: `src/manifest.js`,
output: {
file: `${OUTPUT_PATH}/manifest.json`,
format: 'es',
sourcemap: false,
plugins: [
manifestJsonOutputPlugin(),
],
},
}
];

View File

@@ -0,0 +1,16 @@
[flake8]
extend-exclude =
node_modules/**/*.py
ignore = E131,W503,W504
max-line-length = 119
hang-closing = False
[isort]
known_first_party=hotpocket_backend,hotpocket_backend_testing,hotpocket_common,hotpocket_soa,hotpocket_testing,hotpocket_workspace_tools
multi_line_output=3
include_trailing_comma=true
force_sort_within_sections=true
line_length=80
use_parentheses=true
combine_as_imports=true
star_first=true

View File

@@ -0,0 +1,300 @@
import HotPocketExtension from '../common';
const AUTH_URL = (new URL('/integrations/extension/authenticate/', HotPocketExtension.base_url)).toString();
const POST_AUTH_URL = (new URL('/integrations/extension/post-authenticate/', HotPocketExtension.base_url)).toString();
const RPC_URL = (new URL('/rpc/', HotPocketExtension.base_url)).toString();
const makeJSONRPCCall = (method, params) => {
return {
'jsonrpc': '2.0',
'id': (new Date().toISOString()),
'method': method,
'params': params,
};
};
const executeJSONRPCCall = async (url, call, {accessToken}) => {
HotPocketExtension.LOGGER.debug(
'HotPocketExtension.background.executeJSONRPCCall()', url, call, accessToken,
);
const headers = {
'Content-Type': 'application/json',
};
if (accessToken) {
headers['Authorization'] = `Bearer ${accessToken}`;
}
let result = null;
let error = null;
try {
const response = await fetch(
url,
{
body: JSON.stringify(call),
credentials: 'include',
headers: headers,
method: 'POST',
},
);
HotPocketExtension.LOGGER.debug(
'HotPocketExtension.content.executeJSONRPCCall()', response,
);
if (response.status !== 200) {
error = {
code: -32000,
message: 'Fetch error',
data: response,
};
} else {
const callResult = await response.json();
if (callResult.error) {
HotPocketExtension.LOGGER.error(
'HotPocketExtension.content.executeJSONRPCCall(): RPC error',
callResult.error.code,
callResult.error.message,
callResult.error.data,
);
error = callResult.error;
} else {
result = callResult.result;
}
}
} catch (exception) {
HotPocketExtension.LOGGER.error(
'HotPocketExtension.content.executeJSONRPCCall(): Fetch error', exception,
);
error = {
code: -32000,
message: 'Fetch error',
data: exception,
};
}
return [result, error];
};
const doSave = async (accessToken, tab) => {
const call = makeJSONRPCCall('saves.create', [tab.url]);
const [result, error] = await executeJSONRPCCall(RPC_URL, call, {accessToken});
HotPocketExtension.LOGGER.debug(
'HotPocketExtension.content.doSave():', result, error,
);
if (error !== null) {
return false;
}
return true;
};
const doCreateAndStoreAccessToken = async (authKey) => {
const accessTokenCall = makeJSONRPCCall(
'accounts.access_tokens.create',
[
authKey,
{
platform: navigator.platform,
version: HotPocketExtension.version,
},
],
);
const [accessToken, error] = await executeJSONRPCCall(
RPC_URL, accessTokenCall, {accessToken: null},
);
if (error === null) {
await HotPocketExtension.api.storage.local.set({
accessToken: accessToken,
});
}
return [accessToken, error];
};
const doHandleAuthFlow = (authTab) => {
return new Promise((resolve, reject) => {
const onTabsUpdated = (tabId, changeInfo, updatedTab) => {
if (tabId === authTab.id) {
HotPocketExtension.LOGGER.debug(
'HotPocketExtension.content.doHandleAuthFlow.onTabsUpdated()', updatedTab, changeInfo,
);
const changedURL = changeInfo.url;
if (changedURL && changedURL.startsWith(POST_AUTH_URL)) {
const parsedChangedURL = new URL(changedURL);
const authKey = parsedChangedURL.searchParams.get('auth_key');
doCreateAndStoreAccessToken(authKey).
then((result) => {
HotPocketExtension.LOGGER.debug(
'doHandleAuthFlow.onTabsUpdated.doGetAndStoreAccessToken.then()', result,
);
const [accessToken, error] = result;
if (error !== null) {
reject(error);
} else {
resolve(accessToken);
}
}).
catch((error) => {
reject(error);
}).
finally(() => {
HotPocketExtension.api.tabs.onUpdated.removeListener(onTabsUpdated);
HotPocketExtension.api.tabs.remove(authTab.id);
});
}
}
};
HotPocketExtension.api.tabs.onUpdated.addListener(onTabsUpdated);
});
};
const doCheckAuth = async (accessToken) => {
if (accessToken === null) {
return null;
}
const call = makeJSONRPCCall('accounts.auth.check');
const [result, error] = await executeJSONRPCCall(RPC_URL, call, {
accessToken,
});
if (error !== null) {
if (error.data instanceof Error) {
throw error;
}
if (error.data.status === 403) {
return null;
}
}
if (result === false) {
return null;
}
return accessToken;
};
const doGetAccessToken = async () => {
let storageResult = await HotPocketExtension.api.storage.local.get('accessToken');
let accessToken = await doCheckAuth(
storageResult.accessToken || null,
);
if (accessToken === null) {
const authTab = await HotPocketExtension.api.tabs.create({
url: AUTH_URL,
});
accessToken = await doHandleAuthFlow(authTab);
}
return accessToken;
};
const doSendTabMessage = (tab, message) => {
HotPocketExtension.api.tabs.sendMessage(tab.id, message).
then((result) => {
HotPocketExtension.LOGGER.debug(
'HotPocketExtension.content.doSendTabMessage(): message sent', message, result,
);
}).
catch((error) => {
HotPocketExtension.LOGGER.error(
'HotPocketExtension.content.doSendTabMessage(): could not send message', error,
);
});
};
const onTabCreated = (tab) => {
HotPocketExtension.LOGGER.debug('HotPocketExtension.onTabCreated()', tab);
HotPocketExtension.api.action.enable(tab.id);
};
const onBrowserActionClicked = async (tab) => {
HotPocketExtension.LOGGER.debug(
'HotPocketExtension.onBrowserActionClicked()', tab.url,
);
if (!tab.url) {
return;
}
let result = false;
let error = null;
try {
let accessToken = await doGetAccessToken();
result = await doSave(accessToken, tab);
HotPocketExtension.LOGGER.debug(
'HotPocketExtension.onBrowserActionClicked()', result,
);
} catch (exception) {
HotPocketExtension.LOGGER.error(
'Unhandled exception when handling action click',
exception,
);
error = exception;
}
const message = {
type: 'HotPocket:Extension:save',
result: result,
error: error,
};
doSendTabMessage(tab, message);
};
const onMessage = (message, sender, sendResponse) => {
HotPocketExtension.LOGGER.debug(
'HotPocketExtension.background.onMessage()', message, sender, sendResponse,
);
let response = {ok: true};
try {
if (message.type === 'HotPocket:Extension:ping') {
HotPocketExtension.LOGGER.debug(sender.tab.id);
doSendTabMessage(sender.tab, {
type: 'HotPocket:Extension:pong',
});
}
} catch (exception) {
HotPocketExtension.LOGGER.error(
'HotPocketExtension.background.onMessage(): Unhandled exception when handling content message',
message,
exception,
);
response.ok = false;
}
sendResponse(response);
return true;
};
export default ({...configuration}) => {
HotPocketExtension.configure(configuration, {
background: true,
});
HotPocketExtension.api.tabs.onCreated.addListener(onTabCreated);
HotPocketExtension.api.action.onClicked.addListener(onBrowserActionClicked);
HotPocketExtension.api.runtime.onMessage.addListener(onMessage);
HotPocketExtension.LOGGER.info(`HotPocket v${HotPocketExtension.version} by BTHLabs`);
};

View File

@@ -0,0 +1,6 @@
import main from './main';
main({
platform: 'Safari',
api: browser,
});

View File

@@ -0,0 +1,34 @@
const ENV = __HOTPOCKET_EXTENSION_ENV__;
const DEBUG = (ENV === 'development') ? true : false;
const noop = () => {};
const HotPocketExtension = {
platform: null,
version: __HOTPOCKET_EXTENSION_VERSION__,
debug: DEBUG,
api: null,
base_url: __HOTPOCKET_EXTENSION_BASE_URL__,
LOGGER: {
// eslint-disable-next-line no-console
debug: (DEBUG === true) ? console.log : noop,
error: console.error,
info: console.info,
warning: console.warn,
},
configure: ({platform, debug, api}, options) => {
options = options || {};
HotPocketExtension.platform = platform;
HotPocketExtension.debug = debug;
HotPocketExtension.api = api;
const background = (options.background === true) ? true : false;
if (background === false && HotPocketExtension.debug === true) {
window.HotPocketExtension = HotPocketExtension;
}
},
};
export default HotPocketExtension;

View File

@@ -0,0 +1,118 @@
import HotPocketExtension from '../common';
import POPUP from './templates/popup.html';
import POPUP_CONTENT_SUCCESS from './templates/popup_content_success.html';
import POPUP_CONTENT_ERROR from './templates/popup_content_error.html';
class Popup {
constructor () {
this.container = null;
this.timeout = null;
}
setCloseTimeout = () => {
this.timeout = window.setTimeout(this.close, 5000);
};
clearCloseTimeout = () => {
if (this.timeout !== null) {
window.clearTimeout(this.timeout);
this.timeout = null;
}
};
close = () => {
this.clearCloseTimeout();
if (this.container !== null) {
this.container.remove();
this.container = null;
}
};
show = (content) => {
this.close();
this.container = document.createElement('div');
this.container.id = 'hotpocket-extension-popup';
this.container.hotPocketExtensionPopup = this;
const shadow = this.container.attachShadow({mode: 'open'});
shadow.innerHTML = POPUP;
const body = shadow.querySelector('.hotpocket-extension-popup-body');
body.innerHTML = content;
const i18nElements = shadow.querySelectorAll('[data-message]');
for (let i18nElement of i18nElements) {
i18nElement.innerHTML = HotPocketExtension.api.i18n.getMessage(
i18nElement.dataset.message,
);
}
const closeElements = shadow.querySelectorAll('.hotpocket-extension-popup-close');
for (const closeElement of closeElements) {
closeElement.addEventListener('click', this.onCloseClick);
}
document.body.appendChild(this.container);
};
onCloseClick = (event) => {
this.close();
};
}
let currentPopup = null;
const doHandleSaveMessage = (message) => {
if (currentPopup !== null) {
currentPopup.close();
}
currentPopup = new Popup();
currentPopup.show(
(message.result === true) ? POPUP_CONTENT_SUCCESS : POPUP_CONTENT_ERROR,
);
};
const doSendMessage = (message) => {
HotPocketExtension.api.runtime.sendMessage(message).
then((result) => {
HotPocketExtension.LOGGER.debug(
'HotPocketExtension.content.doSendMessage(): message sent', message, result,
);
}).
catch((error) => {
HotPocketExtension.LOGGER.error(
'HotPocketExtension.content.doSendMessage(): could not send message', error,
);
});
};
export default ({...configuration}) => {
HotPocketExtension.configure(configuration);
HotPocketExtension.api.runtime.onMessage.addListener((message, sender, sendResponse) => {
HotPocketExtension.LOGGER.debug('HotPocketExtension.content.onMessage()', message, sender, sendResponse);
let response = {ok: true};
try {
if (message.type === 'HotPocket:Extension:save') {
doHandleSaveMessage(message);
}
} catch (exception) {
HotPocketExtension.LOGGER.error(
'HotPocketExtension.content.onMessage(): Unhandled exception when handling service worker message',
message,
exception,
);
response.ok = false;
}
sendResponse(response);
return true;
});
doSendMessage({
type: 'HotPocket:Extension:ping',
});
HotPocketExtension.LOGGER.info(`HotPocket v${HotPocketExtension.version} by BTHLabs`);
};

View File

@@ -0,0 +1,6 @@
import main from './main';
main({
platform: 'Safari',
api: window.browser,
});

View File

@@ -0,0 +1,64 @@
<style type="text/css">
:host {
all: initial;
font-family: sans-serif !important;
font-size: 16px !important;
line-height: 1.5 !important;
position: fixed;
top: 20px;
right: 20px;
z-index: 999999;
}
.hotpocket-extension-popup {
background: #212529;
border: 1px solid #495057;
border-radius: 0.375rem;
color: white;
width: 300px;
}
.hotpocket-extension-popup .hotpocket-extension-popup-close {
opacity: 0.75;
}
.hotpocket-extension-popup .hotpocket-extension-popup-close:hover {
opacity: 1;
}
.hotpocket-extension-popup .hotpocket-extension-popup-header {
background: rgba(222, 226, 230, 0.3);
border-bottom: 1px solid #495057;
padding: 0.5rem 1rem;
position: relative;
}
.hotpocket-extension-popup .hotpocket-extension-popup-header strong {
margin-right: 1rem;
}
.hotpocket-extension-popup .hotpocket-extension-popup-header .hotpocket-extension-popup-close {
cursor: pointer;
display: block;
line-height: 1.5;
position: absolute;
right: 0.5rem;
text-align: center;
top: 0.5rem;
width: 1.5rem;
}
.hotpocket-extension-popup .hotpocket-extension-popup-body {
padding: 1rem;
}
.hotpocket-extension-popup .hotpocket-extension-popup-body > * {
margin: 0px;
}
.hotpocket-extension-popup .hotpocket-extension-popup-message-success {
color: rgba(25, 135, 84, 1);
}
.hotpocket-extension-popup .hotpocket-extension-popup-message-error {
color: rgba(220, 53, 69, 1);
}
</style>
<div class="hotpocket-extension-popup">
<div class="hotpocket-extension-popup-header">
<strong>HotPocket by BTHLabs</strong>
<a class="hotpocket-extension-popup-close">&times</a>
</div>
<div class="hotpocket-extension-popup-body">
</div>
</div>

View File

@@ -0,0 +1,5 @@
<p class="hotpocket-extension-popup-message hotpocket-extension-popup-message-error">
<strong data-message="content_popup_content_error_title"></strong>
<br>
<span data-message="content_popup_content_error_message"></span>
</p>

View File

@@ -0,0 +1,5 @@
<p class="hotpocket-extension-popup-message hotpocket-extension-popup-message-success">
<strong data-message="content_popup_content_success_title"></strong>
<br>
<span data-message="content_popup_content_success_message"></span>
</p>

View File

@@ -0,0 +1 @@
export default 'This file intentionally left blank';

View File

@@ -0,0 +1,34 @@
{
"manifest_version": 3,
"default_locale": "en",
"name": "__MSG_extension_name__",
"description": "__MSG_extension_description__",
"version": "1.0",
"icons": {
"48": "images/icon-48.png",
"64": "images/icon-64.png",
"96": "images/icon-96.png",
"128": "images/icon-128.png",
"256": "images/icon-256.png",
"512": "images/icon-512.png"
},
"content_scripts": [
{
"js": [
"content-bundle.js"
],
"matches": [
"<all_urls>"
]
}
],
"action": {
"default_title": "__MSG_extension_name__",
"default_icon": "images/toolbar-icon.svg"
},
"permissions": [
"storage",
"activeTab",
"tabs"
]
}

View File

@@ -0,0 +1,9 @@
{
"description": "__MSG_extension_description_Safari__",
"background": {
"scripts": [
"background-bundle.js"
],
"type": "module"
}
}

112
services/extension/tasks.py Normal file
View File

@@ -0,0 +1,112 @@
# -*- coding: utf-8 -*-
# type: ignore
from __future__ import annotations
from invoke import task
from invoke.exceptions import UnexpectedExit
from hotpocket_workspace_tools import get_workspace_mode
WORKSPACE_MODE = get_workspace_mode()
@task
def clean(ctx):
ctx.run('rm -rf dist/')
@task
def test(ctx):
# ctx.run('pytest -v --disable-warnings')
print('NOOP')
@task
def flake8(ctx):
ctx.run('flake8')
@task
def isort(ctx, check=False, diff=False):
command_parts = [
'isort',
]
if check is True:
command_parts.append('--check')
if diff is True:
command_parts.append('--diff')
command_parts.append('.')
ctx.run(' '.join(command_parts))
@task
def eslint(ctx):
ctx.run('yarn run eslint')
@task
def lint(ctx):
ihazsuccess = True
try:
flake8(ctx)
except UnexpectedExit:
ihazsuccess = False
try:
isort(ctx, check=True)
except UnexpectedExit:
ihazsuccess = False
try:
eslint(ctx)
except UnexpectedExit:
ihazsuccess = False
if ihazsuccess is False:
raise RuntimeError('FIAL')
@task
def typecheck(ctx):
ctx.run('mypy .')
@task
def django_shell(ctx):
raise NotImplementedError()
@task
def ci(ctx):
ihazsuccess = True
ci_tasks = [test, lint, typecheck]
for ci_task in ci_tasks:
try:
ci_task(ctx)
except UnexpectedExit:
ihazsuccess = False
if ihazsuccess is False:
raise RuntimeError('FIAL')
@task
def setup(ctx):
print('NOOP')
@task
def start_web(ctx):
raise NotImplementedError()
@task
def start_safari(ctx):
ctx.run('yarn watch:safari')

1157
services/extension/yarn.lock Normal file

File diff suppressed because it is too large Load Diff