12 Commits

Author SHA1 Message Date
a0f1a4ce80 Release v25.9.17
All checks were successful
CI / Checks (push) Successful in 16m16s
2025-09-17 20:38:59 +02:00
80fbbcddf3 BTHLABS-0000: bump-version task
Enough with manual version bumps :D
Co-authored-by: Tomek Wójcik <labs@tomekwojcik.pl>
Co-committed-by: Tomek Wójcik <labs@tomekwojcik.pl>
2025-09-17 20:27:20 +02:00
0ab87e25a4 BTHLABS-0000: Add scope to PWA manifest so share sheet target. 2025-09-17 20:27:20 +02:00
495255206e BTHLABS-57: Pre-auth page in the extension 2025-09-17 20:27:20 +02:00
46254730bd BTHLABS-52: Firefox Desktop Extension 2025-09-17 20:27:08 +02:00
d1e60babf4 BTHLABS-56: _Copy share link_ button in view association page
Co-authored-by: Tomek Wójcik <labs@tomekwojcik.pl>
Co-committed-by: Tomek Wójcik <labs@tomekwojcik.pl>
2025-09-15 06:28:38 +00:00
ab84f685c0 BTHLABS-51: Chrome Web Extension
Co-authored-by: Tomek Wójcik <labs@tomekwojcik.pl>
Co-committed-by: Tomek Wójcik <labs@tomekwojcik.pl>
2025-09-14 06:34:43 +00:00
1a8c4bfebc BTHLABS-0000: eslint.config.js fixes and code cleanup 2025-09-13 09:05:29 +02:00
b15b48f702 BTHLABS-55: Inline create save form
Co-authored-by: Tomek Wójcik <labs@tomekwojcik.pl>
Co-committed-by: Tomek Wójcik <labs@tomekwojcik.pl>
2025-09-13 06:56:44 +00:00
29c732faa0 Release v25.9.12
All checks were successful
CI / Checks (push) Successful in 15m7s
2025-09-11 20:50:18 +02:00
dcebccf947 BTHLABS-50: Safari Web Extension: Reloaded
Turns out, getting this thing out into the wild isn't as simple as I thought :D
Co-authored-by: Tomek Wójcik <labs@tomekwojcik.pl>
Co-committed-by: Tomek Wójcik <labs@tomekwojcik.pl>
2025-09-11 15:57:11 +00:00
67138c7035 BTHLABS-0000: Use absolute URLs in ui.meta.manifest_json 2025-09-09 15:21:45 +02:00
77 changed files with 1965 additions and 212 deletions

View File

