mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2025-11-16 06:26:20 +00:00
Compare commits
22 Commits
only-use-d
...
jinja2-tem
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
148db6ea25 | ||
|
|
4eb4b401a1 | ||
|
|
5d40e16c73 | ||
|
|
492bbce6b6 | ||
|
|
0394a56be5 | ||
|
|
7839551d6b | ||
|
|
9c5588c791 | ||
|
|
5a43a350de | ||
|
|
3c31f023ce | ||
|
|
4cbcc59461 | ||
|
|
4be0260381 | ||
|
|
957a3c1c16 | ||
|
|
85897e0bf9 | ||
|
|
63095f70ea | ||
|
|
8d5b0b5576 | ||
|
|
1b077abd93 | ||
|
|
32ea1a8721 | ||
|
|
fff32cef0d | ||
|
|
8fb146f3e4 | ||
|
|
770b0faa45 | ||
|
|
f6faa90340 | ||
|
|
669fd3ae0b |
55
.github/workflows/test-container-build.yml
vendored
Normal file
55
.github/workflows/test-container-build.yml
vendored
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
name: ChangeDetection.io Container Build Test
|
||||||
|
|
||||||
|
# Triggers the workflow on push or pull request events
|
||||||
|
|
||||||
|
# This line doesnt work, even tho it is the documented one
|
||||||
|
#on: [push, pull_request]
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- requirements.txt
|
||||||
|
- Dockerfile
|
||||||
|
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- requirements.txt
|
||||||
|
- Dockerfile
|
||||||
|
|
||||||
|
# Changes to requirements.txt packages and Dockerfile may or may not always be compatible with arm etc, so worth testing
|
||||||
|
# @todo: some kind of path filter for requirements.txt and Dockerfile
|
||||||
|
jobs:
|
||||||
|
test-container-build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up Python 3.9
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: 3.9
|
||||||
|
|
||||||
|
# Just test that the build works, some libraries won't compile on ARM/rPi etc
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v1
|
||||||
|
with:
|
||||||
|
image: tonistiigi/binfmt:latest
|
||||||
|
platforms: all
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
id: buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
with:
|
||||||
|
install: true
|
||||||
|
version: latest
|
||||||
|
driver-opts: image=moby/buildkit:master
|
||||||
|
|
||||||
|
- name: Test that the docker containers can build
|
||||||
|
id: docker_build
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
# https://github.com/docker/build-push-action#customizing
|
||||||
|
with:
|
||||||
|
context: ./
|
||||||
|
file: ./Dockerfile
|
||||||
|
platforms: linux/arm/v7,linux/arm/v6,linux/amd64,linux/arm64,
|
||||||
|
cache-from: type=local,src=/tmp/.buildx-cache
|
||||||
|
cache-to: type=local,dest=/tmp/.buildx-cache
|
||||||
12
.github/workflows/test-only.yml
vendored
12
.github/workflows/test-only.yml
vendored
@@ -1,28 +1,25 @@
|
|||||||
name: ChangeDetection.io Test
|
name: ChangeDetection.io App Test
|
||||||
|
|
||||||
# Triggers the workflow on push or pull request events
|
# Triggers the workflow on push or pull request events
|
||||||
on: [push, pull_request]
|
on: [push, pull_request]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test-build:
|
test-application:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Set up Python 3.9
|
- name: Set up Python 3.9
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: 3.9
|
python-version: 3.9
|
||||||
|
|
||||||
- name: Show env vars
|
|
||||||
run: set
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
pip install flake8 pytest
|
pip install flake8 pytest
|
||||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||||
if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi
|
if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi
|
||||||
|
|
||||||
- name: Lint with flake8
|
- name: Lint with flake8
|
||||||
run: |
|
run: |
|
||||||
# stop the build if there are Python syntax errors or undefined names
|
# stop the build if there are Python syntax errors or undefined names
|
||||||
@@ -39,7 +36,4 @@ jobs:
|
|||||||
# Each test is totally isolated and performs its own cleanup/reset
|
# Each test is totally isolated and performs its own cleanup/reset
|
||||||
cd changedetectionio; ./run_all_tests.sh
|
cd changedetectionio; ./run_all_tests.sh
|
||||||
|
|
||||||
# https://github.com/docker/build-push-action/blob/master/docs/advanced/test-before-push.md ?
|
|
||||||
# https://github.com/docker/buildx/issues/59 ? Needs to be one platform?
|
|
||||||
|
|
||||||
# https://github.com/docker/buildx/issues/495#issuecomment-918925854
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Otherwise, it's always best to PR into the `dev` branch.
|
|||||||
|
|
||||||
Please be sure that all new functionality has a matching test!
|
Please be sure that all new functionality has a matching test!
|
||||||
|
|
||||||
Use `pytest` to validate/test, you can run the existing tests as `pytest tests/test_notifications.py` for example
|
Use `pytest` to validate/test, you can run the existing tests as `pytest tests/test_notification.py` for example
|
||||||
|
|
||||||
```
|
```
|
||||||
pip3 install -r requirements-dev
|
pip3 install -r requirements-dev
|
||||||
|
|||||||
15
Dockerfile
15
Dockerfile
@@ -5,13 +5,14 @@ FROM python:3.8-slim as builder
|
|||||||
ARG CRYPTOGRAPHY_DONT_BUILD_RUST=1
|
ARG CRYPTOGRAPHY_DONT_BUILD_RUST=1
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
libssl-dev \
|
g++ \
|
||||||
libffi-dev \
|
|
||||||
gcc \
|
gcc \
|
||||||
libc-dev \
|
libc-dev \
|
||||||
|
libffi-dev \
|
||||||
|
libssl-dev \
|
||||||
libxslt-dev \
|
libxslt-dev \
|
||||||
zlib1g-dev \
|
make \
|
||||||
g++
|
zlib1g-dev
|
||||||
|
|
||||||
RUN mkdir /install
|
RUN mkdir /install
|
||||||
WORKDIR /install
|
WORKDIR /install
|
||||||
@@ -25,6 +26,11 @@ RUN pip install --target=/dependencies -r /requirements.txt
|
|||||||
RUN pip install --target=/dependencies playwright~=1.26 \
|
RUN pip install --target=/dependencies playwright~=1.26 \
|
||||||
|| echo "WARN: Failed to install Playwright. The application can still run, but the Playwright option will be disabled."
|
|| echo "WARN: Failed to install Playwright. The application can still run, but the Playwright option will be disabled."
|
||||||
|
|
||||||
|
|
||||||
|
RUN pip install --target=/dependencies jq~=1.3 \
|
||||||
|
|| echo "WARN: Failed to install JQ. The application can still run, but the Jq: filter option will be disabled."
|
||||||
|
|
||||||
|
|
||||||
# Final image stage
|
# Final image stage
|
||||||
FROM python:3.8-slim
|
FROM python:3.8-slim
|
||||||
|
|
||||||
@@ -58,6 +64,7 @@ EXPOSE 5000
|
|||||||
|
|
||||||
# The actual flask app
|
# The actual flask app
|
||||||
COPY changedetectionio /app/changedetectionio
|
COPY changedetectionio /app/changedetectionio
|
||||||
|
|
||||||
# The eventlet server wrapper
|
# The eventlet server wrapper
|
||||||
COPY changedetection.py /app/changedetection.py
|
COPY changedetection.py /app/changedetection.py
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ recursive-include changedetectionio/api *
|
|||||||
recursive-include changedetectionio/templates *
|
recursive-include changedetectionio/templates *
|
||||||
recursive-include changedetectionio/static *
|
recursive-include changedetectionio/static *
|
||||||
recursive-include changedetectionio/model *
|
recursive-include changedetectionio/model *
|
||||||
|
recursive-include changedetectionio/tests *
|
||||||
include changedetection.py
|
include changedetection.py
|
||||||
global-exclude *.pyc
|
global-exclude *.pyc
|
||||||
global-exclude node_modules
|
global-exclude node_modules
|
||||||
|
|||||||
42
README.md
42
README.md
@@ -121,8 +121,8 @@ See the wiki for more information https://github.com/dgtlmoon/changedetection.io
|
|||||||
|
|
||||||
|
|
||||||
## Filters
|
## Filters
|
||||||
XPath, JSONPath, jq, and CSS support comes baked in! You can be as specific as you need, use XPath exported from various XPath element query creation tools.
|
|
||||||
|
|
||||||
|
XPath, JSONPath, jq, and CSS support comes baked in! You can be as specific as you need, use XPath exported from various XPath element query creation tools.
|
||||||
(We support LXML `re:test`, `re:math` and `re:replace`.)
|
(We support LXML `re:test`, `re:math` and `re:replace`.)
|
||||||
|
|
||||||
## Notifications
|
## Notifications
|
||||||
@@ -161,46 +161,14 @@ This will re-parse the JSON and apply formatting to the text, making it super ea
|
|||||||
|
|
||||||
### JSONPath or jq?
|
### JSONPath or jq?
|
||||||
|
|
||||||
For more complex parsing, filtering, and modifying of JSON data, jq is recommended due to the built-in operators and functions. Refer to the [documentation](https://stedolan.github.io/jq/manual/) for more information on jq.
|
For more complex parsing, filtering, and modifying of JSON data, jq is recommended due to the built-in operators and functions. Refer to the [documentation](https://stedolan.github.io/jq/manual/) for more specifc information on jq.
|
||||||
|
|
||||||
The example below adds the price in dollars to each item in the JSON data, and then filters to only show items that are greater than 10.
|
One big advantage of `jq` is that you can use logic in your JSON filter, such as filters to only show items that have a value greater than/less than etc.
|
||||||
|
|
||||||
#### Sample input data from API
|
See the wiki https://github.com/dgtlmoon/changedetection.io/wiki/JSON-Selector-Filter-help for more information and examples
|
||||||
```
|
|
||||||
{
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"name": "Product A",
|
|
||||||
"priceInCents": 2500
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Product B",
|
|
||||||
"priceInCents": 500
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Product C",
|
|
||||||
"priceInCents": 2000
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Sample jq
|
Note: `jq` library must be added separately (`pip3 install jq`)
|
||||||
`jq:.items[] | . + { "priceInDollars": (.priceInCents / 100) } | select(.priceInDollars > 10)`
|
|
||||||
|
|
||||||
#### Sample output data
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"name": "Product A",
|
|
||||||
"priceInCents": 2500,
|
|
||||||
"priceInDollars": 25
|
|
||||||
}
|
|
||||||
{
|
|
||||||
"name": "Product C",
|
|
||||||
"priceInCents": 2000,
|
|
||||||
"priceInDollars": 20
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Parse JSON embedded in HTML!
|
### Parse JSON embedded in HTML!
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ from flask_wtf import CSRFProtect
|
|||||||
from changedetectionio import html_tools
|
from changedetectionio import html_tools
|
||||||
from changedetectionio.api import api_v1
|
from changedetectionio.api import api_v1
|
||||||
|
|
||||||
__version__ = '0.39.20'
|
__version__ = '0.39.20.4'
|
||||||
|
|
||||||
datastore = None
|
datastore = None
|
||||||
|
|
||||||
@@ -194,6 +194,9 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
watch_api.add_resource(api_v1.Watch, '/api/v1/watch/<string:uuid>',
|
watch_api.add_resource(api_v1.Watch, '/api/v1/watch/<string:uuid>',
|
||||||
resource_class_kwargs={'datastore': datastore, 'update_q': update_q})
|
resource_class_kwargs={'datastore': datastore, 'update_q': update_q})
|
||||||
|
|
||||||
|
watch_api.add_resource(api_v1.SystemInfo, '/api/v1/systeminfo',
|
||||||
|
resource_class_kwargs={'datastore': datastore, 'update_q': update_q})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -636,20 +639,27 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
# Only works reliably with Playwright
|
# Only works reliably with Playwright
|
||||||
visualselector_enabled = os.getenv('PLAYWRIGHT_DRIVER_URL', False) and default['fetch_backend'] == 'html_webdriver'
|
visualselector_enabled = os.getenv('PLAYWRIGHT_DRIVER_URL', False) and default['fetch_backend'] == 'html_webdriver'
|
||||||
|
|
||||||
|
# JQ is difficult to install on windows and must be manually added (outside requirements.txt)
|
||||||
|
jq_support = True
|
||||||
|
try:
|
||||||
|
import jq
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
jq_support = False
|
||||||
|
|
||||||
output = render_template("edit.html",
|
output = render_template("edit.html",
|
||||||
uuid=uuid,
|
|
||||||
watch=datastore.data['watching'][uuid],
|
|
||||||
form=form,
|
|
||||||
has_empty_checktime=using_default_check_time,
|
|
||||||
has_default_notification_urls=True if len(datastore.data['settings']['application']['notification_urls']) else False,
|
|
||||||
using_global_webdriver_wait=default['webdriver_delay'] is None,
|
|
||||||
current_base_url=datastore.data['settings']['application']['base_url'],
|
current_base_url=datastore.data['settings']['application']['base_url'],
|
||||||
emailprefix=os.getenv('NOTIFICATION_MAIL_BUTTON_PREFIX', False),
|
emailprefix=os.getenv('NOTIFICATION_MAIL_BUTTON_PREFIX', False),
|
||||||
|
form=form,
|
||||||
|
has_default_notification_urls=True if len(datastore.data['settings']['application']['notification_urls']) else False,
|
||||||
|
has_empty_checktime=using_default_check_time,
|
||||||
|
jq_support=jq_support,
|
||||||
|
playwright_enabled=os.getenv('PLAYWRIGHT_DRIVER_URL', False),
|
||||||
settings_application=datastore.data['settings']['application'],
|
settings_application=datastore.data['settings']['application'],
|
||||||
|
using_global_webdriver_wait=default['webdriver_delay'] is None,
|
||||||
|
uuid=uuid,
|
||||||
visualselector_data_is_ready=visualselector_data_is_ready,
|
visualselector_data_is_ready=visualselector_data_is_ready,
|
||||||
visualselector_enabled=visualselector_enabled,
|
visualselector_enabled=visualselector_enabled,
|
||||||
playwright_enabled=os.getenv('PLAYWRIGHT_DRIVER_URL', False)
|
watch=datastore.data['watching'][uuid],
|
||||||
)
|
)
|
||||||
|
|
||||||
return output
|
return output
|
||||||
@@ -809,8 +819,10 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
|
|
||||||
newest_file = history[dates[-1]]
|
newest_file = history[dates[-1]]
|
||||||
|
|
||||||
|
# Read as binary and force decode as UTF-8
|
||||||
|
# Windows may fail decode in python if we just use 'r' mode (chardet decode exception)
|
||||||
try:
|
try:
|
||||||
with open(newest_file, 'r') as f:
|
with open(newest_file, 'r', encoding='utf-8', errors='ignore') as f:
|
||||||
newest_version_file_contents = f.read()
|
newest_version_file_contents = f.read()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
newest_version_file_contents = "Unable to read {}.\n".format(newest_file)
|
newest_version_file_contents = "Unable to read {}.\n".format(newest_file)
|
||||||
@@ -823,7 +835,7 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
previous_file = history[dates[-2]]
|
previous_file = history[dates[-2]]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(previous_file, 'r') as f:
|
with open(previous_file, 'r', encoding='utf-8', errors='ignore') as f:
|
||||||
previous_version_file_contents = f.read()
|
previous_version_file_contents = f.read()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
previous_version_file_contents = "Unable to read {}.\n".format(previous_file)
|
previous_version_file_contents = "Unable to read {}.\n".format(previous_file)
|
||||||
@@ -900,7 +912,7 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
timestamp = list(watch.history.keys())[-1]
|
timestamp = list(watch.history.keys())[-1]
|
||||||
filename = watch.history[timestamp]
|
filename = watch.history[timestamp]
|
||||||
try:
|
try:
|
||||||
with open(filename, 'r') as f:
|
with open(filename, 'r', encoding='utf-8', errors='ignore') as f:
|
||||||
tmp = f.readlines()
|
tmp = f.readlines()
|
||||||
|
|
||||||
# Get what needs to be highlighted
|
# Get what needs to be highlighted
|
||||||
|
|||||||
@@ -122,3 +122,37 @@ class CreateWatch(Resource):
|
|||||||
return {'status': "OK"}, 200
|
return {'status': "OK"}, 200
|
||||||
|
|
||||||
return list, 200
|
return list, 200
|
||||||
|
|
||||||
|
class SystemInfo(Resource):
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
# datastore is a black box dependency
|
||||||
|
self.datastore = kwargs['datastore']
|
||||||
|
self.update_q = kwargs['update_q']
|
||||||
|
|
||||||
|
@auth.check_token
|
||||||
|
def get(self):
|
||||||
|
import time
|
||||||
|
overdue_watches = []
|
||||||
|
|
||||||
|
# Check all watches and report which have not been checked but should have been
|
||||||
|
|
||||||
|
for uuid, watch in self.datastore.data.get('watching', {}).items():
|
||||||
|
# see if now - last_checked is greater than the time that should have been
|
||||||
|
# this is not super accurate (maybe they just edited it) but better than nothing
|
||||||
|
t = watch.threshold_seconds()
|
||||||
|
if not t:
|
||||||
|
# Use the system wide default
|
||||||
|
t = self.datastore.threshold_seconds
|
||||||
|
|
||||||
|
time_since_check = time.time() - watch.get('last_checked')
|
||||||
|
|
||||||
|
# Allow 5 minutes of grace time before we decide it's overdue
|
||||||
|
if time_since_check - (5 * 60) > t:
|
||||||
|
overdue_watches.append(uuid)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'queue_size': self.update_q.qsize(),
|
||||||
|
'overdue_watches': overdue_watches,
|
||||||
|
'uptime': round(time.time() - self.datastore.start_time, 2),
|
||||||
|
'watch_count': len(self.datastore.data.get('watching', {}))
|
||||||
|
}, 200
|
||||||
|
|||||||
@@ -102,6 +102,14 @@ def main():
|
|||||||
has_password=datastore.data['settings']['application']['password'] != False
|
has_password=datastore.data['settings']['application']['password'] != False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Monitored websites will not receive a Referer header
|
||||||
|
# when a user clicks on an outgoing link.
|
||||||
|
@app.after_request
|
||||||
|
def hide_referrer(response):
|
||||||
|
if os.getenv("HIDE_REFERER", False):
|
||||||
|
response.headers["Referrer-Policy"] = "no-referrer"
|
||||||
|
return response
|
||||||
|
|
||||||
# Proxy sub-directory support
|
# Proxy sub-directory support
|
||||||
# Set environment var USE_X_SETTINGS=1 on this script
|
# Set environment var USE_X_SETTINGS=1 on this script
|
||||||
# And then in your proxy_pass settings
|
# And then in your proxy_pass settings
|
||||||
|
|||||||
@@ -575,6 +575,11 @@ class html_requests(Fetcher):
|
|||||||
ignore_status_codes=False,
|
ignore_status_codes=False,
|
||||||
current_css_filter=None):
|
current_css_filter=None):
|
||||||
|
|
||||||
|
# Make requests use a more modern looking user-agent
|
||||||
|
if not 'User-Agent' in request_headers:
|
||||||
|
request_headers['User-Agent'] = os.getenv("DEFAULT_SETTINGS_HEADERS_USERAGENT",
|
||||||
|
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36')
|
||||||
|
|
||||||
proxies = {}
|
proxies = {}
|
||||||
|
|
||||||
# Allows override the proxy on a per-request basis
|
# Allows override the proxy on a per-request basis
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ class perform_site_check():
|
|||||||
|
|
||||||
|
|
||||||
def run(self, uuid):
|
def run(self, uuid):
|
||||||
|
from jinja2 import Environment
|
||||||
|
|
||||||
changed_detected = False
|
changed_detected = False
|
||||||
screenshot = False # as bytes
|
screenshot = False # as bytes
|
||||||
stripped_text_from_html = ""
|
stripped_text_from_html = ""
|
||||||
@@ -65,7 +67,11 @@ class perform_site_check():
|
|||||||
request_headers['Accept-Encoding'] = request_headers['Accept-Encoding'].replace(', br', '')
|
request_headers['Accept-Encoding'] = request_headers['Accept-Encoding'].replace(', br', '')
|
||||||
|
|
||||||
timeout = self.datastore.data['settings']['requests'].get('timeout')
|
timeout = self.datastore.data['settings']['requests'].get('timeout')
|
||||||
url = watch.get('url')
|
|
||||||
|
# Jinja2 available in URLs along with https://pypi.org/project/jinja2-time/
|
||||||
|
jinja2_env = Environment(extensions=['jinja2_time.TimeExtension'])
|
||||||
|
url = str(jinja2_env.from_string(watch.get('url')).render())
|
||||||
|
|
||||||
request_body = self.datastore.data['watching'][uuid].get('body')
|
request_body = self.datastore.data['watching'][uuid].get('body')
|
||||||
request_method = self.datastore.data['watching'][uuid].get('method')
|
request_method = self.datastore.data['watching'][uuid].get('method')
|
||||||
ignore_status_codes = self.datastore.data['watching'][uuid].get('ignore_status_codes', False)
|
ignore_status_codes = self.datastore.data['watching'][uuid].get('ignore_status_codes', False)
|
||||||
|
|||||||
@@ -303,12 +303,16 @@ class ValidateCSSJSONXPATHInput(object):
|
|||||||
|
|
||||||
# Re #265 - maybe in the future fetch the page and offer a
|
# Re #265 - maybe in the future fetch the page and offer a
|
||||||
# warning/notice that its possible the rule doesnt yet match anything?
|
# warning/notice that its possible the rule doesnt yet match anything?
|
||||||
|
|
||||||
if 'jq:' in line:
|
|
||||||
if not self.allow_json:
|
if not self.allow_json:
|
||||||
raise ValidationError("jq not permitted in this field!")
|
raise ValidationError("jq not permitted in this field!")
|
||||||
|
|
||||||
|
if 'jq:' in line:
|
||||||
|
try:
|
||||||
import jq
|
import jq
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
# `jq` requires full compilation in windows and so isn't generally available
|
||||||
|
raise ValidationError("jq not support not found")
|
||||||
|
|
||||||
input = line.replace('jq:', '')
|
input = line.replace('jq:', '')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import json
|
|
||||||
from typing import List
|
|
||||||
|
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from jsonpath_ng.ext import parse
|
|
||||||
import jq
|
|
||||||
import re
|
|
||||||
from inscriptis import get_text
|
from inscriptis import get_text
|
||||||
from inscriptis.model.config import ParserConfig
|
from inscriptis.model.config import ParserConfig
|
||||||
|
from jsonpath_ng.ext import parse
|
||||||
|
from typing import List
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
|
||||||
class FilterNotFoundInResponse(ValueError):
|
class FilterNotFoundInResponse(ValueError):
|
||||||
def __init__(self, msg):
|
def __init__(self, msg):
|
||||||
@@ -85,9 +84,18 @@ def _parse_json(json_data, json_filter):
|
|||||||
jsonpath_expression = parse(json_filter.replace('json:', ''))
|
jsonpath_expression = parse(json_filter.replace('json:', ''))
|
||||||
match = jsonpath_expression.find(json_data)
|
match = jsonpath_expression.find(json_data)
|
||||||
return _get_stripped_text_from_json_match(match)
|
return _get_stripped_text_from_json_match(match)
|
||||||
|
|
||||||
if 'jq:' in json_filter:
|
if 'jq:' in json_filter:
|
||||||
|
|
||||||
|
try:
|
||||||
|
import jq
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
# `jq` requires full compilation in windows and so isn't generally available
|
||||||
|
raise Exception("jq not support not found")
|
||||||
|
|
||||||
jq_expression = jq.compile(json_filter.replace('jq:', ''))
|
jq_expression = jq.compile(json_filter.replace('jq:', ''))
|
||||||
match = jq_expression.input(json_data).all()
|
match = jq_expression.input(json_data).all()
|
||||||
|
|
||||||
return _get_stripped_text_from_json_match(match)
|
return _get_stripped_text_from_json_match(match)
|
||||||
|
|
||||||
def _get_stripped_text_from_json_match(match):
|
def _get_stripped_text_from_json_match(match):
|
||||||
|
|||||||
@@ -13,10 +13,6 @@ class model(dict):
|
|||||||
'watching': {},
|
'watching': {},
|
||||||
'settings': {
|
'settings': {
|
||||||
'headers': {
|
'headers': {
|
||||||
'User-Agent': getenv("DEFAULT_SETTINGS_HEADERS_USERAGENT", 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36'),
|
|
||||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
|
|
||||||
'Accept-Encoding': 'gzip, deflate', # No support for brolti in python requests yet.
|
|
||||||
'Accept-Language': 'en-GB,en-US;q=0.9,en;'
|
|
||||||
},
|
},
|
||||||
'requests': {
|
'requests': {
|
||||||
'timeout': int(getenv("DEFAULT_SETTINGS_REQUESTS_TIMEOUT", "45")), # Default 45 seconds
|
'timeout': int(getenv("DEFAULT_SETTINGS_REQUESTS_TIMEOUT", "45")), # Default 45 seconds
|
||||||
|
|||||||
@@ -118,7 +118,10 @@ class model(dict):
|
|||||||
if os.path.isfile(fname):
|
if os.path.isfile(fname):
|
||||||
logging.debug("Reading history index " + str(time.time()))
|
logging.debug("Reading history index " + str(time.time()))
|
||||||
with open(fname, "r") as f:
|
with open(fname, "r") as f:
|
||||||
tmp_history = dict(i.strip().split(',', 2) for i in f.readlines())
|
for i in f.readlines():
|
||||||
|
if ',' in i:
|
||||||
|
k, v = i.strip().split(',', 2)
|
||||||
|
tmp_history[k] = v
|
||||||
|
|
||||||
if len(tmp_history):
|
if len(tmp_history):
|
||||||
self.__newest_history_key = list(tmp_history.keys())[-1]
|
self.__newest_history_key = list(tmp_history.keys())[-1]
|
||||||
@@ -151,28 +154,30 @@ class model(dict):
|
|||||||
import uuid
|
import uuid
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
output_path = "{}/{}".format(self.__datastore_path, self['uuid'])
|
output_path = os.path.join(self.__datastore_path, self['uuid'])
|
||||||
|
|
||||||
self.ensure_data_dir_exists()
|
self.ensure_data_dir_exists()
|
||||||
|
snapshot_fname = os.path.join(output_path, str(uuid.uuid4()))
|
||||||
|
|
||||||
snapshot_fname = "{}/{}.stripped.txt".format(output_path, uuid.uuid4())
|
|
||||||
logging.debug("Saving history text {}".format(snapshot_fname))
|
logging.debug("Saving history text {}".format(snapshot_fname))
|
||||||
|
|
||||||
|
# in /diff/ and /preview/ we are going to assume for now that it's UTF-8 when reading
|
||||||
|
# most sites are utf-8 and some are even broken utf-8
|
||||||
with open(snapshot_fname, 'wb') as f:
|
with open(snapshot_fname, 'wb') as f:
|
||||||
f.write(contents)
|
f.write(contents)
|
||||||
f.close()
|
f.close()
|
||||||
|
|
||||||
# Append to index
|
# Append to index
|
||||||
# @todo check last char was \n
|
# @todo check last char was \n
|
||||||
index_fname = "{}/history.txt".format(output_path)
|
index_fname = os.path.join(output_path, "history.txt")
|
||||||
with open(index_fname, 'a') as f:
|
with open(index_fname, 'a') as f:
|
||||||
f.write("{},{}\n".format(timestamp, snapshot_fname))
|
f.write("{},{}\n".format(timestamp, snapshot_fname))
|
||||||
f.close()
|
f.close()
|
||||||
|
|
||||||
self.__newest_history_key = timestamp
|
self.__newest_history_key = timestamp
|
||||||
self.__history_n+=1
|
self.__history_n += 1
|
||||||
|
|
||||||
#@todo bump static cache of the last timestamp so we dont need to examine the file to set a proper ''viewed'' status
|
# @todo bump static cache of the last timestamp so we dont need to examine the file to set a proper ''viewed'' status
|
||||||
return snapshot_fname
|
return snapshot_fname
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
# exit when any command fails
|
# exit when any command fails
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||||
|
|
||||||
find tests/test_*py -type f|while read test_name
|
find tests/test_*py -type f|while read test_name
|
||||||
do
|
do
|
||||||
echo "TEST RUNNING $test_name"
|
echo "TEST RUNNING $test_name"
|
||||||
@@ -23,6 +25,13 @@ export BASE_URL="https://really-unique-domain.io"
|
|||||||
pytest tests/test_notification.py
|
pytest tests/test_notification.py
|
||||||
|
|
||||||
|
|
||||||
|
## JQ + JSON: filter test
|
||||||
|
# jq is not available on windows and we should just test it when the package is installed
|
||||||
|
# this will re-test with jq support
|
||||||
|
pip3 install jq~=1.3
|
||||||
|
pytest tests/test_jsonpath_jq_selector.py
|
||||||
|
|
||||||
|
|
||||||
# Now for the selenium and playwright/browserless fetchers
|
# Now for the selenium and playwright/browserless fetchers
|
||||||
# Note - this is not UI functional tests - just checking that each one can fetch the content
|
# Note - this is not UI functional tests - just checking that each one can fetch the content
|
||||||
|
|
||||||
@@ -38,7 +47,9 @@ docker kill $$-test_selenium
|
|||||||
|
|
||||||
echo "TESTING WEBDRIVER FETCH > PLAYWRIGHT/BROWSERLESS..."
|
echo "TESTING WEBDRIVER FETCH > PLAYWRIGHT/BROWSERLESS..."
|
||||||
# Not all platforms support playwright (not ARM/rPI), so it's not packaged in requirements.txt
|
# Not all platforms support playwright (not ARM/rPI), so it's not packaged in requirements.txt
|
||||||
pip3 install playwright~=1.24
|
PLAYWRIGHT_VERSION=$(grep -i -E "RUN pip install.+" "$SCRIPT_DIR/../Dockerfile" | grep --only-matching -i -E "playwright[=><~+]+[0-9\.]+")
|
||||||
|
echo "using $PLAYWRIGHT_VERSION"
|
||||||
|
pip3 install "$PLAYWRIGHT_VERSION"
|
||||||
docker run -d --name $$-test_browserless -e "DEFAULT_LAUNCH_ARGS=[\"--window-size=1920,1080\"]" --rm -p 3000:3000 --shm-size="2g" browserless/chrome:1.53-chrome-stable
|
docker run -d --name $$-test_browserless -e "DEFAULT_LAUNCH_ARGS=[\"--window-size=1920,1080\"]" --rm -p 3000:3000 --shm-size="2g" browserless/chrome:1.53-chrome-stable
|
||||||
# takes a while to spin up
|
# takes a while to spin up
|
||||||
sleep 5
|
sleep 5
|
||||||
|
|||||||
@@ -30,14 +30,14 @@ class ChangeDetectionStore:
|
|||||||
def __init__(self, datastore_path="/datastore", include_default_watches=True, version_tag="0.0.0"):
|
def __init__(self, datastore_path="/datastore", include_default_watches=True, version_tag="0.0.0"):
|
||||||
# Should only be active for docker
|
# Should only be active for docker
|
||||||
# logging.basicConfig(filename='/dev/stdout', level=logging.INFO)
|
# logging.basicConfig(filename='/dev/stdout', level=logging.INFO)
|
||||||
self.needs_write = False
|
self.__data = App.model()
|
||||||
self.datastore_path = datastore_path
|
self.datastore_path = datastore_path
|
||||||
self.json_store_path = "{}/url-watches.json".format(self.datastore_path)
|
self.json_store_path = "{}/url-watches.json".format(self.datastore_path)
|
||||||
|
self.needs_write = False
|
||||||
self.proxy_list = None
|
self.proxy_list = None
|
||||||
|
self.start_time = time.time()
|
||||||
self.stop_thread = False
|
self.stop_thread = False
|
||||||
|
|
||||||
self.__data = App.model()
|
|
||||||
|
|
||||||
# Base definition for all watchers
|
# Base definition for all watchers
|
||||||
# deepcopy part of #569 - not sure why its needed exactly
|
# deepcopy part of #569 - not sure why its needed exactly
|
||||||
self.generic_definition = deepcopy(Watch.model(datastore_path = datastore_path, default={}))
|
self.generic_definition = deepcopy(Watch.model(datastore_path = datastore_path, default={}))
|
||||||
@@ -575,3 +575,11 @@ class ChangeDetectionStore:
|
|||||||
continue
|
continue
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
# We incorrectly used common header overrides that should only apply to Requests
|
||||||
|
# These are now handled in content_fetcher::html_requests and shouldnt be passed to Playwright/Selenium
|
||||||
|
def update_7(self):
|
||||||
|
# These were hard-coded in early versions
|
||||||
|
for v in ['User-Agent', 'Accept', 'Accept-Encoding', 'Accept-Language']:
|
||||||
|
if self.data['settings']['headers'].get(v):
|
||||||
|
del self.data['settings']['headers'][v]
|
||||||
|
|||||||
@@ -40,7 +40,8 @@
|
|||||||
<fieldset>
|
<fieldset>
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
{{ render_field(form.url, placeholder="https://...", required=true, class="m-d") }}
|
{{ render_field(form.url, placeholder="https://...", required=true, class="m-d") }}
|
||||||
<span class="pure-form-message-inline">Some sites use JavaScript to create the content, for this you should <a href="https://github.com/dgtlmoon/changedetection.io/wiki/Fetching-pages-with-WebDriver">use the Chrome/WebDriver Fetcher</a></span>
|
<span class="pure-form-message-inline">Some sites use JavaScript to create the content, for this you should <a href="https://github.com/dgtlmoon/changedetection.io/wiki/Fetching-pages-with-WebDriver">use the Chrome/WebDriver Fetcher</a></span><br/>
|
||||||
|
<span class="pure-form-message-inline">You can use variables in the URL, perfect for inserting the current date and other logic, <a href="https://github.com/dgtlmoon/changedetection.io/wiki/Handling-variables-in-the-watched-URL">help and examples here</a></span><br/>
|
||||||
</div>
|
</div>
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
{{ render_field(form.title, class="m-d") }}
|
{{ render_field(form.title, class="m-d") }}
|
||||||
@@ -184,10 +185,14 @@ User-Agent: wonderbra 1.0") }}
|
|||||||
<span class="pure-form-message-inline">
|
<span class="pure-form-message-inline">
|
||||||
<ul>
|
<ul>
|
||||||
<li>CSS - Limit text to this CSS rule, only text matching this CSS rule is included.</li>
|
<li>CSS - Limit text to this CSS rule, only text matching this CSS rule is included.</li>
|
||||||
<li>JSON - Limit text to this JSON rule, using either <a href="https://pypi.org/project/jsonpath-ng/" target="new">JSONPath</a> or <a href="https://stedolan.github.io/jq/" target="new">jq</a>.
|
<li>JSON - Limit text to this JSON rule, using either <a href="https://pypi.org/project/jsonpath-ng/" target="new">JSONPath</a> or <a href="https://stedolan.github.io/jq/" target="new">jq</a> (if installed).
|
||||||
<ul>
|
<ul>
|
||||||
<li>JSONPath: Prefix with <code>json:</code>, use <code>json:$</code> to force re-formatting if required, <a href="https://jsonpath.com/" target="new">test your JSONPath here</a>.</li>
|
<li>JSONPath: Prefix with <code>json:</code>, use <code>json:$</code> to force re-formatting if required, <a href="https://jsonpath.com/" target="new">test your JSONPath here</a>.</li>
|
||||||
|
{% if jq_support %}
|
||||||
<li>jq: Prefix with <code>jq:</code> and <a href="https://jqplay.org/" target="new">test your jq here</a>. Using <a href="https://stedolan.github.io/jq/" target="new">jq</a> allows for complex filtering and processing of JSON data with built-in functions, regex, filtering, and more. See examples and documentation <a href="https://stedolan.github.io/jq/manual/" target="new">here</a>.</li>
|
<li>jq: Prefix with <code>jq:</code> and <a href="https://jqplay.org/" target="new">test your jq here</a>. Using <a href="https://stedolan.github.io/jq/" target="new">jq</a> allows for complex filtering and processing of JSON data with built-in functions, regex, filtering, and more. See examples and documentation <a href="https://stedolan.github.io/jq/manual/" target="new">here</a>.</li>
|
||||||
|
{% else %}
|
||||||
|
<li>jq support not installed</li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>XPath - Limit text to this XPath rule, simply start with a forward-slash,
|
<li>XPath - Limit text to this XPath rule, simply start with a forward-slash,
|
||||||
@@ -198,7 +203,7 @@ User-Agent: wonderbra 1.0") }}
|
|||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
Please be sure that you thoroughly understand how to write CSS, JSONPath, XPath, or jq selector rules before filing an issue on GitHub! <a
|
Please be sure that you thoroughly understand how to write CSS, JSONPath, XPath{% if jq_support %}, or jq selector{%endif%} rules before filing an issue on GitHub! <a
|
||||||
href="https://github.com/dgtlmoon/changedetection.io/wiki/CSS-Selector-help">here for more CSS selector help</a>.<br/>
|
href="https://github.com/dgtlmoon/changedetection.io/wiki/CSS-Selector-help">here for more CSS selector help</a>.<br/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -147,6 +147,16 @@ def test_api_simple(client, live_server):
|
|||||||
# @todo how to handle None/default global values?
|
# @todo how to handle None/default global values?
|
||||||
assert watch['history_n'] == 2, "Found replacement history section, which is in its own API"
|
assert watch['history_n'] == 2, "Found replacement history section, which is in its own API"
|
||||||
|
|
||||||
|
# basic systeminfo check
|
||||||
|
res = client.get(
|
||||||
|
url_for("systeminfo"),
|
||||||
|
headers={'x-api-key': api_key},
|
||||||
|
)
|
||||||
|
info = json.loads(res.data)
|
||||||
|
assert info.get('watch_count') == 1
|
||||||
|
assert info.get('uptime') > 0.5
|
||||||
|
|
||||||
|
|
||||||
# Finally delete the watch
|
# Finally delete the watch
|
||||||
res = client.delete(
|
res = client.delete(
|
||||||
url_for("watch", uuid=watch_uuid),
|
url_for("watch", uuid=watch_uuid),
|
||||||
|
|||||||
33
changedetectionio/tests/test_jinja2.py
Normal file
33
changedetectionio/tests/test_jinja2.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
import time
|
||||||
|
from flask import url_for
|
||||||
|
from .util import live_server_setup
|
||||||
|
|
||||||
|
|
||||||
|
# If there was only a change in the whitespacing, then we shouldnt have a change detected
|
||||||
|
def test_jinja2_in_url_query(client, live_server):
|
||||||
|
live_server_setup(live_server)
|
||||||
|
|
||||||
|
# Give the endpoint time to spin up
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# Add our URL to the import page
|
||||||
|
test_url = url_for('test_return_query', _external=True)
|
||||||
|
|
||||||
|
# because url_for() will URL-encode the var, but we dont here
|
||||||
|
full_url = "{}?{}".format(test_url,
|
||||||
|
"date={% now 'Europe/Berlin', '%Y' %}.{% now 'Europe/Berlin', '%m' %}.{% now 'Europe/Berlin', '%d' %}", )
|
||||||
|
res = client.post(
|
||||||
|
url_for("form_quick_watch_add"),
|
||||||
|
data={"url": full_url, "tag": "test"},
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
assert b"Watch added" in res.data
|
||||||
|
time.sleep(3)
|
||||||
|
# It should report nothing found (no new 'unviewed' class)
|
||||||
|
res = client.get(
|
||||||
|
url_for("preview_page", uuid="first"),
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
assert b'date=2' in res.data
|
||||||
@@ -5,7 +5,12 @@ import time
|
|||||||
from flask import url_for, escape
|
from flask import url_for, escape
|
||||||
from . util import live_server_setup
|
from . util import live_server_setup
|
||||||
import pytest
|
import pytest
|
||||||
|
jq_support = True
|
||||||
|
|
||||||
|
try:
|
||||||
|
import jq
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
jq_support = False
|
||||||
|
|
||||||
def test_setup(live_server):
|
def test_setup(live_server):
|
||||||
live_server_setup(live_server)
|
live_server_setup(live_server)
|
||||||
@@ -40,13 +45,14 @@ and it can also be repeated
|
|||||||
assert text == "23.5"
|
assert text == "23.5"
|
||||||
|
|
||||||
# also check for jq
|
# also check for jq
|
||||||
|
if jq_support:
|
||||||
text = html_tools.extract_json_as_string(content, "jq:.offers.price")
|
text = html_tools.extract_json_as_string(content, "jq:.offers.price")
|
||||||
assert text == "23.5"
|
assert text == "23.5"
|
||||||
|
|
||||||
text = html_tools.extract_json_as_string('{"id":5}', "json:$.id")
|
text = html_tools.extract_json_as_string('{"id":5}', "jq:.id")
|
||||||
assert text == "5"
|
assert text == "5"
|
||||||
|
|
||||||
text = html_tools.extract_json_as_string('{"id":5}', "jq:.id")
|
text = html_tools.extract_json_as_string('{"id":5}', "json:$.id")
|
||||||
assert text == "5"
|
assert text == "5"
|
||||||
|
|
||||||
# When nothing at all is found, it should throw JSONNOTFound
|
# When nothing at all is found, it should throw JSONNOTFound
|
||||||
@@ -54,6 +60,7 @@ and it can also be repeated
|
|||||||
with pytest.raises(html_tools.JSONNotFound) as e_info:
|
with pytest.raises(html_tools.JSONNotFound) as e_info:
|
||||||
html_tools.extract_json_as_string('COMPLETE GIBBERISH, NO JSON!', "json:$.id")
|
html_tools.extract_json_as_string('COMPLETE GIBBERISH, NO JSON!', "json:$.id")
|
||||||
|
|
||||||
|
if jq_support:
|
||||||
with pytest.raises(html_tools.JSONNotFound) as e_info:
|
with pytest.raises(html_tools.JSONNotFound) as e_info:
|
||||||
html_tools.extract_json_as_string('COMPLETE GIBBERISH, NO JSON!', "jq:.id")
|
html_tools.extract_json_as_string('COMPLETE GIBBERISH, NO JSON!', "jq:.id")
|
||||||
|
|
||||||
@@ -271,6 +278,7 @@ def test_check_jsonpath_filter(client, live_server):
|
|||||||
check_json_filter('json:boss.name', client, live_server)
|
check_json_filter('json:boss.name', client, live_server)
|
||||||
|
|
||||||
def test_check_jq_filter(client, live_server):
|
def test_check_jq_filter(client, live_server):
|
||||||
|
if jq_support:
|
||||||
check_json_filter('jq:.boss.name', client, live_server)
|
check_json_filter('jq:.boss.name', client, live_server)
|
||||||
|
|
||||||
def check_json_filter_bool_val(json_filter, client, live_server):
|
def check_json_filter_bool_val(json_filter, client, live_server):
|
||||||
@@ -329,6 +337,7 @@ def test_check_jsonpath_filter_bool_val(client, live_server):
|
|||||||
check_json_filter_bool_val("json:$['available']", client, live_server)
|
check_json_filter_bool_val("json:$['available']", client, live_server)
|
||||||
|
|
||||||
def test_check_jq_filter_bool_val(client, live_server):
|
def test_check_jq_filter_bool_val(client, live_server):
|
||||||
|
if jq_support:
|
||||||
check_json_filter_bool_val("jq:.available", client, live_server)
|
check_json_filter_bool_val("jq:.available", client, live_server)
|
||||||
|
|
||||||
# Re #265 - Extended JSON selector test
|
# Re #265 - Extended JSON selector test
|
||||||
@@ -408,4 +417,5 @@ def test_check_jsonpath_ext_filter(client, live_server):
|
|||||||
check_json_ext_filter('json:$[?(@.status==Sold)]', client, live_server)
|
check_json_ext_filter('json:$[?(@.status==Sold)]', client, live_server)
|
||||||
|
|
||||||
def test_check_jq_ext_filter(client, live_server):
|
def test_check_jq_ext_filter(client, live_server):
|
||||||
|
if jq_support:
|
||||||
check_json_ext_filter('jq:.[] | select(.status | contains("Sold"))', client, live_server)
|
check_json_ext_filter('jq:.[] | select(.status | contains("Sold"))', client, live_server)
|
||||||
@@ -159,5 +159,10 @@ def live_server_setup(live_server):
|
|||||||
ret = " ".join([auth.username, auth.password, auth.type])
|
ret = " ".join([auth.username, auth.password, auth.type])
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
# Just return some GET var
|
||||||
|
@live_server.app.route('/test-return-query', methods=['GET'])
|
||||||
|
def test_return_query():
|
||||||
|
return request.query_string
|
||||||
|
|
||||||
live_server.start()
|
live_server.start()
|
||||||
|
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ def test_visual_selector_content_ready(client, live_server):
|
|||||||
live_server_setup(live_server)
|
live_server_setup(live_server)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
# Add our URL to the import page, maybe better to use something we control?
|
# Add our URL to the import page, because the docker container (playwright/selenium) wont be able to connect to our usual test url
|
||||||
# We use an external URL because the docker container is too difficult to setup to connect back to the pytest socket
|
test_url = "https://changedetection.io/ci-test/test-runjs.html"
|
||||||
test_url = 'https://news.ycombinator.com'
|
|
||||||
res = client.post(
|
res = client.post(
|
||||||
url_for("form_quick_watch_add"),
|
url_for("form_quick_watch_add"),
|
||||||
data={"url": test_url, "tag": '', 'edit_and_watch_submit_button': 'Edit > Watch'},
|
data={"url": test_url, "tag": '', 'edit_and_watch_submit_button': 'Edit > Watch'},
|
||||||
@@ -25,13 +25,27 @@ def test_visual_selector_content_ready(client, live_server):
|
|||||||
|
|
||||||
res = client.post(
|
res = client.post(
|
||||||
url_for("edit_page", uuid="first", unpause_on_save=1),
|
url_for("edit_page", uuid="first", unpause_on_save=1),
|
||||||
data={"css_filter": ".does-not-exist", "url": test_url, "tag": "", "headers": "", 'fetch_backend': "html_webdriver"},
|
data={
|
||||||
|
"url": test_url,
|
||||||
|
"tag": "",
|
||||||
|
"headers": "",
|
||||||
|
'fetch_backend': "html_webdriver",
|
||||||
|
'webdriver_js_execute_code': 'document.querySelector("button[name=test-button]").click();'
|
||||||
|
},
|
||||||
follow_redirects=True
|
follow_redirects=True
|
||||||
)
|
)
|
||||||
assert b"unpaused" in res.data
|
assert b"unpaused" in res.data
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
wait_for_all_checks(client)
|
wait_for_all_checks(client)
|
||||||
uuid = extract_UUID_from_client(client)
|
uuid = extract_UUID_from_client(client)
|
||||||
|
|
||||||
|
# Check the JS execute code before extract worked
|
||||||
|
res = client.get(
|
||||||
|
url_for("preview_page", uuid="first"),
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
assert b'I smell JavaScript' in res.data
|
||||||
|
|
||||||
assert os.path.isfile(os.path.join('test-datastore', uuid, 'last-screenshot.png')), "last-screenshot.png should exist"
|
assert os.path.isfile(os.path.join('test-datastore', uuid, 'last-screenshot.png')), "last-screenshot.png should exist"
|
||||||
assert os.path.isfile(os.path.join('test-datastore', uuid, 'elements.json')), "xpath elements.json data should exist"
|
assert os.path.isfile(os.path.join('test-datastore', uuid, 'elements.json')), "xpath elements.json data should exist"
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ services:
|
|||||||
# Respect proxy_pass type settings, `proxy_set_header Host "localhost";` and `proxy_set_header X-Forwarded-Prefix /app;`
|
# Respect proxy_pass type settings, `proxy_set_header Host "localhost";` and `proxy_set_header X-Forwarded-Prefix /app;`
|
||||||
# More here https://github.com/dgtlmoon/changedetection.io/wiki/Running-changedetection.io-behind-a-reverse-proxy-sub-directory
|
# More here https://github.com/dgtlmoon/changedetection.io/wiki/Running-changedetection.io-behind-a-reverse-proxy-sub-directory
|
||||||
# - USE_X_SETTINGS=1
|
# - USE_X_SETTINGS=1
|
||||||
|
#
|
||||||
|
# Hides the `Referer` header so that monitored websites can't see the changedetection.io hostname.
|
||||||
|
# - HIDE_REFERER=true
|
||||||
|
|
||||||
# Comment out ports: when using behind a reverse proxy , enable networks: etc.
|
# Comment out ports: when using behind a reverse proxy , enable networks: etc.
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
flask~= 2.0
|
flask ~= 2.0
|
||||||
flask_wtf
|
flask_wtf
|
||||||
eventlet>=0.31.0
|
eventlet >= 0.31.0
|
||||||
validators
|
validators
|
||||||
timeago ~=1.0
|
timeago ~= 1.0
|
||||||
inscriptis ~= 2.2
|
inscriptis ~= 2.2
|
||||||
feedgen ~= 0.9
|
feedgen ~= 0.9
|
||||||
flask-login ~= 0.5
|
flask-login ~= 0.5
|
||||||
@@ -10,13 +10,17 @@ flask_restful
|
|||||||
pytz
|
pytz
|
||||||
|
|
||||||
# Set these versions together to avoid a RequestsDependencyWarning
|
# Set these versions together to avoid a RequestsDependencyWarning
|
||||||
requests[socks] ~= 2.26
|
# >= 2.26 also adds Brotli support if brotli is installed
|
||||||
|
brotli ~= 1.0
|
||||||
|
requests[socks] ~= 2.28
|
||||||
|
|
||||||
urllib3 > 1.26
|
urllib3 > 1.26
|
||||||
chardet > 2.3.0
|
chardet > 2.3.0
|
||||||
|
|
||||||
wtforms ~= 3.0
|
wtforms ~= 3.0
|
||||||
jsonpath-ng ~= 1.5.3
|
jsonpath-ng ~= 1.5.3
|
||||||
jq ~= 1.3.0
|
|
||||||
|
# jq not available on Windows so must be installed manually
|
||||||
|
|
||||||
# Notification library
|
# Notification library
|
||||||
apprise ~= 1.1.0
|
apprise ~= 1.1.0
|
||||||
@@ -42,4 +46,9 @@ selenium ~= 4.1.0
|
|||||||
# need to revisit flask login versions
|
# need to revisit flask login versions
|
||||||
werkzeug ~= 2.0.0
|
werkzeug ~= 2.0.0
|
||||||
|
|
||||||
|
# Templating, so far just in the URLs but in the future can be for the notifications also
|
||||||
|
jinja2
|
||||||
|
jinja2-time
|
||||||
|
|
||||||
# playwright is installed at Dockerfile build time because it's not available on all platforms
|
# playwright is installed at Dockerfile build time because it's not available on all platforms
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user