WIP
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Has been cancelled
ChangeDetection.io Container Build Test / Build linux/amd64 (alpine) (push) Has been cancelled
ChangeDetection.io Container Build Test / Build linux/arm64 (alpine) (push) Has been cancelled
ChangeDetection.io Container Build Test / Build linux/amd64 (main) (push) Has been cancelled
ChangeDetection.io Container Build Test / Build linux/arm/v7 (main) (push) Has been cancelled
ChangeDetection.io Container Build Test / Build linux/arm/v8 (main) (push) Has been cancelled
ChangeDetection.io Container Build Test / Build linux/arm64 (main) (push) Has been cancelled
ChangeDetection.io App Test / lint-code (push) Has been cancelled
ChangeDetection.io App Test / lint-translations (push) Has been cancelled
ChangeDetection.io App Test / lint-template-i18n (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built package works basically. (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-10 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-11 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-12 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-13 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-14 (push) Has been cancelled

This commit is contained in:
dgtlmoon
2026-07-16 23:32:02 +02:00
parent 70dd20b12c
commit b853dd340c
5 changed files with 49 additions and 6 deletions
@@ -42,6 +42,19 @@ def _base_fetchers(datastore):
return out
def _autocomplete_choices():
"""(locales, timezones) for the datalist autocompletes - same sources the FetcherConfig
validators use (babel CLDR + stdlib zoneinfo)."""
from zoneinfo import available_timezones
timezones = sorted(available_timezones())
try:
from babel.localedata import locale_identifiers
locales = sorted({lid.replace('_', '-') for lid in locale_identifiers()})
except Exception:
locales = ['en-US', 'en-GB', 'de-DE', 'fr-FR', 'es-ES', 'it-IT', 'ja-JP', 'zh-CN', 'pt-BR', 'nl-NL']
return locales, timezones
def _caps_for(base_name):
"""Capability dict for an engine, so the form renders only the fields it can honour
(e.g. html_requests has no screenshots -> no viewport/locale/timezone)."""
@@ -131,8 +144,10 @@ def construct_blueprint(datastore: ChangeDetectionStore):
flash(gettext("Browser added"))
return redirect(url_for('ui.browser_config.browsers_overview'))
locale_choices, timezone_choices = _autocomplete_choices()
return render_template("browser-config-form.html", form=form, mode='add',
base_fetcher=base_fetcher, base_label=base_label, caps=caps.model_dump(),
locale_choices=locale_choices, timezone_choices=timezone_choices,
form_action=url_for('ui.browser_config.browser_config_add', base_fetcher=base_fetcher))
@browser_config_blueprint.route("/browsers/edit/<string:config_id>", methods=['GET', 'POST'])
@@ -173,10 +188,12 @@ def construct_blueprint(datastore: ChangeDetectionStore):
else:
form = BrowserOptionsForm(data=_entry_to_formdata(entry) if entry else {'label': base_label})
locale_choices, timezone_choices = _autocomplete_choices()
return render_template("browser-config-form.html", form=form, mode='edit',
config_id=config_id, is_builtin=is_builtin,
base_fetcher=base, base_label=base_label,
caps=_caps_for(base) if base else {},
locale_choices=locale_choices, timezone_choices=timezone_choices,
form_action=url_for('ui.browser_config.browser_config_edit', config_id=config_id))
@browser_config_blueprint.route("/browsers/remove/<string:config_id>", methods=['POST'])
@@ -56,9 +56,9 @@ class BrowserOptionsForm(Form):
validators.Optional(), validators.NumberRange(min=1, max=10000)])
locale = StringField(_l('Locale'), validators=[validators.Optional(), validators.Length(max=35)],
render_kw={"placeholder": "de-DE"})
render_kw={"placeholder": "de-DE", "list": "locale-datalist", "autocomplete": "off"})
timezone_id = StringField(_l('Timezone'), validators=[validators.Optional(), validators.Length(max=64)],
render_kw={"placeholder": "Europe/Berlin"})
render_kw={"placeholder": "Europe/Berlin", "list": "timezone-datalist", "autocomplete": "off"})
screenshot_format = SelectField(_l('Screenshot format'), choices=SCREENSHOT_FORMATS, default='JPEG')
@@ -44,11 +44,17 @@
<legend>{{ _('Language & timezone') }}</legend>
<div class="pure-control-group">
{{ render_field(form.locale) }}
<span class="pure-form-message-inline">{{ _('e.g. <code>de-DE</code>, <code>en-GB</code>.')|safe }}</span>
<datalist id="locale-datalist">
{% for l in locale_choices or [] %}<option value="{{ l }}"></option>{% endfor %}
</datalist>
<span class="pure-form-message-inline">{{ _('Start typing to search - e.g. <code>de-DE</code>, <code>en-GB</code>.')|safe }}</span>
</div>
<div class="pure-control-group">
{{ render_field(form.timezone_id) }}
<span class="pure-form-message-inline">{{ _('e.g. <code>Europe/Berlin</code>.')|safe }}</span>
<datalist id="timezone-datalist">
{% for tz in timezone_choices or [] %}<option value="{{ tz }}"></option>{% endfor %}
</datalist>
<span class="pure-form-message-inline">{{ _('Start typing to search - e.g. <code>Europe/Berlin</code>.')|safe }}</span>
</div>
</fieldset>
{% endif %}
@@ -10,6 +10,7 @@ Only registered when the playwright library is importable (see register_builtin_
Cross-platform temp isolation: a per-fetch temp dir with best-effort cleanup (tolerates the
Windows file-lock case). Browser processes are separate OS processes reclaimed on close().
"""
import os
import shutil
import tempfile
@@ -37,6 +38,9 @@ class fetcher(playwright_fetcher):
async def _get_browser(self, browser_type):
# Dedicated per-fetch dir; tempfile respects TMPDIR/%TEMP% so it's cross-platform.
self._local_tmp_dir = tempfile.mkdtemp(prefix='cdio-playwright-')
engine = self._resolve_browser_type_name()
logger.info(f"html_playwright_builtin: launching LOCAL headless '{engine}' for watch "
f"{getattr(self, 'watch_uuid', None)} - temp/downloads dir: {self._local_tmp_dir}")
return await browser_type.launch(headless=True, downloads_path=self._local_tmp_dir)
async def quit(self, watch=None):
@@ -45,11 +49,20 @@ class fetcher(playwright_fetcher):
try:
await super().quit(watch=watch)
finally:
tmp = self._local_tmp_dir
bc = getattr(self, 'browser_config', None)
delete = True if bc is None else bool(getattr(bc, 'delete_created_files', True))
if self._local_tmp_dir and delete:
if not tmp:
logger.info("html_playwright_builtin: no local temp dir to clean up")
elif delete:
existed = os.path.isdir(tmp)
# ignore_errors tolerates Windows file locks / an already-removed dir.
shutil.rmtree(self._local_tmp_dir, ignore_errors=True)
shutil.rmtree(tmp, ignore_errors=True)
logger.info(f"html_playwright_builtin: cleaned up temp browser dir {tmp} "
f"(existed={existed}, removed={not os.path.isdir(tmp)})")
else:
logger.info(f"html_playwright_builtin: keeping temp browser dir {tmp} "
f"(delete_created_files is off)")
self._local_tmp_dir = None
+7
View File
@@ -222,6 +222,13 @@ class difference_detection_processor():
# Inject the resolved per-watch browser behaviour; fetchers that read it apply what
# they can, others ignore it. Never None so consumers can read attributes freely.
self.fetcher.browser_config = browser_config
try:
logger.debug(
f"Watch {self.watch.get('uuid')} fetch: backend='{prefer_fetch_backend}' "
f"browser_config={browser_config.model_dump() if browser_config else None}"
)
except Exception as e:
logger.debug(f"Could not log browser_config: {e}")
if self.watch.has_browser_steps:
self.fetcher.browser_steps = browser_steps_get_valid_steps(self.watch.get('browser_steps', []))