@@ -66,7 +66,7 @@ $ docker run --rm -it \
-e HOTPOCKET_BACKEND_INITIAL_ACCOUNT_USERNAME=hotpocket \
-e HOTPOCKET_BACKEND_INITIAL_ACCOUNT_PASSWORD=hotpocketm4st3r \
-p 8000:8000 \
docker-hosted.nexus.bthlabs.pl/hotpocket/backend:aio-v25.9.8-01
docker-hosted.nexus.bthlabs.pl/hotpocket/backend:aio-v25.9.12-01
```
The command above will set up and start the application. The SQLite file will

View File

@@ -1,6 +1,6 @@
services:
backend:
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:aio-v25.9.8-01"
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:aio-v25.9.12-01"
environment:
HOTPOCKET_BACKEND_SECRET_KEY: "thisisntright"
HOTPOCKET_BACKEND_INITIAL_ACCOUNT_USERNAME: "hotpocket"

View File

@@ -8,7 +8,7 @@ x-backend-environment: &x-backend-environment
services:
webapp:
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:deployment-v25.9.8-01"
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:deployment-v25.9.12-01"
environment:
<<: *x-backend-environment
HOTPOCKET_BACKEND_ALLOWED_HOSTS: "app.staging.hotpocket.bthlab.bthlabs.net"
@@ -21,7 +21,7 @@ services:
restart: "unless-stopped"
admin:
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:deployment-v25.9.8-01"
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:deployment-v25.9.12-01"
environment:
<<: *x-backend-environment
HOTPOCKET_BACKEND_APP: "admin"
@@ -35,7 +35,7 @@ services:
restart: "unless-stopped"
celery-worker:
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:deployment-v25.9.8-01"
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:deployment-v25.9.12-01"
command:
- "/srv/venv/bin/celery"
- "-A"
@@ -57,7 +57,7 @@ services:
restart: "unless-stopped"
celery-beat:
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:deployment-v25.9.8-01"
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:deployment-v25.9.12-01"
command:
- "/srv/venv/bin/celery"
- "-A"

View File

@@ -1,6 +0,0 @@
{
"run": {
"echo": true,
"pty": true
}
}

7
invoke.yaml Normal file
View File

@@ -0,0 +1,7 @@
run:
echo: true
pty: true
files_to_version:
- "deployment/aio/docker-compose.yaml"
- "deployment/fullstack/docker-compose.yaml"
- "README.md"

View File

@@ -90,3 +90,5 @@ Shared (Extension)/Resources/images/
Shared (Extension)/Resources/background-bundle.js
Shared (Extension)/Resources/content-bundle.js
Shared (Extension)/Resources/manifest.json
Shared (Extension)/Resources/preauth.html
Shared (Extension)/Resources/preauth.js

View File

@@ -114,6 +114,8 @@
"Resources/content-bundle.js",
Resources/images,
Resources/manifest.json,
Resources/preauth.html,
Resources/preauth.js,
SafariWebExtensionHandler.m,
);
target = 4CABCAD42E56F0C900D8A354 /* HotPocket Extension (iOS) */;
@@ -126,6 +128,8 @@
"Resources/content-bundle.js",
Resources/images,
Resources/manifest.json,
Resources/preauth.html,
Resources/preauth.js,
SafariWebExtensionHandler.m,
);
target = 4CABCADE2E56F0C900D8A354 /* HotPocket Extension (macOS) */;
@@ -462,7 +466,7 @@
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 2025091701;
DEVELOPMENT_TEAM = 648728X64K;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "iOS (Extension)/Info.plist";
@@ -474,12 +478,12 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 25.9.8;
MARKETING_VERSION = 25.9.17;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
);
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.iOS.Extension;
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.Extension;
PRODUCT_NAME = "HotPocket Extension";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
@@ -492,7 +496,7 @@
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 2025091701;
DEVELOPMENT_TEAM = 648728X64K;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "iOS (Extension)/Info.plist";
@@ -504,12 +508,12 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 25.9.8;
MARKETING_VERSION = 25.9.17;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
);
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.iOS.Extension;
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.Extension;
PRODUCT_NAME = "HotPocket Extension";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
@@ -525,31 +529,36 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 2025091701;
DEVELOPMENT_TEAM = 648728X64K;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "iOS (App)/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = HotPocket;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-present BTHLabs";
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UIMainStoryboardFile = Main;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 25.9.8;
MARKETING_VERSION = 25.9.17;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
"-framework",
WebKit,
);
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.iOS;
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket;
PRODUCT_NAME = HotPocket;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
TARGETED_DEVICE_FAMILY = "1,2";
};
@@ -561,31 +570,36 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 2025091701;
DEVELOPMENT_TEAM = 648728X64K;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "iOS (App)/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = HotPocket;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-present BTHLabs";
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UIMainStoryboardFile = Main;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 25.9.8;
MARKETING_VERSION = 25.9.17;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
"-framework",
WebKit,
);
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.iOS;
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket;
PRODUCT_NAME = HotPocket;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
@@ -598,7 +612,7 @@
CODE_SIGN_ENTITLEMENTS = "macOS (Extension)/HotPocket.entitlements";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 2025091701;
DEVELOPMENT_TEAM = 648728X64K;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
@@ -612,12 +626,12 @@
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
MARKETING_VERSION = 25.9.8;
MARKETING_VERSION = 25.9.17;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
);
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.macOS.Extension;
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.Extension;
PRODUCT_NAME = "HotPocket Extension";
SDKROOT = macosx;
SKIP_INSTALL = YES;
@@ -629,9 +643,9 @@
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_ENTITLEMENTS = "macOS (Extension)/HotPocket.entitlements";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 2025091701;
DEVELOPMENT_TEAM = 648728X64K;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
@@ -645,12 +659,12 @@
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
MARKETING_VERSION = 25.9.8;
MARKETING_VERSION = 25.9.17;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
);
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.macOS.Extension;
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.Extension;
PRODUCT_NAME = "HotPocket Extension";
SDKROOT = macosx;
SKIP_INSTALL = YES;
@@ -666,12 +680,13 @@
CODE_SIGN_ENTITLEMENTS = "macOS (App)/HotPocket.entitlements";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 2025091701;
DEVELOPMENT_TEAM = 648728X64K;
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = HotPocket;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-present BTHLabs";
INFOPLIST_KEY_NSMainStoryboardFile = Main;
INFOPLIST_KEY_NSPrincipalClass = NSApplication;
LD_RUNPATH_SEARCH_PATHS = (
@@ -679,14 +694,14 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 25.9.8;
MARKETING_VERSION = 25.9.17;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
"-framework",
WebKit,
);
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.macOS;
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket;
PRODUCT_NAME = HotPocket;
REGISTER_APP_GROUPS = YES;
SDKROOT = macosx;
@@ -700,14 +715,15 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "macOS (App)/HotPocket.entitlements";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 2025091701;
DEVELOPMENT_TEAM = 648728X64K;
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = HotPocket;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-present BTHLabs";
INFOPLIST_KEY_NSMainStoryboardFile = Main;
INFOPLIST_KEY_NSPrincipalClass = NSApplication;
LD_RUNPATH_SEARCH_PATHS = (
@@ -715,14 +731,14 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 25.9.8;
MARKETING_VERSION = 25.9.17;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
"-framework",
WebKit,
);
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.macOS;
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket;
PRODUCT_NAME = HotPocket;
REGISTER_APP_GROUPS = YES;
SDKROOT = macosx;

3
services/apple/README.md Normal file
View File

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

View File

@@ -0,0 +1,5 @@
run:
echo: true
pty: true
files_to_version:
- "HotPocket.xcodeproj/project.pbxproj"

511
services/apple/poetry.lock generated Normal file
View File

@@ -0,0 +1,511 @@
# 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.dependencies]
invoke = "2.2.0"
[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-apple"
version = "25.9.12"
description = "HotPocket Apple Integrations"
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"

16
services/apple/setup.cfg Normal file
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

124
services/apple/tasks.py Normal file
View File

@@ -0,0 +1,124 @@
# -*- coding: utf-8 -*-
# type: ignore
from __future__ import annotations
import re
from invoke import Context, task
from invoke.exceptions import UnexpectedExit
from hotpocket_workspace_tools import get_workspace_mode
import hotpocket_workspace_tools.tasks
WORKSPACE_MODE = get_workspace_mode()
@task
def clean(ctx: Context):
ctx.run('rm -rf DerivedData/')
@task
def test(ctx: Context):
print('NOOP')
@task
def flake8(ctx: Context):
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: Context):
print('NOOP')
@task
def lint(ctx: Context):
ihazsuccess = True
try:
flake8(ctx)
except UnexpectedExit:
ihazsuccess = False
try:
isort(ctx, check=True)
except UnexpectedExit:
ihazsuccess = False
if ihazsuccess is False:
raise RuntimeError('FIAL')
@task
def typecheck(ctx: Context):
ctx.run('mypy .')
@task
def django_shell(ctx: Context):
raise NotImplementedError()
@task
def ci(ctx: Context):
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: Context):
print('NOOP')
@task
def start_web(ctx: Context):
raise NotImplementedError()
@task
def bump_version(ctx: Context, next_version: str, build: str | None = None):
hotpocket_workspace_tools.tasks.utils.bump_version(
ctx, next_version, build=build,
)
pbxproj_content = None
with open('HotPocket.xcodeproj/project.pbxproj', 'r', encoding='utf-8') as pbxproj_content_f:
pbxproj_content = pbxproj_content_f.read()
pbxproj_content = re.sub(
r'CURRENT_PROJECT_VERSION = [0-9]{10};',
f'CURRENT_PROJECT_VERSION = {build};',
pbxproj_content,
flags=re.MULTILINE,
)
with open('HotPocket.xcodeproj/project.pbxproj', 'w', encoding='utf-8') as pbxproj_content_f:
pbxproj_content_f.write(pbxproj_content)

View File

@@ -7,7 +7,7 @@ export default defineConfig([
{
files: [
'eslint.config.js',
'hotpocket_backend/apps/ui/static/ui/js/hotpocket.*.js',
'hotpocket_backend/apps/ui/static/ui/js/hotpocket-backend.*.js',
],
plugins: {
js,
@@ -66,7 +66,6 @@ export default defineConfig([
},
{
ignores: [
'hotpocket_backend/apps/**/static/**/*.js',
'hotpocket_backend/static/**/*.js',
],
},

View File

@@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
version = '25.9.8'
version = '25.9.17'

View File

@@ -11,7 +11,10 @@ import uuid6
from hotpocket_backend.apps.accounts.models import AccessToken
from hotpocket_backend.apps.core.conf import settings
from hotpocket_soa.dto.accounts import AccessTokensQuery
from hotpocket_soa.dto.accounts import (
AccessTokenMetaUpdateIn,
AccessTokensQuery,
)
LOGGER = logging.getLogger(__name__)
@@ -54,6 +57,16 @@ class AccessTokensService:
f'Access Token not found: pk=`{pk}`',
) from exception
def get_by_key(self, *, key: str) -> AccessToken:
try:
query_set = AccessToken.active_objects
return query_set.get(key=key)
except AccessToken.DoesNotExist as exception:
raise self.AccessTokenNotFound(
f'Access Token not found: key=`{key}`',
) from exception
def search(self,
*,
query: AccessTokensQuery,
@@ -79,3 +92,27 @@ class AccessTokensService:
access_token.soft_delete()
return True
def update_meta(self,
*,
pk: uuid.UUID,
update: AccessTokenMetaUpdateIn,
) -> AccessToken:
access_token = AccessToken.active_objects.get(pk=pk)
next_meta = {
**(access_token.meta or {}),
}
if update.version is not None:
next_meta['version'] = update.version
if update.platform is not None:
next_meta['platform'] = update.platform
access_token.meta = next_meta
access_token.save()
access_token.refresh_from_db()
return access_token

View File

@@ -21,3 +21,5 @@ class StarUnstarAssociationViewMode(enum.Enum):
class UIAccessTokenOriginApp(enum.Enum):
SAFARI_WEB_EXTENSION = _('Safari Web Extension')
CHROME_EXTENSION = _('Chrome Extension')
FIREFOX_EXTENSION = _('Firefox Extension')

View File

@@ -1,10 +1,62 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
import logging
from bthlabs_jsonrpc_core import register_method
from django import db
from django.http import HttpRequest
from hotpocket_soa.dto.accounts import AccessTokenMetaUpdateIn
from hotpocket_soa.services import AccessTokensService
LOGGER = logging.getLogger(__name__)
@register_method('accounts.auth.check')
def check(request: HttpRequest) -> bool:
return request.user.is_anonymous is False
@register_method('accounts.auth.check_access_token')
def check_access_token(request: HttpRequest,
access_token: str,
meta: dict | None = None,
) -> bool:
result = True
try:
access_tokens_service = AccessTokensService()
with db.transaction.atomic():
access_token_object = access_tokens_service.get_by_key(
account_uuid=request.user.pk,
key=access_token,
)
meta_update = AccessTokenMetaUpdateIn.model_validate(
(meta or {}),
)
_ = access_tokens_service.update_meta(
access_token=access_token_object,
update=meta_update,
)
except AccessTokensService.AccessTokenNotFound as exception:
LOGGER.error(
'Access Token not found: account_uuid=`%s` key=`%s`',
request.user.pk,
access_token,
exc_info=exception,
)
result = False
except AccessTokensService.AccessTokenAccessDenied as exception:
LOGGER.error(
'Access Token access denied: account_uuid=`%s` key=`%s`',
request.user.pk,
access_token,
exc_info=exception,
)
result = False
return result

View File

@@ -23,7 +23,6 @@ body, html {
}
body > .ui-viewport {
padding-bottom: 3.5rem;
padding-top: 3.5rem;
}
@@ -103,3 +102,23 @@ body.ui-mode-standalone #offcanvas-controls .offcanvas-body {
#BrowseSavesView-Search .form-select {
min-width: 10rem;
}
#navbar .ui-inline-create-save-form {
bottom: 0px;
right: 0px;
top: 0px;
width: 500px;
}
@media screen and (max-width: 767px) {
#navbar .ui-inline-create-save-form {
right: 0.5rem;
width: calc(100vw - 0.5rem - 0.5rem);
}
}
@media screen and (min-width: 768px) {
#InlineCreateSaveForm {
position: relative;
}
}

View File

@@ -0,0 +1,52 @@
/*!
* 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.
*/
((HotPocket, htmx) => {
class InlineCreateSaveForm {
constructor (app) {
this.app = app;
this.element = null;
this.buttonTrigger = null;
this.form = null;
}
onLoad = (event) => {
this.element = document.getElementById('InlineCreateSaveForm');
this.buttonTrigger = this.element.querySelector('.ui-inline-create-save-form-trigger');
this.buttonClose = this.element.querySelector('.ui-inline-create-save-form-close');
this.form = this.element.querySelector('.ui-inline-create-save-form');
this.buttonTrigger.addEventListener('click', this.onTriggerClick);
this.buttonClose.addEventListener('click', this.onCloseClick);
};
onTriggerClick = (event) => {
event.preventDefault();
this.form.classList.remove('d-none');
this.form.classList.add('d-flex');
};
onCloseClick = (event) => {
event.stopPropagation();
event.preventDefault();
this.form.classList.add('d-none');
this.form.classList.remove('d-flex');
};
}
HotPocket.addPlugin('UI.InlineCreateSaveForm', (app) => {
return new InlineCreateSaveForm(app);
});
})(window.HotPocket, window.htmx);

View File

@@ -0,0 +1,58 @@
/*!
* 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.
*/
((HotPocket) => {
class HotPocketUIPasteboardLinkPlugin {
constructor (app) {
}
onLoad (event) {
let canCopy = false;
if (navigator.clipboard && navigator.clipboard.writeText) {
canCopy = true;
}
for (let pasteboardLink of document.querySelectorAll('.ui-pasteboard-link')) {
if (canCopy === false) {
pasteboardLink.classList.add('d-none');
} else {
pasteboardLink.addEventListener('click', this.onClick);
}
}
}
onClick = (event) => {
event.stopPropagation();
event.preventDefault();
const icon = event.target.querySelector('i.bi');
icon.classList.replace('bi-clipboard-fill', 'bi-clipboard');
navigator.clipboard.writeText(event.target.href).
then(() => {
icon.classList.replace('bi-clipboard', 'bi-clipboard-fill');
}).
catch((reason) => {
console.error('HotPocket.UI.PasteboardLink.onClick()', reason);
window.alert('Could not copy the link :(');
});
return false;
};
}
HotPocket.addPlugin('UI.PasteboardLink', (app) => {
return new HotPocketUIPasteboardLinkPlugin(app);
});
})(window.HotPocket);

View File

@@ -41,7 +41,20 @@
if (navigator.share) {
shareButton.addEventListener('click', this.onShareButtonClick);
} else {
shareButton.addClass('d-none');
shareButton.remove();
}
}
const uiPlugin = this.app.plugin('UI');
for (let controlButton of document.querySelectorAll('#ViewAssociationView .ui-controls > a.btn')) {
if (uiPlugin.jsEnabled === true) {
if (controlButton.classList.contains('ui-noscript-show')) {
controlButton.remove();
}
} else {
if (controlButton.classList.contains('ui-noscript-hide')) {
controlButton.remove();
}
}
}
};
@@ -59,12 +72,9 @@
onShareButtonClick = (event) => {
event.preventDefault();
const shareUrl = new URL(window.location.href);
shareUrl.searchParams.set('share', 'true');
navigator.share({
title: document.title,
url: shareUrl.toString(),
url: event.target.href,
});
return false;

View File

@@ -18,6 +18,7 @@
class HotPocketUIPlugin {
constructor (app) {
document.body.classList.add('ui-js-enabled');
this.jsEnabled = document.body.classList.contains('ui-js-enabled');
if (window.navigator.standalone === true) {
document.querySelector('body').classList.add('ui-mode-standalone');

View File

@@ -25,8 +25,8 @@
</a>
</p>
{% if show_controls %}
<p class="mb-3 text-center">
{% spaceless %}
<div class="d-flex justify-content-center mb-3">
<div class="btn-group ui-controls" role="group">
<a
class="btn btn-primary btn-sm"
href="{% url 'ui.associations.edit' pk=association.pk %}"
@@ -35,14 +35,29 @@
<i class="bi bi-pencil"></i> {% translate 'Edit' %}
</a>
<a
class="btn btn-secondary btn-sm ms-2 ui-noscript-hide ui-share-button"
href="#"
class="btn btn-secondary btn-sm ui-noscript-hide ui-share-button"
href="{{ share_url }}"
role="button"
>
<i class="bi bi-box-arrow-up"></i> {% translate 'Share' %}
</a>
{% endspaceless %}
</p>
<a
class="btn btn-secondary btn-sm ui-noscript-hide ui-pasteboard-link"
href="{{ share_url }}"
role="button"
>
<i class="bi bi-clipboard"></i> {% translate 'Copy share link' %}
</a>
<a
class="btn btn-secondary btn-sm ui-noscript-show"
href="{{ share_url }}"
rel="noopener noreferer"
target="_blank"
>
<i class="bi bi-link-45deg"></i> {% translate 'Share link' %}
</a>
</div>
</div>
{% endif %}
{% if association.description %}
<div class="row mb-3">

View File

@@ -40,7 +40,25 @@
</a>
</li>
{% endif %}
<li class="nav-item ms-auto d-flex align-items-center">
{% if not request.user.is_anonymous %}
<li id="InlineCreateSaveForm" class="nav-item ms-auto me-1 d-flex align-items-center">
<a class="nav-link px-1 py-0 fs-3 text-primary ui-inline-create-save-form-trigger" href="{% url 'ui.saves.create' %}">
<i class="bi bi-plus-circle-fill"></i>
</a>
<form action="{% url 'ui.saves.create' %}" class="position-absolute d-none align-items-center bg-body-tertiary ui-inline-create-save-form" method="post">
{% csrf_token %}
<label class="visually-hidden" for="input-inline-create-save-form-url">{% translate 'URL' %}</label>
<input class="form-control flex-grow-1 me-1 form-control-sm" id="input-inline-create-save-form-url" name="url" placeholder="{% translate 'URL' %}" type="text">
<button class="btn btn-primary btn-sm flex-grow-0 flex-shrink-0 me-1" role="button">
<i class="bi bi-floppy-fill"></i>
</button>
<button class="btn btn-outline-danger btn-sm flex-grow-0 flex-shrink-0 ui-inline-create-save-form-close">
<i class="bi bi-x-lg"></i>
</button>
</form>
</li>
{% endif %}
<li class="nav-item {% if request.user.is_anonymous %}ms-auto{% endif %} d-flex align-items-center">
<a class="nav-link px-1 py-0 fs-3" data-bs-toggle="offcanvas" href="#offcanvas-controls" aria-controls="offcanvas-controls">
<i class="bi bi-person-circle"></i>
</a>
@@ -74,20 +92,6 @@
</template>
</div>
{% if not request.user.is_anonymous %}
<nav id="button-bar" class="navbar navbar-expand-sm bg-body-tertiary fixed-bottom {% block button_bar_class %}{% endblock %}">
<div class="container">
<ul class="navbar-nav my-0 mx-auto">
<li class="nav-item d-flex align-items-center ms-2">
<a class="nav-link py-1 px-1 text-primary ui-button-bar-link" href="{% url 'ui.saves.create' %}">
<i class="bi bi-plus-circle-fill"></i>
</a>
</li>
</ul>
</div>
</nav>
{% endif %}
<div class="offcanvas offcanvas-end" tabindex="-1" id="offcanvas-controls" aria-labelledby="offcanvas-controls-label">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="offcanvas-controls-label">
@@ -147,6 +151,8 @@
<script src="{% static 'ui/js/hotpocket-backend.ui.BrowseSavesView.js' %}" type="text/javascript"></script>
<script src="{% static 'ui/js/hotpocket-backend.ui.ViewAssociationView.js' %}" type="text/javascript"></script>
<script src="{% static 'ui/js/hotpocket-backend.ui.BrowseAccountAppsView.js' %}" type="text/javascript"></script>
<script src="{% static 'ui/js/hotpocket-backend.ui.InlineCreateSaveForm.js' %}" type="text/javascript"></script>
<script src="{% static 'ui/js/hotpocket-backend.ui.PasteboardLink.js' %}" type="text/javascript"></script>
{% block page_scripts %}{% endblock %}
<script type="text/javascript">
(() => {

View File

@@ -132,10 +132,15 @@ def render_access_token_app(access_token: AccessTokenOut) -> str:
variant = 'secondary'
origin_app = access_token.get_origin_app()
match origin_app:
case AccessTokenOriginApp.SAFARI_WEB_EXTENSION:
app = UIAccessTokenOriginApp[origin_app.value].value
variant = 'info'
extension_origin_apps = (
AccessTokenOriginApp.SAFARI_WEB_EXTENSION,
AccessTokenOriginApp.CHROME_EXTENSION,
AccessTokenOriginApp.FIREFOX_EXTENSION,
)
if origin_app in extension_origin_apps:
app = UIAccessTokenOriginApp[origin_app.value].value
variant = 'info'
return format_html(
'<span class="badge text-bg-{}">{}</span>',

View File

@@ -198,12 +198,21 @@ def view(request: HttpRequest, pk: uuid.UUID) -> HttpResponse:
if is_share is True:
show_controls = show_controls and False
share_url = reverse(
'ui.associations.view',
args=(association.pk,),
query=[
('share', 'true'),
],
)
return render(
request,
'ui/associations/view.html',
{
'association': association,
'show_controls': show_controls,
'share_url': share_url,
},
)

View File

@@ -12,24 +12,33 @@ def manifest_json(request: HttpRequest) -> JsonResponse:
result = {
'name': settings.SITE_TITLE,
'short_name': settings.SITE_SHORT_TITLE,
'start_url': reverse('ui.associations.browse'),
'start_url': request.build_absolute_uri(
reverse('ui.associations.browse'),
),
'display': 'standalone',
'background_color': '#212529',
'theme_color': '#2b3035',
'icons': [
{
'src': static('ui/img/icon-192.png'),
'src': request.build_absolute_uri(
static('ui/img/icon-192.png'),
),
'sizes': '192x192',
'type': 'image/png',
},
{
'src': static('ui/img/icon-512.png'),
'src': request.build_absolute_uri(
static('ui/img/icon-512.png'),
),
'sizes': '512x512',
'type': 'image/png',
},
],
'scope': '/',
'share_target': {
'action': reverse('ui.integrations.android.share_sheet'),
'action': request.build_absolute_uri(
reverse('ui.integrations.android.share_sheet'),
),
'method': 'POST',
'enctype': 'multipart/form-data',
'params': {

View File

@@ -72,6 +72,7 @@ JSONRPC_METHOD_MODULES = [
CORS_ALLOWED_ORIGIN_REGEXES = [
r'safari-web-extension:\/\/.+?',
r'chrome-extension:\/\/.+?',
]
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_HEADERS = (

View File

@@ -1,3 +1,8 @@
run:
echo: true
pty: true
files_to_version:
- "hotpocket_backend/_meta.py"
- "ops/bin/entrypoint-deployment.sh"
- "package.json"
- "pyproject.toml"

View File

@@ -13,7 +13,7 @@ cat <<EOF
|_|
production
HotPocket v25.9.8 [${HOTPOCKET_BACKEND_IMAGE_ID}] (https://hotpocket.app/)
HotPocket v25.9.17 [${HOTPOCKET_BACKEND_IMAGE_ID}] (https://hotpocket.app/)
Copyright 2025-present by BTHLabs. All rights reserved. (https://bthlabs.pl/)
Licensed under Apache-2.0
EOF

View File

@@ -1,6 +1,6 @@
{
"name": "hotpocket-backend",
"version": "25.9.8",
"version": "25.9.17",
"description": "HotPocket Backend",
"main": "hotpocket_backend/apps/frontend/src/index.js",
"repository": "https://git.bthlabs.pl/tomekwojcik/hotpocket",

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "hotpocket-backend"
version = "25.9.8"
version = "25.9.17"
description = "HotPocket Backend"
authors = ["Tomek Wójcik <contact@bthlabs.pl>"]
license = "Apache-2.0"

View File

@@ -5,32 +5,33 @@ from __future__ import annotations
import os
from invoke import task
from invoke import Context, task
from invoke.exceptions import UnexpectedExit
from hotpocket_workspace_tools import WorkspaceMode, get_workspace_mode
from hotpocket_workspace_tools.tasks import * # noqa: F401,F403
WORKSPACE_MODE = get_workspace_mode()
ENV = os.getenv('HOTPOCKET_BACKEND_ENV', 'docker')
@task
def clean(ctx):
def clean(ctx: Context):
ctx.run('rm -rf hotpocket_backend/static/')
@task
def test(ctx):
def test(ctx: Context):
ctx.run('pytest -v --disable-warnings')
@task
def flake8(ctx):
def flake8(ctx: Context):
ctx.run('flake8')
@task
def isort(ctx, check=False, diff=False):
def isort(ctx: Context, check=False, diff=False):
command_parts = [
'isort',
]
@@ -47,12 +48,12 @@ def isort(ctx, check=False, diff=False):
@task
def eslint(ctx):
def eslint(ctx: Context):
ctx.run('yarn run eslint')
@task
def lint(ctx):
def lint(ctx: Context):
ihazsuccess = True
try:
@@ -75,17 +76,17 @@ def lint(ctx):
@task
def typecheck(ctx):
def typecheck(ctx: Context):
ctx.run('mypy .')
@task
def django_shell(ctx):
def django_shell(ctx: Context):
ctx.run('python manage.py shell_plus')
@task
def ci(ctx):
def ci(ctx: Context):
ihazsuccess = True
ci_tasks = [test, lint, typecheck]
@@ -100,7 +101,7 @@ def ci(ctx):
@task
def setup(ctx):
def setup(ctx: Context):
ctx.run('python manage.py migrate')
ctx.run('python manage.py create_initial_account hotpocket hotpocketm4st3r')
@@ -109,13 +110,13 @@ def setup(ctx):
@task
def start_web(ctx):
def start_web(ctx: Context):
bind = os.getenv('HOTPOCKET_BACKEND_RUNSERVER_BIND_WEB', '127.0.0.1:8001')
ctx.run(f'python manage.py runserver {bind}')
@task
def start_admin(ctx):
def start_admin(ctx: Context):
bind = os.getenv('HOTPOCKET_BACKEND_RUNSERVER_BIND_ADMIN', '127.0.0.1:8002')
ctx.run(f'python manage.py runserver {bind}', env=dict(
DJANGO_SETTINGS_MODULE=f'hotpocket_backend.settings.{ENV}.admin',
@@ -124,7 +125,7 @@ def start_admin(ctx):
@task
def start_celery_worker(ctx, concurrency=2):
def start_celery_worker(ctx: Context, concurrency=2):
command_parts = [
'celery',
'-A hotpocket_backend.celery:app',
@@ -137,5 +138,5 @@ def start_celery_worker(ctx, concurrency=2):
@task
def start_celery_beat(ctx, schedule_path='run/celery-beat-schedule'):
def start_celery_beat(ctx: Context, schedule_path='run/celery-beat-schedule'):
ctx.run(f'celery -A hotpocket_backend.celery:app beat -l INFO -s {schedule_path}')

View File

@@ -11,7 +11,7 @@ from hotpocket_backend.apps.accounts.models import AccessToken
def AccessTokenMetaFactory() -> dict:
return {
'platform': 'MacIntel',
'version': '1987.10.03',
'version': '1985.12.12',
}
@@ -19,7 +19,7 @@ class AccessTokenFactory(factory.django.DjangoModelFactory):
account_uuid = None
key = factory.LazyFunction(lambda: str(uuid.uuid4()))
origin = factory.LazyFunction(
lambda: f'safari-web-extension//{uuid.uuid4()}',
lambda: f'safari-web-extension://{uuid.uuid4()}',
)
meta = factory.LazyFunction(AccessTokenMetaFactory)

View File

@@ -1,2 +1,3 @@
from .access_token import * # noqa: F401,F403
from .account import * # noqa: F401,F403
from .apps import * # noqa: F401,F403

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# type: ignore
from __future__ import annotations
import uuid
import pytest
@pytest.fixture
def safari_extension_origin():
return f'safari-web-extension://{uuid.uuid4()}'
@pytest.fixture
def safari_extension_meta():
return {
'platform': 'MacIntel',
'version': '1987.10.03',
}

View File

@@ -24,15 +24,28 @@ class AccessTokensTestingService:
assert access_token.updated_at is not None
def assert_deleted(self, *, pk: uuid.UUID, reference: typing.Any = None):
association = AccessToken.objects.get(pk=pk)
assert association.deleted_at is not None
access_token = AccessToken.objects.get(pk=pk)
assert access_token.deleted_at is not None
if reference is not None:
assert association.updated_at > reference.updated_at
assert access_token.updated_at > reference.updated_at
def assert_not_deleted(self, *, pk: uuid.UUID, reference: typing.Any = None):
association = AccessToken.objects.get(pk=pk)
assert association.deleted_at is None
access_token = AccessToken.objects.get(pk=pk)
assert access_token.deleted_at is None
if reference is not None:
assert association.updated_at == reference.updated_at
assert access_token.updated_at == reference.updated_at
def assert_meta_updated(self, *, pk: uuid.UUID, meta_update: dict, reference: typing.Any = None):
access_token = AccessToken.objects.get(pk=pk)
if len(meta_update) > 0 and reference is not None:
expected_meta = {
**reference.meta,
**meta_update,
}
assert access_token.meta == expected_meta
if reference is not None:
assert access_token.updated_at > reference.updated_at

View File

@@ -27,6 +27,14 @@ def test_authenticated_ok(authenticated_client: Client,
assert hasattr(result.context['association'], 'target') is True
assert result.context['association'].target.pk == association_out.target.pk
assert result.context['show_controls'] is True
assert 'share_url' in result.context
expected_share_url = reverse(
'ui.associations.view',
args=(association_out.pk,),
query=[('share', 'true')],
)
assert result.context['share_url'] == expected_share_url
@pytest.mark.django_db

View File

@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
# type: ignore
from __future__ import annotations
import http
from django.test import Client
from django.urls import reverse
def test_ok(client: Client, settings):
# When
result = client.get(reverse('ui.meta.manifest_json'))
# Then
assert result.status_code == http.HTTPStatus.OK
payload = result.json()
assert payload['name'] == settings.SITE_TITLE
assert payload['short_name'] == settings.SITE_SHORT_TITLE
assert payload['start_url'] == f"http://testserver{reverse('ui.associations.browse')}"
assert payload['share_target']['action'] == (
f"http://testserver{reverse('ui.integrations.android.share_sheet')}"
)

View File

@@ -14,29 +14,16 @@ from hotpocket_backend_testing.services.accounts import (
)
@pytest.fixture
def origin():
return f'safari-web-extension://{uuid.uuid4()}'
@pytest.fixture
def auth_key():
return str(uuid.uuid4())
@pytest.fixture
def meta():
return {
'platform': 'MacIntel',
'version': '1987.10.03',
}
@pytest.fixture
def call(rpc_call_factory, auth_key, meta):
def call(rpc_call_factory, auth_key, safari_extension_meta):
return rpc_call_factory(
'accounts.access_tokens.create',
[auth_key, meta],
[auth_key, safari_extension_meta],
)
@@ -44,9 +31,9 @@ def call(rpc_call_factory, auth_key, meta):
def test_ok(authenticated_client: Client,
auth_key,
call,
origin,
safari_extension_origin,
account,
meta,
safari_extension_meta,
):
# Given
session = authenticated_client.session
@@ -59,7 +46,7 @@ def test_ok(authenticated_client: Client,
data=call,
content_type='application/json',
headers={
'Origin': origin,
'Origin': safari_extension_origin,
},
)
@@ -72,8 +59,8 @@ def test_ok(authenticated_client: Client,
AccessTokensTestingService().assert_created(
key=call_result['result'],
account_uuid=account.pk,
origin=origin,
meta=meta,
origin=safari_extension_origin,
meta=safari_extension_meta,
)
assert 'extension_auth_key' not in authenticated_client.session
@@ -82,7 +69,7 @@ def test_ok(authenticated_client: Client,
@pytest.mark.django_db
def test_auth_key_missing(authenticated_client: Client,
call,
origin,
safari_extension_origin,
):
# When
result = authenticated_client.post(
@@ -90,7 +77,7 @@ def test_auth_key_missing(authenticated_client: Client,
data=call,
content_type='application/json',
headers={
'Origin': origin,
'Origin': safari_extension_origin,
},
)
@@ -105,7 +92,7 @@ def test_auth_key_missing(authenticated_client: Client,
@pytest.mark.django_db
def test_auth_key_mismatch(authenticated_client: Client,
call,
origin,
safari_extension_origin,
):
# Given
session = authenticated_client.session
@@ -118,7 +105,7 @@ def test_auth_key_mismatch(authenticated_client: Client,
data=call,
content_type='application/json',
headers={
'Origin': origin,
'Origin': safari_extension_origin,
},
)

View File

@@ -0,0 +1,205 @@
# -*- coding: utf-8 -*-
# type: ignore
from __future__ import annotations
import http
from django.test import Client
from django.urls import reverse
import pytest
from hotpocket_backend_testing.services.accounts import (
AccessTokensTestingService,
)
@pytest.fixture
def call_factory(request: pytest.FixtureRequest, rpc_call_factory):
default_access_token = request.getfixturevalue('access_token_out')
default_meta_update = request.getfixturevalue('safari_extension_meta')
def factory(access_token=None, meta_update=None):
return rpc_call_factory(
'accounts.auth.check_access_token',
[
(
access_token.key
if access_token is not None
else default_access_token.key
),
(
meta_update
if meta_update is not None
else default_meta_update
),
],
)
return factory
@pytest.fixture
def call(call_factory):
return call_factory()
@pytest.mark.django_db
def test_ok(authenticated_client: Client,
call,
access_token_out,
safari_extension_meta,
):
# When
result = authenticated_client.post(
reverse('ui.rpc'),
data=call,
content_type='application/json',
)
# Then
assert result.status_code == http.HTTPStatus.OK
call_result = result.json()
assert 'error' not in call_result
assert call_result['result'] is True
AccessTokensTestingService().assert_meta_updated(
pk=access_token_out.pk,
meta_update=safari_extension_meta,
reference=access_token_out,
)
@pytest.mark.parametrize(
'meta_keys_to_pop',
[
('platform',),
('version',),
('platform', 'version'),
],
)
@pytest.mark.django_db
def test_ok_with_partial_meta_update(meta_keys_to_pop,
safari_extension_meta,
authenticated_client: Client,
call_factory,
access_token_out,
):
# Given
meta_update = {**safari_extension_meta}
for meta_key_to_pop in meta_keys_to_pop:
meta_update.pop(meta_key_to_pop)
call = call_factory(meta_update=meta_update)
# When
result = authenticated_client.post(
reverse('ui.rpc'),
data=call,
content_type='application/json',
)
# Then
assert result.status_code == http.HTTPStatus.OK
call_result = result.json()
assert 'error' not in call_result
assert call_result['result'] is True
AccessTokensTestingService().assert_meta_updated(
pk=access_token_out.pk,
meta_update=meta_update,
reference=access_token_out,
)
@pytest.mark.django_db
def test_invalid_access_token(authenticated_client: Client,
call,
):
# Given
call['params'][0] = 'thisisntright'
# When
result = authenticated_client.post(
reverse('ui.rpc'),
data=call,
content_type='application/json',
)
# Then
assert result.status_code == http.HTTPStatus.OK
call_result = result.json()
assert 'error' not in call_result
assert call_result['result'] is False
@pytest.mark.django_db
def test_deleted_access_token(call_factory,
deleted_access_token_out,
authenticated_client: Client,
):
# Given
call = call_factory(access_token=deleted_access_token_out)
# When
result = authenticated_client.post(
reverse('ui.rpc'),
data=call,
content_type='application/json',
)
# Then
assert result.status_code == http.HTTPStatus.OK
call_result = result.json()
assert 'error' not in call_result
assert call_result['result'] is False
@pytest.mark.django_db
def test_other_account_access_token(call_factory,
other_account_access_token_out,
authenticated_client: Client,
):
# Given
call = call_factory(access_token=other_account_access_token_out)
# When
result = authenticated_client.post(
reverse('ui.rpc'),
data=call,
content_type='application/json',
)
# Then
assert result.status_code == http.HTTPStatus.OK
call_result = result.json()
assert 'error' not in call_result
assert call_result['result'] is False
@pytest.mark.django_db
def test_inactive_account(inactive_account_client: Client, call):
# When
result = inactive_account_client.post(
reverse('ui.rpc'),
data=call,
)
# Then
assert result.status_code == http.HTTPStatus.FORBIDDEN
@pytest.mark.django_db
def test_anonymous(client: Client, call):
# When
result = client.post(
reverse('ui.rpc'),
data=call,
)
# Then
assert result.status_code == http.HTTPStatus.FORBIDDEN

View File

@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>hotpocket.app</title>
<meta name="robots" content="noindex, nofollow">
</head>
<body>
<h1>SOON</h1>
</body>
</html>

View File

@@ -1,2 +1,4 @@
node_modules/
dist/
secrets/*.json
secrets/*.pem

View File

@@ -3,6 +3,10 @@
"message": "Save to HotPocket",
"description": "The display name for the extension."
},
"extension_name_development": {
"message": "HotPocket Development",
"description": "The display name for the extension."
},
"extension_description": {
"message": "Save to HotPocket. Straight from the toolbar!",
"description": "Description of what the extension does."

Binary file not shown.

After

Width:  |  Height:  |  Size: 874 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 594 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -5,9 +5,9 @@ import globals from 'globals';
export default defineConfig([
{
files: [
'eslint.config.js',
'src/**/*.js',
ignores: [
'dist/**',
'rollup.config.js',
],
plugins: {
js,

View File

@@ -1,3 +1,8 @@
run:
echo: true
pty: true
files_to_version:
- "src/content/preauth.html"
- "src/manifest/common.json"
- "package.json"
- "pyproject.toml"

View File

@@ -1,6 +1,6 @@
{
"name": "hotpocket-extension",
"version": "25.9.8",
"version": "25.9.17",
"description": "HotPocket Extension",
"main": "src/index.js",
"repository": "https://git.bthlabs.pl/tomekwojcik/hotpocket",
@@ -12,7 +12,13 @@
"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"
"watch:safari": "HOTPOCKET_EXTENSION_TARGET=safari npx rollup -c rollup.config.js -w",
"build:chrome": "NODE_ENV=production HOTPOCKET_EXTENSION_TARGET=chrome npx rollup -c rollup.config.js",
"dev:chrome": "HOTPOCKET_EXTENSION_TARGET=chrome npx rollup -c rollup.config.js",
"watch:chrome": "HOTPOCKET_EXTENSION_TARGET=chrome npx rollup -c rollup.config.js -w",
"build:firefox": "NODE_ENV=production HOTPOCKET_EXTENSION_TARGET=firefox npx rollup -c rollup.config.js",
"dev:firefox": "HOTPOCKET_EXTENSION_TARGET=firefox npx rollup -c rollup.config.js",
"watch:firefox": "HOTPOCKET_EXTENSION_TARGET=firefox npx rollup -c rollup.config.js -w"
},
"devDependencies": {
"@eslint/js": "9.33.0",

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "hotpocket-extension"
version = "25.9.8"
version = "25.9.17"
description = "HotPocket Extension"
authors = ["Tomek Wójcik <contact@bthlabs.pl>"]
license = "Apache-2.0"

View File

@@ -3,7 +3,9 @@ import copy from 'rollup-plugin-copy';
import {string} from 'rollup-plugin-string';
import packageJSON from './package.json' with {type: 'json'};
import manifestChrome from './src/manifest/chrome.json' with {type: 'json'};
import manifestCommon from './src/manifest/common.json' with {type: 'json'};
import manifestFirefox from './src/manifest/firefox.json' with {type: 'json'};
import manifestSafari from './src/manifest/safari.json' with {type: 'json'};
const BANNER = `/*!
@@ -38,7 +40,9 @@ if (BASE_URL === null) {
const PLUGINS = [
string({
include: /\.html$/,
include: [
'src/content/templates/*.html',
],
}),
replace({
include: /\.js$/,
@@ -62,10 +66,24 @@ const manifestJsonOutputPlugin = () => {
...result,
...manifestSafari,
};
} else if (TARGET == 'chrome') {
result = {
...result,
...manifestChrome,
};
} else if (TARGET == 'firefox') {
result = {
...result,
...manifestFirefox,
};
}
result.version = packageJSON.version;
if (IS_PRODUCTION === false) {
result.name = '__MSG_extension_name_development__';
}
return JSON.stringify(result, null, 2);
},
};
@@ -74,6 +92,8 @@ const manifestJsonOutputPlugin = () => {
let OUTPUT_PATH = `dist/${TARGET}`;
if (TARGET === 'safari') {
OUTPUT_PATH = '../apple/Shared (Extension)/Resources';
} else if (IS_PRODUCTION === true) {
OUTPUT_PATH = `dist/${TARGET}-production`;
}
export default [
@@ -97,6 +117,13 @@ export default [
src: 'assets/images',
dest: OUTPUT_PATH,
},
{
src: [
'src/content/preauth.html',
'src/content/preauth.js',
],
dest: OUTPUT_PATH,
},
],
}),
],

View File

View File

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

View File

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

View File

@@ -1,8 +1,23 @@
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 POST_AUTH_PATH = '/integrations/extension/post-authenticate/';
const RPC_PATH = '/rpc/';
let authSessionToken = null;
let rpcURL = null;
const updateRpcURL = () => {
rpcURL = null;
if (HotPocketExtension.base_url !== null) {
rpcURL = (new URL(RPC_PATH, HotPocketExtension.base_url)).toString();
}
HotPocketExtension.LOGGER.debug(
'HotPocketExtension.background.updateRpcURL()',
HotPocketExtension.base_url,
rpcURL,
);
};
const makeJSONRPCCall = (method, params) => {
return {
@@ -29,9 +44,12 @@ const executeJSONRPCCall = async (url, call, {accessToken}) => {
let result = null;
let error = null;
const effectiveURL = new URL(url);
effectiveURL.searchParams.append('method', call.method);
try {
const response = await fetch(
url,
effectiveURL.toString(),
{
body: JSON.stringify(call),
credentials: 'include',
@@ -40,7 +58,7 @@ const executeJSONRPCCall = async (url, call, {accessToken}) => {
},
);
HotPocketExtension.LOGGER.debug(
'HotPocketExtension.content.executeJSONRPCCall()', response,
'HotPocketExtension.background.executeJSONRPCCall()', response,
);
if (response.status !== 200) {
@@ -53,7 +71,7 @@ const executeJSONRPCCall = async (url, call, {accessToken}) => {
const callResult = await response.json();
if (callResult.error) {
HotPocketExtension.LOGGER.error(
'HotPocketExtension.content.executeJSONRPCCall(): RPC error',
'HotPocketExtension.background.executeJSONRPCCall(): RPC error',
callResult.error.code,
callResult.error.message,
callResult.error.data,
@@ -66,7 +84,7 @@ const executeJSONRPCCall = async (url, call, {accessToken}) => {
}
} catch (exception) {
HotPocketExtension.LOGGER.error(
'HotPocketExtension.content.executeJSONRPCCall(): Fetch error', exception,
'HotPocketExtension.background.executeJSONRPCCall(): Fetch error', exception,
);
error = {
code: -32000,
@@ -78,11 +96,18 @@ const executeJSONRPCCall = async (url, call, {accessToken}) => {
return [result, error];
};
const getAccessTokenMeta = () => {
return {
platform: navigator.platform,
version: HotPocketExtension.version,
};
};
const doSave = async (accessToken, tab) => {
const call = makeJSONRPCCall('saves.create', [tab.url]);
const [result, error] = await executeJSONRPCCall(RPC_URL, call, {accessToken});
const [result, error] = await executeJSONRPCCall(rpcURL, call, {accessToken});
HotPocketExtension.LOGGER.debug(
'HotPocketExtension.content.doSave():', result, error,
'HotPocketExtension.background.doSave():', result, error,
);
if (error !== null) {
@@ -97,15 +122,12 @@ const doCreateAndStoreAccessToken = async (authKey) => {
'accounts.access_tokens.create',
[
authKey,
{
platform: navigator.platform,
version: HotPocketExtension.version,
},
getAccessTokenMeta(),
],
);
const [accessToken, error] = await executeJSONRPCCall(
RPC_URL, accessTokenCall, {accessToken: null},
rpcURL, accessTokenCall, {accessToken: null},
);
if (error === null) {
@@ -118,15 +140,34 @@ const doCreateAndStoreAccessToken = async (authKey) => {
};
const doHandleAuthFlow = (authTab) => {
HotPocketExtension.LOGGER.debug(
'HotPocketExtension.background.doHandleAuthFlow()', authTab,
);
let currentAuthTabId = authTab.id;
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;
const changedURL = changeInfo.url;
if (changedURL && changedURL.startsWith(POST_AUTH_URL)) {
HotPocketExtension.LOGGER.debug(
'HotPocketExtension.background.doHandleAuthFlow.onTabsUpdated()',
updatedTab,
changedURL,
(changedURL && changedURL.includes(POST_AUTH_PATH)),
changeInfo,
);
const expectedSessionTabQuery = `?authSessionToken=${authSessionToken}`;
if (tabId !== currentAuthTabId && changedURL.includes(expectedSessionTabQuery)) {
// When redirecting from the preauth page to the HotPocket instance,
// Safari "replaces" the auth tab with a new one. This nasty hack will
// allow the extension to keep track of it.
// I hate computers.
currentAuthTabId = tabId;
}
if (tabId === currentAuthTabId) {
if (changedURL && changedURL.includes(POST_AUTH_PATH)) {
const parsedChangedURL = new URL(changedURL);
const authKey = parsedChangedURL.searchParams.get('auth_key');
@@ -147,8 +188,9 @@ const doHandleAuthFlow = (authTab) => {
reject(error);
}).
finally(() => {
authSessionToken = null;
HotPocketExtension.api.tabs.onUpdated.removeListener(onTabsUpdated);
HotPocketExtension.api.tabs.remove(authTab.id);
HotPocketExtension.api.tabs.remove(currentAuthTabId);
});
}
}
@@ -163,9 +205,12 @@ const doCheckAuth = async (accessToken) => {
return null;
}
const call = makeJSONRPCCall('accounts.auth.check');
const call = makeJSONRPCCall(
'accounts.auth.check_access_token',
[accessToken, getAccessTokenMeta()],
);
const [result, error] = await executeJSONRPCCall(RPC_URL, call, {
const [result, error] = await executeJSONRPCCall(rpcURL, call, {
accessToken,
});
@@ -186,15 +231,28 @@ const doCheckAuth = async (accessToken) => {
return accessToken;
};
const doGetAccessToken = async () => {
let storageResult = await HotPocketExtension.api.storage.local.get('accessToken');
let accessToken = await doCheckAuth(
storageResult.accessToken || null,
const doSetupRPC = async () => {
let storageResult = await HotPocketExtension.api.storage.local.get(
['accessToken', 'baseURL'],
);
let accessToken = null;
if (storageResult.baseURL) {
HotPocketExtension.base_url = storageResult.baseURL;
updateRpcURL();
accessToken = await doCheckAuth(
storageResult.accessToken || null,
);
}
if (accessToken === null) {
authSessionToken = crypto.randomUUID();
const authTab = await HotPocketExtension.api.tabs.create({
url: AUTH_URL,
url: HotPocketExtension.api.runtime.getURL(
`preauth.html?authSessionToken=${authSessionToken}`,
),
});
accessToken = await doHandleAuthFlow(authTab);
@@ -207,24 +265,53 @@ const doSendTabMessage = (tab, message) => {
HotPocketExtension.api.tabs.sendMessage(tab.id, message).
then((result) => {
HotPocketExtension.LOGGER.debug(
'HotPocketExtension.content.doSendTabMessage(): message sent', message, result,
'HotPocketExtension.background.doSendTabMessage(): message sent',
message,
result,
);
}).
catch((error) => {
HotPocketExtension.LOGGER.error(
'HotPocketExtension.content.doSendTabMessage(): could not send message', error,
'HotPocketExtension.background.doSendTabMessage(): could not send message',
error,
);
});
};
const doUpdateBaseURL = (nextBaseURL) => {
HotPocketExtension.base_url = nextBaseURL;
updateRpcURL();
HotPocketExtension.api.storage.local.
set({
baseURL: nextBaseURL,
}).
then(() => {
HotPocketExtension.LOGGER.debug(
'HotPocketExtension.background.doUpdateBaseURL()', 'Base URL saved',
);
}).
catch((error) => {
HotPocketExtension.LOGGER.error(
'HotPocketExtension.background.doUpdateBaseURL()', error,
);
});
};
const onTabCreated = (tab) => {
HotPocketExtension.LOGGER.debug('HotPocketExtension.onTabCreated()', tab);
HotPocketExtension.api.action.enable(tab.id);
HotPocketExtension.LOGGER.debug(
'HotPocketExtension.background.onTabCreated()', tab,
);
HotPocketExtension.api.action.enable(tab.id).catch((error) => {
HotPocketExtension.LOGGER.error(
'HotPocketExtension.background.onTabCreated()', tab.id, error,
);
});
};
const onBrowserActionClicked = async (tab) => {
HotPocketExtension.LOGGER.debug(
'HotPocketExtension.onBrowserActionClicked()', tab.url,
'HotPocketExtension.background.onBrowserActionClicked()', tab.url,
);
if (!tab.url) {
@@ -235,11 +322,11 @@ const onBrowserActionClicked = async (tab) => {
let error = null;
try {
let accessToken = await doGetAccessToken();
let accessToken = await doSetupRPC();
result = await doSave(accessToken, tab);
HotPocketExtension.LOGGER.debug(
'HotPocketExtension.onBrowserActionClicked()', result,
'HotPocketExtension.background.onBrowserActionClicked()', result,
);
} catch (exception) {
HotPocketExtension.LOGGER.error(
@@ -266,10 +353,11 @@ const 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',
});
} else if (message.type === 'HotPocket:Extension:setBaseURL') {
doUpdateBaseURL(message.result);
}
} catch (exception) {
HotPocketExtension.LOGGER.error(
@@ -290,6 +378,8 @@ export default ({...configuration}) => {
background: true,
});
updateRpcURL();
HotPocketExtension.api.tabs.onCreated.addListener(onTabCreated);
HotPocketExtension.api.action.onClicked.addListener(onBrowserActionClicked);

View File

@@ -8,7 +8,7 @@ const HotPocketExtension = {
version: __HOTPOCKET_EXTENSION_VERSION__,
debug: DEBUG,
api: null,
base_url: __HOTPOCKET_EXTENSION_BASE_URL__,
base_url: null,
LOGGER: {
// eslint-disable-next-line no-console
debug: (DEBUG === true) ? console.log : noop,

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,45 @@
/*!
* 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.
*/
(() => {
'use strict';
document.addEventListener('DOMContentLoaded', (event) => {
const form = document.getElementById('PreauthForm');
form.addEventListener('submit', (event) => {
event.stopPropagation();
event.preventDefault();
const inputBaseURL = document.getElementById('id_base_url');
const baseURL = inputBaseURL.value;
let api = window.browser || null;
if (api === null && window.chrome) {
api = window.chrome;
}
api.runtime.sendMessage({
type: 'HotPocket:Extension:setBaseURL',
result: baseURL,
});
const loginURL = new URL('/integrations/extension/authenticate/', inputBaseURL.value);
window.location.replace(loginURL.toString());
return false;
});
});
})();

