From 4487871426b03ff07301d678470010afe10ed814 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Tue, 14 Jul 2026 16:46:19 -0700 Subject: [PATCH 1/5] Readable error when app package download fails in web worker pyfetch(app_package_url) was piped straight into unpack_archive without a status check, so any non-2xx response (transient 500, expired auth 401, deleted app 404) wrote the JSON/HTML error body to a temp file and surfaced as a cryptic "shutil.ReadError: ...package.zip?v=0.0 is not a zip file". Check response.ok and raise "Failed to download app package: HTTP - " with the first 200 chars of the error body. Applied to both the Flet client (client/web) and the flet build cookiecutter template. --- client/web/python-worker.js | 15 +++++++++++++++ .../{{cookiecutter.out_dir}}/web/python-worker.js | 15 +++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/client/web/python-worker.js b/client/web/python-worker.js index 390ea96747..d42593d55c 100644 --- a/client/web/python-worker.js +++ b/client/web/python-worker.js @@ -82,6 +82,21 @@ self.initPyodide = async function () { if "script" not in py_args: print("Downloading app archive") response = await pyfetch(app_package_url) + # Fail with a readable error on a non-2xx response. + # Without this check the error body (JSON/HTML from the + # server) lands in unpack_archive and surfaces as a + # cryptic "... is not a zip file" ReadError. + if not response.ok: + _err_body = "" + try: + _err_body = (await response.text())[:200].strip() + except Exception: + pass + raise RuntimeError( + f"Failed to download app package: " + f"HTTP {response.status} {app_package_url}" + + (f" — {_err_body}" if _err_body else "") + ) # Pick format from the URL's path extension. Pyodide's # filename-based sniff trips over query strings like # ?v=42. We only support zip and tar.gz (matching what diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/web/python-worker.js b/sdk/python/templates/build/{{cookiecutter.out_dir}}/web/python-worker.js index 390ea96747..d42593d55c 100644 --- a/sdk/python/templates/build/{{cookiecutter.out_dir}}/web/python-worker.js +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/web/python-worker.js @@ -82,6 +82,21 @@ self.initPyodide = async function () { if "script" not in py_args: print("Downloading app archive") response = await pyfetch(app_package_url) + # Fail with a readable error on a non-2xx response. + # Without this check the error body (JSON/HTML from the + # server) lands in unpack_archive and surfaces as a + # cryptic "... is not a zip file" ReadError. + if not response.ok: + _err_body = "" + try: + _err_body = (await response.text())[:200].strip() + except Exception: + pass + raise RuntimeError( + f"Failed to download app package: " + f"HTTP {response.status} {app_package_url}" + + (f" — {_err_body}" if _err_body else "") + ) # Pick format from the URL's path extension. Pyodide's # filename-based sniff trips over query strings like # ?v=42. We only support zip and tar.gz (matching what From 6872e44ce05e14700d42b370dd476482b53f25fa Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Tue, 14 Jul 2026 16:53:51 -0700 Subject: [PATCH 2/5] Prepare 0.86.1 release: readable app package download errors --- CHANGELOG.md | 6 ++++++ packages/flet/CHANGELOG.md | 4 ++++ packages/flet/pubspec.yaml | 2 +- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ac38efc16..9382e81807 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.86.1 + +### Bug fixes + +* Fix the cryptic `shutil.ReadError: ... is not a zip file` failure in web apps when the app package download fails. The Pyodide worker piped the `pyfetch(app_package_url)` response straight into `unpack_archive()` without a status check, so any non-2xx response (transient server 500, expired auth 401, deleted app 404) wrote the JSON/HTML error body to a temp file and crashed while unpacking it. The worker now checks `response.ok` and raises a readable `Failed to download app package: HTTP — ` error, including the first 200 chars of the error body. Applied to both the Flet web client and the `flet build` template ([#6680](https://github.com/flet-dev/flet/pull/6680)) by @FeodorFitsner. + ## 0.86.0 ### New features diff --git a/packages/flet/CHANGELOG.md b/packages/flet/CHANGELOG.md index 41217b664c..9c78d050fe 100644 --- a/packages/flet/CHANGELOG.md +++ b/packages/flet/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.86.1 + +_No changes in the `flet` Dart package; version bumped for release coordination with the web client's readable app-package download errors ([#6680](https://github.com/flet-dev/flet/pull/6680))._ + ## 0.86.0 _No changes in the `flet` Dart package; version bumped for release coordination with the multi-version bundled CPython support on the Python side ([#6577](https://github.com/flet-dev/flet/pull/6577))._ diff --git a/packages/flet/pubspec.yaml b/packages/flet/pubspec.yaml index 41ca2cded8..156c0f8673 100644 --- a/packages/flet/pubspec.yaml +++ b/packages/flet/pubspec.yaml @@ -2,7 +2,7 @@ name: flet description: Write entire Flutter app in Python or add server-driven UI experience into existing Flutter app. homepage: https://flet.dev repository: https://github.com/flet-dev/flet/tree/main/packages/flet -version: 0.86.0 +version: 0.86.1 # Supported platforms platforms: From c0a5a5f72b875b7a70794d4b009a5bc904f526fa Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Tue, 14 Jul 2026 17:38:36 -0700 Subject: [PATCH 3/5] Fix page return type: remove unused BasePage from BaseControl and ControlEvent Recovers changes from #6606 by @Iaw4tch. --- CHANGELOG.md | 1 + sdk/python/packages/flet/src/flet/controls/base_control.py | 3 +-- sdk/python/packages/flet/src/flet/controls/control_event.py | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9382e81807..27a8316117 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### Bug fixes * Fix the cryptic `shutil.ReadError: ... is not a zip file` failure in web apps when the app package download fails. The Pyodide worker piped the `pyfetch(app_package_url)` response straight into `unpack_archive()` without a status check, so any non-2xx response (transient server 500, expired auth 401, deleted app 404) wrote the JSON/HTML error body to a temp file and crashed while unpacking it. The worker now checks `response.ok` and raises a readable `Failed to download app package: HTTP — ` error, including the first 200 chars of the error body. Applied to both the Flet web client and the `flet build` template ([#6680](https://github.com/flet-dev/flet/pull/6680)) by @FeodorFitsner. +* Remove unused `BasePage` return type and import from `BaseControl` and `ControlEvent` ([#6606](https://github.com/flet-dev/flet/pull/6606)) by @Iaw4tch. ## 0.86.0 diff --git a/sdk/python/packages/flet/src/flet/controls/base_control.py b/sdk/python/packages/flet/src/flet/controls/base_control.py index a064b6e39f..5a3a25eb4f 100644 --- a/sdk/python/packages/flet/src/flet/controls/base_control.py +++ b/sdk/python/packages/flet/src/flet/controls/base_control.py @@ -23,7 +23,6 @@ if TYPE_CHECKING: - from .base_page import BasePage from .keys import KeyValue from .page import Page @@ -296,7 +295,7 @@ def parent(self) -> Optional["BaseControl"]: return parent_ref() if parent_ref else None @property - def page(self) -> "Union[Page, BasePage]": + def page(self) -> "Page": """ The page to which this control belongs to. """ diff --git a/sdk/python/packages/flet/src/flet/controls/control_event.py b/sdk/python/packages/flet/src/flet/controls/control_event.py index 3c8bd45cba..40bf5f46cd 100644 --- a/sdk/python/packages/flet/src/flet/controls/control_event.py +++ b/sdk/python/packages/flet/src/flet/controls/control_event.py @@ -19,7 +19,6 @@ if TYPE_CHECKING: from .base_control import BaseControl # noqa from .page import Page - from .base_page import BasePage _BaseControlType = BaseControl else: @@ -128,7 +127,7 @@ class Event(Generic[EventControlType]): """ @property - def page(self) -> Union["Page", "BasePage"]: + def page(self) -> "Page": """ Page that owns the event source control. """ From 557775aeac7b8f0d23109942feecbf3e00049c41 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Wed, 15 Jul 2026 08:28:55 -0700 Subject: [PATCH 4/5] flet-mcp: index top-level functions + resolve enum members inline in get_api Two improvements driven by production agent-trace analysis: 1. Enum member/query inline. get_api(name, member=/query=) on an enum used to return an error redirecting to search_enum_members, costing a round-trip -- traces showed models burning one call per color on get_api('Colors', query='RED'). It now resolves to a ranked member search inline and renders the matches; a no-match result points at search_enum_members. 2. Index top-level callables. get_api('run'), ('use_state'), ('component'), ('memo'), ('observable') and the other hooks/decorators previously returned 'not found' because the builder only indexed classes. Add a functions bucket: the builder now extracts each package's re-exported public callables (entry point, reactive hooks, decorators), the store serves them, and render_text emits a signature line. api.json is a gitignored build artifact, so the new bucket ships on the next flet-mcp build. Tests + docstrings updated (42 passing). --- .../flet-mcp/src/flet_mcp/api_store.py | 61 ++++++++++++++---- .../flet-mcp/src/flet_mcp/build/api.py | 64 +++++++++++++++++++ .../packages/flet-mcp/src/flet_mcp/server.py | 14 ++-- .../packages/flet-mcp/tests/test_api_store.py | 60 +++++++++++++++-- 4 files changed, 179 insertions(+), 20 deletions(-) diff --git a/sdk/python/packages/flet-mcp/src/flet_mcp/api_store.py b/sdk/python/packages/flet-mcp/src/flet_mcp/api_store.py index 4fcc1b574a..32cca1d788 100644 --- a/sdk/python/packages/flet-mcp/src/flet_mcp/api_store.py +++ b/sdk/python/packages/flet-mcp/src/flet_mcp/api_store.py @@ -27,8 +27,8 @@ # Buckets in preference order for ambiguous names, and the `kind` value # stamped onto entries from buckets that don't carry their own. -_BUCKET_RANK = {"controls": 0, "types": 1, "events": 2, "enums": 3} -_BUCKET_KIND = {"types": "type", "events": "event"} +_BUCKET_RANK = {"controls": 0, "types": 1, "events": 2, "functions": 3, "enums": 4} +_BUCKET_KIND = {"types": "type", "events": "event", "functions": "function"} # Sphinx roles in docstrings (":attr:`style`", ":class:`~flet.Text`") are # docs-site markup — render them as plain backticked names. @@ -191,6 +191,21 @@ def render_text(hit: dict[str, Any]) -> str: if hit.get("note"): lines.append(f"note: {hit['note']}") + # Top-level callables (ft.run, hooks, decorators): render a signature. + if hit.get("kind") == "function": + params = ", ".join( + a["name"] + + (f": {a['type']}" if a.get("type") else "") + + (f" = {a['default']}" if a.get("default") else "") + for a in hit.get("args") or [] + ) + prefix = "async " if hit.get("async") else "" + sig = f"{prefix}{name}({params})" + if hit.get("return_type"): + sig += f" -> {hit['return_type']}" + lines.append(f"signature: {sig}") + return "\n".join(lines) + if hit.get("kind") == "large_enum": lines.append(f"total members: {hit.get('total_members')}") samples = [ @@ -240,6 +255,7 @@ def __init__(self) -> None: self._events: dict[str, dict] | None = None self._types: dict[str, dict] | None = None self._enums: dict[str, dict] | None = None + self._functions: dict[str, dict] | None = None self._by_name: dict[str, list[tuple[str, dict]]] | None = None def _load(self) -> dict[str, Any]: @@ -251,12 +267,13 @@ def _load(self) -> dict[str, Any]: self._events = {e["name"]: e for e in self._raw.get("events", [])} self._types = {t["name"]: t for t in self._raw.get("types", [])} self._enums = {e["name"]: e for e in self._raw.get("enums", [])} + self._functions = {f["name"]: f for f in self._raw.get("functions", [])} # Names are NOT unique across (or even within) buckets — e.g. # the Text control vs the canvas Text shape. Keep every entry # per name so `get` can rank candidates instead of letting the # last dict insert silently win. self._by_name = {} - for bucket in ("controls", "types", "events", "enums"): + for bucket in ("controls", "types", "events", "functions", "enums"): for e in self._raw.get(bucket, []): self._by_name.setdefault(e["name"], []).append((bucket, e)) return self._raw @@ -346,9 +363,11 @@ def get( Member docstrings (properties/events/methods) are trimmed to their first sentence. Pass `member` for one member's full entry, or `query` for a case-insensitive substring filter over member names (`member` - wins if both are given). Enum responses use the same truncation as - `get_enum` for large enums; `member`/`query` don't apply to enums — - use search_enum_members / enum_has_member there. + wins if both are given). On an enum, `member`/`query` resolve inline + to a ranked member search (same result as `search_enum_members`); a + bare enum name returns the same truncation as `get_enum` for large + enums. Top-level callables (functions bucket) return a `function` + entry with no members. Names are not globally unique (e.g. the Text control vs the canvas Text shape): the best candidate wins and the response notes the @@ -360,16 +379,34 @@ def get( bucket, entry = cands[0] if bucket == "enums": - if member is not None or query is not None: + ename = entry["name"] + # Resolve enum members inline rather than bouncing the caller to + # search_enum_members: production traces show models burning a + # round-trip per value on get_api("Colors", query="RED"). `member` + # and `query` are both treated as a member search here. + term = member if member is not None else query + if term is not None: + matches = self.search_enum_members(ename, term, limit=20) + note = ( + f"members of {ename} matching '{term}'" + if matches + else f"no {ename} member matches '{term}' — try " + "search_enum_members for a broader search" + ) return { - "error": ( - f"'{name}' is an enum — use search_enum_members or " - "enum_has_member to look up its members" - ) + "kind": "enum", + "name": ename, + "note": note, + "members": [{"name": m} for m in matches], } - enum = self.get_enum(entry["name"]) + enum = self.get_enum(ename) return {"kind": enum.get("kind", "enum"), **enum} + if bucket == "functions": + # Top-level callables (ft.run, hooks, decorators) carry no + # members — return the entry as-is for render_text. + return {"kind": "function", **entry} + hit = entry if bucket == "controls" else {"kind": _BUCKET_KIND[bucket], **entry} if member is not None: return self._get_member(hit, name, member) diff --git a/sdk/python/packages/flet-mcp/src/flet_mcp/build/api.py b/sdk/python/packages/flet-mcp/src/flet_mcp/build/api.py index 759ecb081a..04ccfb5ba8 100644 --- a/sdk/python/packages/flet-mcp/src/flet_mcp/build/api.py +++ b/sdk/python/packages/flet-mcp/src/flet_mcp/build/api.py @@ -256,6 +256,62 @@ def _extract_methods(cls: griffe.Class) -> list[dict[str, Any]]: return methods +def _function_entry(func: griffe.Function, name: str) -> dict[str, Any]: + """Serialize a top-level callable (function/decorator/hook).""" + args: list[dict[str, Any]] = [] + for param in func.parameters: + if param.name in ("self", "cls"): + continue + args.append( + { + "name": param.name, + "type": _annotation_str(param.annotation), + "default": _default_str(param.default), + } + ) + module_path = func.canonical_path.rsplit(".", 1)[0] + entry: dict[str, Any] = { + "name": name, + "module": module_path, + "package": _package_from_module(module_path), + "kind": "function", + "summary": _first_line(func.docstring), + "docstring": _full_docstring(func.docstring), + "args": args, + "return_type": _annotation_str(func.annotation), + } + if "async" in func.labels: + entry["async"] = True + return entry + + +def _collect_public_functions( + module: griffe.Module, functions: list[dict], seen: set[str] +) -> None: + """Index a package's top-level public callables — the module-level + functions re-exported from the package root: the app entry point + (``run``/``app``), reactive hooks (``use_state``/``use_ref``/…), and + decorators (``component``/``memo``/``observable``). Only the top module's + direct members are scanned — that is the public surface; nested/internal + helpers are skipped. Re-export aliases are resolved to their target.""" + for name, obj in module.members.items(): + if name.startswith("_"): + continue + target: Any = obj + if isinstance(obj, griffe.Alias): + try: + target = obj.final_target + except Exception: + continue + if not isinstance(target, griffe.Function): + continue + cp = target.canonical_path + if cp in seen: + continue + seen.add(cp) + functions.append(_function_entry(target, name)) + + def _extract_enum_members(cls: griffe.Class) -> list[dict[str, str]]: members: list[dict[str, str]] = [] for name, member in cls.members.items(): @@ -563,6 +619,7 @@ def build_api(output_path: Path, packages: list[str] | None = None) -> dict[str, events: list[dict] = [] types: list[dict] = [] enums: list[dict] = [] + functions: list[dict] = [] loaded: list[griffe.Module] = [] for pkg in packages: @@ -581,6 +638,11 @@ def build_api(output_path: Path, packages: list[str] | None = None) -> dict[str, for module in loaded: _walk_module(module, controls, events, types, enums) + # Pass 3: top-level public callables (entry point, hooks, decorators). + seen_functions: set[str] = set() + for module in loaded: + _collect_public_functions(module, functions, seen_functions) + # Inject Icons and CupertinoIcons from JSON files (not Python Enums) _inject_icon_enums(enums) @@ -591,6 +653,7 @@ def build_api(output_path: Path, packages: list[str] | None = None) -> dict[str, "events": events, "types": types, "enums": enums, + "functions": functions, "cli": cli_help, } @@ -602,6 +665,7 @@ def build_api(output_path: Path, packages: list[str] | None = None) -> dict[str, "events": len(events), "types": len(types), "enums": len(enums), + "functions": len(functions), "cli_commands": len(cli_help), } logger.info("API build stats: %s", stats) diff --git a/sdk/python/packages/flet-mcp/src/flet_mcp/server.py b/sdk/python/packages/flet-mcp/src/flet_mcp/server.py index 1102d6a80e..a602019dac 100644 --- a/sdk/python/packages/flet-mcp/src/flet_mcp/server.py +++ b/sdk/python/packages/flet-mcp/src/flet_mcp/server.py @@ -371,7 +371,10 @@ def get_api( Looks across visual controls, non-visual services, dataclass types (ButtonStyle, Padding, TextStyle, Border, Theme, ColorScheme, ...), - event classes, and enums (MainAxisAlignment, TextAlign, Icons, ...). + event classes, enums (MainAxisAlignment, TextAlign, Icons, ...), and + top-level callables — the app entry point (run), reactive hooks + (use_state, use_ref, use_effect, ...), and decorators (component, + memo, observable), rendered with a `signature:` line. This is the primary verification tool — when you have a name in mind, call this first. A "not found" response is a definitive negative: the @@ -414,9 +417,12 @@ def get_api( vs the canvas Text shape) — the primary one is returned and the `note:` line lists the others with a dotted name that selects them, e.g. `get_api("canvas.Text")`. - * Neither applies to enums — use search_enum_members / - enum_has_member / find_icon there. Large enums (Icons, - CupertinoIcons) are always truncated to a sample. + * On an enum, `member`/`query` resolve inline to a ranked member + search (same result as search_enum_members) instead of erroring — + e.g. `get_api("Colors", query="RED")` lists the matching members. + For concept-based icon lookup ("delete", "user") prefer find_icon. + Large enums (Icons, CupertinoIcons) are truncated to a sample when + fetched whole. Errors are returned as JSON objects (`{"error": ...}`), sometimes with `available_members`/`available_files` hints. diff --git a/sdk/python/packages/flet-mcp/tests/test_api_store.py b/sdk/python/packages/flet-mcp/tests/test_api_store.py index 425ae8cee7..f6d453a2f9 100644 --- a/sdk/python/packages/flet-mcp/tests/test_api_store.py +++ b/sdk/python/packages/flet-mcp/tests/test_api_store.py @@ -187,6 +187,28 @@ ], }, ], + "functions": [ + { + "name": "run", + "module": "flet", + "package": "flet", + "kind": "function", + "summary": "Run the app.", + "docstring": "Run the app.", + "args": [{"name": "target", "type": "Callable", "default": ""}], + "return_type": "None", + }, + { + "name": "use_state", + "module": "flet.core.reactive", + "package": "flet", + "kind": "function", + "summary": "Reactive state hook.", + "docstring": "Reactive state hook.", + "args": [{"name": "initial", "type": "T", "default": ""}], + "return_type": "tuple[T, Callable]", + }, + ], } @@ -198,8 +220,9 @@ def store() -> ApiStore: s._events = {e["name"]: e for e in RAW["events"]} s._types = {t["name"]: t for t in RAW["types"]} s._enums = {e["name"]: e for e in RAW["enums"]} + s._functions = {f["name"]: f for f in RAW.get("functions", [])} s._by_name = {} - for bucket in ("controls", "types", "events", "enums"): + for bucket in ("controls", "types", "events", "functions", "enums"): for e in RAW[bucket]: s._by_name.setdefault(e["name"], []).append((bucket, e)) return s @@ -340,9 +363,38 @@ def test_member_wins_over_query(store): assert hit["member"]["name"] == "push_route" -def test_enum_rejects_member_and_query(store): - assert "error" in store.get("TextAlign", query="left") - assert "error" in store.get("TextAlign", member="LEFT") +def test_enum_member_and_query_resolve_inline(store): + # get_api on an enum with query/member now searches members inline + # instead of erroring (saves the caller a round-trip). + hit = store.get("TextAlign", query="left") + assert "error" not in hit + assert hit["kind"] == "enum" + assert [m["name"] for m in hit["members"]] == ["LEFT"] + # member= is treated as the same member search. + assert [m["name"] for m in store.get("TextAlign", member="LEFT")["members"]] == [ + "LEFT" + ] + # Works for large enums too, rendered as a member list. + assert "REMOVE" in render_text(store.get("Icons", query="remove")) + + +def test_enum_query_no_match_notes_search(store): + hit = store.get("TextAlign", query="zzz") + assert "error" not in hit + assert hit["members"] == [] + assert "search_enum_members" in hit["note"] + + +def test_function_lookup_and_render(store): + hit = store.get("run") + assert hit["kind"] == "function" + text = render_text(hit) + assert text.startswith("run (function) — Run the app.") + assert "signature: run(target: Callable) -> None" in text + + +def test_hook_function_lookup(store): + assert store.get("use_state")["kind"] == "function" # ---------- enum member search ranking ---------- From e47985da857f66ee3870b462dc79936bd30552f5 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Wed, 15 Jul 2026 08:34:13 -0700 Subject: [PATCH 5/5] Add 0.86.1 changelog entry for flet-mcp get_api improvements --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27a8316117..872dbba3c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## 0.86.1 +### Improvements + +* `flet-mcp`'s `get_api` tool now covers **top-level callables** — the app entry point (`run`), reactive hooks (`use_state`, `use_ref`, `use_effect`, …), and decorators (`component`, `memo`, `observable`) — which previously returned `not found` because only classes were indexed. A new `functions` bucket in the API builder extracts each package's re-exported public callables and renders them with a `signature:` line. `get_api` also **resolves enum member lookups inline**: `get_api("Colors", query="RED")` now returns the matching members instead of erroring with a redirect to `search_enum_members`. Both changes remove wasted agent round-trips observed in production usage by @FeodorFitsner. + ### Bug fixes * Fix the cryptic `shutil.ReadError: ... is not a zip file` failure in web apps when the app package download fails. The Pyodide worker piped the `pyfetch(app_package_url)` response straight into `unpack_archive()` without a status check, so any non-2xx response (transient server 500, expired auth 401, deleted app 404) wrote the JSON/HTML error body to a temp file and crashed while unpacking it. The worker now checks `response.ok` and raises a readable `Failed to download app package: HTTP — ` error, including the first 200 chars of the error body. Applied to both the Flet web client and the `flet build` template ([#6680](https://github.com/flet-dev/flet/pull/6680)) by @FeodorFitsner.