[{"data":1,"prerenderedAt":15216},["ShallowReactive",2],{"navigation_docs":3,"-python-fastapi-fastapi-testing":3541,"-python-fastapi-fastapi-testing-surround":15211},[4,1707,1912,2366,2547,2673,2880,3002,3052,3109,3143,3269,3346,3360,3467,3484,3537],{"title":5,"icon":6,"path":7,"stem":8,"children":9},"C#","i-devicon-csharp","\u002Fcsharp","01.csharp",[10,13,60,90,120,202,219,253,379,404,457,650,1364,1654,1703],{"title":11,"path":7,"stem":12},"C# та .NET","01.csharp\u002Findex",{"title":14,"icon":15,"path":16,"stem":17,"children":18,"page":59},"Fundamentals","i-lucide-book-open","\u002Fcsharp\u002Ffundamentals","01.csharp\u002F01.fundamentals",[19,23,27,31,35,39,43,47,51,55],{"title":20,"path":21,"stem":22},"Вступ до екосистеми .NET","\u002Fcsharp\u002Ffundamentals\u002Fintroduction-to-ecosystem","01.csharp\u002F01.fundamentals\u002F01.introduction-to-ecosystem",{"title":24,"path":25,"stem":26},"Структура програми на C#","\u002Fcsharp\u002Ffundamentals\u002Fprogram-structure","01.csharp\u002F01.fundamentals\u002F02.program-structure",{"title":28,"path":29,"stem":30},"Змінні та Типи Даних","\u002Fcsharp\u002Ffundamentals\u002Fvariables-data-types","01.csharp\u002F01.fundamentals\u002F03.variables-data-types",{"title":32,"path":33,"stem":34},"Масиви","\u002Fcsharp\u002Ffundamentals\u002Farrays","01.csharp\u002F01.fundamentals\u002F04.arrays",{"title":36,"path":37,"stem":38},"Strings & Text Handling","\u002Fcsharp\u002Ffundamentals\u002Fstrings-text-handling","01.csharp\u002F01.fundamentals\u002F05.strings-text-handling",{"title":40,"path":41,"stem":42},"Дати і Час","\u002Fcsharp\u002Ffundamentals\u002Fdates-time-handling","01.csharp\u002F01.fundamentals\u002F06.dates-time-handling",{"title":44,"path":45,"stem":46},"Потік Керування","\u002Fcsharp\u002Ffundamentals\u002Fcontrol-flow","01.csharp\u002F01.fundamentals\u002F07.control-flow",{"title":48,"path":49,"stem":50},"Методи","\u002Fcsharp\u002Ffundamentals\u002Fmethods","01.csharp\u002F01.fundamentals\u002F08.methods",{"title":52,"path":53,"stem":54},"Основи Відлагодження","\u002Fcsharp\u002Ffundamentals\u002Fdebugging-basics","01.csharp\u002F01.fundamentals\u002F09.debugging-basics",{"title":56,"path":57,"stem":58},"Інтерактивна Консоль (Classic)","\u002Fcsharp\u002Ffundamentals\u002Finteractive-console","01.csharp\u002F01.fundamentals\u002F10.interactive-console",false,{"title":61,"icon":62,"path":63,"stem":64,"children":65,"page":59},"OOP","i-lucide-box","\u002Fcsharp\u002Foop","01.csharp\u002F02.oop",[66,70,74,78,82,86],{"title":67,"path":68,"stem":69},"Package Management (Управління Пакетами)","\u002Fcsharp\u002Foop\u002Fpackage-management","01.csharp\u002F02.oop\u002F01.package-management",{"title":71,"path":72,"stem":73},"Класи та Об'єкти","\u002Fcsharp\u002Foop\u002Fclasses-objects","01.csharp\u002F02.oop\u002F02.classes-objects",{"title":75,"path":76,"stem":77},"Властивості та Поля","\u002Fcsharp\u002Foop\u002Fproperties-fields","01.csharp\u002F02.oop\u002F03.properties-fields",{"title":79,"path":80,"stem":81},"Стовпи ООП","\u002Fcsharp\u002Foop\u002Foop-pillars","01.csharp\u002F02.oop\u002F04.oop-pillars",{"title":83,"path":84,"stem":85},"Advanced Types","\u002Fcsharp\u002Foop\u002Fadvanced-types","01.csharp\u002F02.oop\u002F05.advanced-types",{"title":87,"path":88,"stem":89},"Namespaces (Простори Імен)","\u002Fcsharp\u002Foop\u002Fnamespaces","01.csharp\u002F02.oop\u002F06.namespaces",{"title":91,"icon":92,"path":93,"stem":94,"children":95,"page":59},"Advanced Core","i-lucide-zap","\u002Fcsharp\u002Fadvanced-core","01.csharp\u002F03.advanced-core",[96,100,104,108,112,116],{"title":97,"path":98,"stem":99},"Generics (Узагальнення)","\u002Fcsharp\u002Fadvanced-core\u002Fgenerics","01.csharp\u002F03.advanced-core\u002F01.generics",{"title":101,"path":102,"stem":103},"Делегати, Події та Лямбда-вирази","\u002Fcsharp\u002Fadvanced-core\u002Fdelegates-events-lambdas","01.csharp\u002F03.advanced-core\u002F02.delegates-events-lambdas",{"title":105,"path":106,"stem":107},"Interfaces Deep Dive (Інтерфейси: Поглиблений Розгляд)","\u002Fcsharp\u002Fadvanced-core\u002Finterfaces-deep-dive","01.csharp\u002F03.advanced-core\u002F03.interfaces-deep-dive",{"title":109,"path":110,"stem":111},"Обробка Винятків","\u002Fcsharp\u002Fadvanced-core\u002Fexception-handling","01.csharp\u002F03.advanced-core\u002F04.exception-handling",{"title":113,"path":114,"stem":115},"Pattern Matching","\u002Fcsharp\u002Fadvanced-core\u002Fpattern-matching","01.csharp\u002F03.advanced-core\u002F05.pattern-matching",{"title":117,"path":118,"stem":119},"Додаткові Можливості C#","\u002Fcsharp\u002Fadvanced-core\u002Fadditional-features","01.csharp\u002F03.advanced-core\u002F06.additional-features",{"title":121,"icon":122,"path":123,"stem":124,"children":125,"page":59},"Architecture Best Practices","i-lucide-building-2","\u002Fcsharp\u002Farchitecture-best-practices","01.csharp\u002F04.architecture-best-practices",[126,130,149,153,157,161,165,169],{"title":127,"path":128,"stem":129},"Software Design Principles (Частина 1)","\u002Fcsharp\u002Farchitecture-best-practices\u002Fsoftware-design-principles","01.csharp\u002F04.architecture-best-practices\u002F01.software-design-principles",{"title":131,"icon":132,"path":133,"stem":134,"children":135,"page":59},"Design Patterns","i-lucide-folder","\u002Fcsharp\u002Farchitecture-best-practices\u002Fdesign-patterns","01.csharp\u002F04.architecture-best-practices\u002F02.design-patterns",[136],{"title":137,"icon":132,"path":138,"stem":139,"children":140,"page":59},"Creational","\u002Fcsharp\u002Farchitecture-best-practices\u002Fdesign-patterns\u002Fcreational","01.csharp\u002F04.architecture-best-practices\u002F02.design-patterns\u002Fcreational",[141,145],{"title":142,"path":143,"stem":144},"Singleton (Одинак)","\u002Fcsharp\u002Farchitecture-best-practices\u002Fdesign-patterns\u002Fcreational\u002Fsingleton","01.csharp\u002F04.architecture-best-practices\u002F02.design-patterns\u002Fcreational\u002F01.singleton",{"title":146,"path":147,"stem":148},"Builder (Будівельник)","\u002Fcsharp\u002Farchitecture-best-practices\u002Fdesign-patterns\u002Fcreational\u002Fbuilder","01.csharp\u002F04.architecture-best-practices\u002F02.design-patterns\u002Fcreational\u002F02.builder",{"title":150,"path":151,"stem":152},"Building Professional CLIs","\u002Fcsharp\u002Farchitecture-best-practices\u002Fbuilding-professional-clis","01.csharp\u002F04.architecture-best-practices\u002F03.building-professional-clis",{"title":154,"path":155,"stem":156},"Validation & Flow Control","\u002Fcsharp\u002Farchitecture-best-practices\u002Fvalidation-flow-control","01.csharp\u002F04.architecture-best-practices\u002F04.validation-flow-control",{"title":158,"path":159,"stem":160},"The Modern .NET Host (Microsoft.Extensions)","\u002Fcsharp\u002Farchitecture-best-practices\u002Fmodern-dotnet-host","01.csharp\u002F04.architecture-best-practices\u002F05.modern-dotnet-host",{"title":162,"path":163,"stem":164},"Data Mapper: Repository та DAO патерни (Частина 1)","\u002Fcsharp\u002Farchitecture-best-practices\u002Fdata-mapper-part1","01.csharp\u002F04.architecture-best-practices\u002F06.data-mapper-part1",{"title":166,"path":167,"stem":168},"Data Mapper: Repository та DAO патерни (Частина 2)","\u002Fcsharp\u002Farchitecture-best-practices\u002Fdata-mapper-part2","01.csharp\u002F04.architecture-best-practices\u002F07.data-mapper-part2",{"title":170,"icon":132,"path":171,"stem":172,"children":173,"page":59},"Di Ioc","\u002Fcsharp\u002Farchitecture-best-practices\u002Fdi-ioc","01.csharp\u002F04.architecture-best-practices\u002F08.di-ioc",[174,178,182,186,190,194,198],{"title":175,"path":176,"stem":177},"Проблема залежностей та Інверсія Контролю","\u002Fcsharp\u002Farchitecture-best-practices\u002Fdi-ioc\u002Fthe-dependency-problem","01.csharp\u002F04.architecture-best-practices\u002F08.di-ioc\u002F01.the-dependency-problem",{"title":179,"path":180,"stem":181},"Будуємо власний Service Container","\u002Fcsharp\u002Farchitecture-best-practices\u002Fdi-ioc\u002Fbuild-your-own-container","01.csharp\u002F04.architecture-best-practices\u002F08.di-ioc\u002F02.build-your-own-container",{"title":183,"path":184,"stem":185},"Service Locator: Паттерн та Анти-паттерн","\u002Fcsharp\u002Farchitecture-best-practices\u002Fdi-ioc\u002Fservice-locator-pattern","01.csharp\u002F04.architecture-best-practices\u002F08.di-ioc\u002F03.service-locator-pattern",{"title":187,"path":188,"stem":189},"Паттерни Dependency Injection","\u002Fcsharp\u002Farchitecture-best-practices\u002Fdi-ioc\u002Fdependency-injection-patterns","01.csharp\u002F04.architecture-best-practices\u002F08.di-ioc\u002F04.dependency-injection-patterns",{"title":191,"path":192,"stem":193},"Microsoft DI: IServiceCollection та IServiceProvider","\u002Fcsharp\u002Farchitecture-best-practices\u002Fdi-ioc\u002Fmicrosoft-di-deep-dive","01.csharp\u002F04.architecture-best-practices\u002F08.di-ioc\u002F05.microsoft-di-deep-dive",{"title":195,"path":196,"stem":197},"Service Lifetimes та Scopes","\u002Fcsharp\u002Farchitecture-best-practices\u002Fdi-ioc\u002Fservice-lifetimes-and-scopes","01.csharp\u002F04.architecture-best-practices\u002F08.di-ioc\u002F06.service-lifetimes-and-scopes",{"title":199,"path":200,"stem":201},"DI Анти-паттерни та Найкращі Практики","\u002Fcsharp\u002Farchitecture-best-practices\u002Fdi-ioc\u002Fdi-anti-patterns-and-best-practices","01.csharp\u002F04.architecture-best-practices\u002F08.di-ioc\u002F07.di-anti-patterns-and-best-practices",{"title":203,"icon":132,"path":204,"stem":205,"children":206,"page":59},"Standard Library","\u002Fcsharp\u002Fstandard-library","01.csharp\u002F05.standard-library",[207,211,215],{"title":208,"path":209,"stem":210},"Collections (Колекції)","\u002Fcsharp\u002Fstandard-library\u002Fcollections","01.csharp\u002F05.standard-library\u002F01.collections",{"title":212,"path":213,"stem":214},"High Performance Types (Високопродуктивні Типи)","\u002Fcsharp\u002Fstandard-library\u002Fhigh-performance-types","01.csharp\u002F05.standard-library\u002F02.high-performance-types",{"title":216,"path":217,"stem":218},"LINQ (Language Integrated Query)","\u002Fcsharp\u002Fstandard-library\u002Flinq","01.csharp\u002F05.standard-library\u002F03.linq",{"title":220,"icon":221,"path":222,"stem":223,"children":224,"page":59},"System Internals Concurrency","i-lucide-server","\u002Fcsharp\u002Fsystem-internals-concurrency","01.csharp\u002F06.system-internals-concurrency",[225,229,233,237,241,245,249],{"title":226,"path":227,"stem":228},"Memory Management","\u002Fcsharp\u002Fsystem-internals-concurrency\u002Fmemory-management","01.csharp\u002F06.system-internals-concurrency\u002F01.memory-management",{"title":230,"path":231,"stem":232},"Reflection API: System.Type та Метадані","\u002Fcsharp\u002Fsystem-internals-concurrency\u002Freflection-fundamentals","01.csharp\u002F06.system-internals-concurrency\u002F02.reflection-fundamentals",{"title":234,"path":235,"stem":236},"Attributes та Dynamic Language Runtime","\u002Fcsharp\u002Fsystem-internals-concurrency\u002Fattributes-dynamic","01.csharp\u002F06.system-internals-concurrency\u002F03.attributes-dynamic",{"title":238,"path":239,"stem":240},"Expression Trees: Швидка Альтернатива Рефлексії","\u002Fcsharp\u002Fsystem-internals-concurrency\u002Fexpression-trees-compiled","01.csharp\u002F06.system-internals-concurrency\u002F04.expression-trees-compiled",{"title":242,"path":243,"stem":244},"Source Generators: Compile-Time Code Generation","\u002Fcsharp\u002Fsystem-internals-concurrency\u002Fsource-generators","01.csharp\u002F06.system-internals-concurrency\u002F05.source-generators",{"title":246,"path":247,"stem":248},"Multithreading Fundamentals","\u002Fcsharp\u002Fsystem-internals-concurrency\u002Fmultithreading-fundamentals","01.csharp\u002F06.system-internals-concurrency\u002F06.multithreading-fundamentals",{"title":250,"path":251,"stem":252},"Synchronization Primitives","\u002Fcsharp\u002Fsystem-internals-concurrency\u002Fsynchronization-primitives","01.csharp\u002F06.system-internals-concurrency\u002F07.synchronization-primitives",{"title":254,"icon":255,"path":256,"stem":257,"children":258,"page":59},"System Programming Windows","i-lucide-cpu","\u002Fcsharp\u002Fsystem-programming-windows","01.csharp\u002F07.system-programming-windows",[259,263,267,271,275,279,283,287,291,295,299,303,307,311,315,319,323,327,331,335,339,343,347,351,355,359,363,367,371,375],{"title":260,"path":261,"stem":262},"Як Працює Операційна Система","\u002Fcsharp\u002Fsystem-programming-windows\u002Fhow-os-works","01.csharp\u002F07.system-programming-windows\u002F01.how-os-works",{"title":264,"path":265,"stem":266},"Процеси в .NET — API та Запуск","\u002Fcsharp\u002Fsystem-programming-windows\u002Fprocesses-in-dotnet","01.csharp\u002F07.system-programming-windows\u002F02.processes-in-dotnet",{"title":268,"path":269,"stem":270},"Процеси в .NET — IPC та Міжпроцесна Комунікація","\u002Fcsharp\u002Fsystem-programming-windows\u002F02a.processes-ipc","01.csharp\u002F07.system-programming-windows\u002F02a.processes-ipc",{"title":272,"path":273,"stem":274},"Application Domains та Збірки — AppDomain і AssemblyLoadContext","\u002Fcsharp\u002Fsystem-programming-windows\u002Fappdomains-assemblies","01.csharp\u002F07.system-programming-windows\u002F03.appdomains-assemblies",{"title":276,"path":277,"stem":278},"Application Domains та Збірки — Plug-in Система з Hot-Reload","\u002Fcsharp\u002Fsystem-programming-windows\u002F03a.appdomains-plugin-system","01.csharp\u002F07.system-programming-windows\u002F03a.appdomains-plugin-system",{"title":280,"path":281,"stem":282},"Потоки — Основи та API Thread","\u002Fcsharp\u002Fsystem-programming-windows\u002Fthread-fundamentals","01.csharp\u002F07.system-programming-windows\u002F04.thread-fundamentals",{"title":284,"path":285,"stem":286},"Потоки — Lifecycle, Пріоритети та Безпечне Завершення","\u002Fcsharp\u002Fsystem-programming-windows\u002F04a.thread-lifecycle-priorities","01.csharp\u002F07.system-programming-windows\u002F04a.thread-lifecycle-priorities",{"title":288,"path":289,"stem":290},"Проблеми Спільного Стану — Race Condition та Data Race","\u002Fcsharp\u002Fsystem-programming-windows\u002Fshared-state-problems","01.csharp\u002F07.system-programming-windows\u002F05.shared-state-problems",{"title":292,"path":293,"stem":294},"Проблеми Спільного Стану — Memory Model та volatile","\u002Fcsharp\u002Fsystem-programming-windows\u002F05a.shared-state-memory-model","01.csharp\u002F07.system-programming-windows\u002F05a.shared-state-memory-model",{"title":296,"path":297,"stem":298},"Синхронізація — Monitor, lock та еволюція примітивів","\u002Fcsharp\u002Fsystem-programming-windows\u002Fsynchronization-fundamentals","01.csharp\u002F07.system-programming-windows\u002F06.synchronization-fundamentals",{"title":300,"path":301,"stem":302},"Синхронізація — Наскрізний Приклад та Deadlock Detection","\u002Fcsharp\u002Fsystem-programming-windows\u002F06a.synchronization-walkthrough","01.csharp\u002F07.system-programming-windows\u002F06a.synchronization-walkthrough",{"title":304,"path":305,"stem":306},"Синхронізація — Mutex, Semaphore та Event-Based Primitives","\u002Fcsharp\u002Fsystem-programming-windows\u002Fsynchronization-advanced","01.csharp\u002F07.system-programming-windows\u002F07.synchronization-advanced",{"title":308,"path":309,"stem":310},"Синхронізація — Interlocked, Volatile та Lock-Free Структури","\u002Fcsharp\u002Fsystem-programming-windows\u002F07a.synchronization-advanced-walkthrough","01.csharp\u002F07.system-programming-windows\u002F07a.synchronization-advanced-walkthrough",{"title":312,"path":313,"stem":314},"Interlocked, CAS та Lock-Free Структури","\u002Fcsharp\u002Fsystem-programming-windows\u002Finterlocked-cas-lockfree","01.csharp\u002F07.system-programming-windows\u002F08.interlocked-cas-lockfree",{"title":316,"path":317,"stem":318},"Volatile, Memory Model та Spinning","\u002Fcsharp\u002Fsystem-programming-windows\u002F08a.volatile-memory-model","01.csharp\u002F07.system-programming-windows\u002F08a.volatile-memory-model",{"title":320,"path":321,"stem":322},"ThreadPool — Пул Потоків для Ефективного Виконання","\u002Fcsharp\u002Fsystem-programming-windows\u002Fthread-pool","01.csharp\u002F07.system-programming-windows\u002F09.thread-pool",{"title":324,"path":325,"stem":326},"ThreadPool — Просунуті Сценарії та Внутрішня Будова","\u002Fcsharp\u002Fsystem-programming-windows\u002F09a.thread-pool-advanced","01.csharp\u002F07.system-programming-windows\u002F09a.thread-pool-advanced",{"title":328,"path":329,"stem":330},"Concurrent та Immutable Collections","\u002Fcsharp\u002Fsystem-programming-windows\u002Fconcurrent-collections","01.csharp\u002F07.system-programming-windows\u002F10.concurrent-collections",{"title":332,"path":333,"stem":334},"TPL, Task та Композиція — Від Thread до Task","\u002Fcsharp\u002Fsystem-programming-windows\u002Ftpl-parallel-plinq","01.csharp\u002F07.system-programming-windows\u002F11.tpl-parallel-plinq",{"title":336,"path":337,"stem":338},"Parallel Class та PLINQ — Data Parallelism","\u002Fcsharp\u002Fsystem-programming-windows\u002F11a.tpl-parallel-plinq-advanced","01.csharp\u002F07.system-programming-windows\u002F11a.tpl-parallel-plinq-advanced",{"title":340,"path":341,"stem":342},"Async\u002FAwait — Фундамент Асинхронного Програмування","\u002Fcsharp\u002Fsystem-programming-windows\u002Fasync-fundamentals","01.csharp\u002F07.system-programming-windows\u002F12.async-fundamentals",{"title":344,"path":345,"stem":346},"SynchronizationContext та ConfigureAwait — Контекст Виконання","\u002Fcsharp\u002Fsystem-programming-windows\u002Fasync-context-configureawait","01.csharp\u002F07.system-programming-windows\u002F13.async-context-configureawait",{"title":348,"path":349,"stem":350},"Async — Просунуті Паттерни","\u002Fcsharp\u002Fsystem-programming-windows\u002Fasync-advanced","01.csharp\u002F07.system-programming-windows\u002F14.async-advanced",{"title":352,"path":353,"stem":354},"System.Threading.Channels — Async Producer-Consumer","\u002Fcsharp\u002Fsystem-programming-windows\u002Fchannels","01.csharp\u002F07.system-programming-windows\u002F15.channels",{"title":356,"path":357,"stem":358},"Асинхронна Синхронізація","\u002Fcsharp\u002Fsystem-programming-windows\u002Fasync-synchronization","01.csharp\u002F07.system-programming-windows\u002F16.async-synchronization",{"title":360,"path":361,"stem":362},"Unsafe Code та Вказівники","\u002Fcsharp\u002Fsystem-programming-windows\u002Funsafe-code","01.csharp\u002F07.system-programming-windows\u002F17.unsafe-code",{"title":364,"path":365,"stem":366},"P\u002FInvoke та Windows API — Міст між .NET та Native Code","\u002Fcsharp\u002Fsystem-programming-windows\u002Fpinvoke-winapi","01.csharp\u002F07.system-programming-windows\u002F18.pinvoke-winapi",{"title":368,"path":369,"stem":370},"Реєстр Windows — Центральна База Конфігурації Системи","\u002Fcsharp\u002Fsystem-programming-windows\u002Fwindows-registry","01.csharp\u002F07.system-programming-windows\u002F19.windows-registry",{"title":372,"path":373,"stem":374},"Windows Hooks, Hotkeys та Services — Глибока Інтеграція з ОС","\u002Fcsharp\u002Fsystem-programming-windows\u002Fwindows-hooks-services","01.csharp\u002F07.system-programming-windows\u002F20.windows-hooks-services",{"title":376,"path":377,"stem":378},"Системне Програмування C# (Windows) — 07.system-programming-windows","\u002Fcsharp\u002Fsystem-programming-windows\u002Fimplementation_plan","01.csharp\u002F07.system-programming-windows\u002Fimplementation_plan",{"title":380,"icon":132,"path":381,"stem":382,"children":383,"page":59},"Io","\u002Fcsharp\u002Fio","01.csharp\u002F08.io",[384,388,392,396,400],{"title":385,"path":386,"stem":387},"8.1.1. Основи роботи з файловою системою","\u002Fcsharp\u002Fio\u002Ffile-system-basics","01.csharp\u002F08.io\u002F01.file-system-basics",{"title":389,"path":390,"stem":391},"8.1.2. Потоки (Streams) та Серіалізація Даних","\u002Fcsharp\u002Fio\u002Fstreams-serialization","01.csharp\u002F08.io\u002F02.streams-serialization",{"title":393,"path":394,"stem":395},"8.2.1. JSON Serialization з System.Text.Json","\u002Fcsharp\u002Fio\u002Fjson-serialization","01.csharp\u002F08.io\u002F03.json-serialization",{"title":397,"path":398,"stem":399},"8.2.2. XML Serialization та LINQ to XML","\u002Fcsharp\u002Fio\u002Fxml-serialization","01.csharp\u002F08.io\u002F04.xml-serialization",{"title":401,"path":402,"stem":403},"8.2.3. Binary Serialization: MessagePack та Protocol Buffers","\u002Fcsharp\u002Fio\u002Fbinary-serialization","01.csharp\u002F08.io\u002F05.binary-serialization",{"title":405,"icon":132,"path":406,"stem":407,"children":408,"page":59},"Ado Net","\u002Fcsharp\u002Fado-net","01.csharp\u002F09.ado-net",[409,413,417,421,425,429,433,437,441,445,449,453],{"title":410,"path":411,"stem":412},"9.1. Введення в ADO.NET","\u002Fcsharp\u002Fado-net\u002Fintroduction-to-adonet","01.csharp\u002F09.ado-net\u002F01.introduction-to-adonet",{"title":414,"path":415,"stem":416},"9.2. Клас DbConnection — з'єднання з базою даних","\u002Fcsharp\u002Fado-net\u002Fconnection","01.csharp\u002F09.ado-net\u002F02.connection",{"title":418,"path":419,"stem":420},"9.3. Клас DbCommand — виконання SQL-запитів","\u002Fcsharp\u002Fado-net\u002Fcommand-and-queries","01.csharp\u002F09.ado-net\u002F03.command-and-queries",{"title":422,"path":423,"stem":424},"9.4. Клас DbDataReader — ефективне читання даних","\u002Fcsharp\u002Fado-net\u002Fdatareader","01.csharp\u002F09.ado-net\u002F04.datareader",{"title":426,"path":427,"stem":428},"9.5. Параметризовані запити та захист від SQL Injection","\u002Fcsharp\u002Fado-net\u002Fparameters-and-sql-injection","01.csharp\u002F09.ado-net\u002F05.parameters-and-sql-injection",{"title":430,"path":431,"stem":432},"9.6. Транзакції в ADO.NET","\u002Fcsharp\u002Fado-net\u002Ftransactions","01.csharp\u002F09.ado-net\u002F06.transactions",{"title":434,"path":435,"stem":436},"9.7. DbProviderFactory — провайдер-незалежний код","\u002Fcsharp\u002Fado-net\u002Fprovider-factory","01.csharp\u002F09.ado-net\u002F07.provider-factory",{"title":438,"path":439,"stem":440},"9.8. Асинхронний доступ до даних","\u002Fcsharp\u002Fado-net\u002Fasync-data-access","01.csharp\u002F09.ado-net\u002F08.async-data-access",{"title":442,"path":443,"stem":444},"9.9. Від'єднаний режим: DataSet, DataTable, DataRow","\u002Fcsharp\u002Fado-net\u002Fdisconnected-mode-dataset","01.csharp\u002F09.ado-net\u002F09.disconnected-mode-dataset",{"title":446,"path":447,"stem":448},"9.10. DataAdapter — міст між DataSet та базою даних","\u002Fcsharp\u002Fado-net\u002Fdata-adapter","01.csharp\u002F09.ado-net\u002F10.data-adapter",{"title":450,"path":451,"stem":452},"9.11. Data Mapper та Repository: Архітектура доступу до даних","\u002Fcsharp\u002Fado-net\u002Fdata-mapper-repository","01.csharp\u002F09.ado-net\u002F11.data-mapper-repository",{"title":454,"path":455,"stem":456},"9.12. Identity Map, Unit of Work та Specification Pattern","\u002Fcsharp\u002Fado-net\u002Fadvanced-patterns","01.csharp\u002F09.ado-net\u002F12.advanced-patterns",{"title":458,"icon":255,"path":459,"stem":460,"children":461,"page":59},"Ef Core","\u002Fcsharp\u002Fef-core","01.csharp\u002F10.ef-core",[462,466,470,474,478,482,486,490,494,498,502,506,510,514,518,522,526,532,538,542,546,550,554,558,562,566,570,574,578,582,586,590,594,598,602,606,610,614,618,622,626,630,634,638,642,646],{"title":463,"path":464,"stem":465},"Що таке ORM? Від SQL до об'єктів","\u002Fcsharp\u002Fef-core\u002Fwhat-is-orm","01.csharp\u002F10.ef-core\u002F01.what-is-orm",{"title":467,"path":468,"stem":469},"Перший проєкт — від нуля до CRUD","\u002Fcsharp\u002Fef-core\u002Ffirst-project","01.csharp\u002F10.ef-core\u002F02.first-project",{"title":471,"path":472,"stem":473},"DbContext — Серце EF Core","\u002Fcsharp\u002Fef-core\u002Fdbcontext-deep-dive","01.csharp\u002F10.ef-core\u002F03.dbcontext-deep-dive",{"title":475,"path":476,"stem":477},"Провайдери баз даних — Архітектура та Вибір СУБД","\u002Fcsharp\u002Fef-core\u002Fdatabase-providers","01.csharp\u002F10.ef-core\u002F04.database-providers",{"title":479,"path":480,"stem":481},"Конвенції EF Core — Магія без конфігурації","\u002Fcsharp\u002Fef-core\u002Fconventions","01.csharp\u002F10.ef-core\u002F05.conventions",{"title":483,"path":484,"stem":485},"Fluent API та Data Annotations — Явна конфігурація моделі","\u002Fcsharp\u002Fef-core\u002Ffluent-api-vs-annotations","01.csharp\u002F10.ef-core\u002F06.fluent-api-vs-annotations",{"title":487,"path":488,"stem":489},"Зв'язки — One-to-One та One-to-Many","\u002Fcsharp\u002Fef-core\u002Frelationships-basics","01.csharp\u002F10.ef-core\u002F07.relationships-basics",{"title":491,"path":492,"stem":493},"Зв'язки Advanced — Many-to-Many та Складні Сценарії","\u002Fcsharp\u002Fef-core\u002Frelationships-advanced","01.csharp\u002F10.ef-core\u002F08.relationships-advanced",{"title":495,"path":496,"stem":497},"Властивості — Типи, Конвертери, Компаратори (Частина 1)","\u002Fcsharp\u002Fef-core\u002Fproperty-configuration-part1","01.csharp\u002F10.ef-core\u002F09.property-configuration-part1",{"title":499,"path":500,"stem":501},"Властивості — Value Comparers, Generators, Shadow Properties (Частина 2)","\u002Fcsharp\u002Fef-core\u002Fproperty-configuration-part2","01.csharp\u002F10.ef-core\u002F09.property-configuration-part2",{"title":503,"path":504,"stem":505},"Складні типи — Owned Types та Complex Types (Частина 1)","\u002Fcsharp\u002Fef-core\u002Fcomplex-types-owned-part1","01.csharp\u002F10.ef-core\u002F10.complex-types-owned-part1",{"title":507,"path":508,"stem":509},"Складні типи — Complex Types, Keyless Entities, Порівняння (Частина 2)","\u002Fcsharp\u002Fef-core\u002Fcomplex-types-owned-part2","01.csharp\u002F10.ef-core\u002F10.complex-types-owned-part2",{"title":511,"path":512,"stem":513},"JSON Columns — Складні дані у JSON (Частина 1)","\u002Fcsharp\u002Fef-core\u002Fjson-columns-part1","01.csharp\u002F10.ef-core\u002F11.json-columns-part1",{"title":515,"path":516,"stem":517},"JSON Columns — Value Comparers, Індекси, Провайдери (Частина 2)","\u002Fcsharp\u002Fef-core\u002Fjson-columns-part2","01.csharp\u002F10.ef-core\u002F11.json-columns-part2",{"title":519,"path":520,"stem":521},"Успадкування — Абстрактні класи та TPH (Частина 1)","\u002Fcsharp\u002Fef-core\u002Finheritance-part1","01.csharp\u002F10.ef-core\u002F12.inheritance-part1",{"title":523,"path":524,"stem":525},"Успадкування — TPT, TPC та Порівняння Стратегій (Частина 2)","\u002Fcsharp\u002Fef-core\u002Finheritance-part2","01.csharp\u002F10.ef-core\u002F12.inheritance-part2",{"title":527,"path":528,"stem":529,"children":530},"Індекси, Обмеження та Схема (Частина 1)","\u002Fcsharp\u002Fef-core\u002Findexes-constraints-part1","01.csharp\u002F10.ef-core\u002F13.indexes-constraints-part1",[531],{"title":527,"path":528,"stem":529},{"title":533,"path":534,"stem":535,"children":536},"Індекси, Обмеження та Схема (Частина 2)","\u002Fcsharp\u002Fef-core\u002Findexes-constraints-part2","01.csharp\u002F10.ef-core\u002F13.indexes-constraints-part2",[537],{"title":533,"path":534,"stem":535},{"title":539,"path":540,"stem":541},"Seed Data — Початкові Дані (Частина 1)","\u002Fcsharp\u002Fef-core\u002Fseeding-part1","01.csharp\u002F10.ef-core\u002F14.seeding-part1",{"title":543,"path":544,"stem":545},"Seed Data — SQL-скрипти, Bogus та Стратегії (Частина 2)","\u002Fcsharp\u002Fef-core\u002Fseeding-part2","01.csharp\u002F10.ef-core\u002F14.seeding-part2",{"title":547,"path":548,"stem":549},"Global Query Filters — Глобальні Фільтри (Частина 1)","\u002Fcsharp\u002Fef-core\u002Fglobal-query-filters-part1","01.csharp\u002F10.ef-core\u002F15.global-query-filters-part1",{"title":551,"path":552,"stem":553},"Global Query Filters — Підводні камені та Інтеграція (Частина 2)","\u002Fcsharp\u002Fef-core\u002Fglobal-query-filters-part2","01.csharp\u002F10.ef-core\u002F15.global-query-filters-part2",{"title":555,"path":556,"stem":557},"LINQ-запити в EF Core (Частина 1)","\u002Fcsharp\u002Fef-core\u002Flinq-queries-part1","01.csharp\u002F10.ef-core\u002F16.linq-queries-part1",{"title":559,"path":560,"stem":561},"LINQ-запити в EF Core (Частина 2)","\u002Fcsharp\u002Fef-core\u002Flinq-queries-part2","01.csharp\u002F10.ef-core\u002F16.linq-queries-part2",{"title":563,"path":564,"stem":565},"Завантаження Пов'язаних Даних (Частина 1)","\u002Fcsharp\u002Fef-core\u002Floading-related-data-part1","01.csharp\u002F10.ef-core\u002F17.loading-related-data-part1",{"title":567,"path":568,"stem":569},"Завантаження Пов'язаних Даних (Частина 2)","\u002Fcsharp\u002Fef-core\u002Floading-related-data-part2","01.csharp\u002F10.ef-core\u002F17.loading-related-data-part2",{"title":571,"path":572,"stem":573},"Raw SQL, Views та Stored Procedures (Частина 1)","\u002Fcsharp\u002Fef-core\u002Fraw-sql-part1","01.csharp\u002F10.ef-core\u002F18.raw-sql-part1",{"title":575,"path":576,"stem":577},"Raw SQL — Stored Procedures, DbFunction та Bulk Operations (Частина 2)","\u002Fcsharp\u002Fef-core\u002Fraw-sql-part2","01.csharp\u002F10.ef-core\u002F18.raw-sql-part2",{"title":579,"path":580,"stem":581},"Продвинуті Запити — Compiled Queries, Bulk та Оптимізація (Частина 1)","\u002Fcsharp\u002Fef-core\u002Fadvanced-queries-part1","01.csharp\u002F10.ef-core\u002F19.advanced-queries-part1",{"title":583,"path":584,"stem":585},"Продвинуті Запити — Query Tags, Bulk та Interceptors (Частина 2)","\u002Fcsharp\u002Fef-core\u002Fadvanced-queries-part2","01.csharp\u002F10.ef-core\u002F19.advanced-queries-part2",{"title":587,"path":588,"stem":589},"Change Tracker — Відстеження Змін (Частина 1)","\u002Fcsharp\u002Fef-core\u002Fchange-tracking-part1","01.csharp\u002F10.ef-core\u002F20.change-tracking-part1",{"title":591,"path":592,"stem":593},"Change Tracker — Графи Об'єктів та Disconnected (Частина 2)","\u002Fcsharp\u002Fef-core\u002Fchange-tracking-part2","01.csharp\u002F10.ef-core\u002F20.change-tracking-part2",{"title":595,"path":596,"stem":597},"Збереження Даних та Транзакції (Частина 1)","\u002Fcsharp\u002Fef-core\u002Fsaving-data-part1","01.csharp\u002F10.ef-core\u002F21.saving-data-part1",{"title":599,"path":600,"stem":601},"Збереження Даних — Concurrency та Outbox (Частина 2)","\u002Fcsharp\u002Fef-core\u002Fsaving-data-part2","01.csharp\u002F10.ef-core\u002F21.saving-data-part2",{"title":603,"path":604,"stem":605},"Конкурентність та Блокування (Частина 1)","\u002Fcsharp\u002Fef-core\u002Fconcurrency-part1","01.csharp\u002F10.ef-core\u002F22.concurrency-part1",{"title":607,"path":608,"stem":609},"Конкурентність — Дедлоки та Queue Processing (Частина 2)","\u002Fcsharp\u002Fef-core\u002Fconcurrency-part2","01.csharp\u002F10.ef-core\u002F22.concurrency-part2",{"title":611,"path":612,"stem":613},"Міграції в EF Core — Основи (Частина 1)","\u002Fcsharp\u002Fef-core\u002Fmigrations-basics-part1","01.csharp\u002F10.ef-core\u002F23.migrations-basics-part1",{"title":615,"path":616,"stem":617},"Міграції в EF Core — Основи (Частина 2)","\u002Fcsharp\u002Fef-core\u002Fmigrations-basics-part2","01.csharp\u002F10.ef-core\u002F23.migrations-basics-part2",{"title":619,"path":620,"stem":621},"Міграції — Просунуті Сценарії (Частина 1)","\u002Fcsharp\u002Fef-core\u002Fmigrations-advanced-part1","01.csharp\u002F10.ef-core\u002F24.migrations-advanced-part1",{"title":623,"path":624,"stem":625},"Міграції — Просунуті Сценарії (Частина 2)","\u002Fcsharp\u002Fef-core\u002Fmigrations-advanced-part2","01.csharp\u002F10.ef-core\u002F24.migrations-advanced-part2",{"title":627,"path":628,"stem":629},"Управління Схемою та Database-First (Частина 1)","\u002Fcsharp\u002Fef-core\u002Fschema-management-part1","01.csharp\u002F10.ef-core\u002F25.schema-management-part1",{"title":631,"path":632,"stem":633},"Управління Схемою та Database-First (Частина 2)","\u002Fcsharp\u002Fef-core\u002Fschema-management-part2","01.csharp\u002F10.ef-core\u002F25.schema-management-part2",{"title":635,"path":636,"stem":637},"Продуктивність EF Core — Основи (Частина 1)","\u002Fcsharp\u002Fef-core\u002Fperformance-fundamentals-part1","01.csharp\u002F10.ef-core\u002F26.performance-fundamentals-part1",{"title":639,"path":640,"stem":641},"Interceptors в EF Core (Частина 1)","\u002Fcsharp\u002Fef-core\u002Finterceptors-part1","01.csharp\u002F10.ef-core\u002F29.interceptors-part1",{"title":643,"path":644,"stem":645},"Interceptors в EF Core — Connection, Transaction та Materialization (Частина 2)","\u002Fcsharp\u002Fef-core\u002Finterceptors-part2","01.csharp\u002F10.ef-core\u002F29.interceptors-part2",{"title":647,"path":648,"stem":649},"План вивчення Entity Framework Core — Повний курс","\u002Fcsharp\u002Fef-core\u002Fimplementation_plan","01.csharp\u002F10.ef-core\u002Fimplementation_plan",{"title":651,"icon":652,"path":653,"stem":654,"children":655,"page":59},"ASP.NET","i-devicon-dotnetcore","\u002Fcsharp\u002Faspnet","01.csharp\u002F11.aspnet",[656,730,791,869,927,941,967,1057,1111,1182,1212,1289,1346],{"title":657,"icon":658,"path":659,"stem":660,"children":661,"page":59},"Minimal API","i-lucide-network","\u002Fcsharp\u002Faspnet\u002Fminimal-api","01.csharp\u002F11.aspnet\u002F01.minimal-api",[662,666,670,674,678,682,686,690,694,698,702,706,710,714,718,722,726],{"title":663,"path":664,"stem":665},"Вступ до ASP.NET та еволюція фреймворку","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Fintroduction","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F01.introduction",{"title":667,"path":668,"stem":669},"Перший додаток на ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Ffirst-application","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F02.first-application",{"title":671,"path":672,"stem":673},"WebApplication, Builder та Dependency Injection","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Fwebapplication-builder","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F03.webapplication-builder",{"title":675,"path":676,"stem":677},"Конвеєр запитів та Middleware","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Frequest-pipeline-middleware","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F04.request-pipeline-middleware",{"title":679,"path":680,"stem":681},"Маршрутизація в ASP.NET Core: Основи","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Frouting-basics","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F05.routing-basics",{"title":683,"path":684,"stem":685},"Маршрутизація в ASP.NET Core: Розширені можливості","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Frouting-advanced","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F06.routing-advanced",{"title":687,"path":688,"stem":689},"Статичні файли в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Fstatic-files","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F07.static-files",{"title":691,"path":692,"stem":693},"Статичні Активи: MapStaticAssets (ASP.NET Core 9.0)","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Fstatic-assets","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F08.static-assets",{"title":695,"path":696,"stem":697},"Конфігурація в ASP.NET Core: Основи","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Fconfiguration-fundamentals","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F09.configuration-fundamentals",{"title":699,"path":700,"stem":701},"Конфігурація: Паттерн Options","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Fconfiguration-options","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F10.configuration-options",{"title":703,"path":704,"stem":705},"Логування в ASP.NET Core: Основи","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Flogging-basics","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F11.logging-basics",{"title":707,"path":708,"stem":709},"Логування: Serilog та Middleware","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Flogging-advanced","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F12.logging-advanced",{"title":711,"path":712,"stem":713},"Управління станом: HttpContext.Items та Cookies","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Fstate-management","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F13.state-management",{"title":715,"path":716,"stem":717},"Стан сесії: Sessions","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Fsession-state","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F14.session-state",{"title":719,"path":720,"stem":721},"Структура проєкту: від хаосу до архітектури","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Fproject-structure","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F15.project-structure",{"title":723,"path":724,"stem":725},"Scalar у Minimal API: повний проєкт і Fluent OpenAPI","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Fscalar-openapi-fluent","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F16.scalar-openapi-fluent",{"title":727,"path":728,"stem":729},"Swagger \u002F Swashbuckle у Minimal API: окремий класичний шлях","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Fswagger-swashbuckle","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F17.swagger-swashbuckle",{"title":731,"icon":658,"path":732,"stem":733,"children":734,"page":59},"API","\u002Fcsharp\u002Faspnet\u002Fapi","01.csharp\u002F11.aspnet\u002F02.api",[735,739,743,747,751,755,759,763,767,771,775,779,783,787],{"title":736,"path":737,"stem":738},"Що таке API. Клієнт-серверна архітектура","\u002Fcsharp\u002Faspnet\u002Fapi\u002Fwhat-is-api","01.csharp\u002F11.aspnet\u002F02.api\u002F01.what-is-api",{"title":740,"path":741,"stem":742},"Формати даних: JSON, XML, TOML та бінарні формати","\u002Fcsharp\u002Faspnet\u002Fapi\u002Fdata-formats","01.csharp\u002F11.aspnet\u002F02.api\u002F02.data-formats",{"title":744,"path":745,"stem":746},"Парадигми API та концепція REST","\u002Fcsharp\u002Faspnet\u002Fapi\u002Fapi-paradigms-rest","01.csharp\u002F11.aspnet\u002F02.api\u002F03.api-paradigms-rest",{"title":748,"path":749,"stem":750},"HTTP-методи, статус-коди та заголовки","\u002Fcsharp\u002Faspnet\u002Fapi\u002Fhttp-methods-status-codes","01.csharp\u002F11.aspnet\u002F02.api\u002F04.http-methods-status-codes",{"title":752,"path":753,"stem":754},"Організація HTTP API за принципами REST","\u002Fcsharp\u002Faspnet\u002Fapi\u002Frest-organizing","01.csharp\u002F11.aspnet\u002F02.api\u002F05.rest-organizing",{"title":756,"path":757,"stem":758},"Номенклатура URL та CRUD-операції","\u002Fcsharp\u002Faspnet\u002Fapi\u002Furl-nomenclature-crud","01.csharp\u002F11.aspnet\u002F02.api\u002F06.url-nomenclature-crud",{"title":760,"path":761,"stem":762},"Правила дизайну: іменування та стандарти","\u002Fcsharp\u002Faspnet\u002Fapi\u002Fapi-design-naming","01.csharp\u002F11.aspnet\u002F02.api\u002F07.api-design-naming",{"title":764,"path":765,"stem":766},"Валідація, ліміти та обробка помилок","\u002Fcsharp\u002Faspnet\u002Fapi\u002Fapi-design-validation","01.csharp\u002F11.aspnet\u002F02.api\u002F08.api-design-validation",{"title":768,"path":769,"stem":770},"Обробка помилок у Minimal API","\u002Fcsharp\u002Faspnet\u002Fapi\u002Ferror-handling-http","01.csharp\u002F11.aspnet\u002F02.api\u002F09.error-handling-http",{"title":772,"path":773,"stem":774},"Ідемпотентність та синхронізація стану","\u002Fcsharp\u002Faspnet\u002Fapi\u002Fidempotency-sync","01.csharp\u002F11.aspnet\u002F02.api\u002F10.idempotency-sync",{"title":776,"path":777,"stem":778},"Пагінація та організація списків","\u002Fcsharp\u002Faspnet\u002Fapi\u002Fpagination-lists","01.csharp\u002F11.aspnet\u002F02.api\u002F11.pagination-lists",{"title":780,"path":781,"stem":782},"Безпека API, кешування та інтернаціоналізація","\u002Fcsharp\u002Faspnet\u002Fapi\u002Fsecurity-auth","01.csharp\u002F11.aspnet\u002F02.api\u002F12.security-auth",{"title":784,"path":785,"stem":786},"Процес проєктування API та документування","\u002Fcsharp\u002Faspnet\u002Fapi\u002Fapi-design-process","01.csharp\u002F11.aspnet\u002F02.api\u002F13.api-design-process",{"title":788,"path":789,"stem":790},"OpenAPI: контракт, специфікація та документація API","\u002Fcsharp\u002Faspnet\u002Fapi\u002Fopenapi","01.csharp\u002F11.aspnet\u002F02.api\u002F14.openapi",{"title":792,"icon":793,"path":794,"stem":795,"children":796,"page":59},"Auth","i-lucide-shield-check","\u002Fcsharp\u002Faspnet\u002Fauth","01.csharp\u002F11.aspnet\u002F03.auth",[797,801,805,809,813,817,821,825,829,833,837,841,845,849,853,857,861,865],{"title":798,"path":799,"stem":800},"Основи аутентифікації та авторизації","\u002Fcsharp\u002Faspnet\u002Fauth\u002Fauth-fundamentals","01.csharp\u002F11.aspnet\u002F03.auth\u002F01.auth-fundamentals",{"title":802,"path":803,"stem":804},"JWT-аутентифікація","\u002Fcsharp\u002Faspnet\u002Fauth\u002Fjwt-authentication","01.csharp\u002F11.aspnet\u002F03.auth\u002F02.jwt-authentication",{"title":806,"path":807,"stem":808},"Авторизація: ролі, політики та resource-based доступ","\u002Fcsharp\u002Faspnet\u002Fauth\u002Fauthorization-policies","01.csharp\u002F11.aspnet\u002F03.auth\u002F03.authorization-policies",{"title":810,"path":811,"stem":812},"Cookie-аутентифікація та ASP.NET Core Identity","\u002Fcsharp\u002Faspnet\u002Fauth\u002Fcookie-auth-identity","01.csharp\u002F11.aspnet\u002F03.auth\u002F04.cookie-auth-identity",{"title":814,"path":815,"stem":816},"JWT + Refresh Tokens (HttpOnly Cookie)","\u002Fcsharp\u002Faspnet\u002Fauth\u002F04b.identity-auth-jwt","01.csharp\u002F11.aspnet\u002F03.auth\u002F04b.identity-auth-jwt",{"title":818,"path":819,"stem":820},"Identity: Підтвердження Email та Скидання Пароля","\u002Fcsharp\u002Faspnet\u002Fauth\u002Fidentity-email-confirmation","01.csharp\u002F11.aspnet\u002F03.auth\u002F05.identity-email-confirmation",{"title":822,"path":823,"stem":824},"Identity: Двофакторна Аутентифікація (2FA)","\u002Fcsharp\u002Faspnet\u002Fauth\u002Fidentity-two-factor","01.csharp\u002F11.aspnet\u002F03.auth\u002F06.identity-two-factor",{"title":826,"path":827,"stem":828},"Identity: Внутрішня Архітектура та Кастомізація","\u002Fcsharp\u002Faspnet\u002Fauth\u002Fidentity-internals","01.csharp\u002F11.aspnet\u002F03.auth\u002F07.identity-internals",{"title":830,"path":831,"stem":832},"OAuth 2.0 та зовнішні провайдери","\u002Fcsharp\u002Faspnet\u002Fauth\u002Foauth-external-providers","01.csharp\u002F11.aspnet\u002F03.auth\u002F08.oauth-external-providers",{"title":834,"path":835,"stem":836},"Безпека на практиці: CORS, HTTPS та захист від атак","\u002Fcsharp\u002Faspnet\u002Fauth\u002Fsecurity-hardening","01.csharp\u002F11.aspnet\u002F03.auth\u002F09.security-hardening",{"title":838,"path":839,"stem":840},"Теорія OAuth 2.0: Поняття, Аналогії та Флоу","\u002Fcsharp\u002Faspnet\u002Fauth\u002Foauth-theory","01.csharp\u002F11.aspnet\u002F03.auth\u002F10.oauth-theory",{"title":842,"path":843,"stem":844},"OIDC, OAuth 2.0 та Keycloak в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Fauth\u002Foidc-keycloak","01.csharp\u002F11.aspnet\u002F03.auth\u002F10.oidc-keycloak",{"title":846,"path":847,"stem":848},"API Keys аутентифікація в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Fauth\u002Fapi-keys","01.csharp\u002F11.aspnet\u002F03.auth\u002F11.api-keys",{"title":850,"path":851,"stem":852},"Rate Limiting та Throttling в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Fauth\u002Frate-limiting","01.csharp\u002F11.aspnet\u002F03.auth\u002F12.rate-limiting",{"title":854,"path":855,"stem":856},"Refresh Token Rotation в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Fauth\u002Frefresh-token-rotation","01.csharp\u002F11.aspnet\u002F03.auth\u002F13.refresh-token-rotation",{"title":858,"path":859,"stem":860},"Certificate Authentication та mTLS в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Fauth\u002Fcertificate-auth","01.csharp\u002F11.aspnet\u002F03.auth\u002F14.certificate-auth",{"title":862,"path":863,"stem":864},"RBAC, ABAC та ReBAC в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Fauth\u002Frbac-abac-rebac","01.csharp\u002F11.aspnet\u002F03.auth\u002F15.rbac-abac-rebac",{"title":866,"path":867,"stem":868},"Multi-tenancy та ізоляція даних в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Fauth\u002Fmulti-tenancy","01.csharp\u002F11.aspnet\u002F03.auth\u002F16.multi-tenancy",{"title":870,"icon":871,"path":872,"stem":873,"children":874,"page":59},"Нотифікації","i-lucide-bell","\u002Fcsharp\u002Faspnet\u002Fnotifications","01.csharp\u002F11.aspnet\u002F04.notifications",[875,879,883,887,891,895,899,903,907,911,915,919,923],{"title":876,"path":877,"stem":878},"In-App нотифікації через базу даних","\u002Fcsharp\u002Faspnet\u002Fnotifications\u002Fin-app-database-notifications","01.csharp\u002F11.aspnet\u002F04.notifications\u002F01.in-app-database-notifications",{"title":880,"path":881,"stem":882},"Polling: Регулярний запит оновлень","\u002Fcsharp\u002Faspnet\u002Fnotifications\u002Fpolling","01.csharp\u002F11.aspnet\u002F04.notifications\u002F02.polling",{"title":884,"path":885,"stem":886},"Server-Sent Events: Однострімовий push від сервера","\u002Fcsharp\u002Faspnet\u002Fnotifications\u002Fserver-sent-events","01.csharp\u002F11.aspnet\u002F04.notifications\u002F03.server-sent-events",{"title":888,"path":889,"stem":890},"WebSockets: Двостороннє з'єднання в реальному часі","\u002Fcsharp\u002Faspnet\u002Fnotifications\u002Fwebsockets","01.csharp\u002F11.aspnet\u002F04.notifications\u002F04.websockets",{"title":892,"path":893,"stem":894},"SignalR: Абстракція над транспортами реального часу","\u002Fcsharp\u002Faspnet\u002Fnotifications\u002Fsignalr","01.csharp\u002F11.aspnet\u002F04.notifications\u002F05.signalr",{"title":896,"path":897,"stem":898},"Background Services: Фонові задачі в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Fnotifications\u002Fbackground-services","01.csharp\u002F11.aspnet\u002F04.notifications\u002F06.background-services",{"title":900,"path":901,"stem":902},"Web Push нотифікації","\u002Fcsharp\u002Faspnet\u002Fnotifications\u002Fweb-push","01.csharp\u002F11.aspnet\u002F04.notifications\u002F07.web-push",{"title":904,"path":905,"stem":906},"Email нотифікації","\u002Fcsharp\u002Faspnet\u002Fnotifications\u002Femail-notifications","01.csharp\u002F11.aspnet\u002F04.notifications\u002F08.email-notifications",{"title":908,"path":909,"stem":910},"Порівняння підходів: Як вибрати правильну технологію нотифікацій","\u002Fcsharp\u002Faspnet\u002Fnotifications\u002Fchoosing-the-right-approach","01.csharp\u002F11.aspnet\u002F04.notifications\u002F09.choosing-the-right-approach",{"title":912,"path":913,"stem":914},"Hangfire: Надійне планування фонових задач","\u002Fcsharp\u002Faspnet\u002Fnotifications\u002Fhangfire","01.csharp\u002F11.aspnet\u002F04.notifications\u002F10.hangfire",{"title":916,"path":917,"stem":918},"Практика: Конвертація зображень у WebP через Hangfire","\u002Fcsharp\u002Faspnet\u002Fnotifications\u002Fhangfire-image-webp","01.csharp\u002F11.aspnet\u002F04.notifications\u002F11.hangfire-image-webp",{"title":920,"path":921,"stem":922},"Практика: Підготовка відео до HLS-стрімінгу через Hangfire","\u002Fcsharp\u002Faspnet\u002Fnotifications\u002Fhangfire-video-hls","01.csharp\u002F11.aspnet\u002F04.notifications\u002F12.hangfire-video-hls",{"title":924,"path":925,"stem":926},"Telegram-нотифікації: від одного повідомлення до масових розсилок і мульти-канального підходу","\u002Fcsharp\u002Faspnet\u002Fnotifications\u002Ftelegram-notifications","01.csharp\u002F11.aspnet\u002F04.notifications\u002F13.telegram-notifications",{"title":928,"icon":929,"path":930,"stem":931,"children":932,"page":59},"Інтернаціоналізація","i-lucide-languages","\u002Fcsharp\u002Faspnet\u002Fi18n","01.csharp\u002F11.aspnet\u002F05.i18n",[933,937],{"title":934,"path":935,"stem":936},"Інтернаціоналізація (i18n) у Minimal API: від A до Я","\u002Fcsharp\u002Faspnet\u002Fi18n\u002Finternationalization","01.csharp\u002F11.aspnet\u002F05.i18n\u002F01.internationalization",{"title":938,"path":939,"stem":940},"Humanizer: людиномовні рядки у .NET","\u002Fcsharp\u002Faspnet\u002Fi18n\u002Fhumanizer","01.csharp\u002F11.aspnet\u002F05.i18n\u002F02.humanizer",{"title":942,"icon":943,"path":944,"stem":945,"children":946,"page":59},"Кешування","i-lucide-layers","\u002Fcsharp\u002Faspnet\u002Fcaching","01.csharp\u002F11.aspnet\u002F06.caching",[947,951,955,959,963],{"title":948,"path":949,"stem":950},"Огляд кешування: чотири рівні і коли що обирати","\u002Fcsharp\u002Faspnet\u002Fcaching\u002Fcaching","01.csharp\u002F11.aspnet\u002F06.caching\u002F01.caching",{"title":952,"path":953,"stem":954},"IMemoryCache: кеш в оперативній пам'яті","\u002Fcsharp\u002Faspnet\u002Fcaching\u002Fmemory-cache","01.csharp\u002F11.aspnet\u002F06.caching\u002F02.memory-cache",{"title":956,"path":957,"stem":958},"IDistributedCache і Redis: розподілений кеш","\u002Fcsharp\u002Faspnet\u002Fcaching\u002Fdistributed-cache","01.csharp\u002F11.aspnet\u002F06.caching\u002F03.distributed-cache",{"title":960,"path":961,"stem":962},"Response Cache: HTTP-кешування через Cache-Control","\u002Fcsharp\u002Faspnet\u002Fcaching\u002Fresponse-cache","01.csharp\u002F11.aspnet\u002F06.caching\u002F04.response-cache",{"title":964,"path":965,"stem":966},"Output Cache: серверний кеш HTTP-відповідей (.NET 7+)","\u002Fcsharp\u002Faspnet\u002Fcaching\u002Foutput-cache","01.csharp\u002F11.aspnet\u002F06.caching\u002F05.output-cache",{"title":968,"icon":969,"path":970,"stem":971,"children":972,"page":59},"Тестування","i-lucide-test-tube","\u002Fcsharp\u002Faspnet\u002Ftesting","01.csharp\u002F11.aspnet\u002F07.testing",[973,977,981,985,989,993,997,1001,1005,1009,1013,1017,1021,1025,1029,1033,1037,1041,1045,1049,1053],{"title":974,"path":975,"stem":976},"Що таке тестування? Від інтуїції до науки","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Fwhat-is-testing","01.csharp\u002F11.aspnet\u002F07.testing\u002F01.what-is-testing",{"title":978,"path":979,"stem":980},"Піраміда тестування — Стратегія, а не Догма","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Ftesting-pyramid","01.csharp\u002F11.aspnet\u002F07.testing\u002F02.testing-pyramid",{"title":982,"path":983,"stem":984},"Дві Школи Тестування — Лондон проти Детройту","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Ftesting-schools","01.csharp\u002F11.aspnet\u002F07.testing\u002F03.testing-schools",{"title":986,"path":987,"stem":988},"TDD та BDD — Тести як Дизайн-інструмент","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Ftdd-and-bdd","01.csharp\u002F11.aspnet\u002F07.testing\u002F04.tdd-and-bdd",{"title":990,"path":991,"stem":992},"Що саме тестувати — Техніки аналізу та Циклomatична складність","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Fwhat-to-test","01.csharp\u002F11.aspnet\u002F07.testing\u002F05.what-to-test",{"title":994,"path":995,"stem":996},"Тестові Фреймворки — Навіщо вони і що всередині","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Ftest-frameworks","01.csharp\u002F11.aspnet\u002F07.testing\u002F06.test-frameworks",{"title":998,"path":999,"stem":1000},"xUnit — Факти, Теорії та Lifecycle тестів","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Fxunit-basics","01.csharp\u002F11.aspnet\u002F07.testing\u002F07.xunit-basics",{"title":1002,"path":1003,"stem":1004},"xUnit Advanced — Fixtures, Кастомізація та Розширення","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Fxunit-advanced","01.csharp\u002F11.aspnet\u002F07.testing\u002F08.xunit-advanced",{"title":1006,"path":1007,"stem":1008},"Moq — Глибоке занурення в мокування","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Fmocking-with-moq","01.csharp\u002F11.aspnet\u002F07.testing\u002F09.mocking-with-moq",{"title":1010,"path":1011,"stem":1012},"Тестування Баз Даних — EF Core, SQLite та Testcontainers","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Fdatabase-testing","01.csharp\u002F11.aspnet\u002F07.testing\u002F10.database-testing",{"title":1014,"path":1015,"stem":1016},"Integration Testing — Частина 1 [Теорія та WebApplicationFactory]","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Fintegration-testing","01.csharp\u002F11.aspnet\u002F07.testing\u002F11.integration-testing",{"title":1018,"path":1019,"stem":1020},"Інтеграційне тестування — Практика","\u002Fcsharp\u002Faspnet\u002Ftesting\u002F11a.integration-testing-practice","01.csharp\u002F11.aspnet\u002F07.testing\u002F11a.integration-testing-practice",{"title":1022,"path":1023,"stem":1024},"Integration Testing — Частина 2 [Просунуті Сценарії та Testcontainers]","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Fintegration-testing-advanced","01.csharp\u002F11.aspnet\u002F07.testing\u002F12.integration-testing-advanced",{"title":1026,"path":1027,"stem":1028},"Професійний Postman: Колекції, Змінні та GitHub Інтеграція","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Fpostman-professional","01.csharp\u002F11.aspnet\u002F07.testing\u002F13.postman-professional",{"title":1030,"path":1031,"stem":1032},"HttpClient у Тестах Частина 1: Архітектура та MockHttpMessageHandler","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Fhttpclient-testing","01.csharp\u002F11.aspnet\u002F07.testing\u002F14.httpclient-testing",{"title":1034,"path":1035,"stem":1036},"HttpClient у Тестах Частина 2: WireMock.Net та Resilience","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Fwiremock-net","01.csharp\u002F11.aspnet\u002F07.testing\u002F15.wiremock-net",{"title":1038,"path":1039,"stem":1040},"Патерни та Анти-патерни Тестування: Test Smells","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Ftesting-patterns","01.csharp\u002F11.aspnet\u002F07.testing\u002F16.testing-patterns",{"title":1042,"path":1043,"stem":1044},"Просунуті інструменти: Time, Snapshots та Властивості","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Fadvanced-testing-tools","01.csharp\u002F11.aspnet\u002F07.testing\u002F17.advanced-testing-tools",{"title":1046,"path":1047,"stem":1048},"Тестування Архітектури з NetArchTest","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Farchitecture-testing","01.csharp\u002F11.aspnet\u002F07.testing\u002F18.architecture-testing",{"title":1050,"path":1051,"stem":1052},"Тестування Продуктивності: BenchmarkDotNet, NBomber та k6","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Fperformance-testing","01.csharp\u002F11.aspnet\u002F07.testing\u002F19.performance-testing",{"title":1054,"path":1055,"stem":1056},"Залишок плану для курсу \"Тестування ASP.NET Minimal API\"","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Fremaining_plan","01.csharp\u002F11.aspnet\u002F07.testing\u002Fremaining_plan",{"title":1058,"icon":1059,"path":1060,"stem":1061,"children":1062,"page":59},"Платежі","i-lucide-credit-card","\u002Fcsharp\u002Faspnet\u002Fpayments","01.csharp\u002F11.aspnet\u002F08.payments",[1063,1067,1071,1075,1079,1083,1087,1091,1095,1099,1103,1107],{"title":1064,"path":1065,"stem":1066},"Основи платіжної інфраструктури","\u002Fcsharp\u002Faspnet\u002Fpayments\u002Fpayment-fundamentals","01.csharp\u002F11.aspnet\u002F08.payments\u002F01.payment-fundamentals",{"title":1068,"path":1069,"stem":1070},"Методи оплати в Україні","\u002Fcsharp\u002Faspnet\u002Fpayments\u002Fpayment-methods-ukraine","01.csharp\u002F11.aspnet\u002F08.payments\u002F02.payment-methods-ukraine",{"title":1072,"path":1073,"stem":1074},"PCI DSS та безпека платежів","\u002Fcsharp\u002Faspnet\u002Fpayments\u002Fpci-dss-security","01.csharp\u002F11.aspnet\u002F08.payments\u002F03.pci-dss-security",{"title":1076,"path":1077,"stem":1078},"Архітектура платіжної підсистеми","\u002Fcsharp\u002Faspnet\u002Fpayments\u002Fpayment-architecture","01.csharp\u002F11.aspnet\u002F08.payments\u002F04.payment-architecture",{"title":1080,"path":1081,"stem":1082},"Інтеграція LiqPay (ПриватБанк)","\u002Fcsharp\u002Faspnet\u002Fpayments\u002Fliqpay-integration","01.csharp\u002F11.aspnet\u002F08.payments\u002F05.liqpay-integration",{"title":1084,"path":1085,"stem":1086},"Інтеграція Monobank Acquiring API","\u002Fcsharp\u002Faspnet\u002Fpayments\u002Fmonobank-acquiring","01.csharp\u002F11.aspnet\u002F08.payments\u002F06.monobank-acquiring",{"title":1088,"path":1089,"stem":1090},"Інтеграція Stripe","\u002Fcsharp\u002Faspnet\u002Fpayments\u002Fstripe-integration","01.csharp\u002F11.aspnet\u002F08.payments\u002F07.stripe-integration",{"title":1092,"path":1093,"stem":1094},"Webhooks — глибоке занурення","\u002Fcsharp\u002Faspnet\u002Fpayments\u002Fwebhooks-deep-dive","01.csharp\u002F11.aspnet\u002F08.payments\u002F08.webhooks-deep-dive",{"title":1096,"path":1097,"stem":1098},"Підписки та рекурентні платежі","\u002Fcsharp\u002Faspnet\u002Fpayments\u002Fsubscriptions-recurring","01.csharp\u002F11.aspnet\u002F08.payments\u002F09.subscriptions-recurring",{"title":1100,"path":1101,"stem":1102},"Повернення коштів та диспути","\u002Fcsharp\u002Faspnet\u002Fpayments\u002Frefunds-disputes","01.csharp\u002F11.aspnet\u002F08.payments\u002F10.refunds-disputes",{"title":1104,"path":1105,"stem":1106},"Тестування платіжних інтеграцій","\u002Fcsharp\u002Faspnet\u002Fpayments\u002Ftesting-payments","01.csharp\u002F11.aspnet\u002F08.payments\u002F11.testing-payments",{"title":1108,"path":1109,"stem":1110},"Чекліст виходу в Production","\u002Fcsharp\u002Faspnet\u002Fpayments\u002Fproduction-checklist","01.csharp\u002F11.aspnet\u002F08.payments\u002F12.production-checklist",{"title":1112,"icon":1113,"items":1114,"path":1127,"stem":1128,"children":1129,"page":59},"Популярні бібліотеки","lucide:box",[1115,1116,1117,1118,1119,1120,1121,1122,1123,1124,1125,1126],"01.fluent-validation","02.mapster","03.erroror-result-pattern","04.serilog","05.mediatr","06.polly","07.health-checks","08.feature-management","09.fluent-email","10.quest-pdf","11.bogus","12.humanizer-guard","\u002Fcsharp\u002Faspnet\u002Flibraries","01.csharp\u002F11.aspnet\u002F09.libraries",[1130,1134,1138,1142,1146,1150,1154,1158,1162,1166,1170,1174,1178],{"title":1131,"path":1132,"stem":1133},"Валідація з FluentValidation в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Flibraries\u002Ffluent-validation","01.csharp\u002F11.aspnet\u002F09.libraries\u002F01.fluent-validation",{"title":1135,"path":1136,"stem":1137},"Маппінг об","\u002Fcsharp\u002Faspnet\u002Flibraries\u002Fmapster","01.csharp\u002F11.aspnet\u002F09.libraries\u002F02.mapster",{"title":1139,"path":1140,"stem":1141},"Обробка помилок з ErrorOr та Result Pattern в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Flibraries\u002Ferroror-result-pattern","01.csharp\u002F11.aspnet\u002F09.libraries\u002F03.erroror-result-pattern",{"title":1143,"path":1144,"stem":1145},"Структуроване логування з Serilog в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Flibraries\u002Fserilog","01.csharp\u002F11.aspnet\u002F09.libraries\u002F04.serilog",{"title":1147,"path":1148,"stem":1149},"CQRS та Mediator з MediatR в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Flibraries\u002Fmediatr","01.csharp\u002F11.aspnet\u002F09.libraries\u002F05.mediatr",{"title":1151,"path":1152,"stem":1153},"Відмовостійкість з Polly в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Flibraries\u002Fpolly","01.csharp\u002F11.aspnet\u002F09.libraries\u002F06.polly",{"title":1155,"path":1156,"stem":1157},"Health Checks в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Flibraries\u002Fhealth-checks","01.csharp\u002F11.aspnet\u002F09.libraries\u002F07.health-checks",{"title":1159,"path":1160,"stem":1161},"Feature Management та Feature Flags в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Flibraries\u002Ffeature-management","01.csharp\u002F11.aspnet\u002F09.libraries\u002F08.feature-management",{"title":1163,"path":1164,"stem":1165},"Відправка Email з FluentEmail в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Flibraries\u002Ffluent-email","01.csharp\u002F11.aspnet\u002F09.libraries\u002F09.fluent-email",{"title":1167,"path":1168,"stem":1169},"Генерація PDF з QuestPDF в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Flibraries\u002Fquest-pdf","01.csharp\u002F11.aspnet\u002F09.libraries\u002F10.quest-pdf",{"title":1171,"path":1172,"stem":1173},"Генерація тестових даних з Bogus в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Flibraries\u002Fbogus","01.csharp\u002F11.aspnet\u002F09.libraries\u002F11.bogus",{"title":1175,"path":1176,"stem":1177},"Humanizer та Guard Clauses в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Flibraries\u002Fhumanizer-guard","01.csharp\u002F11.aspnet\u002F09.libraries\u002F12.humanizer-guard",{"title":1179,"path":1180,"stem":1181},"План модуля 10.libraries — Популярні бібліотеки ASP.NET","\u002Fcsharp\u002Faspnet\u002Flibraries\u002Fplan","01.csharp\u002F11.aspnet\u002F09.libraries\u002Fplan",{"title":1183,"icon":1184,"path":1185,"stem":1186,"children":1187,"page":59},"Razor Pages","i-lucide-layout-template","\u002Fcsharp\u002Faspnet\u002Frazor-pages","01.csharp\u002F11.aspnet\u002F10.razor-pages",[1188,1192,1196,1200,1204,1208],{"title":1189,"path":1190,"stem":1191},"Від Minimal API до Razor Pages: концептуальний перехід","\u002Fcsharp\u002Faspnet\u002Frazor-pages\u002Ffrom-minimal-api","01.csharp\u002F11.aspnet\u002F10.razor-pages\u002F01.from-minimal-api",{"title":1193,"path":1194,"stem":1195},"PageModel: логіка сторінки Razor Pages","\u002Fcsharp\u002Faspnet\u002Frazor-pages\u002Fpage-model","01.csharp\u002F11.aspnet\u002F10.razor-pages\u002F02.page-model",{"title":1197,"path":1198,"stem":1199},"Razor синтаксис: шаблонізатор у .cshtml","\u002Fcsharp\u002Faspnet\u002Frazor-pages\u002Frazor-syntax","01.csharp\u002F11.aspnet\u002F10.razor-pages\u002F03.razor-syntax",{"title":1201,"path":1202,"stem":1203},"Tag Helpers: типізований HTML","\u002Fcsharp\u002Faspnet\u002Frazor-pages\u002Ftag-helpers","01.csharp\u002F11.aspnet\u002F10.razor-pages\u002F04.tag-helpers",{"title":1205,"path":1206,"stem":1207},"Форми і валідація: повний цикл обробки даних","\u002Fcsharp\u002Faspnet\u002Frazor-pages\u002Fforms-validation","01.csharp\u002F11.aspnet\u002F10.razor-pages\u002F05.forms-validation",{"title":1209,"path":1210,"stem":1211},"Практичний проєкт: TaskManager на Razor Pages","\u002Fcsharp\u002Faspnet\u002Frazor-pages\u002Fproject-task-manager","01.csharp\u002F11.aspnet\u002F10.razor-pages\u002F06.project-task-manager",{"title":1213,"path":1214,"stem":1215,"children":1216,"page":59},"ASP.NET Core MVC","\u002Fcsharp\u002Faspnet\u002Fmvc","01.csharp\u002F11.aspnet\u002F11.mvc",[1217,1221,1225,1229,1233,1237,1241,1245,1249,1253,1257,1261,1265,1269,1273,1277,1281,1285],{"title":1218,"path":1219,"stem":1220},"Патерн MVC: архітектура, що змінила веб","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Fmvc-pattern","01.csharp\u002F11.aspnet\u002F11.mvc\u002F01.mvc-pattern",{"title":1222,"path":1223,"stem":1224},"Від Razor Pages до MVC: концептуальний перехід","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Ffrom-razor-pages","01.csharp\u002F11.aspnet\u002F11.mvc\u002F02.from-razor-pages",{"title":1226,"path":1227,"stem":1228},"Controllers та Actions: серце MVC","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Fcontrollers-actions","01.csharp\u002F11.aspnet\u002F11.mvc\u002F03.controllers-actions",{"title":1230,"path":1231,"stem":1232},"Маршрутизація в MVC: Convention vs Attribute Routing","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Frouting-mvc","01.csharp\u002F11.aspnet\u002F11.mvc\u002F04.routing-mvc",{"title":1234,"path":1235,"stem":1236},"Model Binding: від HTTP до C#","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Fmodel-binding","01.csharp\u002F11.aspnet\u002F11.mvc\u002F05.model-binding",{"title":1238,"path":1239,"stem":1240},"Views, ViewData, ViewBag, TempData і ViewModel","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Fviews-viewdata-tempdata","01.csharp\u002F11.aspnet\u002F11.mvc\u002F06.views-viewdata-tempdata",{"title":1242,"path":1243,"stem":1244},"Filters: аспектно-орієнтоване програмування в MVC","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Ffilters","01.csharp\u002F11.aspnet\u002F11.mvc\u002F07.filters",{"title":1246,"path":1247,"stem":1248},"Areas: структурування великих застосунків","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Fareas","01.csharp\u002F11.aspnet\u002F11.mvc\u002F08.areas",{"title":1250,"path":1251,"stem":1252},"View Components: повторювані незалежні блоки UI","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Fview-components","01.csharp\u002F11.aspnet\u002F11.mvc\u002F09.view-components",{"title":1254,"path":1255,"stem":1256},"Display та Editor Templates","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Fdisplay-editor-templates","01.csharp\u002F11.aspnet\u002F11.mvc\u002F10.display-editor-templates",{"title":1258,"path":1259,"stem":1260},"Валідація: IValidatableObject та FluentValidation","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Fvalidation-advanced","01.csharp\u002F11.aspnet\u002F11.mvc\u002F11.validation-advanced",{"title":1262,"path":1263,"stem":1264},"HTMX: інтерактивність через HTML-атрибути","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Fhtmx","01.csharp\u002F11.aspnet\u002F11.mvc\u002F12.htmx",{"title":1266,"path":1267,"stem":1268},"HTMX у ASP.NET Core MVC: серверна інтеграція","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Fajax-htmx-mvc","01.csharp\u002F11.aspnet\u002F11.mvc\u002F13.ajax-htmx-mvc",{"title":1270,"path":1271,"stem":1272},"Практичний проєкт: Каталог товарів з HTMX","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Fhtmx-project","01.csharp\u002F11.aspnet\u002F11.mvc\u002F14.htmx-project",{"title":1274,"path":1275,"stem":1276},"Завантаження та обробка файлів","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Ffile-upload","01.csharp\u002F11.aspnet\u002F11.mvc\u002F15.file-upload",{"title":1278,"path":1279,"stem":1280},"Глобалізація та Локалізація MVC","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Fglobalization-localization","01.csharp\u002F11.aspnet\u002F11.mvc\u002F16.globalization-localization",{"title":1282,"path":1283,"stem":1284},"Підсумковий проєкт: Блог-платформа","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Fmvc-project","01.csharp\u002F11.aspnet\u002F11.mvc\u002F17.mvc-project",{"title":1286,"path":1287,"stem":1288},"План курсу: ASP.NET Core MVC","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Fplan","01.csharp\u002F11.aspnet\u002F11.mvc\u002Fplan",{"title":1290,"path":1291,"stem":1292,"children":1293,"page":59},"Web Api","\u002Fcsharp\u002Faspnet\u002Fweb-api","01.csharp\u002F11.aspnet\u002F12.web-api",[1294,1298,1302,1306,1310,1314,1318,1322,1326,1330,1334,1338,1342],{"title":1295,"path":1296,"stem":1297},"Від Minimal API до Controller-based API","\u002Fcsharp\u002Faspnet\u002Fweb-api\u002Ffrom-minimal-api-to-controllers","01.csharp\u002F11.aspnet\u002F12.web-api\u002F01.from-minimal-api-to-controllers",{"title":1299,"path":1300,"stem":1301},"ControllerBase, ActionResult\u003CT> та Response Types","\u002Fcsharp\u002Faspnet\u002Fweb-api\u002Fcontroller-base-actionresult","01.csharp\u002F11.aspnet\u002F12.web-api\u002F02.controller-base-actionresult",{"title":1303,"path":1304,"stem":1305},"Content Negotiation - JSON, XML та власні форматери","\u002Fcsharp\u002Faspnet\u002Fweb-api\u002Fcontent-negotiation","01.csharp\u002F11.aspnet\u002F12.web-api\u002F03.content-negotiation",{"title":1307,"path":1308,"stem":1309},"Версіонування API","\u002Fcsharp\u002Faspnet\u002Fweb-api\u002Fapi-versioning","01.csharp\u002F11.aspnet\u002F12.web-api\u002F04.api-versioning",{"title":1311,"path":1312,"stem":1313},"ProblemDetails та структурована обробка помилок","\u002Fcsharp\u002Faspnet\u002Fweb-api\u002Fproblemdetails-error-handling","01.csharp\u002F11.aspnet\u002F12.web-api\u002F05.problemdetails-error-handling",{"title":1315,"path":1316,"stem":1317},"Фільтри у Web API контексті","\u002Fcsharp\u002Faspnet\u002Fweb-api\u002Ffilters-for-api","01.csharp\u002F11.aspnet\u002F12.web-api\u002F06.filters-for-api",{"title":1319,"path":1320,"stem":1321},"Пагінація, фільтрація та сортування","\u002Fcsharp\u002Faspnet\u002Fweb-api\u002Fpagination-filtering-sorting","01.csharp\u002F11.aspnet\u002F12.web-api\u002F07.pagination-filtering-sorting",{"title":1323,"path":1324,"stem":1325},"HATEOAS та Resource Expansion","\u002Fcsharp\u002Faspnet\u002Fweb-api\u002Fhateoas-resource-expansion","01.csharp\u002F11.aspnet\u002F12.web-api\u002F08.hateoas-resource-expansion",{"title":1327,"path":1328,"stem":1329},"Гібридна архітектура - Minimal API + Controllers","\u002Fcsharp\u002Faspnet\u002Fweb-api\u002Fminimal-api-vs-controllers-hybrid","01.csharp\u002F11.aspnet\u002F12.web-api\u002F09.minimal-api-vs-controllers-hybrid",{"title":1331,"path":1332,"stem":1333},"Документація API - Swashbuckle, NSwag та генерація клієнтів","\u002Fcsharp\u002Faspnet\u002Fweb-api\u002Fapi-documentation-generation","01.csharp\u002F11.aspnet\u002F12.web-api\u002F10.api-documentation-generation",{"title":1335,"path":1336,"stem":1337},"Health Checks та моніторинг API","\u002Fcsharp\u002Faspnet\u002Fweb-api\u002Fhealth-checks-monitoring","01.csharp\u002F11.aspnet\u002F12.web-api\u002F11.health-checks-monitoring",{"title":1339,"path":1340,"stem":1341},"Підсумковий проєкт - Production-Ready REST API","\u002Fcsharp\u002Faspnet\u002Fweb-api\u002Fweb-api-project","01.csharp\u002F11.aspnet\u002F12.web-api\u002F12.web-api-project",{"title":1343,"path":1344,"stem":1345},"План курсу: ASP.NET Core Web API (Controllers)","\u002Fcsharp\u002Faspnet\u002Fweb-api\u002Fplan","01.csharp\u002F11.aspnet\u002F12.web-api\u002Fplan",{"title":1347,"icon":1348,"path":1349,"stem":1350,"children":1351,"page":59},"Моніторинг","i-lucide-activity","\u002Fcsharp\u002Faspnet\u002Fmonitoring","01.csharp\u002F11.aspnet\u002F13.monitoring",[1352,1356,1360],{"title":1353,"path":1354,"stem":1355},"Спостережуваність: від console.log до production-систем","\u002Fcsharp\u002Faspnet\u002Fmonitoring\u002Fobservability-intro","01.csharp\u002F11.aspnet\u002F13.monitoring\u002F01.observability-intro",{"title":1357,"path":1358,"stem":1359},"Health Checks: перший рівень observability","\u002Fcsharp\u002Faspnet\u002Fmonitoring\u002Fhealth-checks","01.csharp\u002F11.aspnet\u002F13.monitoring\u002F02.health-checks",{"title":1361,"path":1362,"stem":1363},"Вбудовані метрики .NET 10 та System.Diagnostics.Metrics","\u002Fcsharp\u002Faspnet\u002Fmonitoring\u002Fdotnet-metrics","01.csharp\u002F11.aspnet\u002F13.monitoring\u002F03.dotnet-metrics",{"title":1365,"icon":1366,"path":1367,"stem":1368,"children":1369,"page":59},"Desktop UI","i-lucide-app-window","\u002Fcsharp\u002Fdesktop-ui","01.csharp\u002F12.desktop-ui",[1370,1374,1378,1382,1386,1390,1394,1398,1402,1406,1410,1414,1418,1422,1426,1430,1434,1438,1442,1446,1450,1454,1458,1462,1466,1470,1474,1478,1482,1486,1490,1494,1498,1502,1506,1510,1514,1518,1522,1526,1530,1534,1538,1542,1546,1550,1554,1558,1562,1566,1570,1574,1578,1582,1586,1590,1594,1598,1602,1606,1610,1614,1618,1622,1626,1630,1634,1638,1642,1646,1650],{"title":1371,"path":1372,"stem":1373},"Що таке десктопна розробка?","\u002Fcsharp\u002Fdesktop-ui\u002Fwhat-is-desktop-dev","01.csharp\u002F12.desktop-ui\u002F01.what-is-desktop-dev",{"title":1375,"path":1376,"stem":1377},"Архітектура WPF — як влаштований графічний інтерфейс","\u002Fcsharp\u002Fdesktop-ui\u002Fwpf-architecture","01.csharp\u002F12.desktop-ui\u002F02.wpf-architecture",{"title":1379,"path":1380,"stem":1381},"Перший WPF-проєкт — від нуля до вікна","\u002Fcsharp\u002Fdesktop-ui\u002Ffirst-wpf-app","01.csharp\u002F12.desktop-ui\u002F03.first-wpf-app",{"title":1383,"path":1384,"stem":1385},"Перший Avalonia-проєкт: WPF для всіх платформ","\u002Fcsharp\u002Fdesktop-ui\u002F03a.first-avalonia-app","01.csharp\u002F12.desktop-ui\u002F03a.first-avalonia-app",{"title":1387,"path":1388,"stem":1389},"XAML: декларативний інтерфейс","\u002Fcsharp\u002Fdesktop-ui\u002Fxaml-basics","01.csharp\u002F12.desktop-ui\u002F04.xaml-basics",{"title":1391,"path":1392,"stem":1393},"Fluent UI у WPF — сучасний дизайн Windows 11","\u002Fcsharp\u002Fdesktop-ui\u002F04a.wpf-fluent-ui","01.csharp\u002F12.desktop-ui\u002F04a.wpf-fluent-ui",{"title":1395,"path":1396,"stem":1397},"WPF UI — сучасна бібліотека Fluent контролів","\u002Fcsharp\u002Fdesktop-ui\u002F04b.wpf-ui-library","01.csharp\u002F12.desktop-ui\u002F04b.wpf-ui-library",{"title":1399,"path":1400,"stem":1401},"HandyControl — велика бібліотека UI контролів для WPF","\u002Fcsharp\u002Fdesktop-ui\u002F04c.handycontrol-library","01.csharp\u002F12.desktop-ui\u002F04c.handycontrol-library",{"title":1403,"path":1404,"stem":1405},"Простори імен та ресурси XAML","\u002Fcsharp\u002Fdesktop-ui\u002Fxaml-namespaces-resources","01.csharp\u002F12.desktop-ui\u002F05.xaml-namespaces-resources",{"title":1407,"path":1408,"stem":1409},"XAML в Avalonia: ключові відмінності від WPF","\u002Fcsharp\u002Fdesktop-ui\u002F05a.avalonia-xaml-differences","01.csharp\u002F12.desktop-ui\u002F05a.avalonia-xaml-differences",{"title":1411,"path":1412,"stem":1413},"Розширення розмітки XAML (Markup Extensions)","\u002Fcsharp\u002Fdesktop-ui\u002Fxaml-markup-extensions","01.csharp\u002F12.desktop-ui\u002F06.xaml-markup-extensions",{"title":1415,"path":1416,"stem":1417},"Панелі Layout: StackPanel, WrapPanel, DockPanel","\u002Fcsharp\u002Fdesktop-ui\u002Flayout-panels-part1","01.csharp\u002F12.desktop-ui\u002F07.layout-panels-part1",{"title":1419,"path":1420,"stem":1421},"Grid, Canvas, UniformGrid","\u002Fcsharp\u002Fdesktop-ui\u002Flayout-panels-part2","01.csharp\u002F12.desktop-ui\u002F07.layout-panels-part2",{"title":1423,"path":1424,"stem":1425},"Просунуті техніки Layout","\u002Fcsharp\u002Fdesktop-ui\u002Flayout-advanced","01.csharp\u002F12.desktop-ui\u002F08.layout-advanced",{"title":1427,"path":1428,"stem":1429},"Адаптивний Layout та найкращі практики","\u002Fcsharp\u002Fdesktop-ui\u002Flayout-responsive","01.csharp\u002F12.desktop-ui\u002F09.layout-responsive",{"title":1431,"path":1432,"stem":1433},"Layout в Avalonia: відмінності та нові можливості","\u002Fcsharp\u002Fdesktop-ui\u002F09a.layout-avalonia","01.csharp\u002F12.desktop-ui\u002F09a.layout-avalonia",{"title":1435,"path":1436,"stem":1437},"Button, Image, ProgressBar та інші базові контроли","\u002Fcsharp\u002Fdesktop-ui\u002Fbasic-controls","01.csharp\u002F12.desktop-ui\u002F10.basic-controls",{"title":1439,"path":1440,"stem":1441},"Контроли в Avalonia: відмінності від WPF","\u002Fcsharp\u002Fdesktop-ui\u002F10a.controls-avalonia","01.csharp\u002F12.desktop-ui\u002F10a.controls-avalonia",{"title":1443,"path":1444,"stem":1445},"Текстові контроли — TextBlock, TextBox, RichTextBox","\u002Fcsharp\u002Fdesktop-ui\u002Ftext-controls","01.csharp\u002F12.desktop-ui\u002F11.text-controls",{"title":1447,"path":1448,"stem":1449},"Контроли вибору — CheckBox, RadioButton, ComboBox, ListBox, DatePicker","\u002Fcsharp\u002Fdesktop-ui\u002Fselection-controls","01.csharp\u002F12.desktop-ui\u002F12.selection-controls",{"title":1451,"path":1452,"stem":1453},"Content Model — GroupBox, Expander, TabControl, StatusBar","\u002Fcsharp\u002Fdesktop-ui\u002Fcontent-controls","01.csharp\u002F12.desktop-ui\u002F13.content-controls",{"title":1455,"path":1456,"stem":1457},"UI\u002FUX принципи десктопних застосунків","\u002Fcsharp\u002Fdesktop-ui\u002F13a.ui-ux-principles","01.csharp\u002F12.desktop-ui\u002F13a.ui-ux-principles",{"title":1459,"path":1460,"stem":1461},"Dependency Properties — Концепція та Value Resolution","\u002Fcsharp\u002Fdesktop-ui\u002Fdependency-properties-part1","01.csharp\u002F12.desktop-ui\u002F14.dependency-properties-part1",{"title":1463,"path":1464,"stem":1465},"Avalonia Property System — StyledProperty та DirectProperty","\u002Fcsharp\u002Fdesktop-ui\u002F14a.avalonia-property-system","01.csharp\u002F12.desktop-ui\u002F14a.avalonia-property-system",{"title":1467,"path":1468,"stem":1469},"Attached Properties — Властивості без меж","\u002Fcsharp\u002Fdesktop-ui\u002Fattached-properties","01.csharp\u002F12.desktop-ui\u002F15.attached-properties",{"title":1471,"path":1472,"stem":1473},"Routed Events — Маршрутизація подій у WPF","\u002Fcsharp\u002Fdesktop-ui\u002Frouted-events","01.csharp\u002F12.desktop-ui\u002F16.routed-events",{"title":1475,"path":1476,"stem":1477},"Data Binding — Від Code-Behind до Декларативності","\u002Fcsharp\u002Fdesktop-ui\u002Fdata-binding-basics-part1","01.csharp\u002F12.desktop-ui\u002F17.data-binding-basics-part1",{"title":1479,"path":1480,"stem":1481},"INotifyPropertyChanged — Живе оновлення UI","\u002Fcsharp\u002Fdesktop-ui\u002Fdata-binding-basics-part2","01.csharp\u002F12.desktop-ui\u002F17.data-binding-basics-part2",{"title":1483,"path":1484,"stem":1485},"Compiled Bindings в Avalonia — Безпека на етапі компіляції","\u002Fcsharp\u002Fdesktop-ui\u002F17a.avalonia-compiled-bindings","01.csharp\u002F12.desktop-ui\u002F17a.avalonia-compiled-bindings",{"title":1487,"path":1488,"stem":1489},"Просунутий Data Binding — ElementName, RelativeSource, MultiBinding","\u002Fcsharp\u002Fdesktop-ui\u002Fdata-binding-advanced","01.csharp\u002F12.desktop-ui\u002F18.data-binding-advanced",{"title":1491,"path":1492,"stem":1493},"Value Converters — Перетворення типів даних у Data Binding","\u002Fcsharp\u002Fdesktop-ui\u002Fvalue-converters","01.csharp\u002F12.desktop-ui\u002F19.value-converters",{"title":1495,"path":1496,"stem":1497},"Data Templates — Візуалізація об'єктів у WPF","\u002Fcsharp\u002Fdesktop-ui\u002Fdata-templates","01.csharp\u002F12.desktop-ui\u002F20.data-templates",{"title":1499,"path":1500,"stem":1501},"Collections Binding Part 1 — ObservableCollection та ItemsControl","\u002Fcsharp\u002Fdesktop-ui\u002Fcollections-binding-part1","01.csharp\u002F12.desktop-ui\u002F21.collections-binding-part1",{"title":1503,"path":1504,"stem":1505},"Collections Binding Part 2 — ICollectionView, Filtering, Sorting та Virtualization","\u002Fcsharp\u002Fdesktop-ui\u002Fcollections-binding-part2","01.csharp\u002F12.desktop-ui\u002F21.collections-binding-part2",{"title":1507,"path":1508,"stem":1509},"MVVM Pattern — Від Spaghetti Code до архітектури","\u002Fcsharp\u002Fdesktop-ui\u002Fmvvm-pattern","01.csharp\u002F12.desktop-ui\u002F22.mvvm-pattern",{"title":1511,"path":1512,"stem":1513},"ViewModel Implementation — Від BaseViewModel до валідації","\u002Fcsharp\u002Fdesktop-ui\u002Fviewmodel-implementation","01.csharp\u002F12.desktop-ui\u002F23.viewmodel-implementation",{"title":1515,"path":1516,"stem":1517},"Commands — Від event handlers до декларативних команд","\u002Fcsharp\u002Fdesktop-ui\u002Fcommands","01.csharp\u002F12.desktop-ui\u002F24.commands",{"title":1519,"path":1520,"stem":1521},"MVVM Toolkit — MVVM без boilerplate через Source Generators","\u002Fcsharp\u002Fdesktop-ui\u002Fmvvm-toolkit","01.csharp\u002F12.desktop-ui\u002F25.mvvm-toolkit",{"title":1523,"path":1524,"stem":1525},"Messenger Pattern — Комунікація між ViewModel без прямих посилань","\u002Fcsharp\u002Fdesktop-ui\u002Fmessenger-pattern","01.csharp\u002F12.desktop-ui\u002F26.messenger-pattern",{"title":1527,"path":1528,"stem":1529},"Стилі WPF — CSS для десктопу","\u002Fcsharp\u002Fdesktop-ui\u002Fstyles-basics","01.csharp\u002F12.desktop-ui\u002F27.styles-basics",{"title":1531,"path":1532,"stem":1533},"CSS-like стилі Avalonia","\u002Fcsharp\u002Fdesktop-ui\u002F27a.avalonia-css-styling","01.csharp\u002F12.desktop-ui\u002F27a.avalonia-css-styling",{"title":1535,"path":1536,"stem":1537},"Control Templates — Частина 1. Концепція та TemplateBinding","\u002Fcsharp\u002Fdesktop-ui\u002Fcontrol-templates-part1","01.csharp\u002F12.desktop-ui\u002F28.control-templates-part1",{"title":1539,"path":1540,"stem":1541},"Control Templates — Частина 2. Named Parts та ContentPresenter","\u002Fcsharp\u002Fdesktop-ui\u002Fcontrol-templates-part2","01.csharp\u002F12.desktop-ui\u002F28.control-templates-part2",{"title":1543,"path":1544,"stem":1545},"Control Themes в Avalonia — нова ера стилізації","\u002Fcsharp\u002Fdesktop-ui\u002F28a.avalonia-control-themes","01.csharp\u002F12.desktop-ui\u002F28a.avalonia-control-themes",{"title":1547,"path":1548,"stem":1549},"Triggers та Visual State Manager у WPF","\u002Fcsharp\u002Fdesktop-ui\u002Ftriggers-visual-states","01.csharp\u002F12.desktop-ui\u002F29.triggers-visual-states",{"title":1551,"path":1552,"stem":1553},"Pseudo-classes в Avalonia — замість WPF Triggers","\u002Fcsharp\u002Fdesktop-ui\u002F29a.avalonia-pseudo-classes","01.csharp\u002F12.desktop-ui\u002F29a.avalonia-pseudo-classes",{"title":1555,"path":1556,"stem":1557},"Теми та ресурсні словники у WPF","\u002Fcsharp\u002Fdesktop-ui\u002Fresources-themes","01.csharp\u002F12.desktop-ui\u002F30.resources-themes",{"title":1559,"path":1560,"stem":1561},"Avalonia Themes — Fluent Design та система тематизації","\u002Fcsharp\u002Fdesktop-ui\u002F30a.avalonia-themes-fluent","01.csharp\u002F12.desktop-ui\u002F30a.avalonia-themes-fluent",{"title":1563,"path":1564,"stem":1565},"Контроли колекцій — глибоке занурення","\u002Fcsharp\u002Fdesktop-ui\u002Fcollection-controls","01.csharp\u002F12.desktop-ui\u002F31.collection-controls",{"title":1567,"path":1568,"stem":1569},"DataGrid — колонки та базове відображення","\u002Fcsharp\u002Fdesktop-ui\u002Fdatagrid-part1","01.csharp\u002F12.desktop-ui\u002F32.datagrid-part1",{"title":1571,"path":1572,"stem":1573},"DataGrid — сортування, фільтрація, редагування","\u002Fcsharp\u002Fdesktop-ui\u002Fdatagrid-part2","01.csharp\u002F12.desktop-ui\u002F32.datagrid-part2",{"title":1575,"path":1576,"stem":1577},"TreeView та GridView","\u002Fcsharp\u002Fdesktop-ui\u002Ftreeview-listview","01.csharp\u002F12.desktop-ui\u002F33.treeview-listview",{"title":1579,"path":1580,"stem":1581},"Меню, Toolbar, ContextMenu, StatusBar","\u002Fcsharp\u002Fdesktop-ui\u002Fmenus-toolbars","01.csharp\u002F12.desktop-ui\u002F34.menus-toolbars",{"title":1583,"path":1584,"stem":1585},"Навігація та керування вікнами. Частина 1: вікна та сторінки","\u002Fcsharp\u002Fdesktop-ui\u002Fnavigation-windows-part1","01.csharp\u002F12.desktop-ui\u002F35.navigation-windows-part1",{"title":1587,"path":1588,"stem":1589},"Навігація та керування вікнами. Частина 2: MVVM-навігація","\u002Fcsharp\u002Fdesktop-ui\u002Fnavigation-windows-part2","01.csharp\u002F12.desktop-ui\u002F35.navigation-windows-part2",{"title":1591,"path":1592,"stem":1593},"Avalonia — Навігація та діалоги","\u002Fcsharp\u002Fdesktop-ui\u002F35a.avalonia-navigation-dialogs","01.csharp\u002F12.desktop-ui\u002F35a.avalonia-navigation-dialogs",{"title":1595,"path":1596,"stem":1597},"Діалоги та File Pickers у WPF","\u002Fcsharp\u002Fdesktop-ui\u002Fdialogs-file-pickers","01.csharp\u002F12.desktop-ui\u002F36.dialogs-file-pickers",{"title":1599,"path":1600,"stem":1601},"UserControl: компонентний підхід у WPF","\u002Fcsharp\u002Fdesktop-ui\u002Fuser-controls","01.csharp\u002F12.desktop-ui\u002F37.user-controls",{"title":1603,"path":1604,"stem":1605},"Custom Controls: Lookless Controls у WPF","\u002Fcsharp\u002Fdesktop-ui\u002Fcustom-controls","01.csharp\u002F12.desktop-ui\u002F38.custom-controls",{"title":1607,"path":1608,"stem":1609},"Avalonia TemplatedControl — Lookless Controls","\u002Fcsharp\u002Fdesktop-ui\u002F38a.avalonia-templated-controls","01.csharp\u002F12.desktop-ui\u002F38a.avalonia-templated-controls",{"title":1611,"path":1612,"stem":1613},"Анімації у WPF: Storyboard та Easing Functions","\u002Fcsharp\u002Fdesktop-ui\u002Fanimations-transitions","01.csharp\u002F12.desktop-ui\u002F39.animations-transitions",{"title":1615,"path":1616,"stem":1617},"Анімації в Avalonia","\u002Fcsharp\u002Fdesktop-ui\u002F39a.avalonia-animations","01.csharp\u002F12.desktop-ui\u002F39a.avalonia-animations",{"title":1619,"path":1620,"stem":1621},"2D Графіка та Мультимедіа у WPF","\u002Fcsharp\u002Fdesktop-ui\u002Fmedia-graphics","01.csharp\u002F12.desktop-ui\u002F40.media-graphics",{"title":1623,"path":1624,"stem":1625},"Dependency Injection у WPF та Avalonia","\u002Fcsharp\u002Fdesktop-ui\u002Fdi-integration","01.csharp\u002F12.desktop-ui\u002F41.di-integration",{"title":1627,"path":1628,"stem":1629},"SQLite та EF Core у десктопних додатках","\u002Fcsharp\u002Fdesktop-ui\u002Fdata-persistence-part1","01.csharp\u002F12.desktop-ui\u002F42.data-persistence-part1",{"title":1631,"path":1632,"stem":1633},"Repository Pattern та Unit of Work","\u002Fcsharp\u002Fdesktop-ui\u002Fdata-persistence-part2","01.csharp\u002F12.desktop-ui\u002F43.data-persistence-part2",{"title":1635,"path":1636,"stem":1637},"Тестування ViewModels","\u002Fcsharp\u002Fdesktop-ui\u002Fviewmodel-testing","01.csharp\u002F12.desktop-ui\u002F44.viewmodel-testing",{"title":1639,"path":1640,"stem":1641},"Avalonia Headless Testing — тестування UI без вікон","\u002Fcsharp\u002Fdesktop-ui\u002F44a.avalonia-headless-testing","01.csharp\u002F12.desktop-ui\u002F44a.avalonia-headless-testing",{"title":1643,"path":1644,"stem":1645},"Кросплатформна розробка з Avalonia","\u002Fcsharp\u002Fdesktop-ui\u002Favalonia-cross-platform","01.csharp\u002F12.desktop-ui\u002F45.avalonia-cross-platform",{"title":1647,"path":1648,"stem":1649},"Пакування та розгортання Avalonia додатків","\u002Fcsharp\u002Fdesktop-ui\u002Favalonia-packaging-deployment","01.csharp\u002F12.desktop-ui\u002F46.avalonia-packaging-deployment",{"title":1651,"path":1652,"stem":1653},"Розгортання WPF застосунків","\u002Fcsharp\u002Fdesktop-ui\u002Fwpf-packaging-deployment","01.csharp\u002F12.desktop-ui\u002F47.wpf-packaging-deployment",{"title":1655,"icon":658,"path":1656,"stem":1657,"children":1658,"page":59},"Network Programming","\u002Fcsharp\u002Fnetwork-programming","01.csharp\u002F13.network-programming",[1659,1663,1667,1671,1675,1679,1683,1687,1691,1695,1699],{"title":1660,"path":1661,"stem":1662},"Основи комп'ютерних мереж","\u002Fcsharp\u002Fnetwork-programming\u002Ffoundations","01.csharp\u002F13.network-programming\u002F01.foundations",{"title":1664,"path":1665,"stem":1666},"Модель OSI та стек TCP\u002FIP","\u002Fcsharp\u002Fnetwork-programming\u002Fosi-model","01.csharp\u002F13.network-programming\u002F02.osi-model",{"title":1668,"path":1669,"stem":1670},"IP-протокол та адресація","\u002Fcsharp\u002Fnetwork-programming\u002Fip-addressing","01.csharp\u002F13.network-programming\u002F03.ip-addressing",{"title":1672,"path":1673,"stem":1674},"UDP — протокол без з'єднання","\u002Fcsharp\u002Fnetwork-programming\u002Fudp","01.csharp\u002F13.network-programming\u002F05.udp",{"title":1676,"path":1677,"stem":1678},"UDP Broadcast та Multicast","\u002Fcsharp\u002Fnetwork-programming\u002Fudp-broadcast-multicast","01.csharp\u002F13.network-programming\u002F06.udp-broadcast-multicast",{"title":1680,"path":1681,"stem":1682},"HTTP — протокол вебу","\u002Fcsharp\u002Fnetwork-programming\u002Fhttp-fundamentals","01.csharp\u002F13.network-programming\u002F07.http-fundamentals",{"title":1684,"path":1685,"stem":1686},"HttpListener — вбудований HTTP-сервер .NET","\u002Fcsharp\u002Fnetwork-programming\u002F07a.http-listener","01.csharp\u002F13.network-programming\u002F07a.http-listener",{"title":1688,"path":1689,"stem":1690},"HTTP Advanced — cookies, аутентифікація та HTTPS","\u002Fcsharp\u002Fnetwork-programming\u002Fhttp-advanced","01.csharp\u002F13.network-programming\u002F08.http-advanced",{"title":1692,"path":1693,"stem":1694},"SMTP та протоколи електронної пошти","\u002Fcsharp\u002Fnetwork-programming\u002Fsmtp","01.csharp\u002F13.network-programming\u002F09.smtp",{"title":1696,"path":1697,"stem":1698},"WebSocket — повнодуплексний протокол реального часу","\u002Fcsharp\u002Fnetwork-programming\u002Fwebsockets","01.csharp\u002F13.network-programming\u002F10.websockets",{"title":1700,"path":1701,"stem":1702},"TLS\u002FSSL — криптографічний захист мережевих з'єднань","\u002Fcsharp\u002Fnetwork-programming\u002Ftls-ssl","01.csharp\u002F13.network-programming\u002F11.tls-ssl",{"title":1704,"path":1705,"stem":1706},"C# & .NET: The Ultimate Roadmap","\u002Fcsharp\u002Froadmap","01.csharp\u002Froadmap",{"title":1708,"icon":1709,"path":1710,"stem":1711,"children":1712,"page":59},"C++","i-devicon-cplusplus","\u002Fcpp","02.cpp",[1713,1717,1721,1725,1729,1733,1737,1741,1745,1748,1752,1756,1760,1764,1768,1772,1776,1780,1784,1788,1792,1796,1800,1804,1808,1812,1816,1820,1824,1828,1832,1836,1840,1844,1848,1852,1856,1860,1864,1868,1872,1876,1880,1884,1888,1892,1896,1900,1904,1908],{"title":1714,"path":1715,"stem":1716},"Вступ у програмування та алгоритми","\u002Fcpp\u002Fintro-algorithms","02.cpp\u002F01.intro-algorithms",{"title":1718,"path":1719,"stem":1720},"Code Style: угоди про оформлення коду","\u002Fcpp\u002Fcode-style","02.cpp\u002F02.code-style",{"title":1722,"path":1723,"stem":1724},"Середовище розробки та перший проєкт","\u002Fcpp\u002Fide-setup","02.cpp\u002F03.ide-setup",{"title":1726,"path":1727,"stem":1728},"Вивід даних на екран","\u002Fcpp\u002Fdata-output","02.cpp\u002F04.data-output",{"title":1730,"path":1731,"stem":1732},"Типи даних, змінні та константи","\u002Fcpp\u002Fdata-types-variables","02.cpp\u002F05.data-types-variables",{"title":1734,"path":1735,"stem":1736},"Ввід даних з клавіатури","\u002Fcpp\u002Fdata-input","02.cpp\u002F06.data-input",{"title":1738,"path":1739,"stem":1740},"Оператори, перетворення типів та логічні операції","\u002Fcpp\u002Foperators-type-conversion","02.cpp\u002F07.operators-type-conversion",{"title":1742,"path":1743,"stem":1744},"Цикли","\u002Fcpp\u002Floops","02.cpp\u002F08.loops",{"title":32,"path":1746,"stem":1747},"\u002Fcpp\u002Farrays","02.cpp\u002F09.arrays",{"title":1749,"path":1750,"stem":1751},"Алгоритми сортування та аналіз складності","\u002Fcpp\u002Fsorting","02.cpp\u002F10.sorting",{"title":1753,"path":1754,"stem":1755},"Алгоритми пошуку","\u002Fcpp\u002Fsearching","02.cpp\u002F11.searching",{"title":1757,"path":1758,"stem":1759},"Функції: основи","\u002Fcpp\u002Ffunctions-basics","02.cpp\u002F12.functions-basics",{"title":1761,"path":1762,"stem":1763},"Функції: прототипи, область видимості та додаткові можливості","\u002Fcpp\u002Ffunctions-scope","02.cpp\u002F13.functions-scope",{"title":1765,"path":1766,"stem":1767},"Функції: перевантаження та шаблони","\u002Fcpp\u002Ffunctions-overloading-templates","02.cpp\u002F14.functions-overloading-templates",{"title":1769,"path":1770,"stem":1771},"Вказівники: основи","\u002Fcpp\u002Fpointers-basics","02.cpp\u002F15.pointers-basics",{"title":1773,"path":1774,"stem":1775},"Посилання (References)","\u002Fcpp\u002Freferences","02.cpp\u002F16.references",{"title":1777,"path":1778,"stem":1779},"Вказівники, const і масиви","\u002Fcpp\u002Fpointers-const-arrays","02.cpp\u002F17.pointers-const-arrays",{"title":1781,"path":1782,"stem":1783},"Адресна арифметика","\u002Fcpp\u002Fpointer-arithmetic","02.cpp\u002F18.pointer-arithmetic",{"title":1785,"path":1786,"stem":1787},"Динамічна пам'ять","\u002Fcpp\u002Fdynamic-memory","02.cpp\u002F19.dynamic-memory",{"title":1789,"path":1790,"stem":1791},"Вказівники типу void","\u002Fcpp\u002Fvoid-pointers","02.cpp\u002F20.void-pointers",{"title":1793,"path":1794,"stem":1795},"Вказівники на вказівники","\u002Fcpp\u002Fpointers-to-pointers","02.cpp\u002F21.pointers-to-pointers",{"title":1797,"path":1798,"stem":1799},"Оператор доступу до членів через вказівник (->)","\u002Fcpp\u002Fmember-access-operator","02.cpp\u002F22.member-access-operator",{"title":1801,"path":1802,"stem":1803},"Цикл for-each (Range-based for)","\u002Fcpp\u002Fforeach-loop","02.cpp\u002F23.foreach-loop",{"title":1805,"path":1806,"stem":1807},"Вказівники на функції","\u002Fcpp\u002Ffunction-pointers","02.cpp\u002F24.function-pointers",{"title":1809,"path":1810,"stem":1811},"Лямбда-вирази","\u002Fcpp\u002Flambdas","02.cpp\u002F25.lambdas",{"title":1813,"path":1814,"stem":1815},"Лямбда-захоплення","\u002Fcpp\u002Flambda-captures","02.cpp\u002F26.lambda-captures",{"title":1817,"path":1818,"stem":1819},"Еліпсис","\u002Fcpp\u002Fellipsis","02.cpp\u002F27.ellipsis",{"title":1821,"path":1822,"stem":1823},"Безпечні альтернативи еліпсису","\u002Fcpp\u002F27a.ellipsis","02.cpp\u002F27a.ellipsis",{"title":1825,"path":1826,"stem":1827},"Аргументи командного рядка","\u002Fcpp\u002Fcommand-line-arguments","02.cpp\u002F28.command-line-arguments",{"title":1829,"path":1830,"stem":1831},"Перерахування (enum)","\u002Fcpp\u002Fenum","02.cpp\u002F29.enum",{"title":1833,"path":1834,"stem":1835},"Класи-перерахування (enum class)","\u002Fcpp\u002Fenum-class","02.cpp\u002F30.enum-class",{"title":1837,"path":1838,"stem":1839},"Псевдоніми типів (typedef і using)","\u002Fcpp\u002Ftype-aliases","02.cpp\u002F31.type-aliases",{"title":1841,"path":1842,"stem":1843},"Системи числення та двійкова арифметика","\u002Fcpp\u002Fnumber-systems","02.cpp\u002F32.number-systems",{"title":1845,"path":1846,"stem":1847},"Структури (struct): агрегування даних","\u002Fcpp\u002Fstruct","02.cpp\u002F33.struct",{"title":1849,"path":1850,"stem":1851},"Структури у функціях","\u002Fcpp\u002Fstruct-functions","02.cpp\u002F34.struct-functions",{"title":1853,"path":1854,"stem":1855},"Масиви структур і вкладені структури","\u002Fcpp\u002Fstruct-arrays","02.cpp\u002F35.struct-arrays",{"title":1857,"path":1858,"stem":1859},"Патерни struct та межі застосування","\u002Fcpp\u002Fstruct-patterns","02.cpp\u002F36.struct-patterns",{"title":1861,"path":1862,"stem":1863},"Символи та таблиця ASCII","\u002Fcpp\u002Fascii-characters","02.cpp\u002F37.ascii-characters",{"title":1865,"path":1866,"stem":1867},"Unicode та кодування UTF","\u002Fcpp\u002Funicode-utf","02.cpp\u002F38.unicode-utf",{"title":1869,"path":1870,"stem":1871},"C-style рядки","\u002Fcpp\u002Fc-strings","02.cpp\u002F39.c-strings",{"title":1873,"path":1874,"stem":1875},"Вступ до std::string","\u002Fcpp\u002Fstd-string-intro","02.cpp\u002F40.std-string-intro",{"title":1877,"path":1878,"stem":1879},"Довжина, ємність та доступ до символів std::string","\u002Fcpp\u002Fstd-string-capacity-access","02.cpp\u002F41.std-string-capacity-access",{"title":1881,"path":1882,"stem":1883},"Модифікація std::string: присвоювання, додавання, вставка, видалення та заміна","\u002Fcpp\u002Fstd-string-modification","02.cpp\u002F42.std-string-modification",{"title":1885,"path":1886,"stem":1887},"Пошук у std::string: find, npos та практичні патерни","\u002Fcpp\u002Fstd-string-search","02.cpp\u002F43.std-string-search",{"title":1889,"path":1890,"stem":1891},"std::string_view: невласницький погляд на рядок без копіювання","\u002Fcpp\u002Fstd-string-view","02.cpp\u002F44.std-string-view",{"title":1893,"path":1894,"stem":1895},"Об'єднання (union): один блок пам'яті, кілька інтерпретацій","\u002Fcpp\u002Funion","02.cpp\u002F45.union",{"title":1897,"path":1898,"stem":1899},"Організація коду: файли, препроцесор, простори імен","\u002Fcpp\u002Fmultifile-programs","02.cpp\u002F46.multifile-programs",{"title":1901,"path":1902,"stem":1903},"Робота з файлами: C-стиль (stdio.h)","\u002Fcpp\u002Fc-style-files","02.cpp\u002F47.c-style-files",{"title":1905,"path":1906,"stem":1907},"Робота з файлами: C++-стиль (fstream)","\u002Fcpp\u002Fcpp-style-files","02.cpp\u002F48.cpp-style-files",{"title":1909,"path":1910,"stem":1911},"План навчання: Курс C++ — Продовження (Статті 29–60+)","\u002Fcpp\u002Fcurriculum-plan","02.cpp\u002Fcurriculum-plan",{"title":1913,"icon":1914,"path":1915,"stem":1916,"children":1917,"page":59},"JavaScript","i-devicon-javascript","\u002Fjavascript","03.javascript",[1918,1944,1998,2020,2324,2362],{"title":1919,"icon":1920,"path":1921,"stem":1922,"children":1923,"page":59},"Events","i-lucide-mouse-pointer-click","\u002Fjavascript\u002Fevents","03.javascript\u002F01.events",[1924,1928,1932,1936,1940],{"title":1925,"path":1926,"stem":1927},"Вступ до подій браузера","\u002Fjavascript\u002Fevents\u002Fintro","03.javascript\u002F01.events\u002F01.intro",{"title":1929,"path":1930,"stem":1931},"Бульбашковий механізм (Bubbling) та занурення (Capturing)","\u002Fjavascript\u002Fevents\u002Fbubbling-capturing","03.javascript\u002F01.events\u002F02.bubbling-capturing",{"title":1933,"path":1934,"stem":1935},"Делегування подій (Event Delegation)","\u002Fjavascript\u002Fevents\u002Fdelegate-events","03.javascript\u002F01.events\u002F03.delegate-events",{"title":1937,"path":1938,"stem":1939},"Типові дії браузера та preventDefault()","\u002Fjavascript\u002Fevents\u002Fprevent-default","03.javascript\u002F01.events\u002F04.prevent-default",{"title":1941,"path":1942,"stem":1943},"Запуск користувацьких подій (Custom Events)","\u002Fjavascript\u002Fevents\u002Fcustom-events","03.javascript\u002F01.events\u002F05.custom-events",{"title":1945,"icon":1946,"path":1947,"stem":1948,"children":1949,"page":59},"Network","i-lucide-globe","\u002Fjavascript\u002Fnetwork","03.javascript\u002F02.network",[1950,1954,1958,1962,1966,1970,1974,1978,1982,1986,1990,1994],{"title":1951,"path":1952,"stem":1953},"Fetch API - Сучасний підхід до HTTP-запитів","\u002Fjavascript\u002Fnetwork\u002F01-fetch-api","03.javascript\u002F02.network\u002F01-fetch-api",{"title":1955,"path":1956,"stem":1957},"FormData - Робота з формами та файлами","\u002Fjavascript\u002Fnetwork\u002F02-formdata","03.javascript\u002F02.network\u002F02-formdata",{"title":1959,"path":1960,"stem":1961},"Відстеження прогресу завантаження","\u002Fjavascript\u002Fnetwork\u002F03-download-progress","03.javascript\u002F02.network\u002F03-download-progress",{"title":1963,"path":1964,"stem":1965},"Переривання fetch-запитів","\u002Fjavascript\u002Fnetwork\u002F04-abort-requests","03.javascript\u002F02.network\u002F04-abort-requests",{"title":1967,"path":1968,"stem":1969},"CORS - Запити між різними джерелами","\u002Fjavascript\u002Fnetwork\u002F05-cors","03.javascript\u002F02.network\u002F05-cors",{"title":1971,"path":1972,"stem":1973},"Fetch API - Повний довідник опцій","\u002Fjavascript\u002Fnetwork\u002F06-fetch-options","03.javascript\u002F02.network\u002F06-fetch-options",{"title":1975,"path":1976,"stem":1977},"URL Objects - Робота з посиланнями","\u002Fjavascript\u002Fnetwork\u002F07-url-objects","03.javascript\u002F02.network\u002F07-url-objects",{"title":1979,"path":1980,"stem":1981},"XMLHttpRequest - AJAX та низькорівневі запити","\u002Fjavascript\u002Fnetwork\u002F08-xmlhttprequest","03.javascript\u002F02.network\u002F08-xmlhttprequest",{"title":1983,"path":1984,"stem":1985},"Відновлюване завантаження файлів","\u002Fjavascript\u002Fnetwork\u002F09-resumable-upload","03.javascript\u002F02.network\u002F09-resumable-upload",{"title":1987,"path":1988,"stem":1989},"Cookies, document.cookie та світ після \"Cookiepocalypse\"","\u002Fjavascript\u002Fnetwork\u002F10-cookies","03.javascript\u002F02.network\u002F10-cookies",{"title":1991,"path":1992,"stem":1993},"js-cookie: Керування Cookies без Болю","\u002Fjavascript\u002Fnetwork\u002F11-js-cookie","03.javascript\u002F02.network\u002F11-js-cookie",{"title":1995,"path":1996,"stem":1997},"Axios: Потужний HTTP-клієнт для JavaScript","\u002Fjavascript\u002Fnetwork\u002F12-axios","03.javascript\u002F02.network\u002F12-axios",{"title":1999,"icon":2000,"path":2001,"stem":2002,"children":2003,"page":59},"Bom","i-lucide-monitor","\u002Fjavascript\u002Fbom","03.javascript\u002F03.bom",[2004,2008,2012,2016],{"title":2005,"path":2006,"stem":2007},"LocalStorage, SessionStorage та patterns збереження даних","\u002Fjavascript\u002Fbom\u002F01-localstorage","03.javascript\u002F03.bom\u002F01-localstorage",{"title":2009,"path":2010,"stem":2011},"Location Object - Керування адресою сторінки","\u002Fjavascript\u002Fbom\u002F02-location-object","03.javascript\u002F03.bom\u002F02-location-object",{"title":2013,"path":2014,"stem":2015},"History API - Керування історією браузера","\u002Fjavascript\u002Fbom\u002F03-history-api","03.javascript\u002F03.bom\u002F03-history-api",{"title":2017,"path":2018,"stem":2019},"Navigator Object - Ідентифікація та Можливості Пристрою","\u002Fjavascript\u002Fbom\u002F04-navigator-object","03.javascript\u002F03.bom\u002F04-navigator-object",{"title":2021,"icon":2022,"path":2023,"stem":2024,"children":2025},"React","i-devicon-react","\u002Fjavascript\u002Freact","03.javascript\u002F04.react\u002Findex",[2026,2027,2031,2035,2039,2043,2106,2141,2293],{"title":2021,"path":2023,"stem":2024},{"title":2028,"path":2029,"stem":2030},"Робота з Формами в React","\u002Fjavascript\u002Freact\u002Freact-forms","03.javascript\u002F04.react\u002F01.react-forms",{"title":2032,"path":2033,"stem":2034},"React Hook Form: Професійна Робота з Формами","\u002Fjavascript\u002Freact\u002Freact-hook-form","03.javascript\u002F04.react\u002F02.react-hook-form",{"title":2036,"path":2037,"stem":2038},"React Hook Form: Глибоке Розуміння Архітектури та Оптимізації","\u002Fjavascript\u002Freact\u002Freact-hook-form-new","03.javascript\u002F04.react\u002F02.react-hook-form-new",{"title":2040,"path":2041,"stem":2042},"Axios та React: Професійна Архітектура Запитів","\u002Fjavascript\u002Freact\u002Fdata-fetching-axios","03.javascript\u002F04.react\u002F03.data-fetching-axios",{"title":2044,"icon":132,"path":2045,"stem":2046,"children":2047},"Tanstack Query","\u002Fjavascript\u002Freact\u002Ftanstack-query","03.javascript\u002F04.react\u002F04.tanstack-query\u002Findex",[2048,2050,2054,2058,2062,2066,2070,2074,2078,2082,2086,2090,2094,2098,2102],{"title":2049,"path":2045,"stem":2046},"TanStack Query: Майстерність Керування Станом Сервера",{"title":2051,"path":2052,"stem":2053},"Парадигма Server State: Чому useEffect недостатньо","\u002Fjavascript\u002Freact\u002Ftanstack-query\u002Fserver-state-paradigm","03.javascript\u002F04.react\u002F04.tanstack-query\u002F01.server-state-paradigm",{"title":2055,"path":2056,"stem":2057},"Встановлення та Налаштування: Фундамент","\u002Fjavascript\u002Freact\u002Ftanstack-query\u002Finstallation-and-devtools","03.javascript\u002F04.react\u002F04.tanstack-query\u002F02.installation-and-devtools",{"title":2059,"path":2060,"stem":2061},"Основи Запитів та Магія Ключів","\u002Fjavascript\u002Freact\u002Ftanstack-query\u002Fquery-basics-and-keys","03.javascript\u002F04.react\u002F04.tanstack-query\u002F03.query-basics-and-keys",{"title":2063,"path":2064,"stem":2065},"Синхронізація Даних: Життєвий Цикл Запиту","\u002Fjavascript\u002Freact\u002Ftanstack-query\u002Fdata-synchronization","03.javascript\u002F04.react\u002F04.tanstack-query\u002F04.data-synchronization",{"title":2067,"path":2068,"stem":2069},"Мутації та Інвалідація: Зміна Даних","\u002Fjavascript\u002Freact\u002Ftanstack-query\u002Fmutations-and-invalidation","03.javascript\u002F04.react\u002F04.tanstack-query\u002F05.mutations-and-invalidation",{"title":2071,"path":2072,"stem":2073},"Оптимістичні Оновлення: Швидше за Світло","\u002Fjavascript\u002Freact\u002Ftanstack-query\u002Foptimistic-updates","03.javascript\u002F04.react\u002F04.tanstack-query\u002F06.optimistic-updates",{"title":2075,"path":2076,"stem":2077},"Пагінація та Infinite Scroll","\u002Fjavascript\u002Freact\u002Ftanstack-query\u002Fpagination-and-load-more","03.javascript\u002F04.react\u002F04.tanstack-query\u002F07.pagination-and-load-more",{"title":2079,"path":2080,"stem":2081},"Просунуті Патерни та Оптимізація","\u002Fjavascript\u002Freact\u002Ftanstack-query\u002Fadvanced-patterns","03.javascript\u002F04.react\u002F04.tanstack-query\u002F08.advanced-patterns",{"title":2083,"path":2084,"stem":2085},"Архітектура та Best Practices","\u002Fjavascript\u002Freact\u002Ftanstack-query\u002Farchitecture-and-best-practices","03.javascript\u002F04.react\u002F04.tanstack-query\u002F09.architecture-and-best-practices",{"title":2087,"path":2088,"stem":2089},"Server-Side Rendering (SSR) та Гідратація","\u002Fjavascript\u002Freact\u002Ftanstack-query\u002Fserver-side-rendering","03.javascript\u002F04.react\u002F04.tanstack-query\u002F10.server-side-rendering",{"title":2091,"path":2092,"stem":2093},"Стратегії Тестування","\u002Fjavascript\u002Freact\u002Ftanstack-query\u002Ftesting-strategies","03.javascript\u002F04.react\u002F04.tanstack-query\u002F11.testing-strategies",{"title":2095,"path":2096,"stem":2097},"Аутентифікація та Обробка Помилок","\u002Fjavascript\u002Freact\u002Ftanstack-query\u002Fauthentication-and-errors","03.javascript\u002F04.react\u002F04.tanstack-query\u002F12.authentication-and-errors",{"title":2099,"path":2100,"stem":2101},"React Suspense та Майбутнє","\u002Fjavascript\u002Freact\u002Ftanstack-query\u002Freact-suspense","03.javascript\u002F04.react\u002F04.tanstack-query\u002F13.react-suspense",{"title":2103,"path":2104,"stem":2105},"Глибоке Занурення в Продуктивність","\u002Fjavascript\u002Freact\u002Ftanstack-query\u002Fperformance-deep-dive","03.javascript\u002F04.react\u002F04.tanstack-query\u002F14.performance-deep-dive",{"title":2107,"icon":2022,"path":2108,"stem":2109,"children":2110},"React Router","\u002Fjavascript\u002Freact\u002Freact-router","03.javascript\u002F04.react\u002F05.react-router\u002Findex",[2111,2113,2117,2121,2125,2129,2133,2137],{"title":2112,"path":2108,"stem":2109},"React Router: Навігаційна система сучасного вебу",{"title":2114,"path":2115,"stem":2116},"Налаштування та Базовий Роутинг","\u002Fjavascript\u002Freact\u002Freact-router\u002Fsetup-and-basic-routing","03.javascript\u002F04.react\u002F05.react-router\u002F01.setup-and-basic-routing",{"title":2118,"path":2119,"stem":2120},"Динамічна Навігація","\u002Fjavascript\u002Freact\u002Freact-router\u002Fnavigation-and-links","03.javascript\u002F04.react\u002F05.react-router\u002F02.navigation-and-links",{"title":2122,"path":2123,"stem":2124},"Вкладені Маршрути та Макети","\u002Fjavascript\u002Freact\u002Freact-router\u002Fnested-routes-and-layouts","03.javascript\u002F04.react\u002F05.react-router\u002F03.nested-routes-and-layouts",{"title":2126,"path":2127,"stem":2128},"Динамічні Маршрути та Параметри","\u002Fjavascript\u002Freact\u002Freact-router\u002Fdynamic-routing","03.javascript\u002F04.react\u002F05.react-router\u002F04.dynamic-routing",{"title":2130,"path":2131,"stem":2132},"Data APIs: Loaders та Actions","\u002Fjavascript\u002Freact\u002Freact-router\u002Fdata-loading","03.javascript\u002F04.react\u002F05.react-router\u002F05.data-loading",{"title":2134,"path":2135,"stem":2136},"Просунуті Патерни","\u002Fjavascript\u002Freact\u002Freact-router\u002Fadvanced-patterns","03.javascript\u002F04.react\u002F05.react-router\u002F06.advanced-patterns",{"title":2138,"path":2139,"stem":2140},"Legacy Routing: Компонентний підхід","\u002Fjavascript\u002Freact\u002Freact-router\u002Flegacy-routing","03.javascript\u002F04.react\u002F05.react-router\u002F07.legacy-routing",{"title":2142,"icon":132,"path":2143,"stem":2144,"children":2145},"Redux","\u002Fjavascript\u002Freact\u002Fredux","03.javascript\u002F04.react\u002F06.redux\u002Findex",[2146,2148,2164,2193,2202,2223,2239,2268],{"title":2147,"path":2143,"stem":2144},"Redux: Еволюція управління станом",{"title":14,"icon":15,"path":2149,"stem":2150,"children":2151,"page":59},"\u002Fjavascript\u002Freact\u002Fredux\u002Ffundamentals","03.javascript\u002F04.react\u002F06.redux\u002F01.fundamentals",[2152,2156,2160],{"title":2153,"path":2154,"stem":2155},"Вступ до State Management","\u002Fjavascript\u002Freact\u002Fredux\u002Ffundamentals\u002Fintro-state-management","03.javascript\u002F04.react\u002F06.redux\u002F01.fundamentals\u002F01.intro-state-management",{"title":2157,"path":2158,"stem":2159},"Філософія Redux та Три Принципи","\u002Fjavascript\u002Freact\u002Fredux\u002Ffundamentals\u002Fredux-philosophy","03.javascript\u002F04.react\u002F06.redux\u002F01.fundamentals\u002F02.redux-philosophy",{"title":2161,"path":2162,"stem":2163},"Чисті функції та Іммутабельність","\u002Fjavascript\u002Freact\u002Fredux\u002Ffundamentals\u002Fpure-functions-immutability","03.javascript\u002F04.react\u002F06.redux\u002F01.fundamentals\u002F03.pure-functions-immutability",{"title":2165,"icon":132,"path":2166,"stem":2167,"children":2168,"page":59},"Classic Redux","\u002Fjavascript\u002Freact\u002Fredux\u002Fclassic-redux","03.javascript\u002F04.react\u002F06.redux\u002F02.classic-redux",[2169,2173,2177,2181,2185,2189],{"title":2170,"path":2171,"stem":2172},"Створення Store (Classic Redux)","\u002Fjavascript\u002Freact\u002Fredux\u002Fclassic-redux\u002Fstore-setup","03.javascript\u002F04.react\u002F06.redux\u002F02.classic-redux\u002F01.store-setup",{"title":2174,"path":2175,"stem":2176},"Actions, Constants та Action Creators","\u002Fjavascript\u002Freact\u002Fredux\u002Fclassic-redux\u002Factions-constants","03.javascript\u002F04.react\u002F06.redux\u002F02.classic-redux\u002F02.actions-constants",{"title":2178,"path":2179,"stem":2180},"Логіка Reducers","\u002Fjavascript\u002Freact\u002Fredux\u002Fclassic-redux\u002Freducers","03.javascript\u002F04.react\u002F06.redux\u002F02.classic-redux\u002F03.reducers",{"title":2182,"path":2183,"stem":2184},"Комбінування Reducers (Root Reducer)","\u002Fjavascript\u002Freact\u002Fredux\u002Fclassic-redux\u002Fdata-flow","03.javascript\u002F04.react\u002F06.redux\u002F02.classic-redux\u002F04.data-flow",{"title":2186,"path":2187,"stem":2188},"Підключення до React (React-Redux)","\u002Fjavascript\u002Freact\u002Fredux\u002Fclassic-redux\u002Freact-redux-connection","03.javascript\u002F04.react\u002F06.redux\u002F02.classic-redux\u002F05.react-redux-connection",{"title":2190,"path":2191,"stem":2192},"Middleware та Асинхронність (Redux Thunk)","\u002Fjavascript\u002Freact\u002Fredux\u002Fclassic-redux\u002Fmiddleware-thunk","03.javascript\u002F04.react\u002F06.redux\u002F02.classic-redux\u002F06.middleware-thunk",{"title":2194,"icon":132,"path":2195,"stem":2196,"children":2197,"page":59},"Transition To Rtk","\u002Fjavascript\u002Freact\u002Fredux\u002Ftransition-to-rtk","03.javascript\u002F04.react\u002F06.redux\u002F03.transition-to-rtk",[2198],{"title":2199,"path":2200,"stem":2201},"Проблеми класичного Redux","\u002Fjavascript\u002Freact\u002Fredux\u002Ftransition-to-rtk\u002Fproblems-with-classic","03.javascript\u002F04.react\u002F06.redux\u002F03.transition-to-rtk\u002F01.problems-with-classic",{"title":2203,"icon":132,"path":2204,"stem":2205,"children":2206,"page":59},"Redux Toolkit","\u002Fjavascript\u002Freact\u002Fredux\u002Fredux-toolkit","03.javascript\u002F04.react\u002F06.redux\u002F04.redux-toolkit",[2207,2211,2215,2219],{"title":2208,"path":2209,"stem":2210},"Налаштування Store з configureStore","\u002Fjavascript\u002Freact\u002Fredux\u002Fredux-toolkit\u002Fconfigure-store","03.javascript\u002F04.react\u002F06.redux\u002F04.redux-toolkit\u002F01.configure-store",{"title":2212,"path":2213,"stem":2214},"createSlice: Революція в Redux","\u002Fjavascript\u002Freact\u002Fredux\u002Fredux-toolkit\u002Fcreate-slice","03.javascript\u002F04.react\u002F06.redux\u002F04.redux-toolkit\u002F02.create-slice",{"title":2216,"path":2217,"stem":2218},"Асинхронність з createAsyncThunk","\u002Fjavascript\u002Freact\u002Fredux\u002Fredux-toolkit\u002Fasync-thunks","03.javascript\u002F04.react\u002F06.redux\u002F04.redux-toolkit\u002F03.async-thunks",{"title":2220,"path":2221,"stem":2222},"04. Entity Adapter: Керування нормалізованим станом","\u002Fjavascript\u002Freact\u002Fredux\u002Fredux-toolkit\u002Fentity-adapter","03.javascript\u002F04.react\u002F06.redux\u002F04.redux-toolkit\u002F04.entity-adapter",{"title":2224,"icon":92,"path":2225,"stem":2226,"children":2227,"page":59},"Advanced","\u002Fjavascript\u002Freact\u002Fredux\u002Fadvanced","03.javascript\u002F04.react\u002F06.redux\u002F05.advanced",[2228,2232,2236],{"title":2229,"path":2230,"stem":2231},"Мемоізація та Селектори: Повний Гайд по Reselect","\u002Fjavascript\u002Freact\u002Fredux\u002Fadvanced\u002Fselectors-reselect","03.javascript\u002F04.react\u002F06.redux\u002F05.advanced\u002F01.selectors-reselect",{"title":2233,"path":2234,"stem":2235},"RTK Query: Архітектура Серверного Кешу","\u002Fjavascript\u002Freact\u002Fredux\u002Fadvanced\u002Frtk-query-intro","03.javascript\u002F04.react\u002F06.redux\u002F05.advanced\u002F02.rtk-query-intro",{"title":2083,"path":2237,"stem":2238},"\u002Fjavascript\u002Freact\u002Fredux\u002Fadvanced\u002Farchitecture-best-practices","03.javascript\u002F04.react\u002F06.redux\u002F05.advanced\u002F03.architecture-best-practices",{"title":2240,"icon":132,"path":2241,"stem":2242,"children":2243,"page":59},"Project Kanban","\u002Fjavascript\u002Freact\u002Fredux\u002Fproject-kanban","03.javascript\u002F04.react\u002F06.redux\u002F06.project-kanban",[2244,2248,2252,2256,2260,2264],{"title":2245,"path":2246,"stem":2247},"Проєкт: Kanban Board (Trello Clone)","\u002Fjavascript\u002Freact\u002Fredux\u002Fproject-kanban\u002Fproject-overview","03.javascript\u002F04.react\u002F06.redux\u002F06.project-kanban\u002F01.project-overview",{"title":2249,"path":2250,"stem":2251},"Налаштування та Типізація","\u002Fjavascript\u002Freact\u002Fredux\u002Fproject-kanban\u002Fsetup-and-types","03.javascript\u002F04.react\u002F06.redux\u002F06.project-kanban\u002F02.setup-and-types",{"title":2253,"path":2254,"stem":2255},"Board Slice: Серце Дошки","\u002Fjavascript\u002Freact\u002Fredux\u002Fproject-kanban\u002Fboard-slice","03.javascript\u002F04.react\u002F06.redux\u002F06.project-kanban\u002F03.board-slice",{"title":2257,"path":2258,"stem":2259},"Логіка Drag & Drop","\u002Fjavascript\u002Freact\u002Fredux\u002Fproject-kanban\u002Fdrag-and-drop-logic","03.javascript\u002F04.react\u002F06.redux\u002F06.project-kanban\u002F04.drag-and-drop-logic",{"title":2261,"path":2262,"stem":2263},"Інтеграція з RTK Query","\u002Fjavascript\u002Freact\u002Fredux\u002Fproject-kanban\u002Frtk-query-integration","03.javascript\u002F04.react\u002F06.redux\u002F06.project-kanban\u002F05.rtk-query-integration",{"title":2265,"path":2266,"stem":2267},"Optimistic Updates","\u002Fjavascript\u002Freact\u002Fredux\u002Fproject-kanban\u002Foptimistic-updates","03.javascript\u002F04.react\u002F06.redux\u002F06.project-kanban\u002F06.optimistic-updates",{"title":2269,"icon":132,"path":2270,"stem":2271,"children":2272,"page":59},"Testing","\u002Fjavascript\u002Freact\u002Fredux\u002Ftesting","03.javascript\u002F04.react\u002F06.redux\u002F07.testing",[2273,2277,2281,2285,2289],{"title":2274,"path":2275,"stem":2276},"Тестування Redux","\u002Fjavascript\u002Freact\u002Fredux\u002Ftesting\u002Fintro-testing","03.javascript\u002F04.react\u002F06.redux\u002F07.testing\u002F01.intro-testing",{"title":2278,"path":2279,"stem":2280},"Тестування Reducers","\u002Fjavascript\u002Freact\u002Fredux\u002Ftesting\u002Ftesting-reducers","03.javascript\u002F04.react\u002F06.redux\u002F07.testing\u002F02.testing-reducers",{"title":2282,"path":2283,"stem":2284},"Тестування Селекторів","\u002Fjavascript\u002Freact\u002Fredux\u002Ftesting\u002Ftesting-selectors","03.javascript\u002F04.react\u002F06.redux\u002F07.testing\u002F03.testing-selectors",{"title":2286,"path":2287,"stem":2288},"Тестування Компонентів (Integration)","\u002Fjavascript\u002Freact\u002Fredux\u002Ftesting\u002Ftesting-components","03.javascript\u002F04.react\u002F06.redux\u002F07.testing\u002F04.testing-components",{"title":2290,"path":2291,"stem":2292},"Тестування Async Thunks","\u002Fjavascript\u002Freact\u002Fredux\u002Ftesting\u002Ftesting-thunks","03.javascript\u002F04.react\u002F06.redux\u002F07.testing\u002F05.testing-thunks",{"title":2294,"icon":132,"path":2295,"stem":2296,"children":2297},"Ui Libraries","\u002Fjavascript\u002Freact\u002Fui-libraries","03.javascript\u002F04.react\u002F07.ui-libraries\u002Findex",[2298,2300,2304,2308,2312,2316,2320],{"title":2299,"path":2295,"stem":2296},"UI Бібліотеки в React",{"title":2301,"path":2302,"stem":2303},"Вступ до UI Бібліотек: Навіщо Винаходити Велосипед Двічі?","\u002Fjavascript\u002Freact\u002Fui-libraries\u002Fintroduction-to-ui-libraries","03.javascript\u002F04.react\u002F07.ui-libraries\u002F01.introduction-to-ui-libraries",{"title":2305,"path":2306,"stem":2307},"Філософія shadcn\u002Fui: \"Not a Component Library\"","\u002Fjavascript\u002Freact\u002Fui-libraries\u002Fshadcn-philosophy","03.javascript\u002F04.react\u002F07.ui-libraries\u002F02.shadcn-philosophy",{"title":2309,"path":2310,"stem":2311},"Установка та Налаштування shadcn\u002Fui","\u002Fjavascript\u002Freact\u002Fui-libraries\u002Fshadcn-installation","03.javascript\u002F04.react\u002F07.ui-libraries\u002F03.shadcn-installation",{"title":2313,"path":2314,"stem":2315},"Базові Компоненти shadcn\u002Fui: Фундамент Інтерфейсу","\u002Fjavascript\u002Freact\u002Fui-libraries\u002Fshadcn-components-basics","03.javascript\u002F04.react\u002F07.ui-libraries\u002F04.shadcn-components-basics",{"title":2317,"path":2318,"stem":2319},"Компоненти Форм: Побудова Інтерактивних Form","\u002Fjavascript\u002Freact\u002Fui-libraries\u002Fshadcn-components-forms","03.javascript\u002F04.react\u002F07.ui-libraries\u002F05.shadcn-components-forms",{"title":2321,"path":2322,"stem":2323},"Складні Компоненти: Dialog, Dropdown, Table та Command","\u002Fjavascript\u002Freact\u002Fui-libraries\u002Fshadcn-components-advanced","03.javascript\u002F04.react\u002F07.ui-libraries\u002F06.shadcn-components-advanced",{"title":2325,"icon":2326,"path":2327,"stem":2328,"children":2329,"page":59},"TypeScript","i-devicon-typescript","\u002Fjavascript\u002Ftypescript","03.javascript\u002F05.typescript",[2330,2334,2338,2342,2346,2350,2354,2358],{"title":2331,"path":2332,"stem":2333},"TypeScript: Броня для вашого коду","\u002Fjavascript\u002Ftypescript\u002Fintro-and-basic-types","03.javascript\u002F05.typescript\u002F01.intro-and-basic-types",{"title":2335,"path":2336,"stem":2337},"Майстерність Моделювання Даних: Інтерфейси та Просунуті Типи","\u002Fjavascript\u002Ftypescript\u002Finterfaces-and-advanced-types","03.javascript\u002F05.typescript\u002F02.interfaces-and-advanced-types",{"title":2339,"path":2340,"stem":2341},"Алхімія Типів: Generics та Utility Types","\u002Fjavascript\u002Ftypescript\u002Fgenerics-and-utilities","03.javascript\u002F05.typescript\u002F03.generics-and-utilities",{"title":2343,"path":2344,"stem":2345},"Архітектура та Шаблони: Класи в TypeScript","\u002Fjavascript\u002Ftypescript\u002Fclasses-and-oop","03.javascript\u002F05.typescript\u002F04.classes-and-oop",{"title":2347,"path":2348,"stem":2349},"Продакшн та Екосистема: Advanced Config & Workflow","\u002Fjavascript\u002Ftypescript\u002Fadvanced-patterns-and-config","03.javascript\u002F05.typescript\u002F05.advanced-patterns-and-config",{"title":2351,"path":2352,"stem":2353},"TypeScript у світі React","\u002Fjavascript\u002Ftypescript\u002Freact-basics","03.javascript\u002F05.typescript\u002F06.react-basics",{"title":2355,"path":2356,"stem":2357},"React + TypeScript: Продвинуті патерни","\u002Fjavascript\u002Ftypescript\u002Freact-advanced","03.javascript\u002F05.typescript\u002F07.react-advanced",{"title":2359,"path":2360,"stem":2361},"React + TypeScript: Екосистема та бібліотеки","\u002Fjavascript\u002Ftypescript\u002Freact-ecosystem","03.javascript\u002F05.typescript\u002F08.react-ecosystem",{"title":2363,"path":2364,"stem":2365},"Atomic Design","\u002Fjavascript\u002Fatomic-design","03.javascript\u002F2.atomic-design",{"title":2367,"icon":2368,"path":2369,"stem":2370,"children":2371,"page":59},"Java","i-devicon-java","\u002Fjava","04.java",[2372,2375,2378,2382,2386,2390,2394],{"title":162,"path":2373,"stem":2374},"\u002Fjava\u002Fdata-mapper-part1","04.java\u002F01.data-mapper-part1",{"title":166,"path":2376,"stem":2377},"\u002Fjava\u002Fdata-mapper-part2","04.java\u002F02.data-mapper-part2",{"title":2379,"path":2380,"stem":2381},"Service Layer: Організація бізнес-логіки","\u002Fjava\u002Fservice-layer","04.java\u002F03.service-layer",{"title":2383,"path":2384,"stem":2385},"Rich Domain Model та State Pattern","\u002Fjava\u002Frich-domain-model","04.java\u002F04.rich-domain-model",{"title":2387,"path":2388,"stem":2389},"Патерни для складної бізнес-логіки","\u002Fjava\u002Fbusiness-logic-patterns","04.java\u002F05.business-logic-patterns",{"title":2391,"path":2392,"stem":2393},"Обробка помилок та валідація","\u002Fjava\u002Ferror-handling-validation","04.java\u002F06.error-handling-validation",{"title":2395,"path":2396,"stem":2397,"children":2398,"page":59},"Проектування баз даних","\u002Fjava\u002Fpr2","04.java\u002Fpr2",[2399,2403,2407,2411,2415,2419,2423,2427,2431,2435,2439,2443,2447,2451,2455,2459,2463,2467,2471,2475,2479,2483,2487,2491,2495,2499,2503,2507,2511,2515,2519,2523,2527,2531,2535,2539,2543],{"title":2400,"path":2401,"stem":2402},"Концептуальне моделювання: Мистецтво розуміння предметної області","\u002Fjava\u002Fpr2\u002Fconceptual-modeling","04.java\u002Fpr2\u002F01.conceptual-modeling",{"title":2404,"path":2405,"stem":2406},"Логічне моделювання: Від бізнес-ідей до структур даних","\u002Fjava\u002Fpr2\u002Flogical-modeling","04.java\u002Fpr2\u002F02.logical-modeling",{"title":2408,"path":2409,"stem":2410},"Нормалізація: Гігієна даних та боротьба з аномаліями","\u002Fjava\u002Fpr2\u002Fnormalization","04.java\u002Fpr2\u002F03.normalization",{"title":2412,"path":2413,"stem":2414},"Фізична схема: Від абстракції до DDL","\u002Fjava\u002Fpr2\u002Fphysical-schema","04.java\u002Fpr2\u002F04.physical-schema",{"title":2416,"path":2417,"stem":2418},"Архітектурна класифікація таблиць","\u002Fjava\u002Fpr2\u002Ftable-classification","04.java\u002Fpr2\u002F05.table-classification",{"title":2420,"path":2421,"stem":2422},"Database Migrations: Версіонування схеми з Flyway","\u002Fjava\u002Fpr2\u002Fdatabase-migrations","04.java\u002Fpr2\u002F06.database-migrations",{"title":2424,"path":2425,"stem":2426},"А що, якби це була не реляційна БД?","\u002Fjava\u002Fpr2\u002Fbeyond-relational","04.java\u002Fpr2\u002F07.beyond-relational",{"title":2428,"path":2429,"stem":2430},"Object-Relational Impedance Mismatch: Два світи, що не хочуть дружити","\u002Fjava\u002Fpr2\u002Fimpedance-mismatch","04.java\u002Fpr2\u002F09.impedance-mismatch",{"title":2432,"path":2433,"stem":2434},"JDBC: Перший контакт із базою даних","\u002Fjava\u002Fpr2\u002Fjdbc-fundamentals","04.java\u002Fpr2\u002F10.jdbc-fundamentals",{"title":2436,"path":2437,"stem":2438},"Якість коду: Spotless, SpotBugs та SonarQube","\u002Fjava\u002Fpr2\u002F10a.code-quality","04.java\u002Fpr2\u002F10a.code-quality",{"title":2440,"path":2441,"stem":2442},"Connection Pool: Патерн Object Pool для JDBC-з'єднань","\u002Fjava\u002Fpr2\u002Fconnection-pool","04.java\u002Fpr2\u002F11.connection-pool",{"title":2444,"path":2445,"stem":2446},"Row Data Gateway: Об'єкт як обгортка рядка таблиці","\u002Fjava\u002Fpr2\u002Frow-data-gateway","04.java\u002Fpr2\u002F12.row-data-gateway",{"title":2448,"path":2449,"stem":2450},"Table Data Gateway: Фасад таблиці як архітектурний відступ","\u002Fjava\u002Fpr2\u002Ftable-data-gateway","04.java\u002Fpr2\u002F13.table-data-gateway",{"title":2452,"path":2453,"stem":2454},"Repository + Data Mapper: Правильна шарова архітектура з JDBC","\u002Fjava\u002Fpr2\u002Frepository-data-mapper","04.java\u002Fpr2\u002F14.repository-data-mapper",{"title":2456,"path":2457,"stem":2458},"Identity Map: Кешування сутностей у рамках сесії","\u002Fjava\u002Fpr2\u002Fidentity-map","04.java\u002Fpr2\u002F15.identity-map",{"title":2460,"path":2461,"stem":2462},"Unit of Work: Відстеження змін і координація JDBC-транзакцій","\u002Fjava\u002Fpr2\u002Funit-of-work","04.java\u002Fpr2\u002F16.unit-of-work",{"title":2464,"path":2465,"stem":2466},"Strategy: Замінювані SQL-стратегії для підтримки різних СУБД","\u002Fjava\u002Fpr2\u002Fstrategy-sql","04.java\u002Fpr2\u002F17.strategy-sql",{"title":2468,"path":2469,"stem":2470},"Proxy: Lazy Loading для One-To-Many колекцій","\u002Fjava\u002Fpr2\u002Fproxy-lazy-loading","04.java\u002Fpr2\u002F18.proxy-lazy-loading",{"title":2472,"path":2473,"stem":2474},"Generic Repository через Java Reflection: анотації та динамічний SQL","\u002Fjava\u002Fpr2\u002Fgeneric-repository-reflection","04.java\u002Fpr2\u002F19.generic-repository-reflection",{"title":2476,"path":2477,"stem":2478},"Specification Pattern: Композиція бізнес-правил для складних запитів","\u002Fjava\u002Fpr2\u002Fspecification-pattern","04.java\u002Fpr2\u002F20.specification-pattern",{"title":2480,"path":2481,"stem":2482},"Розширені можливості Specification Pattern: підзапити, агрегації та гібридний підхід","\u002Fjava\u002Fpr2\u002F20a.advanced-specifications","04.java\u002Fpr2\u002F20a.advanced-specifications",{"title":2484,"path":2485,"stem":2486},"Асинхронність у JDBC: Від блокуючих викликів до CompletableFuture","\u002Fjava\u002Fpr2\u002Fasynchronous-jdbc","04.java\u002Fpr2\u002F21.asynchronous-jdbc",{"title":2488,"path":2489,"stem":2490},"Інтеграційне тестування JDBC-репозиторіїв: Embedded H2 та патерн AAA","\u002Fjava\u002Fpr2\u002Fintegration-testing-h2","04.java\u002Fpr2\u002F22.integration-testing-h2",{"title":2492,"path":2493,"stem":2494},"Testcontainers: Тестування з реальною PostgreSQL у Docker-контейнерах","\u002Fjava\u002Fpr2\u002Fintegration-testing-testcontainers","04.java\u002Fpr2\u002F23.integration-testing-testcontainers",{"title":2496,"path":2497,"stem":2498},"Google Guice: Впровадження залежностей у JavaFX-проєкті","\u002Fjava\u002Fpr2\u002Fdependency-injection-guice","04.java\u002Fpr2\u002F24.dependency-injection-guice",{"title":2500,"path":2501,"stem":2502},"JavaFX: Основи побудови графічних інтерфейсів","\u002Fjava\u002Fpr2\u002Fjavafx-fundamentals","04.java\u002Fpr2\u002F25.javafx-fundamentals",{"title":2504,"path":2505,"stem":2506},"Properties та Bindings: Реактивність у JavaFX","\u002Fjava\u002Fpr2\u002Fjavafx-properties-bindings","04.java\u002Fpr2\u002F26.javafx-properties-bindings",{"title":2508,"path":2509,"stem":2510},"MVC vs MVP vs MVVM: Еволюція архітектурних патернів UI","\u002Fjava\u002Fpr2\u002Fui-architecture-patterns","04.java\u002Fpr2\u002F27.ui-architecture-patterns",{"title":2512,"path":2513,"stem":2514},"MVVM на практиці: Побудова ViewModel","\u002Fjava\u002Fpr2\u002Fmvvm-viewmodel-implementation","04.java\u002Fpr2\u002F28.mvvm-viewmodel-implementation",{"title":2516,"path":2517,"stem":2518},"View та Controller: Зв'язування з ViewModel через FXML","\u002Fjava\u002Fpr2\u002Fmvvm-view-controller","04.java\u002Fpr2\u002F29.mvvm-view-controller",{"title":2520,"path":2521,"stem":2522},"Інтеграція MVVM з Guice: Автоматична ін'єкція залежностей","\u002Fjava\u002Fpr2\u002Fmvvm-guice-integration","04.java\u002Fpr2\u002F30.mvvm-guice-integration",{"title":2524,"path":2525,"stem":2526},"Валідація та обробка помилок у MVVM","\u002Fjava\u002Fpr2\u002Fmvvm-validation-error-handling","04.java\u002Fpr2\u002F31.mvvm-validation-error-handling",{"title":2528,"path":2529,"stem":2530},"Навігація та управління екранами у JavaFX MVVM","\u002Fjava\u002Fpr2\u002Fmvvm-navigation-screen-management","04.java\u002Fpr2\u002F32.mvvm-navigation-screen-management",{"title":2532,"path":2533,"stem":2534},"Тестування JavaFX MVVM-додатків","\u002Fjava\u002Fpr2\u002Fmvvm-testing","04.java\u002Fpr2\u002F33.mvvm-testing",{"title":2536,"path":2537,"stem":2538},"Стилізація та теми у JavaFX: CSS та User Experience","\u002Fjava\u002Fpr2\u002Fjavafx-styling-themes","04.java\u002Fpr2\u002F34.javafx-styling-themes",{"title":2540,"path":2541,"stem":2542},"AtlantaFX: Сучасні теми для JavaFX додатків","\u002Fjava\u002Fpr2\u002Fatlantafx-modern-themes","04.java\u002Fpr2\u002F35.atlantafx-modern-themes",{"title":2544,"path":2545,"stem":2546},"Пакування та розповсюдження JavaFX-додатків","\u002Fjava\u002Fpr2\u002Fjar-packaging-distribution","04.java\u002Fpr2\u002F36.jar-packaging-distribution",{"title":2548,"icon":2549,"path":2550,"stem":2551,"children":2552,"page":59},"Python","i-devicon-python","\u002Fpython","05.python",[2553,2557,2560,2564,2568,2572,2576,2580,2584,2588,2592,2596,2600,2604,2608,2669],{"title":2554,"path":2555,"stem":2556},"Модулі, Пакети та Віртуальні Середовища","\u002Fpython\u002Fmodules-packages-venv","05.python\u002F00.modules-packages-venv",{"title":71,"path":2558,"stem":2559},"\u002Fpython\u002Fclasses-objects","05.python\u002F01.classes-objects",{"title":2561,"path":2562,"stem":2563},"Інкапсуляція, Керування Доступом та Властивості","\u002Fpython\u002Fencapsulation","05.python\u002F02.encapsulation",{"title":2565,"path":2566,"stem":2567},"Наслідування, MRO та суперсила super()","\u002Fpython\u002Finheritance-mro","05.python\u002F03.inheritance-mro",{"title":2569,"path":2570,"stem":2571},"Абстракція — ABC проти Статичних Протоколів (PEP 544)","\u002Fpython\u002Fabstraction-protocols","05.python\u002F04.abstraction-protocols",{"title":2573,"path":2574,"stem":2575},"Магічні методи (Dunder) та Емуляція протоколів","\u002Fpython\u002Fdunder-methods","05.python\u002F05.dunder-methods",{"title":2577,"path":2578,"stem":2579},"Декоратори та Керування життєвим циклом методів","\u002Fpython\u002Fdecorators-static-class","05.python\u002F06.decorators-static-class",{"title":2581,"path":2582,"stem":2583},"Дескриптори — Магія доступу до атрибутів","\u002Fpython\u002Fdescriptors","05.python\u002F07.descriptors",{"title":2585,"path":2586,"stem":2587},"Метакласи — Динамічне створення класів під капотом CPython","\u002Fpython\u002Fmetaclasses","05.python\u002F08.metaclasses",{"title":2589,"path":2590,"stem":2591},"Dataclasses, NamedTuple та сучасні контейнери Python","\u002Fpython\u002Fmodern-containers","05.python\u002F09.modern-containers",{"title":2593,"path":2594,"stem":2595},"GIL та модель конкурентності CPython — фундамент перед потоками і процесами","\u002Fpython\u002Fgil-concurrency-intro","05.python\u002F11.gil-concurrency-intro",{"title":2597,"path":2598,"stem":2599},"Threading — конкурентність для I\u002FO-bound задач","\u002Fpython\u002Fthreading","05.python\u002F12.threading",{"title":2601,"path":2602,"stem":2603},"Multiprocessing — справжній паралелізм для CPU-bound задач","\u002Fpython\u002Fmultiprocessing","05.python\u002F13.multiprocessing",{"title":2605,"path":2606,"stem":2607},"asyncio — кооперативна конкурентність та event loop","\u002Fpython\u002Fasyncio","05.python\u002F14.asyncio",{"title":2609,"icon":92,"path":2610,"stem":2611,"children":2612,"page":59},"FastAPI","\u002Fpython\u002Ffastapi","05.python\u002Ffastapi",[2613,2617,2621,2625,2629,2633,2637,2641,2645,2649,2653,2657,2661,2665],{"title":2614,"path":2615,"stem":2616},"Глибокий Typing та Pydantic v2 — від анотацій до валідації","\u002Fpython\u002Ffastapi\u002Ftyping-pydantic","05.python\u002Ffastapi\u002F15.typing-pydantic",{"title":2618,"path":2619,"stem":2620},"WSGI, ASGI та Python Web-екосистема","\u002Fpython\u002Ffastapi\u002Fwsgi-asgi-ecosystem","05.python\u002Ffastapi\u002F16.wsgi-asgi-ecosystem",{"title":2622,"path":2623,"stem":2624},"FastAPI: Перший додаток, Uvicorn та OpenAPI","\u002Fpython\u002Ffastapi\u002Ffastapi-intro","05.python\u002Ffastapi\u002F17.fastapi-intro",{"title":2626,"path":2627,"stem":2628},"Маршрутизація, параметри запитів та APIRouter","\u002Fpython\u002Ffastapi\u002Ffastapi-routing-params","05.python\u002Ffastapi\u002F18.fastapi-routing-params",{"title":2630,"path":2631,"stem":2632},"Pydantic v2 у FastAPI — схеми, валідація та серіалізація","\u002Fpython\u002Ffastapi\u002Ffastapi-pydantic-schemas","05.python\u002Ffastapi\u002F19.fastapi-pydantic-schemas",{"title":2634,"path":2635,"stem":2636},"Dependency Injection — серце архітектури FastAPI","\u002Fpython\u002Ffastapi\u002Ffastapi-dependency-injection","05.python\u002Ffastapi\u002F20.fastapi-dependency-injection",{"title":2638,"path":2639,"stem":2640},"Обробка помилок, Middleware та CORS у FastAPI","\u002Fpython\u002Ffastapi\u002Ffastapi-errors-middleware","05.python\u002Ffastapi\u002F21.fastapi-errors-middleware",{"title":2642,"path":2643,"stem":2644},"SQLAlchemy 2.0 — ORM, Core та Async Engine","\u002Fpython\u002Ffastapi\u002Fsqlalchemy-orm","05.python\u002Ffastapi\u002F22.sqlalchemy-orm",{"title":2646,"path":2647,"stem":2648},"Alembic — міграції бази даних","\u002Fpython\u002Ffastapi\u002Falembic-migrations","05.python\u002Ffastapi\u002F23.alembic-migrations",{"title":2650,"path":2651,"stem":2652},"Repository Pattern та Clean Architecture у FastAPI","\u002Fpython\u002Ffastapi\u002Ffastapi-repository-clean-arch","05.python\u002Ffastapi\u002F24.fastapi-repository-clean-arch",{"title":2654,"path":2655,"stem":2656},"Автентифікація та авторизація — JWT, OAuth2, RBAC","\u002Fpython\u002Ffastapi\u002Ffastapi-auth-security","05.python\u002Ffastapi\u002F25.fastapi-auth-security",{"title":2658,"path":2659,"stem":2660},"Тестування FastAPI — pytest, httpx, fixtures","\u002Fpython\u002Ffastapi\u002Ffastapi-testing","05.python\u002Ffastapi\u002F26.fastapi-testing",{"title":2662,"path":2663,"stem":2664},"Async на повну — Background Tasks, WebSockets, SSE та Celery","\u002Fpython\u002Ffastapi\u002Ffastapi-async-advanced","05.python\u002Ffastapi\u002F27.fastapi-async-advanced",{"title":2666,"path":2667,"stem":2668},"Deployment — Docker, CI\u002FCD, Monitoring","\u002Fpython\u002Ffastapi\u002Ffastapi-deployment","05.python\u002Ffastapi\u002F28.fastapi-deployment",{"title":2670,"path":2671,"stem":2672},"[object Object]","\u002Fpython\u002Foop-plan","05.python\u002Foop-plan",{"title":2674,"icon":2675,"path":2676,"stem":2677,"children":2678,"page":59},"Бази даних","i-lucide-database","\u002Fdatabases","06.databases",[2679,2709,2732,2769,2798,2816,2850,2862,2871],{"title":2680,"icon":2681,"path":2682,"stem":2683,"children":2684,"page":59},"Intro","i-lucide-play","\u002Fdatabases\u002Fintro","06.databases\u002F01.intro",[2685,2689,2693,2697,2701,2705],{"title":2686,"path":2687,"stem":2688},"Введення в теорію баз даних","\u002Fdatabases\u002Fintro\u002Fintroduction-to-databases","06.databases\u002F01.intro\u002F01.introduction-to-databases",{"title":2690,"path":2691,"stem":2692},"Реляційна модель даних","\u002Fdatabases\u002Fintro\u002Frelational-model-theory","06.databases\u002F01.intro\u002F02.relational-model-theory",{"title":2694,"path":2695,"stem":2696},"ER-моделювання","\u002Fdatabases\u002Fintro\u002Fer-modeling","06.databases\u002F01.intro\u002F03.er-modeling",{"title":2698,"path":2699,"stem":2700},"Логічне проектування БД","\u002Fdatabases\u002Fintro\u002Flogical-schema","06.databases\u002F01.intro\u002F04.logical-schema",{"title":2702,"path":2703,"stem":2704},"Класифікація таблиць","\u002Fdatabases\u002Fintro\u002Ftable-classification","06.databases\u002F01.intro\u002F05.table-classification",{"title":2706,"path":2707,"stem":2708},"PlantUML для баз даних","\u002Fdatabases\u002Fintro\u002Fplantuml-diagrams","06.databases\u002F01.intro\u002F06.plantuml-diagrams",{"title":2710,"icon":2675,"path":2711,"stem":2712,"children":2713,"page":59},"MS SQL Server Start","\u002Fdatabases\u002Fms-sql-server-start","06.databases\u002F02.ms-sql-server-start",[2714,2718,2724,2728],{"title":2715,"path":2716,"stem":2717},"Типи даних у MS SQL Server","\u002Fdatabases\u002Fms-sql-server-start\u002Fdata-types","06.databases\u002F02.ms-sql-server-start\u002F01.data-types",{"title":2719,"path":2720,"stem":2721,"children":2722},"Індекси у MS SQL Server","\u002Fdatabases\u002Fms-sql-server-start\u002Fsql-indexes","06.databases\u002F02.ms-sql-server-start\u002F02.sql-indexes",[2723],{"title":2719,"path":2720,"stem":2721},{"title":2725,"path":2726,"stem":2727},"Системні бази даних MS SQL Server","\u002Fdatabases\u002Fms-sql-server-start\u002Fsystem-databases","06.databases\u002F02.ms-sql-server-start\u002F03.system-databases",{"title":2729,"path":2730,"stem":2731},"Огляд мови SQL та запитів","\u002Fdatabases\u002Fms-sql-server-start\u002Fsql-queries-overview","06.databases\u002F02.ms-sql-server-start\u002F04.sql-queries-overview",{"title":2733,"icon":2675,"path":2734,"stem":2735,"children":2736,"page":59},"SQL","\u002Fdatabases\u002Fsql","06.databases\u002F03.sql",[2737,2741,2745,2749,2753,2757,2761,2765],{"title":2738,"path":2739,"stem":2740},"Налаштування демонстраційної бази даних","\u002Fdatabases\u002Fsql\u002Fsample-database-setup","06.databases\u002F03.sql\u002F00.sample-database-setup",{"title":2742,"path":2743,"stem":2744},"DDL - Створення таблиць (CREATE TABLE)","\u002Fdatabases\u002Fsql\u002Fddl-create-table","06.databases\u002F03.sql\u002F01.ddl-create-table",{"title":2746,"path":2747,"stem":2748},"DDL - Зміна та видалення таблиць (ALTER, DROP)","\u002Fdatabases\u002Fsql\u002Fddl-alter-drop-table","06.databases\u002F03.sql\u002F02.ddl-alter-drop-table",{"title":2750,"path":2751,"stem":2752},"SELECT запити - Основи","\u002Fdatabases\u002Fsql\u002Fselect-queries-fundamentals","06.databases\u002F03.sql\u002F03.select-queries-fundamentals",{"title":2754,"path":2755,"stem":2756},"SELECT запити - Розширені можливості","\u002Fdatabases\u002Fsql\u002Fselect-queries-advanced","06.databases\u002F03.sql\u002F04.select-queries-advanced",{"title":2758,"path":2759,"stem":2760},"INSERT запити - Додавання даних","\u002Fdatabases\u002Fsql\u002Finsert-queries","06.databases\u002F03.sql\u002F05.insert-queries",{"title":2762,"path":2763,"stem":2764},"UPDATE та DELETE запити","\u002Fdatabases\u002Fsql\u002Fupdate-delete-queries","06.databases\u002F03.sql\u002F06.update-delete-queries",{"title":2766,"path":2767,"stem":2768},"Транзакції в SQL","\u002Fdatabases\u002Fsql\u002Ftransactions","06.databases\u002F03.sql\u002F07.transactions",{"title":2770,"icon":2675,"path":2771,"stem":2772,"children":2773,"page":59},"Multi Table Databases","\u002Fdatabases\u002Fmulti-table-databases","06.databases\u002F04.multi-table-databases",[2774,2778,2782,2786,2790,2794],{"title":2775,"path":2776,"stem":2777},"Зв'язки та нормалізація БД","\u002Fdatabases\u002Fmulti-table-databases\u002Frelationships-and-normalization","06.databases\u002F04.multi-table-databases\u002F00.relationships-and-normalization",{"title":2779,"path":2780,"stem":2781},"INNER JOIN - З'єднання таблиць","\u002Fdatabases\u002Fmulti-table-databases\u002Finner-join","06.databases\u002F04.multi-table-databases\u002F01.inner-join",{"title":2783,"path":2784,"stem":2785},"OUTER JOINs - LEFT, RIGHT, FULL","\u002Fdatabases\u002Fmulti-table-databases\u002Fouter-joins","06.databases\u002F04.multi-table-databases\u002F02.outer-joins",{"title":2787,"path":2788,"stem":2789},"CROSS та SELF JOINs","\u002Fdatabases\u002Fmulti-table-databases\u002Fcross-self-joins","06.databases\u002F04.multi-table-databases\u002F03.cross-self-joins",{"title":2791,"path":2792,"stem":2793},"Підзапити (Subqueries)","\u002Fdatabases\u002Fmulti-table-databases\u002Fsubqueries","06.databases\u002F04.multi-table-databases\u002F04.subqueries",{"title":2795,"path":2796,"stem":2797},"Агрегації з JOIN","\u002Fdatabases\u002Fmulti-table-databases\u002Faggregations-with-joins","06.databases\u002F04.multi-table-databases\u002F05.aggregations-with-joins",{"title":2799,"icon":2800,"path":2801,"stem":2802,"children":2803,"page":59},"Aggregate Functions","i-lucide-calculator","\u002Fdatabases\u002Faggregate-functions","06.databases\u002F05.aggregate-functions",[2804,2808,2812],{"title":2805,"path":2806,"stem":2807},"Функції агрегування в MS SQL Server","\u002Fdatabases\u002Faggregate-functions\u002Fintroduction-aggregate-functions","06.databases\u002F05.aggregate-functions\u002F01.introduction-aggregate-functions",{"title":2809,"path":2810,"stem":2811},"Групування даних в MS SQL Server","\u002Fdatabases\u002Faggregate-functions\u002Fgrouping-data","06.databases\u002F05.aggregate-functions\u002F02.grouping-data",{"title":2813,"path":2814,"stem":2815},"Підзапити з агрегатними функціями","\u002Fdatabases\u002Faggregate-functions\u002Fsubqueries-aggregates","06.databases\u002F05.aggregate-functions\u002F03.subqueries-aggregates",{"title":2817,"icon":2818,"path":2819,"stem":2820,"children":2821,"page":59},"Тригери та зберігаємі процедури","i-lucide-database-zap","\u002Fdatabases\u002Ftriggers-stored-procedures","06.databases\u002F07.triggers-stored-procedures",[2822,2826,2830,2834,2838,2842,2846],{"title":2823,"path":2824,"stem":2825},"DML-тригери","\u002Fdatabases\u002Ftriggers-stored-procedures\u002Fdml-triggers","06.databases\u002F07.triggers-stored-procedures\u002F01.dml-triggers",{"title":2827,"path":2828,"stem":2829},"DDL-тригери","\u002Fdatabases\u002Ftriggers-stored-procedures\u002Fddl-triggers","06.databases\u002F07.triggers-stored-procedures\u002F02.ddl-triggers",{"title":2831,"path":2832,"stem":2833},"Transact-SQL розширення","\u002Fdatabases\u002Ftriggers-stored-procedures\u002Ftransact-sql-extensions","06.databases\u002F07.triggers-stored-procedures\u002F03.transact-sql-extensions",{"title":2835,"path":2836,"stem":2837},"Транзакції","\u002Fdatabases\u002Ftriggers-stored-procedures\u002Ftransactions","06.databases\u002F07.triggers-stored-procedures\u002F04.transactions",{"title":2839,"path":2840,"stem":2841},"Зберігаємі процедури","\u002Fdatabases\u002Ftriggers-stored-procedures\u002Fstored-procedures","06.databases\u002F07.triggers-stored-procedures\u002F05.stored-procedures",{"title":2843,"path":2844,"stem":2845},"Користувацькі функції","\u002Fdatabases\u002Ftriggers-stored-procedures\u002Fuser-defined-functions","06.databases\u002F07.triggers-stored-procedures\u002F06.user-defined-functions",{"title":2847,"path":2848,"stem":2849},"Безпека баз даних","\u002Fdatabases\u002Ftriggers-stored-procedures\u002Fsecurity","06.databases\u002F07.triggers-stored-procedures\u002F08.security",{"title":2847,"icon":793,"path":2851,"stem":2852,"children":2853,"page":59},"\u002Fdatabases\u002Fsecurity","06.databases\u002F08.security",[2854,2858],{"title":2855,"path":2856,"stem":2857},"Вступ до безпеки баз даних","\u002Fdatabases\u002Fsecurity\u002Fintroduction","06.databases\u002F08.security\u002F01.introduction",{"title":2859,"path":2860,"stem":2861},"Системні представлення та метадані","\u002Fdatabases\u002Fsecurity\u002Fsystem-views","06.databases\u002F08.security\u002F02.system-views",{"title":2863,"icon":2864,"path":2865,"stem":2866,"children":2867,"page":59},"Резервне копіювання та відновлення","i-lucide-database-backup","\u002Fdatabases\u002Fbackup-recovery","06.databases\u002F09.backup-recovery",[2868],{"title":2863,"path":2869,"stem":2870},"\u002Fdatabases\u002Fbackup-recovery\u002Fbackup-restore","06.databases\u002F09.backup-recovery\u002F01.backup-restore",{"title":2872,"icon":2873,"path":2874,"stem":2875,"children":2876,"page":59},"Повнотекстовий пошук","i-lucide-search","\u002Fdatabases\u002Ffull-text-search","06.databases\u002F10.full-text-search",[2877],{"title":2872,"path":2878,"stem":2879},"\u002Fdatabases\u002Ffull-text-search\u002Ffull-text-search","06.databases\u002F10.full-text-search\u002F01.full-text-search",{"title":2881,"icon":2882,"path":2883,"stem":2884,"children":2885,"page":59},"Tools","i-lucide-wrench","\u002Ftools","07.tools",[2886,2962],{"title":2887,"icon":2888,"path":2889,"stem":2890,"children":2891},"Docker","i-simple-icons-docker","\u002Ftools\u002Fdocker","07.tools\u002F01.docker\u002Findex",[2892,2894,2898,2902,2906,2910,2914,2918,2922,2926,2930,2934,2938,2942,2946,2950,2954,2958],{"title":2893,"path":2889,"stem":2890},"Docker: від нуля до production",{"title":2895,"path":2896,"stem":2897},"Контейнеризація — від проблеми до рішення","\u002Ftools\u002Fdocker\u002Fcontainerization-concept","07.tools\u002F01.docker\u002F01.containerization-concept",{"title":2899,"path":2900,"stem":2901},"Docker — що це і навіщо?","\u002Ftools\u002Fdocker\u002Fdocker-what-and-why","07.tools\u002F01.docker\u002F02.docker-what-and-why",{"title":2903,"path":2904,"stem":2905},"Архітектура Docker Engine","\u002Ftools\u002Fdocker\u002Fdocker-architecture","07.tools\u002F01.docker\u002F03.docker-architecture",{"title":2907,"path":2908,"stem":2909},"Встановлення Docker","\u002Ftools\u002Fdocker\u002Finstallation","07.tools\u002F01.docker\u002F04.installation",{"title":2911,"path":2912,"stem":2913},"Перший контейнер — docker run","\u002Ftools\u002Fdocker\u002Ffirst-container","07.tools\u002F01.docker\u002F05.first-container",{"title":2915,"path":2916,"stem":2917},"Життєвий цикл контейнера","\u002Ftools\u002Fdocker\u002Fcontainer-lifecycle","07.tools\u002F01.docker\u002F06.container-lifecycle",{"title":2919,"path":2920,"stem":2921},"Docker Images — фундаментальні концепції","\u002Ftools\u002Fdocker\u002Fdocker-images-fundamentals","07.tools\u002F01.docker\u002F07.docker-images-fundamentals",{"title":2923,"path":2924,"stem":2925},"Dockerfile — основи","\u002Ftools\u002Fdocker\u002Fdockerfile-basics","07.tools\u002F01.docker\u002F08.dockerfile-basics",{"title":2927,"path":2928,"stem":2929},"Dockerfile — просунуті техніки","\u002Ftools\u002Fdocker\u002Fdockerfile-advanced","07.tools\u002F01.docker\u002F09.dockerfile-advanced",{"title":2931,"path":2932,"stem":2933},"Build Context та кешування шарів","\u002Ftools\u002Fdocker\u002Fbuild-context-and-cache","07.tools\u002F01.docker\u002F10.build-context-and-cache",{"title":2935,"path":2936,"stem":2937},"Реєстри Docker-образів","\u002Ftools\u002Fdocker\u002Fimage-registries","07.tools\u002F01.docker\u002F11.image-registries",{"title":2939,"path":2940,"stem":2941},"Контейнеризація .NET додатків","\u002Ftools\u002Fdocker\u002Fdotnet-containerization","07.tools\u002F01.docker\u002F12.dotnet-containerization",{"title":2943,"path":2944,"stem":2945},"Томи та збереження даних","\u002Ftools\u002Fdocker\u002Fvolumes-and-data","07.tools\u002F01.docker\u002F13.volumes-and-data",{"title":2947,"path":2948,"stem":2949},"Основи мережі в Docker","\u002Ftools\u002Fdocker\u002Fnetworking-basics","07.tools\u002F01.docker\u002F14.networking-basics",{"title":2951,"path":2952,"stem":2953},"Змінні оточення та конфігурація","\u002Ftools\u002Fdocker\u002Fenvironment-and-configuration","07.tools\u002F01.docker\u002F15.environment-and-configuration",{"title":2955,"path":2956,"stem":2957},"Docker Compose — оркестрація контейнерів","\u002Ftools\u002Fdocker\u002Fdocker-compose-basics","07.tools\u002F01.docker\u002F16.docker-compose-basics",{"title":2959,"path":2960,"stem":2961},"Docker Compose — Multi-Service застосунки","\u002Ftools\u002Fdocker\u002Fcompose-multi-service","07.tools\u002F01.docker\u002F17.compose-multi-service",{"title":2963,"icon":2964,"path":2965,"stem":2966,"children":2967},"Kubernetes","simple-icons:kubernetes","\u002Ftools\u002Fkubernetes","07.tools\u002F02.kubernetes\u002Findex",[2968,2970,2974,2978,2982,2986,2990,2994,2998],{"title":2969,"path":2965,"stem":2966},"Kubernetes: від розробки до production",{"title":2971,"path":2972,"stem":2973},"Kubernetes — коли Docker Compose більше не вистачає","\u002Ftools\u002Fkubernetes\u002Fwhy-kubernetes","07.tools\u002F02.kubernetes\u002F01.why-kubernetes",{"title":2975,"path":2976,"stem":2977},"Архітектура Kubernetes — анатомія кластера","\u002Ftools\u002Fkubernetes\u002Fkubernetes-architecture","07.tools\u002F02.kubernetes\u002F02.kubernetes-architecture",{"title":2979,"path":2980,"stem":2981},"Локальне середовище — minikube, kind та k3s","\u002Ftools\u002Fkubernetes\u002Flocal-environment","07.tools\u002F02.kubernetes\u002F03.local-environment",{"title":2983,"path":2984,"stem":2985},"Pod — атомарна одиниця Kubernetes","\u002Ftools\u002Fkubernetes\u002Fpods-and-containers","07.tools\u002F02.kubernetes\u002F04.pods-and-containers",{"title":2987,"path":2988,"stem":2989},"Патерни використання Pod","\u002Ftools\u002Fkubernetes\u002Fpod-patterns","07.tools\u002F02.kubernetes\u002F05.pod-patterns",{"title":2991,"path":2992,"stem":2993},"Deployment — декларативне управління Pod","\u002Ftools\u002Fkubernetes\u002Fdeployment-basics","07.tools\u002F02.kubernetes\u002F06.deployment-basics",{"title":2995,"path":2996,"stem":2997},"Rolling Updates та управління життєвим циклом Deployment","\u002Ftools\u002Fkubernetes\u002Fdeployment-rolling-updates","07.tools\u002F02.kubernetes\u002F07.deployment-rolling-updates",{"title":2999,"path":3000,"stem":3001},"Service — мережева абстракція для Pod","\u002Ftools\u002Fkubernetes\u002Fservices-networking","07.tools\u002F02.kubernetes\u002F08.services-networking",{"title":3003,"icon":3004,"path":3005,"stem":3006,"children":3007,"page":59},"Software Engineering","i-lucide-code-2","\u002Fsoftware-engineering","09.software-engineering",[3008,3012,3016,3020,3024,3028,3032,3036,3040,3044,3048],{"title":3009,"path":3010,"stem":3011},"1. Аналіз предметної області. Експертні знання та складність","\u002Fsoftware-engineering\u002Fintro-subdomains","09.software-engineering\u002F01.intro-subdomains",{"title":3013,"path":3014,"stem":3015},"2. Обмежені контексти. Інтеграція обмежених контекстів","\u002Fsoftware-engineering\u002Fintegrating-limited-contexts","09.software-engineering\u002F02.integrating-limited-contexts",{"title":3017,"path":3018,"stem":3019},"3. Реалізація простої бізнес-логіки","\u002Fsoftware-engineering\u002Fsimple","09.software-engineering\u002F03.simple",{"title":3021,"path":3022,"stem":3023},"4. Опрацювання складної бізнес-логіки","\u002Fsoftware-engineering\u002Fcomplex-business-logic","09.software-engineering\u002F04.complex-business-logic",{"title":3025,"path":3026,"stem":3027},"5. Моделювання фактора часу. Подієво-орієнтована архітектура.","\u002Fsoftware-engineering\u002Fmodelling-the-time-factor","09.software-engineering\u002F05.modelling-the-time-factor",{"title":3029,"path":3030,"stem":3031},"6. Архітектурні патерни","\u002Fsoftware-engineering\u002Farchitectural-patterns","09.software-engineering\u002F06.architectural-patterns",{"title":3033,"path":3034,"stem":3035},"Паттерни взаємодії","\u002Fsoftware-engineering\u002Fpatterns-of-interaction","09.software-engineering\u002F07.patterns-of-interaction",{"title":3037,"path":3038,"stem":3039},"Евристика проєктування","\u002Fsoftware-engineering\u002Fdesign-heuristics","09.software-engineering\u002F08.design-heuristics",{"title":3041,"path":3042,"stem":3043},"Еволюція проєктних рішень","\u002Fsoftware-engineering\u002Fevolution-of-design-solutions","09.software-engineering\u002F09.evolution-of-design-solutions",{"title":3045,"path":3046,"stem":3047},"EventStorming","\u002Fsoftware-engineering\u002Feventstorming","09.software-engineering\u002F10.eventstorming",{"title":3049,"path":3050,"stem":3051},"DDD на практиці","\u002Fsoftware-engineering\u002Fddd-in-practice","09.software-engineering\u002F11.ddd-in-practice",{"title":3053,"icon":943,"path":3054,"stem":3055,"children":3056,"page":59},"DDD","\u002Fddd","10.ddd",[3057,3061,3065,3069,3073,3077,3081,3085,3089,3093,3097,3101,3105],{"title":3058,"path":3059,"stem":3060},"Аналіз предметної області","\u002Fddd\u002Fdomain-analysis","10.ddd\u002F01.domain-analysis",{"title":3062,"path":3063,"stem":3064},"Експертні знання про предметну область","\u002Fddd\u002Fdomain-expert-knowledge","10.ddd\u002F02.domain-expert-knowledge",{"title":3066,"path":3067,"stem":3068},"Як осмислити складність предметної області","\u002Fddd\u002Fmanaging-domain-complexity","10.ddd\u002F03.managing-domain-complexity",{"title":3070,"path":3071,"stem":3072},"Інтеграція обмежених контекстів","\u002Fddd\u002Fbounded-context-integration","10.ddd\u002F04.bounded-context-integration",{"title":3074,"path":3075,"stem":3076},"Реалізація простої бізнес-логіки","\u002Fddd\u002Fsimple-business-logic","10.ddd\u002F05.simple-business-logic",{"title":3078,"path":3079,"stem":3080},"Обробка складної бізнес-логіки","\u002Fddd\u002Fcomplex-business-logic","10.ddd\u002F06.complex-business-logic",{"title":3082,"path":3083,"stem":3084},"Моделювання фактора часу","\u002Fddd\u002Ftime-modeling","10.ddd\u002F07.time-modeling",{"title":3086,"path":3087,"stem":3088},"Глава 8. Архітектурні Патерни","\u002Fddd\u002Farchitectural-patterns","10.ddd\u002F08.architectural-patterns",{"title":3090,"path":3091,"stem":3092},"Глава 9. Патерни Взаємодії","\u002Fddd\u002Finteraction-patterns","10.ddd\u002F09.interaction-patterns",{"title":3094,"path":3095,"stem":3096},"Глава 10. Проектні Евристики","\u002Fddd\u002Fdesign-heuristics","10.ddd\u002F10.design-heuristics",{"title":3098,"path":3099,"stem":3100},"Глава 11. Еволюція Проектних Рішень","\u002Fddd\u002Fevolution-of-design-decisions","10.ddd\u002F11.evolution-of-design-decisions",{"title":3102,"path":3103,"stem":3104},"Глава 12. EventStorming","\u002Fddd\u002Fevent-storming","10.ddd\u002F12.event-storming",{"title":3106,"path":3107,"stem":3108},"Глава 13. DDD на Практиці","\u002Fddd\u002Fddd-in-practice","10.ddd\u002F13.ddd-in-practice",{"title":3110,"icon":3111,"path":3112,"stem":3113,"children":3114,"page":59},"Media Streaming","i-lucide-video","\u002Fmedia-streaming","11.media-streaming",[3115,3119,3123,3127,3131,3135,3139],{"title":3116,"path":3117,"stem":3118},"01. Магія Стрімінгу: Що відбувається, коли ви натискаєте \"Play\"","\u002Fmedia-streaming\u002Fintroduction","11.media-streaming\u002F01.introduction",{"title":3120,"path":3121,"stem":3122},"02. Анатомія Медіа: Кодеки, Контейнери та Стиснення","\u002Fmedia-streaming\u002Faudio-video-anatomy","11.media-streaming\u002F02.audio-video-anatomy",{"title":3124,"path":3125,"stem":3126},"03. The Gym: FFmpeg Deep Dive","\u002Fmedia-streaming\u002Fffmpeg-gym","11.media-streaming\u002F03.ffmpeg-gym",{"title":3128,"path":3129,"stem":3130},"04. HLS Protocol: HTTP Live Streaming у Деталях","\u002Fmedia-streaming\u002Fhls-protocol","11.media-streaming\u002F04.hls-protocol",{"title":3132,"path":3133,"stem":3134},"05. DASH Protocol: Відкритий Стандарт","\u002Fmedia-streaming\u002Fdash-protocol","11.media-streaming\u002F05.dash-protocol",{"title":3136,"path":3137,"stem":3138},"06. Масштабування: CDN та Adaptive Bitrate","\u002Fmedia-streaming\u002Fcdn-and-adaptive-bitrate","11.media-streaming\u002F06.cdn-and-adaptive-bitrate",{"title":3140,"path":3141,"stem":3142},"07. Війна із Затримкою (Latency)","\u002Fmedia-streaming\u002Frealtime-latency","11.media-streaming\u002F07.realtime-latency",{"title":3144,"icon":3145,"path":3146,"stem":3147,"children":3148,"page":59},"HTML & CSS","i-devicon-html5","\u002Fhtml-css","12.html-css",[3149,3153,3157,3161,3165,3169,3173,3177,3181,3185,3189,3193,3197,3201,3205,3209,3213,3217,3221,3225,3229,3233,3237,3241,3245,3249,3253,3257,3261,3265],{"title":3150,"path":3151,"stem":3152},"Вступ до HTML. Структура документа","\u002Fhtml-css\u002Fintro-html-structure","12.html-css\u002F01.intro-html-structure",{"title":3154,"path":3155,"stem":3156},"Форматування тексту в HTML","\u002Fhtml-css\u002Fhtml-text-formatting","12.html-css\u002F02.html-text-formatting",{"title":3158,"path":3159,"stem":3160},"Посилання та зображення в HTML","\u002Fhtml-css\u002Fhtml-links-images","12.html-css\u002F03.html-links-images",{"title":3162,"path":3163,"stem":3164},"Списки та таблиці в HTML","\u002Fhtml-css\u002Fhtml-lists-tables","12.html-css\u002F04.html-lists-tables",{"title":3166,"path":3167,"stem":3168},"Форми в HTML","\u002Fhtml-css\u002Fhtml-forms","12.html-css\u002F05.html-forms",{"title":3170,"path":3171,"stem":3172},"Семантичні елементи HTML5","\u002Fhtml-css\u002Fhtml-semantic-elements","12.html-css\u002F06.html-semantic-elements",{"title":3174,"path":3175,"stem":3176},"Мультимедіа та розширені елементи HTML","\u002Fhtml-css\u002Fhtml-multimedia-advanced","12.html-css\u002F07.html-multimedia-advanced",{"title":3178,"path":3179,"stem":3180},"Мікророзмітка та SEO в HTML","\u002Fhtml-css\u002Fhtml-microdata-seo","12.html-css\u002F08.html-microdata-seo",{"title":3182,"path":3183,"stem":3184},"Вступ до CSS. Селектори та специфічність","\u002Fhtml-css\u002Fcss-intro-selectors","12.html-css\u002F09.css-intro-selectors",{"title":3186,"path":3187,"stem":3188},"Блокова модель CSS. Відступи. Box Sizing","\u002Fhtml-css\u002Fcss-box-model","12.html-css\u002F10.css-box-model",{"title":3190,"path":3191,"stem":3192},"Розміри у CSS: повний довідник одиниць і ключових слів","\u002Fhtml-css\u002F10a.css-sizing","12.html-css\u002F10a.css-sizing",{"title":3194,"path":3195,"stem":3196},"Типографіка в CSS. Шрифти та текст","\u002Fhtml-css\u002Fcss-typography","12.html-css\u002F11.css-typography",{"title":3198,"path":3199,"stem":3200},"Кольори та фони в CSS","\u002Fhtml-css\u002Fcss-colors-backgrounds","12.html-css\u002F12.css-colors-backgrounds",{"title":3202,"path":3203,"stem":3204},"Тіні та фільтри в CSS","\u002Fhtml-css\u002F12b.css-shadows-filters","12.html-css\u002F12b.css-shadows-filters",{"title":3206,"path":3207,"stem":3208},"CSS Flexbox: Фундамент гнучких макетів","\u002Fhtml-css\u002Fcss-flexbox-fundamentals","12.html-css\u002F13.css-flexbox-fundamentals",{"title":3210,"path":3211,"stem":3212},"CSS Flexbox: Вирівнювання та Позиціонування","\u002Fhtml-css\u002Fcss-flexbox-alignment-sizing-and-patterns","12.html-css\u002F14.css-flexbox-alignment-sizing-and-patterns",{"title":3214,"path":3215,"stem":3216},"CSS Grid. Двовимірний макет. Частина 1","\u002Fhtml-css\u002Fcss-layout-grid","12.html-css\u002F15.css-layout-grid",{"title":3218,"path":3219,"stem":3220},"CSS Grid. Двовимірний макет. Частина 2","\u002Fhtml-css\u002Fcss-layout-grid-advanced","12.html-css\u002F16.css-layout-grid-advanced",{"title":3222,"path":3223,"stem":3224},"Позиціонування в CSS. Z-index. Stacking Context","\u002Fhtml-css\u002Fcss-positioning","12.html-css\u002F17.css-positioning",{"title":3226,"path":3227,"stem":3228},"CSS Анімації та Переходи","\u002Fhtml-css\u002Fcss-animations-transitions","12.html-css\u002F18.css-animations-transitions",{"title":3230,"path":3231,"stem":3232},"Адаптивний дизайн. Media Queries. Частина 1","\u002Fhtml-css\u002Fcss-responsive-media-queries","12.html-css\u002F19.css-responsive-media-queries",{"title":3234,"path":3235,"stem":3236},"Адаптивний дизайн. Частина 2: clamp(), Container Queries, @layer","\u002Fhtml-css\u002Fcss-responsive-advanced","12.html-css\u002F20.css-responsive-advanced",{"title":3238,"path":3239,"stem":3240},"CSS Custom Properties. Методології. Сучасний CSS","\u002Fhtml-css\u002Fcss-variables-methodologies","12.html-css\u002F21.css-variables-methodologies",{"title":3242,"path":3243,"stem":3244},"Сучасний CSS 2023–2025: Нові можливості","\u002Fhtml-css\u002Fcss-modern-features","12.html-css\u002F22.css-modern-features",{"title":3246,"path":3247,"stem":3248},"CSS Nesting, @layer, @scope та @property: нативний препроцесор","\u002Fhtml-css\u002F22a.css-nesting-modern-syntax","12.html-css\u002F22a.css-nesting-modern-syntax",{"title":3250,"path":3251,"stem":3252},"CSS для форм та інтерактивних станів","\u002Fhtml-css\u002Fcss-forms-interactive-states","12.html-css\u002F23.css-forms-interactive-states",{"title":3254,"path":3255,"stem":3256},"Доступність у CSS (CSS Accessibility)","\u002Fhtml-css\u002Fcss-accessibility","12.html-css\u002F24.css-accessibility",{"title":3258,"path":3259,"stem":3260},"CSS-функції та сучасні sizing primitives","\u002Fhtml-css\u002Fcss-functions-sizing","12.html-css\u002F25.css-functions-sizing",{"title":3262,"path":3263,"stem":3264},"Rendering Pipeline і CSS Performance","\u002Fhtml-css\u002Fcss-rendering-performance","12.html-css\u002F26.css-rendering-performance",{"title":3266,"path":3267,"stem":3268},"CSS Best Practices: типові ситуації та правильні рішення","\u002Fhtml-css\u002Fcss-best-practices","12.html-css\u002F27.css-best-practices",{"title":3270,"path":3271,"stem":3272,"children":3273,"page":59},"AWS","\u002Faws","13.aws",[3274,3278,3282,3286,3290,3294,3298,3302,3306,3310,3314,3318,3322,3326,3330,3334,3338,3342],{"title":3275,"path":3276,"stem":3277},"Реєстрація AWS акаунту та студентські програми","\u002Faws\u002Faccount-registration","13.aws\u002F00.account-registration",{"title":3279,"path":3280,"stem":3281},"Вступ до хмарних обчислень та AWS","\u002Faws\u002Fintroduction-to-cloud","13.aws\u002F01.introduction-to-cloud",{"title":3283,"path":3284,"stem":3285},"AWS IAM — Identity and Access Management","\u002Faws\u002Fiam","13.aws\u002F02.iam",{"title":3287,"path":3288,"stem":3289},"AWS IAM CLI — Довідник команд","\u002Faws\u002F02a.iam-doc","13.aws\u002F02a.iam-doc",{"title":3291,"path":3292,"stem":3293},"Docker та контейнеризація в AWS — ECR, ECS та Fargate","\u002Faws\u002Fdocker-ecs","13.aws\u002F03.docker-ecs",{"title":3295,"path":3296,"stem":3297},"AWS ECR \u002F ECS CLI — Довідник команд","\u002Faws\u002F03a.docker-ecs-doc","13.aws\u002F03a.docker-ecs-doc",{"title":3299,"path":3300,"stem":3301},"Amazon EC2 — Elastic Compute Cloud","\u002Faws\u002Fec2","13.aws\u002F04.ec2",{"title":3303,"path":3304,"stem":3305},"AWS EC2 CLI — Довідник команд","\u002Faws\u002F04a.ec2-doc","13.aws\u002F04a.ec2-doc",{"title":3307,"path":3308,"stem":3309},"Elastic Load Balancing та Auto Scaling","\u002Faws\u002Falb-asg","13.aws\u002F05.alb-asg",{"title":3311,"path":3312,"stem":3313},"Amazon S3 — Simple Storage Service","\u002Faws\u002Fs3","13.aws\u002F06.s3",{"title":3315,"path":3316,"stem":3317},"Amazon CloudFront — Content Delivery Network","\u002Faws\u002Fcloudfront","13.aws\u002F07.cloudfront",{"title":3319,"path":3320,"stem":3321},"Amazon RDS — Relational Database Service","\u002Faws\u002Frds","13.aws\u002F08.rds",{"title":3323,"path":3324,"stem":3325},"Amazon DynamoDB — NoSQL Database","\u002Faws\u002Fdynamodb","13.aws\u002F09.dynamodb",{"title":3327,"path":3328,"stem":3329},"AWS Lambda та Serverless Compute","\u002Faws\u002Flambda","13.aws\u002F10.lambda",{"title":3331,"path":3332,"stem":3333},"Amazon Bedrock - Foundation Models, RAG та Agents","\u002Faws\u002Fbedrock","13.aws\u002F22.bedrock",{"title":3335,"path":3336,"stem":3337},"Amazon Rekognition - Комп'ютерний зір","\u002Faws\u002Frekognition","13.aws\u002F23.rekognition",{"title":3339,"path":3340,"stem":3341},"Amazon Textract - Інтелектуальний аналіз документів","\u002Faws\u002Ftextract","13.aws\u002F24.textract",{"title":3343,"path":3344,"stem":3345},"Amazon Polly, Transcribe, Comprehend та Translate","\u002Faws\u002Faudio-nlp-services","13.aws\u002F25.audio-nlp-services",{"title":3347,"path":3348,"stem":3349,"children":3350,"page":59},"Ai Development","\u002Fai-development","14.ai-development",[3351],{"title":3352,"path":3353,"stem":3354,"children":3355,"page":59},"Foundations","\u002Fai-development\u002Ffoundations","14.ai-development\u002F00.foundations",[3356],{"title":3357,"path":3358,"stem":3359},"Модуль 0.1. Що таке Large Language Model (LLM)","\u002Fai-development\u002Ffoundations\u002Fwhat-is-llm","14.ai-development\u002F00.foundations\u002F01.what-is-llm",{"title":3361,"path":3362,"stem":3363,"children":3364,"page":59},"React Native","\u002Freact-native","15.react-native",[3365,3386],{"title":3366,"path":3367,"stem":3368,"children":3369,"page":59},"Version1","\u002Freact-native\u002Fversion1","15.react-native\u002Fversion1",[3370,3374,3378,3382],{"title":3371,"path":3372,"stem":3373},"Що таке React Native","\u002Freact-native\u002Fversion1\u002Fwhat-is-react-native","15.react-native\u002Fversion1\u002F01.what-is-react-native",{"title":3375,"path":3376,"stem":3377},"Як влаштований React Native «під капотом»","\u002Freact-native\u002Fversion1\u002Freact-native-architecture","15.react-native\u002Fversion1\u002F02.react-native-architecture",{"title":3379,"path":3380,"stem":3381},"Від React до React Native: ключові відмінності","\u002Freact-native\u002Fversion1\u002Freact-native-vs-react","15.react-native\u002Fversion1\u002F03.react-native-vs-react",{"title":3383,"path":3384,"stem":3385},"View та Text — фундаментальні будівельні блоки","\u002Freact-native\u002Fversion1\u002Fview-and-text","15.react-native\u002Fversion1\u002F04.view-and-text",{"title":3387,"path":3388,"stem":3389,"children":3390,"page":59},"Version2","\u002Freact-native\u002Fversion2","15.react-native\u002Fversion2",[3391,3395,3399,3403,3407,3411,3415,3419,3423,3427,3431,3435,3439,3443,3447,3451,3455,3459,3463],{"title":3392,"path":3393,"stem":3394},"React Native — навіщо він потрібен","\u002Freact-native\u002Fversion2\u002Fwhy-react-native","15.react-native\u002Fversion2\u002F01.why-react-native",{"title":3396,"path":3397,"stem":3398},"Налаштування середовища розробки","\u002Freact-native\u002Fversion2\u002Fenvironment-setup","15.react-native\u002Fversion2\u002F02.environment-setup",{"title":3400,"path":3401,"stem":3402},"Expo і класичний React Native — два шляхи одного фреймворку","\u002Freact-native\u002Fversion2\u002Fexpo-vs-cli-landscape","15.react-native\u002Fversion2\u002F03.expo-vs-cli-landscape",{"title":3404,"path":3405,"stem":3406},"Середовище Expo — перший запуск","\u002Freact-native\u002Fversion2\u002Fexpo-setup-and-tooling","15.react-native\u002Fversion2\u002F04.expo-setup-and-tooling",{"title":3408,"path":3409,"stem":3410},"Структура проєкту та перші UI-примітиви","\u002Freact-native\u002Fversion2\u002Fproject-structure-and-conventions","15.react-native\u002Fversion2\u002F05.project-structure-and-conventions",{"title":3412,"path":3413,"stem":3414},"Базові компоненти React Native","\u002Freact-native\u002Fversion2\u002Fcore-components","15.react-native\u002Fversion2\u002F06.core-components",{"title":3416,"path":3417,"stem":3418},"Flexbox і layout у React Native","\u002Freact-native\u002Fversion2\u002Fflexbox-and-layout","15.react-native\u002Fversion2\u002F07.flexbox-and-layout",{"title":3420,"path":3421,"stem":3422},"StyleSheet і теми (light \u002F dark)","\u002Freact-native\u002Fversion2\u002Fstylesheet-and-theming","15.react-native\u002Fversion2\u002F08.stylesheet-and-theming",{"title":3424,"path":3425,"stem":3426},"Списки та віртуалізація","\u002Freact-native\u002Fversion2\u002Flists-and-virtualization","15.react-native\u002Fversion2\u002F09.lists-and-virtualization",{"title":3428,"path":3429,"stem":3430},"Форми, ввід і валідація","\u002Freact-native\u002Fversion2\u002Fforms-input-validation","15.react-native\u002Fversion2\u002F10.forms-input-validation",{"title":3432,"path":3433,"stem":3434},"Основи Expo Router","\u002Freact-native\u002Fversion2\u002Fexpo-router-basics","15.react-native\u002Fversion2\u002F11.expo-router-basics",{"title":3436,"path":3437,"stem":3438},"Вкладені навігатори та модалки","\u002Freact-native\u002Fversion2\u002Fnested-navigators-and-modals","15.react-native\u002Fversion2\u002F12.nested-navigators-and-modals",{"title":3440,"path":3441,"stem":3442},"Deep linking і параметри маршрутів","\u002Freact-native\u002Fversion2\u002Fdeep-linking-and-params","15.react-native\u002Fversion2\u002F13.deep-linking-and-params",{"title":3444,"path":3445,"stem":3446},"Робота з мережею та життєвий цикл застосунку","\u002Freact-native\u002Fversion2\u002Fnetworking-and-app-lifecycle","15.react-native\u002Fversion2\u002F14.networking-and-app-lifecycle",{"title":3448,"path":3449,"stem":3450},"Redux Toolkit на мобільних платформах","\u002Freact-native\u002Fversion2\u002Fredux-toolkit-on-mobile","15.react-native\u002Fversion2\u002F15.redux-toolkit-on-mobile",{"title":3452,"path":3453,"stem":3454},"RTK Query на mobile","\u002Freact-native\u002Fversion2\u002Frtk-query-mobile","15.react-native\u002Fversion2\u002F16.rtk-query-mobile",{"title":3456,"path":3457,"stem":3458},"Автентифікація та Безпечне Зберігання","\u002Freact-native\u002Fversion2\u002Fauth-secure-storage","15.react-native\u002Fversion2\u002F18.auth-secure-storage",{"title":3460,"path":3461,"stem":3462},"Локальне сховище — AsyncStorage, MMKV, SQLite","\u002Freact-native\u002Fversion2\u002Flocal-storage-mmkv-sqlite","15.react-native\u002Fversion2\u002F19.local-storage-mmkv-sqlite",{"title":3464,"path":3465,"stem":3466},"Офлайн-First і синхронізація","\u002Freact-native\u002Fversion2\u002Foffline-first-sync","15.react-native\u002Fversion2\u002F20.offline-first-sync",{"title":3468,"path":3469,"stem":3470,"children":3471,"page":59},"Ai Python","\u002Fai-python","16.ai-python",[3472,3476,3480],{"title":3473,"path":3474,"stem":3475},"Вступ до штучного інтелекту та машинного навчання","\u002Fai-python\u002Fintro-to-ai","16.ai-python\u002F01.intro-to-ai",{"title":3477,"path":3478,"stem":3479},"Знайомство з Jupyter Notebook та JupyterLab","\u002Fai-python\u002Fjupyter-notebook","16.ai-python\u002F02.jupyter-notebook",{"title":3481,"path":3482,"stem":3483},"Робота з даними з використанням NumPy і pandas","\u002Fai-python\u002Fnumpy-pandas","16.ai-python\u002F03.numpy-pandas",{"title":3485,"path":3486,"stem":3487,"children":3488,"page":59},"Tailwind","\u002Ftailwind","21.tailwind",[3489,3493,3497,3501,3505,3509,3513,3517,3521,3525,3529,3533],{"title":3490,"path":3491,"stem":3492},"Що таке Tailwind CSS і навіщо він потрібен","\u002Ftailwind\u002Ftailwind-intro-philosophy","21.tailwind\u002F01.tailwind-intro-philosophy",{"title":3494,"path":3495,"stem":3496},"Встановлення та налаштування Tailwind CSS v4","\u002Ftailwind\u002Ftailwind-installation-setup","21.tailwind\u002F02.tailwind-installation-setup",{"title":3498,"path":3499,"stem":3500},"Utility-класи: основи та система Tailwind","\u002Ftailwind\u002Ftailwind-utility-classes-core","21.tailwind\u002F03.tailwind-utility-classes-core",{"title":3502,"path":3503,"stem":3504},"Layout: Flexbox та Grid через Tailwind","\u002Ftailwind\u002Ftailwind-flexbox-grid","21.tailwind\u002F04.tailwind-flexbox-grid",{"title":3506,"path":3507,"stem":3508},"Кастомізація теми через @theme у Tailwind v4","\u002Ftailwind\u002Ftailwind-theme-customization","21.tailwind\u002F05.tailwind-theme-customization",{"title":3510,"path":3511,"stem":3512},"Варіанти: hover, focus, responsive, dark mode та нові v4","\u002Ftailwind\u002Ftailwind-variants-states","21.tailwind\u002F06.tailwind-variants-states",{"title":3514,"path":3515,"stem":3516},"Типографіка та система кольорів у Tailwind v4","\u002Ftailwind\u002Ftailwind-typography-colors","21.tailwind\u002F07.tailwind-typography-colors",{"title":3518,"path":3519,"stem":3520},"Компоненти та повторюваність: @apply, @utility та патерни","\u002Ftailwind\u002Ftailwind-components-patterns","21.tailwind\u002F08.tailwind-components-patterns",{"title":3522,"path":3523,"stem":3524},"Темна тема та система дизайн-токенів у Tailwind v4","\u002Ftailwind\u002Ftailwind-dark-mode-theming","21.tailwind\u002F09.tailwind-dark-mode-theming",{"title":3526,"path":3527,"stem":3528},"Довільні значення та контейнерні запити у Tailwind v4","\u002Ftailwind\u002Ftailwind-arbitrary-container-queries","21.tailwind\u002F10.tailwind-arbitrary-container-queries",{"title":3530,"path":3531,"stem":3532},"Анімації, трансформації та 3D у Tailwind v4","\u002Ftailwind\u002Ftailwind-animations-transforms","21.tailwind\u002F11.tailwind-animations-transforms",{"title":3534,"path":3535,"stem":3536},"Tailwind CLI, PostCSS та інтеграція з фреймворками","\u002Ftailwind\u002Ftailwind-cli-tooling","21.tailwind\u002F12.tailwind-cli-tooling",{"title":3538,"path":3539,"stem":3540},"Тестування компонентів діаграм","\u002Ftest-components","98.test-components",{"id":3542,"title":2658,"body":3543,"description":15205,"extension":15206,"links":15207,"meta":15208,"navigation":3637,"path":2659,"seo":15209,"stem":2660,"__hash__":15210},"docs\u002F05.python\u002Ffastapi\u002F26.fastapi-testing.md",{"type":3544,"value":3545,"toc":15137},"minimark",[3546,3550,3559,3567,3570,3575,3582,3788,3791,3821,3826,3829,4012,4014,4018,4033,4037,4046,4147,4151,4165,4383,4409,4411,4415,4418,4421,4800,4803,4826,4832,4838,4857,5210,5239,5245,5252,5264,5270,5462,5468,5480,5483,5753,6018,6020,6024,6038,6045,6057,6140,6144,6151,6154,6284,6288,6291,6297,6341,6347,6350,6374,6553,6557,6560,6566,6572,6580,6582,6586,6589,6593,6603,6609,6714,6724,6738,6742,6751,6771,6777,6975,7000,7004,7010,7088,7094,7179,7185,7264,7275,7300,7302,7306,7312,7319,7323,7326,7345,7351,7516,7519,7537,7541,7544,7899,7933,7935,7939,7948,7954,8092,8103,8106,8286,8289,8630,8632,8636,8639,8650,8657,8661,8675,8772,8779,8795,8799,8805,8927,8930,9077,9086,9279,9320,9322,9326,9336,9347,9359,9368,9372,9379,9609,9615,9679,9683,9686,9818,9821,9950,9952,9956,9967,9981,9985,9991,10115,10118,10138,10142,10145,10163,10174,10177,10200,10209,10211,10215,10222,10229,10254,10732,10734,10738,10745,10749,10756,10855,10857,10863,10869,11133,11135,11141,11147,11405,11407,11413,11423,11776,11778,11784,11787,12216,12218,12224,12227,12499,12501,12507,12513,13300,13302,13308,13318,13713,13715,13721,13724,13988,13993,14077,14079,14087,14090,14095,14429,14434,14928,14930,14934,14937,15067,15070,15072,15076,15079,15122,15133],[3547,3548,2658],"h1",{"id":3549},"тестування-fastapi-pytest-httpx-fixtures",[3551,3552,3553,3554,3558],"p",{},"Якісний програмний продукт відрізняється від «просто працюючого прототипу» однією важливою характеристикою — ",[3555,3556,3557],"strong",{},"передбачуваністю",". У світі корпоративної розробки ви не можете дозволити собі сподіватися, що чергова зміна в коді не зламає авторизацію чи логіку підрахунку кошика. Єдиний спосіб гарантувати стабільність системи при постійному рефакторингу та додаванні нових фіч — це автоматизоване тестування.",[3551,3560,3561,3562,3566],{},"Розробники, які приходять у Python-екосистему з платформи .NET, уже мають міцну базу знань про життєвий цикл тестів, мокування та інтеграційні тести. Однак інструментарій Python має свою специфіку. Цей матеріал покликаний не лише навчити вас писати тести для FastAPI з нуля, але й провести паралелі з концепціями .NET (xUnit, NUnit, ",[3563,3564,3565],"code",{},"WebApplicationFactory","), щоб полегшити перехід та сформувати глибоке розуміння процесів.",[3568,3569],"hr",{},[3571,3572,3574],"h2",{"id":3573},"_1-філософія-тестування-у-fastapi-та-піраміда-тестування","1. Філософія тестування у FastAPI та піраміда тестування",[3551,3576,3577,3578,3581],{},"Перш ніж писати перший рядок тестового коду, згадаймо класичну концепцію — ",[3555,3579,3580],{},"піраміду тестування",". Вона визначає ідеальне співвідношення різних типів тестів у проекті:",[3583,3584,3585],"plant-uml",{},[3586,3587,3592],"pre",{"className":3588,"code":3589,"language":3590,"meta":3591,"style":3591},"language-plantuml shiki shiki-themes light-plus dark-plus dark-plus","@startuml\nskinparam style plain\nskinparam backgroundColor #ffffff\nskinparam ArrowColor #3b82f6\nskinparam RectangleBackgroundColor #f8fafc\nskinparam RectangleBorderColor #cbd5e1\n\nrectangle \"E2E \u002F System Tests\\n(Повне тестування інтерфейсу та API)\" as e2e #fee2e2\nrectangle \"Integration \u002F API Tests\\n(Тестування ендпоінтів, БД, інтеграцій)\" as integration #ffedd5\nrectangle \"Unit Tests\\n(Чисті функції, Pydantic схеми, бізнес-логіка)\" as unit #e0f2fe\n\ne2e -[hidden]down-> integration\nintegration -[hidden]down-> unit\n\nnote right of e2e\n  Найдорожчі в підтримці,\n  повільні, запускають усю систему.\n  (Selenium, Playwright)\nend note\n\nnote right of integration\n  Перевіряють взаємодію компонентів,\n  роботу з БД та HTTP.\n  (FastAPI TestClient + pytest fixtures)\nend note\n\nnote right of unit\n  Найшвидші, ізольовані,\n  тестують логіку в пам'яті.\n  (pytest + Mock)\nend note\n\n@enduml\n","plantuml","",[3563,3593,3594,3602,3608,3614,3620,3626,3632,3639,3645,3651,3657,3662,3668,3674,3679,3685,3691,3697,3703,3709,3714,3720,3726,3732,3738,3743,3748,3754,3760,3766,3772,3777,3782],{"__ignoreMap":3591},[3595,3596,3599],"span",{"class":3597,"line":3598},"line",1,[3595,3600,3601],{},"@startuml\n",[3595,3603,3605],{"class":3597,"line":3604},2,[3595,3606,3607],{},"skinparam style plain\n",[3595,3609,3611],{"class":3597,"line":3610},3,[3595,3612,3613],{},"skinparam backgroundColor #ffffff\n",[3595,3615,3617],{"class":3597,"line":3616},4,[3595,3618,3619],{},"skinparam ArrowColor #3b82f6\n",[3595,3621,3623],{"class":3597,"line":3622},5,[3595,3624,3625],{},"skinparam RectangleBackgroundColor #f8fafc\n",[3595,3627,3629],{"class":3597,"line":3628},6,[3595,3630,3631],{},"skinparam RectangleBorderColor #cbd5e1\n",[3595,3633,3635],{"class":3597,"line":3634},7,[3595,3636,3638],{"emptyLinePlaceholder":3637},true,"\n",[3595,3640,3642],{"class":3597,"line":3641},8,[3595,3643,3644],{},"rectangle \"E2E \u002F System Tests\\n(Повне тестування інтерфейсу та API)\" as e2e #fee2e2\n",[3595,3646,3648],{"class":3597,"line":3647},9,[3595,3649,3650],{},"rectangle \"Integration \u002F API Tests\\n(Тестування ендпоінтів, БД, інтеграцій)\" as integration #ffedd5\n",[3595,3652,3654],{"class":3597,"line":3653},10,[3595,3655,3656],{},"rectangle \"Unit Tests\\n(Чисті функції, Pydantic схеми, бізнес-логіка)\" as unit #e0f2fe\n",[3595,3658,3660],{"class":3597,"line":3659},11,[3595,3661,3638],{"emptyLinePlaceholder":3637},[3595,3663,3665],{"class":3597,"line":3664},12,[3595,3666,3667],{},"e2e -[hidden]down-> integration\n",[3595,3669,3671],{"class":3597,"line":3670},13,[3595,3672,3673],{},"integration -[hidden]down-> unit\n",[3595,3675,3677],{"class":3597,"line":3676},14,[3595,3678,3638],{"emptyLinePlaceholder":3637},[3595,3680,3682],{"class":3597,"line":3681},15,[3595,3683,3684],{},"note right of e2e\n",[3595,3686,3688],{"class":3597,"line":3687},16,[3595,3689,3690],{},"  Найдорожчі в підтримці,\n",[3595,3692,3694],{"class":3597,"line":3693},17,[3595,3695,3696],{},"  повільні, запускають усю систему.\n",[3595,3698,3700],{"class":3597,"line":3699},18,[3595,3701,3702],{},"  (Selenium, Playwright)\n",[3595,3704,3706],{"class":3597,"line":3705},19,[3595,3707,3708],{},"end note\n",[3595,3710,3712],{"class":3597,"line":3711},20,[3595,3713,3638],{"emptyLinePlaceholder":3637},[3595,3715,3717],{"class":3597,"line":3716},21,[3595,3718,3719],{},"note right of integration\n",[3595,3721,3723],{"class":3597,"line":3722},22,[3595,3724,3725],{},"  Перевіряють взаємодію компонентів,\n",[3595,3727,3729],{"class":3597,"line":3728},23,[3595,3730,3731],{},"  роботу з БД та HTTP.\n",[3595,3733,3735],{"class":3597,"line":3734},24,[3595,3736,3737],{},"  (FastAPI TestClient + pytest fixtures)\n",[3595,3739,3741],{"class":3597,"line":3740},25,[3595,3742,3708],{},[3595,3744,3746],{"class":3597,"line":3745},26,[3595,3747,3638],{"emptyLinePlaceholder":3637},[3595,3749,3751],{"class":3597,"line":3750},27,[3595,3752,3753],{},"note right of unit\n",[3595,3755,3757],{"class":3597,"line":3756},28,[3595,3758,3759],{},"  Найшвидші, ізольовані,\n",[3595,3761,3763],{"class":3597,"line":3762},29,[3595,3764,3765],{},"  тестують логіку в пам'яті.\n",[3595,3767,3769],{"class":3597,"line":3768},30,[3595,3770,3771],{},"  (pytest + Mock)\n",[3595,3773,3775],{"class":3597,"line":3774},31,[3595,3776,3708],{},[3595,3778,3780],{"class":3597,"line":3779},32,[3595,3781,3638],{"emptyLinePlaceholder":3637},[3595,3783,3785],{"class":3597,"line":3784},33,[3595,3786,3787],{},"@enduml\n",[3551,3789,3790],{},"У контексті FastAPI-застосунків кожен рівень піраміди має своє практичне втілення:",[3792,3793,3794,3801,3815],"ol",{},[3795,3796,3797,3800],"li",{},[3555,3798,3799],{},"Unit-тести",": Тестують найменші ізольовані елементи коду. У FastAPI це зазвичай Pydantic-схеми (чи правильно валідуються дані), допоміжні чисті функції (хешування паролів, форматування дат) та окремі сервіси бізнес-логіки без доступу до зовнішніх ресурсів.",[3795,3802,3803,3806,3807,3810,3811,3814],{},[3555,3804,3805],{},"Integration (інтеграційні) тести",": Перевіряють, як компоненти системи працюють разом. Це серце тестування FastAPI. Тут ми використовуємо ",[3563,3808,3809],{},"TestClient"," (на базі ",[3563,3812,3813],{},"httpx",") для виконання віртуальних HTTP-запитів до наших ендпоінтів, використовуємо тестову базу даних PostgreSQL і перевіряємо middleware та обробку помилок.",[3795,3816,3817,3820],{},[3555,3818,3819],{},"E2E-тести",": Перевіряють систему від початку до кінця з точки зору кінцевого користувача, зазвичай імітуючи дії у веб-браузері.",[3822,3823,3825],"h3",{"id":3824},"порівняльний-аналіз-концепцій-c-python","Порівняльний аналіз концепцій: C# ↔ Python",[3551,3827,3828],{},"Для розробників із досвідом в ASP.NET Core інструментарій Python може здатися незвичним через динамічну природу мови та іншу філософію тестових фреймворків. Порівняймо ключові концепції:",[3830,3831,3832,3849],"table",{},[3833,3834,3835],"thead",{},[3836,3837,3838,3843,3846],"tr",{},[3839,3840,3842],"th",{"align":3841},"left","Концепція у .NET (ASP.NET Core)",[3839,3844,3845],{"align":3841},"Еквівалент у Python (FastAPI \u002F pytest)",[3839,3847,3848],{"align":3841},"Опис різниці",[3850,3851,3852,3870,3901,3930,3958,3977,3997],"tbody",{},[3836,3853,3854,3860,3865],{},[3855,3856,3857],"td",{"align":3841},[3555,3858,3859],{},"xUnit \u002F NUnit",[3855,3861,3862],{"align":3841},[3555,3863,3864],{},"pytest",[3855,3866,3867,3869],{"align":3841},[3563,3868,3864],{}," є функціональним фреймворком. Замість класів-фікстур та атрибутів використовуються прості функції та декоратори.",[3836,3871,3872,3883,3894],{},[3855,3873,3874],{"align":3841},[3555,3875,3876,3879,3880],{},[3563,3877,3878],{},"[Fact]"," \u002F ",[3563,3881,3882],{},"[Theory]",[3855,3884,3885],{"align":3841},[3555,3886,3887,3890,3891],{},[3563,3888,3889],{},"test_*"," функції \u002F ",[3563,3892,3893],{},"@pytest.mark.parametrize",[3855,3895,3896,3897,3900],{"align":3841},"В pytest будь-яка функція, яка починається з ",[3563,3898,3899],{},"test_",", є тестом. Параметризація робиться через декоратор.",[3836,3902,3903,3911,3920],{},[3855,3904,3905],{"align":3841},[3555,3906,3907,3910],{},[3563,3908,3909],{},"IClassFixture\u003CT>"," \u002F Setup-Teardown",[3855,3912,3913],{"align":3841},[3555,3914,3915,3916,3919],{},"pytest Fixtures (",[3563,3917,3918],{},"yield",")",[3855,3921,3922,3923,3926,3927,3929],{"align":3841},"Замість життєвого циклу класів та ",[3563,3924,3925],{},"IDisposable",", pytest використовує потужну систему DI-фікстур з ключовим словом ",[3563,3928,3918],{},".",[3836,3931,3932,3942,3955],{},[3855,3933,3934],{"align":3841},[3555,3935,3936,3938,3939],{},[3563,3937,3565],{}," + ",[3563,3940,3941],{},"HttpClient",[3855,3943,3944],{"align":3841},[3555,3945,3946,3948,3949,3952,3953,3919],{},[3563,3947,3809],{}," (або ",[3563,3950,3951],{},"AsyncClient"," з ",[3563,3954,3813],{},[3855,3956,3957],{"align":3841},"FastAPI надає вбудований клієнт, який запускає ASGI-застосунок в пам'яті без підняття реального мережевого порту.",[3836,3959,3960,3967,3974],{},[3855,3961,3962],{"align":3841},[3555,3963,3964],{},[3563,3965,3966],{},"ConfigureTestServices",[3855,3968,3969],{"align":3841},[3555,3970,3971],{},[3563,3972,3973],{},"app.dependency_overrides",[3855,3975,3976],{"align":3841},"Замість перевизначення сервісів у IoC-контейнері DI через перевизначення провайдерів, FastAPI дозволяє просто підмінити будь-яку залежність через словник перевизначень.",[3836,3978,3979,3984,3994],{},[3855,3980,3981],{"align":3841},[3555,3982,3983],{},"Moq \u002F NSubstitute",[3855,3985,3986],{"align":3841},[3555,3987,3988,3879,3991],{},[3563,3989,3990],{},"unittest.mock",[3563,3992,3993],{},"pytest-mock",[3855,3995,3996],{"align":3841},"Завдяки динамічній природі Python, мокування не потребує інтерфейсів. Будь-який об'єкт або метод можна \"запатчити\" під час виконання.",[3836,3998,3999,4004,4009],{},[3855,4000,4001],{"align":3841},[3555,4002,4003],{},"Bogus",[3855,4005,4006],{"align":3841},[3555,4007,4008],{},"Faker \u002F Factory Boy",[3855,4010,4011],{"align":3841},"Бібліотеки для генерації реалістичних тестових даних (імена, пошти, дати).",[3568,4013],{},[3571,4015,4017],{"id":4016},"_2-знайомство-з-pytest-та-налаштування-середовища","2. Знайомство з pytest та налаштування середовища",[3551,4019,4020,4022,4023,4026,4027,4029,4030,3929],{},[3555,4021,3864],{}," — це стандарт де-факто для тестування в Python. На відміну від стандартної бібліотеки ",[3563,4024,4025],{},"unittest"," (яка побудована на базі застарілої концепції JUnit з Java), ",[3563,4028,3864],{}," використовує максимально лаконічний, \"пітонічний\" підхід. Вам не потрібно створювати класи та успадковувати їх від базових тестових класів — достатньо написати звичайну функцію та використати стандартне ключове слово ",[3563,4031,4032],{},"assert",[3822,4034,4036],{"id":4035},"встановлення-залежностей","Встановлення залежностей",[3551,4038,4039,4040,4042,4043,4045],{},"Для організації повноцінного тестового середовища нам знадобиться сам фреймворк ",[3563,4041,3864],{},", клієнт ",[3563,4044,3813],{}," для запитів та утиліти для асинхронного тестування.",[4047,4048,4049,4087,4118],"tabs",{},[4050,4051,4053],"tabs-item",{"label":4052},"pip",[3586,4054,4058],{"className":4055,"code":4056,"language":4057,"meta":3591,"style":3591},"language-bash shiki shiki-themes light-plus dark-plus dark-plus","pip install pytest pytest-asyncio httpx pytest-cov faker factory-boy\n","bash",[3563,4059,4060],{"__ignoreMap":3591},[3595,4061,4062,4065,4069,4072,4075,4078,4081,4084],{"class":3597,"line":3598},[3595,4063,4052],{"class":4064},"s8Opu",[3595,4066,4068],{"class":4067},"sbdoH"," install",[3595,4070,4071],{"class":4067}," pytest",[3595,4073,4074],{"class":4067}," pytest-asyncio",[3595,4076,4077],{"class":4067}," httpx",[3595,4079,4080],{"class":4067}," pytest-cov",[3595,4082,4083],{"class":4067}," faker",[3595,4085,4086],{"class":4067}," factory-boy\n",[4050,4088,4090],{"label":4089},"uv",[3586,4091,4093],{"className":4055,"code":4092,"language":4057,"meta":3591,"style":3591},"uv add --dev pytest pytest-asyncio httpx pytest-cov faker factory-boy\n",[3563,4094,4095],{"__ignoreMap":3591},[3595,4096,4097,4099,4102,4106,4108,4110,4112,4114,4116],{"class":3597,"line":3598},[3595,4098,4089],{"class":4064},[3595,4100,4101],{"class":4067}," add",[3595,4103,4105],{"class":4104},"su1O8"," --dev",[3595,4107,4071],{"class":4067},[3595,4109,4074],{"class":4067},[3595,4111,4077],{"class":4067},[3595,4113,4080],{"class":4067},[3595,4115,4083],{"class":4067},[3595,4117,4086],{"class":4067},[4050,4119,4121],{"label":4120},"poetry",[3586,4122,4124],{"className":4055,"code":4123,"language":4057,"meta":3591,"style":3591},"poetry add -D pytest pytest-asyncio httpx pytest-cov faker factory-boy\n",[3563,4125,4126],{"__ignoreMap":3591},[3595,4127,4128,4130,4132,4135,4137,4139,4141,4143,4145],{"class":3597,"line":3598},[3595,4129,4120],{"class":4064},[3595,4131,4101],{"class":4067},[3595,4133,4134],{"class":4104}," -D",[3595,4136,4071],{"class":4067},[3595,4138,4074],{"class":4067},[3595,4140,4077],{"class":4067},[3595,4142,4080],{"class":4067},[3595,4144,4083],{"class":4067},[3595,4146,4086],{"class":4067},[3822,4148,4150],{"id":4149},"конфігурація-pytest","Конфігурація pytest",[3551,4152,4153,4154,4157,4158,4161,4162,4164],{},"У Python-проектах конфігурація інструментів тестування зазвичай розміщується у файлі ",[3563,4155,4156],{},"pyproject.toml"," (сучасний стандарт) або ",[3563,4159,4160],{},"pytest.ini",". Додамо базові налаштування в ",[3563,4163,4156],{}," вашого проекту, щоб pytest знав, де шукати тести та як обробляти асинхронні виклики:",[3586,4166,4170],{"className":4167,"code":4168,"language":4169,"meta":3591,"style":3591},"language-toml shiki shiki-themes light-plus dark-plus dark-plus","[tool.pytest.ini_options]\n# Визначає мінімальну версію pytest\nminversion = \"7.0\"\n\n# Шляхи, де pytest шукатиме тести за замовчуванням\ntestpaths = [\"tests\"]\n\n# Шаблони для пошуку тестових файлів, класів та функцій\npython_files = \"test_*.py\"\npython_classes = \"Test*\"\npython_functions = \"test_*\"\n\n# Додає поточну директорію до шляхів пошуку модулів Python\npythonpath = [\".\"]\n\n# Налаштування виводу та поведінки\n# -v: verbose (детальний вивід)\n# -ra: показати підсумки для всіх тестів, окрім успішних (passed)\n# --strict-markers: заборонити використання незареєстрованих маркерів\naddopts = \"-v -ra --strict-markers\"\n\n# Автоматичне виявлення асинхронних тестів без потреби писати @pytest.mark.asyncio над кожним\nasyncio_mode = \"auto\"\nasyncio_default_fixture_loop_scope = \"function\"\n\n# Реєстрація користувацьких маркерів\nmarkers = [\n    \"unit: швидкі unit-тести\",\n    \"integration: інтеграційні тести, що потребують БД або зовнішніх викликів\",\n    \"auth: тести, пов'язані з автентифікацією та безпекою\"\n]\n","toml",[3563,4171,4172,4178,4184,4196,4200,4205,4219,4223,4228,4238,4248,4258,4262,4267,4279,4283,4288,4293,4298,4303,4313,4317,4322,4332,4342,4346,4351,4359,4367,4374,4379],{"__ignoreMap":3591},[3595,4173,4174],{"class":3597,"line":3598},[3595,4175,4177],{"class":4176},"sHH4Y","[tool.pytest.ini_options]\n",[3595,4179,4180],{"class":3597,"line":3604},[3595,4181,4183],{"class":4182},"spJ8K","# Визначає мінімальну версію pytest\n",[3595,4185,4186,4190,4193],{"class":3597,"line":3610},[3595,4187,4189],{"class":4188},"siwwj","minversion",[3595,4191,4192],{"class":4176}," = ",[3595,4194,4195],{"class":4067},"\"7.0\"\n",[3595,4197,4198],{"class":3597,"line":3616},[3595,4199,3638],{"emptyLinePlaceholder":3637},[3595,4201,4202],{"class":3597,"line":3622},[3595,4203,4204],{"class":4182},"# Шляхи, де pytest шукатиме тести за замовчуванням\n",[3595,4206,4207,4210,4213,4216],{"class":3597,"line":3628},[3595,4208,4209],{"class":4188},"testpaths",[3595,4211,4212],{"class":4176}," = [",[3595,4214,4215],{"class":4067},"\"tests\"",[3595,4217,4218],{"class":4176},"]\n",[3595,4220,4221],{"class":3597,"line":3634},[3595,4222,3638],{"emptyLinePlaceholder":3637},[3595,4224,4225],{"class":3597,"line":3641},[3595,4226,4227],{"class":4182},"# Шаблони для пошуку тестових файлів, класів та функцій\n",[3595,4229,4230,4233,4235],{"class":3597,"line":3647},[3595,4231,4232],{"class":4188},"python_files",[3595,4234,4192],{"class":4176},[3595,4236,4237],{"class":4067},"\"test_*.py\"\n",[3595,4239,4240,4243,4245],{"class":3597,"line":3653},[3595,4241,4242],{"class":4188},"python_classes",[3595,4244,4192],{"class":4176},[3595,4246,4247],{"class":4067},"\"Test*\"\n",[3595,4249,4250,4253,4255],{"class":3597,"line":3659},[3595,4251,4252],{"class":4188},"python_functions",[3595,4254,4192],{"class":4176},[3595,4256,4257],{"class":4067},"\"test_*\"\n",[3595,4259,4260],{"class":3597,"line":3664},[3595,4261,3638],{"emptyLinePlaceholder":3637},[3595,4263,4264],{"class":3597,"line":3670},[3595,4265,4266],{"class":4182},"# Додає поточну директорію до шляхів пошуку модулів Python\n",[3595,4268,4269,4272,4274,4277],{"class":3597,"line":3676},[3595,4270,4271],{"class":4188},"pythonpath",[3595,4273,4212],{"class":4176},[3595,4275,4276],{"class":4067},"\".\"",[3595,4278,4218],{"class":4176},[3595,4280,4281],{"class":3597,"line":3681},[3595,4282,3638],{"emptyLinePlaceholder":3637},[3595,4284,4285],{"class":3597,"line":3687},[3595,4286,4287],{"class":4182},"# Налаштування виводу та поведінки\n",[3595,4289,4290],{"class":3597,"line":3693},[3595,4291,4292],{"class":4182},"# -v: verbose (детальний вивід)\n",[3595,4294,4295],{"class":3597,"line":3699},[3595,4296,4297],{"class":4182},"# -ra: показати підсумки для всіх тестів, окрім успішних (passed)\n",[3595,4299,4300],{"class":3597,"line":3705},[3595,4301,4302],{"class":4182},"# --strict-markers: заборонити використання незареєстрованих маркерів\n",[3595,4304,4305,4308,4310],{"class":3597,"line":3711},[3595,4306,4307],{"class":4188},"addopts",[3595,4309,4192],{"class":4176},[3595,4311,4312],{"class":4067},"\"-v -ra --strict-markers\"\n",[3595,4314,4315],{"class":3597,"line":3716},[3595,4316,3638],{"emptyLinePlaceholder":3637},[3595,4318,4319],{"class":3597,"line":3722},[3595,4320,4321],{"class":4182},"# Автоматичне виявлення асинхронних тестів без потреби писати @pytest.mark.asyncio над кожним\n",[3595,4323,4324,4327,4329],{"class":3597,"line":3728},[3595,4325,4326],{"class":4188},"asyncio_mode",[3595,4328,4192],{"class":4176},[3595,4330,4331],{"class":4067},"\"auto\"\n",[3595,4333,4334,4337,4339],{"class":3597,"line":3734},[3595,4335,4336],{"class":4188},"asyncio_default_fixture_loop_scope",[3595,4338,4192],{"class":4176},[3595,4340,4341],{"class":4067},"\"function\"\n",[3595,4343,4344],{"class":3597,"line":3740},[3595,4345,3638],{"emptyLinePlaceholder":3637},[3595,4347,4348],{"class":3597,"line":3745},[3595,4349,4350],{"class":4182},"# Реєстрація користувацьких маркерів\n",[3595,4352,4353,4356],{"class":3597,"line":3750},[3595,4354,4355],{"class":4188},"markers",[3595,4357,4358],{"class":4176}," = [\n",[3595,4360,4361,4364],{"class":3597,"line":3756},[3595,4362,4363],{"class":4067},"    \"unit: швидкі unit-тести\"",[3595,4365,4366],{"class":4176},",\n",[3595,4368,4369,4372],{"class":3597,"line":3762},[3595,4370,4371],{"class":4067},"    \"integration: інтеграційні тести, що потребують БД або зовнішніх викликів\"",[3595,4373,4366],{"class":4176},[3595,4375,4376],{"class":3597,"line":3768},[3595,4377,4378],{"class":4067},"    \"auth: тести, пов'язані з автентифікацією та безпекою\"\n",[3595,4380,4381],{"class":3597,"line":3774},[3595,4382,4218],{"class":4176},[4384,4385,4386,4391],"note",{},[3551,4387,4388],{},[3555,4389,4390],{},"Чому важлива реєстрація маркерів?",[3551,4392,4393,4394,4397,4398,4400,4401,4404,4405,4408],{},"Параметр ",[3563,4395,4396],{},"--strict-markers"," разом із секцією ",[3563,4399,4355],{}," запобігає друкарським помилкам. Якщо ви випадково напишете ",[3563,4402,4403],{},"@pytest.mark.unitt"," замість ",[3563,4406,4407],{},"@pytest.mark.unit",", pytest видасть помилку під час запуску, а не просто проігнорує цей маркер.",[3568,4410],{},[3571,4412,4414],{"id":4413},"_3-написання-перших-unit-тестів-валідація-pydantic-схем","3. Написання перших Unit-тестів: Валідація Pydantic-схем",[3551,4416,4417],{},"Почнемо з найнижчого рівня піраміди — модульних (unit) тестів. Pydantic-схем у FastAPI виконують роль DTO (Data Transfer Objects) та контрактів API. Тестування схем дозволяє переконатися, що бізнес-правила валідації працюють коректно ще до того, як дані потраплять у базу даних чи оброблятимуться ендпоінтом.",[3551,4419,4420],{},"Уявімо, що у нас є схема для створення завдання (Task) у нашому додатку TaskForge:",[3586,4422,4427],{"className":4423,"code":4424,"filename":4425,"language":4426,"meta":3591,"style":3591},"language-python shiki shiki-themes light-plus dark-plus dark-plus","from datetime import datetime, date\nfrom typing import Optional\nfrom pydantic import BaseModel, Field, field_validator, model_validator\n\nclass TaskCreate(BaseModel):\n    title: str = Field(..., min_length=3, max_length=100, description=\"Назва завдання\")\n    description: Optional[str] = Field(None, max_length=1000)\n    due_date: Optional[date] = Field(None, description=\"Дедлайн виконання\")\n    priority: int = Field(default=1, ge=1, le=5, description=\"Пріоритет від 1 до 5\")\n\n    @field_validator(\"due_date\")\n    @classmethod\n    def prevent_past_dates(cls, value: Optional[date]) -> Optional[date]:\n        if value and value \u003C date.today():\n            raise ValueError(\"Дедлайн не може бути в минулому\")\n        return value\n\n    @model_validator(mode=\"after\")\n    def validate_title_description(self) -> \"TaskCreate\":\n        if self.title.lower() in (self.description or \"\").lower():\n            raise ValueError(\"Опис завдання не повинен повністю містити назву завдання\")\n        return self\n","app\u002Fschemas\u002Ftask.py","python",[3563,4428,4429,4444,4456,4468,4472,4490,4535,4559,4577,4626,4630,4642,4650,4671,4685,4700,4708,4712,4729,4750,4780,4793],{"__ignoreMap":3591},[3595,4430,4431,4435,4438,4441],{"class":3597,"line":3598},[3595,4432,4434],{"class":4433},"s8xlr","from",[3595,4436,4437],{"class":4176}," datetime ",[3595,4439,4440],{"class":4433},"import",[3595,4442,4443],{"class":4176}," datetime, date\n",[3595,4445,4446,4448,4451,4453],{"class":3597,"line":3604},[3595,4447,4434],{"class":4433},[3595,4449,4450],{"class":4176}," typing ",[3595,4452,4440],{"class":4433},[3595,4454,4455],{"class":4176}," Optional\n",[3595,4457,4458,4460,4463,4465],{"class":3597,"line":3610},[3595,4459,4434],{"class":4433},[3595,4461,4462],{"class":4176}," pydantic ",[3595,4464,4440],{"class":4433},[3595,4466,4467],{"class":4176}," BaseModel, Field, field_validator, model_validator\n",[3595,4469,4470],{"class":3597,"line":3616},[3595,4471,3638],{"emptyLinePlaceholder":3637},[3595,4473,4474,4477,4481,4484,4487],{"class":3597,"line":3622},[3595,4475,4476],{"class":4104},"class",[3595,4478,4480],{"class":4479},"sN1BT"," TaskCreate",[3595,4482,4483],{"class":4176},"(",[3595,4485,4486],{"class":4479},"BaseModel",[3595,4488,4489],{"class":4176},"):\n",[3595,4491,4492,4495,4498,4501,4504,4507,4511,4514,4517,4519,4522,4524,4527,4529,4532],{"class":3597,"line":3628},[3595,4493,4494],{"class":4176},"    title: ",[3595,4496,4497],{"class":4479},"str",[3595,4499,4500],{"class":4176}," = Field(..., ",[3595,4502,4503],{"class":4188},"min_length",[3595,4505,4506],{"class":4176},"=",[3595,4508,4510],{"class":4509},"sJj4R","3",[3595,4512,4513],{"class":4176},", ",[3595,4515,4516],{"class":4188},"max_length",[3595,4518,4506],{"class":4176},[3595,4520,4521],{"class":4509},"100",[3595,4523,4513],{"class":4176},[3595,4525,4526],{"class":4188},"description",[3595,4528,4506],{"class":4176},[3595,4530,4531],{"class":4067},"\"Назва завдання\"",[3595,4533,4534],{"class":4176},")\n",[3595,4536,4537,4540,4542,4545,4548,4550,4552,4554,4557],{"class":3597,"line":3634},[3595,4538,4539],{"class":4176},"    description: Optional[",[3595,4541,4497],{"class":4479},[3595,4543,4544],{"class":4176},"] = Field(",[3595,4546,4547],{"class":4104},"None",[3595,4549,4513],{"class":4176},[3595,4551,4516],{"class":4188},[3595,4553,4506],{"class":4176},[3595,4555,4556],{"class":4509},"1000",[3595,4558,4534],{"class":4176},[3595,4560,4561,4564,4566,4568,4570,4572,4575],{"class":3597,"line":3641},[3595,4562,4563],{"class":4176},"    due_date: Optional[date] = Field(",[3595,4565,4547],{"class":4104},[3595,4567,4513],{"class":4176},[3595,4569,4526],{"class":4188},[3595,4571,4506],{"class":4176},[3595,4573,4574],{"class":4067},"\"Дедлайн виконання\"",[3595,4576,4534],{"class":4176},[3595,4578,4579,4582,4585,4588,4591,4593,4596,4598,4601,4603,4605,4607,4610,4612,4615,4617,4619,4621,4624],{"class":3597,"line":3647},[3595,4580,4581],{"class":4176},"    priority: ",[3595,4583,4584],{"class":4479},"int",[3595,4586,4587],{"class":4176}," = Field(",[3595,4589,4590],{"class":4188},"default",[3595,4592,4506],{"class":4176},[3595,4594,4595],{"class":4509},"1",[3595,4597,4513],{"class":4176},[3595,4599,4600],{"class":4188},"ge",[3595,4602,4506],{"class":4176},[3595,4604,4595],{"class":4509},[3595,4606,4513],{"class":4176},[3595,4608,4609],{"class":4188},"le",[3595,4611,4506],{"class":4176},[3595,4613,4614],{"class":4509},"5",[3595,4616,4513],{"class":4176},[3595,4618,4526],{"class":4188},[3595,4620,4506],{"class":4176},[3595,4622,4623],{"class":4067},"\"Пріоритет від 1 до 5\"",[3595,4625,4534],{"class":4176},[3595,4627,4628],{"class":3597,"line":3653},[3595,4629,3638],{"emptyLinePlaceholder":3637},[3595,4631,4632,4635,4637,4640],{"class":3597,"line":3659},[3595,4633,4634],{"class":4064},"    @field_validator",[3595,4636,4483],{"class":4176},[3595,4638,4639],{"class":4067},"\"due_date\"",[3595,4641,4534],{"class":4176},[3595,4643,4644,4647],{"class":3597,"line":3664},[3595,4645,4646],{"class":4064},"    @",[3595,4648,4649],{"class":4479},"classmethod\n",[3595,4651,4652,4655,4658,4660,4663,4665,4668],{"class":3597,"line":3670},[3595,4653,4654],{"class":4104},"    def",[3595,4656,4657],{"class":4064}," prevent_past_dates",[3595,4659,4483],{"class":4176},[3595,4661,4662],{"class":4188},"cls",[3595,4664,4513],{"class":4176},[3595,4666,4667],{"class":4188},"value",[3595,4669,4670],{"class":4176},": Optional[date]) -> Optional[date]:\n",[3595,4672,4673,4676,4679,4682],{"class":3597,"line":3676},[3595,4674,4675],{"class":4433},"        if",[3595,4677,4678],{"class":4176}," value ",[3595,4680,4681],{"class":4104},"and",[3595,4683,4684],{"class":4176}," value \u003C date.today():\n",[3595,4686,4687,4690,4693,4695,4698],{"class":3597,"line":3681},[3595,4688,4689],{"class":4433},"            raise",[3595,4691,4692],{"class":4479}," ValueError",[3595,4694,4483],{"class":4176},[3595,4696,4697],{"class":4067},"\"Дедлайн не може бути в минулому\"",[3595,4699,4534],{"class":4176},[3595,4701,4702,4705],{"class":3597,"line":3687},[3595,4703,4704],{"class":4433},"        return",[3595,4706,4707],{"class":4176}," value\n",[3595,4709,4710],{"class":3597,"line":3693},[3595,4711,3638],{"emptyLinePlaceholder":3637},[3595,4713,4714,4717,4719,4722,4724,4727],{"class":3597,"line":3699},[3595,4715,4716],{"class":4064},"    @model_validator",[3595,4718,4483],{"class":4176},[3595,4720,4721],{"class":4188},"mode",[3595,4723,4506],{"class":4176},[3595,4725,4726],{"class":4067},"\"after\"",[3595,4728,4534],{"class":4176},[3595,4730,4731,4733,4736,4738,4741,4744,4747],{"class":3597,"line":3705},[3595,4732,4654],{"class":4104},[3595,4734,4735],{"class":4064}," validate_title_description",[3595,4737,4483],{"class":4176},[3595,4739,4740],{"class":4188},"self",[3595,4742,4743],{"class":4176},") -> ",[3595,4745,4746],{"class":4067},"\"TaskCreate\"",[3595,4748,4749],{"class":4176},":\n",[3595,4751,4752,4754,4757,4760,4763,4766,4768,4771,4774,4777],{"class":3597,"line":3711},[3595,4753,4675],{"class":4433},[3595,4755,4756],{"class":4104}," self",[3595,4758,4759],{"class":4176},".title.lower() ",[3595,4761,4762],{"class":4104},"in",[3595,4764,4765],{"class":4176}," (",[3595,4767,4740],{"class":4104},[3595,4769,4770],{"class":4176},".description ",[3595,4772,4773],{"class":4104},"or",[3595,4775,4776],{"class":4067}," \"\"",[3595,4778,4779],{"class":4176},").lower():\n",[3595,4781,4782,4784,4786,4788,4791],{"class":3597,"line":3716},[3595,4783,4689],{"class":4433},[3595,4785,4692],{"class":4479},[3595,4787,4483],{"class":4176},[3595,4789,4790],{"class":4067},"\"Опис завдання не повинен повністю містити назву завдання\"",[3595,4792,4534],{"class":4176},[3595,4794,4795,4797],{"class":3597,"line":3722},[3595,4796,4704],{"class":4433},[3595,4798,4799],{"class":4104}," self\n",[3551,4801,4802],{},"Ця схема має кілька правил валідації:",[3792,4804,4805,4811,4817,4823],{},[3795,4806,4807,4810],{},[3563,4808,4809],{},"title"," має бути від 3 до 100 символів.",[3795,4812,4813,4816],{},[3563,4814,4815],{},"priority"," обмежений діапазоном від 1 до 5 включно.",[3795,4818,4819,4822],{},[3563,4820,4821],{},"due_date"," не може бути в минулому (custom field validator).",[3795,4824,4825],{},"Опис не повинен містити в собі назву (custom model validator).",[3551,4827,4828,4829,3929],{},"Створимо файл для тестів валідації: ",[3563,4830,4831],{},"tests\u002Funit\u002Ftest_task_schemas.py",[3822,4833,4835,4836],{"id":4834},"написання-тестів-за-допомогою-стандартного-assert","Написання тестів за допомогою стандартного ",[3563,4837,4032],{},[3551,4839,4840,4841,4844,4845,4848,4849,4852,4853,4856],{},"На відміну від C# xUnit, де ми пишемо ",[3563,4842,4843],{},"Assert.Equal(expected, actual)"," або ",[3563,4846,4847],{},"Assert.Throws\u003CValidationException>(...)",", у pytest ми пишемо стандартні вирази Python ",[3563,4850,4851],{},"assert =="," та контекстні менеджери ",[3563,4854,4855],{},"with pytest.raises(ValidationError)",":",[3586,4858,4860],{"className":4423,"code":4859,"filename":4831,"language":4426,"meta":3591,"style":3591},"from datetime import date, timedelta\nimport pytest\nfrom pydantic import ValidationError\nfrom app.schemas.task import TaskCreate\n\n@pytest.mark.unit\ndef test_task_create_valid_data():\n    \"\"\"Тест успішної валідації з коректними даними.\"\"\"\n    valid_data = {\n        \"title\": \"Learn FastAPI testing\",\n        \"description\": \"Read article about pytest, fixtures and write unit tests.\",\n        \"due_date\": date.today() + timedelta(days=1),\n        \"priority\": 3\n    }\n\n    task = TaskCreate(**valid_data)\n\n    assert task.title == valid_data[\"title\"]\n    assert task.description == valid_data[\"description\"]\n    assert task.due_date == valid_data[\"due_date\"]\n    assert task.priority == valid_data[\"priority\"]\n\n@pytest.mark.unit\ndef test_task_create_title_too_short():\n    \"\"\"Перевірка помилки валідації, якщо назва занадто коротка.\"\"\"\n    invalid_data = {\n        \"title\": \"Ab\",  # Менше 3 символів\n        \"priority\": 1\n    }\n\n    # Контекстний менеджер очікує підняття вказаної помилки\n    with pytest.raises(ValidationError) as exc_info:\n        TaskCreate(**invalid_data)\n\n    # exc_info.value містить екземпляр ValidationError\n    errors = exc_info.value.errors()\n    assert len(errors) == 1\n    assert errors[0][\"loc\"] == (\"title\",)\n    assert errors[0][\"type\"] == \"string_too_short\"\n",[3563,4861,4862,4873,4880,4891,4903,4907,4912,4923,4928,4933,4946,4958,4976,4986,4991,4995,5000,5004,5017,5029,5040,5052,5056,5060,5069,5074,5079,5094,5103,5107,5111,5116,5130,5135,5140,5146,5152,5165,5190],{"__ignoreMap":3591},[3595,4863,4864,4866,4868,4870],{"class":3597,"line":3598},[3595,4865,4434],{"class":4433},[3595,4867,4437],{"class":4176},[3595,4869,4440],{"class":4433},[3595,4871,4872],{"class":4176}," date, timedelta\n",[3595,4874,4875,4877],{"class":3597,"line":3604},[3595,4876,4440],{"class":4433},[3595,4878,4879],{"class":4176}," pytest\n",[3595,4881,4882,4884,4886,4888],{"class":3597,"line":3610},[3595,4883,4434],{"class":4433},[3595,4885,4462],{"class":4176},[3595,4887,4440],{"class":4433},[3595,4889,4890],{"class":4176}," ValidationError\n",[3595,4892,4893,4895,4898,4900],{"class":3597,"line":3616},[3595,4894,4434],{"class":4433},[3595,4896,4897],{"class":4176}," app.schemas.task ",[3595,4899,4440],{"class":4433},[3595,4901,4902],{"class":4176}," TaskCreate\n",[3595,4904,4905],{"class":3597,"line":3622},[3595,4906,3638],{"emptyLinePlaceholder":3637},[3595,4908,4909],{"class":3597,"line":3628},[3595,4910,4911],{"class":4064},"@pytest.mark.unit\n",[3595,4913,4914,4917,4920],{"class":3597,"line":3634},[3595,4915,4916],{"class":4104},"def",[3595,4918,4919],{"class":4064}," test_task_create_valid_data",[3595,4921,4922],{"class":4176},"():\n",[3595,4924,4925],{"class":3597,"line":3641},[3595,4926,4927],{"class":4067},"    \"\"\"Тест успішної валідації з коректними даними.\"\"\"\n",[3595,4929,4930],{"class":3597,"line":3647},[3595,4931,4932],{"class":4176},"    valid_data = {\n",[3595,4934,4935,4938,4941,4944],{"class":3597,"line":3653},[3595,4936,4937],{"class":4067},"        \"title\"",[3595,4939,4940],{"class":4176},": ",[3595,4942,4943],{"class":4067},"\"Learn FastAPI testing\"",[3595,4945,4366],{"class":4176},[3595,4947,4948,4951,4953,4956],{"class":3597,"line":3659},[3595,4949,4950],{"class":4067},"        \"description\"",[3595,4952,4940],{"class":4176},[3595,4954,4955],{"class":4067},"\"Read article about pytest, fixtures and write unit tests.\"",[3595,4957,4366],{"class":4176},[3595,4959,4960,4963,4966,4969,4971,4973],{"class":3597,"line":3664},[3595,4961,4962],{"class":4067},"        \"due_date\"",[3595,4964,4965],{"class":4176},": date.today() + timedelta(",[3595,4967,4968],{"class":4188},"days",[3595,4970,4506],{"class":4176},[3595,4972,4595],{"class":4509},[3595,4974,4975],{"class":4176},"),\n",[3595,4977,4978,4981,4983],{"class":3597,"line":3670},[3595,4979,4980],{"class":4067},"        \"priority\"",[3595,4982,4940],{"class":4176},[3595,4984,4985],{"class":4509},"3\n",[3595,4987,4988],{"class":3597,"line":3676},[3595,4989,4990],{"class":4176},"    }\n",[3595,4992,4993],{"class":3597,"line":3681},[3595,4994,3638],{"emptyLinePlaceholder":3637},[3595,4996,4997],{"class":3597,"line":3687},[3595,4998,4999],{"class":4176},"    task = TaskCreate(**valid_data)\n",[3595,5001,5002],{"class":3597,"line":3693},[3595,5003,3638],{"emptyLinePlaceholder":3637},[3595,5005,5006,5009,5012,5015],{"class":3597,"line":3699},[3595,5007,5008],{"class":4433},"    assert",[3595,5010,5011],{"class":4176}," task.title == valid_data[",[3595,5013,5014],{"class":4067},"\"title\"",[3595,5016,4218],{"class":4176},[3595,5018,5019,5021,5024,5027],{"class":3597,"line":3705},[3595,5020,5008],{"class":4433},[3595,5022,5023],{"class":4176}," task.description == valid_data[",[3595,5025,5026],{"class":4067},"\"description\"",[3595,5028,4218],{"class":4176},[3595,5030,5031,5033,5036,5038],{"class":3597,"line":3711},[3595,5032,5008],{"class":4433},[3595,5034,5035],{"class":4176}," task.due_date == valid_data[",[3595,5037,4639],{"class":4067},[3595,5039,4218],{"class":4176},[3595,5041,5042,5044,5047,5050],{"class":3597,"line":3716},[3595,5043,5008],{"class":4433},[3595,5045,5046],{"class":4176}," task.priority == valid_data[",[3595,5048,5049],{"class":4067},"\"priority\"",[3595,5051,4218],{"class":4176},[3595,5053,5054],{"class":3597,"line":3722},[3595,5055,3638],{"emptyLinePlaceholder":3637},[3595,5057,5058],{"class":3597,"line":3728},[3595,5059,4911],{"class":4064},[3595,5061,5062,5064,5067],{"class":3597,"line":3734},[3595,5063,4916],{"class":4104},[3595,5065,5066],{"class":4064}," test_task_create_title_too_short",[3595,5068,4922],{"class":4176},[3595,5070,5071],{"class":3597,"line":3740},[3595,5072,5073],{"class":4067},"    \"\"\"Перевірка помилки валідації, якщо назва занадто коротка.\"\"\"\n",[3595,5075,5076],{"class":3597,"line":3745},[3595,5077,5078],{"class":4176},"    invalid_data = {\n",[3595,5080,5081,5083,5085,5088,5091],{"class":3597,"line":3750},[3595,5082,4937],{"class":4067},[3595,5084,4940],{"class":4176},[3595,5086,5087],{"class":4067},"\"Ab\"",[3595,5089,5090],{"class":4176},",  ",[3595,5092,5093],{"class":4182},"# Менше 3 символів\n",[3595,5095,5096,5098,5100],{"class":3597,"line":3756},[3595,5097,4980],{"class":4067},[3595,5099,4940],{"class":4176},[3595,5101,5102],{"class":4509},"1\n",[3595,5104,5105],{"class":3597,"line":3762},[3595,5106,4990],{"class":4176},[3595,5108,5109],{"class":3597,"line":3768},[3595,5110,3638],{"emptyLinePlaceholder":3637},[3595,5112,5113],{"class":3597,"line":3774},[3595,5114,5115],{"class":4182},"    # Контекстний менеджер очікує підняття вказаної помилки\n",[3595,5117,5118,5121,5124,5127],{"class":3597,"line":3779},[3595,5119,5120],{"class":4433},"    with",[3595,5122,5123],{"class":4176}," pytest.raises(ValidationError) ",[3595,5125,5126],{"class":4433},"as",[3595,5128,5129],{"class":4176}," exc_info:\n",[3595,5131,5132],{"class":3597,"line":3784},[3595,5133,5134],{"class":4176},"        TaskCreate(**invalid_data)\n",[3595,5136,5138],{"class":3597,"line":5137},34,[3595,5139,3638],{"emptyLinePlaceholder":3637},[3595,5141,5143],{"class":3597,"line":5142},35,[3595,5144,5145],{"class":4182},"    # exc_info.value містить екземпляр ValidationError\n",[3595,5147,5149],{"class":3597,"line":5148},36,[3595,5150,5151],{"class":4176},"    errors = exc_info.value.errors()\n",[3595,5153,5155,5157,5160,5163],{"class":3597,"line":5154},37,[3595,5156,5008],{"class":4433},[3595,5158,5159],{"class":4064}," len",[3595,5161,5162],{"class":4176},"(errors) == ",[3595,5164,5102],{"class":4509},[3595,5166,5168,5170,5173,5176,5179,5182,5185,5187],{"class":3597,"line":5167},38,[3595,5169,5008],{"class":4433},[3595,5171,5172],{"class":4176}," errors[",[3595,5174,5175],{"class":4509},"0",[3595,5177,5178],{"class":4176},"][",[3595,5180,5181],{"class":4067},"\"loc\"",[3595,5183,5184],{"class":4176},"] == (",[3595,5186,5014],{"class":4067},[3595,5188,5189],{"class":4176},",)\n",[3595,5191,5193,5195,5197,5199,5201,5204,5207],{"class":3597,"line":5192},39,[3595,5194,5008],{"class":4433},[3595,5196,5172],{"class":4176},[3595,5198,5175],{"class":4509},[3595,5200,5178],{"class":4176},[3595,5202,5203],{"class":4067},"\"type\"",[3595,5205,5206],{"class":4176},"] == ",[3595,5208,5209],{"class":4067},"\"string_too_short\"\n",[4384,5211,5212,5220],{},[3551,5213,5214],{},[3555,5215,5216,5217],{},"Анатомія ",[3563,5218,5219],{},"pytest.raises",[3551,5221,5222,5223,5226,5227,5230,5231,5234,5235,5238],{},"Контекстний менеджер ",[3563,5224,5225],{},"pytest.raises(ValidationError)"," є аналогом ",[3563,5228,5229],{},"Assert.Throws\u003CT>()"," в xUnit. Він перехоплює виключення, яке виникає всередині блоку ",[3563,5232,5233],{},"with",". Через властивість ",[3563,5236,5237],{},".value"," ми можемо отримати деталі виключення (наприклад, список помилок Pydantic) і зробити додаткові перевірки (asserts).",[3822,5240,5242,5243],{"id":5241},"параметризовані-тести-pytestmarkparametrize","Параметризовані тести: ",[3563,5244,3893],{},[3551,5246,5247,5248,5251],{},"У реальних проектах вам часто потрібно перевірити один і той самий алгоритм або правило валідації на великому наборі різних вхідних даних. Замість того, щоб копіювати один і той самий тестовий метод десять разів із різними параметрами, у тестуванні використовують концепцію ",[3555,5249,5250],{},"параметризованих тестів"," (або table-driven tests).",[3551,5253,5254,5255,5257,5258,5261,5262,3929],{},"В .NET xUnit для цього є пара ",[3563,5256,3882],{}," та ",[3563,5259,5260],{},"[InlineData]",". У pytest аналогічна логіка реалізована за допомогою вбудованого декоратора ",[3563,5263,3893],{},[3551,5265,5266,5267,4856],{},"Розглянемо, як протестувати різні варіанти невалідних даних для нашої схеми ",[3563,5268,5269],{},"TaskCreate",[3586,5271,5273],{"className":4423,"code":5272,"filename":4831,"language":4426,"meta":3591,"style":3591},"# Продовження файлу tests\u002Funit\u002Ftest_task_schemas.py\n\n@pytest.mark.unit\n@pytest.mark.parametrize(\n    \"invalid_priority\",\n    [0, 6, -1, 10]\n)\ndef test_task_create_invalid_priority(invalid_priority):\n    \"\"\"Перевірка обмежень для поля priority (має бути від 1 до 5).\"\"\"\n    invalid_data = {\n        \"title\": \"Learn pytest\",\n        \"priority\": invalid_priority\n    }\n\n    with pytest.raises(ValidationError) as exc_info:\n        TaskCreate(**invalid_data)\n\n    errors = exc_info.value.errors()\n    assert errors[0][\"loc\"] == (\"priority\",)\n    # Pydantic повертає детальний опис помилки обмеження\n    assert \"greater_than_equal\" in errors[0][\"type\"] or \"less_than_equal\" in errors[0][\"type\"]\n",[3563,5274,5275,5280,5284,5288,5295,5302,5326,5330,5344,5349,5353,5364,5371,5375,5379,5389,5393,5397,5401,5419,5424],{"__ignoreMap":3591},[3595,5276,5277],{"class":3597,"line":3598},[3595,5278,5279],{"class":4182},"# Продовження файлу tests\u002Funit\u002Ftest_task_schemas.py\n",[3595,5281,5282],{"class":3597,"line":3604},[3595,5283,3638],{"emptyLinePlaceholder":3637},[3595,5285,5286],{"class":3597,"line":3610},[3595,5287,4911],{"class":4064},[3595,5289,5290,5292],{"class":3597,"line":3616},[3595,5291,3893],{"class":4064},[3595,5293,5294],{"class":4176},"(\n",[3595,5296,5297,5300],{"class":3597,"line":3622},[3595,5298,5299],{"class":4067},"    \"invalid_priority\"",[3595,5301,4366],{"class":4176},[3595,5303,5304,5307,5309,5311,5314,5317,5319,5321,5324],{"class":3597,"line":3628},[3595,5305,5306],{"class":4176},"    [",[3595,5308,5175],{"class":4509},[3595,5310,4513],{"class":4176},[3595,5312,5313],{"class":4509},"6",[3595,5315,5316],{"class":4176},", -",[3595,5318,4595],{"class":4509},[3595,5320,4513],{"class":4176},[3595,5322,5323],{"class":4509},"10",[3595,5325,4218],{"class":4176},[3595,5327,5328],{"class":3597,"line":3634},[3595,5329,4534],{"class":4176},[3595,5331,5332,5334,5337,5339,5342],{"class":3597,"line":3641},[3595,5333,4916],{"class":4104},[3595,5335,5336],{"class":4064}," test_task_create_invalid_priority",[3595,5338,4483],{"class":4176},[3595,5340,5341],{"class":4188},"invalid_priority",[3595,5343,4489],{"class":4176},[3595,5345,5346],{"class":3597,"line":3647},[3595,5347,5348],{"class":4067},"    \"\"\"Перевірка обмежень для поля priority (має бути від 1 до 5).\"\"\"\n",[3595,5350,5351],{"class":3597,"line":3653},[3595,5352,5078],{"class":4176},[3595,5354,5355,5357,5359,5362],{"class":3597,"line":3659},[3595,5356,4937],{"class":4067},[3595,5358,4940],{"class":4176},[3595,5360,5361],{"class":4067},"\"Learn pytest\"",[3595,5363,4366],{"class":4176},[3595,5365,5366,5368],{"class":3597,"line":3664},[3595,5367,4980],{"class":4067},[3595,5369,5370],{"class":4176},": invalid_priority\n",[3595,5372,5373],{"class":3597,"line":3670},[3595,5374,4990],{"class":4176},[3595,5376,5377],{"class":3597,"line":3676},[3595,5378,3638],{"emptyLinePlaceholder":3637},[3595,5380,5381,5383,5385,5387],{"class":3597,"line":3681},[3595,5382,5120],{"class":4433},[3595,5384,5123],{"class":4176},[3595,5386,5126],{"class":4433},[3595,5388,5129],{"class":4176},[3595,5390,5391],{"class":3597,"line":3687},[3595,5392,5134],{"class":4176},[3595,5394,5395],{"class":3597,"line":3693},[3595,5396,3638],{"emptyLinePlaceholder":3637},[3595,5398,5399],{"class":3597,"line":3699},[3595,5400,5151],{"class":4176},[3595,5402,5403,5405,5407,5409,5411,5413,5415,5417],{"class":3597,"line":3705},[3595,5404,5008],{"class":4433},[3595,5406,5172],{"class":4176},[3595,5408,5175],{"class":4509},[3595,5410,5178],{"class":4176},[3595,5412,5181],{"class":4067},[3595,5414,5184],{"class":4176},[3595,5416,5049],{"class":4067},[3595,5418,5189],{"class":4176},[3595,5420,5421],{"class":3597,"line":3711},[3595,5422,5423],{"class":4182},"    # Pydantic повертає детальний опис помилки обмеження\n",[3595,5425,5426,5428,5431,5434,5436,5438,5440,5442,5445,5447,5450,5452,5454,5456,5458,5460],{"class":3597,"line":3716},[3595,5427,5008],{"class":4433},[3595,5429,5430],{"class":4067}," \"greater_than_equal\"",[3595,5432,5433],{"class":4104}," in",[3595,5435,5172],{"class":4176},[3595,5437,5175],{"class":4509},[3595,5439,5178],{"class":4176},[3595,5441,5203],{"class":4067},[3595,5443,5444],{"class":4176},"] ",[3595,5446,4773],{"class":4104},[3595,5448,5449],{"class":4067}," \"less_than_equal\"",[3595,5451,5433],{"class":4104},[3595,5453,5172],{"class":4176},[3595,5455,5175],{"class":4509},[3595,5457,5178],{"class":4176},[3595,5459,5203],{"class":4067},[3595,5461,4218],{"class":4176},[3551,5463,5464,5465,5467],{},"Декоратор ",[3563,5466,3893],{}," приймає два основних аргументи:",[3792,5469,5470,5477],{},[3795,5471,5472,5473,5476],{},"Рядок з іменами параметрів, які будуть передані у тестову функцію (наприклад, ",[3563,5474,5475],{},"\"invalid_priority\"",").",[3795,5478,5479],{},"Ітерований об'єкт (список, кортеж), де кожен елемент є набором аргументів для одного запуску тесту.",[3551,5481,5482],{},"Ми можемо передавати кілька параметрів одночасно. Давайте протестуємо логіку нашого кастомного валідатора, який забороняє вибирати дедлайн у минулому, а також перевіримо поведінку з коректними датами:",[3586,5484,5486],{"className":4423,"code":5485,"filename":4831,"language":4426,"meta":3591,"style":3591},"# Продовження файлу tests\u002Funit\u002Ftest_task_schemas.py\n\n@pytest.mark.unit\n@pytest.mark.parametrize(\n    \"due_date_value, should_be_valid\",\n    [\n        (date.today(), True),  # Сьогодні — дозволено\n        (date.today() + timedelta(days=1), True),  # Майбутнє — дозволено\n        (date.today() - timedelta(days=1), False),  # Вчора — помилка\n        (date.today() - timedelta(days=365), False),  # Рік тому — помилка\n    ]\n)\ndef test_task_due_date_validation(due_date_value, should_be_valid):\n    \"\"\"Тестування кастомного валідатора prevent_past_dates.\"\"\"\n    data = {\n        \"title\": \"Build a house\",\n        \"due_date\": due_date_value,\n        \"priority\": 1\n    }\n\n    if should_be_valid:\n        task = TaskCreate(**data)\n        assert task.due_date == due_date_value\n    else:\n        with pytest.raises(ValidationError) as exc_info:\n            TaskCreate(**data)\n        errors = exc_info.value.errors()\n        assert errors[0][\"loc\"] == (\"due_date\",)\n        assert \"Дедлайн не може бути в минулому\" in errors[0][\"msg\"]\n",[3563,5487,5488,5492,5496,5500,5506,5513,5518,5532,5553,5574,5594,5599,5603,5622,5627,5632,5643,5650,5658,5662,5666,5674,5679,5687,5694,5705,5710,5715,5733],{"__ignoreMap":3591},[3595,5489,5490],{"class":3597,"line":3598},[3595,5491,5279],{"class":4182},[3595,5493,5494],{"class":3597,"line":3604},[3595,5495,3638],{"emptyLinePlaceholder":3637},[3595,5497,5498],{"class":3597,"line":3610},[3595,5499,4911],{"class":4064},[3595,5501,5502,5504],{"class":3597,"line":3616},[3595,5503,3893],{"class":4064},[3595,5505,5294],{"class":4176},[3595,5507,5508,5511],{"class":3597,"line":3622},[3595,5509,5510],{"class":4067},"    \"due_date_value, should_be_valid\"",[3595,5512,4366],{"class":4176},[3595,5514,5515],{"class":3597,"line":3628},[3595,5516,5517],{"class":4176},"    [\n",[3595,5519,5520,5523,5526,5529],{"class":3597,"line":3634},[3595,5521,5522],{"class":4176},"        (date.today(), ",[3595,5524,5525],{"class":4104},"True",[3595,5527,5528],{"class":4176},"),  ",[3595,5530,5531],{"class":4182},"# Сьогодні — дозволено\n",[3595,5533,5534,5537,5539,5541,5543,5546,5548,5550],{"class":3597,"line":3641},[3595,5535,5536],{"class":4176},"        (date.today() + timedelta(",[3595,5538,4968],{"class":4188},[3595,5540,4506],{"class":4176},[3595,5542,4595],{"class":4509},[3595,5544,5545],{"class":4176},"), ",[3595,5547,5525],{"class":4104},[3595,5549,5528],{"class":4176},[3595,5551,5552],{"class":4182},"# Майбутнє — дозволено\n",[3595,5554,5555,5558,5560,5562,5564,5566,5569,5571],{"class":3597,"line":3647},[3595,5556,5557],{"class":4176},"        (date.today() - timedelta(",[3595,5559,4968],{"class":4188},[3595,5561,4506],{"class":4176},[3595,5563,4595],{"class":4509},[3595,5565,5545],{"class":4176},[3595,5567,5568],{"class":4104},"False",[3595,5570,5528],{"class":4176},[3595,5572,5573],{"class":4182},"# Вчора — помилка\n",[3595,5575,5576,5578,5580,5582,5585,5587,5589,5591],{"class":3597,"line":3653},[3595,5577,5557],{"class":4176},[3595,5579,4968],{"class":4188},[3595,5581,4506],{"class":4176},[3595,5583,5584],{"class":4509},"365",[3595,5586,5545],{"class":4176},[3595,5588,5568],{"class":4104},[3595,5590,5528],{"class":4176},[3595,5592,5593],{"class":4182},"# Рік тому — помилка\n",[3595,5595,5596],{"class":3597,"line":3659},[3595,5597,5598],{"class":4176},"    ]\n",[3595,5600,5601],{"class":3597,"line":3664},[3595,5602,4534],{"class":4176},[3595,5604,5605,5607,5610,5612,5615,5617,5620],{"class":3597,"line":3670},[3595,5606,4916],{"class":4104},[3595,5608,5609],{"class":4064}," test_task_due_date_validation",[3595,5611,4483],{"class":4176},[3595,5613,5614],{"class":4188},"due_date_value",[3595,5616,4513],{"class":4176},[3595,5618,5619],{"class":4188},"should_be_valid",[3595,5621,4489],{"class":4176},[3595,5623,5624],{"class":3597,"line":3676},[3595,5625,5626],{"class":4067},"    \"\"\"Тестування кастомного валідатора prevent_past_dates.\"\"\"\n",[3595,5628,5629],{"class":3597,"line":3681},[3595,5630,5631],{"class":4176},"    data = {\n",[3595,5633,5634,5636,5638,5641],{"class":3597,"line":3687},[3595,5635,4937],{"class":4067},[3595,5637,4940],{"class":4176},[3595,5639,5640],{"class":4067},"\"Build a house\"",[3595,5642,4366],{"class":4176},[3595,5644,5645,5647],{"class":3597,"line":3693},[3595,5646,4962],{"class":4067},[3595,5648,5649],{"class":4176},": due_date_value,\n",[3595,5651,5652,5654,5656],{"class":3597,"line":3699},[3595,5653,4980],{"class":4067},[3595,5655,4940],{"class":4176},[3595,5657,5102],{"class":4509},[3595,5659,5660],{"class":3597,"line":3705},[3595,5661,4990],{"class":4176},[3595,5663,5664],{"class":3597,"line":3711},[3595,5665,3638],{"emptyLinePlaceholder":3637},[3595,5667,5668,5671],{"class":3597,"line":3716},[3595,5669,5670],{"class":4433},"    if",[3595,5672,5673],{"class":4176}," should_be_valid:\n",[3595,5675,5676],{"class":3597,"line":3722},[3595,5677,5678],{"class":4176},"        task = TaskCreate(**data)\n",[3595,5680,5681,5684],{"class":3597,"line":3728},[3595,5682,5683],{"class":4433},"        assert",[3595,5685,5686],{"class":4176}," task.due_date == due_date_value\n",[3595,5688,5689,5692],{"class":3597,"line":3734},[3595,5690,5691],{"class":4433},"    else",[3595,5693,4749],{"class":4176},[3595,5695,5696,5699,5701,5703],{"class":3597,"line":3740},[3595,5697,5698],{"class":4433},"        with",[3595,5700,5123],{"class":4176},[3595,5702,5126],{"class":4433},[3595,5704,5129],{"class":4176},[3595,5706,5707],{"class":3597,"line":3745},[3595,5708,5709],{"class":4176},"            TaskCreate(**data)\n",[3595,5711,5712],{"class":3597,"line":3750},[3595,5713,5714],{"class":4176},"        errors = exc_info.value.errors()\n",[3595,5716,5717,5719,5721,5723,5725,5727,5729,5731],{"class":3597,"line":3756},[3595,5718,5683],{"class":4433},[3595,5720,5172],{"class":4176},[3595,5722,5175],{"class":4509},[3595,5724,5178],{"class":4176},[3595,5726,5181],{"class":4067},[3595,5728,5184],{"class":4176},[3595,5730,4639],{"class":4067},[3595,5732,5189],{"class":4176},[3595,5734,5735,5737,5740,5742,5744,5746,5748,5751],{"class":3597,"line":3762},[3595,5736,5683],{"class":4433},[3595,5738,5739],{"class":4067}," \"Дедлайн не може бути в минулому\"",[3595,5741,5433],{"class":4104},[3595,5743,5172],{"class":4176},[3595,5745,5175],{"class":4509},[3595,5747,5178],{"class":4176},[3595,5749,5750],{"class":4067},"\"msg\"",[3595,5752,4218],{"class":4176},[5754,5755,5756,5761,5764,6002],"tip",{},[3551,5757,5758],{},[3555,5759,5760],{},"Порівняння синтаксису параметризації",[3551,5762,5763],{},"Зверніть увагу, наскільки чисто виглядає параметризований тест у pytest порівняно з C#:",[5765,5766,5767,5910],"code-group",{},[3586,5768,5773],{"className":5769,"code":5770,"filename":5771,"language":5772,"meta":3591,"style":3591},"language-csharp shiki shiki-themes light-plus dark-plus dark-plus","[Theory]\n[InlineData(\"2026-07-13\", true)]\n[InlineData(\"2026-07-14\", true)]\n[InlineData(\"2026-07-12\", false)]\npublic void TestDueDate(string dateStr, bool shouldBeValid)\n{\n    var date = DateOnly.Parse(dateStr);\n    \u002F\u002F Логіка тесту...\n}\n","xUnit (C#)","csharp",[3563,5774,5775,5785,5805,5822,5840,5869,5874,5900,5905],{"__ignoreMap":3591},[3595,5776,5777,5780,5783],{"class":3597,"line":3598},[3595,5778,5779],{"class":4176},"[",[3595,5781,5782],{"class":4479},"Theory",[3595,5784,4218],{"class":4176},[3595,5786,5787,5789,5792,5794,5797,5799,5802],{"class":3597,"line":3604},[3595,5788,5779],{"class":4176},[3595,5790,5791],{"class":4479},"InlineData",[3595,5793,4483],{"class":4176},[3595,5795,5796],{"class":4067},"\"2026-07-13\"",[3595,5798,4513],{"class":4176},[3595,5800,5801],{"class":4104},"true",[3595,5803,5804],{"class":4176},")]\n",[3595,5806,5807,5809,5811,5813,5816,5818,5820],{"class":3597,"line":3610},[3595,5808,5779],{"class":4176},[3595,5810,5791],{"class":4479},[3595,5812,4483],{"class":4176},[3595,5814,5815],{"class":4067},"\"2026-07-14\"",[3595,5817,4513],{"class":4176},[3595,5819,5801],{"class":4104},[3595,5821,5804],{"class":4176},[3595,5823,5824,5826,5828,5830,5833,5835,5838],{"class":3597,"line":3616},[3595,5825,5779],{"class":4176},[3595,5827,5791],{"class":4479},[3595,5829,4483],{"class":4176},[3595,5831,5832],{"class":4067},"\"2026-07-12\"",[3595,5834,4513],{"class":4176},[3595,5836,5837],{"class":4104},"false",[3595,5839,5804],{"class":4176},[3595,5841,5842,5845,5848,5851,5853,5856,5859,5861,5864,5867],{"class":3597,"line":3622},[3595,5843,5844],{"class":4104},"public",[3595,5846,5847],{"class":4104}," void",[3595,5849,5850],{"class":4064}," TestDueDate",[3595,5852,4483],{"class":4176},[3595,5854,5855],{"class":4104},"string",[3595,5857,5858],{"class":4188}," dateStr",[3595,5860,4513],{"class":4176},[3595,5862,5863],{"class":4104},"bool",[3595,5865,5866],{"class":4188}," shouldBeValid",[3595,5868,4534],{"class":4176},[3595,5870,5871],{"class":3597,"line":3628},[3595,5872,5873],{"class":4176},"{\n",[3595,5875,5876,5879,5882,5884,5887,5889,5892,5894,5897],{"class":3597,"line":3634},[3595,5877,5878],{"class":4104},"    var",[3595,5880,5881],{"class":4188}," date",[3595,5883,4192],{"class":4176},[3595,5885,5886],{"class":4188},"DateOnly",[3595,5888,3929],{"class":4176},[3595,5890,5891],{"class":4064},"Parse",[3595,5893,4483],{"class":4176},[3595,5895,5896],{"class":4188},"dateStr",[3595,5898,5899],{"class":4176},");\n",[3595,5901,5902],{"class":3597,"line":3641},[3595,5903,5904],{"class":4182},"    \u002F\u002F Логіка тесту...\n",[3595,5906,5907],{"class":3597,"line":3647},[3595,5908,5909],{"class":4176},"}\n",[3586,5911,5914],{"className":4423,"code":5912,"filename":5913,"language":4426,"meta":3591,"style":3591},"@pytest.mark.parametrize(\n    \"due_date_value, should_be_valid\",\n    [\n        (date.today(), True),\n        (date.today() + timedelta(days=1), True),\n        (date.today() - timedelta(days=1), False),\n    ]\n)\ndef test_due_date(due_date_value, should_be_valid):\n    # Логіка тесту...\n","pytest (Python)",[3563,5915,5916,5922,5928,5932,5940,5956,5972,5976,5980,5997],{"__ignoreMap":3591},[3595,5917,5918,5920],{"class":3597,"line":3598},[3595,5919,3893],{"class":4064},[3595,5921,5294],{"class":4176},[3595,5923,5924,5926],{"class":3597,"line":3604},[3595,5925,5510],{"class":4067},[3595,5927,4366],{"class":4176},[3595,5929,5930],{"class":3597,"line":3610},[3595,5931,5517],{"class":4176},[3595,5933,5934,5936,5938],{"class":3597,"line":3616},[3595,5935,5522],{"class":4176},[3595,5937,5525],{"class":4104},[3595,5939,4975],{"class":4176},[3595,5941,5942,5944,5946,5948,5950,5952,5954],{"class":3597,"line":3622},[3595,5943,5536],{"class":4176},[3595,5945,4968],{"class":4188},[3595,5947,4506],{"class":4176},[3595,5949,4595],{"class":4509},[3595,5951,5545],{"class":4176},[3595,5953,5525],{"class":4104},[3595,5955,4975],{"class":4176},[3595,5957,5958,5960,5962,5964,5966,5968,5970],{"class":3597,"line":3628},[3595,5959,5557],{"class":4176},[3595,5961,4968],{"class":4188},[3595,5963,4506],{"class":4176},[3595,5965,4595],{"class":4509},[3595,5967,5545],{"class":4176},[3595,5969,5568],{"class":4104},[3595,5971,4975],{"class":4176},[3595,5973,5974],{"class":3597,"line":3634},[3595,5975,5598],{"class":4176},[3595,5977,5978],{"class":3597,"line":3641},[3595,5979,4534],{"class":4176},[3595,5981,5982,5984,5987,5989,5991,5993,5995],{"class":3597,"line":3647},[3595,5983,4916],{"class":4104},[3595,5985,5986],{"class":4064}," test_due_date",[3595,5988,4483],{"class":4176},[3595,5990,5614],{"class":4188},[3595,5992,4513],{"class":4176},[3595,5994,5619],{"class":4188},[3595,5996,4489],{"class":4176},[3595,5998,5999],{"class":3597,"line":3653},[3595,6000,6001],{"class":4182},"    # Логіка тесту...\n",[3551,6003,6004,6005,6008,6009,6011,6012,4844,6015,3929],{},"У Python ми можемо передавати об'єкти складних типів (наприклад, динамічно обчислені дати ",[3563,6006,6007],{},"date.today() + timedelta(...)",") безпосередньо у параметри декоратора. В C# атрибути ",[3563,6010,5260],{}," приймають лише константні вирази, що змушує розробників використовувати складніші конструкції на кшталт ",[3563,6013,6014],{},"[MemberData]",[3563,6016,6017],{},"[ClassData]",[3568,6019],{},[3571,6021,6023],{"id":6022},"_4-фікстури-fixtures-у-pytest-основа-dependency-injection","4. Фікстури (Fixtures) у pytest: Основа Dependency Injection",[3551,6025,6026,6027,5257,6030,6033,6034,6037],{},"У традиційних об'єктно-орієнтованих тестових фреймворках (на кшталт xUnit чи MSTest) для підготовки тестового середовища використовуються методи ",[3563,6028,6029],{},"SetUp",[3563,6031,6032],{},"TearDown"," (або конструктор класу та метод ",[3563,6035,6036],{},"Dispose()","). Усі тести в межах одного класу ділять ці методи.",[3551,6039,6040,6041,6044],{},"Цей підхід має суттєвий недолік — ",[3555,6042,6043],{},"жорстку залежність",". Якщо частина тестів у класі потребує підключення до БД, а частина — ні, вам доведеться або створювати підключення для всіх тестів, або розносити їх по різних файлах, що штучно ускладнює структуру проекту.",[3551,6046,6047,6049,6050,6053,6054,3929],{},[3563,6048,3864],{}," пропонує принципово іншу модель — ",[3555,6051,6052],{},"фікстури (fixtures)",". Це функції, які постачають ресурси, дані або сервіси для ваших тестів через механізм ",[3555,6055,6056],{},"впровадження залежностей (Dependency Injection)",[3583,6058,6059],{},[3586,6060,6062],{"className":3588,"code":6061,"language":3590,"meta":3591,"style":3591},"@startuml\nskinparam style plain\nskinparam backgroundColor #ffffff\nskinparam ArrowColor #3b82f6\n\npackage \"Тестове середовище pytest\" {\n    component \"conftest.py\\n(Глобальні фікстури)\" as conftest #f8fafc\n    component \"test_users.py\\n(Файл тестів)\" as test_file #f8fafc\n}\n\ndatabase \"Тестова База Даних\" as db #e2e8f0\n\nconftest -down-> db : 1. Створює схему \u002F з'єднання\nconftest -right-> test_file : 2. Впроваджує fixture `db_session`\ntest_file -> test_file : 3. Виконує тест з `db_session`\nconftest \u003C-down- db : 4. Робить Rollback транзакції (teardown)\n@enduml\n",[3563,6063,6064,6068,6072,6076,6080,6084,6089,6094,6099,6103,6107,6112,6116,6121,6126,6131,6136],{"__ignoreMap":3591},[3595,6065,6066],{"class":3597,"line":3598},[3595,6067,3601],{},[3595,6069,6070],{"class":3597,"line":3604},[3595,6071,3607],{},[3595,6073,6074],{"class":3597,"line":3610},[3595,6075,3613],{},[3595,6077,6078],{"class":3597,"line":3616},[3595,6079,3619],{},[3595,6081,6082],{"class":3597,"line":3622},[3595,6083,3638],{"emptyLinePlaceholder":3637},[3595,6085,6086],{"class":3597,"line":3628},[3595,6087,6088],{},"package \"Тестове середовище pytest\" {\n",[3595,6090,6091],{"class":3597,"line":3634},[3595,6092,6093],{},"    component \"conftest.py\\n(Глобальні фікстури)\" as conftest #f8fafc\n",[3595,6095,6096],{"class":3597,"line":3641},[3595,6097,6098],{},"    component \"test_users.py\\n(Файл тестів)\" as test_file #f8fafc\n",[3595,6100,6101],{"class":3597,"line":3647},[3595,6102,5909],{},[3595,6104,6105],{"class":3597,"line":3653},[3595,6106,3638],{"emptyLinePlaceholder":3637},[3595,6108,6109],{"class":3597,"line":3659},[3595,6110,6111],{},"database \"Тестова База Даних\" as db #e2e8f0\n",[3595,6113,6114],{"class":3597,"line":3664},[3595,6115,3638],{"emptyLinePlaceholder":3637},[3595,6117,6118],{"class":3597,"line":3670},[3595,6119,6120],{},"conftest -down-> db : 1. Створює схему \u002F з'єднання\n",[3595,6122,6123],{"class":3597,"line":3676},[3595,6124,6125],{},"conftest -right-> test_file : 2. Впроваджує fixture `db_session`\n",[3595,6127,6128],{"class":3597,"line":3681},[3595,6129,6130],{},"test_file -> test_file : 3. Виконує тест з `db_session`\n",[3595,6132,6133],{"class":3597,"line":3687},[3595,6134,6135],{},"conftest \u003C-down- db : 4. Робить Rollback транзакції (teardown)\n",[3595,6137,6138],{"class":3597,"line":3693},[3595,6139,3787],{},[3822,6141,6143],{"id":6142},"створення-базової-фікстури","Створення базової фікстури",[3551,6145,6146,6147,6150],{},"Для створення фікстури використовується декоратор ",[3563,6148,6149],{},"@pytest.fixture",". Коли тестова функція приймає аргумент із таким самим ім'ям, як і фікстура, pytest автоматично викликає функцію-фікстуру і передає її результат як цей аргумент.",[3551,6152,6153],{},"Уявімо, що нам потрібен тестовий словник із типовими даними користувача для створення профілю:",[3586,6155,6157],{"className":4423,"code":6156,"language":4426,"meta":3591,"style":3591},"import pytest\n\n@pytest.fixture\ndef sample_user_data():\n    \"\"\"Фікстура, що повертає початкові дані для тестування користувачів.\"\"\"\n    return {\n        \"email\": \"student@kostyl.dev\",\n        \"username\": \"kostyl_dev\",\n        \"password\": \"SuperSecretPassword123\"\n    }\n\n# Використання фікстури в тесті\ndef test_user_data_keys(sample_user_data):\n    assert \"email\" in sample_user_data\n    assert sample_user_data[\"username\"] == \"kostyl_dev\"\n",[3563,6158,6159,6165,6169,6174,6183,6188,6196,6208,6220,6230,6234,6238,6243,6257,6269],{"__ignoreMap":3591},[3595,6160,6161,6163],{"class":3597,"line":3598},[3595,6162,4440],{"class":4433},[3595,6164,4879],{"class":4176},[3595,6166,6167],{"class":3597,"line":3604},[3595,6168,3638],{"emptyLinePlaceholder":3637},[3595,6170,6171],{"class":3597,"line":3610},[3595,6172,6173],{"class":4064},"@pytest.fixture\n",[3595,6175,6176,6178,6181],{"class":3597,"line":3616},[3595,6177,4916],{"class":4104},[3595,6179,6180],{"class":4064}," sample_user_data",[3595,6182,4922],{"class":4176},[3595,6184,6185],{"class":3597,"line":3622},[3595,6186,6187],{"class":4067},"    \"\"\"Фікстура, що повертає початкові дані для тестування користувачів.\"\"\"\n",[3595,6189,6190,6193],{"class":3597,"line":3628},[3595,6191,6192],{"class":4433},"    return",[3595,6194,6195],{"class":4176}," {\n",[3595,6197,6198,6201,6203,6206],{"class":3597,"line":3634},[3595,6199,6200],{"class":4067},"        \"email\"",[3595,6202,4940],{"class":4176},[3595,6204,6205],{"class":4067},"\"student@kostyl.dev\"",[3595,6207,4366],{"class":4176},[3595,6209,6210,6213,6215,6218],{"class":3597,"line":3641},[3595,6211,6212],{"class":4067},"        \"username\"",[3595,6214,4940],{"class":4176},[3595,6216,6217],{"class":4067},"\"kostyl_dev\"",[3595,6219,4366],{"class":4176},[3595,6221,6222,6225,6227],{"class":3597,"line":3647},[3595,6223,6224],{"class":4067},"        \"password\"",[3595,6226,4940],{"class":4176},[3595,6228,6229],{"class":4067},"\"SuperSecretPassword123\"\n",[3595,6231,6232],{"class":3597,"line":3653},[3595,6233,4990],{"class":4176},[3595,6235,6236],{"class":3597,"line":3659},[3595,6237,3638],{"emptyLinePlaceholder":3637},[3595,6239,6240],{"class":3597,"line":3664},[3595,6241,6242],{"class":4182},"# Використання фікстури в тесті\n",[3595,6244,6245,6247,6250,6252,6255],{"class":3597,"line":3670},[3595,6246,4916],{"class":4104},[3595,6248,6249],{"class":4064}," test_user_data_keys",[3595,6251,4483],{"class":4176},[3595,6253,6254],{"class":4188},"sample_user_data",[3595,6256,4489],{"class":4176},[3595,6258,6259,6261,6264,6266],{"class":3597,"line":3676},[3595,6260,5008],{"class":4433},[3595,6262,6263],{"class":4067}," \"email\"",[3595,6265,5433],{"class":4104},[3595,6267,6268],{"class":4176}," sample_user_data\n",[3595,6270,6271,6273,6276,6279,6281],{"class":3597,"line":3681},[3595,6272,5008],{"class":4433},[3595,6274,6275],{"class":4176}," sample_user_data[",[3595,6277,6278],{"class":4067},"\"username\"",[3595,6280,5206],{"class":4176},[3595,6282,6283],{"class":4067},"\"kostyl_dev\"\n",[3822,6285,6287],{"id":6286},"області-видимості-fixture-scopes","Області видимості (Fixture Scopes)",[3551,6289,6290],{},"Ресурси, які ми створюємо для тестів, мають різну «вартість». Наприклад, генерація словника в пам'яті відбувається миттєво, а от створення підключення до бази даних PostgreSQL або підняття тестового сервера — операції важкі.",[3551,6292,6293,6294,4856],{},"pytest дозволяє контролювати життєвий цикл фікстури за допомогою параметра ",[3563,6295,6296],{},"scope",[6298,6299,6300,6310,6320,6327,6334],"field-group",{},[6301,6302,6305,6306,6309],"field",{"name":6303,"type":6304},"scope='function'","за замовчуванням","Фікстура викликається ",[3555,6307,6308],{},"для кожного тестового методу"," окремо. Це гарантує повну ізоляцію: зміни, внесені в об'єкт в одному тесті, не вплинуть на інший тест.",[6301,6311,6305,6313,6316,6317,5476],{"name":6312},"scope='class'",[3555,6314,6315],{},"один раз на клас тестів"," (якщо тести об'єднані в клас ",[3563,6318,6319],{},"class TestSomething",[6301,6321,6305,6323,6326],{"name":6322},"scope='module'",[3555,6324,6325],{},"один раз на Python-файл (модуль)",". Корисно для групування тестів, що працюють з одним файлом конфігурації.",[6301,6328,6330,6331,3929],{"name":6329},"scope='package'","Фікстура створюється ",[3555,6332,6333],{},"один раз на пакет (папку)",[6301,6335,6330,6337,6340],{"name":6336},"scope='session'",[3555,6338,6339],{},"один раз на весь час запуску тестів (тестову сесію)",". Ідеально підходить для запуску тестових контейнерів (testcontainers) або створення пулу підключень до бази даних.",[3822,6342,6344,6345],{"id":6343},"життєвий-цикл-setup-та-teardown-через-yield","Життєвий цикл: Setup та Teardown через ",[3563,6346,3918],{},[3551,6348,6349],{},"Часто фікстурі потрібно не просто створити ресурс, але й прибрати за собою після завершення тесту (закрити файловий потік, очистити таблиці БД, видалити тимчасові файли).",[3551,6351,6352,6353,6356,6357,6359,6360,6362,6363,6366,6367,6369,6370,6373],{},"Для цього замість ключового слова ",[3563,6354,6355],{},"return"," використовується ",[3563,6358,3918],{},". Код до ",[3563,6361,3918],{}," виконується ",[3555,6364,6365],{},"до"," запуску тесту (Setup), а код після ",[3563,6368,3918],{}," — ",[3555,6371,6372],{},"після"," завершення тесту (Teardown), навіть якщо тест завершився з помилкою.",[3586,6375,6377],{"className":4423,"code":6376,"language":4426,"meta":3591,"style":3591},"import os\nimport pytest\n\n@pytest.fixture(scope=\"function\")\ndef temp_file():\n    # Setup: створюємо тимчасовий файл\n    file_path = \"temp_test_data.txt\"\n    with open(file_path, \"w\") as f:\n        f.write(\"Test content\")\n\n    # Повертаємо шлях до файлу в тест\n    yield file_path\n\n    # Teardown: видаляємо файл після завершення тесту\n    if os.path.exists(file_path):\n        os.remove(file_path)\n\ndef test_read_temp_file(temp_file):\n    with open(temp_file, \"r\") as f:\n        content = f.read()\n    assert content == \"Test content\"\n",[3563,6378,6379,6386,6392,6396,6411,6420,6425,6433,6454,6464,6468,6473,6481,6485,6490,6497,6502,6506,6520,6538,6543],{"__ignoreMap":3591},[3595,6380,6381,6383],{"class":3597,"line":3598},[3595,6382,4440],{"class":4433},[3595,6384,6385],{"class":4176}," os\n",[3595,6387,6388,6390],{"class":3597,"line":3604},[3595,6389,4440],{"class":4433},[3595,6391,4879],{"class":4176},[3595,6393,6394],{"class":3597,"line":3610},[3595,6395,3638],{"emptyLinePlaceholder":3637},[3595,6397,6398,6400,6402,6404,6406,6409],{"class":3597,"line":3616},[3595,6399,6149],{"class":4064},[3595,6401,4483],{"class":4176},[3595,6403,6296],{"class":4188},[3595,6405,4506],{"class":4176},[3595,6407,6408],{"class":4067},"\"function\"",[3595,6410,4534],{"class":4176},[3595,6412,6413,6415,6418],{"class":3597,"line":3622},[3595,6414,4916],{"class":4104},[3595,6416,6417],{"class":4064}," temp_file",[3595,6419,4922],{"class":4176},[3595,6421,6422],{"class":3597,"line":3628},[3595,6423,6424],{"class":4182},"    # Setup: створюємо тимчасовий файл\n",[3595,6426,6427,6430],{"class":3597,"line":3634},[3595,6428,6429],{"class":4176},"    file_path = ",[3595,6431,6432],{"class":4067},"\"temp_test_data.txt\"\n",[3595,6434,6435,6437,6440,6443,6446,6449,6451],{"class":3597,"line":3641},[3595,6436,5120],{"class":4433},[3595,6438,6439],{"class":4064}," open",[3595,6441,6442],{"class":4176},"(file_path, ",[3595,6444,6445],{"class":4067},"\"w\"",[3595,6447,6448],{"class":4176},") ",[3595,6450,5126],{"class":4433},[3595,6452,6453],{"class":4176}," f:\n",[3595,6455,6456,6459,6462],{"class":3597,"line":3647},[3595,6457,6458],{"class":4176},"        f.write(",[3595,6460,6461],{"class":4067},"\"Test content\"",[3595,6463,4534],{"class":4176},[3595,6465,6466],{"class":3597,"line":3653},[3595,6467,3638],{"emptyLinePlaceholder":3637},[3595,6469,6470],{"class":3597,"line":3659},[3595,6471,6472],{"class":4182},"    # Повертаємо шлях до файлу в тест\n",[3595,6474,6475,6478],{"class":3597,"line":3664},[3595,6476,6477],{"class":4433},"    yield",[3595,6479,6480],{"class":4176}," file_path\n",[3595,6482,6483],{"class":3597,"line":3670},[3595,6484,3638],{"emptyLinePlaceholder":3637},[3595,6486,6487],{"class":3597,"line":3676},[3595,6488,6489],{"class":4182},"    # Teardown: видаляємо файл після завершення тесту\n",[3595,6491,6492,6494],{"class":3597,"line":3681},[3595,6493,5670],{"class":4433},[3595,6495,6496],{"class":4176}," os.path.exists(file_path):\n",[3595,6498,6499],{"class":3597,"line":3687},[3595,6500,6501],{"class":4176},"        os.remove(file_path)\n",[3595,6503,6504],{"class":3597,"line":3693},[3595,6505,3638],{"emptyLinePlaceholder":3637},[3595,6507,6508,6510,6513,6515,6518],{"class":3597,"line":3699},[3595,6509,4916],{"class":4104},[3595,6511,6512],{"class":4064}," test_read_temp_file",[3595,6514,4483],{"class":4176},[3595,6516,6517],{"class":4188},"temp_file",[3595,6519,4489],{"class":4176},[3595,6521,6522,6524,6526,6529,6532,6534,6536],{"class":3597,"line":3705},[3595,6523,5120],{"class":4433},[3595,6525,6439],{"class":4064},[3595,6527,6528],{"class":4176},"(temp_file, ",[3595,6530,6531],{"class":4067},"\"r\"",[3595,6533,6448],{"class":4176},[3595,6535,5126],{"class":4433},[3595,6537,6453],{"class":4176},[3595,6539,6540],{"class":3597,"line":3711},[3595,6541,6542],{"class":4176},"        content = f.read()\n",[3595,6544,6545,6547,6550],{"class":3597,"line":3716},[3595,6546,5008],{"class":4433},[3595,6548,6549],{"class":4176}," content == ",[3595,6551,6552],{"class":4067},"\"Test content\"\n",[3822,6554,6556],{"id":6555},"conftestpy-джерело-істини","conftest.py — Джерело істини",[3551,6558,6559],{},"Де ви маєте зберігати свої фікстури? Якщо ви напишете їх у файлі з тестами, вони будуть доступні лише в межах цього файлу.",[3551,6561,6562,6563,3929],{},"Для того, щоб зробити фікстури глобальними та доступними для багатьох файлів без явного імпорту, pytest використовує спеціальний файл з назвою ",[3563,6564,6565],{},"conftest.py",[3551,6567,6568,6569,6571],{},"pytest автоматично сканує каталог з тестами, знаходить усі файли ",[3563,6570,6565],{}," і реєструє описані в них фікстури. Ви можете створювати кілька таких файлів на різних рівнях вкладеності папок, створюючи ієрархію областей видимості:",[3586,6573,6578],{"className":6574,"code":6576,"language":6577},[6575],"language-text","tests\u002F\n│\n├── conftest.py          # Глобальні фікстури (тестова сесія, конфіги, клієнт)\n│\n├── unit\u002F\n│   ├── conftest.py      # Фікстури суто для unit-тестів\n│   └── test_schemas.py\n│\n└── integration\u002F\n    ├── conftest.py      # Фікстури для роботи з БД, перевизначення DI\n    └── test_endpoints.py\n","text",[3563,6579,6576],{"__ignoreMap":3591},[3568,6581],{},[3571,6583,6585],{"id":6584},"_5-інтеграційне-тестування-ендпоінтів-fastapi-testclient-та-httpx","5. Інтеграційне тестування ендпоінтів: FastAPI TestClient та HTTPX",[3551,6587,6588],{},"Коли ми переходимо від модульних тестів до інтеграційних, нашою метою стає перевірка поведінки системи в цілому. Ми хочемо перевірити, як наші ендпоінти реагують на HTTP-запити: чи правильно працюють роутери, чи спрацьовують потрібні dependency injections (залежності), чи коректно обробляються виключення (exceptions) і чи повертаються правильні HTTP-статуси та JSON-тіло відповіді.",[3822,6590,6592],{"id":6591},"як-працює-testclient-під-капотом","Як працює TestClient під капотом",[3551,6594,6595,6596,6599,6600,6602],{},"У світі ASP.NET Core для створення інтеграційних тестів використовується клас ",[3563,6597,6598],{},"WebApplicationFactory\u003CTStartup>",", який розгортає ваш додаток в оперативній пам'яті (in-memory TestServer) і надає об'єкт ",[3563,6601,3941],{}," для надсилання запитів до нього.",[3551,6604,6605,6606,3929],{},"FastAPI пропонує дуже схожий і навіть простіший механізм за допомогою вбудованого інструменту ",[3563,6607,6608],{},"fastapi.testclient.TestClient",[3583,6610,6611],{},[3586,6612,6614],{"className":3588,"code":6613,"language":3590,"meta":3591,"style":3591},"@startuml\nskinparam style plain\nskinparam backgroundColor #ffffff\nskinparam ArrowColor #3b82f6\n\nparticipant \"pytest (Test)\" as test #f8fafc\nparticipant \"TestClient (httpx)\" as client #e0f2fe\nparticipant \"ASGI Application\\n(FastAPI app)\" as app #e8f5e9\ndatabase \"Database \u002F External Services\" as db #f3e5f5\n\ntest -> client: client.get(\"\u002Fapi\u002Ftasks\")\nactivate client\nclient -> app: Виклик ASGI-інтерфейсу (в пам'яті)\nactivate app\napp -> db: Запит даних\ndb --> app: Результат\napp --> client: HTTP-like Response (status, body)\ndeactivate app\nclient --> test: httpx.Response\ndeactivate client\n@enduml\n",[3563,6615,6616,6620,6624,6628,6632,6636,6641,6646,6651,6656,6660,6665,6670,6675,6680,6685,6690,6695,6700,6705,6710],{"__ignoreMap":3591},[3595,6617,6618],{"class":3597,"line":3598},[3595,6619,3601],{},[3595,6621,6622],{"class":3597,"line":3604},[3595,6623,3607],{},[3595,6625,6626],{"class":3597,"line":3610},[3595,6627,3613],{},[3595,6629,6630],{"class":3597,"line":3616},[3595,6631,3619],{},[3595,6633,6634],{"class":3597,"line":3622},[3595,6635,3638],{"emptyLinePlaceholder":3637},[3595,6637,6638],{"class":3597,"line":3628},[3595,6639,6640],{},"participant \"pytest (Test)\" as test #f8fafc\n",[3595,6642,6643],{"class":3597,"line":3634},[3595,6644,6645],{},"participant \"TestClient (httpx)\" as client #e0f2fe\n",[3595,6647,6648],{"class":3597,"line":3641},[3595,6649,6650],{},"participant \"ASGI Application\\n(FastAPI app)\" as app #e8f5e9\n",[3595,6652,6653],{"class":3597,"line":3647},[3595,6654,6655],{},"database \"Database \u002F External Services\" as db #f3e5f5\n",[3595,6657,6658],{"class":3597,"line":3653},[3595,6659,3638],{"emptyLinePlaceholder":3637},[3595,6661,6662],{"class":3597,"line":3659},[3595,6663,6664],{},"test -> client: client.get(\"\u002Fapi\u002Ftasks\")\n",[3595,6666,6667],{"class":3597,"line":3664},[3595,6668,6669],{},"activate client\n",[3595,6671,6672],{"class":3597,"line":3670},[3595,6673,6674],{},"client -> app: Виклик ASGI-інтерфейсу (в пам'яті)\n",[3595,6676,6677],{"class":3597,"line":3676},[3595,6678,6679],{},"activate app\n",[3595,6681,6682],{"class":3597,"line":3681},[3595,6683,6684],{},"app -> db: Запит даних\n",[3595,6686,6687],{"class":3597,"line":3687},[3595,6688,6689],{},"db --> app: Результат\n",[3595,6691,6692],{"class":3597,"line":3693},[3595,6693,6694],{},"app --> client: HTTP-like Response (status, body)\n",[3595,6696,6697],{"class":3597,"line":3699},[3595,6698,6699],{},"deactivate app\n",[3595,6701,6702],{"class":3597,"line":3705},[3595,6703,6704],{},"client --> test: httpx.Response\n",[3595,6706,6707],{"class":3597,"line":3711},[3595,6708,6709],{},"deactivate client\n",[3595,6711,6712],{"class":3597,"line":3716},[3595,6713,3787],{},[3551,6715,6716,6717,6719,6720,6723],{},"Під капотом ",[3563,6718,3809],{}," використовує популярну бібліотеку ",[3555,6721,6722],{},"HTTPX"," — сучасний HTTP-клієнт для Python, який підтримує як синхронні, так і асинхронні запити.",[3551,6725,6726,6727,6729,6730,6733,6734,6737],{},"Найважливіша особливість ",[3563,6728,3809],{}," полягає в тому, що він ",[3555,6731,6732],{},"не запускає реальний веб-сервер"," (як-от Uvicorn) на мережевому порту (наприклад, ",[3563,6735,6736],{},"http:\u002F\u002F127.0.0.1:8000","). Замість цього він використовує ASGI-стандарт. Клієнт напряму комунікує з вашим FastAPI-додатком через внутрішні виклики функцій в оперативній пам'яті. Це робить виконання тестів надзвичайно швидким — ви можете прогнати сотні API-тестів за лічені секунди.",[3822,6739,6741],{"id":6740},"синхронний-testclient-проти-асинхронного-asyncclient","Синхронний TestClient проти асинхронного AsyncClient",[3551,6743,6744,6745,6747,6748,5476],{},"FastAPI підтримує як класичні синхронні ендпоінти (",[3563,6746,4916],{},"), так і сучасні асинхронні (",[3563,6749,6750],{},"async def",[3551,6752,6753,6754,6756,6757,6760,6761,6763,6764,6767,6768,3929],{},"Для тестування синхронних запитів підходить вбудований ",[3563,6755,3809],{},". Проте, якщо ваш додаток активно використовує асинхронну роботу (наприклад, асинхронний драйвер SQLAlchemy ",[3563,6758,6759],{},"asyncpg"," чи асинхронний клієнт Redis), використання синхронного ",[3563,6762,3809],{}," може призвести до помилок з блокуванням Event Loop. У таких випадках потрібно використовувати асинхронний клієнт — ",[3563,6765,6766],{},"httpx.AsyncClient"," разом із плагіном ",[3563,6769,6770],{},"pytest-asyncio",[3551,6772,6773,6774,4856],{},"Порівняймо налаштування обох клієнтів у нашому глобальному файлі ",[3563,6775,6776],{},"tests\u002Fconftest.py",[3586,6778,6780],{"className":4423,"code":6779,"filename":6776,"language":4426,"meta":3591,"style":3591},"import pytest\nfrom fastapi.testclient import TestClient\nfrom httpx import AsyncClient\nfrom app.main import app  # Імпорт вашого FastAPI застосунку\n\n# 1. Синхронний клієнт для простих тестів\n@pytest.fixture(scope=\"module\")\ndef client():\n    \"\"\"Фікстура для створення синхронного тестового клієнта.\"\"\"\n    # Context manager гарантує спрацювання подій startup\u002Fshutdown застосунку\n    with TestClient(app) as c:\n        yield c\n\n# 2. Асинхронний клієнт для тестування асинхронних ендпоінтів\n@pytest.fixture(scope=\"module\")\nasync def async_client():\n    \"\"\"Фікстура для створення асинхронного тестового клієнта.\"\"\"\n    # AsyncClient приймає наш app і base_url\n    async with AsyncClient(app=app, base_url=\"http:\u002F\u002Ftest\") as ac:\n        yield ac\n",[3563,6781,6782,6788,6800,6812,6827,6831,6836,6851,6860,6865,6870,6882,6890,6894,6899,6913,6926,6931,6936,6968],{"__ignoreMap":3591},[3595,6783,6784,6786],{"class":3597,"line":3598},[3595,6785,4440],{"class":4433},[3595,6787,4879],{"class":4176},[3595,6789,6790,6792,6795,6797],{"class":3597,"line":3604},[3595,6791,4434],{"class":4433},[3595,6793,6794],{"class":4176}," fastapi.testclient ",[3595,6796,4440],{"class":4433},[3595,6798,6799],{"class":4176}," TestClient\n",[3595,6801,6802,6804,6807,6809],{"class":3597,"line":3610},[3595,6803,4434],{"class":4433},[3595,6805,6806],{"class":4176}," httpx ",[3595,6808,4440],{"class":4433},[3595,6810,6811],{"class":4176}," AsyncClient\n",[3595,6813,6814,6816,6819,6821,6824],{"class":3597,"line":3616},[3595,6815,4434],{"class":4433},[3595,6817,6818],{"class":4176}," app.main ",[3595,6820,4440],{"class":4433},[3595,6822,6823],{"class":4176}," app  ",[3595,6825,6826],{"class":4182},"# Імпорт вашого FastAPI застосунку\n",[3595,6828,6829],{"class":3597,"line":3622},[3595,6830,3638],{"emptyLinePlaceholder":3637},[3595,6832,6833],{"class":3597,"line":3628},[3595,6834,6835],{"class":4182},"# 1. Синхронний клієнт для простих тестів\n",[3595,6837,6838,6840,6842,6844,6846,6849],{"class":3597,"line":3634},[3595,6839,6149],{"class":4064},[3595,6841,4483],{"class":4176},[3595,6843,6296],{"class":4188},[3595,6845,4506],{"class":4176},[3595,6847,6848],{"class":4067},"\"module\"",[3595,6850,4534],{"class":4176},[3595,6852,6853,6855,6858],{"class":3597,"line":3641},[3595,6854,4916],{"class":4104},[3595,6856,6857],{"class":4064}," client",[3595,6859,4922],{"class":4176},[3595,6861,6862],{"class":3597,"line":3647},[3595,6863,6864],{"class":4067},"    \"\"\"Фікстура для створення синхронного тестового клієнта.\"\"\"\n",[3595,6866,6867],{"class":3597,"line":3653},[3595,6868,6869],{"class":4182},"    # Context manager гарантує спрацювання подій startup\u002Fshutdown застосунку\n",[3595,6871,6872,6874,6877,6879],{"class":3597,"line":3659},[3595,6873,5120],{"class":4433},[3595,6875,6876],{"class":4176}," TestClient(app) ",[3595,6878,5126],{"class":4433},[3595,6880,6881],{"class":4176}," c:\n",[3595,6883,6884,6887],{"class":3597,"line":3664},[3595,6885,6886],{"class":4433},"        yield",[3595,6888,6889],{"class":4176}," c\n",[3595,6891,6892],{"class":3597,"line":3670},[3595,6893,3638],{"emptyLinePlaceholder":3637},[3595,6895,6896],{"class":3597,"line":3676},[3595,6897,6898],{"class":4182},"# 2. Асинхронний клієнт для тестування асинхронних ендпоінтів\n",[3595,6900,6901,6903,6905,6907,6909,6911],{"class":3597,"line":3681},[3595,6902,6149],{"class":4064},[3595,6904,4483],{"class":4176},[3595,6906,6296],{"class":4188},[3595,6908,4506],{"class":4176},[3595,6910,6848],{"class":4067},[3595,6912,4534],{"class":4176},[3595,6914,6915,6918,6921,6924],{"class":3597,"line":3687},[3595,6916,6917],{"class":4104},"async",[3595,6919,6920],{"class":4104}," def",[3595,6922,6923],{"class":4064}," async_client",[3595,6925,4922],{"class":4176},[3595,6927,6928],{"class":3597,"line":3693},[3595,6929,6930],{"class":4067},"    \"\"\"Фікстура для створення асинхронного тестового клієнта.\"\"\"\n",[3595,6932,6933],{"class":3597,"line":3699},[3595,6934,6935],{"class":4182},"    # AsyncClient приймає наш app і base_url\n",[3595,6937,6938,6941,6944,6947,6950,6953,6956,6958,6961,6963,6965],{"class":3597,"line":3705},[3595,6939,6940],{"class":4433},"    async",[3595,6942,6943],{"class":4433}," with",[3595,6945,6946],{"class":4176}," AsyncClient(",[3595,6948,6949],{"class":4188},"app",[3595,6951,6952],{"class":4176},"=app, ",[3595,6954,6955],{"class":4188},"base_url",[3595,6957,4506],{"class":4176},[3595,6959,6960],{"class":4067},"\"http:\u002F\u002Ftest\"",[3595,6962,6448],{"class":4176},[3595,6964,5126],{"class":4433},[3595,6966,6967],{"class":4176}," ac:\n",[3595,6969,6970,6972],{"class":3597,"line":3711},[3595,6971,6886],{"class":4433},[3595,6973,6974],{"class":4176}," ac\n",[4384,6976,6977,6985],{},[3551,6978,6979],{},[3555,6980,6981,6982,6984],{},"Чому важливо використовувати ",[3563,6983,5233],{}," при створенні TestClient?",[3551,6986,6987,6988,6991,6992,6995,6996,6999],{},"Коли ви використовуєте ",[3563,6989,6990],{},"with TestClient(app) as client",", FastAPI запускає ваші життєві події (Lifespan events) — логіку, яка прописана в ",[3563,6993,6994],{},"@app.lifespan"," (наприклад, ініціалізація пулу підключень бази даних чи завантаження ML-моделей). Якщо ви створите клієнт просто як ",[3563,6997,6998],{},"client = TestClient(app)",", ці події не запустяться, і ваші тести впадуть через відсутність необхідних ресурсів.",[3822,7001,7003],{"id":7002},"написання-перших-тестів-для-ендпоінтів","Написання перших тестів для ендпоінтів",[3551,7005,7006,7007,4856],{},"Створимо найпростіший ендпоінт перевірки працездатності системи (Health Check) в ",[3563,7008,7009],{},"app\u002Fmain.py",[3586,7011,7013],{"className":4423,"code":7012,"filename":7009,"language":4426,"meta":3591,"style":3591},"from fastapi import FastAPI\n\napp = FastAPI()\n\n@app.get(\"\u002Fhealth\")\ndef health_check():\n    return {\"status\": \"healthy\", \"version\": \"1.0.0\"}\n",[3563,7014,7015,7027,7031,7036,7040,7052,7061],{"__ignoreMap":3591},[3595,7016,7017,7019,7022,7024],{"class":3597,"line":3598},[3595,7018,4434],{"class":4433},[3595,7020,7021],{"class":4176}," fastapi ",[3595,7023,4440],{"class":4433},[3595,7025,7026],{"class":4176}," FastAPI\n",[3595,7028,7029],{"class":3597,"line":3604},[3595,7030,3638],{"emptyLinePlaceholder":3637},[3595,7032,7033],{"class":3597,"line":3610},[3595,7034,7035],{"class":4176},"app = FastAPI()\n",[3595,7037,7038],{"class":3597,"line":3616},[3595,7039,3638],{"emptyLinePlaceholder":3637},[3595,7041,7042,7045,7047,7050],{"class":3597,"line":3622},[3595,7043,7044],{"class":4064},"@app.get",[3595,7046,4483],{"class":4176},[3595,7048,7049],{"class":4067},"\"\u002Fhealth\"",[3595,7051,4534],{"class":4176},[3595,7053,7054,7056,7059],{"class":3597,"line":3628},[3595,7055,4916],{"class":4104},[3595,7057,7058],{"class":4064}," health_check",[3595,7060,4922],{"class":4176},[3595,7062,7063,7065,7068,7071,7073,7076,7078,7081,7083,7086],{"class":3597,"line":3634},[3595,7064,6192],{"class":4433},[3595,7066,7067],{"class":4176}," {",[3595,7069,7070],{"class":4067},"\"status\"",[3595,7072,4940],{"class":4176},[3595,7074,7075],{"class":4067},"\"healthy\"",[3595,7077,4513],{"class":4176},[3595,7079,7080],{"class":4067},"\"version\"",[3595,7082,4940],{"class":4176},[3595,7084,7085],{"class":4067},"\"1.0.0\"",[3595,7087,5909],{"class":4176},[3551,7089,7090,7091,4856],{},"Тепер напишемо для нього синхронний тест у файлі ",[3563,7092,7093],{},"tests\u002Fintegration\u002Ftest_health.py",[3586,7095,7097],{"className":4423,"code":7096,"filename":7093,"language":4426,"meta":3591,"style":3591},"import pytest\n\n@pytest.mark.integration\ndef test_health_check_sync(client):\n    \"\"\"Синхронний тест ендпоінту \u002Fhealth.\"\"\"\n    response = client.get(\"\u002Fhealth\")\n\n    assert response.status_code == 200\n    assert response.json() == {\"status\": \"healthy\", \"version\": \"1.0.0\"}\n",[3563,7098,7099,7105,7109,7114,7128,7133,7142,7146,7156],{"__ignoreMap":3591},[3595,7100,7101,7103],{"class":3597,"line":3598},[3595,7102,4440],{"class":4433},[3595,7104,4879],{"class":4176},[3595,7106,7107],{"class":3597,"line":3604},[3595,7108,3638],{"emptyLinePlaceholder":3637},[3595,7110,7111],{"class":3597,"line":3610},[3595,7112,7113],{"class":4064},"@pytest.mark.integration\n",[3595,7115,7116,7118,7121,7123,7126],{"class":3597,"line":3616},[3595,7117,4916],{"class":4104},[3595,7119,7120],{"class":4064}," test_health_check_sync",[3595,7122,4483],{"class":4176},[3595,7124,7125],{"class":4188},"client",[3595,7127,4489],{"class":4176},[3595,7129,7130],{"class":3597,"line":3622},[3595,7131,7132],{"class":4067},"    \"\"\"Синхронний тест ендпоінту \u002Fhealth.\"\"\"\n",[3595,7134,7135,7138,7140],{"class":3597,"line":3628},[3595,7136,7137],{"class":4176},"    response = client.get(",[3595,7139,7049],{"class":4067},[3595,7141,4534],{"class":4176},[3595,7143,7144],{"class":3597,"line":3634},[3595,7145,3638],{"emptyLinePlaceholder":3637},[3595,7147,7148,7150,7153],{"class":3597,"line":3641},[3595,7149,5008],{"class":4433},[3595,7151,7152],{"class":4176}," response.status_code == ",[3595,7154,7155],{"class":4509},"200\n",[3595,7157,7158,7160,7163,7165,7167,7169,7171,7173,7175,7177],{"class":3597,"line":3647},[3595,7159,5008],{"class":4433},[3595,7161,7162],{"class":4176}," response.json() == {",[3595,7164,7070],{"class":4067},[3595,7166,4940],{"class":4176},[3595,7168,7075],{"class":4067},[3595,7170,4513],{"class":4176},[3595,7172,7080],{"class":4067},[3595,7174,4940],{"class":4176},[3595,7176,7085],{"class":4067},[3595,7178,5909],{"class":4176},[3551,7180,7181,7182,4856],{},"Для асинхронного тесту того самого ендпоінту ми маємо позначити тест як асинхронний і використати ",[3563,7183,7184],{},"await",[3586,7186,7188],{"className":4423,"code":7187,"filename":7093,"language":4426,"meta":3591,"style":3591},"# Продовження файлу tests\u002Fintegration\u002Ftest_health.py\n\n@pytest.mark.integration\nasync def test_health_check_async(async_client):\n    \"\"\"Асинхронний тест ендпоінту \u002Fhealth за допомогою AsyncClient.\"\"\"\n    response = await async_client.get(\"\u002Fhealth\")\n\n    assert response.status_code == 200\n    assert response.json()[\"status\"] == \"healthy\"\n",[3563,7189,7190,7195,7199,7203,7219,7224,7238,7242,7250],{"__ignoreMap":3591},[3595,7191,7192],{"class":3597,"line":3598},[3595,7193,7194],{"class":4182},"# Продовження файлу tests\u002Fintegration\u002Ftest_health.py\n",[3595,7196,7197],{"class":3597,"line":3604},[3595,7198,3638],{"emptyLinePlaceholder":3637},[3595,7200,7201],{"class":3597,"line":3610},[3595,7202,7113],{"class":4064},[3595,7204,7205,7207,7209,7212,7214,7217],{"class":3597,"line":3616},[3595,7206,6917],{"class":4104},[3595,7208,6920],{"class":4104},[3595,7210,7211],{"class":4064}," test_health_check_async",[3595,7213,4483],{"class":4176},[3595,7215,7216],{"class":4188},"async_client",[3595,7218,4489],{"class":4176},[3595,7220,7221],{"class":3597,"line":3622},[3595,7222,7223],{"class":4067},"    \"\"\"Асинхронний тест ендпоінту \u002Fhealth за допомогою AsyncClient.\"\"\"\n",[3595,7225,7226,7229,7231,7234,7236],{"class":3597,"line":3628},[3595,7227,7228],{"class":4176},"    response = ",[3595,7230,7184],{"class":4433},[3595,7232,7233],{"class":4176}," async_client.get(",[3595,7235,7049],{"class":4067},[3595,7237,4534],{"class":4176},[3595,7239,7240],{"class":3597,"line":3634},[3595,7241,3638],{"emptyLinePlaceholder":3637},[3595,7243,7244,7246,7248],{"class":3597,"line":3641},[3595,7245,5008],{"class":4433},[3595,7247,7152],{"class":4176},[3595,7249,7155],{"class":4509},[3595,7251,7252,7254,7257,7259,7261],{"class":3597,"line":3647},[3595,7253,5008],{"class":4433},[3595,7255,7256],{"class":4176}," response.json()[",[3595,7258,7070],{"class":4067},[3595,7260,5206],{"class":4176},[3595,7262,7263],{"class":4067},"\"healthy\"\n",[3551,7265,7266,7267,7270,7271,7274],{},"Зверніть увагу на об'єкт відповіді (",[3563,7268,7269],{},"response","). Він є екземпляром класу ",[3563,7272,7273],{},"httpx.Response"," і надає такі корисні властивості та методи:",[6298,7276,7277,7281,7289,7297],{},[6301,7278,7280],{"name":7279,"type":4584},"status_code","HTTP-код відповіді (наприклад, 200, 201, 400, 401, 404, 500).",[6301,7282,7285,7286,3929],{"name":7283,"type":7284},"json()","method","Парсить тіло відповіді як JSON і повертає стандартний Python dict або list. Якщо тіло не є валідним JSON, виникає виключення ",[3563,7287,7288],{},"json.JSONDecodeError",[6301,7290,7293,7294,5476],{"name":7291,"type":7292},"headers","dict-like","Словник із заголовками відповіді (наприклад, ",[3563,7295,7296],{},"response.headers[\"content-type\"]",[6301,7298,7299],{"name":6577,"type":4497},"Повертає сире тіло відповіді у вигляді рядка (корисно для налагодження, якщо сервер повернув HTML-сторінку помилки замість JSON).",[3568,7301],{},[3571,7303,7305],{"id":7304},"_6-робота-з-тестовою-базою-даних-database-fixtures-та-транзакційні-відкати","6. Робота з тестовою базою даних (Database Fixtures) та транзакційні відкати",[3551,7307,7308,7309],{},"Тестування ендпоінтів, які працюють з базою даних, є однією з найскладніших задач. Тут постає серйозний виклик: ",[3555,7310,7311],{},"як зробити так, щоб тести не залежали один від одного та виконувалися швидко?",[3551,7313,7314,7315,7318],{},"Якщо перший тест створює завдання з назвою ",[3563,7316,7317],{},"\"Task 1\"",", а другий тест намагається отримати список усіх завдань і очікує там порожній список — другий тест впаде через залишки даних від першого.",[3822,7320,7322],{"id":7321},"стратегії-очищення-бази-даних","Стратегії очищення бази даних",[3551,7324,7325],{},"В екосистемі .NET розробники використовують кілька підходів:",[7327,7328,7329,7335],"ul",{},[3795,7330,7331,7334],{},[3563,7332,7333],{},"InMemoryDatabase"," (швидко, але не підтримує специфічні фічі на кшталт сирих SQL-запитів чи JSONB-колон PostgreSQL).",[3795,7336,7337,7340,7341,7344],{},[3563,7338,7339],{},"Respawn"," — бібліотека, яка очищає таблиці (виконує ",[3563,7342,7343],{},"TRUNCATE",") перед або після кожного тесту.",[3551,7346,7347,7348,3929],{},"У Python найбільш елегантним, надійним та швидким підходом є ",[3555,7349,7350],{},"транзакційний відкат (Transaction Rollback)",[3583,7352,7353],{},[3586,7354,7356],{"className":3588,"code":7355,"language":3590,"meta":3591,"style":3591},"@startuml\nskinparam style plain\nskinparam backgroundColor #ffffff\nskinparam ArrowColor #3b82f6\n\nparticipant \"pytest (Test)\" as test #f8fafc\nparticipant \"SQLAlchemy Session\" as session #e0f2fe\ndatabase \"PostgreSQL\" as db #f3e5f5\n\n== Setup ==\ntest -> session: Почати транзакцію (transaction.begin)\nactivate session\nsession -> db: BEGIN TRANSACTION\n\n== Test Execution ==\ntest -> session: session.add(Task)\nsession -> db: INSERT INTO tasks ...\ntest -> session: session.commit() (імітація)\nnote right of session\n  SQLAlchemy перехоплює комміт\n  і не відправляє його в БД реально,\n  або використовує SAVEPOINT\nend note\n\n== Teardown ==\ntest -> session: Відкат (transaction.rollback)\nsession -> db: ROLLBACK TRANSACTION\ndeactivate session\nnote right of db\n  Всі зміни скасовано!\n  База даних повертається\n  до початкового стану.\nend note\n@enduml\n",[3563,7357,7358,7362,7366,7370,7374,7378,7382,7387,7392,7396,7401,7406,7411,7416,7420,7425,7430,7435,7440,7445,7450,7455,7460,7464,7468,7473,7478,7483,7488,7493,7498,7503,7508,7512],{"__ignoreMap":3591},[3595,7359,7360],{"class":3597,"line":3598},[3595,7361,3601],{},[3595,7363,7364],{"class":3597,"line":3604},[3595,7365,3607],{},[3595,7367,7368],{"class":3597,"line":3610},[3595,7369,3613],{},[3595,7371,7372],{"class":3597,"line":3616},[3595,7373,3619],{},[3595,7375,7376],{"class":3597,"line":3622},[3595,7377,3638],{"emptyLinePlaceholder":3637},[3595,7379,7380],{"class":3597,"line":3628},[3595,7381,6640],{},[3595,7383,7384],{"class":3597,"line":3634},[3595,7385,7386],{},"participant \"SQLAlchemy Session\" as session #e0f2fe\n",[3595,7388,7389],{"class":3597,"line":3641},[3595,7390,7391],{},"database \"PostgreSQL\" as db #f3e5f5\n",[3595,7393,7394],{"class":3597,"line":3647},[3595,7395,3638],{"emptyLinePlaceholder":3637},[3595,7397,7398],{"class":3597,"line":3653},[3595,7399,7400],{},"== Setup ==\n",[3595,7402,7403],{"class":3597,"line":3659},[3595,7404,7405],{},"test -> session: Почати транзакцію (transaction.begin)\n",[3595,7407,7408],{"class":3597,"line":3664},[3595,7409,7410],{},"activate session\n",[3595,7412,7413],{"class":3597,"line":3670},[3595,7414,7415],{},"session -> db: BEGIN TRANSACTION\n",[3595,7417,7418],{"class":3597,"line":3676},[3595,7419,3638],{"emptyLinePlaceholder":3637},[3595,7421,7422],{"class":3597,"line":3681},[3595,7423,7424],{},"== Test Execution ==\n",[3595,7426,7427],{"class":3597,"line":3687},[3595,7428,7429],{},"test -> session: session.add(Task)\n",[3595,7431,7432],{"class":3597,"line":3693},[3595,7433,7434],{},"session -> db: INSERT INTO tasks ...\n",[3595,7436,7437],{"class":3597,"line":3699},[3595,7438,7439],{},"test -> session: session.commit() (імітація)\n",[3595,7441,7442],{"class":3597,"line":3705},[3595,7443,7444],{},"note right of session\n",[3595,7446,7447],{"class":3597,"line":3711},[3595,7448,7449],{},"  SQLAlchemy перехоплює комміт\n",[3595,7451,7452],{"class":3597,"line":3716},[3595,7453,7454],{},"  і не відправляє його в БД реально,\n",[3595,7456,7457],{"class":3597,"line":3722},[3595,7458,7459],{},"  або використовує SAVEPOINT\n",[3595,7461,7462],{"class":3597,"line":3728},[3595,7463,3708],{},[3595,7465,7466],{"class":3597,"line":3734},[3595,7467,3638],{"emptyLinePlaceholder":3637},[3595,7469,7470],{"class":3597,"line":3740},[3595,7471,7472],{},"== Teardown ==\n",[3595,7474,7475],{"class":3597,"line":3745},[3595,7476,7477],{},"test -> session: Відкат (transaction.rollback)\n",[3595,7479,7480],{"class":3597,"line":3750},[3595,7481,7482],{},"session -> db: ROLLBACK TRANSACTION\n",[3595,7484,7485],{"class":3597,"line":3756},[3595,7486,7487],{},"deactivate session\n",[3595,7489,7490],{"class":3597,"line":3762},[3595,7491,7492],{},"note right of db\n",[3595,7494,7495],{"class":3597,"line":3768},[3595,7496,7497],{},"  Всі зміни скасовано!\n",[3595,7499,7500],{"class":3597,"line":3774},[3595,7501,7502],{},"  База даних повертається\n",[3595,7504,7505],{"class":3597,"line":3779},[3595,7506,7507],{},"  до початкового стану.\n",[3595,7509,7510],{"class":3597,"line":3784},[3595,7511,3708],{},[3595,7513,7514],{"class":3597,"line":5137},[3595,7515,3787],{},[3551,7517,7518],{},"Суть підходу:",[3792,7520,7521,7524,7527,7534],{},[3795,7522,7523],{},"Перед стартом тесту ми відкриваємо транзакцію в базі даних.",[3795,7525,7526],{},"Під час тесту всі операції запису (INSERT, UPDATE, DELETE) виконуються в межах цієї транзакції.",[3795,7528,7529,7530,7533],{},"Після завершення тесту (неважливо, успішного чи ні) ми робимо ",[3555,7531,7532],{},"ROLLBACK"," цієї транзакції.",[3795,7535,7536],{},"Дані ніколи фізично не зберігаються в БД назавжди, і кожен тест бачить абсолютно чисту схему.",[3822,7538,7540],{"id":7539},"реалізація-фікстур-бази-даних-у-conftestpy","Реалізація фікстур бази даних у conftest.py",[3551,7542,7543],{},"Давайте реалізуємо цей механізм за допомогою SQLAlchemy та FastAPI. Ми будемо використовувати асинхронний драйвер.",[3586,7545,7547],{"className":4423,"code":7546,"filename":6776,"language":4426,"meta":3591,"style":3591},"import pytest\nfrom sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession\nfrom app.database import Base, get_db\nfrom app.main import app\n\n# Використовуємо окрему тестову базу даних (можна PostgreSQL або SQLite в пам'яті для швидкості)\nTEST_DATABASE_URL = \"postgresql+asyncpg:\u002F\u002Fpostgres:postgres@localhost:5432\u002Ftest_taskforge\"\n\n# Створюємо тестовий Engine\nengine = create_async_engine(TEST_DATABASE_URL, echo=False)\nTestingSessionLocal = async_sessionmaker(autocommit=False, autoflush=False, bind=engine, expire_on_commit=False)\n\n@pytest.fixture(scope=\"session\", autouse=True)\nasync def setup_test_db():\n    \"\"\"Фікстура, що створює схему БД перед тестами та видаляє її після.\"\"\"\n    async with engine.begin() as conn:\n        # Створюємо всі таблиці\n        await conn.run_sync(Base.metadata.create_all)\n\n    yield\n\n    async with engine.begin() as conn:\n        # Видаляємо всі таблиці після завершення тестової сесії\n        await conn.run_sync(Base.metadata.drop_all)\n\n@pytest.fixture(scope=\"function\")\nasync def db_session() -> AsyncSession:\n    \"\"\"Фікстура, яка створює сесію БД для кожного тесту та робить rollback наприкінці.\"\"\"\n    connection = await engine.connect()\n    # Починаємо зовнішню транзакцію\n    transaction = await connection.begin()\n\n    # Створюємо сесію, прив'язану до цього з'єднання\n    session = TestingSessionLocal(bind=connection)\n\n    yield session\n\n    # Закриваємо сесію та відкочуємо транзакцію\n    await session.close()\n    await transaction.rollback()\n    await connection.close()\n",[3563,7548,7549,7555,7567,7579,7590,7594,7599,7607,7611,7616,7630,7668,7672,7696,7707,7712,7726,7731,7739,7743,7748,7752,7764,7769,7776,7780,7794,7806,7811,7821,7826,7836,7840,7845,7855,7859,7866,7870,7875,7883,7891],{"__ignoreMap":3591},[3595,7550,7551,7553],{"class":3597,"line":3598},[3595,7552,4440],{"class":4433},[3595,7554,4879],{"class":4176},[3595,7556,7557,7559,7562,7564],{"class":3597,"line":3604},[3595,7558,4434],{"class":4433},[3595,7560,7561],{"class":4176}," sqlalchemy.ext.asyncio ",[3595,7563,4440],{"class":4433},[3595,7565,7566],{"class":4176}," create_async_engine, async_sessionmaker, AsyncSession\n",[3595,7568,7569,7571,7574,7576],{"class":3597,"line":3610},[3595,7570,4434],{"class":4433},[3595,7572,7573],{"class":4176}," app.database ",[3595,7575,4440],{"class":4433},[3595,7577,7578],{"class":4176}," Base, get_db\n",[3595,7580,7581,7583,7585,7587],{"class":3597,"line":3616},[3595,7582,4434],{"class":4433},[3595,7584,6818],{"class":4176},[3595,7586,4440],{"class":4433},[3595,7588,7589],{"class":4176}," app\n",[3595,7591,7592],{"class":3597,"line":3622},[3595,7593,3638],{"emptyLinePlaceholder":3637},[3595,7595,7596],{"class":3597,"line":3628},[3595,7597,7598],{"class":4182},"# Використовуємо окрему тестову базу даних (можна PostgreSQL або SQLite в пам'яті для швидкості)\n",[3595,7600,7601,7604],{"class":3597,"line":3634},[3595,7602,7603],{"class":4176},"TEST_DATABASE_URL = ",[3595,7605,7606],{"class":4067},"\"postgresql+asyncpg:\u002F\u002Fpostgres:postgres@localhost:5432\u002Ftest_taskforge\"\n",[3595,7608,7609],{"class":3597,"line":3641},[3595,7610,3638],{"emptyLinePlaceholder":3637},[3595,7612,7613],{"class":3597,"line":3647},[3595,7614,7615],{"class":4182},"# Створюємо тестовий Engine\n",[3595,7617,7618,7621,7624,7626,7628],{"class":3597,"line":3653},[3595,7619,7620],{"class":4176},"engine = create_async_engine(TEST_DATABASE_URL, ",[3595,7622,7623],{"class":4188},"echo",[3595,7625,4506],{"class":4176},[3595,7627,5568],{"class":4104},[3595,7629,4534],{"class":4176},[3595,7631,7632,7635,7638,7640,7642,7644,7647,7649,7651,7653,7656,7659,7662,7664,7666],{"class":3597,"line":3659},[3595,7633,7634],{"class":4176},"TestingSessionLocal = async_sessionmaker(",[3595,7636,7637],{"class":4188},"autocommit",[3595,7639,4506],{"class":4176},[3595,7641,5568],{"class":4104},[3595,7643,4513],{"class":4176},[3595,7645,7646],{"class":4188},"autoflush",[3595,7648,4506],{"class":4176},[3595,7650,5568],{"class":4104},[3595,7652,4513],{"class":4176},[3595,7654,7655],{"class":4188},"bind",[3595,7657,7658],{"class":4176},"=engine, ",[3595,7660,7661],{"class":4188},"expire_on_commit",[3595,7663,4506],{"class":4176},[3595,7665,5568],{"class":4104},[3595,7667,4534],{"class":4176},[3595,7669,7670],{"class":3597,"line":3664},[3595,7671,3638],{"emptyLinePlaceholder":3637},[3595,7673,7674,7676,7678,7680,7682,7685,7687,7690,7692,7694],{"class":3597,"line":3670},[3595,7675,6149],{"class":4064},[3595,7677,4483],{"class":4176},[3595,7679,6296],{"class":4188},[3595,7681,4506],{"class":4176},[3595,7683,7684],{"class":4067},"\"session\"",[3595,7686,4513],{"class":4176},[3595,7688,7689],{"class":4188},"autouse",[3595,7691,4506],{"class":4176},[3595,7693,5525],{"class":4104},[3595,7695,4534],{"class":4176},[3595,7697,7698,7700,7702,7705],{"class":3597,"line":3676},[3595,7699,6917],{"class":4104},[3595,7701,6920],{"class":4104},[3595,7703,7704],{"class":4064}," setup_test_db",[3595,7706,4922],{"class":4176},[3595,7708,7709],{"class":3597,"line":3681},[3595,7710,7711],{"class":4067},"    \"\"\"Фікстура, що створює схему БД перед тестами та видаляє її після.\"\"\"\n",[3595,7713,7714,7716,7718,7721,7723],{"class":3597,"line":3687},[3595,7715,6940],{"class":4433},[3595,7717,6943],{"class":4433},[3595,7719,7720],{"class":4176}," engine.begin() ",[3595,7722,5126],{"class":4433},[3595,7724,7725],{"class":4176}," conn:\n",[3595,7727,7728],{"class":3597,"line":3693},[3595,7729,7730],{"class":4182},"        # Створюємо всі таблиці\n",[3595,7732,7733,7736],{"class":3597,"line":3699},[3595,7734,7735],{"class":4433},"        await",[3595,7737,7738],{"class":4176}," conn.run_sync(Base.metadata.create_all)\n",[3595,7740,7741],{"class":3597,"line":3705},[3595,7742,3638],{"emptyLinePlaceholder":3637},[3595,7744,7745],{"class":3597,"line":3711},[3595,7746,7747],{"class":4433},"    yield\n",[3595,7749,7750],{"class":3597,"line":3716},[3595,7751,3638],{"emptyLinePlaceholder":3637},[3595,7753,7754,7756,7758,7760,7762],{"class":3597,"line":3722},[3595,7755,6940],{"class":4433},[3595,7757,6943],{"class":4433},[3595,7759,7720],{"class":4176},[3595,7761,5126],{"class":4433},[3595,7763,7725],{"class":4176},[3595,7765,7766],{"class":3597,"line":3728},[3595,7767,7768],{"class":4182},"        # Видаляємо всі таблиці після завершення тестової сесії\n",[3595,7770,7771,7773],{"class":3597,"line":3734},[3595,7772,7735],{"class":4433},[3595,7774,7775],{"class":4176}," conn.run_sync(Base.metadata.drop_all)\n",[3595,7777,7778],{"class":3597,"line":3740},[3595,7779,3638],{"emptyLinePlaceholder":3637},[3595,7781,7782,7784,7786,7788,7790,7792],{"class":3597,"line":3745},[3595,7783,6149],{"class":4064},[3595,7785,4483],{"class":4176},[3595,7787,6296],{"class":4188},[3595,7789,4506],{"class":4176},[3595,7791,6408],{"class":4067},[3595,7793,4534],{"class":4176},[3595,7795,7796,7798,7800,7803],{"class":3597,"line":3750},[3595,7797,6917],{"class":4104},[3595,7799,6920],{"class":4104},[3595,7801,7802],{"class":4064}," db_session",[3595,7804,7805],{"class":4176},"() -> AsyncSession:\n",[3595,7807,7808],{"class":3597,"line":3756},[3595,7809,7810],{"class":4067},"    \"\"\"Фікстура, яка створює сесію БД для кожного тесту та робить rollback наприкінці.\"\"\"\n",[3595,7812,7813,7816,7818],{"class":3597,"line":3762},[3595,7814,7815],{"class":4176},"    connection = ",[3595,7817,7184],{"class":4433},[3595,7819,7820],{"class":4176}," engine.connect()\n",[3595,7822,7823],{"class":3597,"line":3768},[3595,7824,7825],{"class":4182},"    # Починаємо зовнішню транзакцію\n",[3595,7827,7828,7831,7833],{"class":3597,"line":3774},[3595,7829,7830],{"class":4176},"    transaction = ",[3595,7832,7184],{"class":4433},[3595,7834,7835],{"class":4176}," connection.begin()\n",[3595,7837,7838],{"class":3597,"line":3779},[3595,7839,3638],{"emptyLinePlaceholder":3637},[3595,7841,7842],{"class":3597,"line":3784},[3595,7843,7844],{"class":4182},"    # Створюємо сесію, прив'язану до цього з'єднання\n",[3595,7846,7847,7850,7852],{"class":3597,"line":5137},[3595,7848,7849],{"class":4176},"    session = TestingSessionLocal(",[3595,7851,7655],{"class":4188},[3595,7853,7854],{"class":4176},"=connection)\n",[3595,7856,7857],{"class":3597,"line":5142},[3595,7858,3638],{"emptyLinePlaceholder":3637},[3595,7860,7861,7863],{"class":3597,"line":5148},[3595,7862,6477],{"class":4433},[3595,7864,7865],{"class":4176}," session\n",[3595,7867,7868],{"class":3597,"line":5154},[3595,7869,3638],{"emptyLinePlaceholder":3637},[3595,7871,7872],{"class":3597,"line":5167},[3595,7873,7874],{"class":4182},"    # Закриваємо сесію та відкочуємо транзакцію\n",[3595,7876,7877,7880],{"class":3597,"line":5192},[3595,7878,7879],{"class":4433},"    await",[3595,7881,7882],{"class":4176}," session.close()\n",[3595,7884,7886,7888],{"class":3597,"line":7885},40,[3595,7887,7879],{"class":4433},[3595,7889,7890],{"class":4176}," transaction.rollback()\n",[3595,7892,7894,7896],{"class":3597,"line":7893},41,[3595,7895,7879],{"class":4433},[3595,7897,7898],{"class":4176}," connection.close()\n",[4384,7900,7901,7920],{},[3551,7902,7903],{},[3555,7904,7905,7906,7909,7910,5257,7913,7909,7916,7919],{},"Чому ",[3563,7907,7908],{},"scope=\"session\""," для ",[3563,7911,7912],{},"setup_test_db",[3563,7914,7915],{},"scope=\"function\"",[3563,7917,7918],{},"db_session","?",[3551,7921,7922,7923,7926,7927,7929,7930,7932],{},"Створення таблиць (",[3563,7924,7925],{},"create_all",") — це повільна операція, яка займає сотні мілісекунд. Робити це перед кожним тестом недоцільно. Тому ми робимо це один раз на початку всієї тестової сесії (",[3563,7928,7908],{},").\nА от саму сесію та транзакцію ми створюємо окремо для кожного тесту (",[3563,7931,7915],{},"), щоб гарантувати ізоляцію даних та відкат змін після кожного окремого тесту.",[3568,7934],{},[3571,7936,7938],{"id":7937},"_7-dependency-overrides-перевизначення-залежностей-у-fastapi","7. Dependency Overrides: Перевизначення залежностей у FastAPI",[3551,7940,7941,7942,7944,7945],{},"Тепер, коли у нас є фікстура ",[3563,7943,7918],{},", яка вміє відкочувати транзакції, постає питання: ",[3555,7946,7947],{},"як змусити FastAPI використовувати саме цю тестову сесію замість робочої під час виконання HTTP-запитів через TestClient?",[3551,7949,7950,7951,7953],{},"В ASP.NET Core для цього довелося б налаштовувати ",[3563,7952,3565],{}," та переписувати сервіси в DI-контейнері:",[3586,7955,7958],{"className":5769,"code":7956,"filename":7957,"language":5772,"meta":3591,"style":3591},"\u002F\u002F Перевизначення у WebApplicationFactory\nbuilder.ConfigureServices(services => {\n    var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions\u003CAppDbContext>));\n    services.Remove(descriptor);\n    services.AddDbContext\u003CAppDbContext>(options => options.UseInMemoryDatabase(\"TestDb\"));\n});\n","ASP.NET Core (C#)",[3563,7959,7960,7965,7983,8034,8051,8087],{"__ignoreMap":3591},[3595,7961,7962],{"class":3597,"line":3598},[3595,7963,7964],{"class":4182},"\u002F\u002F Перевизначення у WebApplicationFactory\n",[3595,7966,7967,7970,7972,7975,7977,7980],{"class":3597,"line":3604},[3595,7968,7969],{"class":4188},"builder",[3595,7971,3929],{"class":4176},[3595,7973,7974],{"class":4064},"ConfigureServices",[3595,7976,4483],{"class":4176},[3595,7978,7979],{"class":4188},"services",[3595,7981,7982],{"class":4176}," => {\n",[3595,7984,7985,7987,7990,7992,7994,7996,7999,8001,8004,8007,8009,8011,8014,8017,8020,8022,8025,8028,8031],{"class":3597,"line":3610},[3595,7986,5878],{"class":4104},[3595,7988,7989],{"class":4188}," descriptor",[3595,7991,4192],{"class":4176},[3595,7993,7979],{"class":4188},[3595,7995,3929],{"class":4176},[3595,7997,7998],{"class":4064},"SingleOrDefault",[3595,8000,4483],{"class":4176},[3595,8002,8003],{"class":4188},"d",[3595,8005,8006],{"class":4176}," => ",[3595,8008,8003],{"class":4188},[3595,8010,3929],{"class":4176},[3595,8012,8013],{"class":4188},"ServiceType",[3595,8015,8016],{"class":4176}," == ",[3595,8018,8019],{"class":4104},"typeof",[3595,8021,4483],{"class":4176},[3595,8023,8024],{"class":4479},"DbContextOptions",[3595,8026,8027],{"class":4176},"\u003C",[3595,8029,8030],{"class":4479},"AppDbContext",[3595,8032,8033],{"class":4176},">));\n",[3595,8035,8036,8039,8041,8044,8046,8049],{"class":3597,"line":3616},[3595,8037,8038],{"class":4188},"    services",[3595,8040,3929],{"class":4176},[3595,8042,8043],{"class":4064},"Remove",[3595,8045,4483],{"class":4176},[3595,8047,8048],{"class":4188},"descriptor",[3595,8050,5899],{"class":4176},[3595,8052,8053,8055,8057,8060,8062,8064,8067,8070,8072,8074,8076,8079,8081,8084],{"class":3597,"line":3622},[3595,8054,8038],{"class":4188},[3595,8056,3929],{"class":4176},[3595,8058,8059],{"class":4064},"AddDbContext",[3595,8061,8027],{"class":4176},[3595,8063,8030],{"class":4479},[3595,8065,8066],{"class":4176},">(",[3595,8068,8069],{"class":4188},"options",[3595,8071,8006],{"class":4176},[3595,8073,8069],{"class":4188},[3595,8075,3929],{"class":4176},[3595,8077,8078],{"class":4064},"UseInMemoryDatabase",[3595,8080,4483],{"class":4176},[3595,8082,8083],{"class":4067},"\"TestDb\"",[3595,8085,8086],{"class":4176},"));\n",[3595,8088,8089],{"class":3597,"line":3628},[3595,8090,8091],{"class":4176},"});\n",[3551,8093,8094,8095,8098,8099,3929],{},"У FastAPI впровадження залежностей є частиною самого фреймворку, а не зовнішнього IoC-контейнера. Перевизначити будь-яку залежність (наприклад, функцію ",[3563,8096,8097],{},"get_db",", яка повертає сесію бази даних) можна через один простий словник: ",[3555,8100,8101],{},[3563,8102,3973],{},[3551,8104,8105],{},"Давайте створимо фікстуру тестового клієнта, яка автоматично підміняє залежність від бази даних:",[3586,8107,8109],{"className":4423,"code":8108,"filename":6776,"language":4426,"meta":3591,"style":3591},"# Продовження tests\u002Fconftest.py\n\nfrom httpx import AsyncClient\n\n# Наш реальний провайдер сесії БД, який використовується в ендпоінтах\n# Наприклад:\n# async def get_db():\n#     async with SessionLocal() as session:\n#         yield session\n\n@pytest.fixture(scope=\"function\")\nasync def client(db_session: AsyncSession):\n    \"\"\"Фікстура клієнта, яка підміняє робочу БД на тестову для кожного запиту.\"\"\"\n\n    # Створюємо функцію-заглушку, яка повертатиме нашу тестову сесію\n    async def _override_get_db():\n        yield db_session\n\n    # Записуємо перевизначення в словник FastAPI\n    app.dependency_overrides[get_db] = _override_get_db\n\n    async with AsyncClient(app=app, base_url=\"http:\u002F\u002Ftest\") as ac:\n        yield ac\n\n    # ВАЖЛИВО: Очищуємо перевизначення після тесту, щоб інші тести не постраждали\n    app.dependency_overrides.clear()\n",[3563,8110,8111,8116,8120,8130,8134,8139,8144,8149,8154,8159,8163,8177,8192,8197,8201,8206,8217,8224,8228,8233,8238,8242,8266,8272,8276,8281],{"__ignoreMap":3591},[3595,8112,8113],{"class":3597,"line":3598},[3595,8114,8115],{"class":4182},"# Продовження tests\u002Fconftest.py\n",[3595,8117,8118],{"class":3597,"line":3604},[3595,8119,3638],{"emptyLinePlaceholder":3637},[3595,8121,8122,8124,8126,8128],{"class":3597,"line":3610},[3595,8123,4434],{"class":4433},[3595,8125,6806],{"class":4176},[3595,8127,4440],{"class":4433},[3595,8129,6811],{"class":4176},[3595,8131,8132],{"class":3597,"line":3616},[3595,8133,3638],{"emptyLinePlaceholder":3637},[3595,8135,8136],{"class":3597,"line":3622},[3595,8137,8138],{"class":4182},"# Наш реальний провайдер сесії БД, який використовується в ендпоінтах\n",[3595,8140,8141],{"class":3597,"line":3628},[3595,8142,8143],{"class":4182},"# Наприклад:\n",[3595,8145,8146],{"class":3597,"line":3634},[3595,8147,8148],{"class":4182},"# async def get_db():\n",[3595,8150,8151],{"class":3597,"line":3641},[3595,8152,8153],{"class":4182},"#     async with SessionLocal() as session:\n",[3595,8155,8156],{"class":3597,"line":3647},[3595,8157,8158],{"class":4182},"#         yield session\n",[3595,8160,8161],{"class":3597,"line":3653},[3595,8162,3638],{"emptyLinePlaceholder":3637},[3595,8164,8165,8167,8169,8171,8173,8175],{"class":3597,"line":3659},[3595,8166,6149],{"class":4064},[3595,8168,4483],{"class":4176},[3595,8170,6296],{"class":4188},[3595,8172,4506],{"class":4176},[3595,8174,6408],{"class":4067},[3595,8176,4534],{"class":4176},[3595,8178,8179,8181,8183,8185,8187,8189],{"class":3597,"line":3664},[3595,8180,6917],{"class":4104},[3595,8182,6920],{"class":4104},[3595,8184,6857],{"class":4064},[3595,8186,4483],{"class":4176},[3595,8188,7918],{"class":4188},[3595,8190,8191],{"class":4176},": AsyncSession):\n",[3595,8193,8194],{"class":3597,"line":3670},[3595,8195,8196],{"class":4067},"    \"\"\"Фікстура клієнта, яка підміняє робочу БД на тестову для кожного запиту.\"\"\"\n",[3595,8198,8199],{"class":3597,"line":3676},[3595,8200,3638],{"emptyLinePlaceholder":3637},[3595,8202,8203],{"class":3597,"line":3681},[3595,8204,8205],{"class":4182},"    # Створюємо функцію-заглушку, яка повертатиме нашу тестову сесію\n",[3595,8207,8208,8210,8212,8215],{"class":3597,"line":3687},[3595,8209,6940],{"class":4104},[3595,8211,6920],{"class":4104},[3595,8213,8214],{"class":4064}," _override_get_db",[3595,8216,4922],{"class":4176},[3595,8218,8219,8221],{"class":3597,"line":3693},[3595,8220,6886],{"class":4433},[3595,8222,8223],{"class":4176}," db_session\n",[3595,8225,8226],{"class":3597,"line":3699},[3595,8227,3638],{"emptyLinePlaceholder":3637},[3595,8229,8230],{"class":3597,"line":3705},[3595,8231,8232],{"class":4182},"    # Записуємо перевизначення в словник FastAPI\n",[3595,8234,8235],{"class":3597,"line":3711},[3595,8236,8237],{"class":4176},"    app.dependency_overrides[get_db] = _override_get_db\n",[3595,8239,8240],{"class":3597,"line":3716},[3595,8241,3638],{"emptyLinePlaceholder":3637},[3595,8243,8244,8246,8248,8250,8252,8254,8256,8258,8260,8262,8264],{"class":3597,"line":3722},[3595,8245,6940],{"class":4433},[3595,8247,6943],{"class":4433},[3595,8249,6946],{"class":4176},[3595,8251,6949],{"class":4188},[3595,8253,6952],{"class":4176},[3595,8255,6955],{"class":4188},[3595,8257,4506],{"class":4176},[3595,8259,6960],{"class":4067},[3595,8261,6448],{"class":4176},[3595,8263,5126],{"class":4433},[3595,8265,6967],{"class":4176},[3595,8267,8268,8270],{"class":3597,"line":3728},[3595,8269,6886],{"class":4433},[3595,8271,6974],{"class":4176},[3595,8273,8274],{"class":3597,"line":3734},[3595,8275,3638],{"emptyLinePlaceholder":3637},[3595,8277,8278],{"class":3597,"line":3740},[3595,8279,8280],{"class":4182},"    # ВАЖЛИВО: Очищуємо перевизначення після тесту, щоб інші тести не постраждали\n",[3595,8282,8283],{"class":3597,"line":3745},[3595,8284,8285],{"class":4176},"    app.dependency_overrides.clear()\n",[3551,8287,8288],{},"Тепер давайте подивимось, як написати інтеграційний тест для створення завдання (Task) через API:",[3586,8290,8293],{"className":4423,"code":8291,"filename":8292,"language":4426,"meta":3591,"style":3591},"import pytest\nfrom datetime import date\n\n@pytest.mark.integration\nasync def test_create_task_endpoint(client):\n    \"\"\"Перевірка успішного створення завдання через POST ендпоінт.\"\"\"\n    payload = {\n        \"title\": \"Write unit tests\",\n        \"description\": \"Cover all endpoints with pytest\",\n        \"due_date\": str(date.today()),\n        \"priority\": 2\n    }\n\n    response = await client.post(\"\u002Fapi\u002Ftasks\", json=payload)\n\n    assert response.status_code == 201\n    data = response.json()\n    assert data[\"id\"] is not None\n    assert data[\"title\"] == payload[\"title\"]\n    assert data[\"priority\"] == payload[\"priority\"]\n\n@pytest.mark.integration\nasync def test_create_task_validation_error(client):\n    \"\"\"Перевірка обробки невалідних даних при створенні завдання.\"\"\"\n    # Пріоритет поза межами дозволеного [1..5]\n    payload = {\n        \"title\": \"Invalid task\",\n        \"priority\": 99\n    }\n\n    response = await client.post(\"\u002Fapi\u002Ftasks\", json=payload)\n\n    assert response.status_code == 422  # Unprocessable Entity\n    data = response.json()\n    assert \"detail\" in data\n    # Перевіряємо локацію помилки валідації\n    assert data[\"detail\"][0][\"loc\"] == [\"body\", \"priority\"]\n","tests\u002Fintegration\u002Ftest_tasks.py",[3563,8294,8295,8301,8312,8316,8320,8335,8340,8345,8356,8367,8378,8387,8391,8395,8415,8419,8428,8433,8454,8469,8483,8487,8491,8506,8511,8516,8520,8531,8540,8544,8548,8564,8568,8580,8584,8596,8601],{"__ignoreMap":3591},[3595,8296,8297,8299],{"class":3597,"line":3598},[3595,8298,4440],{"class":4433},[3595,8300,4879],{"class":4176},[3595,8302,8303,8305,8307,8309],{"class":3597,"line":3604},[3595,8304,4434],{"class":4433},[3595,8306,4437],{"class":4176},[3595,8308,4440],{"class":4433},[3595,8310,8311],{"class":4176}," date\n",[3595,8313,8314],{"class":3597,"line":3610},[3595,8315,3638],{"emptyLinePlaceholder":3637},[3595,8317,8318],{"class":3597,"line":3616},[3595,8319,7113],{"class":4064},[3595,8321,8322,8324,8326,8329,8331,8333],{"class":3597,"line":3622},[3595,8323,6917],{"class":4104},[3595,8325,6920],{"class":4104},[3595,8327,8328],{"class":4064}," test_create_task_endpoint",[3595,8330,4483],{"class":4176},[3595,8332,7125],{"class":4188},[3595,8334,4489],{"class":4176},[3595,8336,8337],{"class":3597,"line":3628},[3595,8338,8339],{"class":4067},"    \"\"\"Перевірка успішного створення завдання через POST ендпоінт.\"\"\"\n",[3595,8341,8342],{"class":3597,"line":3634},[3595,8343,8344],{"class":4176},"    payload = {\n",[3595,8346,8347,8349,8351,8354],{"class":3597,"line":3641},[3595,8348,4937],{"class":4067},[3595,8350,4940],{"class":4176},[3595,8352,8353],{"class":4067},"\"Write unit tests\"",[3595,8355,4366],{"class":4176},[3595,8357,8358,8360,8362,8365],{"class":3597,"line":3647},[3595,8359,4950],{"class":4067},[3595,8361,4940],{"class":4176},[3595,8363,8364],{"class":4067},"\"Cover all endpoints with pytest\"",[3595,8366,4366],{"class":4176},[3595,8368,8369,8371,8373,8375],{"class":3597,"line":3653},[3595,8370,4962],{"class":4067},[3595,8372,4940],{"class":4176},[3595,8374,4497],{"class":4479},[3595,8376,8377],{"class":4176},"(date.today()),\n",[3595,8379,8380,8382,8384],{"class":3597,"line":3659},[3595,8381,4980],{"class":4067},[3595,8383,4940],{"class":4176},[3595,8385,8386],{"class":4509},"2\n",[3595,8388,8389],{"class":3597,"line":3664},[3595,8390,4990],{"class":4176},[3595,8392,8393],{"class":3597,"line":3670},[3595,8394,3638],{"emptyLinePlaceholder":3637},[3595,8396,8397,8399,8401,8404,8407,8409,8412],{"class":3597,"line":3676},[3595,8398,7228],{"class":4176},[3595,8400,7184],{"class":4433},[3595,8402,8403],{"class":4176}," client.post(",[3595,8405,8406],{"class":4067},"\"\u002Fapi\u002Ftasks\"",[3595,8408,4513],{"class":4176},[3595,8410,8411],{"class":4188},"json",[3595,8413,8414],{"class":4176},"=payload)\n",[3595,8416,8417],{"class":3597,"line":3681},[3595,8418,3638],{"emptyLinePlaceholder":3637},[3595,8420,8421,8423,8425],{"class":3597,"line":3687},[3595,8422,5008],{"class":4433},[3595,8424,7152],{"class":4176},[3595,8426,8427],{"class":4509},"201\n",[3595,8429,8430],{"class":3597,"line":3693},[3595,8431,8432],{"class":4176},"    data = response.json()\n",[3595,8434,8435,8437,8440,8443,8445,8448,8451],{"class":3597,"line":3699},[3595,8436,5008],{"class":4433},[3595,8438,8439],{"class":4176}," data[",[3595,8441,8442],{"class":4067},"\"id\"",[3595,8444,5444],{"class":4176},[3595,8446,8447],{"class":4104},"is",[3595,8449,8450],{"class":4104}," not",[3595,8452,8453],{"class":4104}," None\n",[3595,8455,8456,8458,8460,8462,8465,8467],{"class":3597,"line":3705},[3595,8457,5008],{"class":4433},[3595,8459,8439],{"class":4176},[3595,8461,5014],{"class":4067},[3595,8463,8464],{"class":4176},"] == payload[",[3595,8466,5014],{"class":4067},[3595,8468,4218],{"class":4176},[3595,8470,8471,8473,8475,8477,8479,8481],{"class":3597,"line":3711},[3595,8472,5008],{"class":4433},[3595,8474,8439],{"class":4176},[3595,8476,5049],{"class":4067},[3595,8478,8464],{"class":4176},[3595,8480,5049],{"class":4067},[3595,8482,4218],{"class":4176},[3595,8484,8485],{"class":3597,"line":3716},[3595,8486,3638],{"emptyLinePlaceholder":3637},[3595,8488,8489],{"class":3597,"line":3722},[3595,8490,7113],{"class":4064},[3595,8492,8493,8495,8497,8500,8502,8504],{"class":3597,"line":3728},[3595,8494,6917],{"class":4104},[3595,8496,6920],{"class":4104},[3595,8498,8499],{"class":4064}," test_create_task_validation_error",[3595,8501,4483],{"class":4176},[3595,8503,7125],{"class":4188},[3595,8505,4489],{"class":4176},[3595,8507,8508],{"class":3597,"line":3734},[3595,8509,8510],{"class":4067},"    \"\"\"Перевірка обробки невалідних даних при створенні завдання.\"\"\"\n",[3595,8512,8513],{"class":3597,"line":3740},[3595,8514,8515],{"class":4182},"    # Пріоритет поза межами дозволеного [1..5]\n",[3595,8517,8518],{"class":3597,"line":3745},[3595,8519,8344],{"class":4176},[3595,8521,8522,8524,8526,8529],{"class":3597,"line":3750},[3595,8523,4937],{"class":4067},[3595,8525,4940],{"class":4176},[3595,8527,8528],{"class":4067},"\"Invalid task\"",[3595,8530,4366],{"class":4176},[3595,8532,8533,8535,8537],{"class":3597,"line":3756},[3595,8534,4980],{"class":4067},[3595,8536,4940],{"class":4176},[3595,8538,8539],{"class":4509},"99\n",[3595,8541,8542],{"class":3597,"line":3762},[3595,8543,4990],{"class":4176},[3595,8545,8546],{"class":3597,"line":3768},[3595,8547,3638],{"emptyLinePlaceholder":3637},[3595,8549,8550,8552,8554,8556,8558,8560,8562],{"class":3597,"line":3774},[3595,8551,7228],{"class":4176},[3595,8553,7184],{"class":4433},[3595,8555,8403],{"class":4176},[3595,8557,8406],{"class":4067},[3595,8559,4513],{"class":4176},[3595,8561,8411],{"class":4188},[3595,8563,8414],{"class":4176},[3595,8565,8566],{"class":3597,"line":3779},[3595,8567,3638],{"emptyLinePlaceholder":3637},[3595,8569,8570,8572,8574,8577],{"class":3597,"line":3784},[3595,8571,5008],{"class":4433},[3595,8573,7152],{"class":4176},[3595,8575,8576],{"class":4509},"422",[3595,8578,8579],{"class":4182},"  # Unprocessable Entity\n",[3595,8581,8582],{"class":3597,"line":5137},[3595,8583,8432],{"class":4176},[3595,8585,8586,8588,8591,8593],{"class":3597,"line":5142},[3595,8587,5008],{"class":4433},[3595,8589,8590],{"class":4067}," \"detail\"",[3595,8592,5433],{"class":4104},[3595,8594,8595],{"class":4176}," data\n",[3595,8597,8598],{"class":3597,"line":5148},[3595,8599,8600],{"class":4182},"    # Перевіряємо локацію помилки валідації\n",[3595,8602,8603,8605,8607,8610,8612,8614,8616,8618,8621,8624,8626,8628],{"class":3597,"line":5154},[3595,8604,5008],{"class":4433},[3595,8606,8439],{"class":4176},[3595,8608,8609],{"class":4067},"\"detail\"",[3595,8611,5178],{"class":4176},[3595,8613,5175],{"class":4509},[3595,8615,5178],{"class":4176},[3595,8617,5181],{"class":4067},[3595,8619,8620],{"class":4176},"] == [",[3595,8622,8623],{"class":4067},"\"body\"",[3595,8625,4513],{"class":4176},[3595,8627,5049],{"class":4067},[3595,8629,4218],{"class":4176},[3568,8631],{},[3571,8633,8635],{"id":8634},"_8-мокування-mocking-ізоляція-від-зовнішніх-сервісів","8. Мокування (Mocking): Ізоляція від зовнішніх сервісів",[3551,8637,8638],{},"У ідеальному світі кожен тест мав би запускатися у повному середовищі з реальними інтеграціями. Проте на практиці ваші API часто взаємодіють із системами, які ви не контролюєте або які працюють повільно:",[7327,8640,8641,8644,8647],{},[3795,8642,8643],{},"Платіжні шлюзи (Stripe, LiqPay) — ми не можемо знімати реальні гроші під час тестів.",[3795,8645,8646],{},"Поштові сервіси та SMS-шлюзи (SendGrid, Twilio) — ми не хочемо спамити користувачів тестовими листами.",[3795,8648,8649],{},"Зовнішні API сторонніх компаній.",[3551,8651,8652,8653,8656],{},"Для ізоляції нашого коду від цих сервісів використовується ",[3555,8654,8655],{},"мокування (mocking)"," — створення об'єктів-заглушок (mocks), які імітують поведінку реальних компонентів і повертають заздалегідь визначені результати.",[3822,8658,8660],{"id":8659},"філософія-мокування-python-net","Філософія мокування: Python ↔ .NET",[3551,8662,8663,8664,8667,8668,4844,8671,8674],{},"В C# розробники звикли створювати інтерфейси (",[3563,8665,8666],{},"IEmailService",") і використовувати бібліотеки на кшталт ",[3555,8669,8670],{},"Moq",[3555,8672,8673],{},"NSubstitute"," для генерації заглушок у рантаймі:",[3586,8676,8679],{"className":5769,"code":8677,"filename":8678,"language":5772,"meta":3591,"style":3591},"var emailMock = new Mock\u003CIEmailService>();\nemailMock.Setup(x => x.SendEmailAsync(It.IsAny\u003Cstring>(), It.IsAny\u003Cstring>()))\n         .ReturnsAsync(true);\n","Moq (C#)",[3563,8680,8681,8704,8758],{"__ignoreMap":3591},[3595,8682,8683,8686,8689,8691,8694,8697,8699,8701],{"class":3597,"line":3598},[3595,8684,8685],{"class":4104},"var",[3595,8687,8688],{"class":4188}," emailMock",[3595,8690,4192],{"class":4176},[3595,8692,8693],{"class":4104},"new",[3595,8695,8696],{"class":4479}," Mock",[3595,8698,8027],{"class":4176},[3595,8700,8666],{"class":4479},[3595,8702,8703],{"class":4176},">();\n",[3595,8705,8706,8709,8711,8714,8716,8719,8721,8723,8725,8728,8730,8733,8735,8738,8740,8742,8745,8747,8749,8751,8753,8755],{"class":3597,"line":3604},[3595,8707,8708],{"class":4188},"emailMock",[3595,8710,3929],{"class":4176},[3595,8712,8713],{"class":4064},"Setup",[3595,8715,4483],{"class":4176},[3595,8717,8718],{"class":4188},"x",[3595,8720,8006],{"class":4176},[3595,8722,8718],{"class":4188},[3595,8724,3929],{"class":4176},[3595,8726,8727],{"class":4064},"SendEmailAsync",[3595,8729,4483],{"class":4176},[3595,8731,8732],{"class":4188},"It",[3595,8734,3929],{"class":4176},[3595,8736,8737],{"class":4064},"IsAny",[3595,8739,8027],{"class":4176},[3595,8741,5855],{"class":4104},[3595,8743,8744],{"class":4176},">(), ",[3595,8746,8732],{"class":4188},[3595,8748,3929],{"class":4176},[3595,8750,8737],{"class":4064},[3595,8752,8027],{"class":4176},[3595,8754,5855],{"class":4104},[3595,8756,8757],{"class":4176},">()))\n",[3595,8759,8760,8763,8766,8768,8770],{"class":3597,"line":3610},[3595,8761,8762],{"class":4176},"         .",[3595,8764,8765],{"class":4064},"ReturnsAsync",[3595,8767,4483],{"class":4176},[3595,8769,5801],{"class":4104},[3595,8771,5899],{"class":4176},[3551,8773,8774,8775,8778],{},"Python — це мова з динамічною типізацією. Тут нам ",[3555,8776,8777],{},"не потрібні інтерфейси",", щоб замінити один об'єкт іншим. Ми можемо буквально «на льоту» підмінити будь-яку функцію, метод чи цілий клас у пам'яті під час виконання тесту.",[3551,8780,8781,8782,8784,8785,8787,8788,8790,8791,8794],{},"У стандартній бібліотеці Python для цього є модуль ",[3563,8783,3990],{},". Проте при роботі з ",[3563,8786,3864],{}," набагато зручніше використовувати плагін ",[3563,8789,3993],{},", який надає спеціальну фікстуру ",[3563,8792,8793],{},"mocker",". Вона автоматично прибирає (скасовує) всі підміни після завершення тесту, запобігаючи витоку заглушок в інші тести.",[3822,8796,8798],{"id":8797},"практичний-приклад-мокування","Практичний приклад мокування",[3551,8800,8801,8802,4856],{},"Уявімо, що при створенні завдання ми маємо відправляти email-повідомлення виконавцю за допомогою сервісу ",[3563,8803,8804],{},"EmailService",[3586,8806,8809],{"className":4423,"code":8807,"filename":8808,"language":4426,"meta":3591,"style":3591},"import asyncio\n\nclass EmailService:\n    async def send_notification(self, email: str, subject: str, body: str) -> bool:\n        # Симуляція мережевого запиту до SendGrid\n        await asyncio.sleep(2)\n        print(f\"Email sent to {email}\")\n        return True\n","app\u002Fservices\u002Femail.py",[3563,8810,8811,8818,8822,8831,8877,8882,8894,8920],{"__ignoreMap":3591},[3595,8812,8813,8815],{"class":3597,"line":3598},[3595,8814,4440],{"class":4433},[3595,8816,8817],{"class":4176}," asyncio\n",[3595,8819,8820],{"class":3597,"line":3604},[3595,8821,3638],{"emptyLinePlaceholder":3637},[3595,8823,8824,8826,8829],{"class":3597,"line":3610},[3595,8825,4476],{"class":4104},[3595,8827,8828],{"class":4479}," EmailService",[3595,8830,4749],{"class":4176},[3595,8832,8833,8835,8837,8840,8842,8844,8846,8849,8851,8853,8855,8858,8860,8862,8864,8867,8869,8871,8873,8875],{"class":3597,"line":3616},[3595,8834,6940],{"class":4104},[3595,8836,6920],{"class":4104},[3595,8838,8839],{"class":4064}," send_notification",[3595,8841,4483],{"class":4176},[3595,8843,4740],{"class":4188},[3595,8845,4513],{"class":4176},[3595,8847,8848],{"class":4188},"email",[3595,8850,4940],{"class":4176},[3595,8852,4497],{"class":4479},[3595,8854,4513],{"class":4176},[3595,8856,8857],{"class":4188},"subject",[3595,8859,4940],{"class":4176},[3595,8861,4497],{"class":4479},[3595,8863,4513],{"class":4176},[3595,8865,8866],{"class":4188},"body",[3595,8868,4940],{"class":4176},[3595,8870,4497],{"class":4479},[3595,8872,4743],{"class":4176},[3595,8874,5863],{"class":4479},[3595,8876,4749],{"class":4176},[3595,8878,8879],{"class":3597,"line":3622},[3595,8880,8881],{"class":4182},"        # Симуляція мережевого запиту до SendGrid\n",[3595,8883,8884,8886,8889,8892],{"class":3597,"line":3628},[3595,8885,7735],{"class":4433},[3595,8887,8888],{"class":4176}," asyncio.sleep(",[3595,8890,8891],{"class":4509},"2",[3595,8893,4534],{"class":4176},[3595,8895,8896,8899,8901,8904,8907,8910,8912,8915,8918],{"class":3597,"line":3634},[3595,8897,8898],{"class":4064},"        print",[3595,8900,4483],{"class":4176},[3595,8902,8903],{"class":4104},"f",[3595,8905,8906],{"class":4067},"\"Email sent to ",[3595,8908,8909],{"class":4104},"{",[3595,8911,8848],{"class":4176},[3595,8913,8914],{"class":4104},"}",[3595,8916,8917],{"class":4067},"\"",[3595,8919,4534],{"class":4176},[3595,8921,8922,8924],{"class":3597,"line":3641},[3595,8923,4704],{"class":4433},[3595,8925,8926],{"class":4104}," True\n",[3551,8928,8929],{},"Ендпоінт у нашому FastAPI додатку викликає цей сервіс:",[3586,8931,8933],{"className":4423,"code":8932,"filename":7009,"language":4426,"meta":3591,"style":3591},"from fastapi import FastAPI, Depends\nfrom app.services.email import EmailService\n\napp = FastAPI()\nemail_service = EmailService()\n\n@app.post(\"\u002Ftasks\u002Fassign\")\nasync def assign_task(email: str):\n    # Якась логіка...\n    success = await email_service.send_notification(\n        email=email,\n        subject=\"Нове завдання\",\n        body=\"Вам призначено нове завдання у TaskForge\"\n    )\n    return {\"status\": \"assigned\", \"notified\": success}\n",[3563,8934,8935,8946,8958,8962,8966,8971,8975,8987,9006,9011,9021,9029,9041,9051,9056],{"__ignoreMap":3591},[3595,8936,8937,8939,8941,8943],{"class":3597,"line":3598},[3595,8938,4434],{"class":4433},[3595,8940,7021],{"class":4176},[3595,8942,4440],{"class":4433},[3595,8944,8945],{"class":4176}," FastAPI, Depends\n",[3595,8947,8948,8950,8953,8955],{"class":3597,"line":3604},[3595,8949,4434],{"class":4433},[3595,8951,8952],{"class":4176}," app.services.email ",[3595,8954,4440],{"class":4433},[3595,8956,8957],{"class":4176}," EmailService\n",[3595,8959,8960],{"class":3597,"line":3610},[3595,8961,3638],{"emptyLinePlaceholder":3637},[3595,8963,8964],{"class":3597,"line":3616},[3595,8965,7035],{"class":4176},[3595,8967,8968],{"class":3597,"line":3622},[3595,8969,8970],{"class":4176},"email_service = EmailService()\n",[3595,8972,8973],{"class":3597,"line":3628},[3595,8974,3638],{"emptyLinePlaceholder":3637},[3595,8976,8977,8980,8982,8985],{"class":3597,"line":3634},[3595,8978,8979],{"class":4064},"@app.post",[3595,8981,4483],{"class":4176},[3595,8983,8984],{"class":4067},"\"\u002Ftasks\u002Fassign\"",[3595,8986,4534],{"class":4176},[3595,8988,8989,8991,8993,8996,8998,9000,9002,9004],{"class":3597,"line":3641},[3595,8990,6917],{"class":4104},[3595,8992,6920],{"class":4104},[3595,8994,8995],{"class":4064}," assign_task",[3595,8997,4483],{"class":4176},[3595,8999,8848],{"class":4188},[3595,9001,4940],{"class":4176},[3595,9003,4497],{"class":4479},[3595,9005,4489],{"class":4176},[3595,9007,9008],{"class":3597,"line":3647},[3595,9009,9010],{"class":4182},"    # Якась логіка...\n",[3595,9012,9013,9016,9018],{"class":3597,"line":3653},[3595,9014,9015],{"class":4176},"    success = ",[3595,9017,7184],{"class":4433},[3595,9019,9020],{"class":4176}," email_service.send_notification(\n",[3595,9022,9023,9026],{"class":3597,"line":3659},[3595,9024,9025],{"class":4188},"        email",[3595,9027,9028],{"class":4176},"=email,\n",[3595,9030,9031,9034,9036,9039],{"class":3597,"line":3664},[3595,9032,9033],{"class":4188},"        subject",[3595,9035,4506],{"class":4176},[3595,9037,9038],{"class":4067},"\"Нове завдання\"",[3595,9040,4366],{"class":4176},[3595,9042,9043,9046,9048],{"class":3597,"line":3670},[3595,9044,9045],{"class":4188},"        body",[3595,9047,4506],{"class":4176},[3595,9049,9050],{"class":4067},"\"Вам призначено нове завдання у TaskForge\"\n",[3595,9052,9053],{"class":3597,"line":3676},[3595,9054,9055],{"class":4176},"    )\n",[3595,9057,9058,9060,9062,9064,9066,9069,9071,9074],{"class":3597,"line":3681},[3595,9059,6192],{"class":4433},[3595,9061,7067],{"class":4176},[3595,9063,7070],{"class":4067},[3595,9065,4940],{"class":4176},[3595,9067,9068],{"class":4067},"\"assigned\"",[3595,9070,4513],{"class":4176},[3595,9072,9073],{"class":4067},"\"notified\"",[3595,9075,9076],{"class":4176},": success}\n",[3551,9078,9079,9080,9083,9084,4856],{},"Для тестування цього ендпоінту ми не хочемо реально чекати 2 секунди та викликати ",[3563,9081,9082],{},"send_notification",". Напишемо тест із використанням фіктури ",[3563,9085,8793],{},[3586,9087,9090],{"className":4423,"code":9088,"filename":9089,"language":4426,"meta":3591,"style":3591},"import pytest\n\n@pytest.mark.integration\nasync def test_assign_task_with_mocked_email(client, mocker):\n    \"\"\"Тест ендпоінту assign з мокуванням EmailService.\"\"\"\n\n    # 1. Замінюємо реальний метод send_notification на Mock-об'єкт\n    # Шлях вказується відносно того, ДЕ цей об'єкт використовується під час виконання\n    mock_send = mocker.patch(\n        \"app.main.email_service.send_notification\",\n        return_value=True  # Що має повернути метод при виклику\n    )\n\n    # 2. Виконуємо запит через тестовий клієнт\n    response = await client.post(\"\u002Ftasks\u002Fassign?email=student@kostyl.dev\")\n\n    # 3. Перевіряємо відповідь API\n    assert response.status_code == 200\n    assert response.json() == {\"status\": \"assigned\", \"notified\": True}\n\n    # 4. Перевіряємо, чи був викликаний наш mock і з якими аргументами\n    mock_send.assert_called_once_with(\n        email=\"student@kostyl.dev\",\n        subject=\"Нове завдання\",\n        body=\"Вам призначено нове завдання у TaskForge\"\n    )\n","tests\u002Fintegration\u002Ftest_assign.py",[3563,9091,9092,9098,9102,9106,9125,9130,9134,9139,9144,9149,9156,9168,9172,9176,9181,9194,9198,9203,9211,9233,9237,9242,9247,9257,9267,9275],{"__ignoreMap":3591},[3595,9093,9094,9096],{"class":3597,"line":3598},[3595,9095,4440],{"class":4433},[3595,9097,4879],{"class":4176},[3595,9099,9100],{"class":3597,"line":3604},[3595,9101,3638],{"emptyLinePlaceholder":3637},[3595,9103,9104],{"class":3597,"line":3610},[3595,9105,7113],{"class":4064},[3595,9107,9108,9110,9112,9115,9117,9119,9121,9123],{"class":3597,"line":3616},[3595,9109,6917],{"class":4104},[3595,9111,6920],{"class":4104},[3595,9113,9114],{"class":4064}," test_assign_task_with_mocked_email",[3595,9116,4483],{"class":4176},[3595,9118,7125],{"class":4188},[3595,9120,4513],{"class":4176},[3595,9122,8793],{"class":4188},[3595,9124,4489],{"class":4176},[3595,9126,9127],{"class":3597,"line":3622},[3595,9128,9129],{"class":4067},"    \"\"\"Тест ендпоінту assign з мокуванням EmailService.\"\"\"\n",[3595,9131,9132],{"class":3597,"line":3628},[3595,9133,3638],{"emptyLinePlaceholder":3637},[3595,9135,9136],{"class":3597,"line":3634},[3595,9137,9138],{"class":4182},"    # 1. Замінюємо реальний метод send_notification на Mock-об'єкт\n",[3595,9140,9141],{"class":3597,"line":3641},[3595,9142,9143],{"class":4182},"    # Шлях вказується відносно того, ДЕ цей об'єкт використовується під час виконання\n",[3595,9145,9146],{"class":3597,"line":3647},[3595,9147,9148],{"class":4176},"    mock_send = mocker.patch(\n",[3595,9150,9151,9154],{"class":3597,"line":3653},[3595,9152,9153],{"class":4067},"        \"app.main.email_service.send_notification\"",[3595,9155,4366],{"class":4176},[3595,9157,9158,9161,9163,9165],{"class":3597,"line":3659},[3595,9159,9160],{"class":4188},"        return_value",[3595,9162,4506],{"class":4176},[3595,9164,5525],{"class":4104},[3595,9166,9167],{"class":4182},"  # Що має повернути метод при виклику\n",[3595,9169,9170],{"class":3597,"line":3664},[3595,9171,9055],{"class":4176},[3595,9173,9174],{"class":3597,"line":3670},[3595,9175,3638],{"emptyLinePlaceholder":3637},[3595,9177,9178],{"class":3597,"line":3676},[3595,9179,9180],{"class":4182},"    # 2. Виконуємо запит через тестовий клієнт\n",[3595,9182,9183,9185,9187,9189,9192],{"class":3597,"line":3681},[3595,9184,7228],{"class":4176},[3595,9186,7184],{"class":4433},[3595,9188,8403],{"class":4176},[3595,9190,9191],{"class":4067},"\"\u002Ftasks\u002Fassign?email=student@kostyl.dev\"",[3595,9193,4534],{"class":4176},[3595,9195,9196],{"class":3597,"line":3687},[3595,9197,3638],{"emptyLinePlaceholder":3637},[3595,9199,9200],{"class":3597,"line":3693},[3595,9201,9202],{"class":4182},"    # 3. Перевіряємо відповідь API\n",[3595,9204,9205,9207,9209],{"class":3597,"line":3699},[3595,9206,5008],{"class":4433},[3595,9208,7152],{"class":4176},[3595,9210,7155],{"class":4509},[3595,9212,9213,9215,9217,9219,9221,9223,9225,9227,9229,9231],{"class":3597,"line":3705},[3595,9214,5008],{"class":4433},[3595,9216,7162],{"class":4176},[3595,9218,7070],{"class":4067},[3595,9220,4940],{"class":4176},[3595,9222,9068],{"class":4067},[3595,9224,4513],{"class":4176},[3595,9226,9073],{"class":4067},[3595,9228,4940],{"class":4176},[3595,9230,5525],{"class":4104},[3595,9232,5909],{"class":4176},[3595,9234,9235],{"class":3597,"line":3711},[3595,9236,3638],{"emptyLinePlaceholder":3637},[3595,9238,9239],{"class":3597,"line":3716},[3595,9240,9241],{"class":4182},"    # 4. Перевіряємо, чи був викликаний наш mock і з якими аргументами\n",[3595,9243,9244],{"class":3597,"line":3722},[3595,9245,9246],{"class":4176},"    mock_send.assert_called_once_with(\n",[3595,9248,9249,9251,9253,9255],{"class":3597,"line":3728},[3595,9250,9025],{"class":4188},[3595,9252,4506],{"class":4176},[3595,9254,6205],{"class":4067},[3595,9256,4366],{"class":4176},[3595,9258,9259,9261,9263,9265],{"class":3597,"line":3734},[3595,9260,9033],{"class":4188},[3595,9262,4506],{"class":4176},[3595,9264,9038],{"class":4067},[3595,9266,4366],{"class":4176},[3595,9268,9269,9271,9273],{"class":3597,"line":3740},[3595,9270,9045],{"class":4188},[3595,9272,4506],{"class":4176},[3595,9274,9050],{"class":4067},[3595,9276,9277],{"class":3597,"line":3745},[3595,9278,9055],{"class":4176},[9280,9281,9282,9287],"warning",{},[3551,9283,9284],{},[3555,9285,9286],{},"Типова помилка: не той шлях для patch",[3551,9288,9289,9290,9293,9294,9297,9298,9301,9302,9304,9305,9308,9309,9312,9313,9316,9317,3929],{},"Функція ",[3563,9291,9292],{},"mocker.patch"," працює шляхом підміни імені в конкретному просторі імен. Ви повинні патчити об'єкт у тому модулі, ",[3555,9295,9296],{},"куди він імпортований і де викликається",", а не там, де він визначений.\nЯкщо ви напишете ",[3563,9299,9300],{},"mocker.patch(\"app.services.email.EmailService.send_notification\")",", а в ",[3563,9303,7009],{}," вже імпортовано ",[3563,9306,9307],{},"email_service",", патч ",[3555,9310,9311],{},"не спрацює",", бо ",[3563,9314,9315],{},"main"," вже тримає посилання на оригінальний об'єкт. Правильний шлях — саме ",[3563,9318,9319],{},"\"app.main.email_service.send_notification\"",[3568,9321],{},[3571,9323,9325],{"id":9324},"_9-генерація-тестових-даних-з-faker-та-factory-boy","9. Генерація тестових даних з Faker та Factory Boy",[3551,9327,9328,9329,4513,9332,9335],{},"Коли кількість тестів у проекті зростає до сотень, написання тестових даних вручну (hardcoding на кшталт ",[3563,9330,9331],{},"\"test_user@example.com\"",[3563,9333,9334],{},"\"Task title 1\"",") стає серйозною проблемою:",[7327,9337,9338,9341,9344],{},[3795,9339,9340],{},"Тести стають крихкими (зміна унікального обмеження в БД ламає інші тести, бо дані повторюються).",[3795,9342,9343],{},"Тестові сценарії виглядають нереалістично.",[3795,9345,9346],{},"Код тестів розростається через постійне створення великих словників-payload-ів.",[3551,9348,9349,9350,9352,9353,5257,9356,3929],{},"В .NET для вирішення цієї проблеми використовують бібліотеку ",[3555,9351,4003],{},". У Python найкращим вибором є тандем ",[3555,9354,9355],{},"Faker",[3555,9357,9358],{},"Factory Boy",[6298,9360,9361,9365],{},[6301,9362,9364],{"name":9355,"type":9363},"library","Генерує випадкові, але реалістичні дані: імена, адреси, номери телефонів, тексти, паролі, дати тощо.",[6301,9366,9367],{"name":9358,"type":9363},"Дозволяє створювати декларативні фабрики для моделей даних, що спрощує генерацію складних взаємопов'язаних об'єктів (наприклад, User -> Projects -> Tasks). Вона має інтеграцію з SQLAlchemy.",[3822,9369,9371],{"id":9370},"налаштування-фабрик","Налаштування фабрик",[3551,9373,9374,9375,9378],{},"Створимо фабрику для генерації завдань (моделі SQLAlchemy ",[3563,9376,9377],{},"Task","):",[3586,9380,9383],{"className":4423,"code":9381,"filename":9382,"language":4426,"meta":3591,"style":3591},"import factory\nfrom factory import Faker\nfrom app.models.task import Task  # Ваша SQLAlchemy модель завдання\nfrom tests.conftest import TestingSessionLocal  # Тестова сесія\n\nclass TaskFactory(factory.alchemy.SQLAlchemyModelFactory):\n    class Meta:\n        model = Task\n        # Вказуємо тестову сесію для збереження об'єктів у БД\n        sqlalchemy_session = None  # будемо динамічно передавати в сесії\n        sqlalchemy_session_persistence = \"commit\"\n\n    # Декларуємо поля та правила їх генерації\n    id = factory.Sequence(lambda n: n + 1)\n\n    # Використовуємо Faker для генерації реалістичних назв\n    title = Faker(\"sentence\", nb_words=4)\n    description = Faker(\"paragraph\", nb_sentences=3)\n    priority = factory.Iterator([1, 2, 3, 4, 5])\n    due_date = factory.LazyFunction(date.today)\n","tests\u002Ffactories.py",[3563,9384,9385,9392,9404,9419,9434,9438,9462,9472,9477,9482,9492,9500,9504,9509,9530,9534,9539,9559,9578,9604],{"__ignoreMap":3591},[3595,9386,9387,9389],{"class":3597,"line":3598},[3595,9388,4440],{"class":4433},[3595,9390,9391],{"class":4176}," factory\n",[3595,9393,9394,9396,9399,9401],{"class":3597,"line":3604},[3595,9395,4434],{"class":4433},[3595,9397,9398],{"class":4176}," factory ",[3595,9400,4440],{"class":4433},[3595,9402,9403],{"class":4176}," Faker\n",[3595,9405,9406,9408,9411,9413,9416],{"class":3597,"line":3610},[3595,9407,4434],{"class":4433},[3595,9409,9410],{"class":4176}," app.models.task ",[3595,9412,4440],{"class":4433},[3595,9414,9415],{"class":4176}," Task  ",[3595,9417,9418],{"class":4182},"# Ваша SQLAlchemy модель завдання\n",[3595,9420,9421,9423,9426,9428,9431],{"class":3597,"line":3616},[3595,9422,4434],{"class":4433},[3595,9424,9425],{"class":4176}," tests.conftest ",[3595,9427,4440],{"class":4433},[3595,9429,9430],{"class":4176}," TestingSessionLocal  ",[3595,9432,9433],{"class":4182},"# Тестова сесія\n",[3595,9435,9436],{"class":3597,"line":3622},[3595,9437,3638],{"emptyLinePlaceholder":3637},[3595,9439,9440,9442,9445,9447,9450,9452,9455,9457,9460],{"class":3597,"line":3628},[3595,9441,4476],{"class":4104},[3595,9443,9444],{"class":4479}," TaskFactory",[3595,9446,4483],{"class":4176},[3595,9448,9449],{"class":4479},"factory",[3595,9451,3929],{"class":4176},[3595,9453,9454],{"class":4479},"alchemy",[3595,9456,3929],{"class":4176},[3595,9458,9459],{"class":4479},"SQLAlchemyModelFactory",[3595,9461,4489],{"class":4176},[3595,9463,9464,9467,9470],{"class":3597,"line":3634},[3595,9465,9466],{"class":4104},"    class",[3595,9468,9469],{"class":4479}," Meta",[3595,9471,4749],{"class":4176},[3595,9473,9474],{"class":3597,"line":3641},[3595,9475,9476],{"class":4176},"        model = Task\n",[3595,9478,9479],{"class":3597,"line":3647},[3595,9480,9481],{"class":4182},"        # Вказуємо тестову сесію для збереження об'єктів у БД\n",[3595,9483,9484,9487,9489],{"class":3597,"line":3653},[3595,9485,9486],{"class":4176},"        sqlalchemy_session = ",[3595,9488,4547],{"class":4104},[3595,9490,9491],{"class":4182},"  # будемо динамічно передавати в сесії\n",[3595,9493,9494,9497],{"class":3597,"line":3659},[3595,9495,9496],{"class":4176},"        sqlalchemy_session_persistence = ",[3595,9498,9499],{"class":4067},"\"commit\"\n",[3595,9501,9502],{"class":3597,"line":3664},[3595,9503,3638],{"emptyLinePlaceholder":3637},[3595,9505,9506],{"class":3597,"line":3670},[3595,9507,9508],{"class":4182},"    # Декларуємо поля та правила їх генерації\n",[3595,9510,9511,9514,9517,9520,9523,9526,9528],{"class":3597,"line":3676},[3595,9512,9513],{"class":4064},"    id",[3595,9515,9516],{"class":4176}," = factory.Sequence(",[3595,9518,9519],{"class":4104},"lambda",[3595,9521,9522],{"class":4188}," n",[3595,9524,9525],{"class":4176},": n + ",[3595,9527,4595],{"class":4509},[3595,9529,4534],{"class":4176},[3595,9531,9532],{"class":3597,"line":3681},[3595,9533,3638],{"emptyLinePlaceholder":3637},[3595,9535,9536],{"class":3597,"line":3687},[3595,9537,9538],{"class":4182},"    # Використовуємо Faker для генерації реалістичних назв\n",[3595,9540,9541,9544,9547,9549,9552,9554,9557],{"class":3597,"line":3693},[3595,9542,9543],{"class":4176},"    title = Faker(",[3595,9545,9546],{"class":4067},"\"sentence\"",[3595,9548,4513],{"class":4176},[3595,9550,9551],{"class":4188},"nb_words",[3595,9553,4506],{"class":4176},[3595,9555,9556],{"class":4509},"4",[3595,9558,4534],{"class":4176},[3595,9560,9561,9564,9567,9569,9572,9574,9576],{"class":3597,"line":3699},[3595,9562,9563],{"class":4176},"    description = Faker(",[3595,9565,9566],{"class":4067},"\"paragraph\"",[3595,9568,4513],{"class":4176},[3595,9570,9571],{"class":4188},"nb_sentences",[3595,9573,4506],{"class":4176},[3595,9575,4510],{"class":4509},[3595,9577,4534],{"class":4176},[3595,9579,9580,9583,9585,9587,9589,9591,9593,9595,9597,9599,9601],{"class":3597,"line":3705},[3595,9581,9582],{"class":4176},"    priority = factory.Iterator([",[3595,9584,4595],{"class":4509},[3595,9586,4513],{"class":4176},[3595,9588,8891],{"class":4509},[3595,9590,4513],{"class":4176},[3595,9592,4510],{"class":4509},[3595,9594,4513],{"class":4176},[3595,9596,9556],{"class":4509},[3595,9598,4513],{"class":4176},[3595,9600,4614],{"class":4509},[3595,9602,9603],{"class":4176},"])\n",[3595,9605,9606],{"class":3597,"line":3711},[3595,9607,9608],{"class":4176},"    due_date = factory.LazyFunction(date.today)\n",[3551,9610,9611,9612,9614],{},"Тепер ми можемо використовувати цю фабрику в наших тестах. Для цього додамо зручну фікстуру в ",[3563,9613,6565],{},", яка зв'язуватиме фабрику з нашою тестовою сесією транзакції:",[3586,9616,9618],{"className":4423,"code":9617,"filename":6776,"language":4426,"meta":3591,"style":3591},"# Додаємо в tests\u002Fconftest.py\nfrom tests.factories import TaskFactory\n\n@pytest.fixture\ndef task_factory(db_session):\n    \"\"\"Фікстура для створення завдань у тестовій БД.\"\"\"\n    # Зв'язуємо фабрику з поточною тестовою сесією\n    TaskFactory._meta.sqlalchemy_session = db_session\n    return TaskFactory\n",[3563,9619,9620,9625,9637,9641,9645,9658,9663,9668,9673],{"__ignoreMap":3591},[3595,9621,9622],{"class":3597,"line":3598},[3595,9623,9624],{"class":4182},"# Додаємо в tests\u002Fconftest.py\n",[3595,9626,9627,9629,9632,9634],{"class":3597,"line":3604},[3595,9628,4434],{"class":4433},[3595,9630,9631],{"class":4176}," tests.factories ",[3595,9633,4440],{"class":4433},[3595,9635,9636],{"class":4176}," TaskFactory\n",[3595,9638,9639],{"class":3597,"line":3610},[3595,9640,3638],{"emptyLinePlaceholder":3637},[3595,9642,9643],{"class":3597,"line":3616},[3595,9644,6173],{"class":4064},[3595,9646,9647,9649,9652,9654,9656],{"class":3597,"line":3622},[3595,9648,4916],{"class":4104},[3595,9650,9651],{"class":4064}," task_factory",[3595,9653,4483],{"class":4176},[3595,9655,7918],{"class":4188},[3595,9657,4489],{"class":4176},[3595,9659,9660],{"class":3597,"line":3628},[3595,9661,9662],{"class":4067},"    \"\"\"Фікстура для створення завдань у тестовій БД.\"\"\"\n",[3595,9664,9665],{"class":3597,"line":3634},[3595,9666,9667],{"class":4182},"    # Зв'язуємо фабрику з поточною тестовою сесією\n",[3595,9669,9670],{"class":3597,"line":3641},[3595,9671,9672],{"class":4176},"    TaskFactory._meta.sqlalchemy_session = db_session\n",[3595,9674,9675,9677],{"class":3597,"line":3647},[3595,9676,6192],{"class":4433},[3595,9678,9636],{"class":4176},[3822,9680,9682],{"id":9681},"використання-фабрики-в-тестах","Використання фабрики в тестах",[3551,9684,9685],{},"Завдяки фабриці, створення тестових записів у базі даних стає неймовірно простим:",[3586,9687,9690],{"className":4423,"code":9688,"filename":9689,"language":4426,"meta":3591,"style":3591},"import pytest\n\n@pytest.mark.integration\nasync def test_get_all_tasks(client, task_factory):\n    \"\"\"Тест отримання списку завдань.\"\"\"\n    # Створюємо 3 завдання в тестовій БД одним рядком\n    task_factory.create_batch(3)\n\n    response = await client.get(\"\u002Fapi\u002Ftodos\")\n\n    assert response.status_code == 200\n    data = response.json()\n    assert len(data) == 3\n    # Перевіряємо, що унікальні назви згенерувалися\n    assert data[0][\"title\"] != data[1][\"title\"]\n","tests\u002Fintegration\u002Ftest_tasks_query.py",[3563,9691,9692,9698,9702,9706,9726,9731,9736,9745,9749,9763,9767,9775,9779,9790,9795],{"__ignoreMap":3591},[3595,9693,9694,9696],{"class":3597,"line":3598},[3595,9695,4440],{"class":4433},[3595,9697,4879],{"class":4176},[3595,9699,9700],{"class":3597,"line":3604},[3595,9701,3638],{"emptyLinePlaceholder":3637},[3595,9703,9704],{"class":3597,"line":3610},[3595,9705,7113],{"class":4064},[3595,9707,9708,9710,9712,9715,9717,9719,9721,9724],{"class":3597,"line":3616},[3595,9709,6917],{"class":4104},[3595,9711,6920],{"class":4104},[3595,9713,9714],{"class":4064}," test_get_all_tasks",[3595,9716,4483],{"class":4176},[3595,9718,7125],{"class":4188},[3595,9720,4513],{"class":4176},[3595,9722,9723],{"class":4188},"task_factory",[3595,9725,4489],{"class":4176},[3595,9727,9728],{"class":3597,"line":3622},[3595,9729,9730],{"class":4067},"    \"\"\"Тест отримання списку завдань.\"\"\"\n",[3595,9732,9733],{"class":3597,"line":3628},[3595,9734,9735],{"class":4182},"    # Створюємо 3 завдання в тестовій БД одним рядком\n",[3595,9737,9738,9741,9743],{"class":3597,"line":3634},[3595,9739,9740],{"class":4176},"    task_factory.create_batch(",[3595,9742,4510],{"class":4509},[3595,9744,4534],{"class":4176},[3595,9746,9747],{"class":3597,"line":3641},[3595,9748,3638],{"emptyLinePlaceholder":3637},[3595,9750,9751,9753,9755,9758,9761],{"class":3597,"line":3647},[3595,9752,7228],{"class":4176},[3595,9754,7184],{"class":4433},[3595,9756,9757],{"class":4176}," client.get(",[3595,9759,9760],{"class":4067},"\"\u002Fapi\u002Ftodos\"",[3595,9762,4534],{"class":4176},[3595,9764,9765],{"class":3597,"line":3653},[3595,9766,3638],{"emptyLinePlaceholder":3637},[3595,9768,9769,9771,9773],{"class":3597,"line":3659},[3595,9770,5008],{"class":4433},[3595,9772,7152],{"class":4176},[3595,9774,7155],{"class":4509},[3595,9776,9777],{"class":3597,"line":3664},[3595,9778,8432],{"class":4176},[3595,9780,9781,9783,9785,9788],{"class":3597,"line":3670},[3595,9782,5008],{"class":4433},[3595,9784,5159],{"class":4064},[3595,9786,9787],{"class":4176},"(data) == ",[3595,9789,4985],{"class":4509},[3595,9791,9792],{"class":3597,"line":3676},[3595,9793,9794],{"class":4182},"    # Перевіряємо, що унікальні назви згенерувалися\n",[3595,9796,9797,9799,9801,9803,9805,9807,9810,9812,9814,9816],{"class":3597,"line":3681},[3595,9798,5008],{"class":4433},[3595,9800,8439],{"class":4176},[3595,9802,5175],{"class":4509},[3595,9804,5178],{"class":4176},[3595,9806,5014],{"class":4067},[3595,9808,9809],{"class":4176},"] != data[",[3595,9811,4595],{"class":4509},[3595,9813,5178],{"class":4176},[3595,9815,5014],{"class":4067},[3595,9817,4218],{"class":4176},[3551,9819,9820],{},"Якщо нам потрібно перевизначити певні поля для конкретного тесту, ми можемо передати їх як іменовані аргументи:",[3586,9822,9824],{"className":4423,"code":9823,"filename":9689,"language":4426,"meta":3591,"style":3591},"# Продовження файлу tests\u002Fintegration\u002Ftest_tasks_query.py\n\n@pytest.mark.integration\nasync def test_get_task_by_id(client, task_factory):\n    \"\"\"Тест отримання конкретного завдання за ID.\"\"\"\n    # Створюємо завдання з фіксованою назвою\n    task = task_factory.create(title=\"Specific Task Title\")\n\n    response = await client.get(f\"\u002Fapi\u002Ftodos\u002F{task.id}\")\n\n    assert response.status_code == 200\n    data = response.json()\n    assert data[\"id\"] == task.id\n    assert data[\"title\"] == \"Specific Task Title\"\n",[3563,9825,9826,9831,9835,9839,9858,9863,9868,9882,9886,9910,9914,9922,9926,9937],{"__ignoreMap":3591},[3595,9827,9828],{"class":3597,"line":3598},[3595,9829,9830],{"class":4182},"# Продовження файлу tests\u002Fintegration\u002Ftest_tasks_query.py\n",[3595,9832,9833],{"class":3597,"line":3604},[3595,9834,3638],{"emptyLinePlaceholder":3637},[3595,9836,9837],{"class":3597,"line":3610},[3595,9838,7113],{"class":4064},[3595,9840,9841,9843,9845,9848,9850,9852,9854,9856],{"class":3597,"line":3616},[3595,9842,6917],{"class":4104},[3595,9844,6920],{"class":4104},[3595,9846,9847],{"class":4064}," test_get_task_by_id",[3595,9849,4483],{"class":4176},[3595,9851,7125],{"class":4188},[3595,9853,4513],{"class":4176},[3595,9855,9723],{"class":4188},[3595,9857,4489],{"class":4176},[3595,9859,9860],{"class":3597,"line":3622},[3595,9861,9862],{"class":4067},"    \"\"\"Тест отримання конкретного завдання за ID.\"\"\"\n",[3595,9864,9865],{"class":3597,"line":3628},[3595,9866,9867],{"class":4182},"    # Створюємо завдання з фіксованою назвою\n",[3595,9869,9870,9873,9875,9877,9880],{"class":3597,"line":3634},[3595,9871,9872],{"class":4176},"    task = task_factory.create(",[3595,9874,4809],{"class":4188},[3595,9876,4506],{"class":4176},[3595,9878,9879],{"class":4067},"\"Specific Task Title\"",[3595,9881,4534],{"class":4176},[3595,9883,9884],{"class":3597,"line":3641},[3595,9885,3638],{"emptyLinePlaceholder":3637},[3595,9887,9888,9890,9892,9894,9896,9899,9901,9904,9906,9908],{"class":3597,"line":3647},[3595,9889,7228],{"class":4176},[3595,9891,7184],{"class":4433},[3595,9893,9757],{"class":4176},[3595,9895,8903],{"class":4104},[3595,9897,9898],{"class":4067},"\"\u002Fapi\u002Ftodos\u002F",[3595,9900,8909],{"class":4104},[3595,9902,9903],{"class":4176},"task.id",[3595,9905,8914],{"class":4104},[3595,9907,8917],{"class":4067},[3595,9909,4534],{"class":4176},[3595,9911,9912],{"class":3597,"line":3653},[3595,9913,3638],{"emptyLinePlaceholder":3637},[3595,9915,9916,9918,9920],{"class":3597,"line":3659},[3595,9917,5008],{"class":4433},[3595,9919,7152],{"class":4176},[3595,9921,7155],{"class":4509},[3595,9923,9924],{"class":3597,"line":3664},[3595,9925,8432],{"class":4176},[3595,9927,9928,9930,9932,9934],{"class":3597,"line":3670},[3595,9929,5008],{"class":4433},[3595,9931,8439],{"class":4176},[3595,9933,8442],{"class":4067},[3595,9935,9936],{"class":4176},"] == task.id\n",[3595,9938,9939,9941,9943,9945,9947],{"class":3597,"line":3676},[3595,9940,5008],{"class":4433},[3595,9942,8439],{"class":4176},[3595,9944,5014],{"class":4067},[3595,9946,5206],{"class":4176},[3595,9948,9949],{"class":4067},"\"Specific Task Title\"\n",[3568,9951],{},[3571,9953,9955],{"id":9954},"_10-покриття-тестів-кодом-code-coverage-з-pytest-cov","10. Покриття тестів кодом (Code Coverage) з pytest-cov",[3551,9957,9958,9959,9962,9963,9966],{},"Після того, як ви написали десятки тестів, виникає питання: ",[3555,9960,9961],{},"яка частина вашого коду реально перевіряється цими тестами?"," Чи покрили ви всі гілки розгалужень (",[3563,9964,9965],{},"if \u002F else",") в middleware та сервісах?",[3551,9968,9969,9970,9975,9976,3929],{},"Для аналізу покриття коду у світі Python використовується інструмент ",[3555,9971,9972],{},[3563,9973,9974],{},"coverage",", а для інтеграції з pytest — плагін ",[3555,9977,9978],{},[3563,9979,9980],{},"pytest-cov",[3822,9982,9984],{"id":9983},"запуск-тестів-з-аналізом-покриття","Запуск тестів з аналізом покриття",[3551,9986,9987,9988,4856],{},"Для запуску тестування з генерацією звіту про покриття використовується прапорець ",[3563,9989,9990],{},"--cov",[9992,9993,9995,10008,10012,10016,10019,10029,10038,10047,10055,10058,10062,10066,10070,10074,10078,10082,10086,10090,10093,10097,10105,10108],"terminal-preview",{"title":9994},"pytest --cov",[9996,9997,9999,10004,10005],"div",{"className":9998},[3597],[3595,10000,10003],{"className":10001},[10002],"opacity-40","$"," ",[3555,10006,10007],{},"pytest --cov=app tests\u002F",[9996,10009,10011],{"className":10010},[3597],"plugins: asyncio-0.23.6, cov-5.0.0, mock-3.14.0",[9996,10013,10015],{"className":10014},[3597],"collected 12 items",[9996,10017],{"className":10018},[3597],[9996,10020,10022,10023,10028],{"className":10021},[3597],"tests\u002Funit\u002Ftest_task_schemas.py ",[3595,10024,10027],{"className":10025},[10026],"text-green-400","..."," [ 25%]",[9996,10030,10032,10033,10037],{"className":10031},[3597],"tests\u002Fintegration\u002Ftest_health.py ",[3595,10034,10036],{"className":10035},[10026],".."," [ 41%]",[9996,10039,10041,10042,10046],{"className":10040},[3597],"tests\u002Fintegration\u002Ftest_tasks.py ",[3595,10043,10045],{"className":10044},[10026],"...."," [ 75%]",[9996,10048,10050,10051,10054],{"className":10049},[3597],"tests\u002Fintegration\u002Ftest_tasks_query.py ",[3595,10052,10027],{"className":10053},[10026]," [100%]",[9996,10056],{"className":10057},[3597],[9996,10059,10061],{"className":10060},[3597],"---------- coverage: platform darwin, python 3.12.3-final-0 -----------",[9996,10063,10065],{"className":10064},[3597],"Name                     Stmts   Miss  Cover",[9996,10067,10069],{"className":10068},[3597],"--------------------------------------------",[9996,10071,10073],{"className":10072},[3597],"app\u002F__init__.py              0      0   100%",[9996,10075,10077],{"className":10076},[3597],"app\u002Fmain.py                 18      2    88%",[9996,10079,10081],{"className":10080},[3597],"app\u002Fmodels\u002Ftask.py          10      0   100%",[9996,10083,10085],{"className":10084},[3597],"app\u002Fschemas\u002Ftask.py         22      1    95%",[9996,10087,10089],{"className":10088},[3597],"app\u002Fservices\u002Femail.py        7      3    57%",[9996,10091,10069],{"className":10092},[3597],[9996,10094,10096],{"className":10095},[3597],"TOTAL                       57      6    89%",[9996,10098,10100],{"className":10099},[3597],[3595,10101,10104],{"className":10102},[10026,10103],"font-bold","Required test coverage of 80.0% reached. Total coverage: 89.47%",[9996,10106],{"className":10107},[3597],[9996,10109,10111],{"className":10110},[3597],[3595,10112,10114],{"className":10113},[10026,10103],"======= 12 passed in 1.42s =======",[3551,10116,10117],{},"Звіт містить такі колонки:",[7327,10119,10120,10126,10132],{},[3795,10121,10122,10125],{},[3555,10123,10124],{},"Stmts",": Загальна кількість інструкцій (рядків коду) у файлі.",[3795,10127,10128,10131],{},[3555,10129,10130],{},"Miss",": Кількість рядків коду, які жодного разу не виконувалися під час запуску тестів.",[3795,10133,10134,10137],{},[3555,10135,10136],{},"Cover",": Процентне відношення покриття.",[3822,10139,10141],{"id":10140},"детальні-звіти-та-поріг-покриття","Детальні звіти та поріг покриття",[3551,10143,10144],{},"Ви можете згенерувати інтерактивний HTML-звіт, який дозволить побачити конкретні рядки коду, що не були покриті:",[3586,10146,10148],{"className":4055,"code":10147,"language":4057,"meta":3591,"style":3591},"pytest --cov=app --cov-report=html tests\u002F\n",[3563,10149,10150],{"__ignoreMap":3591},[3595,10151,10152,10154,10157,10160],{"class":3597,"line":3598},[3595,10153,3864],{"class":4064},[3595,10155,10156],{"class":4104}," --cov=app",[3595,10158,10159],{"class":4104}," --cov-report=html",[3595,10161,10162],{"class":4067}," tests\u002F\n",[3551,10164,10165,10166,10169,10170,10173],{},"Це створить папку ",[3563,10167,10168],{},"htmlcov\u002F"," з веб-сайтом звіту. Відкрийте ",[3563,10171,10172],{},"htmlcov\u002Findex.html"," у браузері.",[3551,10175,10176],{},"Також корисно налаштувати автоматичне падіння тестів, якщо загальне покриття є нижчим за певний поріг (наприклад, 80%). Це спонукатиме команду писати тести для нових фіч:",[3586,10178,10180],{"className":4167,"code":10179,"filename":4156,"language":4169,"meta":3591,"style":3591},"# Додаємо в конфігурацію pytest в pyproject.toml\n[tool.pytest.ini_options]\naddopts = \"-v -ra --strict-markers --cov=app --cov-fail-under=80 --cov-report=term-missing\"\n",[3563,10181,10182,10187,10191],{"__ignoreMap":3591},[3595,10183,10184],{"class":3597,"line":3598},[3595,10185,10186],{"class":4182},"# Додаємо в конфігурацію pytest в pyproject.toml\n",[3595,10188,10189],{"class":3597,"line":3604},[3595,10190,4177],{"class":4176},[3595,10192,10193,10195,10197],{"class":3597,"line":3610},[3595,10194,4307],{"class":4188},[3595,10196,4192],{"class":4176},[3595,10198,10199],{"class":4067},"\"-v -ra --strict-markers --cov=app --cov-fail-under=80 --cov-report=term-missing\"\n",[3551,10201,4393,10202,10205,10206,10208],{},[3563,10203,10204],{},"--cov-fail-under=80"," змусить команду ",[3563,10207,3864],{}," завершитися з помилкою (exit code != 0), якщо загальний рівень покриття впаде нижче 80%.",[3568,10210],{},[3571,10212,10214],{"id":10213},"_11-cicd-інтеграція-автоматична-перевірка-в-github-actions","11. CI\u002FCD Інтеграція: Автоматична перевірка в GitHub Actions",[3551,10216,10217,10218,10221],{},"Щоб ваші тести реально приносили користь, вони мають виконуватися ",[3555,10219,10220],{},"автоматично на кожен Pull Request"," у вашому репозиторії. Тільки так можна бути впевненим, що нова гілка коду не зламає існуючий функціонал.",[3551,10223,10224,10225,10228],{},"Створимо файл конфігурації GitHub Actions ",[3563,10226,10227],{},".github\u002Fworkflows\u002Fci.yml",". Наш pipeline буде виконувати три етапи:",[3792,10230,10231,10240,10249],{},[3795,10232,10233,10236,10237,3929],{},[3555,10234,10235],{},"Linting"," — перевірка стилю коду за допомогою надшвидкого інструменту ",[3563,10238,10239],{},"ruff",[3795,10241,10242,10245,10246,3929],{},[3555,10243,10244],{},"Type Checking"," — статична перевірка типів через ",[3563,10247,10248],{},"mypy",[3795,10250,10251,10253],{},[3555,10252,2269],{}," — запуск тестів з аналізом покриття та підняттям тимчасової бази даних PostgreSQL.",[3586,10255,10259],{"className":10256,"code":10257,"filename":10227,"language":10258,"meta":3591,"style":3591},"language-yaml shiki shiki-themes light-plus dark-plus dark-plus","name: CI Pipeline\n\non:\n    push:\n        branches: [main, develop]\n    pull_request:\n        branches: [main]\n\njobs:\n    lint-and-test:\n        runs-on: ubuntu-latest\n\n        # Піднімаємо тестову базу даних PostgreSQL за допомогою Docker Compose у сервісах GitHub Actions\n        services:\n            postgres:\n                image: postgres:16\n                env:\n                    POSTGRES_DB: test_taskforge\n                    POSTGRES_USER: postgres\n                    POSTGRES_PASSWORD: postgres\n                ports:\n                    - 5432:5432\n                # Перевірка готовності БД\n                options: >-\n                    --health-cmd pg_isready\n                    --health-interval 10s\n                    --health-timeout 5s\n                    --health-retries 5\n\n        steps:\n            - name: Checkout repository\n              uses: actions\u002Fcheckout@v4\n\n            - name: Set up Python\n              uses: actions\u002Fsetup-python@v5\n              with:\n                  python-version: '3.12'\n                  cache: 'pip' # Кешування залежностей для прискорення CI\n\n            - name: Install dependencies\n              run: |\n                  python -m pip install --upgrade pip\n                  pip install ruff mypy pytest pytest-asyncio pytest-cov pytest-mock httpx faker factory-boy sqlalchemy asyncpg pydantic python-multipart\n\n            - name: Run Ruff Linter\n              run: ruff check app\u002F tests\u002F\n\n            - name: Run Ruff Formatter check\n              run: ruff format --check app\u002F tests\u002F\n\n            - name: Run Mypy Type Checker\n              run: mypy app\u002F\n\n            - name: Run Tests with Coverage\n              env:\n                  # Передаємо рядок підключення до тестової БД в оточення CI\n                  TEST_DATABASE_URL: postgresql+asyncpg:\u002F\u002Fpostgres:postgres@localhost:5432\u002Ftest_taskforge\n              run: pytest\n","yaml",[3563,10260,10261,10273,10277,10284,10291,10308,10315,10325,10329,10336,10343,10353,10357,10362,10369,10376,10386,10393,10403,10413,10422,10429,10437,10442,10455,10460,10465,10470,10475,10479,10486,10498,10508,10512,10523,10532,10539,10549,10562,10566,10577,10587,10593,10599,10604,10616,10626,10631,10643,10653,10658,10670,10680,10685,10697,10705,10711,10722],{"__ignoreMap":3591},[3595,10262,10263,10267,10269],{"class":3597,"line":3598},[3595,10264,10266],{"class":10265},"sKtos","name",[3595,10268,4940],{"class":4176},[3595,10270,10272],{"class":10271},"su9tN","CI Pipeline\n",[3595,10274,10275],{"class":3597,"line":3604},[3595,10276,3638],{"emptyLinePlaceholder":3637},[3595,10278,10279,10282],{"class":3597,"line":3610},[3595,10280,10281],{"class":4104},"on",[3595,10283,4749],{"class":4176},[3595,10285,10286,10289],{"class":3597,"line":3616},[3595,10287,10288],{"class":10265},"    push",[3595,10290,4749],{"class":4176},[3595,10292,10293,10296,10299,10301,10303,10306],{"class":3597,"line":3622},[3595,10294,10295],{"class":10265},"        branches",[3595,10297,10298],{"class":4176},": [",[3595,10300,9315],{"class":10271},[3595,10302,4513],{"class":4176},[3595,10304,10305],{"class":10271},"develop",[3595,10307,4218],{"class":4176},[3595,10309,10310,10313],{"class":3597,"line":3628},[3595,10311,10312],{"class":10265},"    pull_request",[3595,10314,4749],{"class":4176},[3595,10316,10317,10319,10321,10323],{"class":3597,"line":3634},[3595,10318,10295],{"class":10265},[3595,10320,10298],{"class":4176},[3595,10322,9315],{"class":10271},[3595,10324,4218],{"class":4176},[3595,10326,10327],{"class":3597,"line":3641},[3595,10328,3638],{"emptyLinePlaceholder":3637},[3595,10330,10331,10334],{"class":3597,"line":3647},[3595,10332,10333],{"class":10265},"jobs",[3595,10335,4749],{"class":4176},[3595,10337,10338,10341],{"class":3597,"line":3653},[3595,10339,10340],{"class":10265},"    lint-and-test",[3595,10342,4749],{"class":4176},[3595,10344,10345,10348,10350],{"class":3597,"line":3659},[3595,10346,10347],{"class":10265},"        runs-on",[3595,10349,4940],{"class":4176},[3595,10351,10352],{"class":10271},"ubuntu-latest\n",[3595,10354,10355],{"class":3597,"line":3664},[3595,10356,3638],{"emptyLinePlaceholder":3637},[3595,10358,10359],{"class":3597,"line":3670},[3595,10360,10361],{"class":4182},"        # Піднімаємо тестову базу даних PostgreSQL за допомогою Docker Compose у сервісах GitHub Actions\n",[3595,10363,10364,10367],{"class":3597,"line":3676},[3595,10365,10366],{"class":10265},"        services",[3595,10368,4749],{"class":4176},[3595,10370,10371,10374],{"class":3597,"line":3681},[3595,10372,10373],{"class":10265},"            postgres",[3595,10375,4749],{"class":4176},[3595,10377,10378,10381,10383],{"class":3597,"line":3687},[3595,10379,10380],{"class":10265},"                image",[3595,10382,4940],{"class":4176},[3595,10384,10385],{"class":10271},"postgres:16\n",[3595,10387,10388,10391],{"class":3597,"line":3693},[3595,10389,10390],{"class":10265},"                env",[3595,10392,4749],{"class":4176},[3595,10394,10395,10398,10400],{"class":3597,"line":3699},[3595,10396,10397],{"class":10265},"                    POSTGRES_DB",[3595,10399,4940],{"class":4176},[3595,10401,10402],{"class":10271},"test_taskforge\n",[3595,10404,10405,10408,10410],{"class":3597,"line":3705},[3595,10406,10407],{"class":10265},"                    POSTGRES_USER",[3595,10409,4940],{"class":4176},[3595,10411,10412],{"class":10271},"postgres\n",[3595,10414,10415,10418,10420],{"class":3597,"line":3711},[3595,10416,10417],{"class":10265},"                    POSTGRES_PASSWORD",[3595,10419,4940],{"class":4176},[3595,10421,10412],{"class":10271},[3595,10423,10424,10427],{"class":3597,"line":3716},[3595,10425,10426],{"class":10265},"                ports",[3595,10428,4749],{"class":4176},[3595,10430,10431,10434],{"class":3597,"line":3722},[3595,10432,10433],{"class":4176},"                    - ",[3595,10435,10436],{"class":10271},"5432:5432\n",[3595,10438,10439],{"class":3597,"line":3728},[3595,10440,10441],{"class":4182},"                # Перевірка готовності БД\n",[3595,10443,10444,10447,10449,10452],{"class":3597,"line":3734},[3595,10445,10446],{"class":10265},"                options",[3595,10448,4940],{"class":4176},[3595,10450,10451],{"class":4433},">",[3595,10453,10454],{"class":4104},"-\n",[3595,10456,10457],{"class":3597,"line":3740},[3595,10458,10459],{"class":10271},"                    --health-cmd pg_isready\n",[3595,10461,10462],{"class":3597,"line":3745},[3595,10463,10464],{"class":10271},"                    --health-interval 10s\n",[3595,10466,10467],{"class":3597,"line":3750},[3595,10468,10469],{"class":10271},"                    --health-timeout 5s\n",[3595,10471,10472],{"class":3597,"line":3756},[3595,10473,10474],{"class":10271},"                    --health-retries 5\n",[3595,10476,10477],{"class":3597,"line":3762},[3595,10478,3638],{"emptyLinePlaceholder":3637},[3595,10480,10481,10484],{"class":3597,"line":3768},[3595,10482,10483],{"class":10265},"        steps",[3595,10485,4749],{"class":4176},[3595,10487,10488,10491,10493,10495],{"class":3597,"line":3774},[3595,10489,10490],{"class":4176},"            - ",[3595,10492,10266],{"class":10265},[3595,10494,4940],{"class":4176},[3595,10496,10497],{"class":10271},"Checkout repository\n",[3595,10499,10500,10503,10505],{"class":3597,"line":3779},[3595,10501,10502],{"class":10265},"              uses",[3595,10504,4940],{"class":4176},[3595,10506,10507],{"class":10271},"actions\u002Fcheckout@v4\n",[3595,10509,10510],{"class":3597,"line":3784},[3595,10511,3638],{"emptyLinePlaceholder":3637},[3595,10513,10514,10516,10518,10520],{"class":3597,"line":5137},[3595,10515,10490],{"class":4176},[3595,10517,10266],{"class":10265},[3595,10519,4940],{"class":4176},[3595,10521,10522],{"class":10271},"Set up Python\n",[3595,10524,10525,10527,10529],{"class":3597,"line":5142},[3595,10526,10502],{"class":10265},[3595,10528,4940],{"class":4176},[3595,10530,10531],{"class":10271},"actions\u002Fsetup-python@v5\n",[3595,10533,10534,10537],{"class":3597,"line":5148},[3595,10535,10536],{"class":10265},"              with",[3595,10538,4749],{"class":4176},[3595,10540,10541,10544,10546],{"class":3597,"line":5154},[3595,10542,10543],{"class":10265},"                  python-version",[3595,10545,4940],{"class":4176},[3595,10547,10548],{"class":10271},"'3.12'\n",[3595,10550,10551,10554,10556,10559],{"class":3597,"line":5167},[3595,10552,10553],{"class":10265},"                  cache",[3595,10555,4940],{"class":4176},[3595,10557,10558],{"class":10271},"'pip'",[3595,10560,10561],{"class":4182}," # Кешування залежностей для прискорення CI\n",[3595,10563,10564],{"class":3597,"line":5192},[3595,10565,3638],{"emptyLinePlaceholder":3637},[3595,10567,10568,10570,10572,10574],{"class":3597,"line":7885},[3595,10569,10490],{"class":4176},[3595,10571,10266],{"class":10265},[3595,10573,4940],{"class":4176},[3595,10575,10576],{"class":10271},"Install dependencies\n",[3595,10578,10579,10582,10584],{"class":3597,"line":7893},[3595,10580,10581],{"class":10265},"              run",[3595,10583,4940],{"class":4176},[3595,10585,10586],{"class":4433},"|\n",[3595,10588,10590],{"class":3597,"line":10589},42,[3595,10591,10592],{"class":10271},"                  python -m pip install --upgrade pip\n",[3595,10594,10596],{"class":3597,"line":10595},43,[3595,10597,10598],{"class":10271},"                  pip install ruff mypy pytest pytest-asyncio pytest-cov pytest-mock httpx faker factory-boy sqlalchemy asyncpg pydantic python-multipart\n",[3595,10600,10602],{"class":3597,"line":10601},44,[3595,10603,3638],{"emptyLinePlaceholder":3637},[3595,10605,10607,10609,10611,10613],{"class":3597,"line":10606},45,[3595,10608,10490],{"class":4176},[3595,10610,10266],{"class":10265},[3595,10612,4940],{"class":4176},[3595,10614,10615],{"class":10271},"Run Ruff Linter\n",[3595,10617,10619,10621,10623],{"class":3597,"line":10618},46,[3595,10620,10581],{"class":10265},[3595,10622,4940],{"class":4176},[3595,10624,10625],{"class":10271},"ruff check app\u002F tests\u002F\n",[3595,10627,10629],{"class":3597,"line":10628},47,[3595,10630,3638],{"emptyLinePlaceholder":3637},[3595,10632,10634,10636,10638,10640],{"class":3597,"line":10633},48,[3595,10635,10490],{"class":4176},[3595,10637,10266],{"class":10265},[3595,10639,4940],{"class":4176},[3595,10641,10642],{"class":10271},"Run Ruff Formatter check\n",[3595,10644,10646,10648,10650],{"class":3597,"line":10645},49,[3595,10647,10581],{"class":10265},[3595,10649,4940],{"class":4176},[3595,10651,10652],{"class":10271},"ruff format --check app\u002F tests\u002F\n",[3595,10654,10656],{"class":3597,"line":10655},50,[3595,10657,3638],{"emptyLinePlaceholder":3637},[3595,10659,10661,10663,10665,10667],{"class":3597,"line":10660},51,[3595,10662,10490],{"class":4176},[3595,10664,10266],{"class":10265},[3595,10666,4940],{"class":4176},[3595,10668,10669],{"class":10271},"Run Mypy Type Checker\n",[3595,10671,10673,10675,10677],{"class":3597,"line":10672},52,[3595,10674,10581],{"class":10265},[3595,10676,4940],{"class":4176},[3595,10678,10679],{"class":10271},"mypy app\u002F\n",[3595,10681,10683],{"class":3597,"line":10682},53,[3595,10684,3638],{"emptyLinePlaceholder":3637},[3595,10686,10688,10690,10692,10694],{"class":3597,"line":10687},54,[3595,10689,10490],{"class":4176},[3595,10691,10266],{"class":10265},[3595,10693,4940],{"class":4176},[3595,10695,10696],{"class":10271},"Run Tests with Coverage\n",[3595,10698,10700,10703],{"class":3597,"line":10699},55,[3595,10701,10702],{"class":10265},"              env",[3595,10704,4749],{"class":4176},[3595,10706,10708],{"class":3597,"line":10707},56,[3595,10709,10710],{"class":4182},"                  # Передаємо рядок підключення до тестової БД в оточення CI\n",[3595,10712,10714,10717,10719],{"class":3597,"line":10713},57,[3595,10715,10716],{"class":10265},"                  TEST_DATABASE_URL",[3595,10718,4940],{"class":4176},[3595,10720,10721],{"class":10271},"postgresql+asyncpg:\u002F\u002Fpostgres:postgres@localhost:5432\u002Ftest_taskforge\n",[3595,10723,10725,10727,10729],{"class":3597,"line":10724},58,[3595,10726,10581],{"class":10265},[3595,10728,4940],{"class":4176},[3595,10730,10731],{"class":10271},"pytest\n",[3568,10733],{},[3571,10735,10737],{"id":10736},"_12-практичний-проект-від-а-до-я-тестування-taskforge","12. Практичний проект від А до Я: Тестування TaskForge",[3551,10739,10740,10741,10744],{},"Щоб ви могли закріпити теорію на практиці, ми розберемо створення міні-проєкту ",[3555,10742,10743],{},"TaskForge"," з нуля: від написання коду самого API до налаштування тестової інфраструктури, написання фікстур та успішного запуску тестів. Ви зможете скопіювати наведені файли та запустити їх у себе на локальній машині.",[3822,10746,10748],{"id":10747},"крок-1-структура-проєкту","Крок 1: Структура проєкту",[3551,10750,10751,10752,10755],{},"Створіть на комп'ютері нову папку (наприклад, ",[3563,10753,10754],{},"taskforge-demo\u002F",") та організуйте в ній таку структуру файлів:",[10757,10758,10759,10768,10778,10788,10798,10808,10817,10826,10835,10845],"code-tree",{},[3586,10760,10762],{"className":4167,"code":10761,"filename":4156,"language":4169,"meta":3591,"style":3591},"# Файл конфігурації залежностей та pytest\n",[3563,10763,10764],{"__ignoreMap":3591},[3595,10765,10766],{"class":3597,"line":3598},[3595,10767,10761],{"class":4182},[3586,10769,10772],{"className":4423,"code":10770,"filename":10771,"language":4426,"meta":3591,"style":3591},"# Налаштування SQLAlchemy\n","app\u002Fdatabase.py",[3563,10773,10774],{"__ignoreMap":3591},[3595,10775,10776],{"class":3597,"line":3598},[3595,10777,10770],{"class":4182},[3586,10779,10782],{"className":4423,"code":10780,"filename":10781,"language":4426,"meta":3591,"style":3591},"# Опис SQLAlchemy моделей (User, Task)\n","app\u002Fmodels.py",[3563,10783,10784],{"__ignoreMap":3591},[3595,10785,10786],{"class":3597,"line":3598},[3595,10787,10780],{"class":4182},[3586,10789,10792],{"className":4423,"code":10790,"filename":10791,"language":4426,"meta":3591,"style":3591},"# Pydantic схеми для валідації вхідних\u002Fвихідних даних\n","app\u002Fschemas.py",[3563,10793,10794],{"__ignoreMap":3591},[3595,10795,10796],{"class":3597,"line":3598},[3595,10797,10790],{"class":4182},[3586,10799,10802],{"className":4423,"code":10800,"filename":10801,"language":4426,"meta":3591,"style":3591},"# Функції для хешування паролів та генерації JWT\n","app\u002Fsecurity.py",[3563,10803,10804],{"__ignoreMap":3591},[3595,10805,10806],{"class":3597,"line":3598},[3595,10807,10800],{"class":4182},[3586,10809,10811],{"className":4423,"code":10810,"filename":7009,"language":4426,"meta":3591,"style":3591},"# Основний файл FastAPI застосунку з ендпоінтами\n",[3563,10812,10813],{"__ignoreMap":3591},[3595,10814,10815],{"class":3597,"line":3598},[3595,10816,10810],{"class":4182},[3586,10818,10820],{"className":4423,"code":10819,"filename":6776,"language":4426,"meta":3591,"style":3591},"# pytest фікстури (клієнт, БД з транзакційними відкатами, перевизначення DI)\n",[3563,10821,10822],{"__ignoreMap":3591},[3595,10823,10824],{"class":3597,"line":3598},[3595,10825,10819],{"class":4182},[3586,10827,10829],{"className":4423,"code":10828,"filename":9382,"language":4426,"meta":3591,"style":3591},"# Фабрики для генерації тестових даних (Faker + Factory Boy)\n",[3563,10830,10831],{"__ignoreMap":3591},[3595,10832,10833],{"class":3597,"line":3598},[3595,10834,10828],{"class":4182},[3586,10836,10839],{"className":4423,"code":10837,"filename":10838,"language":4426,"meta":3591,"style":3591},"# Тести реєстрації та авторизації\n","tests\u002Ftest_auth.py",[3563,10840,10841],{"__ignoreMap":3591},[3595,10842,10843],{"class":3597,"line":3598},[3595,10844,10837],{"class":4182},[3586,10846,10849],{"className":4423,"code":10847,"filename":10848,"language":4426,"meta":3591,"style":3591},"# Тести для CRUD-операцій із завданнями\n","tests\u002Ftest_tasks.py",[3563,10850,10851],{"__ignoreMap":3591},[3595,10852,10853],{"class":3597,"line":3598},[3595,10854,10847],{"class":4182},[3568,10856],{},[3822,10858,10860,10861,3919],{"id":10859},"крок-2-конфігурація-залежностей-pyprojecttoml","Крок 2: Конфігурація залежностей (",[3563,10862,4156],{},[3551,10864,10865,10866,10868],{},"Файл ",[3563,10867,4156],{}," об'єднує налаштування залежностей та конфігурацію pytest:",[3586,10870,10872],{"className":4167,"code":10871,"filename":4156,"language":4169,"meta":3591,"style":3591},"[project]\nname = \"taskforge-demo\"\nversion = \"1.0.0\"\ndescription = \"A demo app to show FastAPI testing from A to Z\"\nrequires-python = \">=3.11\"\ndependencies = [\n    \"fastapi>=0.110.0\",\n    \"uvicorn>=0.28.0\",\n    \"sqlalchemy>=2.0.28\",\n    \"aiosqlite>=0.20.0\",  # Асинхронний SQLite для тестів та локальної розробки\n    \"pydantic[email]>=2.6.4\",\n    \"python-jose[cryptography]>=3.3.0\",\n    \"bcrypt>=4.0.0\",\n    \"python-multipart>=0.0.32\",\n]\n\n[dependency-groups]\ndev = [\n    \"pytest>=8.1.1\",\n    \"pytest-asyncio>=0.23.6\",\n    \"pytest-cov>=5.0.0\",\n    \"pytest-mock>=3.14.0\",\n    \"httpx>=0.27.0\",\n    \"faker>=24.3.0\",\n    \"factory-boy>=3.3.0\",\n]\n\n[tool.pytest.ini_options]\ntestpaths = [\"tests\"]\npython_files = \"test_*.py\"\npython_classes = \"Test*\"\npython_functions = \"test_*\"\npythonpath = [\".\"]\naddopts = \"-v -ra --cov=app --cov-report=term-missing\"\nasyncio_mode = \"auto\"\nasyncio_default_fixture_loop_scope = \"function\"\n",[3563,10873,10874,10879,10888,10898,10907,10917,10924,10931,10938,10945,10955,10962,10969,10976,10983,10987,10991,10996,11003,11010,11017,11024,11031,11038,11045,11052,11056,11060,11064,11074,11082,11090,11098,11108,11117,11125],{"__ignoreMap":3591},[3595,10875,10876],{"class":3597,"line":3598},[3595,10877,10878],{"class":4176},"[project]\n",[3595,10880,10881,10883,10885],{"class":3597,"line":3604},[3595,10882,10266],{"class":4188},[3595,10884,4192],{"class":4176},[3595,10886,10887],{"class":4067},"\"taskforge-demo\"\n",[3595,10889,10890,10893,10895],{"class":3597,"line":3610},[3595,10891,10892],{"class":4188},"version",[3595,10894,4192],{"class":4176},[3595,10896,10897],{"class":4067},"\"1.0.0\"\n",[3595,10899,10900,10902,10904],{"class":3597,"line":3616},[3595,10901,4526],{"class":4188},[3595,10903,4192],{"class":4176},[3595,10905,10906],{"class":4067},"\"A demo app to show FastAPI testing from A to Z\"\n",[3595,10908,10909,10912,10914],{"class":3597,"line":3622},[3595,10910,10911],{"class":4188},"requires-python",[3595,10913,4192],{"class":4176},[3595,10915,10916],{"class":4067},"\">=3.11\"\n",[3595,10918,10919,10922],{"class":3597,"line":3628},[3595,10920,10921],{"class":4188},"dependencies",[3595,10923,4358],{"class":4176},[3595,10925,10926,10929],{"class":3597,"line":3634},[3595,10927,10928],{"class":4067},"    \"fastapi>=0.110.0\"",[3595,10930,4366],{"class":4176},[3595,10932,10933,10936],{"class":3597,"line":3641},[3595,10934,10935],{"class":4067},"    \"uvicorn>=0.28.0\"",[3595,10937,4366],{"class":4176},[3595,10939,10940,10943],{"class":3597,"line":3647},[3595,10941,10942],{"class":4067},"    \"sqlalchemy>=2.0.28\"",[3595,10944,4366],{"class":4176},[3595,10946,10947,10950,10952],{"class":3597,"line":3653},[3595,10948,10949],{"class":4067},"    \"aiosqlite>=0.20.0\"",[3595,10951,5090],{"class":4176},[3595,10953,10954],{"class":4182},"# Асинхронний SQLite для тестів та локальної розробки\n",[3595,10956,10957,10960],{"class":3597,"line":3659},[3595,10958,10959],{"class":4067},"    \"pydantic[email]>=2.6.4\"",[3595,10961,4366],{"class":4176},[3595,10963,10964,10967],{"class":3597,"line":3664},[3595,10965,10966],{"class":4067},"    \"python-jose[cryptography]>=3.3.0\"",[3595,10968,4366],{"class":4176},[3595,10970,10971,10974],{"class":3597,"line":3670},[3595,10972,10973],{"class":4067},"    \"bcrypt>=4.0.0\"",[3595,10975,4366],{"class":4176},[3595,10977,10978,10981],{"class":3597,"line":3676},[3595,10979,10980],{"class":4067},"    \"python-multipart>=0.0.32\"",[3595,10982,4366],{"class":4176},[3595,10984,10985],{"class":3597,"line":3681},[3595,10986,4218],{"class":4176},[3595,10988,10989],{"class":3597,"line":3687},[3595,10990,3638],{"emptyLinePlaceholder":3637},[3595,10992,10993],{"class":3597,"line":3693},[3595,10994,10995],{"class":4176},"[dependency-groups]\n",[3595,10997,10998,11001],{"class":3597,"line":3699},[3595,10999,11000],{"class":4188},"dev",[3595,11002,4358],{"class":4176},[3595,11004,11005,11008],{"class":3597,"line":3705},[3595,11006,11007],{"class":4067},"    \"pytest>=8.1.1\"",[3595,11009,4366],{"class":4176},[3595,11011,11012,11015],{"class":3597,"line":3711},[3595,11013,11014],{"class":4067},"    \"pytest-asyncio>=0.23.6\"",[3595,11016,4366],{"class":4176},[3595,11018,11019,11022],{"class":3597,"line":3716},[3595,11020,11021],{"class":4067},"    \"pytest-cov>=5.0.0\"",[3595,11023,4366],{"class":4176},[3595,11025,11026,11029],{"class":3597,"line":3722},[3595,11027,11028],{"class":4067},"    \"pytest-mock>=3.14.0\"",[3595,11030,4366],{"class":4176},[3595,11032,11033,11036],{"class":3597,"line":3728},[3595,11034,11035],{"class":4067},"    \"httpx>=0.27.0\"",[3595,11037,4366],{"class":4176},[3595,11039,11040,11043],{"class":3597,"line":3734},[3595,11041,11042],{"class":4067},"    \"faker>=24.3.0\"",[3595,11044,4366],{"class":4176},[3595,11046,11047,11050],{"class":3597,"line":3740},[3595,11048,11049],{"class":4067},"    \"factory-boy>=3.3.0\"",[3595,11051,4366],{"class":4176},[3595,11053,11054],{"class":3597,"line":3745},[3595,11055,4218],{"class":4176},[3595,11057,11058],{"class":3597,"line":3750},[3595,11059,3638],{"emptyLinePlaceholder":3637},[3595,11061,11062],{"class":3597,"line":3756},[3595,11063,4177],{"class":4176},[3595,11065,11066,11068,11070,11072],{"class":3597,"line":3762},[3595,11067,4209],{"class":4188},[3595,11069,4212],{"class":4176},[3595,11071,4215],{"class":4067},[3595,11073,4218],{"class":4176},[3595,11075,11076,11078,11080],{"class":3597,"line":3768},[3595,11077,4232],{"class":4188},[3595,11079,4192],{"class":4176},[3595,11081,4237],{"class":4067},[3595,11083,11084,11086,11088],{"class":3597,"line":3774},[3595,11085,4242],{"class":4188},[3595,11087,4192],{"class":4176},[3595,11089,4247],{"class":4067},[3595,11091,11092,11094,11096],{"class":3597,"line":3779},[3595,11093,4252],{"class":4188},[3595,11095,4192],{"class":4176},[3595,11097,4257],{"class":4067},[3595,11099,11100,11102,11104,11106],{"class":3597,"line":3784},[3595,11101,4271],{"class":4188},[3595,11103,4212],{"class":4176},[3595,11105,4276],{"class":4067},[3595,11107,4218],{"class":4176},[3595,11109,11110,11112,11114],{"class":3597,"line":5137},[3595,11111,4307],{"class":4188},[3595,11113,4192],{"class":4176},[3595,11115,11116],{"class":4067},"\"-v -ra --cov=app --cov-report=term-missing\"\n",[3595,11118,11119,11121,11123],{"class":3597,"line":5142},[3595,11120,4326],{"class":4188},[3595,11122,4192],{"class":4176},[3595,11124,4331],{"class":4067},[3595,11126,11127,11129,11131],{"class":3597,"line":5148},[3595,11128,4336],{"class":4188},[3595,11130,4192],{"class":4176},[3595,11132,4341],{"class":4067},[3568,11134],{},[3822,11136,11138,11139,3919],{"id":11137},"крок-3-налаштування-бази-даних-appdatabasepy","Крок 3: Налаштування бази даних (",[3563,11140,10771],{},[3551,11142,11143,11144,5476],{},"Для простоти розгортання ми будемо використовувати асинхронний SQLite (",[3563,11145,11146],{},"sqlite+aiosqlite",[3586,11148,11150],{"className":4423,"code":11149,"filename":10771,"language":4426,"meta":3591,"style":3591},"from sqlalchemy import event\nfrom sqlalchemy.engine import Engine\nfrom sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession\nfrom sqlalchemy.orm import declarative_base\n\n# Локальна SQLite база даних\nDATABASE_URL = \"sqlite+aiosqlite:\u002F\u002F\u002F.\u002Ftaskforge.db\"\n\nengine = create_async_engine(DATABASE_URL, connect_args={\"check_same_thread\": False})\nSessionLocal = async_sessionmaker(autocommit=False, autoflush=False, bind=engine, expire_on_commit=False)\n\nBase = declarative_base()\n\n# Активація підтримки Foreign Keys для SQLite\n@event.listens_for(Engine, \"connect\")\ndef set_sqlite_pragma(dbapi_connection, connection_record):\n    cursor = dbapi_connection.cursor()\n    cursor.execute(\"PRAGMA foreign_keys=ON\")\n    cursor.close()\n\n# Залежність (Dependency Injection) для ендпоінтів\nasync def get_db() -> AsyncSession:\n    async with SessionLocal() as session:\n        try:\n            yield session\n        finally:\n            await session.close()\n",[3563,11151,11152,11164,11176,11186,11198,11202,11207,11215,11219,11240,11273,11277,11282,11286,11291,11304,11323,11328,11338,11343,11347,11352,11363,11377,11384,11391,11398],{"__ignoreMap":3591},[3595,11153,11154,11156,11159,11161],{"class":3597,"line":3598},[3595,11155,4434],{"class":4433},[3595,11157,11158],{"class":4176}," sqlalchemy ",[3595,11160,4440],{"class":4433},[3595,11162,11163],{"class":4176}," event\n",[3595,11165,11166,11168,11171,11173],{"class":3597,"line":3604},[3595,11167,4434],{"class":4433},[3595,11169,11170],{"class":4176}," sqlalchemy.engine ",[3595,11172,4440],{"class":4433},[3595,11174,11175],{"class":4176}," Engine\n",[3595,11177,11178,11180,11182,11184],{"class":3597,"line":3610},[3595,11179,4434],{"class":4433},[3595,11181,7561],{"class":4176},[3595,11183,4440],{"class":4433},[3595,11185,7566],{"class":4176},[3595,11187,11188,11190,11193,11195],{"class":3597,"line":3616},[3595,11189,4434],{"class":4433},[3595,11191,11192],{"class":4176}," sqlalchemy.orm ",[3595,11194,4440],{"class":4433},[3595,11196,11197],{"class":4176}," declarative_base\n",[3595,11199,11200],{"class":3597,"line":3622},[3595,11201,3638],{"emptyLinePlaceholder":3637},[3595,11203,11204],{"class":3597,"line":3628},[3595,11205,11206],{"class":4182},"# Локальна SQLite база даних\n",[3595,11208,11209,11212],{"class":3597,"line":3634},[3595,11210,11211],{"class":4176},"DATABASE_URL = ",[3595,11213,11214],{"class":4067},"\"sqlite+aiosqlite:\u002F\u002F\u002F.\u002Ftaskforge.db\"\n",[3595,11216,11217],{"class":3597,"line":3641},[3595,11218,3638],{"emptyLinePlaceholder":3637},[3595,11220,11221,11224,11227,11230,11233,11235,11237],{"class":3597,"line":3647},[3595,11222,11223],{"class":4176},"engine = create_async_engine(DATABASE_URL, ",[3595,11225,11226],{"class":4188},"connect_args",[3595,11228,11229],{"class":4176},"={",[3595,11231,11232],{"class":4067},"\"check_same_thread\"",[3595,11234,4940],{"class":4176},[3595,11236,5568],{"class":4104},[3595,11238,11239],{"class":4176},"})\n",[3595,11241,11242,11245,11247,11249,11251,11253,11255,11257,11259,11261,11263,11265,11267,11269,11271],{"class":3597,"line":3653},[3595,11243,11244],{"class":4176},"SessionLocal = async_sessionmaker(",[3595,11246,7637],{"class":4188},[3595,11248,4506],{"class":4176},[3595,11250,5568],{"class":4104},[3595,11252,4513],{"class":4176},[3595,11254,7646],{"class":4188},[3595,11256,4506],{"class":4176},[3595,11258,5568],{"class":4104},[3595,11260,4513],{"class":4176},[3595,11262,7655],{"class":4188},[3595,11264,7658],{"class":4176},[3595,11266,7661],{"class":4188},[3595,11268,4506],{"class":4176},[3595,11270,5568],{"class":4104},[3595,11272,4534],{"class":4176},[3595,11274,11275],{"class":3597,"line":3659},[3595,11276,3638],{"emptyLinePlaceholder":3637},[3595,11278,11279],{"class":3597,"line":3664},[3595,11280,11281],{"class":4176},"Base = declarative_base()\n",[3595,11283,11284],{"class":3597,"line":3670},[3595,11285,3638],{"emptyLinePlaceholder":3637},[3595,11287,11288],{"class":3597,"line":3676},[3595,11289,11290],{"class":4182},"# Активація підтримки Foreign Keys для SQLite\n",[3595,11292,11293,11296,11299,11302],{"class":3597,"line":3681},[3595,11294,11295],{"class":4064},"@event.listens_for",[3595,11297,11298],{"class":4176},"(Engine, ",[3595,11300,11301],{"class":4067},"\"connect\"",[3595,11303,4534],{"class":4176},[3595,11305,11306,11308,11311,11313,11316,11318,11321],{"class":3597,"line":3687},[3595,11307,4916],{"class":4104},[3595,11309,11310],{"class":4064}," set_sqlite_pragma",[3595,11312,4483],{"class":4176},[3595,11314,11315],{"class":4188},"dbapi_connection",[3595,11317,4513],{"class":4176},[3595,11319,11320],{"class":4188},"connection_record",[3595,11322,4489],{"class":4176},[3595,11324,11325],{"class":3597,"line":3693},[3595,11326,11327],{"class":4176},"    cursor = dbapi_connection.cursor()\n",[3595,11329,11330,11333,11336],{"class":3597,"line":3699},[3595,11331,11332],{"class":4176},"    cursor.execute(",[3595,11334,11335],{"class":4067},"\"PRAGMA foreign_keys=ON\"",[3595,11337,4534],{"class":4176},[3595,11339,11340],{"class":3597,"line":3705},[3595,11341,11342],{"class":4176},"    cursor.close()\n",[3595,11344,11345],{"class":3597,"line":3711},[3595,11346,3638],{"emptyLinePlaceholder":3637},[3595,11348,11349],{"class":3597,"line":3716},[3595,11350,11351],{"class":4182},"# Залежність (Dependency Injection) для ендпоінтів\n",[3595,11353,11354,11356,11358,11361],{"class":3597,"line":3722},[3595,11355,6917],{"class":4104},[3595,11357,6920],{"class":4104},[3595,11359,11360],{"class":4064}," get_db",[3595,11362,7805],{"class":4176},[3595,11364,11365,11367,11369,11372,11374],{"class":3597,"line":3728},[3595,11366,6940],{"class":4433},[3595,11368,6943],{"class":4433},[3595,11370,11371],{"class":4176}," SessionLocal() ",[3595,11373,5126],{"class":4433},[3595,11375,11376],{"class":4176}," session:\n",[3595,11378,11379,11382],{"class":3597,"line":3734},[3595,11380,11381],{"class":4433},"        try",[3595,11383,4749],{"class":4176},[3595,11385,11386,11389],{"class":3597,"line":3740},[3595,11387,11388],{"class":4433},"            yield",[3595,11390,7865],{"class":4176},[3595,11392,11393,11396],{"class":3597,"line":3745},[3595,11394,11395],{"class":4433},"        finally",[3595,11397,4749],{"class":4176},[3595,11399,11400,11403],{"class":3597,"line":3750},[3595,11401,11402],{"class":4433},"            await",[3595,11404,7882],{"class":4176},[3568,11406],{},[3822,11408,11410,11411,3919],{"id":11409},"крок-4-опис-моделей-appmodelspy","Крок 4: Опис моделей (",[3563,11412,10781],{},[3551,11414,11415,11416,11419,11420,11422],{},"Нам потрібні дві пов'язані моделі: ",[3563,11417,11418],{},"User"," (користувач) та ",[3563,11421,9377],{}," (завдання).",[3586,11424,11426],{"className":4423,"code":11425,"filename":10781,"language":4426,"meta":3591,"style":3591},"from sqlalchemy import Column, Integer, String, ForeignKey, Date\nfrom sqlalchemy.orm import relationship\nfrom app.database import Base\n\nclass User(Base):\n    __tablename__ = \"users\"\n\n    id = Column(Integer, primary_key=True, index=True)\n    email = Column(String, unique=True, index=True, nullable=False)\n    username = Column(String, unique=True, index=True, nullable=False)\n    hashed_password = Column(String, nullable=False)\n\n    tasks = relationship(\"Task\", back_populates=\"owner\", cascade=\"all, delete-orphan\")\n\nclass Task(Base):\n    __tablename__ = \"tasks\"\n\n    id = Column(Integer, primary_key=True, index=True)\n    title = Column(String, nullable=False)\n    description = Column(String, nullable=True)\n    priority = Column(Integer, default=1)\n    due_date = Column(Date, nullable=True)\n    owner_id = Column(Integer, ForeignKey(\"users.id\", ondelete=\"CASCADE\"), nullable=False)\n\n    owner = relationship(\"User\", back_populates=\"tasks\")\n",[3563,11427,11428,11439,11450,11461,11465,11479,11487,11491,11516,11547,11576,11589,11593,11623,11627,11640,11647,11651,11673,11686,11699,11712,11725,11753,11757],{"__ignoreMap":3591},[3595,11429,11430,11432,11434,11436],{"class":3597,"line":3598},[3595,11431,4434],{"class":4433},[3595,11433,11158],{"class":4176},[3595,11435,4440],{"class":4433},[3595,11437,11438],{"class":4176}," Column, Integer, String, ForeignKey, Date\n",[3595,11440,11441,11443,11445,11447],{"class":3597,"line":3604},[3595,11442,4434],{"class":4433},[3595,11444,11192],{"class":4176},[3595,11446,4440],{"class":4433},[3595,11448,11449],{"class":4176}," relationship\n",[3595,11451,11452,11454,11456,11458],{"class":3597,"line":3610},[3595,11453,4434],{"class":4433},[3595,11455,7573],{"class":4176},[3595,11457,4440],{"class":4433},[3595,11459,11460],{"class":4176}," Base\n",[3595,11462,11463],{"class":3597,"line":3616},[3595,11464,3638],{"emptyLinePlaceholder":3637},[3595,11466,11467,11469,11472,11474,11477],{"class":3597,"line":3622},[3595,11468,4476],{"class":4104},[3595,11470,11471],{"class":4479}," User",[3595,11473,4483],{"class":4176},[3595,11475,11476],{"class":4479},"Base",[3595,11478,4489],{"class":4176},[3595,11480,11481,11484],{"class":3597,"line":3628},[3595,11482,11483],{"class":4176},"    __tablename__ = ",[3595,11485,11486],{"class":4067},"\"users\"\n",[3595,11488,11489],{"class":3597,"line":3634},[3595,11490,3638],{"emptyLinePlaceholder":3637},[3595,11492,11493,11495,11498,11501,11503,11505,11507,11510,11512,11514],{"class":3597,"line":3641},[3595,11494,9513],{"class":4064},[3595,11496,11497],{"class":4176}," = Column(Integer, ",[3595,11499,11500],{"class":4188},"primary_key",[3595,11502,4506],{"class":4176},[3595,11504,5525],{"class":4104},[3595,11506,4513],{"class":4176},[3595,11508,11509],{"class":4188},"index",[3595,11511,4506],{"class":4176},[3595,11513,5525],{"class":4104},[3595,11515,4534],{"class":4176},[3595,11517,11518,11521,11524,11526,11528,11530,11532,11534,11536,11538,11541,11543,11545],{"class":3597,"line":3647},[3595,11519,11520],{"class":4176},"    email = Column(String, ",[3595,11522,11523],{"class":4188},"unique",[3595,11525,4506],{"class":4176},[3595,11527,5525],{"class":4104},[3595,11529,4513],{"class":4176},[3595,11531,11509],{"class":4188},[3595,11533,4506],{"class":4176},[3595,11535,5525],{"class":4104},[3595,11537,4513],{"class":4176},[3595,11539,11540],{"class":4188},"nullable",[3595,11542,4506],{"class":4176},[3595,11544,5568],{"class":4104},[3595,11546,4534],{"class":4176},[3595,11548,11549,11552,11554,11556,11558,11560,11562,11564,11566,11568,11570,11572,11574],{"class":3597,"line":3653},[3595,11550,11551],{"class":4176},"    username = Column(String, ",[3595,11553,11523],{"class":4188},[3595,11555,4506],{"class":4176},[3595,11557,5525],{"class":4104},[3595,11559,4513],{"class":4176},[3595,11561,11509],{"class":4188},[3595,11563,4506],{"class":4176},[3595,11565,5525],{"class":4104},[3595,11567,4513],{"class":4176},[3595,11569,11540],{"class":4188},[3595,11571,4506],{"class":4176},[3595,11573,5568],{"class":4104},[3595,11575,4534],{"class":4176},[3595,11577,11578,11581,11583,11585,11587],{"class":3597,"line":3659},[3595,11579,11580],{"class":4176},"    hashed_password = Column(String, ",[3595,11582,11540],{"class":4188},[3595,11584,4506],{"class":4176},[3595,11586,5568],{"class":4104},[3595,11588,4534],{"class":4176},[3595,11590,11591],{"class":3597,"line":3664},[3595,11592,3638],{"emptyLinePlaceholder":3637},[3595,11594,11595,11598,11601,11603,11606,11608,11611,11613,11616,11618,11621],{"class":3597,"line":3670},[3595,11596,11597],{"class":4176},"    tasks = relationship(",[3595,11599,11600],{"class":4067},"\"Task\"",[3595,11602,4513],{"class":4176},[3595,11604,11605],{"class":4188},"back_populates",[3595,11607,4506],{"class":4176},[3595,11609,11610],{"class":4067},"\"owner\"",[3595,11612,4513],{"class":4176},[3595,11614,11615],{"class":4188},"cascade",[3595,11617,4506],{"class":4176},[3595,11619,11620],{"class":4067},"\"all, delete-orphan\"",[3595,11622,4534],{"class":4176},[3595,11624,11625],{"class":3597,"line":3676},[3595,11626,3638],{"emptyLinePlaceholder":3637},[3595,11628,11629,11631,11634,11636,11638],{"class":3597,"line":3681},[3595,11630,4476],{"class":4104},[3595,11632,11633],{"class":4479}," Task",[3595,11635,4483],{"class":4176},[3595,11637,11476],{"class":4479},[3595,11639,4489],{"class":4176},[3595,11641,11642,11644],{"class":3597,"line":3687},[3595,11643,11483],{"class":4176},[3595,11645,11646],{"class":4067},"\"tasks\"\n",[3595,11648,11649],{"class":3597,"line":3693},[3595,11650,3638],{"emptyLinePlaceholder":3637},[3595,11652,11653,11655,11657,11659,11661,11663,11665,11667,11669,11671],{"class":3597,"line":3699},[3595,11654,9513],{"class":4064},[3595,11656,11497],{"class":4176},[3595,11658,11500],{"class":4188},[3595,11660,4506],{"class":4176},[3595,11662,5525],{"class":4104},[3595,11664,4513],{"class":4176},[3595,11666,11509],{"class":4188},[3595,11668,4506],{"class":4176},[3595,11670,5525],{"class":4104},[3595,11672,4534],{"class":4176},[3595,11674,11675,11678,11680,11682,11684],{"class":3597,"line":3705},[3595,11676,11677],{"class":4176},"    title = Column(String, ",[3595,11679,11540],{"class":4188},[3595,11681,4506],{"class":4176},[3595,11683,5568],{"class":4104},[3595,11685,4534],{"class":4176},[3595,11687,11688,11691,11693,11695,11697],{"class":3597,"line":3711},[3595,11689,11690],{"class":4176},"    description = Column(String, ",[3595,11692,11540],{"class":4188},[3595,11694,4506],{"class":4176},[3595,11696,5525],{"class":4104},[3595,11698,4534],{"class":4176},[3595,11700,11701,11704,11706,11708,11710],{"class":3597,"line":3716},[3595,11702,11703],{"class":4176},"    priority = Column(Integer, ",[3595,11705,4590],{"class":4188},[3595,11707,4506],{"class":4176},[3595,11709,4595],{"class":4509},[3595,11711,4534],{"class":4176},[3595,11713,11714,11717,11719,11721,11723],{"class":3597,"line":3722},[3595,11715,11716],{"class":4176},"    due_date = Column(Date, ",[3595,11718,11540],{"class":4188},[3595,11720,4506],{"class":4176},[3595,11722,5525],{"class":4104},[3595,11724,4534],{"class":4176},[3595,11726,11727,11730,11733,11735,11738,11740,11743,11745,11747,11749,11751],{"class":3597,"line":3728},[3595,11728,11729],{"class":4176},"    owner_id = Column(Integer, ForeignKey(",[3595,11731,11732],{"class":4067},"\"users.id\"",[3595,11734,4513],{"class":4176},[3595,11736,11737],{"class":4188},"ondelete",[3595,11739,4506],{"class":4176},[3595,11741,11742],{"class":4067},"\"CASCADE\"",[3595,11744,5545],{"class":4176},[3595,11746,11540],{"class":4188},[3595,11748,4506],{"class":4176},[3595,11750,5568],{"class":4104},[3595,11752,4534],{"class":4176},[3595,11754,11755],{"class":3597,"line":3734},[3595,11756,3638],{"emptyLinePlaceholder":3637},[3595,11758,11759,11762,11765,11767,11769,11771,11774],{"class":3597,"line":3740},[3595,11760,11761],{"class":4176},"    owner = relationship(",[3595,11763,11764],{"class":4067},"\"User\"",[3595,11766,4513],{"class":4176},[3595,11768,11605],{"class":4188},[3595,11770,4506],{"class":4176},[3595,11772,11773],{"class":4067},"\"tasks\"",[3595,11775,4534],{"class":4176},[3568,11777],{},[3822,11779,11781,11782,3919],{"id":11780},"крок-5-pydantic-схеми-appschemaspy","Крок 5: Pydantic-схеми (",[3563,11783,10791],{},[3551,11785,11786],{},"Схеми відповідають за перевірку типів та обмежень на вході та виході з API.",[3586,11788,11790],{"className":4423,"code":11789,"filename":10791,"language":4426,"meta":3591,"style":3591},"from datetime import date\nfrom typing import Optional\nfrom pydantic import BaseModel, EmailStr, Field, field_validator\n\nclass UserCreate(BaseModel):\n    email: EmailStr\n    username: str = Field(..., min_length=3, max_length=50)\n    password: str = Field(..., min_length=6)\n\nclass UserResponse(BaseModel):\n    id: int\n    email: EmailStr\n    username: str\n\n    class Config:\n        from_attributes = True\n\nclass TokenResponse(BaseModel):\n    access_token: str\n    token_type: str\n\nclass TaskCreate(BaseModel):\n    title: str = Field(..., min_length=3, max_length=100)\n    description: Optional[str] = Field(None, max_length=1000)\n    priority: int = Field(default=1, ge=1, le=5)\n    due_date: Optional[date] = Field(None)\n\n    @field_validator(\"due_date\")\n    @classmethod\n    def prevent_past_dates(cls, value: Optional[date]) -> Optional[date]:\n        if value and value \u003C date.today():\n            raise ValueError(\"Дедлайн не може бути в минулому\")\n        return value\n\nclass TaskResponse(BaseModel):\n    id: int\n    title: str\n    description: Optional[str]\n    priority: int\n    due_date: Optional[date]\n    owner_id: int\n\n    class Config:\n        from_attributes = True\n",[3563,11791,11792,11802,11812,11823,11827,11840,11845,11871,11888,11892,11905,11914,11918,11925,11929,11938,11946,11950,11963,11970,11977,11981,11993,12017,12037,12069,12077,12081,12091,12097,12113,12123,12135,12141,12145,12158,12166,12172,12180,12186,12191,12198,12202,12210],{"__ignoreMap":3591},[3595,11793,11794,11796,11798,11800],{"class":3597,"line":3598},[3595,11795,4434],{"class":4433},[3595,11797,4437],{"class":4176},[3595,11799,4440],{"class":4433},[3595,11801,8311],{"class":4176},[3595,11803,11804,11806,11808,11810],{"class":3597,"line":3604},[3595,11805,4434],{"class":4433},[3595,11807,4450],{"class":4176},[3595,11809,4440],{"class":4433},[3595,11811,4455],{"class":4176},[3595,11813,11814,11816,11818,11820],{"class":3597,"line":3610},[3595,11815,4434],{"class":4433},[3595,11817,4462],{"class":4176},[3595,11819,4440],{"class":4433},[3595,11821,11822],{"class":4176}," BaseModel, EmailStr, Field, field_validator\n",[3595,11824,11825],{"class":3597,"line":3616},[3595,11826,3638],{"emptyLinePlaceholder":3637},[3595,11828,11829,11831,11834,11836,11838],{"class":3597,"line":3622},[3595,11830,4476],{"class":4104},[3595,11832,11833],{"class":4479}," UserCreate",[3595,11835,4483],{"class":4176},[3595,11837,4486],{"class":4479},[3595,11839,4489],{"class":4176},[3595,11841,11842],{"class":3597,"line":3628},[3595,11843,11844],{"class":4176},"    email: EmailStr\n",[3595,11846,11847,11850,11852,11854,11856,11858,11860,11862,11864,11866,11869],{"class":3597,"line":3634},[3595,11848,11849],{"class":4176},"    username: ",[3595,11851,4497],{"class":4479},[3595,11853,4500],{"class":4176},[3595,11855,4503],{"class":4188},[3595,11857,4506],{"class":4176},[3595,11859,4510],{"class":4509},[3595,11861,4513],{"class":4176},[3595,11863,4516],{"class":4188},[3595,11865,4506],{"class":4176},[3595,11867,11868],{"class":4509},"50",[3595,11870,4534],{"class":4176},[3595,11872,11873,11876,11878,11880,11882,11884,11886],{"class":3597,"line":3641},[3595,11874,11875],{"class":4176},"    password: ",[3595,11877,4497],{"class":4479},[3595,11879,4500],{"class":4176},[3595,11881,4503],{"class":4188},[3595,11883,4506],{"class":4176},[3595,11885,5313],{"class":4509},[3595,11887,4534],{"class":4176},[3595,11889,11890],{"class":3597,"line":3647},[3595,11891,3638],{"emptyLinePlaceholder":3637},[3595,11893,11894,11896,11899,11901,11903],{"class":3597,"line":3653},[3595,11895,4476],{"class":4104},[3595,11897,11898],{"class":4479}," UserResponse",[3595,11900,4483],{"class":4176},[3595,11902,4486],{"class":4479},[3595,11904,4489],{"class":4176},[3595,11906,11907,11909,11911],{"class":3597,"line":3659},[3595,11908,9513],{"class":4064},[3595,11910,4940],{"class":4176},[3595,11912,11913],{"class":4479},"int\n",[3595,11915,11916],{"class":3597,"line":3664},[3595,11917,11844],{"class":4176},[3595,11919,11920,11922],{"class":3597,"line":3670},[3595,11921,11849],{"class":4176},[3595,11923,11924],{"class":4479},"str\n",[3595,11926,11927],{"class":3597,"line":3676},[3595,11928,3638],{"emptyLinePlaceholder":3637},[3595,11930,11931,11933,11936],{"class":3597,"line":3681},[3595,11932,9466],{"class":4104},[3595,11934,11935],{"class":4479}," Config",[3595,11937,4749],{"class":4176},[3595,11939,11940,11943],{"class":3597,"line":3687},[3595,11941,11942],{"class":4176},"        from_attributes = ",[3595,11944,11945],{"class":4104},"True\n",[3595,11947,11948],{"class":3597,"line":3693},[3595,11949,3638],{"emptyLinePlaceholder":3637},[3595,11951,11952,11954,11957,11959,11961],{"class":3597,"line":3699},[3595,11953,4476],{"class":4104},[3595,11955,11956],{"class":4479}," TokenResponse",[3595,11958,4483],{"class":4176},[3595,11960,4486],{"class":4479},[3595,11962,4489],{"class":4176},[3595,11964,11965,11968],{"class":3597,"line":3705},[3595,11966,11967],{"class":4176},"    access_token: ",[3595,11969,11924],{"class":4479},[3595,11971,11972,11975],{"class":3597,"line":3711},[3595,11973,11974],{"class":4176},"    token_type: ",[3595,11976,11924],{"class":4479},[3595,11978,11979],{"class":3597,"line":3716},[3595,11980,3638],{"emptyLinePlaceholder":3637},[3595,11982,11983,11985,11987,11989,11991],{"class":3597,"line":3722},[3595,11984,4476],{"class":4104},[3595,11986,4480],{"class":4479},[3595,11988,4483],{"class":4176},[3595,11990,4486],{"class":4479},[3595,11992,4489],{"class":4176},[3595,11994,11995,11997,11999,12001,12003,12005,12007,12009,12011,12013,12015],{"class":3597,"line":3728},[3595,11996,4494],{"class":4176},[3595,11998,4497],{"class":4479},[3595,12000,4500],{"class":4176},[3595,12002,4503],{"class":4188},[3595,12004,4506],{"class":4176},[3595,12006,4510],{"class":4509},[3595,12008,4513],{"class":4176},[3595,12010,4516],{"class":4188},[3595,12012,4506],{"class":4176},[3595,12014,4521],{"class":4509},[3595,12016,4534],{"class":4176},[3595,12018,12019,12021,12023,12025,12027,12029,12031,12033,12035],{"class":3597,"line":3734},[3595,12020,4539],{"class":4176},[3595,12022,4497],{"class":4479},[3595,12024,4544],{"class":4176},[3595,12026,4547],{"class":4104},[3595,12028,4513],{"class":4176},[3595,12030,4516],{"class":4188},[3595,12032,4506],{"class":4176},[3595,12034,4556],{"class":4509},[3595,12036,4534],{"class":4176},[3595,12038,12039,12041,12043,12045,12047,12049,12051,12053,12055,12057,12059,12061,12063,12065,12067],{"class":3597,"line":3740},[3595,12040,4581],{"class":4176},[3595,12042,4584],{"class":4479},[3595,12044,4587],{"class":4176},[3595,12046,4590],{"class":4188},[3595,12048,4506],{"class":4176},[3595,12050,4595],{"class":4509},[3595,12052,4513],{"class":4176},[3595,12054,4600],{"class":4188},[3595,12056,4506],{"class":4176},[3595,12058,4595],{"class":4509},[3595,12060,4513],{"class":4176},[3595,12062,4609],{"class":4188},[3595,12064,4506],{"class":4176},[3595,12066,4614],{"class":4509},[3595,12068,4534],{"class":4176},[3595,12070,12071,12073,12075],{"class":3597,"line":3745},[3595,12072,4563],{"class":4176},[3595,12074,4547],{"class":4104},[3595,12076,4534],{"class":4176},[3595,12078,12079],{"class":3597,"line":3750},[3595,12080,3638],{"emptyLinePlaceholder":3637},[3595,12082,12083,12085,12087,12089],{"class":3597,"line":3756},[3595,12084,4634],{"class":4064},[3595,12086,4483],{"class":4176},[3595,12088,4639],{"class":4067},[3595,12090,4534],{"class":4176},[3595,12092,12093,12095],{"class":3597,"line":3762},[3595,12094,4646],{"class":4064},[3595,12096,4649],{"class":4479},[3595,12098,12099,12101,12103,12105,12107,12109,12111],{"class":3597,"line":3768},[3595,12100,4654],{"class":4104},[3595,12102,4657],{"class":4064},[3595,12104,4483],{"class":4176},[3595,12106,4662],{"class":4188},[3595,12108,4513],{"class":4176},[3595,12110,4667],{"class":4188},[3595,12112,4670],{"class":4176},[3595,12114,12115,12117,12119,12121],{"class":3597,"line":3774},[3595,12116,4675],{"class":4433},[3595,12118,4678],{"class":4176},[3595,12120,4681],{"class":4104},[3595,12122,4684],{"class":4176},[3595,12124,12125,12127,12129,12131,12133],{"class":3597,"line":3779},[3595,12126,4689],{"class":4433},[3595,12128,4692],{"class":4479},[3595,12130,4483],{"class":4176},[3595,12132,4697],{"class":4067},[3595,12134,4534],{"class":4176},[3595,12136,12137,12139],{"class":3597,"line":3784},[3595,12138,4704],{"class":4433},[3595,12140,4707],{"class":4176},[3595,12142,12143],{"class":3597,"line":5137},[3595,12144,3638],{"emptyLinePlaceholder":3637},[3595,12146,12147,12149,12152,12154,12156],{"class":3597,"line":5142},[3595,12148,4476],{"class":4104},[3595,12150,12151],{"class":4479}," TaskResponse",[3595,12153,4483],{"class":4176},[3595,12155,4486],{"class":4479},[3595,12157,4489],{"class":4176},[3595,12159,12160,12162,12164],{"class":3597,"line":5148},[3595,12161,9513],{"class":4064},[3595,12163,4940],{"class":4176},[3595,12165,11913],{"class":4479},[3595,12167,12168,12170],{"class":3597,"line":5154},[3595,12169,4494],{"class":4176},[3595,12171,11924],{"class":4479},[3595,12173,12174,12176,12178],{"class":3597,"line":5167},[3595,12175,4539],{"class":4176},[3595,12177,4497],{"class":4479},[3595,12179,4218],{"class":4176},[3595,12181,12182,12184],{"class":3597,"line":5192},[3595,12183,4581],{"class":4176},[3595,12185,11913],{"class":4479},[3595,12187,12188],{"class":3597,"line":7885},[3595,12189,12190],{"class":4176},"    due_date: Optional[date]\n",[3595,12192,12193,12196],{"class":3597,"line":7893},[3595,12194,12195],{"class":4176},"    owner_id: ",[3595,12197,11913],{"class":4479},[3595,12199,12200],{"class":3597,"line":10589},[3595,12201,3638],{"emptyLinePlaceholder":3637},[3595,12203,12204,12206,12208],{"class":3597,"line":10595},[3595,12205,9466],{"class":4104},[3595,12207,11935],{"class":4479},[3595,12209,4749],{"class":4176},[3595,12211,12212,12214],{"class":3597,"line":10601},[3595,12213,11942],{"class":4176},[3595,12215,11945],{"class":4104},[3568,12217],{},[3822,12219,12221,12222,3919],{"id":12220},"крок-6-логіка-безпеки-appsecuritypy","Крок 6: Логіка безпеки (",[3563,12223,10801],{},[3551,12225,12226],{},"Проста логіка для хешування паролів та формування токенів:",[3586,12228,12230],{"className":4423,"code":12229,"filename":10801,"language":4426,"meta":3591,"style":3591},"import bcrypt\nfrom datetime import datetime, timedelta\nfrom typing import Optional\nfrom jose import jwt\n\nSECRET_KEY = \"super-secret-key-for-demo-purposes-only\"\nALGORITHM = \"HS256\"\n\ndef get_password_hash(password: str) -> str:\n    pwd_bytes = password.encode(\"utf-8\")[:72]\n    salt = bcrypt.gensalt()\n    return bcrypt.hashpw(pwd_bytes, salt).decode(\"utf-8\")\n\ndef verify_password(plain_password: str, hashed_password: str) -> bool:\n    pwd_bytes = plain_password.encode(\"utf-8\")[:72]\n    return bcrypt.checkpw(pwd_bytes, hashed_password.encode(\"utf-8\"))\n\ndef create_access_token(data: dict, expires_delta: Optional[int] = None) -> str:\n    to_encode = data.copy()\n    expire = datetime.utcnow() + timedelta(minutes=expires_delta or 15)\n    to_encode.update({\"exp\": expire})\n    return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)\n",[3563,12231,12232,12239,12250,12260,12272,12276,12284,12292,12296,12318,12334,12339,12350,12354,12385,12398,12410,12414,12452,12457,12475,12486],{"__ignoreMap":3591},[3595,12233,12234,12236],{"class":3597,"line":3598},[3595,12235,4440],{"class":4433},[3595,12237,12238],{"class":4176}," bcrypt\n",[3595,12240,12241,12243,12245,12247],{"class":3597,"line":3604},[3595,12242,4434],{"class":4433},[3595,12244,4437],{"class":4176},[3595,12246,4440],{"class":4433},[3595,12248,12249],{"class":4176}," datetime, timedelta\n",[3595,12251,12252,12254,12256,12258],{"class":3597,"line":3610},[3595,12253,4434],{"class":4433},[3595,12255,4450],{"class":4176},[3595,12257,4440],{"class":4433},[3595,12259,4455],{"class":4176},[3595,12261,12262,12264,12267,12269],{"class":3597,"line":3616},[3595,12263,4434],{"class":4433},[3595,12265,12266],{"class":4176}," jose ",[3595,12268,4440],{"class":4433},[3595,12270,12271],{"class":4176}," jwt\n",[3595,12273,12274],{"class":3597,"line":3622},[3595,12275,3638],{"emptyLinePlaceholder":3637},[3595,12277,12278,12281],{"class":3597,"line":3628},[3595,12279,12280],{"class":4176},"SECRET_KEY = ",[3595,12282,12283],{"class":4067},"\"super-secret-key-for-demo-purposes-only\"\n",[3595,12285,12286,12289],{"class":3597,"line":3634},[3595,12287,12288],{"class":4176},"ALGORITHM = ",[3595,12290,12291],{"class":4067},"\"HS256\"\n",[3595,12293,12294],{"class":3597,"line":3641},[3595,12295,3638],{"emptyLinePlaceholder":3637},[3595,12297,12298,12300,12303,12305,12308,12310,12312,12314,12316],{"class":3597,"line":3647},[3595,12299,4916],{"class":4104},[3595,12301,12302],{"class":4064}," get_password_hash",[3595,12304,4483],{"class":4176},[3595,12306,12307],{"class":4188},"password",[3595,12309,4940],{"class":4176},[3595,12311,4497],{"class":4479},[3595,12313,4743],{"class":4176},[3595,12315,4497],{"class":4479},[3595,12317,4749],{"class":4176},[3595,12319,12320,12323,12326,12329,12332],{"class":3597,"line":3653},[3595,12321,12322],{"class":4176},"    pwd_bytes = password.encode(",[3595,12324,12325],{"class":4067},"\"utf-8\"",[3595,12327,12328],{"class":4176},")[:",[3595,12330,12331],{"class":4509},"72",[3595,12333,4218],{"class":4176},[3595,12335,12336],{"class":3597,"line":3659},[3595,12337,12338],{"class":4176},"    salt = bcrypt.gensalt()\n",[3595,12340,12341,12343,12346,12348],{"class":3597,"line":3664},[3595,12342,6192],{"class":4433},[3595,12344,12345],{"class":4176}," bcrypt.hashpw(pwd_bytes, salt).decode(",[3595,12347,12325],{"class":4067},[3595,12349,4534],{"class":4176},[3595,12351,12352],{"class":3597,"line":3670},[3595,12353,3638],{"emptyLinePlaceholder":3637},[3595,12355,12356,12358,12361,12363,12366,12368,12370,12372,12375,12377,12379,12381,12383],{"class":3597,"line":3676},[3595,12357,4916],{"class":4104},[3595,12359,12360],{"class":4064}," verify_password",[3595,12362,4483],{"class":4176},[3595,12364,12365],{"class":4188},"plain_password",[3595,12367,4940],{"class":4176},[3595,12369,4497],{"class":4479},[3595,12371,4513],{"class":4176},[3595,12373,12374],{"class":4188},"hashed_password",[3595,12376,4940],{"class":4176},[3595,12378,4497],{"class":4479},[3595,12380,4743],{"class":4176},[3595,12382,5863],{"class":4479},[3595,12384,4749],{"class":4176},[3595,12386,12387,12390,12392,12394,12396],{"class":3597,"line":3681},[3595,12388,12389],{"class":4176},"    pwd_bytes = plain_password.encode(",[3595,12391,12325],{"class":4067},[3595,12393,12328],{"class":4176},[3595,12395,12331],{"class":4509},[3595,12397,4218],{"class":4176},[3595,12399,12400,12402,12405,12407],{"class":3597,"line":3687},[3595,12401,6192],{"class":4433},[3595,12403,12404],{"class":4176}," bcrypt.checkpw(pwd_bytes, hashed_password.encode(",[3595,12406,12325],{"class":4067},[3595,12408,12409],{"class":4176},"))\n",[3595,12411,12412],{"class":3597,"line":3693},[3595,12413,3638],{"emptyLinePlaceholder":3637},[3595,12415,12416,12418,12421,12423,12426,12428,12431,12433,12436,12439,12441,12444,12446,12448,12450],{"class":3597,"line":3699},[3595,12417,4916],{"class":4104},[3595,12419,12420],{"class":4064}," create_access_token",[3595,12422,4483],{"class":4176},[3595,12424,12425],{"class":4188},"data",[3595,12427,4940],{"class":4176},[3595,12429,12430],{"class":4479},"dict",[3595,12432,4513],{"class":4176},[3595,12434,12435],{"class":4188},"expires_delta",[3595,12437,12438],{"class":4176},": Optional[",[3595,12440,4584],{"class":4479},[3595,12442,12443],{"class":4176},"] = ",[3595,12445,4547],{"class":4104},[3595,12447,4743],{"class":4176},[3595,12449,4497],{"class":4479},[3595,12451,4749],{"class":4176},[3595,12453,12454],{"class":3597,"line":3705},[3595,12455,12456],{"class":4176},"    to_encode = data.copy()\n",[3595,12458,12459,12462,12465,12468,12470,12473],{"class":3597,"line":3711},[3595,12460,12461],{"class":4176},"    expire = datetime.utcnow() + timedelta(",[3595,12463,12464],{"class":4188},"minutes",[3595,12466,12467],{"class":4176},"=expires_delta ",[3595,12469,4773],{"class":4433},[3595,12471,12472],{"class":4509}," 15",[3595,12474,4534],{"class":4176},[3595,12476,12477,12480,12483],{"class":3597,"line":3716},[3595,12478,12479],{"class":4176},"    to_encode.update({",[3595,12481,12482],{"class":4067},"\"exp\"",[3595,12484,12485],{"class":4176},": expire})\n",[3595,12487,12488,12490,12493,12496],{"class":3597,"line":3722},[3595,12489,6192],{"class":4433},[3595,12491,12492],{"class":4176}," jwt.encode(to_encode, SECRET_KEY, ",[3595,12494,12495],{"class":4188},"algorithm",[3595,12497,12498],{"class":4176},"=ALGORITHM)\n",[3568,12500],{},[3822,12502,12504,12505,3919],{"id":12503},"крок-7-веб-додаток-appmainpy","Крок 7: Веб-додаток (",[3563,12506,7009],{},[3551,12508,12509,12510,5476],{},"Наш API дозволяє реєструватися, авторизуватися та керувати своїми завданнями. Ми також додамо авторизаційний Middleware (через Dependency Injection ",[3563,12511,12512],{},"get_current_user",[3586,12514,12516],{"className":4423,"code":12515,"filename":7009,"language":4426,"meta":3591,"style":3591},"from fastapi import FastAPI, Depends, HTTPException, status\nfrom fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm\nfrom jose import jwt, JWTError\nfrom sqlalchemy.ext.asyncio import AsyncSession\nfrom sqlalchemy import select\nfrom app.database import get_db\nfrom app import models, schemas, security\n\napp = FastAPI(title=\"TaskForge Demo\")\noauth2_scheme = OAuth2PasswordBearer(tokenUrl=\"\u002Fauth\u002Flogin\")\n\n# Перевірка авторизації\nasync def get_current_user(token: str = Depends(oauth2_scheme), db: AsyncSession = Depends(get_db)) -> models.User:\n    credentials_exception = HTTPException(\n        status_code=status.HTTP_401_UNAUTHORIZED,\n        detail=\"Could not validate credentials\",\n        headers={\"WWW-Authenticate\": \"Bearer\"},\n    )\n    try:\n        payload = jwt.decode(token, security.SECRET_KEY, algorithms=[security.ALGORITHM])\n        email: str = payload.get(\"sub\")\n        if email is None:\n            raise credentials_exception\n    except JWTError:\n        raise credentials_exception\n        \n    result = await db.execute(select(models.User).where(models.User.email == email))\n    user = result.scalars().first()\n    if user is None:\n        raise credentials_exception\n    return user\n\n@app.get(\"\u002Fhealth\")\ndef health():\n    return {\"status\": \"healthy\"}\n\n@app.post(\"\u002Fauth\u002Fregister\", response_model=schemas.UserResponse, status_code=status.HTTP_201_CREATED)\nasync def register(user_data: schemas.UserCreate, db: AsyncSession = Depends(get_db)):\n    # Перевірка чи пошта вільна\n    existing = await db.execute(select(models.User).where(models.User.email == user_data.email))\n    if existing.scalars().first():\n        raise HTTPException(status_code=400, detail=\"Email already registered\")\n        \n    hashed = security.get_password_hash(user_data.password)\n    new_user = models.User(email=user_data.email, username=user_data.username, hashed_password=hashed)\n    db.add(new_user)\n    await db.commit()\n    await db.refresh(new_user)\n    return new_user\n\n@app.post(\"\u002Fauth\u002Flogin\", response_model=schemas.TokenResponse)\nasync def login(form_data: OAuth2PasswordRequestForm = Depends(), db: AsyncSession = Depends(get_db)):\n    result = await db.execute(select(models.User).where(models.User.email == form_data.username))\n    user = result.scalars().first()\n    if not user or not security.verify_password(form_data.password, user.hashed_password):\n        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=\"Incorrect email or password\")\n        \n    token = security.create_access_token(data={\"sub\": user.email})\n    return {\"access_token\": token, \"token_type\": \"bearer\"}\n\n@app.post(\"\u002Ftasks\", response_model=schemas.TaskResponse, status_code=status.HTTP_201_CREATED)\nasync def create_task(task_data: schemas.TaskCreate, current_user: models.User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):\n    new_task = models.Task(**task_data.model_dump(), owner_id=current_user.id)\n    db.add(new_task)\n    await db.commit()\n    await db.refresh(new_task)\n    return new_task\n\n@app.get(\"\u002Ftasks\", response_model=list[schemas.TaskResponse])\nasync def get_tasks(current_user: models.User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):\n    result = await db.execute(select(models.Task).where(models.Task.owner_id == current_user.id))\n    return result.scalars().all()\n",[3563,12517,12518,12529,12541,12552,12563,12574,12585,12597,12601,12615,12630,12634,12639,12666,12671,12679,12691,12709,12713,12720,12731,12746,12760,12767,12775,12782,12787,12797,12802,12815,12821,12828,12832,12842,12851,12865,12869,12891,12913,12918,12928,12935,12961,12965,12970,12991,12996,13003,13010,13017,13021,13036,13057,13066,13070,13085,13105,13109,13123,13146,13151,13172,13200,13212,13218,13225,13233,13241,13246,13262,13282,13292],{"__ignoreMap":3591},[3595,12519,12520,12522,12524,12526],{"class":3597,"line":3598},[3595,12521,4434],{"class":4433},[3595,12523,7021],{"class":4176},[3595,12525,4440],{"class":4433},[3595,12527,12528],{"class":4176}," FastAPI, Depends, HTTPException, status\n",[3595,12530,12531,12533,12536,12538],{"class":3597,"line":3604},[3595,12532,4434],{"class":4433},[3595,12534,12535],{"class":4176}," fastapi.security ",[3595,12537,4440],{"class":4433},[3595,12539,12540],{"class":4176}," OAuth2PasswordBearer, OAuth2PasswordRequestForm\n",[3595,12542,12543,12545,12547,12549],{"class":3597,"line":3610},[3595,12544,4434],{"class":4433},[3595,12546,12266],{"class":4176},[3595,12548,4440],{"class":4433},[3595,12550,12551],{"class":4176}," jwt, JWTError\n",[3595,12553,12554,12556,12558,12560],{"class":3597,"line":3616},[3595,12555,4434],{"class":4433},[3595,12557,7561],{"class":4176},[3595,12559,4440],{"class":4433},[3595,12561,12562],{"class":4176}," AsyncSession\n",[3595,12564,12565,12567,12569,12571],{"class":3597,"line":3622},[3595,12566,4434],{"class":4433},[3595,12568,11158],{"class":4176},[3595,12570,4440],{"class":4433},[3595,12572,12573],{"class":4176}," select\n",[3595,12575,12576,12578,12580,12582],{"class":3597,"line":3628},[3595,12577,4434],{"class":4433},[3595,12579,7573],{"class":4176},[3595,12581,4440],{"class":4433},[3595,12583,12584],{"class":4176}," get_db\n",[3595,12586,12587,12589,12592,12594],{"class":3597,"line":3634},[3595,12588,4434],{"class":4433},[3595,12590,12591],{"class":4176}," app ",[3595,12593,4440],{"class":4433},[3595,12595,12596],{"class":4176}," models, schemas, security\n",[3595,12598,12599],{"class":3597,"line":3641},[3595,12600,3638],{"emptyLinePlaceholder":3637},[3595,12602,12603,12606,12608,12610,12613],{"class":3597,"line":3647},[3595,12604,12605],{"class":4176},"app = FastAPI(",[3595,12607,4809],{"class":4188},[3595,12609,4506],{"class":4176},[3595,12611,12612],{"class":4067},"\"TaskForge Demo\"",[3595,12614,4534],{"class":4176},[3595,12616,12617,12620,12623,12625,12628],{"class":3597,"line":3653},[3595,12618,12619],{"class":4176},"oauth2_scheme = OAuth2PasswordBearer(",[3595,12621,12622],{"class":4188},"tokenUrl",[3595,12624,4506],{"class":4176},[3595,12626,12627],{"class":4067},"\"\u002Fauth\u002Flogin\"",[3595,12629,4534],{"class":4176},[3595,12631,12632],{"class":3597,"line":3659},[3595,12633,3638],{"emptyLinePlaceholder":3637},[3595,12635,12636],{"class":3597,"line":3664},[3595,12637,12638],{"class":4182},"# Перевірка авторизації\n",[3595,12640,12641,12643,12645,12648,12650,12653,12655,12657,12660,12663],{"class":3597,"line":3670},[3595,12642,6917],{"class":4104},[3595,12644,6920],{"class":4104},[3595,12646,12647],{"class":4064}," get_current_user",[3595,12649,4483],{"class":4176},[3595,12651,12652],{"class":4188},"token",[3595,12654,4940],{"class":4176},[3595,12656,4497],{"class":4479},[3595,12658,12659],{"class":4176}," = Depends(oauth2_scheme), ",[3595,12661,12662],{"class":4188},"db",[3595,12664,12665],{"class":4176},": AsyncSession = Depends(get_db)) -> models.User:\n",[3595,12667,12668],{"class":3597,"line":3676},[3595,12669,12670],{"class":4176},"    credentials_exception = HTTPException(\n",[3595,12672,12673,12676],{"class":3597,"line":3681},[3595,12674,12675],{"class":4188},"        status_code",[3595,12677,12678],{"class":4176},"=status.HTTP_401_UNAUTHORIZED,\n",[3595,12680,12681,12684,12686,12689],{"class":3597,"line":3687},[3595,12682,12683],{"class":4188},"        detail",[3595,12685,4506],{"class":4176},[3595,12687,12688],{"class":4067},"\"Could not validate credentials\"",[3595,12690,4366],{"class":4176},[3595,12692,12693,12696,12698,12701,12703,12706],{"class":3597,"line":3693},[3595,12694,12695],{"class":4188},"        headers",[3595,12697,11229],{"class":4176},[3595,12699,12700],{"class":4067},"\"WWW-Authenticate\"",[3595,12702,4940],{"class":4176},[3595,12704,12705],{"class":4067},"\"Bearer\"",[3595,12707,12708],{"class":4176},"},\n",[3595,12710,12711],{"class":3597,"line":3699},[3595,12712,9055],{"class":4176},[3595,12714,12715,12718],{"class":3597,"line":3705},[3595,12716,12717],{"class":4433},"    try",[3595,12719,4749],{"class":4176},[3595,12721,12722,12725,12728],{"class":3597,"line":3711},[3595,12723,12724],{"class":4176},"        payload = jwt.decode(token, security.SECRET_KEY, ",[3595,12726,12727],{"class":4188},"algorithms",[3595,12729,12730],{"class":4176},"=[security.ALGORITHM])\n",[3595,12732,12733,12736,12738,12741,12744],{"class":3597,"line":3716},[3595,12734,12735],{"class":4176},"        email: ",[3595,12737,4497],{"class":4479},[3595,12739,12740],{"class":4176}," = payload.get(",[3595,12742,12743],{"class":4067},"\"sub\"",[3595,12745,4534],{"class":4176},[3595,12747,12748,12750,12753,12755,12758],{"class":3597,"line":3722},[3595,12749,4675],{"class":4433},[3595,12751,12752],{"class":4176}," email ",[3595,12754,8447],{"class":4104},[3595,12756,12757],{"class":4104}," None",[3595,12759,4749],{"class":4176},[3595,12761,12762,12764],{"class":3597,"line":3728},[3595,12763,4689],{"class":4433},[3595,12765,12766],{"class":4176}," credentials_exception\n",[3595,12768,12769,12772],{"class":3597,"line":3734},[3595,12770,12771],{"class":4433},"    except",[3595,12773,12774],{"class":4176}," JWTError:\n",[3595,12776,12777,12780],{"class":3597,"line":3740},[3595,12778,12779],{"class":4433},"        raise",[3595,12781,12766],{"class":4176},[3595,12783,12784],{"class":3597,"line":3745},[3595,12785,12786],{"class":4176},"        \n",[3595,12788,12789,12792,12794],{"class":3597,"line":3750},[3595,12790,12791],{"class":4176},"    result = ",[3595,12793,7184],{"class":4433},[3595,12795,12796],{"class":4176}," db.execute(select(models.User).where(models.User.email == email))\n",[3595,12798,12799],{"class":3597,"line":3756},[3595,12800,12801],{"class":4176},"    user = result.scalars().first()\n",[3595,12803,12804,12806,12809,12811,12813],{"class":3597,"line":3762},[3595,12805,5670],{"class":4433},[3595,12807,12808],{"class":4176}," user ",[3595,12810,8447],{"class":4104},[3595,12812,12757],{"class":4104},[3595,12814,4749],{"class":4176},[3595,12816,12817,12819],{"class":3597,"line":3768},[3595,12818,12779],{"class":4433},[3595,12820,12766],{"class":4176},[3595,12822,12823,12825],{"class":3597,"line":3774},[3595,12824,6192],{"class":4433},[3595,12826,12827],{"class":4176}," user\n",[3595,12829,12830],{"class":3597,"line":3779},[3595,12831,3638],{"emptyLinePlaceholder":3637},[3595,12833,12834,12836,12838,12840],{"class":3597,"line":3784},[3595,12835,7044],{"class":4064},[3595,12837,4483],{"class":4176},[3595,12839,7049],{"class":4067},[3595,12841,4534],{"class":4176},[3595,12843,12844,12846,12849],{"class":3597,"line":5137},[3595,12845,4916],{"class":4104},[3595,12847,12848],{"class":4064}," health",[3595,12850,4922],{"class":4176},[3595,12852,12853,12855,12857,12859,12861,12863],{"class":3597,"line":5142},[3595,12854,6192],{"class":4433},[3595,12856,7067],{"class":4176},[3595,12858,7070],{"class":4067},[3595,12860,4940],{"class":4176},[3595,12862,7075],{"class":4067},[3595,12864,5909],{"class":4176},[3595,12866,12867],{"class":3597,"line":5148},[3595,12868,3638],{"emptyLinePlaceholder":3637},[3595,12870,12871,12873,12875,12878,12880,12883,12886,12888],{"class":3597,"line":5154},[3595,12872,8979],{"class":4064},[3595,12874,4483],{"class":4176},[3595,12876,12877],{"class":4067},"\"\u002Fauth\u002Fregister\"",[3595,12879,4513],{"class":4176},[3595,12881,12882],{"class":4188},"response_model",[3595,12884,12885],{"class":4176},"=schemas.UserResponse, ",[3595,12887,7279],{"class":4188},[3595,12889,12890],{"class":4176},"=status.HTTP_201_CREATED)\n",[3595,12892,12893,12895,12897,12900,12902,12905,12908,12910],{"class":3597,"line":5167},[3595,12894,6917],{"class":4104},[3595,12896,6920],{"class":4104},[3595,12898,12899],{"class":4064}," register",[3595,12901,4483],{"class":4176},[3595,12903,12904],{"class":4188},"user_data",[3595,12906,12907],{"class":4176},": schemas.UserCreate, ",[3595,12909,12662],{"class":4188},[3595,12911,12912],{"class":4176},": AsyncSession = Depends(get_db)):\n",[3595,12914,12915],{"class":3597,"line":5192},[3595,12916,12917],{"class":4182},"    # Перевірка чи пошта вільна\n",[3595,12919,12920,12923,12925],{"class":3597,"line":7885},[3595,12921,12922],{"class":4176},"    existing = ",[3595,12924,7184],{"class":4433},[3595,12926,12927],{"class":4176}," db.execute(select(models.User).where(models.User.email == user_data.email))\n",[3595,12929,12930,12932],{"class":3597,"line":7893},[3595,12931,5670],{"class":4433},[3595,12933,12934],{"class":4176}," existing.scalars().first():\n",[3595,12936,12937,12939,12942,12944,12946,12949,12951,12954,12956,12959],{"class":3597,"line":10589},[3595,12938,12779],{"class":4433},[3595,12940,12941],{"class":4176}," HTTPException(",[3595,12943,7279],{"class":4188},[3595,12945,4506],{"class":4176},[3595,12947,12948],{"class":4509},"400",[3595,12950,4513],{"class":4176},[3595,12952,12953],{"class":4188},"detail",[3595,12955,4506],{"class":4176},[3595,12957,12958],{"class":4067},"\"Email already registered\"",[3595,12960,4534],{"class":4176},[3595,12962,12963],{"class":3597,"line":10595},[3595,12964,12786],{"class":4176},[3595,12966,12967],{"class":3597,"line":10601},[3595,12968,12969],{"class":4176},"    hashed = security.get_password_hash(user_data.password)\n",[3595,12971,12972,12975,12977,12980,12983,12986,12988],{"class":3597,"line":10606},[3595,12973,12974],{"class":4176},"    new_user = models.User(",[3595,12976,8848],{"class":4188},[3595,12978,12979],{"class":4176},"=user_data.email, ",[3595,12981,12982],{"class":4188},"username",[3595,12984,12985],{"class":4176},"=user_data.username, ",[3595,12987,12374],{"class":4188},[3595,12989,12990],{"class":4176},"=hashed)\n",[3595,12992,12993],{"class":3597,"line":10618},[3595,12994,12995],{"class":4176},"    db.add(new_user)\n",[3595,12997,12998,13000],{"class":3597,"line":10628},[3595,12999,7879],{"class":4433},[3595,13001,13002],{"class":4176}," db.commit()\n",[3595,13004,13005,13007],{"class":3597,"line":10633},[3595,13006,7879],{"class":4433},[3595,13008,13009],{"class":4176}," db.refresh(new_user)\n",[3595,13011,13012,13014],{"class":3597,"line":10645},[3595,13013,6192],{"class":4433},[3595,13015,13016],{"class":4176}," new_user\n",[3595,13018,13019],{"class":3597,"line":10655},[3595,13020,3638],{"emptyLinePlaceholder":3637},[3595,13022,13023,13025,13027,13029,13031,13033],{"class":3597,"line":10660},[3595,13024,8979],{"class":4064},[3595,13026,4483],{"class":4176},[3595,13028,12627],{"class":4067},[3595,13030,4513],{"class":4176},[3595,13032,12882],{"class":4188},[3595,13034,13035],{"class":4176},"=schemas.TokenResponse)\n",[3595,13037,13038,13040,13042,13045,13047,13050,13053,13055],{"class":3597,"line":10672},[3595,13039,6917],{"class":4104},[3595,13041,6920],{"class":4104},[3595,13043,13044],{"class":4064}," login",[3595,13046,4483],{"class":4176},[3595,13048,13049],{"class":4188},"form_data",[3595,13051,13052],{"class":4176},": OAuth2PasswordRequestForm = Depends(), ",[3595,13054,12662],{"class":4188},[3595,13056,12912],{"class":4176},[3595,13058,13059,13061,13063],{"class":3597,"line":10682},[3595,13060,12791],{"class":4176},[3595,13062,7184],{"class":4433},[3595,13064,13065],{"class":4176}," db.execute(select(models.User).where(models.User.email == form_data.username))\n",[3595,13067,13068],{"class":3597,"line":10687},[3595,13069,12801],{"class":4176},[3595,13071,13072,13074,13076,13078,13080,13082],{"class":3597,"line":10699},[3595,13073,5670],{"class":4433},[3595,13075,8450],{"class":4104},[3595,13077,12808],{"class":4176},[3595,13079,4773],{"class":4104},[3595,13081,8450],{"class":4104},[3595,13083,13084],{"class":4176}," security.verify_password(form_data.password, user.hashed_password):\n",[3595,13086,13087,13089,13091,13093,13096,13098,13100,13103],{"class":3597,"line":10707},[3595,13088,12779],{"class":4433},[3595,13090,12941],{"class":4176},[3595,13092,7279],{"class":4188},[3595,13094,13095],{"class":4176},"=status.HTTP_401_UNAUTHORIZED, ",[3595,13097,12953],{"class":4188},[3595,13099,4506],{"class":4176},[3595,13101,13102],{"class":4067},"\"Incorrect email or password\"",[3595,13104,4534],{"class":4176},[3595,13106,13107],{"class":3597,"line":10713},[3595,13108,12786],{"class":4176},[3595,13110,13111,13114,13116,13118,13120],{"class":3597,"line":10724},[3595,13112,13113],{"class":4176},"    token = security.create_access_token(",[3595,13115,12425],{"class":4188},[3595,13117,11229],{"class":4176},[3595,13119,12743],{"class":4067},[3595,13121,13122],{"class":4176},": user.email})\n",[3595,13124,13126,13128,13130,13133,13136,13139,13141,13144],{"class":3597,"line":13125},59,[3595,13127,6192],{"class":4433},[3595,13129,7067],{"class":4176},[3595,13131,13132],{"class":4067},"\"access_token\"",[3595,13134,13135],{"class":4176},": token, ",[3595,13137,13138],{"class":4067},"\"token_type\"",[3595,13140,4940],{"class":4176},[3595,13142,13143],{"class":4067},"\"bearer\"",[3595,13145,5909],{"class":4176},[3595,13147,13149],{"class":3597,"line":13148},60,[3595,13150,3638],{"emptyLinePlaceholder":3637},[3595,13152,13154,13156,13158,13161,13163,13165,13168,13170],{"class":3597,"line":13153},61,[3595,13155,8979],{"class":4064},[3595,13157,4483],{"class":4176},[3595,13159,13160],{"class":4067},"\"\u002Ftasks\"",[3595,13162,4513],{"class":4176},[3595,13164,12882],{"class":4188},[3595,13166,13167],{"class":4176},"=schemas.TaskResponse, ",[3595,13169,7279],{"class":4188},[3595,13171,12890],{"class":4176},[3595,13173,13175,13177,13179,13182,13184,13187,13190,13193,13196,13198],{"class":3597,"line":13174},62,[3595,13176,6917],{"class":4104},[3595,13178,6920],{"class":4104},[3595,13180,13181],{"class":4064}," create_task",[3595,13183,4483],{"class":4176},[3595,13185,13186],{"class":4188},"task_data",[3595,13188,13189],{"class":4176},": schemas.TaskCreate, ",[3595,13191,13192],{"class":4188},"current_user",[3595,13194,13195],{"class":4176},": models.User = Depends(get_current_user), ",[3595,13197,12662],{"class":4188},[3595,13199,12912],{"class":4176},[3595,13201,13203,13206,13209],{"class":3597,"line":13202},63,[3595,13204,13205],{"class":4176},"    new_task = models.Task(**task_data.model_dump(), ",[3595,13207,13208],{"class":4188},"owner_id",[3595,13210,13211],{"class":4176},"=current_user.id)\n",[3595,13213,13215],{"class":3597,"line":13214},64,[3595,13216,13217],{"class":4176},"    db.add(new_task)\n",[3595,13219,13221,13223],{"class":3597,"line":13220},65,[3595,13222,7879],{"class":4433},[3595,13224,13002],{"class":4176},[3595,13226,13228,13230],{"class":3597,"line":13227},66,[3595,13229,7879],{"class":4433},[3595,13231,13232],{"class":4176}," db.refresh(new_task)\n",[3595,13234,13236,13238],{"class":3597,"line":13235},67,[3595,13237,6192],{"class":4433},[3595,13239,13240],{"class":4176}," new_task\n",[3595,13242,13244],{"class":3597,"line":13243},68,[3595,13245,3638],{"emptyLinePlaceholder":3637},[3595,13247,13249,13251,13253,13255,13257,13259],{"class":3597,"line":13248},69,[3595,13250,7044],{"class":4064},[3595,13252,4483],{"class":4176},[3595,13254,13160],{"class":4067},[3595,13256,4513],{"class":4176},[3595,13258,12882],{"class":4188},[3595,13260,13261],{"class":4176},"=list[schemas.TaskResponse])\n",[3595,13263,13265,13267,13269,13272,13274,13276,13278,13280],{"class":3597,"line":13264},70,[3595,13266,6917],{"class":4104},[3595,13268,6920],{"class":4104},[3595,13270,13271],{"class":4064}," get_tasks",[3595,13273,4483],{"class":4176},[3595,13275,13192],{"class":4188},[3595,13277,13195],{"class":4176},[3595,13279,12662],{"class":4188},[3595,13281,12912],{"class":4176},[3595,13283,13285,13287,13289],{"class":3597,"line":13284},71,[3595,13286,12791],{"class":4176},[3595,13288,7184],{"class":4433},[3595,13290,13291],{"class":4176}," db.execute(select(models.Task).where(models.Task.owner_id == current_user.id))\n",[3595,13293,13295,13297],{"class":3597,"line":13294},72,[3595,13296,6192],{"class":4433},[3595,13298,13299],{"class":4176}," result.scalars().all()\n",[3568,13301],{},[3822,13303,13305,13306,3919],{"id":13304},"крок-8-налаштування-pytest-testsconftestpy","Крок 8: Налаштування pytest (",[3563,13307,6776],{},[3551,13309,13310,13311,13314,13315,13317],{},"Наш тестовий конфіг налаштовує тестову базу даних SQLite в пам'яті (",[3563,13312,13313],{},"sqlite+aiosqlite:\u002F\u002F\u002F:memory:",") та підміняє провайдер залежності ",[3563,13316,8097],{}," в нашому додатку:",[3586,13319,13321],{"className":4423,"code":13320,"filename":6776,"language":4426,"meta":3591,"style":3591},"import pytest\nfrom httpx import AsyncClient\nfrom sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession\nfrom app.database import Base, get_db\nfrom app.main import app\n\n# SQLite в пам'яті для ізоляції тестових прогонів\nTEST_DATABASE_URL = \"sqlite+aiosqlite:\u002F\u002F\u002F:memory:\"\n\nengine = create_async_engine(TEST_DATABASE_URL, connect_args={\"check_same_thread\": False})\nTestingSessionLocal = async_sessionmaker(autocommit=False, autoflush=False, bind=engine, expire_on_commit=False)\n\n@pytest.fixture(scope=\"session\", autouse=True)\nasync def setup_db():\n    \"\"\"Створює схему в пам'яті один раз для всієї сесії тестів.\"\"\"\n    async with engine.begin() as conn:\n        await conn.run_sync(Base.metadata.create_all)\n    yield\n    async with engine.begin() as conn:\n        await conn.run_sync(Base.metadata.drop_all)\n\n@pytest.fixture(scope=\"function\")\nasync def db_session() -> AsyncSession:\n    \"\"\"Створює окрему транзакцію для кожного окремого тесту та робить rollback.\"\"\"\n    connection = await engine.connect()\n    transaction = await connection.begin()\n    session = TestingSessionLocal(bind=connection)\n    \n    yield session\n    \n    await session.close()\n    await transaction.rollback()\n    await connection.close()\n\n@pytest.fixture(scope=\"function\")\nasync def client(db_session: AsyncSession) -> AsyncClient:\n    \"\"\"Створює тестовий клієнт із підміненою сесією БД.\"\"\"\n    async def _override_get_db():\n        yield db_session\n\n    app.dependency_overrides[get_db] = _override_get_db\n    \n    async with AsyncClient(app=app, base_url=\"http:\u002F\u002Ftest\") as ac:\n        yield ac\n        \n    app.dependency_overrides.clear()\n",[3563,13322,13323,13329,13339,13349,13359,13369,13373,13378,13385,13389,13405,13437,13441,13463,13474,13479,13491,13497,13501,13513,13519,13523,13537,13547,13552,13560,13568,13576,13581,13587,13591,13597,13603,13609,13613,13627,13642,13647,13657,13663,13667,13671,13675,13699,13705,13709],{"__ignoreMap":3591},[3595,13324,13325,13327],{"class":3597,"line":3598},[3595,13326,4440],{"class":4433},[3595,13328,4879],{"class":4176},[3595,13330,13331,13333,13335,13337],{"class":3597,"line":3604},[3595,13332,4434],{"class":4433},[3595,13334,6806],{"class":4176},[3595,13336,4440],{"class":4433},[3595,13338,6811],{"class":4176},[3595,13340,13341,13343,13345,13347],{"class":3597,"line":3610},[3595,13342,4434],{"class":4433},[3595,13344,7561],{"class":4176},[3595,13346,4440],{"class":4433},[3595,13348,7566],{"class":4176},[3595,13350,13351,13353,13355,13357],{"class":3597,"line":3616},[3595,13352,4434],{"class":4433},[3595,13354,7573],{"class":4176},[3595,13356,4440],{"class":4433},[3595,13358,7578],{"class":4176},[3595,13360,13361,13363,13365,13367],{"class":3597,"line":3622},[3595,13362,4434],{"class":4433},[3595,13364,6818],{"class":4176},[3595,13366,4440],{"class":4433},[3595,13368,7589],{"class":4176},[3595,13370,13371],{"class":3597,"line":3628},[3595,13372,3638],{"emptyLinePlaceholder":3637},[3595,13374,13375],{"class":3597,"line":3634},[3595,13376,13377],{"class":4182},"# SQLite в пам'яті для ізоляції тестових прогонів\n",[3595,13379,13380,13382],{"class":3597,"line":3641},[3595,13381,7603],{"class":4176},[3595,13383,13384],{"class":4067},"\"sqlite+aiosqlite:\u002F\u002F\u002F:memory:\"\n",[3595,13386,13387],{"class":3597,"line":3647},[3595,13388,3638],{"emptyLinePlaceholder":3637},[3595,13390,13391,13393,13395,13397,13399,13401,13403],{"class":3597,"line":3653},[3595,13392,7620],{"class":4176},[3595,13394,11226],{"class":4188},[3595,13396,11229],{"class":4176},[3595,13398,11232],{"class":4067},[3595,13400,4940],{"class":4176},[3595,13402,5568],{"class":4104},[3595,13404,11239],{"class":4176},[3595,13406,13407,13409,13411,13413,13415,13417,13419,13421,13423,13425,13427,13429,13431,13433,13435],{"class":3597,"line":3659},[3595,13408,7634],{"class":4176},[3595,13410,7637],{"class":4188},[3595,13412,4506],{"class":4176},[3595,13414,5568],{"class":4104},[3595,13416,4513],{"class":4176},[3595,13418,7646],{"class":4188},[3595,13420,4506],{"class":4176},[3595,13422,5568],{"class":4104},[3595,13424,4513],{"class":4176},[3595,13426,7655],{"class":4188},[3595,13428,7658],{"class":4176},[3595,13430,7661],{"class":4188},[3595,13432,4506],{"class":4176},[3595,13434,5568],{"class":4104},[3595,13436,4534],{"class":4176},[3595,13438,13439],{"class":3597,"line":3664},[3595,13440,3638],{"emptyLinePlaceholder":3637},[3595,13442,13443,13445,13447,13449,13451,13453,13455,13457,13459,13461],{"class":3597,"line":3670},[3595,13444,6149],{"class":4064},[3595,13446,4483],{"class":4176},[3595,13448,6296],{"class":4188},[3595,13450,4506],{"class":4176},[3595,13452,7684],{"class":4067},[3595,13454,4513],{"class":4176},[3595,13456,7689],{"class":4188},[3595,13458,4506],{"class":4176},[3595,13460,5525],{"class":4104},[3595,13462,4534],{"class":4176},[3595,13464,13465,13467,13469,13472],{"class":3597,"line":3676},[3595,13466,6917],{"class":4104},[3595,13468,6920],{"class":4104},[3595,13470,13471],{"class":4064}," setup_db",[3595,13473,4922],{"class":4176},[3595,13475,13476],{"class":3597,"line":3681},[3595,13477,13478],{"class":4067},"    \"\"\"Створює схему в пам'яті один раз для всієї сесії тестів.\"\"\"\n",[3595,13480,13481,13483,13485,13487,13489],{"class":3597,"line":3687},[3595,13482,6940],{"class":4433},[3595,13484,6943],{"class":4433},[3595,13486,7720],{"class":4176},[3595,13488,5126],{"class":4433},[3595,13490,7725],{"class":4176},[3595,13492,13493,13495],{"class":3597,"line":3693},[3595,13494,7735],{"class":4433},[3595,13496,7738],{"class":4176},[3595,13498,13499],{"class":3597,"line":3699},[3595,13500,7747],{"class":4433},[3595,13502,13503,13505,13507,13509,13511],{"class":3597,"line":3705},[3595,13504,6940],{"class":4433},[3595,13506,6943],{"class":4433},[3595,13508,7720],{"class":4176},[3595,13510,5126],{"class":4433},[3595,13512,7725],{"class":4176},[3595,13514,13515,13517],{"class":3597,"line":3711},[3595,13516,7735],{"class":4433},[3595,13518,7775],{"class":4176},[3595,13520,13521],{"class":3597,"line":3716},[3595,13522,3638],{"emptyLinePlaceholder":3637},[3595,13524,13525,13527,13529,13531,13533,13535],{"class":3597,"line":3722},[3595,13526,6149],{"class":4064},[3595,13528,4483],{"class":4176},[3595,13530,6296],{"class":4188},[3595,13532,4506],{"class":4176},[3595,13534,6408],{"class":4067},[3595,13536,4534],{"class":4176},[3595,13538,13539,13541,13543,13545],{"class":3597,"line":3728},[3595,13540,6917],{"class":4104},[3595,13542,6920],{"class":4104},[3595,13544,7802],{"class":4064},[3595,13546,7805],{"class":4176},[3595,13548,13549],{"class":3597,"line":3734},[3595,13550,13551],{"class":4067},"    \"\"\"Створює окрему транзакцію для кожного окремого тесту та робить rollback.\"\"\"\n",[3595,13553,13554,13556,13558],{"class":3597,"line":3740},[3595,13555,7815],{"class":4176},[3595,13557,7184],{"class":4433},[3595,13559,7820],{"class":4176},[3595,13561,13562,13564,13566],{"class":3597,"line":3745},[3595,13563,7830],{"class":4176},[3595,13565,7184],{"class":4433},[3595,13567,7835],{"class":4176},[3595,13569,13570,13572,13574],{"class":3597,"line":3750},[3595,13571,7849],{"class":4176},[3595,13573,7655],{"class":4188},[3595,13575,7854],{"class":4176},[3595,13577,13578],{"class":3597,"line":3756},[3595,13579,13580],{"class":4176},"    \n",[3595,13582,13583,13585],{"class":3597,"line":3762},[3595,13584,6477],{"class":4433},[3595,13586,7865],{"class":4176},[3595,13588,13589],{"class":3597,"line":3768},[3595,13590,13580],{"class":4176},[3595,13592,13593,13595],{"class":3597,"line":3774},[3595,13594,7879],{"class":4433},[3595,13596,7882],{"class":4176},[3595,13598,13599,13601],{"class":3597,"line":3779},[3595,13600,7879],{"class":4433},[3595,13602,7890],{"class":4176},[3595,13604,13605,13607],{"class":3597,"line":3784},[3595,13606,7879],{"class":4433},[3595,13608,7898],{"class":4176},[3595,13610,13611],{"class":3597,"line":5137},[3595,13612,3638],{"emptyLinePlaceholder":3637},[3595,13614,13615,13617,13619,13621,13623,13625],{"class":3597,"line":5142},[3595,13616,6149],{"class":4064},[3595,13618,4483],{"class":4176},[3595,13620,6296],{"class":4188},[3595,13622,4506],{"class":4176},[3595,13624,6408],{"class":4067},[3595,13626,4534],{"class":4176},[3595,13628,13629,13631,13633,13635,13637,13639],{"class":3597,"line":5148},[3595,13630,6917],{"class":4104},[3595,13632,6920],{"class":4104},[3595,13634,6857],{"class":4064},[3595,13636,4483],{"class":4176},[3595,13638,7918],{"class":4188},[3595,13640,13641],{"class":4176},": AsyncSession) -> AsyncClient:\n",[3595,13643,13644],{"class":3597,"line":5154},[3595,13645,13646],{"class":4067},"    \"\"\"Створює тестовий клієнт із підміненою сесією БД.\"\"\"\n",[3595,13648,13649,13651,13653,13655],{"class":3597,"line":5167},[3595,13650,6940],{"class":4104},[3595,13652,6920],{"class":4104},[3595,13654,8214],{"class":4064},[3595,13656,4922],{"class":4176},[3595,13658,13659,13661],{"class":3597,"line":5192},[3595,13660,6886],{"class":4433},[3595,13662,8223],{"class":4176},[3595,13664,13665],{"class":3597,"line":7885},[3595,13666,3638],{"emptyLinePlaceholder":3637},[3595,13668,13669],{"class":3597,"line":7893},[3595,13670,8237],{"class":4176},[3595,13672,13673],{"class":3597,"line":10589},[3595,13674,13580],{"class":4176},[3595,13676,13677,13679,13681,13683,13685,13687,13689,13691,13693,13695,13697],{"class":3597,"line":10595},[3595,13678,6940],{"class":4433},[3595,13680,6943],{"class":4433},[3595,13682,6946],{"class":4176},[3595,13684,6949],{"class":4188},[3595,13686,6952],{"class":4176},[3595,13688,6955],{"class":4188},[3595,13690,4506],{"class":4176},[3595,13692,6960],{"class":4067},[3595,13694,6448],{"class":4176},[3595,13696,5126],{"class":4433},[3595,13698,6967],{"class":4176},[3595,13700,13701,13703],{"class":3597,"line":10601},[3595,13702,6886],{"class":4433},[3595,13704,6974],{"class":4176},[3595,13706,13707],{"class":3597,"line":10606},[3595,13708,12786],{"class":4176},[3595,13710,13711],{"class":3597,"line":10618},[3595,13712,8285],{"class":4176},[3568,13714],{},[3822,13716,13718,13719,3919],{"id":13717},"крок-9-фабрики-даних-testsfactoriespy","Крок 9: Фабрики даних (",[3563,13720,9382],{},[3551,13722,13723],{},"Ми налаштовуємо фабрики для автоматичного наповнення БД користувачами та завданнями:",[3586,13725,13727],{"className":4423,"code":13726,"filename":9382,"language":4426,"meta":3591,"style":3591},"import factory\nfrom factory import Faker\nfrom app import models\n\nclass UserFactory(factory.alchemy.SQLAlchemyModelFactory):\n    class Meta:\n        model = models.User\n        sqlalchemy_session = None  # буде налаштовано у фікстурі\n        sqlalchemy_session_persistence = \"commit\"\n\n    id = factory.Sequence(lambda n: n + 1)\n    email = Faker(\"email\")\n    username = Faker(\"user_name\")\n    hashed_password = \"dummy_hashed_password\"  # не принципово для базових тестів\n\nclass TaskFactory(factory.alchemy.SQLAlchemyModelFactory):\n    class Meta:\n        model = models.Task\n        sqlalchemy_session = None\n        sqlalchemy_session_persistence = \"commit\"\n\n    id = factory.Sequence(lambda n: n + 1)\n    title = Faker(\"sentence\", nb_words=3)\n    description = Faker(\"paragraph\", nb_sentences=2)\n    priority = factory.Iterator([1, 2, 3])\n    # Зв'язуємо з UserFactory\n    owner = factory.SubFactory(UserFactory)\n",[3563,13728,13729,13735,13745,13756,13760,13781,13789,13794,13803,13809,13813,13829,13839,13849,13860,13864,13884,13892,13897,13904,13910,13914,13930,13946,13962,13978,13983],{"__ignoreMap":3591},[3595,13730,13731,13733],{"class":3597,"line":3598},[3595,13732,4440],{"class":4433},[3595,13734,9391],{"class":4176},[3595,13736,13737,13739,13741,13743],{"class":3597,"line":3604},[3595,13738,4434],{"class":4433},[3595,13740,9398],{"class":4176},[3595,13742,4440],{"class":4433},[3595,13744,9403],{"class":4176},[3595,13746,13747,13749,13751,13753],{"class":3597,"line":3610},[3595,13748,4434],{"class":4433},[3595,13750,12591],{"class":4176},[3595,13752,4440],{"class":4433},[3595,13754,13755],{"class":4176}," models\n",[3595,13757,13758],{"class":3597,"line":3616},[3595,13759,3638],{"emptyLinePlaceholder":3637},[3595,13761,13762,13764,13767,13769,13771,13773,13775,13777,13779],{"class":3597,"line":3622},[3595,13763,4476],{"class":4104},[3595,13765,13766],{"class":4479}," UserFactory",[3595,13768,4483],{"class":4176},[3595,13770,9449],{"class":4479},[3595,13772,3929],{"class":4176},[3595,13774,9454],{"class":4479},[3595,13776,3929],{"class":4176},[3595,13778,9459],{"class":4479},[3595,13780,4489],{"class":4176},[3595,13782,13783,13785,13787],{"class":3597,"line":3628},[3595,13784,9466],{"class":4104},[3595,13786,9469],{"class":4479},[3595,13788,4749],{"class":4176},[3595,13790,13791],{"class":3597,"line":3634},[3595,13792,13793],{"class":4176},"        model = models.User\n",[3595,13795,13796,13798,13800],{"class":3597,"line":3641},[3595,13797,9486],{"class":4176},[3595,13799,4547],{"class":4104},[3595,13801,13802],{"class":4182},"  # буде налаштовано у фікстурі\n",[3595,13804,13805,13807],{"class":3597,"line":3647},[3595,13806,9496],{"class":4176},[3595,13808,9499],{"class":4067},[3595,13810,13811],{"class":3597,"line":3653},[3595,13812,3638],{"emptyLinePlaceholder":3637},[3595,13814,13815,13817,13819,13821,13823,13825,13827],{"class":3597,"line":3659},[3595,13816,9513],{"class":4064},[3595,13818,9516],{"class":4176},[3595,13820,9519],{"class":4104},[3595,13822,9522],{"class":4188},[3595,13824,9525],{"class":4176},[3595,13826,4595],{"class":4509},[3595,13828,4534],{"class":4176},[3595,13830,13831,13834,13837],{"class":3597,"line":3664},[3595,13832,13833],{"class":4176},"    email = Faker(",[3595,13835,13836],{"class":4067},"\"email\"",[3595,13838,4534],{"class":4176},[3595,13840,13841,13844,13847],{"class":3597,"line":3670},[3595,13842,13843],{"class":4176},"    username = Faker(",[3595,13845,13846],{"class":4067},"\"user_name\"",[3595,13848,4534],{"class":4176},[3595,13850,13851,13854,13857],{"class":3597,"line":3676},[3595,13852,13853],{"class":4176},"    hashed_password = ",[3595,13855,13856],{"class":4067},"\"dummy_hashed_password\"",[3595,13858,13859],{"class":4182},"  # не принципово для базових тестів\n",[3595,13861,13862],{"class":3597,"line":3681},[3595,13863,3638],{"emptyLinePlaceholder":3637},[3595,13865,13866,13868,13870,13872,13874,13876,13878,13880,13882],{"class":3597,"line":3687},[3595,13867,4476],{"class":4104},[3595,13869,9444],{"class":4479},[3595,13871,4483],{"class":4176},[3595,13873,9449],{"class":4479},[3595,13875,3929],{"class":4176},[3595,13877,9454],{"class":4479},[3595,13879,3929],{"class":4176},[3595,13881,9459],{"class":4479},[3595,13883,4489],{"class":4176},[3595,13885,13886,13888,13890],{"class":3597,"line":3693},[3595,13887,9466],{"class":4104},[3595,13889,9469],{"class":4479},[3595,13891,4749],{"class":4176},[3595,13893,13894],{"class":3597,"line":3699},[3595,13895,13896],{"class":4176},"        model = models.Task\n",[3595,13898,13899,13901],{"class":3597,"line":3705},[3595,13900,9486],{"class":4176},[3595,13902,13903],{"class":4104},"None\n",[3595,13905,13906,13908],{"class":3597,"line":3711},[3595,13907,9496],{"class":4176},[3595,13909,9499],{"class":4067},[3595,13911,13912],{"class":3597,"line":3716},[3595,13913,3638],{"emptyLinePlaceholder":3637},[3595,13915,13916,13918,13920,13922,13924,13926,13928],{"class":3597,"line":3722},[3595,13917,9513],{"class":4064},[3595,13919,9516],{"class":4176},[3595,13921,9519],{"class":4104},[3595,13923,9522],{"class":4188},[3595,13925,9525],{"class":4176},[3595,13927,4595],{"class":4509},[3595,13929,4534],{"class":4176},[3595,13931,13932,13934,13936,13938,13940,13942,13944],{"class":3597,"line":3728},[3595,13933,9543],{"class":4176},[3595,13935,9546],{"class":4067},[3595,13937,4513],{"class":4176},[3595,13939,9551],{"class":4188},[3595,13941,4506],{"class":4176},[3595,13943,4510],{"class":4509},[3595,13945,4534],{"class":4176},[3595,13947,13948,13950,13952,13954,13956,13958,13960],{"class":3597,"line":3734},[3595,13949,9563],{"class":4176},[3595,13951,9566],{"class":4067},[3595,13953,4513],{"class":4176},[3595,13955,9571],{"class":4188},[3595,13957,4506],{"class":4176},[3595,13959,8891],{"class":4509},[3595,13961,4534],{"class":4176},[3595,13963,13964,13966,13968,13970,13972,13974,13976],{"class":3597,"line":3740},[3595,13965,9582],{"class":4176},[3595,13967,4595],{"class":4509},[3595,13969,4513],{"class":4176},[3595,13971,8891],{"class":4509},[3595,13973,4513],{"class":4176},[3595,13975,4510],{"class":4509},[3595,13977,9603],{"class":4176},[3595,13979,13980],{"class":3597,"line":3745},[3595,13981,13982],{"class":4182},"    # Зв'язуємо з UserFactory\n",[3595,13984,13985],{"class":3597,"line":3750},[3595,13986,13987],{"class":4176},"    owner = factory.SubFactory(UserFactory)\n",[3551,13989,13990,13991,4856],{},"Зв'яжемо ці фабрики з нашою сесією транзакції. Додайте ці фікстури до файлу ",[3563,13992,6776],{},[3586,13994,13996],{"className":4423,"code":13995,"filename":6776,"language":4426,"meta":3591,"style":3591},"# Додайте в кінець tests\u002Fconftest.py\nfrom tests.factories import UserFactory, TaskFactory\n\n@pytest.fixture\ndef user_factory(db_session):\n    UserFactory._meta.sqlalchemy_session = db_session\n    return UserFactory\n\n@pytest.fixture\ndef task_factory(db_session):\n    TaskFactory._meta.sqlalchemy_session = db_session\n    return TaskFactory\n",[3563,13997,13998,14003,14014,14018,14022,14035,14040,14047,14051,14055,14067,14071],{"__ignoreMap":3591},[3595,13999,14000],{"class":3597,"line":3598},[3595,14001,14002],{"class":4182},"# Додайте в кінець tests\u002Fconftest.py\n",[3595,14004,14005,14007,14009,14011],{"class":3597,"line":3604},[3595,14006,4434],{"class":4433},[3595,14008,9631],{"class":4176},[3595,14010,4440],{"class":4433},[3595,14012,14013],{"class":4176}," UserFactory, TaskFactory\n",[3595,14015,14016],{"class":3597,"line":3610},[3595,14017,3638],{"emptyLinePlaceholder":3637},[3595,14019,14020],{"class":3597,"line":3616},[3595,14021,6173],{"class":4064},[3595,14023,14024,14026,14029,14031,14033],{"class":3597,"line":3622},[3595,14025,4916],{"class":4104},[3595,14027,14028],{"class":4064}," user_factory",[3595,14030,4483],{"class":4176},[3595,14032,7918],{"class":4188},[3595,14034,4489],{"class":4176},[3595,14036,14037],{"class":3597,"line":3628},[3595,14038,14039],{"class":4176},"    UserFactory._meta.sqlalchemy_session = db_session\n",[3595,14041,14042,14044],{"class":3597,"line":3634},[3595,14043,6192],{"class":4433},[3595,14045,14046],{"class":4176}," UserFactory\n",[3595,14048,14049],{"class":3597,"line":3641},[3595,14050,3638],{"emptyLinePlaceholder":3637},[3595,14052,14053],{"class":3597,"line":3647},[3595,14054,6173],{"class":4064},[3595,14056,14057,14059,14061,14063,14065],{"class":3597,"line":3653},[3595,14058,4916],{"class":4104},[3595,14060,9651],{"class":4064},[3595,14062,4483],{"class":4176},[3595,14064,7918],{"class":4188},[3595,14066,4489],{"class":4176},[3595,14068,14069],{"class":3597,"line":3659},[3595,14070,9672],{"class":4176},[3595,14072,14073,14075],{"class":3597,"line":3664},[3595,14074,6192],{"class":4433},[3595,14076,9636],{"class":4176},[3568,14078],{},[3822,14080,14082,14083,5257,14085,3919],{"id":14081},"крок-10-написання-тестів-teststest_authpy-та-teststest_taskspy","Крок 10: Написання тестів (",[3563,14084,10838],{},[3563,14086,10848],{},[3551,14088,14089],{},"Напишемо перевірки для наших ендпоінтів.",[3551,14091,14092],{},[3555,14093,14094],{},"Файл тестів автентифікації:",[3586,14096,14098],{"className":4423,"code":14097,"filename":10838,"language":4426,"meta":3591,"style":3591},"import pytest\n\nasync def test_health_endpoint(client):\n    \"\"\"Швидка перевірка доступності нашого API.\"\"\"\n    response = await client.get(\"\u002Fhealth\")\n    assert response.status_code == 200\n    assert response.json() == {\"status\": \"healthy\"}\n\nasync def test_register_user_success(client):\n    \"\"\"Тестування успішної реєстрації.\"\"\"\n    payload = {\n        \"email\": \"student@kostyl.dev\",\n        \"username\": \"student_dev\",\n        \"password\": \"SecretPassword123\"\n    }\n    response = await client.post(\"\u002Fauth\u002Fregister\", json=payload)\n    \n    assert response.status_code == 201\n    data = response.json()\n    assert data[\"email\"] == payload[\"email\"]\n    assert data[\"username\"] == payload[\"username\"]\n    assert \"id\" in data\n\nasync def test_register_duplicate_email(client, user_factory):\n    \"\"\"Спроба реєстрації з існуючим email має викликати помилку 400.\"\"\"\n    # Створюємо користувача в базі даних через фабрику\n    user_factory.create(email=\"duplicate@kostyl.dev\")\n    \n    payload = {\n        \"email\": \"duplicate@kostyl.dev\",\n        \"username\": \"other_user\",\n        \"password\": \"Password1234\"\n    }\n    response = await client.post(\"\u002Fauth\u002Fregister\", json=payload)\n    \n    assert response.status_code == 400\n    assert response.json()[\"detail\"] == \"Email already registered\"\n",[3563,14099,14100,14106,14110,14125,14130,14142,14150,14164,14168,14183,14188,14192,14202,14213,14222,14226,14242,14246,14254,14258,14272,14286,14297,14301,14321,14326,14331,14345,14349,14353,14363,14374,14383,14387,14403,14407,14416],{"__ignoreMap":3591},[3595,14101,14102,14104],{"class":3597,"line":3598},[3595,14103,4440],{"class":4433},[3595,14105,4879],{"class":4176},[3595,14107,14108],{"class":3597,"line":3604},[3595,14109,3638],{"emptyLinePlaceholder":3637},[3595,14111,14112,14114,14116,14119,14121,14123],{"class":3597,"line":3610},[3595,14113,6917],{"class":4104},[3595,14115,6920],{"class":4104},[3595,14117,14118],{"class":4064}," test_health_endpoint",[3595,14120,4483],{"class":4176},[3595,14122,7125],{"class":4188},[3595,14124,4489],{"class":4176},[3595,14126,14127],{"class":3597,"line":3616},[3595,14128,14129],{"class":4067},"    \"\"\"Швидка перевірка доступності нашого API.\"\"\"\n",[3595,14131,14132,14134,14136,14138,14140],{"class":3597,"line":3622},[3595,14133,7228],{"class":4176},[3595,14135,7184],{"class":4433},[3595,14137,9757],{"class":4176},[3595,14139,7049],{"class":4067},[3595,14141,4534],{"class":4176},[3595,14143,14144,14146,14148],{"class":3597,"line":3628},[3595,14145,5008],{"class":4433},[3595,14147,7152],{"class":4176},[3595,14149,7155],{"class":4509},[3595,14151,14152,14154,14156,14158,14160,14162],{"class":3597,"line":3634},[3595,14153,5008],{"class":4433},[3595,14155,7162],{"class":4176},[3595,14157,7070],{"class":4067},[3595,14159,4940],{"class":4176},[3595,14161,7075],{"class":4067},[3595,14163,5909],{"class":4176},[3595,14165,14166],{"class":3597,"line":3641},[3595,14167,3638],{"emptyLinePlaceholder":3637},[3595,14169,14170,14172,14174,14177,14179,14181],{"class":3597,"line":3647},[3595,14171,6917],{"class":4104},[3595,14173,6920],{"class":4104},[3595,14175,14176],{"class":4064}," test_register_user_success",[3595,14178,4483],{"class":4176},[3595,14180,7125],{"class":4188},[3595,14182,4489],{"class":4176},[3595,14184,14185],{"class":3597,"line":3653},[3595,14186,14187],{"class":4067},"    \"\"\"Тестування успішної реєстрації.\"\"\"\n",[3595,14189,14190],{"class":3597,"line":3659},[3595,14191,8344],{"class":4176},[3595,14193,14194,14196,14198,14200],{"class":3597,"line":3664},[3595,14195,6200],{"class":4067},[3595,14197,4940],{"class":4176},[3595,14199,6205],{"class":4067},[3595,14201,4366],{"class":4176},[3595,14203,14204,14206,14208,14211],{"class":3597,"line":3670},[3595,14205,6212],{"class":4067},[3595,14207,4940],{"class":4176},[3595,14209,14210],{"class":4067},"\"student_dev\"",[3595,14212,4366],{"class":4176},[3595,14214,14215,14217,14219],{"class":3597,"line":3676},[3595,14216,6224],{"class":4067},[3595,14218,4940],{"class":4176},[3595,14220,14221],{"class":4067},"\"SecretPassword123\"\n",[3595,14223,14224],{"class":3597,"line":3681},[3595,14225,4990],{"class":4176},[3595,14227,14228,14230,14232,14234,14236,14238,14240],{"class":3597,"line":3687},[3595,14229,7228],{"class":4176},[3595,14231,7184],{"class":4433},[3595,14233,8403],{"class":4176},[3595,14235,12877],{"class":4067},[3595,14237,4513],{"class":4176},[3595,14239,8411],{"class":4188},[3595,14241,8414],{"class":4176},[3595,14243,14244],{"class":3597,"line":3693},[3595,14245,13580],{"class":4176},[3595,14247,14248,14250,14252],{"class":3597,"line":3699},[3595,14249,5008],{"class":4433},[3595,14251,7152],{"class":4176},[3595,14253,8427],{"class":4509},[3595,14255,14256],{"class":3597,"line":3705},[3595,14257,8432],{"class":4176},[3595,14259,14260,14262,14264,14266,14268,14270],{"class":3597,"line":3711},[3595,14261,5008],{"class":4433},[3595,14263,8439],{"class":4176},[3595,14265,13836],{"class":4067},[3595,14267,8464],{"class":4176},[3595,14269,13836],{"class":4067},[3595,14271,4218],{"class":4176},[3595,14273,14274,14276,14278,14280,14282,14284],{"class":3597,"line":3716},[3595,14275,5008],{"class":4433},[3595,14277,8439],{"class":4176},[3595,14279,6278],{"class":4067},[3595,14281,8464],{"class":4176},[3595,14283,6278],{"class":4067},[3595,14285,4218],{"class":4176},[3595,14287,14288,14290,14293,14295],{"class":3597,"line":3722},[3595,14289,5008],{"class":4433},[3595,14291,14292],{"class":4067}," \"id\"",[3595,14294,5433],{"class":4104},[3595,14296,8595],{"class":4176},[3595,14298,14299],{"class":3597,"line":3728},[3595,14300,3638],{"emptyLinePlaceholder":3637},[3595,14302,14303,14305,14307,14310,14312,14314,14316,14319],{"class":3597,"line":3734},[3595,14304,6917],{"class":4104},[3595,14306,6920],{"class":4104},[3595,14308,14309],{"class":4064}," test_register_duplicate_email",[3595,14311,4483],{"class":4176},[3595,14313,7125],{"class":4188},[3595,14315,4513],{"class":4176},[3595,14317,14318],{"class":4188},"user_factory",[3595,14320,4489],{"class":4176},[3595,14322,14323],{"class":3597,"line":3740},[3595,14324,14325],{"class":4067},"    \"\"\"Спроба реєстрації з існуючим email має викликати помилку 400.\"\"\"\n",[3595,14327,14328],{"class":3597,"line":3745},[3595,14329,14330],{"class":4182},"    # Створюємо користувача в базі даних через фабрику\n",[3595,14332,14333,14336,14338,14340,14343],{"class":3597,"line":3750},[3595,14334,14335],{"class":4176},"    user_factory.create(",[3595,14337,8848],{"class":4188},[3595,14339,4506],{"class":4176},[3595,14341,14342],{"class":4067},"\"duplicate@kostyl.dev\"",[3595,14344,4534],{"class":4176},[3595,14346,14347],{"class":3597,"line":3756},[3595,14348,13580],{"class":4176},[3595,14350,14351],{"class":3597,"line":3762},[3595,14352,8344],{"class":4176},[3595,14354,14355,14357,14359,14361],{"class":3597,"line":3768},[3595,14356,6200],{"class":4067},[3595,14358,4940],{"class":4176},[3595,14360,14342],{"class":4067},[3595,14362,4366],{"class":4176},[3595,14364,14365,14367,14369,14372],{"class":3597,"line":3774},[3595,14366,6212],{"class":4067},[3595,14368,4940],{"class":4176},[3595,14370,14371],{"class":4067},"\"other_user\"",[3595,14373,4366],{"class":4176},[3595,14375,14376,14378,14380],{"class":3597,"line":3779},[3595,14377,6224],{"class":4067},[3595,14379,4940],{"class":4176},[3595,14381,14382],{"class":4067},"\"Password1234\"\n",[3595,14384,14385],{"class":3597,"line":3784},[3595,14386,4990],{"class":4176},[3595,14388,14389,14391,14393,14395,14397,14399,14401],{"class":3597,"line":5137},[3595,14390,7228],{"class":4176},[3595,14392,7184],{"class":4433},[3595,14394,8403],{"class":4176},[3595,14396,12877],{"class":4067},[3595,14398,4513],{"class":4176},[3595,14400,8411],{"class":4188},[3595,14402,8414],{"class":4176},[3595,14404,14405],{"class":3597,"line":5142},[3595,14406,13580],{"class":4176},[3595,14408,14409,14411,14413],{"class":3597,"line":5148},[3595,14410,5008],{"class":4433},[3595,14412,7152],{"class":4176},[3595,14414,14415],{"class":4509},"400\n",[3595,14417,14418,14420,14422,14424,14426],{"class":3597,"line":5154},[3595,14419,5008],{"class":4433},[3595,14421,7256],{"class":4176},[3595,14423,8609],{"class":4067},[3595,14425,5206],{"class":4176},[3595,14427,14428],{"class":4067},"\"Email already registered\"\n",[3551,14430,14431],{},[3555,14432,14433],{},"Файл тестів завдань:",[3586,14435,14437],{"className":4423,"code":14436,"filename":10848,"language":4426,"meta":3591,"style":3591},"import pytest\nfrom app.security import create_access_token\n\n@pytest.fixture\ndef auth_headers(user_factory):\n    \"\"\"Генерує заголовок Authorization для тестів, що потребують авторизації.\"\"\"\n    user = user_factory.create(email=\"auth_user@kostyl.dev\")\n    token = create_access_token(data={\"sub\": user.email})\n    return {\"Authorization\": f\"Bearer {token}\"}\n\nasync def test_create_task_authorized(client, auth_headers):\n    \"\"\"Створення завдання авторизованим користувачем.\"\"\"\n    payload = {\n        \"title\": \"Build complete test suite\",\n        \"description\": \"Ensure everything is covered\",\n        \"priority\": 5\n    }\n    response = await client.post(\"\u002Ftasks\", json=payload, headers=auth_headers)\n    \n    assert response.status_code == 201\n    data = response.json()\n    assert data[\"title\"] == payload[\"title\"]\n    assert data[\"priority\"] == 5\n    assert \"id\" in data\n\nasync def test_create_task_unauthorized(client):\n    \"\"\"Спроба створити завдання без авторизаційного токену.\"\"\"\n    payload = {\"title\": \"Hack the planet\"}\n    response = await client.post(\"\u002Ftasks\", json=payload)\n    assert response.status_code == 401\n\nasync def test_get_only_user_tasks(client, user_factory, task_factory):\n    \"\"\"Користувач має бачити лише свої завдання.\"\"\"\n    user_1 = user_factory.create(email=\"user1@kostyl.dev\")\n    user_2 = user_factory.create(email=\"user2@kostyl.dev\")\n    \n    # Створюємо завдання для різних користувачів\n    task_factory.create_batch(2, owner=user_1)\n    task_factory.create(owner=user_2)\n    \n    token = create_access_token(data={\"sub\": user_1.email})\n    headers = {\"Authorization\": f\"Bearer {token}\"}\n    \n    response = await client.get(\"\u002Ftasks\", headers=headers)\n    \n    assert response.status_code == 200\n    tasks = response.json()\n    # Користувач 1 має бачити лише свої 2 завдання\n    assert len(tasks) == 2\n",[3563,14438,14439,14445,14457,14461,14465,14478,14483,14497,14510,14536,14540,14560,14565,14569,14580,14591,14600,14604,14626,14630,14638,14642,14656,14668,14678,14682,14697,14702,14716,14732,14741,14745,14768,14773,14787,14801,14805,14810,14824,14834,14838,14851,14874,14878,14895,14899,14907,14912,14917],{"__ignoreMap":3591},[3595,14440,14441,14443],{"class":3597,"line":3598},[3595,14442,4440],{"class":4433},[3595,14444,4879],{"class":4176},[3595,14446,14447,14449,14452,14454],{"class":3597,"line":3604},[3595,14448,4434],{"class":4433},[3595,14450,14451],{"class":4176}," app.security ",[3595,14453,4440],{"class":4433},[3595,14455,14456],{"class":4176}," create_access_token\n",[3595,14458,14459],{"class":3597,"line":3610},[3595,14460,3638],{"emptyLinePlaceholder":3637},[3595,14462,14463],{"class":3597,"line":3616},[3595,14464,6173],{"class":4064},[3595,14466,14467,14469,14472,14474,14476],{"class":3597,"line":3622},[3595,14468,4916],{"class":4104},[3595,14470,14471],{"class":4064}," auth_headers",[3595,14473,4483],{"class":4176},[3595,14475,14318],{"class":4188},[3595,14477,4489],{"class":4176},[3595,14479,14480],{"class":3597,"line":3628},[3595,14481,14482],{"class":4067},"    \"\"\"Генерує заголовок Authorization для тестів, що потребують авторизації.\"\"\"\n",[3595,14484,14485,14488,14490,14492,14495],{"class":3597,"line":3634},[3595,14486,14487],{"class":4176},"    user = user_factory.create(",[3595,14489,8848],{"class":4188},[3595,14491,4506],{"class":4176},[3595,14493,14494],{"class":4067},"\"auth_user@kostyl.dev\"",[3595,14496,4534],{"class":4176},[3595,14498,14499,14502,14504,14506,14508],{"class":3597,"line":3641},[3595,14500,14501],{"class":4176},"    token = create_access_token(",[3595,14503,12425],{"class":4188},[3595,14505,11229],{"class":4176},[3595,14507,12743],{"class":4067},[3595,14509,13122],{"class":4176},[3595,14511,14512,14514,14516,14519,14521,14523,14526,14528,14530,14532,14534],{"class":3597,"line":3647},[3595,14513,6192],{"class":4433},[3595,14515,7067],{"class":4176},[3595,14517,14518],{"class":4067},"\"Authorization\"",[3595,14520,4940],{"class":4176},[3595,14522,8903],{"class":4104},[3595,14524,14525],{"class":4067},"\"Bearer ",[3595,14527,8909],{"class":4104},[3595,14529,12652],{"class":4176},[3595,14531,8914],{"class":4104},[3595,14533,8917],{"class":4067},[3595,14535,5909],{"class":4176},[3595,14537,14538],{"class":3597,"line":3653},[3595,14539,3638],{"emptyLinePlaceholder":3637},[3595,14541,14542,14544,14546,14549,14551,14553,14555,14558],{"class":3597,"line":3659},[3595,14543,6917],{"class":4104},[3595,14545,6920],{"class":4104},[3595,14547,14548],{"class":4064}," test_create_task_authorized",[3595,14550,4483],{"class":4176},[3595,14552,7125],{"class":4188},[3595,14554,4513],{"class":4176},[3595,14556,14557],{"class":4188},"auth_headers",[3595,14559,4489],{"class":4176},[3595,14561,14562],{"class":3597,"line":3664},[3595,14563,14564],{"class":4067},"    \"\"\"Створення завдання авторизованим користувачем.\"\"\"\n",[3595,14566,14567],{"class":3597,"line":3670},[3595,14568,8344],{"class":4176},[3595,14570,14571,14573,14575,14578],{"class":3597,"line":3676},[3595,14572,4937],{"class":4067},[3595,14574,4940],{"class":4176},[3595,14576,14577],{"class":4067},"\"Build complete test suite\"",[3595,14579,4366],{"class":4176},[3595,14581,14582,14584,14586,14589],{"class":3597,"line":3681},[3595,14583,4950],{"class":4067},[3595,14585,4940],{"class":4176},[3595,14587,14588],{"class":4067},"\"Ensure everything is covered\"",[3595,14590,4366],{"class":4176},[3595,14592,14593,14595,14597],{"class":3597,"line":3687},[3595,14594,4980],{"class":4067},[3595,14596,4940],{"class":4176},[3595,14598,14599],{"class":4509},"5\n",[3595,14601,14602],{"class":3597,"line":3693},[3595,14603,4990],{"class":4176},[3595,14605,14606,14608,14610,14612,14614,14616,14618,14621,14623],{"class":3597,"line":3699},[3595,14607,7228],{"class":4176},[3595,14609,7184],{"class":4433},[3595,14611,8403],{"class":4176},[3595,14613,13160],{"class":4067},[3595,14615,4513],{"class":4176},[3595,14617,8411],{"class":4188},[3595,14619,14620],{"class":4176},"=payload, ",[3595,14622,7291],{"class":4188},[3595,14624,14625],{"class":4176},"=auth_headers)\n",[3595,14627,14628],{"class":3597,"line":3705},[3595,14629,13580],{"class":4176},[3595,14631,14632,14634,14636],{"class":3597,"line":3711},[3595,14633,5008],{"class":4433},[3595,14635,7152],{"class":4176},[3595,14637,8427],{"class":4509},[3595,14639,14640],{"class":3597,"line":3716},[3595,14641,8432],{"class":4176},[3595,14643,14644,14646,14648,14650,14652,14654],{"class":3597,"line":3722},[3595,14645,5008],{"class":4433},[3595,14647,8439],{"class":4176},[3595,14649,5014],{"class":4067},[3595,14651,8464],{"class":4176},[3595,14653,5014],{"class":4067},[3595,14655,4218],{"class":4176},[3595,14657,14658,14660,14662,14664,14666],{"class":3597,"line":3728},[3595,14659,5008],{"class":4433},[3595,14661,8439],{"class":4176},[3595,14663,5049],{"class":4067},[3595,14665,5206],{"class":4176},[3595,14667,14599],{"class":4509},[3595,14669,14670,14672,14674,14676],{"class":3597,"line":3734},[3595,14671,5008],{"class":4433},[3595,14673,14292],{"class":4067},[3595,14675,5433],{"class":4104},[3595,14677,8595],{"class":4176},[3595,14679,14680],{"class":3597,"line":3740},[3595,14681,3638],{"emptyLinePlaceholder":3637},[3595,14683,14684,14686,14688,14691,14693,14695],{"class":3597,"line":3745},[3595,14685,6917],{"class":4104},[3595,14687,6920],{"class":4104},[3595,14689,14690],{"class":4064}," test_create_task_unauthorized",[3595,14692,4483],{"class":4176},[3595,14694,7125],{"class":4188},[3595,14696,4489],{"class":4176},[3595,14698,14699],{"class":3597,"line":3750},[3595,14700,14701],{"class":4067},"    \"\"\"Спроба створити завдання без авторизаційного токену.\"\"\"\n",[3595,14703,14704,14707,14709,14711,14714],{"class":3597,"line":3756},[3595,14705,14706],{"class":4176},"    payload = {",[3595,14708,5014],{"class":4067},[3595,14710,4940],{"class":4176},[3595,14712,14713],{"class":4067},"\"Hack the planet\"",[3595,14715,5909],{"class":4176},[3595,14717,14718,14720,14722,14724,14726,14728,14730],{"class":3597,"line":3762},[3595,14719,7228],{"class":4176},[3595,14721,7184],{"class":4433},[3595,14723,8403],{"class":4176},[3595,14725,13160],{"class":4067},[3595,14727,4513],{"class":4176},[3595,14729,8411],{"class":4188},[3595,14731,8414],{"class":4176},[3595,14733,14734,14736,14738],{"class":3597,"line":3768},[3595,14735,5008],{"class":4433},[3595,14737,7152],{"class":4176},[3595,14739,14740],{"class":4509},"401\n",[3595,14742,14743],{"class":3597,"line":3774},[3595,14744,3638],{"emptyLinePlaceholder":3637},[3595,14746,14747,14749,14751,14754,14756,14758,14760,14762,14764,14766],{"class":3597,"line":3779},[3595,14748,6917],{"class":4104},[3595,14750,6920],{"class":4104},[3595,14752,14753],{"class":4064}," test_get_only_user_tasks",[3595,14755,4483],{"class":4176},[3595,14757,7125],{"class":4188},[3595,14759,4513],{"class":4176},[3595,14761,14318],{"class":4188},[3595,14763,4513],{"class":4176},[3595,14765,9723],{"class":4188},[3595,14767,4489],{"class":4176},[3595,14769,14770],{"class":3597,"line":3784},[3595,14771,14772],{"class":4067},"    \"\"\"Користувач має бачити лише свої завдання.\"\"\"\n",[3595,14774,14775,14778,14780,14782,14785],{"class":3597,"line":5137},[3595,14776,14777],{"class":4176},"    user_1 = user_factory.create(",[3595,14779,8848],{"class":4188},[3595,14781,4506],{"class":4176},[3595,14783,14784],{"class":4067},"\"user1@kostyl.dev\"",[3595,14786,4534],{"class":4176},[3595,14788,14789,14792,14794,14796,14799],{"class":3597,"line":5142},[3595,14790,14791],{"class":4176},"    user_2 = user_factory.create(",[3595,14793,8848],{"class":4188},[3595,14795,4506],{"class":4176},[3595,14797,14798],{"class":4067},"\"user2@kostyl.dev\"",[3595,14800,4534],{"class":4176},[3595,14802,14803],{"class":3597,"line":5148},[3595,14804,13580],{"class":4176},[3595,14806,14807],{"class":3597,"line":5154},[3595,14808,14809],{"class":4182},"    # Створюємо завдання для різних користувачів\n",[3595,14811,14812,14814,14816,14818,14821],{"class":3597,"line":5167},[3595,14813,9740],{"class":4176},[3595,14815,8891],{"class":4509},[3595,14817,4513],{"class":4176},[3595,14819,14820],{"class":4188},"owner",[3595,14822,14823],{"class":4176},"=user_1)\n",[3595,14825,14826,14829,14831],{"class":3597,"line":5192},[3595,14827,14828],{"class":4176},"    task_factory.create(",[3595,14830,14820],{"class":4188},[3595,14832,14833],{"class":4176},"=user_2)\n",[3595,14835,14836],{"class":3597,"line":7885},[3595,14837,13580],{"class":4176},[3595,14839,14840,14842,14844,14846,14848],{"class":3597,"line":7893},[3595,14841,14501],{"class":4176},[3595,14843,12425],{"class":4188},[3595,14845,11229],{"class":4176},[3595,14847,12743],{"class":4067},[3595,14849,14850],{"class":4176},": user_1.email})\n",[3595,14852,14853,14856,14858,14860,14862,14864,14866,14868,14870,14872],{"class":3597,"line":10589},[3595,14854,14855],{"class":4176},"    headers = {",[3595,14857,14518],{"class":4067},[3595,14859,4940],{"class":4176},[3595,14861,8903],{"class":4104},[3595,14863,14525],{"class":4067},[3595,14865,8909],{"class":4104},[3595,14867,12652],{"class":4176},[3595,14869,8914],{"class":4104},[3595,14871,8917],{"class":4067},[3595,14873,5909],{"class":4176},[3595,14875,14876],{"class":3597,"line":10595},[3595,14877,13580],{"class":4176},[3595,14879,14880,14882,14884,14886,14888,14890,14892],{"class":3597,"line":10601},[3595,14881,7228],{"class":4176},[3595,14883,7184],{"class":4433},[3595,14885,9757],{"class":4176},[3595,14887,13160],{"class":4067},[3595,14889,4513],{"class":4176},[3595,14891,7291],{"class":4188},[3595,14893,14894],{"class":4176},"=headers)\n",[3595,14896,14897],{"class":3597,"line":10606},[3595,14898,13580],{"class":4176},[3595,14900,14901,14903,14905],{"class":3597,"line":10618},[3595,14902,5008],{"class":4433},[3595,14904,7152],{"class":4176},[3595,14906,7155],{"class":4509},[3595,14908,14909],{"class":3597,"line":10628},[3595,14910,14911],{"class":4176},"    tasks = response.json()\n",[3595,14913,14914],{"class":3597,"line":10633},[3595,14915,14916],{"class":4182},"    # Користувач 1 має бачити лише свої 2 завдання\n",[3595,14918,14919,14921,14923,14926],{"class":3597,"line":10645},[3595,14920,5008],{"class":4433},[3595,14922,5159],{"class":4064},[3595,14924,14925],{"class":4176},"(tasks) == ",[3595,14927,8386],{"class":4509},[3568,14929],{},[3822,14931,14933],{"id":14932},"крок-11-запуск-та-аналіз-результатів","Крок 11: Запуск та аналіз результатів",[3551,14935,14936],{},"Тепер, коли всі файли створені, виконайте такі кроки для запуску тестів.",[3792,14938,14939,14966],{},[3795,14940,14941,14944,14945],{},[3555,14942,14943],{},"Встановіть залежності проєкту"," (бажано у віртуальному оточенні):",[9992,14946,14948,14957],{"title":14947},"pip install",[9996,14949,14951,10004,14954],{"className":14950},[3597],[3595,14952,10003],{"className":14953},[10002],[3555,14955,14956],{},"pip install .",[9996,14958,14960,10004,14963],{"className":14959},[3597],[3595,14961,10003],{"className":14962},[10002],[3555,14964,14965],{},"pip install -e \".[dev]\"",[3795,14967,14968,14971,14972],{},[3555,14969,14970],{},"Запустіть pytest"," для проходження всіх тестів та аналізу покриття коду:",[9992,14973,14974,14982,14986,14990,14994,14997,15005,15012,15015,15018,15022,15026,15030,15034,15038,15042,15046,15050,15053,15057,15060],{"title":3864},[9996,14975,14977,10004,14980],{"className":14976},[3597],[3595,14978,10003],{"className":14979},[10002],[3555,14981,3864],{},[9996,14983,14985],{"className":14984},[3597],"====================== test session starts ======================",[9996,14987,14989],{"className":14988},[3597],"platform darwin -- Python 3.12.3, pytest-8.1.1",[9996,14991,14993],{"className":14992},[3597],"collected 6 items",[9996,14995],{"className":14996},[3597],[9996,14998,15000,15001,15004],{"className":14999},[3597],"tests\u002Ftest_auth.py ",[3595,15002,10027],{"className":15003},[10026]," [ 50%]",[9996,15006,15008,15009,10054],{"className":15007},[3597],"tests\u002Ftest_tasks.py ",[3595,15010,10027],{"className":15011},[10026],[9996,15013],{"className":15014},[3597],[9996,15016,10061],{"className":15017},[3597],[9996,15019,15021],{"className":15020},[3597],"Name                  Stmts   Miss  Cover   Missing",[9996,15023,15025],{"className":15024},[3597],"---------------------------------------------------",[9996,15027,15029],{"className":15028},[3597],"app\u002F__init__.py           0      0   100%",[9996,15031,15033],{"className":15032},[3597],"app\u002Fdatabase.py          18      3    83%   24-27",[9996,15035,15037],{"className":15036},[3597],"app\u002Fmain.py              56      3    95%   58-59, 70",[9996,15039,15041],{"className":15040},[3597],"app\u002Fmodels.py            20      0   100%",[9996,15043,15045],{"className":15044},[3597],"app\u002Fschemas.py           26      1    96%   19",[9996,15047,15049],{"className":15048},[3597],"app\u002Fsecurity.py          14      0   100%",[9996,15051,15025],{"className":15052},[3597],[9996,15054,15056],{"className":15055},[3597],"TOTAL                   134      7    95%",[9996,15058],{"className":15059},[3597],[9996,15061,15063],{"className":15062},[3597],[3595,15064,15066],{"className":15065},[10026,10103],"======================= 6 passed in 0.98s =======================",[3551,15068,15069],{},"Всі тести пройшли успішно, а загальний відсоток покриття коду (95%) значно перевищує необхідний поріг.",[3568,15071],{},[3571,15073,15075],{"id":15074},"домашнє-завдання-taskforge-practice","Домашнє завдання (TaskForge Practice)",[3551,15077,15078],{},"Для успішного завершення цієї теми виконайте наступні завдання:",[3792,15080,15081,15095,15106],{},[3795,15082,15083,15086,15087,15089,15090,15092,15093,3929],{},[3555,15084,15085],{},"Додаткові валідації",": Додайте до схеми ",[3563,15088,5269],{}," перевірку того, що ",[3563,15091,4526],{}," не може бути пустим рядком, і напишіть відповідний тест у ",[3563,15094,10848],{},[3795,15096,15097,15100,15101,15103,15104,3929],{},[3555,15098,15099],{},"Мокування відправки листів",": Додайте в ",[3563,15102,7009],{}," виклик сервісу надсилання повідомлень при успішній реєстрації користувача та покрийте цей флоу тестом, використовуючи ",[3563,15105,9292],{},[3795,15107,15108,15111,15112,15115,15116,4844,15119,3929],{},[3555,15109,15110],{},"CRUD-тести видалення",": Допишіть інтеграційний тест для ",[3563,15113,15114],{},"DELETE \u002Ftasks\u002F{id}",", перевіривши, що користувач може видаляти лише власні завдання, а при спробі видалити чуже отримує ",[3563,15117,15118],{},"403 Forbidden",[3563,15120,15121],{},"404 Not Found",[15123,15124,15125],"blockquote",{},[3551,15126,15127,10004,15130],{},[3555,15128,15129],{},"Git Commit:",[3563,15131,15132],{},"feat: add comprehensive test suite with pytest, fixtures and CI",[15134,15135,15136],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s8Opu, html code.shiki .s8Opu{--shiki-light:#795E26;--shiki-default:#DCDCAA;--shiki-dark:#DCDCAA}html pre.shiki code .sbdoH, html code.shiki .sbdoH{--shiki-light:#A31515;--shiki-default:#CE9178;--shiki-dark:#CE9178}html pre.shiki code .su1O8, html code.shiki .su1O8{--shiki-light:#0000FF;--shiki-default:#569CD6;--shiki-dark:#569CD6}html pre.shiki code .sHH4Y, html code.shiki .sHH4Y{--shiki-light:#000000;--shiki-default:#D4D4D4;--shiki-dark:#D4D4D4}html pre.shiki code .spJ8K, html code.shiki .spJ8K{--shiki-light:#008000;--shiki-default:#6A9955;--shiki-dark:#6A9955}html pre.shiki code .siwwj, html code.shiki .siwwj{--shiki-light:#001080;--shiki-default:#9CDCFE;--shiki-dark:#9CDCFE}html pre.shiki code .s8xlr, html code.shiki .s8xlr{--shiki-light:#AF00DB;--shiki-default:#C586C0;--shiki-dark:#C586C0}html pre.shiki code .sN1BT, html code.shiki .sN1BT{--shiki-light:#267F99;--shiki-default:#4EC9B0;--shiki-dark:#4EC9B0}html pre.shiki code .sJj4R, html code.shiki .sJj4R{--shiki-light:#098658;--shiki-default:#B5CEA8;--shiki-dark:#B5CEA8}html pre.shiki code .sKtos, html code.shiki .sKtos{--shiki-light:#800000;--shiki-default:#569CD6;--shiki-dark:#569CD6}html pre.shiki code .su9tN, html code.shiki .su9tN{--shiki-light:#0000FF;--shiki-default:#CE9178;--shiki-dark:#CE9178}",{"title":3591,"searchDepth":3604,"depth":3604,"links":15138},[15139,15142,15146,15152,15159,15164,15168,15169,15173,15177,15181,15182,15204],{"id":3573,"depth":3604,"text":3574,"children":15140},[15141],{"id":3824,"depth":3610,"text":3825},{"id":4016,"depth":3604,"text":4017,"children":15143},[15144,15145],{"id":4035,"depth":3610,"text":4036},{"id":4149,"depth":3610,"text":4150},{"id":4413,"depth":3604,"text":4414,"children":15147},[15148,15150],{"id":4834,"depth":3610,"text":15149},"Написання тестів за допомогою стандартного assert",{"id":5241,"depth":3610,"text":15151},"Параметризовані тести: @pytest.mark.parametrize",{"id":6022,"depth":3604,"text":6023,"children":15153},[15154,15155,15156,15158],{"id":6142,"depth":3610,"text":6143},{"id":6286,"depth":3610,"text":6287},{"id":6343,"depth":3610,"text":15157},"Життєвий цикл: Setup та Teardown через yield",{"id":6555,"depth":3610,"text":6556},{"id":6584,"depth":3604,"text":6585,"children":15160},[15161,15162,15163],{"id":6591,"depth":3610,"text":6592},{"id":6740,"depth":3610,"text":6741},{"id":7002,"depth":3610,"text":7003},{"id":7304,"depth":3604,"text":7305,"children":15165},[15166,15167],{"id":7321,"depth":3610,"text":7322},{"id":7539,"depth":3610,"text":7540},{"id":7937,"depth":3604,"text":7938},{"id":8634,"depth":3604,"text":8635,"children":15170},[15171,15172],{"id":8659,"depth":3610,"text":8660},{"id":8797,"depth":3610,"text":8798},{"id":9324,"depth":3604,"text":9325,"children":15174},[15175,15176],{"id":9370,"depth":3610,"text":9371},{"id":9681,"depth":3610,"text":9682},{"id":9954,"depth":3604,"text":9955,"children":15178},[15179,15180],{"id":9983,"depth":3610,"text":9984},{"id":10140,"depth":3610,"text":10141},{"id":10213,"depth":3604,"text":10214},{"id":10736,"depth":3604,"text":10737,"children":15183},[15184,15185,15187,15189,15191,15193,15195,15197,15199,15201,15203],{"id":10747,"depth":3610,"text":10748},{"id":10859,"depth":3610,"text":15186},"Крок 2: Конфігурація залежностей (pyproject.toml)",{"id":11137,"depth":3610,"text":15188},"Крок 3: Налаштування бази даних (app\u002Fdatabase.py)",{"id":11409,"depth":3610,"text":15190},"Крок 4: Опис моделей (app\u002Fmodels.py)",{"id":11780,"depth":3610,"text":15192},"Крок 5: Pydantic-схеми (app\u002Fschemas.py)",{"id":12220,"depth":3610,"text":15194},"Крок 6: Логіка безпеки (app\u002Fsecurity.py)",{"id":12503,"depth":3610,"text":15196},"Крок 7: Веб-додаток (app\u002Fmain.py)",{"id":13304,"depth":3610,"text":15198},"Крок 8: Налаштування pytest (tests\u002Fconftest.py)",{"id":13717,"depth":3610,"text":15200},"Крок 9: Фабрики даних (tests\u002Ffactories.py)",{"id":14081,"depth":3610,"text":15202},"Крок 10: Написання тестів (tests\u002Ftest_auth.py та tests\u002Ftest_tasks.py)",{"id":14932,"depth":3610,"text":14933},{"id":15074,"depth":3604,"text":15075},"Глибоке занурення у тестування FastAPI застосунків. Перехід від концепцій xUnit до pytest, налаштування середовища, unit-тестування Pydantic-схем та параметризовані тести.","md",null,{},{"title":2658,"description":15205},"aQgYZSKbVlOk855Fp9Vg6uu4lCEKUM-8yQXlA7ppTAc",[15212,15214],{"title":2654,"path":2655,"stem":2656,"description":15213,"children":-1},"Глибоке занурення у безпеку FastAPI — хешування паролів, JSON Web Tokens, OAuth2 flow, RBAC на рівні проєкту та Security Headers. Порівняння з ASP.NET підходом для розробників, що мігрують на Python.",{"title":2662,"path":2663,"stem":2664,"description":15215,"children":-1},"Глибоке занурення в просунуті асинхронні можливості FastAPI. Навчимося виконувати фонові завдання, працювати з WebSockets та SSE, а також інтегрувати Celery для важких обчислень.",1787230576004]