Migrate from Poetry to uv and adopt src layout - #302
Conversation
- Move the package to src/film2trello/ (src layout) - Convert pyproject.toml to PEP 621 with uv dependency groups and a hatchling build backend; replace poetry.lock with uv.lock - Add .python-version pinning 3.11 - Rewrite the Dockerfile to install deps with uv (frozen, no dev), building the project from the src layout - Switch both GitHub Actions workflows from Poetry to astral-sh/setup-uv - Update .dockerignore/.gitignore for uv's .venv (keep README.md in the build context, required by hatchling) and point pytest testpaths at tests/ - Update README setup/development instructions to uv Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ENNdCPoC2tTAdsnS2JSPK9
|
Warning Review limit reached
Next review available in: 36 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughThe project migrates from Poetry to Hatchling and uv. It adds HTTP scraping, CSFD parsing, Trello synchronization, inbox processing, a Telegram bot, and a Click CLI. Docker and CI workflows now use uv. ChangesFilm processing and deployment stack
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant TelegramUser
participant TelegramBot
participant FilmProcessor
participant TrelloAPI
TelegramUser->>TelegramBot: send film link
TelegramBot->>FilmProcessor: process message
FilmProcessor->>TrelloAPI: create or update film card
FilmProcessor-->>TelegramBot: return progress or error
TelegramBot-->>TelegramUser: send status response
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 11
🧹 Nitpick comments (8)
src/film2trello/bot.py (1)
140-152: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRename
helpand annotatesanitize.
helpshadows the builtin for the whole module.get_help_textstates the purpose and removes the shadowing.sanitizealso has no return annotation; add-> str.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/film2trello/bot.py` around lines 140 - 152, Rename the help function to get_help_text and update all references to it so the module no longer shadows the builtin. Add a -> str return annotation to sanitize while preserving its existing behavior.src/film2trello/core.py (2)
34-43: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winCheck board membership before scraping.
check_usernameruns afterget_csfd_pages. Scraping several CSFD pages with retries is the slowest step in this flow. If the user is not a board member, that work is discarded. Move thecheck_usernamecall ahead of the scraping step so the cheap authorization check fails fast.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/film2trello/core.py` around lines 34 - 43, Move the check_username authorization call in the main film-processing flow to immediately after the Trello/API setup and before get_csfd_pages and get_film scraping. Preserve the existing username, trello_api, and board_id arguments and keep the subsequent scraping and list-analysis behavior unchanged.
142-150: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winDuplicate duration labels reach Trello.
prepare_duration_labelsmaps each duration to a bracket, so durations in one bracket produce identical label dicts. For example[100, 105]yields two2hentries.get_missing_labelsonly removes labels already on the card, soupdate_card_labelsposts the same label twice concurrently. The duplicate then relies on the "label is already on the card" text match to be swallowed. Deduplicate the labels here.♻️ Proposed change
def get_labels(film: Film) -> list[dict[str, str]]: labels = trello.prepare_duration_labels(film["durations"]) if film.get("kvifftv_url"): labels.append(trello.KVIFFTV_LABEL) if film.get("netflix_url"): labels.append(trello.NETFLIX_LABEL) if film["is_tvshow"]: labels.append(trello.TVSHOW_LABEL) - return labels + return list({label["name"]: label for label in labels}.values())🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/film2trello/core.py` around lines 142 - 150, The get_labels function returns duplicate duration label dictionaries when multiple durations map to the same bracket. Deduplicate the labels produced by trello.prepare_duration_labels before appending the optional KVIFFTV_LABEL, NETFLIX_LABEL, and TVSHOW_LABEL entries, while preserving the existing label contents and order.src/film2trello/trello.py (1)
49-57: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueCorrect the return annotation of the decorator wrapper.
wrapperisasync def, so-> Coroutinestates that awaiting it yields a coroutine. The wrapper returns the value ofawait fn(...).src/film2trello/http.pylines 126-132 repeat this pattern. Use aTypeVarfor the result, or annotate the wrapper result as the awaited type.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/film2trello/trello.py` around lines 49 - 57, Update the return annotation of the async wrapper inside with_trello_api to represent the decorated function’s awaited result rather than Coroutine; use a TypeVar to preserve the generic result type, and apply the same correction to the repeated wrapper pattern in http.py.src/film2trello/cli.py (1)
19-26: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
required=Truehas no effect together withdefault.Click satisfies the requirement with the default value, so this option can never be reported as missing. Choose one behavior: keep
default="zmyDOaFL"and droprequired=True, or keeprequired=Trueand move the board ID to anenvvar.♻️ Proposed change
board_id_option = click.option( "-b", "--board", "board_id", - required=True, help="Trello board ID", default="zmyDOaFL", + show_default=True, + envvar="TRELLO_BOARD_ID", )🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/film2trello/cli.py` around lines 19 - 26, The board_id_option configuration marks the option as required while also supplying a default, making the requirement ineffective. Choose the intended behavior: remove required=True if retaining default="zmyDOaFL", or remove the default and provide the board ID through an envvar while preserving required=True.src/film2trello/http.py (3)
146-152: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRemove or enable the commented-out retry parameters.
Lines 149-151 are debug artifacts. Without them, stamina uses its default wait schedule, which starts near one second and can add several seconds per page. If the shorter waits are the intended behavior, enable them.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/film2trello/http.py` around lines 146 - 152, Clean up the retry configuration on the AntiBotError handler by removing the commented-out wait_initial, wait_max, and wait_jitter artifacts or enabling them if the shorter retry schedule is intended. Keep the existing retry attempts and exception behavior unchanged.
111-117: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winHeader composition is split between the client and the request, so profiles mix.
The root cause is one design choice:
BROWSER_PROFILESentries are partial overlays onBASE_HEADERS, and headers are applied in two places. httpx merges client headers with request headers per name, so any key a profile omits keeps the value of the previously selected profile or of the Chrome-oriented base set. A Firefox request therefore carries a ChromeSec-Ch-Uavalue andSec-Ch-Ua-Mobile: ?0, which real Firefox never sends.
src/film2trello/http.py#L111-L117: select the profile in one place. Either dropheaders=get_default_headers()here and always send a complete header set per request, or select the profile once per client and stop overriding it inget_html.src/film2trello/http.py#L76-L84: make each profile a complete header set, or let a profile declare which base headers to remove, so the Firefox profile sends noSec-Ch-Ua*header.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/film2trello/http.py` around lines 111 - 117, Unify header profile selection so client-level defaults cannot leak into requests: in src/film2trello/http.py lines 111-117, update get_scraper and the related get_html flow to apply one complete profile per request or client without conflicting overrides; in lines 76-84, update BROWSER_PROFILES so each profile is complete or explicitly removes unsupported base headers, ensuring Firefox sends no Sec-Ch-Ua* headers and profiles cannot retain values from another profile.
156-161: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsolidate the Anubis selector in one helper.
csfd.is_antibot_pagedefines a CSFD-only selector (head script#anubis_challenge), whilehttp.get_htmldefines a different selector (script#anubis_challenge). Move one shared selector intohttp.pyand use it for the detection here, or delete the unused CSFD helper.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/film2trello/http.py` around lines 156 - 161, Consolidate Anubis detection around a single shared selector: define or reuse one helper in http.py, update get_html’s page_html challenge check to use it, and remove the redundant CSFD-specific helper if it becomes unused. Preserve detection of the intended Anubis challenge script without maintaining divergent selectors.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Dockerfile`:
- Around line 17-19: Create a dedicated non-root service user in the Dockerfile,
grant it ownership or write access only to the runtime paths required by
film2trello, then switch to that user with USER before the existing CMD
["film2trello", "bot"].
In `@src/film2trello/bot.py`:
- Around line 128-137: Escape exc_text for HTML after sanitization and before
interpolating it into the reply_html call in the exception handler, preserving
the existing error text and help output. Also HTML-escape username wherever it
is interpolated into the generated help markup, using the existing help function
or its caller as the nearest relevant symbol.
In `@src/film2trello/cli.py`:
- Around line 110-112: Update the HTTP error handler around HTTPStatusError to
sanitize the exception and response details before passing them to
logger.exception, reusing the existing sanitize helper from bot.py or its shared
equivalent; ensure Trello key and token values never appear in CLI logs. Prefer
moving get_trello_api authentication from query parameters into an Authorization
header so credentials are also excluded from exception messages and request
URLs.
- Around line 62-70: Update the Click users option in the CLI definition to
remove the hardcoded default assignments and make the option explicitly
required. Load the default user list from the designated environment-variable
configuration instead of embedding Telegram IDs or Trello usernames in the
source, while preserving parse_user handling and multiple-value support.
In `@src/film2trello/core.py`:
- Around line 175-204: Wrap each card’s processing body in the loop around the
visible per-card work, including CSFD lookup, film parsing, Trello updates, and
attachment handling, with a try/except Exception boundary. In the exception
handler, log the failure together with trello.get_card_url(card["id"]) and allow
execution to continue to the next card so the later sorting step still runs.
In `@src/film2trello/csfd.py`:
- Around line 38-42: Update get_base_url to validate that the canonical or
og:url element exists and contains a non-empty URL attribute; when neither valid
source is available, raise a descriptive error instead of propagating IndexError
or returning None. Preserve the canonical-first fallback to og:url and ensure
the function always returns str.
- Around line 153-157: Guard the href check in the overview URL selection logic
so a missing href on tabs[0] does not call startswith on None. Update the code
around ensure_overview_url and the tabs lookup to apply the same missing-href
filtering used by the earlier loop, while preserving the fallback to base_url.
- Around line 87-92: Update parse_poster_url to safely handle srcset entries
without valid numeric descriptors: ignore invalid descriptor tokens, avoid
calling max on an empty parsed mapping, and return an appropriate fallback or no
URL. Build each URL without blindly prepending https:, preserving absolute URLs
while adding the scheme only for protocol-relative entries so
update_card_attachments can skip unusable posters instead of failing.
In `@src/film2trello/http.py`:
- Around line 88-100: Remove the "Connection": "keep-alive" entry from the
BASE_HEADERS default header mapping, leaving all other browser-emulation headers
unchanged.
In `@src/film2trello/trello.py`:
- Around line 283-287: Update has_poster to access each attachment’s previews
defensively with attachment.get("previews"), treating a missing or empty value
as false so one attachment without the field cannot abort the card update.
- Around line 154-163: Update the poster-update branch around has_poster,
scraper.get, and trello_api.post so poster HTTP failures from scraper.get are
caught within the poster step, including raise_on_error-generated
httpx.HTTPStatusError. Make all poster retrieval, thumbnail creation, and
attachment-update failures fail-soft by returning the existing error result
instead of propagating into process_inbox, while preserving normal processing of
subsequent cards.
---
Nitpick comments:
In `@src/film2trello/bot.py`:
- Around line 140-152: Rename the help function to get_help_text and update all
references to it so the module no longer shadows the builtin. Add a -> str
return annotation to sanitize while preserving its existing behavior.
In `@src/film2trello/cli.py`:
- Around line 19-26: The board_id_option configuration marks the option as
required while also supplying a default, making the requirement ineffective.
Choose the intended behavior: remove required=True if retaining
default="zmyDOaFL", or remove the default and provide the board ID through an
envvar while preserving required=True.
In `@src/film2trello/core.py`:
- Around line 34-43: Move the check_username authorization call in the main
film-processing flow to immediately after the Trello/API setup and before
get_csfd_pages and get_film scraping. Preserve the existing username,
trello_api, and board_id arguments and keep the subsequent scraping and
list-analysis behavior unchanged.
- Around line 142-150: The get_labels function returns duplicate duration label
dictionaries when multiple durations map to the same bracket. Deduplicate the
labels produced by trello.prepare_duration_labels before appending the optional
KVIFFTV_LABEL, NETFLIX_LABEL, and TVSHOW_LABEL entries, while preserving the
existing label contents and order.
In `@src/film2trello/http.py`:
- Around line 146-152: Clean up the retry configuration on the AntiBotError
handler by removing the commented-out wait_initial, wait_max, and wait_jitter
artifacts or enabling them if the shorter retry schedule is intended. Keep the
existing retry attempts and exception behavior unchanged.
- Around line 111-117: Unify header profile selection so client-level defaults
cannot leak into requests: in src/film2trello/http.py lines 111-117, update
get_scraper and the related get_html flow to apply one complete profile per
request or client without conflicting overrides; in lines 76-84, update
BROWSER_PROFILES so each profile is complete or explicitly removes unsupported
base headers, ensuring Firefox sends no Sec-Ch-Ua* headers and profiles cannot
retain values from another profile.
- Around line 156-161: Consolidate Anubis detection around a single shared
selector: define or reuse one helper in http.py, update get_html’s page_html
challenge check to use it, and remove the redundant CSFD-specific helper if it
becomes unused. Preserve detection of the intended Anubis challenge script
without maintaining divergent selectors.
In `@src/film2trello/trello.py`:
- Around line 49-57: Update the return annotation of the async wrapper inside
with_trello_api to represent the decorated function’s awaited result rather than
Coroutine; use a TypeVar to preserve the generic result type, and apply the same
correction to the repeated wrapper pattern in http.py.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 19e9d809-5931-4fcb-8803-f11dd449ce45
⛔ Files ignored due to path filters (2)
poetry.lockis excluded by!**/*.lockuv.lockis excluded by!**/*.lock
📒 Files selected for processing (15)
.dockerignore.github/workflows/build.yml.github/workflows/scripts.yml.gitignore.python-versionDockerfileREADME.mdpyproject.tomlsrc/film2trello/__init__.pysrc/film2trello/bot.pysrc/film2trello/cli.pysrc/film2trello/core.pysrc/film2trello/csfd.pysrc/film2trello/http.pysrc/film2trello/trello.py
There was a problem hiding this comment.
Caution
Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.
🛑 Comments failed to post (10)
src/film2trello/bot.py (1)
128-137: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Escape the exception text before sending it as HTML.
exc_textis interpolated into a message sent withreply_html. Exception messages routinely contain<,>, and&, for example an lxml parser error that quotes markup, or a URL with&between query parameters. Telegram then rejects the message withBadRequest: Can't parse entities. That exception is raised inside theexceptblock, so it propagates out of the handler and the user receives no error report at all.🐛 Proposed fix
+from html import escape + ... except Exception as exc: logger.exception(exc) exc_text = str(exc) if secrets: exc_text = sanitize(exc_text, secrets) await update.message.reply_html( f"Stala se nějaká chyba 😢\n\n" - f"<pre>{exc_text}</pre>\n\n" + f"<pre>{escape(exc_text)}</pre>\n\n" f"{help(board_id, username)}" )Note that
usernameat line 143 is also interpolated inside<code>without escaping. It comes from operator configuration, so escaping it is defensive only.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.except Exception as exc: logger.exception(exc) exc_text = str(exc) if secrets: exc_text = sanitize(exc_text, secrets) await update.message.reply_html( f"Stala se nějaká chyba 😢\n\n" f"<pre>{escape(exc_text)}</pre>\n\n" f"{help(board_id, username)}" )🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/film2trello/bot.py` around lines 128 - 137, Escape exc_text for HTML after sanitization and before interpolating it into the reply_html call in the exception handler, preserving the existing error text and help output. Also HTML-escape username wherever it is interpolated into the generated help markup, using the existing help function or its caller as the nearest relevant symbol.src/film2trello/cli.py (2)
62-70: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash # Check whether these identifiers already appear elsewhere and whether the repository is public. rg -n --hidden -g '!.git' '119318534|175995069|honzajavorek:|zmyDOaFL' || true gh repo view --json visibility,nameWithOwner 2>/dev/null || echo "gh unavailable"Repository: honzajavorek/film2trello
Length of output: 162
🏁 Script executed:
#!/bin/bash set -euo pipefail echo "== file outline ==" ast-grep outline src/film2trello/cli.py || true echo "== cli.py relevant content ==" wc -l src/film2trello/cli.py cat -n src/film2trello/cli.py | sed -n '1,140p' echo "== bot.py run caller context ==" rg -n --hidden -g '!.git' 'from .*cli import|\.run\(|def cli|`@click`\.command|click' src/film2trello || true echo "== repo git status / remote public-ish files ==" git ls-files | grep -E '(^|/)(README|setup\.py|pyproject\.toml|poetry\.lock|uv\.lock|requirements|\.github)' || trueRepository: honzajavorek/film2trello
Length of output: 4470
🌐 Web query:
click option required True envvar how it parses required option envvar💡 Result:
In the Click library for Python, configuring an option with both required=True and an envvar allows the option to be satisfied by either a command-line argument or the specified environment variable [1][2]. If neither the command-line argument nor the environment variable is provided, Click will raise a missing parameter error, effectively enforcing the requirement [3]. Key details on how this interaction works: 1. Satisfaction Priority: Click treats the environment variable as a valid source for the parameter value [1][4]. When required=True is set, Click checks if a value has been provided through any of the available input methods (command-line, environment variable, or default) before proceeding [1][2]. If no value is found after checking these, it raises a usage error [3]. 2. Environment Variable Parsing: Environment variables are always read as strings [1][5]. If the option requires specific types (like integers, files, or paths), Click will attempt to convert the environment variable string into the appropriate type using the defined type parameter [1][5]. 3. Multiple Values: For options defined with multiple=True or nargs > 1, Click uses the ParamType.split_envvar_value method to parse the environment variable string [1][6]. The default behavior is to split the string based on whitespace, though specific types like Path or File have their own splitting rules based on the operating system (e.g., splitting on colons on Unix or semicolons on Windows) [7][8]. 4. Flag Options: Parsing for flags can be more complex. If an option is a boolean flag, Click looks for specific values (e.g., "true", "1", "yes", "on", "t", "y" for activation) to determine the flag's state [5][6]. If an envvar is provided for a flag, it must be set to one of these recognized values to activate the flag [5]. For debugging purposes, you can use show_envvar=True in your
@click.optiondefinition to display the associated environment variable name in help pages and error messages, which clarifies for the user that an environment variable can be used to satisfy the requirement [4][3].Citations:
- 1: https://click.palletsprojects.com/en/latest/options/
- 2: https://click.palletsprojects.com/en/stable/arguments/
- 3: https://github.com/pallets/click/blob/2d3b2435/tests/test_options.py
- 4: https://click.palletsprojects.com/en/latest/api/
- 5: https://click.palletsprojects.com/en/stable/options/
- 6: https://github.com/pallets/click/blob/cdab890e/docs/options.md
- 7: https://docs.devnet-academy.com/docs/click_v7.1.2/options.html
- 8: https://dokk.org/documentation/pallets-click/7.1.2/options/
Sensitive Data Exposure (CWE-359)
Move the default user assignments out of the source.
The
usersdefault hardcodes Telegram IDs and Trello usernames. Read this list from an environment variable and require the option explicitly.🛡️ Proposed change
`@click.option`( "-U", "--user", "users", help="User in format <telegram_id>:<trello_username>", type=parse_user, multiple=True, - default=["119318534:honzajavorek", "175995069:zuzka"], + required=True, + envvar="FILM2TRELLO_USERS", )📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.`@click.option`( "-U", "--user", "users", help="User in format <telegram_id>:<trello_username>", type=parse_user, multiple=True, required=True, envvar="FILM2TRELLO_USERS", )🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/film2trello/cli.py` around lines 62 - 70, Update the Click users option in the CLI definition to remove the hardcoded default assignments and make the option explicitly required. Load the default user list from the designated environment-variable configuration instead of embedding Telegram IDs or Trello usernames in the source, while preserving parse_user handling and multiple-value support.
110-112: 🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win
Sensitive Data Exposure (CWE-532): Insertion of Sensitive Information into Log File
Reachability: External
The Trello key and token are written to the log on any API error.
get_trello_apiinsrc/film2trello/trello.pylines 38-46 attacheskeyandtokenas query parameters on every request. httpx builds theHTTPStatusErrormessage from the request URL, sostr(exc)contains both secrets verbatim. This handler logs that message, andexc.response.textmay repeat request details. Any 4xx or 5xx response, for example a 401 from an expired token, writes live credentials into the log.
src/film2trello/bot.pyline 149 already definessanitizefor this purpose. The CLI path does not use it.🛡️ Proposed fix
+from film2trello.bot import sanitize + ... except HTTPStatusError as exc: - logger.exception(f"{exc}:\n\n{exc.response.text}\n\n") - raise click.Abort() + secrets = [trello_key, trello_token] + message = sanitize(f"{exc}:\n\n{exc.response.text}\n\n", secrets) + logger.error(message) + raise click.Abort() from excA stronger fix is to send the credentials in an
Authorizationheader instead of the query string, so they never appear in an exception message or in server-side access logs.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.except HTTPStatusError as exc: secrets = [trello_key, trello_token] message = sanitize(f"{exc}:\n\n{exc.response.text}\n\n", secrets) logger.error(message) raise click.Abort() from exc🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/film2trello/cli.py` around lines 110 - 112, Update the HTTP error handler around HTTPStatusError to sanitize the exception and response details before passing them to logger.exception, reusing the existing sanitize helper from bot.py or its shared equivalent; ensure Trello key and token values never appear in CLI logs. Prefer moving get_trello_api authentication from query parameters into an Authorization header so credentials are also excluded from exception messages and request URLs.src/film2trello/core.py (1)
175-204: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
One bad card aborts the whole inbox run.
The loop has no error isolation. A single card can raise from several places:
parse_titleraisesValueErrorwhen the year is missing,get_base_urlandparse_durationsraiseIndexErroron unexpected markup, andhttp.get_htmlraisesAntiBotErrorafter the retries are exhausted. Any of these ends the run. Cards later in the list are never refreshed, and the sorting step at lines 206-212 never runs, so the inbox is left partially updated.Wrap the per-card work in
try/except Exception, log the failure with the card URL, and continue with the next card.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/film2trello/core.py` around lines 175 - 204, Wrap each card’s processing body in the loop around the visible per-card work, including CSFD lookup, film parsing, Trello updates, and attachment handling, with a try/except Exception boundary. In the exception handler, log the failure together with trello.get_card_url(card["id"]) and allow execution to continue to the next card so the later sorting step still runs.src/film2trello/csfd.py (3)
38-42: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
get_base_urlcan returnNoneor raise an opaqueIndexError.The declared return type is
str. Two paths break it. If the page has noog:urlelement either,cssselect(...)[0]raisesIndexError. If the element exists without the attribute,.get()returnsNone, andparse_target_urlthen callsmake_links_absolute(None), which fails with aTypeError. Raise a descriptive error instead.♻️ Proposed change
def get_base_url(csfd_html: html.HtmlElement) -> str: - try: - return csfd_html.cssselect("link[rel='canonical']")[0].get("href") - except IndexError: - return csfd_html.cssselect("meta[property='og:url']")[0].get("content") + for selector, attribute in ( + ("link[rel='canonical']", "href"), + ("meta[property='og:url']", "content"), + ): + for element in csfd_html.cssselect(selector): + if value := element.get(attribute): + return value + raise ValueError("Could not determine the base URL of the page")📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.def get_base_url(csfd_html: html.HtmlElement) -> str: for selector, attribute in ( ("link[rel='canonical']", "href"), ("meta[property='og:url']", "content"), ): for element in csfd_html.cssselect(selector): if value := element.get(attribute): return value raise ValueError("Could not determine the base URL of the page")🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/film2trello/csfd.py` around lines 38 - 42, Update get_base_url to validate that the canonical or og:url element exists and contains a non-empty URL attribute; when neither valid source is available, raise a descriptive error instead of propagating IndexError or returning None. Preserve the canonical-first fallback to og:url and ensure the function always returns str.
87-92: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
parse_poster_urlcrashes on srcset shapes that carry no descriptor.Two failure modes exist. If the
srcsetvalue has no width or density descriptor,zoomis empty,srcset_parsedis empty, andmax(srcset_parsed.keys())raisesValueError: max() arg is an empty sequence. If a descriptor token contains no digits,int(re.sub(r"\D", "", z))raisesValueErroras well.The
f"https:{url}"prefix also assumes every entry is protocol-relative. An absolute URL becomeshttps:https://…, andscraper.get(poster_url)insrc/film2trello/trello.pyline 155 then raiseshttpx.UnsupportedProtocol, whichupdate_card_attachmentsdoes not catch. The whole card update fails instead of skipping the poster.♻️ Proposed change
def parse_poster_url(csfd_html: html.HtmlElement) -> str | None: if poster_images := csfd_html.cssselect(".film-posters img"): if srcset := poster_images[0].get("srcset"): - srcset_list = re.split(r"\s+", srcset) - urls = [f"https:{url}" for url in srcset_list[::2]] - zoom = [int(re.sub(r"\D", "", z)) for z in srcset_list[1::2]] - srcset_parsed = dict(zip(zoom, urls)) - return srcset_parsed[max(srcset_parsed.keys())] + srcset_list = re.split(r"\s+", srcset.strip()) + srcset_parsed = {} + for url, descriptor in zip(srcset_list[::2], srcset_list[1::2]): + digits = re.sub(r"\D", "", descriptor) + if not digits: + continue + if url.startswith("//"): + url = f"https:{url}" + srcset_parsed[int(digits)] = url + if srcset_parsed: + return srcset_parsed[max(srcset_parsed)] + return None return None return None📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.if srcset := poster_images[0].get("srcset"): srcset_list = re.split(r"\s+", srcset.strip()) srcset_parsed = {} for url, descriptor in zip(srcset_list[::2], srcset_list[1::2]): digits = re.sub(r"\D", "", descriptor) if not digits: continue if url.startswith("//"): url = f"https:{url}" srcset_parsed[int(digits)] = url if srcset_parsed: return srcset_parsed[max(srcset_parsed)] return None🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/film2trello/csfd.py` around lines 87 - 92, Update parse_poster_url to safely handle srcset entries without valid numeric descriptors: ignore invalid descriptor tokens, avoid calling max on an empty parsed mapping, and return an appropriate fallback or no URL. Build each URL without blindly prepending https:, preserving absolute URLs while adding the scheme only for protocol-relative entries so update_card_attachments can skip unusable posters instead of failing.
153-157: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Guard against a tab element without an
href.
tabs[0].get("href")returnsNonewhen the first tab anchor has nohref.None.startswith("http")then raisesAttributeError. The loop at lines 144-148 already filters missinghrefvalues; apply the same guard here.♻️ Proposed change
if tabs := csfd_html.cssselect(".main-movie-profile .tabs a"): overview_url = tabs[0].get("href") - if overview_url.startswith("http"): + if overview_url and overview_url.startswith("http"): return ensure_overview_url(overview_url) return ensure_overview_url(base_url)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.if tabs := csfd_html.cssselect(".main-movie-profile .tabs a"): overview_url = tabs[0].get("href") if overview_url and overview_url.startswith("http"): return ensure_overview_url(overview_url) return ensure_overview_url(base_url)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/film2trello/csfd.py` around lines 153 - 157, Guard the href check in the overview URL selection logic so a missing href on tabs[0] does not call startswith on None. Update the code around ensure_overview_url and the tabs lookup to apply the same missing-href filtering used by the earlier loop, while preserving the fallback to base_url.src/film2trello/http.py (1)
88-100: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
httpx http2 Connection keep-alive header forbidden HTTP/2 h2 ProtocolError💡 Result:
The Connection header is explicitly prohibited in HTTP/2 (and HTTP/3) because these protocols manage connection lifecycles through their own framing layers rather than through hop-by-hop HTTP headers [1][2][3]. According to the HTTP/2 specification (RFC 7540), any message containing connection-specific header fields—such as Connection or Keep-Alive—must be treated as malformed, typically resulting in a protocol error [3][4]. Regarding your query about
httpxand HTTP/2: 1. Why it occurs: In some cases,httpxmay include the Connection: keep-alive header in requests—often due to internal logic or automatic header population [5]. If a server strictly adheres to the HTTP/2 specification, it will reject these requests with a PROTOCOL_ERROR [3][4][6]. 2. Visibility vs. Reality: You might observe a Connection header inresponse.request.headerswhen debugginghttpx[5]. It is important to note that this reflects the header set by the client-side logic; however, if the transport layer correctly implements HTTP/2, it should not be sending this header over the wire [7]. If you are encountering a ProtocolError, it indicates that the header was actually sent and subsequently rejected by the server or a proxy [3][6]. 3. Resolution: - Ensure you are not manually adding the Connection header to your requests. - If the header is being added automatically byhttpx, you can try to clear or override it by modifying the client's default headers (e.g.,client.headers.clear()or explicitly setting the header to an empty value if supported, though clearing is often more effective) [5]. - If you are using a proxy, ensure the proxy is configured to strip connection-specific headers before forwarding the request to the upstream HTTP/2 server, as many proxies do this automatically to avoid protocol violations [2][8]. If you are seeing this error, it is a sign that the request is effectively violating the HTTP/2 protocol, and the server is correctly refusing to process it [3].Citations:
- 1: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Connection
- 2: https://http.dev/connection
- 3: https://stackoverflow.com/questions/70403584/why-do-http-2-clients-reject-requests-containing-the-connection-header
- 4: https://httpwg.org/specs/rfc7540.html
- 5: encode/httpx#3242
- 6: linkerd/linkerd2#10090
- 7: https://bugzilla.mozilla.org/show_bug.cgi?id=1427256
- 8: golang/go#23699
🏁 Script executed:
#!/bin/bash set -euo pipefail echo "== Locate http.py and inspect relevant sections ==" fd -a 'http\.py$' . | sed 's#^\./##' echo if [ -f src/film2trello/http.py ]; then sed -n '1,140p' src/film2trello/http.py | cat -n fi echo echo "== Search for httpx/http2 usage and get_default_headers ==" rg -n "httpx|http2|Client|AsyncClient|get_default_headers|BASE_HEADERS|BROWSER_PROFILES" .Repository: honzajavorek/film2trello
Length of output: 12114
Remove
Connection: keep-alivefrom the default headers.The scraper uses HTTP/2, and
Connectionis a prohibited connection-specific header there. Depending on strictness, this can cause protocol errors or be dropped while still hurting the browser-emulation profile.♻️ Proposed change
"Cache-Control": "max-age=0", - "Connection": "keep-alive", "Sec-Ch-Ua-Mobile": "?0",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.BASE_HEADERS = { "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.7", "Accept-Language": "cs-CZ,cs;q=0.9,en-US;q=0.8,en;q=0.7", "Accept-Encoding": "gzip, deflate, br", "Cache-Control": "max-age=0", "Sec-Ch-Ua-Mobile": "?0", "Sec-Fetch-Dest": "document", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-Site": "none", "Sec-Fetch-User": "?1", "Upgrade-Insecure-Requests": "1", }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/film2trello/http.py` around lines 88 - 100, Remove the "Connection": "keep-alive" entry from the BASE_HEADERS default header mapping, leaving all other browser-emulation headers unchanged.src/film2trello/trello.py (2)
154-163: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash # Confirm the declared Pillow and httpx versions used by the project. fd -H -t f 'pyproject.toml' --exec rg -n -C2 'pillow|Pillow|httpx'Repository: honzajavorek/film2trello
Length of output: 388
🌐 Web query:
Pillow UnidentifiedImageError base class OSError Image.open exceptions💡 Result:
In the Pillow library, the UnidentifiedImageError exception is a custom exception class that inherits from OSError [1][2][3]. It is specifically raised by the PIL.Image.open function when an image file cannot be opened and identified—typically because the file format is unsupported, the file is corrupted, or it is not a valid image file [1][4][5]. Because UnidentifiedImageError is a subclass of OSError, code that catches OSError will also catch UnidentifiedImageError [6]. However, it is generally recommended to catch the specific UnidentifiedImageError to avoid masking other unrelated OS-related errors [6]. If you encounter this error while working with PNG images, it may be caused by minor data issues, such as missing chunks or checksum failures [1][7][8]. In such cases, you can attempt to work around the error by setting ImageFile.LOAD_TRUNCATED_IMAGES to True [1][7][8]. Example of handling the exception: from PIL import Image, UnidentifiedImageError try: with Image.open("path/to/image.jpg") as img: img.show except UnidentifiedImageError: print("The file could not be identified as an image.") except OSError as e: print(f"An OS error occurred: {e}")
Citations:
- 1: https://pillow.readthedocs.io/en/stable/PIL.html
- 2: https://pillow.readthedocs.io/en/stable/%5Fmodules/PIL.html
- 3: https://github.com/python-pillow/Pillow/blob/master/src/PIL/__init__.py
- 4: https://pillow.readthedocs.io/en/stable/reference/Image.html
- 5: https://pillow.readthedocs.io/en/stable/reference/Image.html?highlight=image.open
- 6: https://stackoverflow.com/questions/63656089/how-to-catch-pil-unidentifiedimageerror-in-except
- 7: python-pillow/Pillow#7349
- 8: python-pillow/Pillow#8669
🏁 Script executed:
#!/bin/bash set -euo pipefail echo "== locate trello.py ==" fd -a 'trello.py' . | sed 's#^\./##' echo "== relevant trello.py lines ==" sed -n '1,230p' src/film2trello/trello.py echo "== process_inbox usages ==" rg -n "process_inbox|update_card_attachments|update_card" srcRepository: honzajavorek/film2trello
Length of output: 7524
🏁 Script executed:
#!/bin/bash set -euo pipefail echo "== core.py relevant sections ==" sed -n '40,90p' src/film2trello/core.py sed -n '140,220p' src/film2trello/core.py echo "== raise_on_error implementation ==" fd -a 'http.py' . | while read -r f; do echo "--- $f" sed -n '1,120p' "$f" done echo "== tests around update_card_attachments ==" sed -n '1,260p' tests/test_trello.py | sed -n '1,260p' echo "== Python exception inheritance probe for installed Pillow ==" python3 - <<'PY' import sys try: import PIL from PIL import Image print("PIL installed:", getattr(PIL, "__version__", "<unknown>")) except Exception as exc: print("PIL import failed:", type(exc).__name__, exc) else: import inspect # Probe whether Open / Unsupported type exists in installed PIL.Image. for name in ["UnidentifiedImageError", "ImageFileDecodeError"]: obj = inspect.getattr_static(Image, name, None) print(f"Image.{name}:", type(obj).__name__, getattr(obj, "__name__", None)) if obj is not None: print(f" bases:", tuplegetattr = getattr(obj, "__bases__", None), bases=tuplegetattr) PYRepository: honzajavorek/film2trello
Length of output: 17385
🌐 Web query:
Pillow 12.1.1 UnidentifiedImageError source OSError💡 Result:
In Pillow (including version 12.1.1), UnidentifiedImageError is an exception class that inherits from OSError [1][2]. It is raised by PIL.Image.open when the library cannot identify or open the provided file [1][3]. Key technical details regarding this error: 1. Inheritance: Because UnidentifiedImageError is a subclass of OSError, code that catches OSError will also catch UnidentifiedImageError [4][5]. For more precise error handling, you should catch UnidentifiedImageError specifically by importing it from PIL [5]. 2. Common Causes: - File Corruption: The file may be truncated, have invalid headers, or contain checksum errors (common in PNG files) [6][7]. - Unsupported/Unknown Format: The file may be in a format that Pillow does not support or cannot automatically identify [1][4]. - Not an Image: The provided file path or stream may not point to a valid image file at all [6][8]. 3. Troubleshooting and Workarounds: - Truncated Files: If you are dealing with potentially truncated or partially corrupted images (especially PNGs), you can often allow Pillow to attempt to open them by setting ImageFile.LOAD_TRUNCATED_IMAGES to True [1][6][7]: from PIL import Image, ImageFile ImageFile.LOAD_TRUNCATED_IMAGES = True img = Image.open('your_image.png') - Debugging: If you need to understand why an image is failing, you can check if your specific Pillow version supports extra debugging parameters (such as warn_possible_formats, though this feature availability can vary by release) or use external tools like pngcheck to verify file integrity [7][9]. Always ensure you are using a compatible version of Pillow for your environment, as installation issues can sometimes lead to reports of version mismatches or unexpected behavior [8].
Citations:
- 1: https://pillow.readthedocs.io/en/stable/%5Fmodules/PIL.html
- 2: https://github.com/python-pillow/Pillow/blob/master/src/PIL/__init__.py
- 3: https://pillow.readthedocs.io/en/stable/reference/Image.html
- 4: python-pillow/Pillow#3892
- 5: https://stackoverflow.com/questions/63656089/how-to-catch-pil-unidentifiedimageerror-in-except
- 6: python-pillow/Pillow#7349
- 7: python-pillow/Pillow#7993
- 8: python-pillow/Pillow#7763
- 9: python-pillow/Pillow#8033
🌐 Web query:
Pillow 12.1.1 UnsupportedProtocol base class ValueError ImageFileDecodeError💡 Result:
The error message sequence you described is not a native behavior of the Pillow library. Specifically, UnsupportedProtocol is not an exception or base class within Pillow [1][2][3][4]. The UnsupportedProtocol exception is a known component of the httpx library [5][6][7], where it signifies an attempt to make a network request using a protocol that is not supported (e.g., trying to use ftp:// instead of http://) [5][7][8]. If you are encountering this combination of errors in your application, it is highly likely that your code is conflating two different libraries: 1. Pillow (PIL) is likely being used for image processing [9]. When it encounters corrupt or unidentifiable image files, it typically raises PIL.UnidentifiedImageError or OSError [2][4][10]. 2. A separate HTTP client (such as httpx) is being used to fetch the image data. If the URL passed to the downloader is invalid or uses an unsupported protocol, the httpx library will raise UnsupportedProtocol [5][7]. If your stack trace shows these errors together, check if your image-loading logic is attempting to download an image from a URL and subsequently passing that result to Pillow. Ensure that the URL passed to your HTTP client is valid and that you have appropriate error handling to catch httpx.UnsupportedProtocol separately from Pillow's image decoding exceptions [10].
Citations:
- 1: https://github.com/python-pillow/Pillow/blob/fdbd719da4c77c7e23e2e9e9b71d0d177f2d3369/src/PIL/ImageFile.py
- 2: https://pillow.readthedocs.io/en/stable/reference/ImageFile.html
- 3: https://pillow.readthedocs.io/en/latest/_modules/PIL/ImageFile.html
- 4: https://pillow.readthedocs.io/en/latest/reference/ImageFile.html
- 5: https://github.com/encode/httpx/blob/b5addb64/httpx/_exceptions.py
- 6: https://github.com/encode/httpx/blob/b5addb64/docs/exceptions.md
- 7: https://fossies.org/dox/httpx-0.28.1/classhttpx_1_1__exceptions_1_1UnsupportedProtocol.html
- 8: https://httpx2.pydantic.dev/exceptions/
- 9: https://pypi.org/project/pillow/12.1.1/
- 10: python-pillow/Pillow#7349
🌐 Web query:
Pillow version history ImageFileDecodeError introduced OSError UnidentifiedImageError💡 Result:
In the Pillow library, the UnidentifiedImageError was introduced in version 7.0.0 [1][2]. Before the introduction of UnidentifiedImageError, Pillow typically raised an OSError (or IOError in older Python versions) when it could not identify or open an image file [3][4]. This change was implemented to provide a specific, catchable exception for cases where image identification fails, replacing the ambiguous use of OSError for these situations [4][1]. Regarding ImageFileDecodeError, there is no public-facing exception by that specific name in the current Pillow API. Pillow continues to use OSError for errors occurring during the image decoding process [5]. Historically, the internal helper function ImageFile.raise_oserror was used to translate codec error codes into OSError instances; this function was deprecated in Pillow 10.2.0 and removed in Pillow 12.0.0, as the translation is now handled automatically [6][7][8].
Citations:
- 1: python-pillow/Pillow#4182
- 2: https://github.com/python-pillow/Pillow/blob/7ff2db143df98d12010bc09ac7c13bb8b0fddc7d/CHANGES.rst
- 3: python-pillow/Pillow#1643
- 4: python-pillow/Pillow#3892
- 5: https://pillow.readthedocs.io/en/latest/_modules/PIL/ImageFile.html
- 6: https://github.com/python-pillow/Pillow/blob/HEAD/CHANGES.rst
- 7: https://github.com/python-pillow/Pillow/blob/12.2.0/docs/deprecations.rst
- 8: https://fossies.org/diffs/Pillow/11.3.0_vs_12.0.0/src/PIL/ImageFile.py-diff.html
🌐 Web query:
Pillow 12.1.1 source Image.open OSError raise UnsupportedProtocol ImageFileDecodeError💡 Result:
There is no native Pillow exception named UnsupportedProtocol. If you are encountering an UnsupportedProtocol error while using Pillow 12.1.1, it is almost certainly being raised by a different library in your environment, most commonly the HTTP client library httpx or its dependency httpcore [1][2][3]. Pillow's own Image.open method typically raises an OSError when it fails to identify or decode an image [4], or occasionally an UnidentifiedImageError (a subclass of OSError) if the file format cannot be determined. If you are seeing UnsupportedProtocol, check your code for these common scenarios: 1. Misuse of HTTP Libraries: If you are attempting to open an image from a URL, ensure you are using a proper request/response flow. For example, if you pass a string that starts with a protocol scheme (like file://) to a function expecting a file path or a raw HTTP response, libraries like httpx will raise UnsupportedProtocol because they do not support that specific scheme [2][3]. 2. Dependency Conflicts: Since many Python projects use both Pillow and HTTP libraries (like httpx or requests), ensure that the data being passed to Image.open is a valid file-like object (e.g., io.BytesIO or an open file pointer) and not a raw URL string or an incompatible object returned by an HTTP client [5]. To debug this: - Verify the exact traceback: Look closely at the file path in the traceback to identify which library is raising the UnsupportedProtocol exception [3]. - Inspect the input: Check the object being passed to Image.open. If it is a string or a URL, you must first download the content into a buffer (e.g., using requests.get(url).content) and pass that buffer to Image.open(io.BytesIO(data)). Pillow 12.1.1, released on February 11, 2026 [6], does not contain changes that would introduce a new UnsupportedProtocol error in Image.open [7][8]. The error is likely coming from an external library interacting with your image loading process.
Citations:
- 1: https://fossies.org/dox/httpx-0.28.1/classhttpx_1_1__exceptions_1_1UnsupportedProtocol.html
- 2: mvantellingen/python-zeep#1211
- 3: https://stackoverflow.com/questions/79678555/async-request-fails-with-httpx-unsupportedprotocol-unless-run-in-debugger
- 4: https://pillow.readthedocs.io/en/stable/reference/ImageFile.html
- 5: https://github.com/python-pillow/Pillow/blob/12.2.0/docs/handbook/image-file-formats.rst
- 6: python-pillow/Pillow#9429
- 7: https://fossies.org/diffs/Pillow/12.1.1_vs_12.2.0/src/PIL/ImageFile.py-diff.html
- 8: https://pillow.readthedocs.io/en/latest/releasenotes/12.1.0.html
🌐 Web query:
httpx 0.25.0 UnsupportedProtocol base class RequestError Exception💡 Result:
In the httpx exception hierarchy, UnsupportedProtocol is a subclass of TransportError, which is in turn a subclass of RequestError [1][2][3]. The full inheritance chain is as follows: HTTPError -> RequestError -> TransportError -> UnsupportedProtocol [1][3][4] - HTTPError: The base class for all exceptions in the library [3][5]. - RequestError: The base class for all exceptions that occur during a.request call [2][3]. - TransportError: The base class for errors that occur at the transport layer [1][4]. - UnsupportedProtocol: Specifically raised when an attempt is made to use a protocol not supported by httpx (e.g., using 'ftp://' instead of 'http://' or 'https://') [2][6][3]. If you are encountering this error, it typically indicates that the URL passed to the client uses a scheme (protocol) that httpx does not recognize [2][3]. If you see an unexpected AttributeError related to UnsupportedProtocol and httpcore, it is often a sign of a version mismatch between httpx and its dependency httpcore [7]. Ensuring your packages are compatible (e.g., matching the versions expected by the libraries you are using) usually resolves this issue [7].
Citations:
- 1: https://github.com/encode/httpx/blob/master/docs/exceptions.md
- 2: https://github.com/encode/httpx/blob/def4778d/httpx/_exceptions.py
- 3: https://www.python-httpx.org/exceptions/
- 4: https://github.com/encode/httpx/blob/b5addb64/httpx/_exceptions.py
- 5: https://github.com/encode/httpx/blob/master/httpx/_exceptions.py
- 6: https://fossies.org/dox/httpx-0.28.1/classhttpx_1_1__exceptions_1_1UnsupportedProtocol.html
- 7: openai/openai-python#714
🌐 Web query:
Pillow 12.1.1 Source GitHub PIL/Image.py "UnidentifiedImageError" "class ImageFileDecodeError"💡 Result:
In Pillow 12.1.1, the
UnidentifiedImageErroris a standard exception class defined inPIL/Image.py[1][2]. It is raised byImage.open()when the library cannot identify the format of an image file [3][4]. RegardingImageFileDecodeError, there is no class by this name in the Pillow source code. Pillow typically uses standard Python exceptions (such asSyntaxError,ValueError, orOSError) or specific internal error handling mechanisms for decoding issues [5][6][7]. For example,PIL.ImageFilecontains a dictionary of error codes inPIL.ImageFile.ERRORS(such as buffer overrun or decoding errors) that are handled by the library, often resulting inOSErrororSyntaxErrorwhen surfacing errors during image operations [5][6][7]. If you are encountering difficulty opening an image, you can use thewarn_possible_formatsargument inImage.open()to see warnings for each format driver that attempted and failed to identify the file, which can provide more insight into the underlying cause [3][4].Citations:
- 1: https://github.com/python-pillow/Pillow/blob/12.1.0/src/PIL/Image.py
- 2: https://pillow.readthedocs.io/en/latest/_modules/PIL/Image.html
- 3: python-pillow/Pillow#8033
- 4: python-pillow/Pillow#8726
- 5: https://pillow.readthedocs.io/en/stable/_modules/PIL/ImageFile.html
- 6: https://fossies.org/linux/Pillow/src/PIL/ImageFile.py
- 7: https://fossies.org/diffs/Pillow/11.3.0_vs_12.0.0/src/PIL/ImageFile.py-diff.html
🌐 Web query:
Pillow 12.1.0 Image.py source Image.save mode error raise ValueError SyntaxError💡 Result:
In Pillow 12.1.0, exceptions such as ValueError and SyntaxError raised during image saving or opening are often related to validation failures, particularly when dealing with invalid, zero-dimension, or corrupted image data [1][2][3]. Key technical context: - Zero-dimension images: In Pillow 12.1.0, attempting to save images with zero width or height can trigger various confusing exceptions (such as ValueError: max arg is an empty sequence or SystemError: tile cannot extend outside image) depending on the specific file format plugin [1][2][4]. This led to discussions and subsequent fixes to raise more descriptive ValueError messages (e.g., Cannot write empty image as [FORMAT]) instead of generic errors [1][2][4]. - SyntaxError: This error is frequently raised in Pillow's ImageFile module during the file opening process (within _open) if the image file cannot be identified, contains corrupted headers, or fails to meet expected structural requirements [3]. - ValueError: Beyond empty image validation, ValueError is commonly raised in the Image.save flow if the requested mode is unsupported by the encoder, if invalid configuration parameters are passed to the plugin, or if an operation is attempted on a closed image object (using DeferredError) [5][6][7]. If you are encountering these errors, check your image dimensions (ensure width and height are > 0) [1][2] and verify that the target file format supports the current image mode [7][8]. If opening a file, ensure the file pointer is valid and the file format is correctly identified [3].
Citations:
- 1: python-pillow/Pillow#9389
- 2: python-pillow/Pillow#9391
- 3: https://pillow.readthedocs.io/en/stable/%5Fmodules/PIL/ImageFile.html
- 4: python-pillow/Pillow#9391
- 5: https://github.com/python-pillow/Pillow/blob/12.1.0/src/PIL/Image.py
- 6: https://pillow.readthedocs.io/en/stable/reference/Image.html
- 7: https://github.com/python-pillow/Pillow/blob/12.1.0/src/PIL/JpegImagePlugin.py
- 8: https://github.com/python-pillow/Pillow/blob/12.1.0/src/PIL/PngImagePlugin.py
🌐 Web query:
Pillow 12.1.0 Image.py Open response.content bytes UnidentifiedImageError💡 Result:
The UnidentifiedImageError in Pillow 12.1.0, when using Image.open with BytesIO(response.content) from a network request, occurs because Pillow cannot detect a valid image format signature (magic number) in the provided byte data [1][2]. This error is almost always caused by issues with the source content rather than a bug in Pillow itself [3][4]. To diagnose and resolve this issue, consider the following common causes: 1. The Request Returned Non-Image Data: The most frequent cause is that the request did not return an image file. Instead, it often returns HTML (e.g., an error page, a login page, or a 404 page) [5][6]. You can verify the content by inspecting the first few bytes of the response: print(response.content[:50]) If the output starts with b'<!doctype html>' or similar text, you have received an HTML page instead of an image [6]. 2. Failed Authentication or Blocked Requests: The server might be blocking your request (e.g., if you lack a User-Agent header) or requiring authentication, resulting in the server returning a redirection or an error page [3]. Ensure your request includes headers that mimic a browser: requests.get(url, headers={'User-Agent': 'Mozilla/5.0'}) 3. Invalid or Corrupted URL: Ensure the URL directly points to the image file. If the URL points to a web page containing an image or a repository viewing page (e.g., a GitHub file viewer page), the response will be HTML, not the image binary [7]. 4. Proper Use of BytesIO: When passing raw bytes to Pillow, you must wrap them in an io.BytesIO object [8][2]: from PIL import Image import io import requests response = requests.get(url) # Check status code to ensure successful download if response.status_code == 200: img = Image.open(io.BytesIO(response.content)) else: print(f"Request failed with status: {response.status_code}") If the issue persists, save the raw content to a file on your local disk and attempt to open it manually to verify if the file itself is corrupted or incorrectly formatted: with open('debug_image.jpg', 'wb') as f: f.write(response.content)
Citations:
- 1: https://pillow.readthedocs.io/en/stable/reference/Image.html?highlight=image.open
- 2: https://pillow.readthedocs.io/en/stable/reference/Image.html
- 3: python-pillow/Pillow#6583
- 4: python-pillow/Pillow#7419
- 5: https://stackoverflow.com/questions/74019972/unidentifiedimageerror-cannot-identify-image-file-png-bytesio
- 6: https://stackoverflow.com/questions/76275157/pil-raises-unidentifiedimageerror-pil-unidentifiedimageerror-cannot-identify-i
- 7: https://stackoverflow.com/questions/70087777/opening-image-from-github-with-python-pil-image-open
- 8: python-pillow/Pillow#4678
🏁 Script executed:
#!/bin/bash set -euo pipefail echo "== fetch Pillow 12.1.1 src/PIL for relevant exception definitions ==" python3 - <<'PY' import urllib.request urls = { "Image.py": "https://raw.githubusercontent.com/python-pillow/Pillow/12.1.1/src/PIL/Image.py", "ImageFile.py": "https://raw.githubusercontent.com/python-pillow/Pillow/12.1.1/src/PIL/ImageFile.py", } for name, url in urls.items(): text = urllib.request.urlopen(url, timeout=20).read().decode() print(f"### {url}") for needle in ["class Exception", "class UnidentifiedImageError", "class ImageFileDecodeError", "raise OSError", "UnsupportedProtocol", "raise ValueError"]: idx = text.find(needle) if idx != -1: start = max(0, text.rfind("\n", 0, idx-300)) end = min(len(text), text.find("\n", idx+300, idx+1500)) print(f"-- around {needle}:") print(text[start:end]) PY echo "== fetch httpx 0.25.0 exception definitions ==" python3 - <<'PY' import urllib.request urls = { "_exceptions.py": "https://raw.githubusercontent.com/encode/httpx/0.25.0/httpx/_exceptions.py", } for name, url in urls.items(): text = urllib.request.urlopen(url, timeout=20).read().decode() print(f"### {url}") for needle in ["class UnsupportedProtocol", "class HTTPError", "class TransportError", "class RequestError"]: idx = text.find(needle) if idx != -1: start = max(0, text.rfind("\n", 0, idx-200)) end = min(len(text), text.find("\n", idx+600, idx+2000)) print(f"-- around {needle}:") print(text[start:end]) PYRepository: honzajavorek/film2trello
Length of output: 2517
Isolate poster updates from page URL uploads.
raise_on_errorturns poster404/5xxresponses intohttpx.HTTPStatusErrorbefore the poster try block runs. Catch the exception directly there, and make the whole poster step fail-soft so poster failures do not abortprocess_inboxbefore remaining cards are processed.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/film2trello/trello.py` around lines 154 - 163, Update the poster-update branch around has_poster, scraper.get, and trello_api.post so poster HTTP failures from scraper.get are caught within the poster step, including raise_on_error-generated httpx.HTTPStatusError. Make all poster retrieval, thumbnail creation, and attachment-update failures fail-soft by returning the existing error result instead of propagating into process_inbox, while preserving normal processing of subsequent cards.
283-287: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Read
previewsdefensively.
attachment["previews"]raisesKeyErrorwhen Trello omits the field for an attachment. Useattachment.get("previews")so a single attachment shape cannot abort the card update.♻️ Proposed change
def has_poster(attachments) -> bool: for attachment in attachments: - if len(attachment["previews"]): + if attachment.get("previews"): return True return False📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.def has_poster(attachments) -> bool: for attachment in attachments: if attachment.get("previews"): return True return False🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/film2trello/trello.py` around lines 283 - 287, Update has_poster to access each attachment’s previews defensively with attachment.get("previews"), treating a missing or empty value as false so one attachment without the field cannot abort the card update.
Create a dedicated `app` user and drop privileges before CMD, addressing the security review on the new uv-based Dockerfile. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ENNdCPoC2tTAdsnS2JSPK9
On the rest of CodeRabbit's reviewThis PR is a packaging migration (Poetry → uv, flat → CodeRabbit reviews moved files as if newly authored, so all its other findings are about pre-existing code that predates this PR. I've intentionally left them out of scope to keep the migration small and reviewable — mixing behavior changes into a packaging PR makes both harder to review and to revert. That said, several look like genuine pre-existing bugs worth a dedicated follow-up (not introduced here):
Happy to open a separate PR (or issue) for any of these if you'd like — just say the word. Keeping this one scoped to the migration. Generated by Claude Code |
Switch [build-system] to uv_build. README.md must stay in the Docker build
context — verified that excluding it makes the wheel build fail
("failed to open file /app/README.md"), since project.readme points at it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ENNdCPoC2tTAdsnS2JSPK9
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ENNdCPoC2tTAdsnS2JSPK9
What
Switches dependency management from Poetry to uv and moves the package into a
src/layout.Changes
film2trello/→src/film2trello/(tests stay intests/, importing the installed package).[project]metadata,[project.scripts],[project.urls],[dependency-groups] devfor pytest/ruff, and a hatchling build backend ([tool.hatch.build.targets.wheel] packages = ["src/film2trello"]). Same dependency versions as before.poetry.lockremoved,uv.lockgenerated..python-version: added, pinning 3.11.uv sync --frozen --no-dev(deps layer cached separately from the project), builds the project from the src layout, runsfilm2trello bot.build.yml,scripts.yml): Poetry →astral-sh/setup-uv, usinguv sync --frozen/uv run …. Fly.io deploy steps unchanged..venv; keepREADME.mdin the Docker build context (hatchling readsproject.readmewhen building the wheel).testpathsnarrowed from.totests.Verification
uv lock --check,uv sync --frozen,uv run pytest(64 passed),uv run ruff check,uv run ruff format --checkall pass.film2trello --helpandfilm2trello inbox --helpwork inside the built image (the same steps Fly.io runs viaflyctl deploy).Notes
FLY_API_TOKEN), but the image the deploy builds is verified to build and run.pytest-asynciois kept as a runtime dependency (as it was under Poetry) to keep this a faithful 1:1 migration; happy to move it to the dev group if you'd prefer.🤖 Generated with Claude Code
Generated by Claude Code
Summary by CodeRabbit
New Features
Documentation