diff --git a/.github/workflows/containers.yml b/.github/workflows/containers.yml
index 0397c026..8c0145d7 100644
--- a/.github/workflows/containers.yml
+++ b/.github/workflows/containers.yml
@@ -88,14 +88,14 @@ jobs:
- name: Build and push :dev
id: docker_build
if: ${{ github.ref }} == "refs/heads/master"
- uses: docker/build-push-action@v5
+ uses: docker/build-push-action@v6
with:
context: ./
file: ./Dockerfile
push: true
tags: |
${{ secrets.DOCKER_HUB_USERNAME }}/changedetection.io:dev,ghcr.io/${{ github.repository }}:dev
- platforms: linux/amd64,linux/arm64,linux/arm/v6,linux/arm/v7,linux/arm/v8
+ platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8,linux/arm64/v8
cache-from: type=gha
cache-to: type=gha,mode=max
@@ -106,7 +106,7 @@ jobs:
- name: Build and push :tag
id: docker_build_tag_release
if: github.event_name == 'release' && startsWith(github.event.release.tag_name, '0.')
- uses: docker/build-push-action@v5
+ uses: docker/build-push-action@v6
with:
context: ./
file: ./Dockerfile
@@ -116,7 +116,7 @@ jobs:
ghcr.io/dgtlmoon/changedetection.io:${{ github.event.release.tag_name }}
${{ secrets.DOCKER_HUB_USERNAME }}/changedetection.io:latest
ghcr.io/dgtlmoon/changedetection.io:latest
- platforms: linux/amd64,linux/arm64,linux/arm/v6,linux/arm/v7,linux/arm/v8
+ platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8,linux/arm64/v8
cache-from: type=gha
cache-to: type=gha,mode=max
# Looks like this was disabled
diff --git a/.github/workflows/test-container-build.yml b/.github/workflows/test-container-build.yml
index c6fd9efb..1e5257bc 100644
--- a/.github/workflows/test-container-build.yml
+++ b/.github/workflows/test-container-build.yml
@@ -51,7 +51,7 @@ jobs:
# Check we can still build under alpine/musl
- name: Test that the docker containers can build (musl via alpine check)
id: docker_build_musl
- uses: docker/build-push-action@v5
+ uses: docker/build-push-action@v6
with:
context: ./
file: ./.github/test/Dockerfile-alpine
@@ -59,12 +59,12 @@ jobs:
- name: Test that the docker containers can build
id: docker_build
- uses: docker/build-push-action@v5
+ uses: docker/build-push-action@v6
# https://github.com/docker/build-push-action#customizing
with:
context: ./
file: ./Dockerfile
- platforms: linux/amd64,linux/arm64,linux/arm/v6,linux/arm/v7,linux/arm/v8
+ platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8,linux/arm64/v8
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache
diff --git a/.github/workflows/test-only.yml b/.github/workflows/test-only.yml
index 5483e6f6..69e42cba 100644
--- a/.github/workflows/test-only.yml
+++ b/.github/workflows/test-only.yml
@@ -4,17 +4,10 @@ name: ChangeDetection.io App Test
on: [push, pull_request]
jobs:
- test-application:
+ lint-code:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
-
- # Mainly just for link/flake8
- - name: Set up Python 3.11
- uses: actions/setup-python@v5
- with:
- python-version: '3.11'
-
- name: Lint with flake8
run: |
pip3 install flake8
@@ -23,202 +16,24 @@ jobs:
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- - name: Spin up ancillary testable services
- run: |
-
- docker network create changedet-network
-
- # Selenium
- docker run --network changedet-network -d --hostname selenium -p 4444:4444 --rm --shm-size="2g" selenium/standalone-chrome:4
-
- # SocketPuppetBrowser + Extra for custom browser test
- docker run --network changedet-network -d -e "LOG_LEVEL=TRACE" --cap-add=SYS_ADMIN --name sockpuppetbrowser --hostname sockpuppetbrowser --rm -p 3000:3000 dgtlmoon/sockpuppetbrowser:latest
- docker run --network changedet-network -d -e "LOG_LEVEL=TRACE" --cap-add=SYS_ADMIN --name sockpuppetbrowser-custom-url --hostname sockpuppetbrowser-custom-url -p 3001:3000 --rm dgtlmoon/sockpuppetbrowser:latest
-
- - name: Build changedetection.io container for testing
- run: |
- # Build a changedetection.io container and start testing inside
- docker build --build-arg LOGGER_LEVEL=TRACE -t test-changedetectionio .
- # Debug info
- docker run test-changedetectionio bash -c 'pip list'
-
- - name: Spin up ancillary SMTP+Echo message test server
- run: |
- # Debug SMTP server/echo message back server
- docker run --network changedet-network -d -p 11025:11025 -p 11080:11080 --hostname mailserver test-changedetectionio bash -c 'python changedetectionio/tests/smtp/smtp-test-server.py'
-
- - name: Show docker container state and other debug info
- run: |
- set -x
- echo "Running processes in docker..."
- docker ps
-
- - name: Test built container with Pytest (generally as requests/plaintext fetching)
- run: |
- # Unit tests
- echo "run test with unittest"
- docker run test-changedetectionio bash -c 'python3 -m unittest changedetectionio.tests.unit.test_notification_diff'
- docker run test-changedetectionio bash -c 'python3 -m unittest changedetectionio.tests.unit.test_watch_model'
- docker run test-changedetectionio bash -c 'python3 -m unittest changedetectionio.tests.unit.test_jinja2_security'
-
- # All tests
- echo "run test with pytest"
- # The default pytest logger_level is TRACE
- # To change logger_level for pytest(test/conftest.py),
- # append the docker option. e.g. '-e LOGGER_LEVEL=DEBUG'
- docker run --name test-cdio-basic-tests --network changedet-network test-changedetectionio bash -c 'cd changedetectionio && ./run_basic_tests.sh'
-
-# PLAYWRIGHT/NODE-> CDP
- - name: Playwright and SocketPuppetBrowser - Specific tests in built container
- run: |
- # Playwright via Sockpuppetbrowser fetch
- # tests/visualselector/test_fetch_data.py will do browser steps
- docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/fetchers/test_content.py'
- docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/test_errorhandling.py'
- docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/visualselector/test_fetch_data.py'
- docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/fetchers/test_custom_js_before_content.py'
-
-
- - name: Playwright and SocketPuppetBrowser - Headers and requests
- run: |
- # Settings headers playwright tests - Call back in from Sockpuppetbrowser, check headers
- docker run --name "changedet" --hostname changedet --rm -e "FLASK_SERVER_NAME=changedet" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000?dumpio=true" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio; pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/test_request.py'
-
- - name: Playwright and SocketPuppetBrowser - Restock detection
- run: |
- # restock detection via playwright - added name=changedet here so that playwright and sockpuppetbrowser can connect to it
- docker run --rm --name "changedet" -e "FLASK_SERVER_NAME=changedet" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-port=5004 --live-server-host=0.0.0.0 tests/restock/test_restock.py'
-
-# STRAIGHT TO CDP
- - name: Pyppeteer and SocketPuppetBrowser - Specific tests in built container
- run: |
- # Playwright via Sockpuppetbrowser fetch
- docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "FAST_PUPPETEER_CHROME_FETCHER=True" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/fetchers/test_content.py'
- docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "FAST_PUPPETEER_CHROME_FETCHER=True" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/test_errorhandling.py'
- docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "FAST_PUPPETEER_CHROME_FETCHER=True" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/visualselector/test_fetch_data.py'
- docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "FAST_PUPPETEER_CHROME_FETCHER=True" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/fetchers/test_custom_js_before_content.py'
-
- - name: Pyppeteer and SocketPuppetBrowser - Headers and requests checks
- run: |
- # Settings headers playwright tests - Call back in from Sockpuppetbrowser, check headers
- docker run --name "changedet" --hostname changedet --rm -e "FAST_PUPPETEER_CHROME_FETCHER=True" -e "FLASK_SERVER_NAME=changedet" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000?dumpio=true" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio; pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/test_request.py'
+ test-application-3-10:
+ needs: lint-code
+ uses: ./.github/workflows/test-stack-reusable-workflow.yml
+ with:
+ python-version: '3.10'
- - name: Pyppeteer and SocketPuppetBrowser - Restock detection
- run: |
- # restock detection via playwright - added name=changedet here so that playwright and sockpuppetbrowser can connect to it
- docker run --rm --name "changedet" -e "FLASK_SERVER_NAME=changedet" -e "FAST_PUPPETEER_CHROME_FETCHER=True" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-port=5004 --live-server-host=0.0.0.0 tests/restock/test_restock.py'
-# SELENIUM
- - name: Specific tests in built container for Selenium
- run: |
- # Selenium fetch
- docker run --rm -e "WEBDRIVER_URL=http://selenium:4444/wd/hub" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio;pytest tests/fetchers/test_content.py && pytest tests/test_errorhandling.py'
-
- - name: Specific tests in built container for headers and requests checks with Selenium
- run: |
- docker run --name "changedet" --hostname changedet --rm -e "FLASK_SERVER_NAME=changedet" -e "WEBDRIVER_URL=http://selenium:4444/wd/hub" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio; pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/test_request.py'
-
-# OTHER STUFF
- - name: Test SMTP notification mime types
- run: |
- # SMTP content types - needs the 'Debug SMTP server/echo message back server' container from above
- docker run --rm --network changedet-network test-changedetectionio bash -c 'cd changedetectionio;pytest tests/smtp/test_notification_smtp.py'
-
- # @todo Add a test via playwright/puppeteer
- # squid with auth is tested in run_proxy_tests.sh -> tests/proxy_list/test_select_custom_proxy.py
- - name: Test proxy squid style interaction
- run: |
- cd changedetectionio
- ./run_proxy_tests.sh
- cd ..
-
- - name: Test proxy SOCKS5 style interaction
- run: |
- cd changedetectionio
- ./run_socks_proxy_tests.sh
- cd ..
+ test-application-3-11:
+ needs: lint-code
+ uses: ./.github/workflows/test-stack-reusable-workflow.yml
+ with:
+ python-version: '3.11'
+ skip-pypuppeteer: true
- - name: Test custom browser URL
- run: |
- cd changedetectionio
- ./run_custom_browser_url_tests.sh
- cd ..
-
- - name: Test changedetection.io container starts+runs basically without error
- run: |
- docker run --name test-changedetectionio -p 5556:5000 -d test-changedetectionio
- sleep 3
- # Should return 0 (no error) when grep finds it
- curl --retry-connrefused --retry 6 -s http://localhost:5556 |grep -q checkbox-uuid
-
- # and IPv6
- curl --retry-connrefused --retry 6 -s -g -6 "http://[::1]:5556"|grep -q checkbox-uuid
-
- # Check whether TRACE log is enabled.
- # Also, check whether TRACE is came from STDERR
- docker logs test-changedetectionio 2>&1 1>/dev/null | grep 'TRACE log is enabled' || exit 1
- # Check whether DEBUG is came from STDOUT
- docker logs test-changedetectionio 2>/dev/null | grep 'DEBUG' || exit 1
-
- docker kill test-changedetectionio
-
- - name: Test changedetection.io SIGTERM and SIGINT signal shutdown
- run: |
-
- echo SIGINT Shutdown request test
- docker run --name sig-test -d test-changedetectionio
- sleep 3
- echo ">>> Sending SIGINT to sig-test container"
- docker kill --signal=SIGINT sig-test
- sleep 3
- # invert the check (it should be not 0/not running)
- docker ps
- # check signal catch(STDERR) log. Because of
- # changedetectionio/__init__.py: logger.add(sys.stderr, level=logger_level)
- docker logs sig-test 2>&1 | grep 'Shutdown: Got Signal - SIGINT' || exit 1
- test -z "`docker ps|grep sig-test`"
- if [ $? -ne 0 ]
- then
- echo "Looks like container was running when it shouldnt be"
- docker ps
- exit 1
- fi
-
- # @todo - scan the container log to see the right "graceful shutdown" text exists
- docker rm sig-test
-
- echo SIGTERM Shutdown request test
- docker run --name sig-test -d test-changedetectionio
- sleep 3
- echo ">>> Sending SIGTERM to sig-test container"
- docker kill --signal=SIGTERM sig-test
- sleep 3
- # invert the check (it should be not 0/not running)
- docker ps
- # check signal catch(STDERR) log. Because of
- # changedetectionio/__init__.py: logger.add(sys.stderr, level=logger_level)
- docker logs sig-test 2>&1 | grep 'Shutdown: Got Signal - SIGTERM' || exit 1
- test -z "`docker ps|grep sig-test`"
- if [ $? -ne 0 ]
- then
- echo "Looks like container was running when it shouldnt be"
- docker ps
- exit 1
- fi
-
- # @todo - scan the container log to see the right "graceful shutdown" text exists
- docker rm sig-test
-
- - name: Dump container log
- if: always()
- run: |
- mkdir output-logs
- docker logs test-cdio-basic-tests > output-logs/test-cdio-basic-tests-stdout.txt
- docker logs test-cdio-basic-tests 2> output-logs/test-cdio-basic-tests-stderr.txt
+ test-application-3-12:
+ needs: lint-code
+ uses: ./.github/workflows/test-stack-reusable-workflow.yml
+ with:
+ python-version: '3.12'
+ skip-pypuppeteer: true
- - name: Store container log
- if: always()
- uses: actions/upload-artifact@v4
- with:
- name: test-cdio-basic-tests-output
- path: output-logs
diff --git a/.github/workflows/test-stack-reusable-workflow.yml b/.github/workflows/test-stack-reusable-workflow.yml
new file mode 100644
index 00000000..f2864680
--- /dev/null
+++ b/.github/workflows/test-stack-reusable-workflow.yml
@@ -0,0 +1,239 @@
+name: ChangeDetection.io App Test
+
+on:
+ workflow_call:
+ inputs:
+ python-version:
+ description: 'Python version to use'
+ required: true
+ type: string
+ default: '3.10'
+ skip-pypuppeteer:
+ description: 'Skip PyPuppeteer (not supported in 3.11/3.12)'
+ required: false
+ type: boolean
+ default: false
+
+jobs:
+ test-application:
+ runs-on: ubuntu-latest
+ env:
+ PYTHON_VERSION: ${{ inputs.python-version }}
+ steps:
+ - uses: actions/checkout@v4
+
+ # Mainly just for link/flake8
+ - name: Set up Python ${{ env.PYTHON_VERSION }}
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ env.PYTHON_VERSION }}
+
+ - name: Build changedetection.io container for testing under Python ${{ env.PYTHON_VERSION }}
+ run: |
+ echo "---- Building for Python ${{ env.PYTHON_VERSION }} -----"
+ # Build a changedetection.io container and start testing inside
+ docker build --build-arg PYTHON_VERSION=${{ env.PYTHON_VERSION }} --build-arg LOGGER_LEVEL=TRACE -t test-changedetectionio .
+ # Debug info
+ docker run test-changedetectionio bash -c 'pip list'
+
+ - name: We should be Python ${{ env.PYTHON_VERSION }} ...
+ run: |
+ docker run test-changedetectionio bash -c 'python3 --version'
+
+ - name: Spin up ancillary testable services
+ run: |
+
+ docker network create changedet-network
+
+ # Selenium
+ docker run --network changedet-network -d --hostname selenium -p 4444:4444 --rm --shm-size="2g" selenium/standalone-chrome:4
+
+ # SocketPuppetBrowser + Extra for custom browser test
+ docker run --network changedet-network -d -e "LOG_LEVEL=TRACE" --cap-add=SYS_ADMIN --name sockpuppetbrowser --hostname sockpuppetbrowser --rm -p 3000:3000 dgtlmoon/sockpuppetbrowser:latest
+ docker run --network changedet-network -d -e "LOG_LEVEL=TRACE" --cap-add=SYS_ADMIN --name sockpuppetbrowser-custom-url --hostname sockpuppetbrowser-custom-url -p 3001:3000 --rm dgtlmoon/sockpuppetbrowser:latest
+
+ - name: Spin up ancillary SMTP+Echo message test server
+ run: |
+ # Debug SMTP server/echo message back server
+ docker run --network changedet-network -d -p 11025:11025 -p 11080:11080 --hostname mailserver test-changedetectionio bash -c 'pip3 install aiosmtpd && python changedetectionio/tests/smtp/smtp-test-server.py'
+ docker ps
+
+ - name: Show docker container state and other debug info
+ run: |
+ set -x
+ echo "Running processes in docker..."
+ docker ps
+
+ - name: Test built container with Pytest (generally as requests/plaintext fetching)
+ run: |
+ # Unit tests
+ echo "run test with unittest"
+ docker run test-changedetectionio bash -c 'python3 -m unittest changedetectionio.tests.unit.test_notification_diff'
+ docker run test-changedetectionio bash -c 'python3 -m unittest changedetectionio.tests.unit.test_watch_model'
+ docker run test-changedetectionio bash -c 'python3 -m unittest changedetectionio.tests.unit.test_jinja2_security'
+
+ # All tests
+ echo "run test with pytest"
+ # The default pytest logger_level is TRACE
+ # To change logger_level for pytest(test/conftest.py),
+ # append the docker option. e.g. '-e LOGGER_LEVEL=DEBUG'
+ docker run --name test-cdio-basic-tests --network changedet-network test-changedetectionio bash -c 'cd changedetectionio && ./run_basic_tests.sh'
+
+# PLAYWRIGHT/NODE-> CDP
+ - name: Playwright and SocketPuppetBrowser - Specific tests in built container
+ run: |
+ # Playwright via Sockpuppetbrowser fetch
+ # tests/visualselector/test_fetch_data.py will do browser steps
+ docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/fetchers/test_content.py'
+ docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/test_errorhandling.py'
+ docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/visualselector/test_fetch_data.py'
+ docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/fetchers/test_custom_js_before_content.py'
+
+
+ - name: Playwright and SocketPuppetBrowser - Headers and requests
+ run: |
+ # Settings headers playwright tests - Call back in from Sockpuppetbrowser, check headers
+ docker run --name "changedet" --hostname changedet --rm -e "FLASK_SERVER_NAME=changedet" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000?dumpio=true" --network changedet-network test-changedetectionio bash -c 'find .; cd changedetectionio; pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/test_request.py; pwd;find .'
+
+ - name: Playwright and SocketPuppetBrowser - Restock detection
+ run: |
+ # restock detection via playwright - added name=changedet here so that playwright and sockpuppetbrowser can connect to it
+ docker run --rm --name "changedet" -e "FLASK_SERVER_NAME=changedet" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-port=5004 --live-server-host=0.0.0.0 tests/restock/test_restock.py'
+
+# STRAIGHT TO CDP
+ - name: Pyppeteer and SocketPuppetBrowser - Specific tests in built container
+ if: ${{ inputs.skip-pypuppeteer == false }}
+ run: |
+ # Playwright via Sockpuppetbrowser fetch
+ docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "FAST_PUPPETEER_CHROME_FETCHER=True" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/fetchers/test_content.py'
+ docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "FAST_PUPPETEER_CHROME_FETCHER=True" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/test_errorhandling.py'
+ docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "FAST_PUPPETEER_CHROME_FETCHER=True" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/visualselector/test_fetch_data.py'
+ docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "FAST_PUPPETEER_CHROME_FETCHER=True" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/fetchers/test_custom_js_before_content.py'
+
+ - name: Pyppeteer and SocketPuppetBrowser - Headers and requests checks
+ if: ${{ inputs.skip-pypuppeteer == false }}
+ run: |
+ # Settings headers playwright tests - Call back in from Sockpuppetbrowser, check headers
+ docker run --name "changedet" --hostname changedet --rm -e "FAST_PUPPETEER_CHROME_FETCHER=True" -e "FLASK_SERVER_NAME=changedet" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000?dumpio=true" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio; pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/test_request.py'
+
+ - name: Pyppeteer and SocketPuppetBrowser - Restock detection
+ if: ${{ inputs.skip-pypuppeteer == false }}
+ run: |
+ # restock detection via playwright - added name=changedet here so that playwright and sockpuppetbrowser can connect to it
+ docker run --rm --name "changedet" -e "FLASK_SERVER_NAME=changedet" -e "FAST_PUPPETEER_CHROME_FETCHER=True" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-port=5004 --live-server-host=0.0.0.0 tests/restock/test_restock.py'
+
+# SELENIUM
+ - name: Specific tests in built container for Selenium
+ run: |
+ # Selenium fetch
+ docker run --rm -e "WEBDRIVER_URL=http://selenium:4444/wd/hub" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio;pytest tests/fetchers/test_content.py && pytest tests/test_errorhandling.py'
+
+ - name: Specific tests in built container for headers and requests checks with Selenium
+ run: |
+ docker run --name "changedet" --hostname changedet --rm -e "FLASK_SERVER_NAME=changedet" -e "WEBDRIVER_URL=http://selenium:4444/wd/hub" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio; pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/test_request.py'
+
+# OTHER STUFF
+ - name: Test SMTP notification mime types
+ run: |
+ # SMTP content types - needs the 'Debug SMTP server/echo message back server' container from above
+ # "mailserver" hostname defined above
+ docker run --rm --network changedet-network test-changedetectionio bash -c 'cd changedetectionio;pytest tests/smtp/test_notification_smtp.py'
+
+ # @todo Add a test via playwright/puppeteer
+ # squid with auth is tested in run_proxy_tests.sh -> tests/proxy_list/test_select_custom_proxy.py
+ - name: Test proxy squid style interaction
+ run: |
+ cd changedetectionio
+ ./run_proxy_tests.sh
+ cd ..
+
+ - name: Test proxy SOCKS5 style interaction
+ run: |
+ cd changedetectionio
+ ./run_socks_proxy_tests.sh
+ cd ..
+
+ - name: Test custom browser URL
+ run: |
+ cd changedetectionio
+ ./run_custom_browser_url_tests.sh
+ cd ..
+
+ - name: Test changedetection.io container starts+runs basically without error
+ run: |
+ docker run --name test-changedetectionio -p 5556:5000 -d test-changedetectionio
+ sleep 3
+ # Should return 0 (no error) when grep finds it
+ curl --retry-connrefused --retry 6 -s http://localhost:5556 |grep -q checkbox-uuid
+
+ # and IPv6
+ curl --retry-connrefused --retry 6 -s -g -6 "http://[::1]:5556"|grep -q checkbox-uuid
+
+ # Check whether TRACE log is enabled.
+ # Also, check whether TRACE is came from STDERR
+ docker logs test-changedetectionio 2>&1 1>/dev/null | grep 'TRACE log is enabled' || exit 1
+ # Check whether DEBUG is came from STDOUT
+ docker logs test-changedetectionio 2>/dev/null | grep 'DEBUG' || exit 1
+
+ docker kill test-changedetectionio
+
+ - name: Test changedetection.io SIGTERM and SIGINT signal shutdown
+ run: |
+
+ echo SIGINT Shutdown request test
+ docker run --name sig-test -d test-changedetectionio
+ sleep 3
+ echo ">>> Sending SIGINT to sig-test container"
+ docker kill --signal=SIGINT sig-test
+ sleep 3
+ # invert the check (it should be not 0/not running)
+ docker ps
+ # check signal catch(STDERR) log. Because of
+ # changedetectionio/__init__.py: logger.add(sys.stderr, level=logger_level)
+ docker logs sig-test 2>&1 | grep 'Shutdown: Got Signal - SIGINT' || exit 1
+ test -z "`docker ps|grep sig-test`"
+ if [ $? -ne 0 ]
+ then
+ echo "Looks like container was running when it shouldnt be"
+ docker ps
+ exit 1
+ fi
+
+ # @todo - scan the container log to see the right "graceful shutdown" text exists
+ docker rm sig-test
+
+ echo SIGTERM Shutdown request test
+ docker run --name sig-test -d test-changedetectionio
+ sleep 3
+ echo ">>> Sending SIGTERM to sig-test container"
+ docker kill --signal=SIGTERM sig-test
+ sleep 3
+ # invert the check (it should be not 0/not running)
+ docker ps
+ # check signal catch(STDERR) log. Because of
+ # changedetectionio/__init__.py: logger.add(sys.stderr, level=logger_level)
+ docker logs sig-test 2>&1 | grep 'Shutdown: Got Signal - SIGTERM' || exit 1
+ test -z "`docker ps|grep sig-test`"
+ if [ $? -ne 0 ]
+ then
+ echo "Looks like container was running when it shouldnt be"
+ docker ps
+ exit 1
+ fi
+
+ # @todo - scan the container log to see the right "graceful shutdown" text exists
+ docker rm sig-test
+
+ - name: Dump container log
+ if: always()
+ run: |
+ mkdir output-logs
+ docker logs test-cdio-basic-tests > output-logs/test-cdio-basic-tests-stdout-${{ env.PYTHON_VERSION }}.txt
+ docker logs test-cdio-basic-tests 2> output-logs/test-cdio-basic-tests-stderr-${{ env.PYTHON_VERSION }}.txt
+
+ - name: Store everything including test-datastore
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: test-cdio-basic-tests-output-py${{ env.PYTHON_VERSION }}
+ path: .
diff --git a/COMMERCIAL_LICENCE.md b/COMMERCIAL_LICENCE.md
new file mode 100644
index 00000000..9ac72335
--- /dev/null
+++ b/COMMERCIAL_LICENCE.md
@@ -0,0 +1,54 @@
+# Generally
+
+In any commercial activity involving 'Hosting' (as defined herein), whether in part or in full, this license must be executed and adhered to.
+
+# Commercial License Agreement
+
+This Commercial License Agreement ("Agreement") is entered into by and between Mr Morresi (the original creator of this software) here-in ("Licensor") and (your company or personal name) _____________ ("Licensee"). This Agreement sets forth the terms and conditions under which Licensor provides its software ("Software") and services to Licensee for the purpose of reselling the software either in part or full, as part of any commercial activity where the activity involves a third party.
+
+### Definition of Hosting
+
+For the purposes of this Agreement, "hosting" means making the functionality of the Program or modified version available to third parties as a service. This includes, without limitation:
+- Enabling third parties to interact with the functionality of the Program or modified version remotely through a computer network.
+- Offering a service the value of which entirely or primarily derives from the value of the Program or modified version.
+- Offering a service that accomplishes for users the primary purpose of the Program or modified version.
+
+## 1. Grant of License
+Subject to the terms and conditions of this Agreement, Licensor grants Licensee a non-exclusive, non-transferable license to install, use, and resell the Software. Licensee may:
+- Resell the Software as part of a service offering or as a standalone product.
+- Host the Software on a server and provide it as a hosted service (e.g., Software as a Service - SaaS).
+- Integrate the Software into a larger product or service that is then sold or provided for commercial purposes, where the software is used either in part or full.
+
+## 2. License Fees
+Licensee agrees to pay Licensor the license fees specified in the ordering document. License fees are due and payable as specified in the ordering document. The fees may include initial licensing costs and recurring fees based on the number of end users, instances of the Software resold, or revenue generated from the resale activities.
+
+## 3. Resale Conditions
+Licensee must comply with the following conditions when reselling the Software, whether the software is resold in part or full:
+- Provide end users with access to the source code under the same open-source license conditions as provided by Licensor.
+- Clearly state in all marketing and sales materials that the Software is provided under a commercial license from Licensor, and provide a link back to https://changedetection.io.
+- Ensure end users are aware of and agree to the terms of the commercial license prior to resale.
+- Do not sublicense or transfer the Software to third parties except as part of an authorized resale activity.
+
+## 4. Hosting and Provision of Services
+Licensee may host the Software (either in part or full) on its servers and provide it as a hosted service to end users. The following conditions apply:
+- Licensee must ensure that all hosted versions of the Software comply with the terms of this Agreement.
+- Licensee must provide Licensor with regular reports detailing the number of end users and instances of the hosted service.
+- Any modifications to the Software made by Licensee for hosting purposes must be made available to end users under the same open-source license conditions, unless agreed otherwise.
+
+## 5. Services
+Licensor will provide support and maintenance services as described in the support policy referenced in the ordering document should such an agreement be signed by all parties. Additional fees may apply for support services provided to end users resold by Licensee.
+
+## 6. Reporting and Audits
+Licensee agrees to provide Licensor with regular reports detailing the number of instances, end users, and revenue generated from the resale of the Software. Licensor reserves the right to audit Licensee’s records to ensure compliance with this Agreement.
+
+## 7. Term and Termination
+This Agreement shall commence on the effective date and continue for the period set forth in the ordering document unless terminated earlier in accordance with this Agreement. Either party may terminate this Agreement if the other party breaches any material term and fails to cure such breach within thirty (30) days after receipt of written notice.
+
+## 8. Limitation of Liability and Disclaimer of Warranty
+Executing this commercial license does not waive the Limitation of Liability or Disclaimer of Warranty as stated in the open-source LICENSE provided with the Software. The Software is provided "as is," without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
+
+## 9. Governing Law
+This Agreement shall be governed by and construed in accordance with the laws of the Czech Republic.
+
+## Contact Information
+For commercial licensing inquiries, please contact contact@changedetection.io and dgtlmoon@gmail.com.
diff --git a/Dockerfile b/Dockerfile
index e592c9bb..6641b947 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -2,7 +2,10 @@
# @NOTE! I would love to move to 3.11 but it breaks the async handler in changedetectionio/content_fetchers/puppeteer.py
# If you know how to fix it, please do! and test it for both 3.10 and 3.11
-FROM python:3.10-slim-bookworm as builder
+
+ARG PYTHON_VERSION=3.11
+
+FROM python:${PYTHON_VERSION}-slim-bookworm AS builder
# See `cryptography` pin comment in requirements.txt
ARG CRYPTOGRAPHY_DONT_BUILD_RUST=1
@@ -23,7 +26,8 @@ WORKDIR /install
COPY requirements.txt /requirements.txt
-RUN pip install --target=/dependencies -r /requirements.txt
+# --extra-index-url https://www.piwheels.org/simple is for cryptography module to be prebuilt (or rustc etc needs to be installed)
+RUN pip install --extra-index-url https://www.piwheels.org/simple --target=/dependencies -r /requirements.txt
# Playwright is an alternative to Selenium
# Excluded this package from requirements.txt to prevent arm/v6 and arm/v7 builds from failing
@@ -32,10 +36,12 @@ RUN pip install --target=/dependencies playwright~=1.41.2 \
|| echo "WARN: Failed to install Playwright. The application can still run, but the Playwright option will be disabled."
# Final image stage
-FROM python:3.10-slim-bookworm
+FROM python:${PYTHON_VERSION}-slim-bookworm
RUN apt-get update && apt-get install -y --no-install-recommends \
libxslt1.1 \
+ # For presenting price amounts correctly in the restock/price detection overview
+ locales \
# For pdftohtml
poppler-utils \
zlib1g \
diff --git a/README.md b/README.md
index d941eccb..87451d24 100644
--- a/README.md
+++ b/README.md
@@ -41,6 +41,20 @@ Using the **Browser Steps** configuration, add basic steps before performing cha
After **Browser Steps** have been run, then visit the **Visual Selector** tab to refine the content you're interested in.
Requires Playwright to be enabled.
+### Awesome restock and price change notifications
+
+Enable the _"Re-stock & Price detection for single product pages"_ option to activate the best way to monitor product pricing, this will extract any meta-data in the HTML page and give you many options to follow the pricing of the product.
+
+Easily organise and monitor prices for products from the dashboard, get alerts and notifications when the price of a product changes or comes back in stock again!
+
+[](https://changedetection.io?src=github)
+
+Set price change notification parameters, upper and lower price, price change percentage and more.
+Always know when a product for sale drops in price.
+
+[](https://changedetection.io?src=github)
+
+
### Example use cases
@@ -272,6 +286,10 @@ I offer commercial support, this software is depended on by network security, ae
[release-link]: https://github.com/dgtlmoon/changedetection.io/releases
[docker-link]: https://hub.docker.com/r/dgtlmoon/changedetection.io
+## Commercial Licencing
+
+If you are reselling this software either in part or full as part of any commercial arrangement, you must abide by our COMMERCIAL_LICENCE.md found in our code repository, please contact dgtlmoon@gmail.com and contact@changedetection.io .
+
## Third-party licenses
changedetectionio.html_tools.elementpath_tostring: Copyright (c), 2018-2021, SISSA (Scuola Internazionale Superiore di Studi Avanzati), Licensed under [MIT license](https://github.com/sissaschool/elementpath/blob/master/LICENSE)
diff --git a/changedetection.py b/changedetection.py
index 2d0b5d2c..ead2b8c5 100755
--- a/changedetection.py
+++ b/changedetection.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python3
+#!/usr/bin/env python3
# Only exists for direct CLI usage
diff --git a/changedetectionio/__init__.py b/changedetectionio/__init__.py
index 6fb76ce4..5ec6f891 100644
--- a/changedetectionio/__init__.py
+++ b/changedetectionio/__init__.py
@@ -1,8 +1,8 @@
-#!/usr/bin/python3
+#!/usr/bin/env python3
# Read more https://github.com/dgtlmoon/changedetection.io/wiki
-__version__ = '0.45.23'
+__version__ = '0.46.02'
from changedetectionio.strtobool import strtobool
from json.decoder import JSONDecodeError
diff --git a/changedetectionio/api/api_v1.py b/changedetectionio/api/api_v1.py
index 85e2b30e..9b3eb440 100644
--- a/changedetectionio/api/api_v1.py
+++ b/changedetectionio/api/api_v1.py
@@ -12,9 +12,10 @@ import copy
# See docs/README.md for rebuilding the docs/apidoc information
from . import api_schema
+from ..model import watch_base
# Build a JSON Schema atleast partially based on our Watch model
-from changedetectionio.model.Watch import base_config as watch_base_config
+watch_base_config = watch_base()
schema = api_schema.build_watch_json_schema(watch_base_config)
schema_create_watch = copy.deepcopy(schema)
@@ -170,23 +171,33 @@ class WatchSingleHistory(Resource):
curl http://localhost:5000/api/v1/watch/cc0cfffa-f449-477b-83ea-0caafd1dc091/history/1677092977 -H"x-api-key:813031b16330fe25e3780cf0325daa45" -H "Content-Type: application/json"
@apiName Get single snapshot content
@apiGroup Watch History
+ @apiParam {String} [html] Optional Set to =1 to return the last HTML (only stores last 2 snapshots, use `latest` as timestamp)
@apiSuccess (200) {String} OK
@apiSuccess (404) {String} ERR Not found
"""
watch = self.datastore.data['watching'].get(uuid)
if not watch:
- abort(404, message='No watch exists with the UUID of {}'.format(uuid))
+ abort(404, message=f"No watch exists with the UUID of {uuid}")
if not len(watch.history):
- abort(404, message='Watch found but no history exists for the UUID {}'.format(uuid))
+ abort(404, message=f"Watch found but no history exists for the UUID {uuid}")
if timestamp == 'latest':
timestamp = list(watch.history.keys())[-1]
- content = watch.get_history_snapshot(timestamp)
+ if request.args.get('html'):
+ content = watch.get_fetched_html(timestamp)
+ if content:
+ response = make_response(content, 200)
+ response.mimetype = "text/html"
+ else:
+ response = make_response("No content found", 404)
+ response.mimetype = "text/plain"
+ else:
+ content = watch.get_history_snapshot(timestamp)
+ response = make_response(content, 200)
+ response.mimetype = "text/plain"
- response = make_response(content, 200)
- response.mimetype = "text/plain"
return response
diff --git a/changedetectionio/blueprint/browser_steps/__init__.py b/changedetectionio/blueprint/browser_steps/__init__.py
index 30797099..f92bf9f8 100644
--- a/changedetectionio/blueprint/browser_steps/__init__.py
+++ b/changedetectionio/blueprint/browser_steps/__init__.py
@@ -187,8 +187,10 @@ def construct_blueprint(datastore: ChangeDetectionStore):
u = browsersteps_sessions[browsersteps_session_id]['browserstepper'].page.url
if is_last_step and u:
(screenshot, xpath_data) = browsersteps_sessions[browsersteps_session_id]['browserstepper'].request_visualselector_data()
- datastore.save_screenshot(watch_uuid=uuid, screenshot=screenshot)
- datastore.save_xpath_data(watch_uuid=uuid, data=xpath_data)
+ watch = datastore.data['watching'].get(uuid)
+ if watch:
+ watch.save_screenshot(screenshot=screenshot)
+ watch.save_xpath_data(data=xpath_data)
# if not this_session.page:
# cleanup_playwright_session()
diff --git a/changedetectionio/blueprint/browser_steps/browser_steps.py b/changedetectionio/blueprint/browser_steps/browser_steps.py
index 76f3d756..6f38be2e 100644
--- a/changedetectionio/blueprint/browser_steps/browser_steps.py
+++ b/changedetectionio/blueprint/browser_steps/browser_steps.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python3
+#!/usr/bin/env python3
import os
import time
@@ -255,8 +255,9 @@ class browsersteps_live_ui(steppable_browser_interface):
def get_current_state(self):
"""Return the screenshot and interactive elements mapping, generally always called after action_()"""
- from pkg_resources import resource_string
- xpath_element_js = resource_string(__name__, "../../content_fetchers/res/xpath_element_scraper.js").decode('utf-8')
+ import importlib.resources
+ xpath_element_js = importlib.resources.files("changedetectionio.content_fetchers.res").joinpath('xpath_element_scraper.js').read_text()
+
now = time.time()
self.page.wait_for_timeout(1 * 1000)
@@ -287,11 +288,9 @@ class browsersteps_live_ui(steppable_browser_interface):
:param current_include_filters:
:return:
"""
-
+ import importlib.resources
self.page.evaluate("var include_filters=''")
- from pkg_resources import resource_string
- # The code that scrapes elements and makes a list of elements/size/position to click on in the VisualSelector
- xpath_element_js = resource_string(__name__, "../../content_fetchers/res/xpath_element_scraper.js").decode('utf-8')
+ xpath_element_js = importlib.resources.files("changedetectionio.content_fetchers.res").joinpath('xpath_element_scraper.js').read_text()
from changedetectionio.content_fetchers import visualselector_xpath_selectors
xpath_element_js = xpath_element_js.replace('%ELEMENTS%', visualselector_xpath_selectors)
xpath_data = self.page.evaluate("async () => {" + xpath_element_js + "}")
diff --git a/changedetectionio/blueprint/check_proxies/__init__.py b/changedetectionio/blueprint/check_proxies/__init__.py
index 62a7dab3..8d7df73f 100644
--- a/changedetectionio/blueprint/check_proxies/__init__.py
+++ b/changedetectionio/blueprint/check_proxies/__init__.py
@@ -30,7 +30,7 @@ def construct_blueprint(datastore: ChangeDetectionStore):
def long_task(uuid, preferred_proxy):
import time
from changedetectionio.content_fetchers import exceptions as content_fetcher_exceptions
- from changedetectionio.processors import text_json_diff
+ from changedetectionio.processors.text_json_diff import text_json_diff
from changedetectionio.safe_jinja import render as jinja_render
status = {'status': '', 'length': 0, 'text': ''}
diff --git a/changedetectionio/blueprint/price_data_follower/__init__.py b/changedetectionio/blueprint/price_data_follower/__init__.py
index 89a2fc67..a41552d8 100644
--- a/changedetectionio/blueprint/price_data_follower/__init__.py
+++ b/changedetectionio/blueprint/price_data_follower/__init__.py
@@ -17,6 +17,8 @@ def construct_blueprint(datastore: ChangeDetectionStore, update_q: PriorityQueue
@price_data_follower_blueprint.route("//accept", methods=['GET'])
def accept(uuid):
datastore.data['watching'][uuid]['track_ldjson_price_data'] = PRICE_DATA_TRACK_ACCEPT
+ datastore.data['watching'][uuid]['processor'] = 'restock_diff'
+ datastore.data['watching'][uuid].clear_watch()
update_q.put(queuedWatchMetaData.PrioritizedItem(priority=1, item={'uuid': uuid, 'skip_when_checksum_same': False}))
return redirect(url_for("index"))
diff --git a/changedetectionio/blueprint/tags/__init__.py b/changedetectionio/blueprint/tags/__init__.py
index 7a49822b..ca974666 100644
--- a/changedetectionio/blueprint/tags/__init__.py
+++ b/changedetectionio/blueprint/tags/__init__.py
@@ -1,4 +1,6 @@
-from flask import Blueprint, request, make_response, render_template, flash, url_for, redirect
+from flask import Blueprint, request, render_template, flash, url_for, redirect
+
+
from changedetectionio.store import ChangeDetectionStore
from changedetectionio.flask_app import login_optionally_required
@@ -96,22 +98,55 @@ def construct_blueprint(datastore: ChangeDetectionStore):
@tags_blueprint.route("/edit/", methods=['GET'])
@login_optionally_required
def form_tag_edit(uuid):
- from changedetectionio import forms
-
+ from changedetectionio.blueprint.tags.form import group_restock_settings_form
if uuid == 'first':
uuid = list(datastore.data['settings']['application']['tags'].keys()).pop()
default = datastore.data['settings']['application']['tags'].get(uuid)
- form = forms.watchForm(formdata=request.form if request.method == 'POST' else None,
- data=default,
- )
- form.datastore=datastore # needed?
+ form = group_restock_settings_form(formdata=request.form if request.method == 'POST' else None,
+ data=default,
+ extra_notification_tokens=datastore.get_unique_notification_tokens_available()
+ )
+
+ template_args = {
+ 'data': default,
+ 'form': form,
+ 'watch': default,
+ 'extra_notification_token_placeholder_info': datastore.get_unique_notification_token_placeholders_available(),
+ }
+
+ included_content = {}
+ if form.extra_form_content():
+ # So that the extra panels can access _helpers.html etc, we set the environment to load from templates/
+ # And then render the code from the module
+ from jinja2 import Environment, FileSystemLoader
+ import importlib.resources
+ templates_dir = str(importlib.resources.files("changedetectionio").joinpath('templates'))
+ env = Environment(loader=FileSystemLoader(templates_dir))
+ template_str = """{% from '_helpers.html' import render_field, render_checkbox_field, render_button %}
+
+