View File

@@ -0,0 +1,13 @@
{
"action": {
"default_title": "__MSG_extension_name__",
"default_icon": {
"16": "images/toolbar-icon-16.png",
"32": "images/toolbar-icon-32.png"
}
},
"background": {
"service_worker": "background-bundle.js",
"type": "module"
}
}

View File

@@ -3,8 +3,10 @@
"default_locale": "en",
"name": "__MSG_extension_name__",
"description": "__MSG_extension_description__",
"version": "25.9.8",
"version": "25.9.17",
"icons": {
"16": "images/icon-16.png",
"32": "images/icon-32.png",
"48": "images/icon-48.png",
"64": "images/icon-64.png",
"96": "images/icon-96.png",
@@ -22,10 +24,6 @@
]
}
],
"action": {
"default_title": "__MSG_extension_name__",
"default_icon": "images/toolbar-icon.svg"
},
"permissions": [
"storage",
"activeTab",

View File

@@ -0,0 +1,27 @@
{
"action": {
"default_title": "__MSG_extension_name__",
"default_icon": {
"16": "images/toolbar-icon-16.png",
"32": "images/toolbar-icon-32.png"
}
},
"background": {
"scripts": [
"background-bundle.js"
],
"type": "module"
},
"browser_specific_settings": {
"gecko": {
"id": "@Extension.HotPocket.BTHLabs",
"strict_min_version": "142.0",
"data_collection_permissions": {
"required": [
"websiteActivity",
"browsingActivity"
]
}
}
}
}

View File

@@ -1,9 +1,14 @@
{
"description": "__MSG_extension_description_Safari__",
"action": {
"default_title": "__MSG_extension_name__",
"default_icon": "images/toolbar-icon.svg"
},
"background": {
"scripts": [
"background-bundle.js"
],
"type": "module"
"type": "module",
"persistent": false
}
}

View File

@@ -3,27 +3,30 @@
from __future__ import annotations
from invoke import task
import json
from invoke import Context, task
from invoke.exceptions import UnexpectedExit
from hotpocket_workspace_tools import get_workspace_mode
from hotpocket_workspace_tools.tasks import * # noqa: F401,F403
WORKSPACE_MODE = get_workspace_mode()
@task
def clean(ctx):
def clean(ctx: Context):
ctx.run('rm -rf dist/')
@task
def test(ctx):
def test(ctx: Context):
# ctx.run('pytest -v --disable-warnings')
print('NOOP')
@task
def flake8(ctx):
def flake8(ctx: Context):
ctx.run('flake8')
@@ -45,12 +48,12 @@ def isort(ctx, check=False, diff=False):
@task
def eslint(ctx):
def eslint(ctx: Context):
ctx.run('yarn run eslint')
@task
def lint(ctx):
def lint(ctx: Context):
ihazsuccess = True
try:
@@ -73,17 +76,17 @@ def lint(ctx):
@task
def typecheck(ctx):
def typecheck(ctx: Context):
ctx.run('mypy .')
@task
def django_shell(ctx):
def django_shell(ctx: Context):
raise NotImplementedError()
@task
def ci(ctx):
def ci(ctx: Context):
ihazsuccess = True
ci_tasks = [test, lint, typecheck]
@@ -98,15 +101,58 @@ def ci(ctx):
@task
def setup(ctx):
def setup(ctx: Context):
print('NOOP')
@task
def start_web(ctx):
def start_web(ctx: Context):
raise NotImplementedError()
@task
def start_safari(ctx):
def start_safari(ctx: Context):
ctx.run('yarn watch:safari')
@task(pre=[clean])
def start_chrome(ctx: Context):
ctx.run('yarn watch:chrome')
@task(pre=[clean])
def start_firefox(ctx: Context):
ctx.run('yarn watch:firefox')
@task
def build_safari(ctx: Context):
ctx.run('yarn build:safari')
@task(pre=[clean])
def build_chrome(ctx: Context):
ctx.run('yarn build:chrome')
ctx.run(' '.join([
r'/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome',
'--pack-extension=dist/chrome-production/',
'--pack-extension-key=secrets/chrome.pem',
]))
@task(pre=[clean])
def build_firefox(ctx: Context):
ctx.run('yarn build:firefox')
firefox_secrets = None
with open('secrets/firefox.json', 'r', encoding='utf-8') as firefox_secrets_f:
firefox_secrets = json.load(firefox_secrets_f)
with ctx.cd('dist/firefox-production'):
ctx.run(' '.join([
'web-ext',
'sign',
'--channel=unlisted',
f'--api-key={firefox_secrets["api_key"]}',
f'--api-secret={firefox_secrets["api_secret"]}',
]))

View File

@@ -7,3 +7,5 @@ import enum
class AccessTokenOriginApp(enum.Enum):
UNKNOWN = 'UNKNOWN'
SAFARI_WEB_EXTENSION = 'SAFARI_WEB_EXTENSION'
CHROME_EXTENSION = 'CHROME_EXTENSION'
FIREFOX_EXTENSION = 'FIREFOX_EXTENSION'

View File

@@ -230,6 +230,9 @@ python-versions = "^3.12"
files = []
develop = true
[package.dependencies]
invoke = "2.2.0"
[package.source]
type = "directory"
url = "workspace_tools"

View File

@@ -26,6 +26,12 @@ class AccessTokenOut(ModelOut):
case 'safari-web-extension':
return AccessTokenOriginApp.SAFARI_WEB_EXTENSION
case 'chrome-extension':
return AccessTokenOriginApp.CHROME_EXTENSION
case 'moz-extension':
return AccessTokenOriginApp.FIREFOX_EXTENSION
case _:
return None
@@ -36,3 +42,8 @@ class AccessTokenOut(ModelOut):
class AccessTokensQuery(Query):
account_uuid: uuid.UUID
before: uuid.UUID | None = pydantic.Field(default=None)
class AccessTokenMetaUpdateIn(pydantic.BaseModel):
version: str | None = None
platform: str | None = None

View File

@@ -6,7 +6,11 @@ import uuid
from hotpocket_backend.apps.accounts.services import (
AccessTokensService as BackendAccessTokensService,
)
from hotpocket_soa.dto.accounts import AccessTokenOut, AccessTokensQuery
from hotpocket_soa.dto.accounts import (
AccessTokenMetaUpdateIn,
AccessTokenOut,
AccessTokensQuery,
)
from .base import ProxyService, SOAError
@@ -76,6 +80,33 @@ class AccessTokensService(ProxyService):
else:
raise
def get_by_key(self,
*,
account_uuid: uuid.UUID,
key: str,
) -> AccessTokenOut:
try:
result = AccessTokenOut.model_validate(
self.call(
self.backend_access_tokens_service,
'get_by_key',
key=key,
),
from_attributes=True,
)
if result.account_uuid != account_uuid:
raise self.AccessTokenAccessDenied(
f'account_uuid=`{account_uuid}` key=`{key}`',
)
return result
except SOAError as exception:
if isinstance(exception.__cause__, BackendAccessTokensService.AccessTokenNotFound) is True:
raise self.AccessTokenNotFound(f'account_uuid=`{account_uuid}` pk=`{key}`') from exception
else:
raise
def search(self,
*,
query: AccessTokensQuery,
@@ -98,3 +129,18 @@ class AccessTokensService(ProxyService):
'delete',
pk=access_token.pk,
)
def update_meta(self,
*,
access_token: AccessTokenOut,
update: AccessTokenMetaUpdateIn,
) -> AccessTokenOut:
return AccessTokenOut.model_validate(
self.call(
self.backend_access_tokens_service,
'update_meta',
pk=access_token.pk,
update=update,
),
from_attributes=True,
)

View File

@@ -0,0 +1 @@
from .utils import bump_version # noqa: F401

View File

@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
from invoke import Context, task
@task
def bump_version(ctx: Context, next_version: str, build: str | None = None):
current_version_result = ctx.run('poetry version -s --no-ansi', hide='out')
assert current_version_result is not None, 'Hm?'
current_version = current_version_result.stdout.strip()
print(f'Bumping version: `{current_version}` -> `{next_version}`')
for file_to_version in ctx.config.get('files_to_version', []):
with open(file_to_version, 'r', encoding='utf-8') as content_f:
content = content_f.read()
content = content.replace(current_version, next_version)
with open(file_to_version, 'w', encoding='utf-8') as content_f:
content_f.write(content)

View File

@@ -8,6 +8,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = "^3.12"
invoke = "2.2.0"
[build-system]
requires = ["poetry-core"]

View File

@@ -1,11 +1,15 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
import base64
import datetime
from invoke import task
from invoke import Context, task
from hotpocket_workspace_tools import get_workspace_mode, WorkspaceMode
from hotpocket_workspace_tools.tasks import (
bump_version as tools_bump_version_task,
)
class DockerBuildContext:
@@ -60,10 +64,11 @@ class DockerBuildContext:
WORKSPACE_MODE = get_workspace_mode()
ALL_SERVICES = ['backend', 'packages', 'extension']
ALL_SERVICES = ['apple', 'backend', 'extension', 'packages']
VERSIONED_SERVICES = ['apple', 'backend', 'extension']
def _run_in_service(ctx, service, command, **kwargs):
def _run_in_service(ctx: Context, service, command, **kwargs):
match WORKSPACE_MODE:
case WorkspaceMode.DOCKER:
return ctx.run(
@@ -75,7 +80,7 @@ def _run_in_service(ctx, service, command, **kwargs):
ctx.run(f'direnv exec . {command}')
def _get_version(ctx, service):
def _get_version(ctx: Context, service):
with ctx.cd(f'services/{service}'):
run_result = ctx.run('poetry version -s', hide='out')
@@ -88,27 +93,27 @@ def _get_head_sha(ctx):
@task
def clean(ctx, service):
def clean(ctx: Context, service):
_run_in_service(ctx, service, 'inv clean')
@task
def test(ctx, service):
def test(ctx: Context, service):
_run_in_service(ctx, service, 'inv test')
@task
def lint(ctx, service):
def lint(ctx: Context, service):
_run_in_service(ctx, service, 'inv lint')
@task
def typecheck(ctx, service):
def typecheck(ctx: Context, service):
_run_in_service(ctx, service, 'inv typecheck')
@task
def shell(ctx, service):
def shell(ctx: Context, service):
assert WORKSPACE_MODE == WorkspaceMode.DOCKER, (
'Just `cd services/{service}` ;)'
)
@@ -116,12 +121,21 @@ def shell(ctx, service):
@task
def django_shell(ctx, service):
def django_shell(ctx: Context, service):
_run_in_service(ctx, service, 'inv django-shell')
@task
def build(ctx,
def png_to_data_url(ctx: Context, png_path):
with open(png_path, 'rb') as png_f:
data = png_f.read()
encoded_data = base64.b64encode(data)
print(f'data:image/png;base64,{encoded_data.decode("utf-8")}')
@task
def build(ctx: Context,
service,
context=None,
builder=None,
@@ -175,7 +189,7 @@ def build(ctx,
@task
def publish(ctx,
def publish(ctx: Context,
service,
context=None,
target='deployment',
@@ -199,12 +213,12 @@ def publish(ctx,
@task
def ci(ctx, service):
def ci(ctx: Context, service):
_run_in_service(ctx, service, 'inv ci')
@task
def setup(ctx, service=None):
def setup(ctx: Context, service=None):
services_to_setup = []
services_to_setup = [*ALL_SERVICES]
@@ -216,7 +230,7 @@ def setup(ctx, service=None):
@task
def install(ctx, service=None):
def install(ctx: Context, service=None):
services_to_setup = []
services_to_setup = [*ALL_SERVICES]
@@ -228,7 +242,7 @@ def install(ctx, service=None):
@task
def lock(ctx, service):
def lock(ctx: Context, service):
_run_in_service(ctx, service, 'poetry lock --no-update')
@@ -238,20 +252,34 @@ def start_cloud(ctx):
@task
def start_web(ctx, service):
def start_web(ctx: Context, service):
_run_in_service(ctx, service, 'inv start-web')
@task
def start_celery_worker(ctx, service):
def start_celery_worker(ctx: Context, service):
_run_in_service(ctx, service, 'inv start-worker')
@task
def start_celery_beat(ctx, service):
def start_celery_beat(ctx: Context, service):
_run_in_service(ctx, service, 'inv start-beat')
@task
def start_app(ctx, service, app):
def start_app(ctx: Context, service, app):
_run_in_service(ctx, service, f'inv start-{app}')
@task
def bump_version(ctx: Context, next_version: str, build: str | None = None):
assert build is not None, '`--build` is required here'
tools_bump_version_task(ctx, next_version, build=build)
for service_to_setup in VERSIONED_SERVICES:
_run_in_service(
ctx,
service_to_setup,
f'inv bump-version {next_version} --build {build}',
)