{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "cell-23-12-00",
   "metadata": {},
   "source": [
    "# 23-12 · Budowanie planu na podstawie listy plików\n",
    "\n",
    "Praktyka do sekcji [„Od analizy do planu działania”](/pl/chapters/rozdzial-23/23-11-plan-dejstvij.html). Niniejszy plik — `projects/python/safesort/src/safesort/planner.py`."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-23-12-01",
   "metadata": {},
   "source": [
    "## Cel\n",
    "\n",
    "Sztuka `build_plan()` i upewnić się, że każdy plik otrzymuje przewidywaną ścieżkę docelową `root/Sorted/<категория>/<имя>` jest jak zwykłe dane, bez żadnej zmiany w systemie plików."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-23-12-02",
   "metadata": {},
   "source": [
    "## Example - Modele i funkcje pomocnicze"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-23-12-03",
   "metadata": {},
   "outputs": [],
   "source": [
    "from dataclasses import dataclass\n",
    "from pathlib import Path\n",
    "\n",
    "OTHER_CATEGORY = \"other\"\n",
    "DEFAULT_EXTENSIONS = {\n",
    "    \"documents\": [\".pdf\", \".docx\", \".txt\", \".odt\"],\n",
    "    \"images\": [\".jpg\", \".jpeg\", \".png\", \".webp\"],\n",
    "    \"archives\": [\".zip\", \".tar\", \".gz\", \".7z\"],\n",
    "}\n",
    "\n",
    "\n",
    "def classify(extension, mapping):\n",
    "    normalized = extension.lower()\n",
    "    for category, extensions in mapping.items():\n",
    "        if normalized in {ext.lower() for ext in extensions}:\n",
    "            return category\n",
    "    return OTHER_CATEGORY\n",
    "\n",
    "\n",
    "@dataclass(frozen=True)\n",
    "class FileInfo:\n",
    "    path: Path\n",
    "    size: int\n",
    "    extension: str\n",
    "\n",
    "\n",
    "@dataclass(frozen=True)\n",
    "class MoveOperation:\n",
    "    source: Path\n",
    "    destination: Path\n",
    "\n",
    "\n",
    "@dataclass(frozen=True)\n",
    "class SortPlan:\n",
    "    root: Path\n",
    "    operations: tuple"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-23-12-04",
   "metadata": {},
   "source": [
    "## build_plan() — plan jako dane"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-23-12-05",
   "metadata": {},
   "outputs": [],
   "source": [
    "def _resolve_collision(candidate, reserved):\n",
    "    if candidate not in reserved and not candidate.exists():\n",
    "        return candidate\n",
    "    stem, suffix, parent = candidate.stem, candidate.suffix, candidate.parent\n",
    "    counter = 1\n",
    "    while True:\n",
    "        alternative = parent / f\"{stem} ({counter}){suffix}\"\n",
    "        if alternative not in reserved and not alternative.exists():\n",
    "            return alternative\n",
    "        counter += 1\n",
    "\n",
    "\n",
    "def build_plan(files, root, destination_name, extensions_mapping):\n",
    "    root = Path(root)\n",
    "    dest_root = root / destination_name\n",
    "    reserved = set()\n",
    "    operations = []\n",
    "    for file in files:\n",
    "        category = classify(file.extension, extensions_mapping)\n",
    "        dest_dir = dest_root / category\n",
    "        candidate = dest_dir / file.path.name\n",
    "        destination = _resolve_collision(candidate, reserved)\n",
    "        reserved.add(destination)\n",
    "        operations.append(MoveOperation(source=file.path, destination=destination))\n",
    "    return SortPlan(root=root, operations=tuple(operations))\n",
    "\n",
    "\n",
    "koren = Path(\"/home/anna/Downloads\")\n",
    "fajly = [\n",
    "    FileInfo(path=koren / \"otchet.pdf\", size=1200, extension=\".pdf\"),\n",
    "    FileInfo(path=koren / \"photo.jpg\", size=204800, extension=\".jpg\"),\n",
    "    FileInfo(path=koren / \"archiv.zip\", size=5000, extension=\".zip\"),\n",
    "]\n",
    "\n",
    "plan = build_plan(fajly, koren, \"Sorted\", DEFAULT_EXTENSIONS)\n",
    "for op in plan.operations:\n",
    "    print(op.source, \"->\", op.destination)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-23-12-06",
   "metadata": {},
   "source": [
    "## Sprawdzenie wyniku"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-23-12-07",
   "metadata": {},
   "outputs": [],
   "source": [
    "destinations = {op.source.name: op.destination for op in plan.operations}\n",
    "assert destinations[\"otchet.pdf\"] == koren / \"Sorted\" / \"documents\" / \"otchet.pdf\"\n",
    "assert destinations[\"photo.jpg\"] == koren / \"Sorted\" / \"images\" / \"photo.jpg\"\n",
    "assert destinations[\"archiv.zip\"] == koren / \"Sorted\" / \"archives\" / \"archiv.zip\"\n",
    "print(\"Верно: каждый файл получил путь Sorted/<категория>/<имя>.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-23-12-08",
   "metadata": {},
   "source": [
    "## Starter\n",
    "\n",
    "Wypełnij zaznaczone miejsce. Niezmieniony starter nie przechodzi tests."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "task-23-12",
   "metadata": {
    "tags": [
     "exercise",
     "starter"
    ]
   },
   "outputs": [],
   "source": [
    "def plan_s_dopolnitelnym_fajlom(filename: str):\n",
    "    # TODO: create FileInfo, append it to fajly, then call build_plan().\n",
    "    raise NotImplementedError\n",
    "\n",
    "\n",
    "plan2 = plan_s_dopolnitelnym_fajlom(\"strannyj.xyz\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-23-12-10",
   "metadata": {},
   "source": [
    "## Task\n",
    "\n",
    "Napisz funkcję, która dodaje pojedynczy plik do danych źródłowych i tworzy dla niego plan."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-23-12-11",
   "metadata": {},
   "source": [
    "## Tests\n",
    "\n",
    "Run After task cell: jest podstawowy przykład i przynajmniej jeden skrajny przypadek."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "tests-23-12",
   "metadata": {
    "tags": [
     "exercise-tests"
    ]
   },
   "outputs": [],
   "source": [
    "destination = next(op.destination for op in plan2.operations if op.source.name == \"strannyj.xyz\")\n",
    "assert destination == koren / \"Sorted\" / \"other\" / \"strannyj.xyz\"\n",
    "plan_pdf = plan_s_dopolnitelnym_fajlom(\"REPORT.PDF\")\n",
    "dest_pdf = next(op.destination for op in plan_pdf.operations if op.source.name == \"REPORT.PDF\")\n",
    "assert dest_pdf.parent.name == \"documents\"\n",
    "print(\"Tests passed\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-23-12-13",
   "metadata": {},
   "source": [
    "## Hint\n",
    "\n",
    "Rozszerzenie można uzyskać jako `Path(filename).suffix`; Normalizacja została już zaimplementowana dla klasyfikatora."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-23-12-14",
   "metadata": {},
   "source": [
    "## Solution\n",
    "\n",
    "Pokaż rozwiązanie po własnej próbie</summary>\n",
    "\n",
    "```python\n",
    "def plan_s_dopolnitelnym_fajlom(filename: str):\n",
    "    path = koren / filename\n",
    "    extra = FileInfo(path=path, size=10, extension=path.suffix)\n",
    "    return build_plan(fajly + [extra], koren, \"Sorted\", DEFAULT_EXTENSIONS)\n",
    "\n",
    "\n",
    "plan2 = plan_s_dopolnitelnym_fajlom(\"strannyj.xyz\")\n",
    "```\n",
    "\n",
    "</details>"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Cartesian Python 3.14",
   "language": "python",
   "name": "cartesian-python314"
  },
  "language_info": {
   "name": "python",
   "version": "3.14.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
