diff --git a/docs/workshops/day_1_sqa.md b/docs/workshops/day_1_sqa.md
index 071ab1b65c7e9f24568ffff9c4ea9d18ce39078d..ae48cddeb6c38b91d98dfc79af226f487a1ea18c 100644
--- a/docs/workshops/day_1_sqa.md
+++ b/docs/workshops/day_1_sqa.md
@@ -1,3 +1,13 @@
 # Day 1: Software Quality Assurance (SQA) Overview
 
-TBD.
+* Session 1: 9:00 - 10:15: [Overview of Software Quality Assurance and TDD][1] 
+* Session 2: 10:30 - 11:45: [Test Driven Development][2]
+* Session 3: 13:00 - 15:00: Hands-on: A small be complete overview of TDD.
+* Session 4: 15:30 - 16:30: Overview, Discussion, Lesson learned  
+
+
+
+[1]: https://docs.google.com/presentation/d/1fDJF9ocOiDXEqm8WCDX_vWSPoG3L3PNI?rtpof=true&usp=drive_fs 
+
+[2]: https://docs.google.com/presentation/d/1f1uDiI4npQOSih5o0EnqBcJqJL2LBLVk?rtpof=true&usp=drive_fs 
+
diff --git a/docs/workshops/day_2_tdd.md b/docs/workshops/day_2_tdd.md
index b13d7d0b60a02138a56a5ae514dba8d103d179f3..9526f8d7da1c93d8dee7ed76c3ead23ccce9740e 100644
--- a/docs/workshops/day_2_tdd.md
+++ b/docs/workshops/day_2_tdd.md
@@ -1,3 +1,9 @@
 # Day 2: Test-Driven Development
 
-TBD.
+* Session 1: 9:00 - 10:00: [Testing Category][1] 
+* Session 2: 10:15 - 11:30: Hands-on: A bigger Case study 
+* Session 3: 13:30 - 15:00: Hands-on: A bigger Case study  
+* Session 4: 15:30 - 16:30: Overview, Discussion, Lesson learned  
+
+[1]: https://docs.google.com/presentation/d/1f1vpyYOu3GSyIeyqGLU-D6REBn0ACN81?rtpof=true&usp=drive_fs 
+
diff --git a/docs/workshops/day_3_bdd.md b/docs/workshops/day_3_bdd.md
index 650773c2cd4f3298f08678aeebd657df97ccf27d..a4d611a668c8e1db4dc9f879269d25af6884c83e 100644
--- a/docs/workshops/day_3_bdd.md
+++ b/docs/workshops/day_3_bdd.md
@@ -1,3 +1,479 @@
-# Day 3: Behavior-Driven Development
+# Day 3: Behavior-Driven Development (BDD)
 
-TBD.
+* Session 1: 09:00 - 10:15: [Overview BDD][1]
+* Session 2: 10:30 - 11:45: Hands-on: A Case study
+* Session 3: 13:00 - 15:00: Hands-on: A Case study
+* Session 4: 15:30 - 16:30: Overview, Discussion, Lesson learned
+
+
+## Functional Tests & BDD
+
+Anda telah diperkenalkan dengan contoh kode test dari tingkat _unit test_ hingga _functional test_ di hari pertama workshop.
+_Unit test_ menguji komponen software terkecil ("unit", seperti fungsi, _class_, ataupun modul) secara independen dan, idealnya, terisolasi dari komponen lainnya.
+Sedangkan _functional test_ menguji software secara utuh dengan menyimulasikan interaksi pengguna ketika menggunakan software.
+
+Hari ini kita akan melihat bagaimana _functional test_ dapat dikembangkan lebih lanjut untuk mendukung proses pengembangan software dengan gaya Behavior-Driven Development (BDD).
+Sebagai contoh, berikut ini adalah potongak kode sebuah _test case_ dari _functional test_ Sitodo PMPL yang menyimulasikan aksi menambahkan todo item baru:
+
+```java
+@Test
+@DisplayName("A user can create a single todo item")
+void addTodoItem_single() {
+    open("/");
+    checkOverallPageLayout();
+
+    // Create a new item
+    postNewTodoItem("Buy milk");
+
+    // See the list for the newly inserted item
+    checkItemsInList(List.of("Buy milk"));
+
+    // The page can be accessed at the new, unique URL
+    String currentUrl = webdriver().driver().url();
+    assertTrue(currentUrl.matches(".+/list/\\d+$"), "The URL was: " + currentUrl);
+}
+```
+
+Dapat dilihat bahwa alur uji coba dituliskan secara imperatif oleh developer.
+Developer mengimplementasikan serangkaian instruksi kepada "aktor" (dalam hal ini, browser yang dikendalikan Selenium WebDriver)
+untuk dapat menyimulasikan alur penggunaan sebuah fitur pada software yang sedang diujicobakan (System Under Test, disingkat SUT).
+Kumpulan instruksi tersebut seringkali sangat teknis dan sulit untuk dipahami oleh anggota tim selain developer,
+terutama anggota tim yang fokus pada aspek bisnis (_requirements_) dan penjaminan mutu.
+
+Walaupun developer bisa meningkatkan _readability_ kode dengan memberikan nama _test case_ secara deskriptif
+serta menuliskan kode dengan gaya penulisan mengikuti bahasa natural (seringkali disebut sebagai _fluent-style_),
+_functional test_ masih terpisah dari kebutuhan (_requirements_) pengembangan sehingga tidak ada keterkaitan (_traceability_)
+antara _requirements_ dengan _test_.
+
+BDD hadir untuk berusaha menjembatani tiga peran dalam pengembangan: bisnis, tim pengembang, dan penjaminan mutu.
+Ketiga peran tersebut berkolaborasi dalam proses pengembangan dengan menyusun _requirements_ yang _traceable_ dari deskripsi, implementasi, dan uji cobanya.
+
+## Persiapan Workshop
+
+Untuk mengikuti rangkaian kegiatan workshop hari ini, harap persiapkan _tools_ berikut di komputer anda:
+
+- Git
+- IntelliJ Community Edition
+  - Pasang _plugin_ Cucumber
+- Java JDK 17 (`java` dan `javac`)
+- Apache Maven (`mvn`)
+- Docker dan Docker Compose (`docker` dan `docker-compose`)
+- Google Chrome
+- Node.js (opsional jika ingin menjalankan _frontend_ secara manual)
+
+Buat salinan _branch_ `development` dari repositori Git kode templat workshop hari ini:
+
+```shell
+# TODO: Siapkan _branch_ yang berisi templat kosong. Saat ini branch development masih mengandung contoh solusi.
+git clone -b development https://gitlab.cs.ui.ac.id/pmpl/workshops/spring-petclinic-bdd.git
+```
+
+Telah tersedia juga berkas [`docker-compose.yml`][2] berikut untuk menjalankan aplikasi Spring Boot + Angular JS bernama Spring Petclinic yang akan dijadikan bahan latihan hari ini:
+
+```yaml
+---
+services:
+  api:
+    image: docker.io/addianto/spring-petclinic-rest:latest
+    ports:
+      - "127.0.0.1:9966:9966"
+  app:
+    image: docker.io/addianto/spring-petclinic-angular:latest
+    ports:
+      - "127.0.0.1:80:8080"
+```
+
+_Backend_ aplikasi menggunakan [Spring Petclinic REST](https://github.com/spring-petclinic/spring-petclinic-rest) yang dapat diakses melalui _port_ 9966 di `127.0.0.1`.
+Sedangkan _frontend_ aplikasi menggunakan [Spring Petclinic Angular](https://github.com/spring-petclinic/spring-petclinic-rest) yang dapat diakses melalui _port_ 80 di `127.0.0.1`.
+Kedua komponen aplikasi ini dapat jalan sebagai _container_, ataupun di-_build_ secara manual.
+Silakan merujuk ke `README.md` masing-masing komponen jika ingin menjalankannya secara manual (tanpa _container_).
+
+Apabila sudah menyalin repositori Git dan telah menyiapkan _tools_ yang dibutuhkan,
+silakan coba jalankan terlebih dahulu aplikasi contoh:
+
+```shell
+docker-compose up --detach
+# Perintah alternatif. Tersedia pada instalasi Docker yang memasang plugin Compose.
+docker compose up --detach
+```
+
+Kemudian buka laman depan aplikasi menggunakan _web browser_ seperti Google Chrome di alamat `http://127.0.0.1`.
+Tampilan aplikasi akan terlihat serupa dengan _screenshot_ berikut:
+
+> TODO: Tambah _screenshot_
+
+## Templat Kode Spring Petclinic BDD
+
+Templat kode yang digunakan pada workshop hari ini mengadopsi templat _starter code_ [Serenity & Cucumber](https://github.com/serenity-bdd/serenity-cucumber-starter).
+Templat kode telah dimodifikasi agar menggunakan contoh aplikasi Spring Petclinic secara lokal menggunakan _container_.
+
+Sesuai dengan nama templat _starter code_ asli yang digunakan, kita akan menggunakan Serenity dan Cucumber.
+Serenity merupakan _test framework_ yang menyediakan _library_ dan mendukung proses otomasi test yang mengikuti alur BDD.
+Sedangkan Cucumber menyediakan _library_ untuk menuliskan deskripsi fitur pada _test suite_ BDD dan menyambungkannya ke _test framework_ seperti Serenity.
+
+## Menulis Deskripsi Fitur & Skenario Test Dengan Gherkin
+
+Ingat kembali bahwa salah satu tujuan BDD adalah ingin menjembatani peran non-teknis (misal: _client_, analis) dengan peran teknis (misal: tim pengembang).
+Peran non-teknis akan berkolaborasi lebih erat dengan peran teknis selama proses pengembangan berlangsung.
+Bentuk kolaborasi lebih erat ini salah satunya dituangkan dalam bentuk keterlibatan peran non-teknis dalam membuat deskripsi fitur serta menyusun skenario test fitur tersebut.
+
+Peran non-teknis dapat membuat deskripsi fitur dan skenario test dengan menggunakan bahasa Gherkin.
+Gherkin adalah bahasa dengan struktur yang sederhana dan dituliskan menggunakan sintaks bahasa natural.
+Deskripsi fitur dan skenario test dapat dituliskan menggunakan Gherkin seperti menuliskan prosa dalam bahasa natural.
+Tentu saja ada aturan sintaks dan struktur yang harus diikuti agar dapat dijalankan oleh _test framework_.
+
+Sebagai contoh, mari lihat potongan contoh skenario test berikut yang dapat ditemukan di [`src/test/resources/features/veterinarian/manage_specialties.feature`][3]:
+
+```gherkin
+Feature: Manage specialties
+  The veterinarian wants to be able to add, edit, and delete specialties.
+
+  Scenario: Add new specialty
+    Given a system operator named "John" is looking at the specialties page
+    When he adds a new specialty called "cardiology"
+    Then he should see the specialty "cardiology" on the list of specialties
+```
+
+Skenario test dituliskan sebagai bagian dari deskripsi fitur yang dituliskan dalam berkas dengan ekstensi `.feature`.
+Pada contoh di atas, deskripsi fitur dimulai dengan teks `Feature: Manage specialties` yang berarti berkas ini merupakan deskripsi fitur "Manage specialties".
+Selanjutnya, terdapat teks bebas yang menjelaskan lebih rinci fitur terkait.
+Teks bebas tersebut dapat juga berisi dengan deskripsi _use case_ atau _user story_.
+
+Sebuah fitur dapat memiliki satu atau lebih skenario test.
+Pada contoh di atas, saat ini hanya ada sebuah skenario test untuk menguji fitur menambahkan bidang spesialis dokter hewan.
+Skenario test berisi tiga buah komponen penting di dalam membuat instruksi pengujian dengan gaya BDD: `Given`, `When`, dan `Then`.
+`Given` merepresentasikan kondisi awal skenario test sebelum menjalankan aksi aktor.
+`When` merepresentasikan aksi aktor dalam sistem.
+`Then` merepresentasikan kondisi akhir skenario test setelah menjalankan aksi aktor.
+
+### Latihan Singkat: Menulis Deskripsi Fitur & Skenario Test Baru
+
+> TODO: Tambah pengantar
+
+1. [ ] Buat berkas deskripsi fitur baru bernama `manage_pet_types.feature` di `src/test/resources/features/veterinarian`.
+2. [ ] Tulis nama fitur baru "Manage pet types" beserta deskripsinya.
+3. [ ] Susun sebuah skenario test baru bernama "Add new pet type" yang menguji operator sistem dapat menambahkan sebuah jenis binatang peliharaan baru ke dalam sistem.
+       Tulis skenarionya mengikuti kerangka `Given`, `When`, dan `Then`.
+
+Salah satu contoh solusi dari latihan mandiri dapat dilihat pada contoh kode berikut:
+
+```gherkin
+Feature: Manage pet types
+  The veterinarian wants to be able to add, edit, and delete pet types.
+
+  Scenario: Add new pet type
+    Given a system operator named "Jane" is looking at the pet types page
+    When she adds a new pet type named "turtle"
+    Then she should see the pet type "turtle" on the list of pet types
+```
+
+## Menjalankan Test Suite BDD
+
+Jalankan skenario test menggunakan Run Configuration yang otomatis terdeteksi oleh IntelliJ.
+Alternatif lain, jalankan skenario test menggunakan perintah Maven berikut:
+
+```shell
+mvn verify
+```
+
+_Test suite_ BDD akan dijalankan oleh _test runner_ secara _headless_ terhadap aplikasi yang berjalan secara lokal.
+Begitu _test suite_ BDD selesai dijalankan, laporan hasil test dapat ditemukan sebagai dokumen HTML di folder `target/site/serenity`.
+Silakan buka berkas `index.html` yang terdapat di folder laporan hasil test.
+Contoh tampilan laporannya dapat dilihat pada _screenshot_ berikut:
+
+> TODO: Tambah _screenshot_
+
+Dapat dilihat bahwa satu skenario test berhasil dijalankan, namun skenario test baru yang anda buat sebelumnya masih gagal.
+Hal ini disebabkan karena skenario test yang anda buat belum disambungkan ke kode test.
+
+Deskripsi fitur memang bermanfaat bagi peran non-teknis agar dapat ikut berkontribusi di dalam proses pengembangan.
+Namun bukan berarti deskripsi fitur dan skenario test secara ajaib bisa otomatis menguji coba sistem sesuai keinginan.
+Peran teknis seperti programmer dan _test engineer_ penting untuk dapat menyambungkan deskripsi fitur ke kode test.
+
+## Membuat Step Definition & Glue Code
+
+Mari lihat kembali contoh skenario test "Add new specialty":
+
+```gherkin
+Scenario: Add new specialty
+  Given a system operator named "John" is looking at the specialties page
+  When he adds a new specialty called "cardiology"
+  Then he should see the specialty "cardiology" on the list of specialties
+```
+
+Masing-masing kalimat yang diawali dengan kata kunci `Given`, `When`, dan `Then` perlu dipasangkan dengan serangkaian instruksi bahasa pemrograman.
+Instruksi-instruksi tersebut seringkali disebut sebagai _step definitions_ yang merupakan bagian dari _Glue Code_,
+yaitu kode program yang mendukung dan menjalankan _test suite_ BDD.
+
+Sebagai contoh, mari lihat _step definition_ padanan kalimat berawalan `Given`:
+
+```java
+@Given("a system operator named {actor} is looking at the specialties page")
+public void lookingAtSpecialtiesPage(Actor actor) {
+    actor.wasAbleTo(
+            Open.browserOn()
+                .the(SpringPetclinicHomePage.class),
+            HoverOverBy.over(Link.withText("Specialties")),
+            Click.on(Link.withText("Specialties"))
+    );
+}
+```
+
+_Step definition_ diimplementasikan sebagai fungsi dengan informasi tambahan yang akan memetakan fungsi tersebut dengan kalimat terkait.
+Pada contoh di atas, fungsi diberikan anotasi `@Given` dengan parameter berisi teks _pattern_ yang akan dicocokkan dengan sebuah kalimat di skenario test.
+
+Apabila kalimat pada skenario test mengandung elemen yang dapat berubah-ubah seperti aktor/pengguna fitur atau input,
+maka teks _pattern_ dapat mengandung _placeholder_ yang akan menjadi parameter bagi fungsi _step definition_.
+Pada contoh di atas, `actor` merupakan tipe parameter khusus yang disediakan templat kode untuk memetakan aktor di skenario test dengan objek `Actor` yang akan dikendalikan oleh implementasi fungsi _step definition_.
+
+Inti utama dari fungsi _step definition_ adalah objek `Actor`, yaitu representasi dari aktor/pengguna fitur.
+Pada contoh di atas, objek `Actor` memanggil fungsi `wasAbleTo` yang menyimulasikan aksi-aksi aktor untuk mencapai kondisi awal yang diinginkan.
+Fungsi `wasAbleTo` menerima parameter berupa satu atau lebih objek `Performable` yang merepresentasikan aksi fisik ketika berinteraksi dengan SUT.
+Pada contoh di atas, aktor melakukan tiga aksi berikut:
+
+1. Buka web browser untuk membuka SUT, yaitu aplikasi Spring Petclinic.
+2. Menggerakkan kursor mouse ke tautan yang mengandung teks "Specialties"
+3. Klik tautan yang mengandung teks "Specialties"
+
+Apabila ketiga aksi tersebut berhasil dilakukan,
+maka kondisi web browser aktor saat ini sudah menampilkan halaman daftar bidang spesialis dokter hewan.
+Dengan demikian, kondisi awal yang dibutuhkan untuk menguji skenario "Add new specialty" tercapai
+dan dapat lanjut ke aksi-aksi utama untuk menambahkan data baru.
+
+Mari lihat _step definition_ padanan kalimat berawalan `When`:
+
+```java
+@When("{actor} adds a new specialty called {string}")
+public void addsNewSpecialty(Actor actor, String name) {
+    actor.attemptsTo(
+            Click.on(Button.withText("Add")),
+            Enter.theValue(name)
+                 .into(InputField.withNameOrId("name"))
+                 .thenHit(Keys.ENTER)
+    );
+}
+```
+
+Pada contoh di atas, objek `Actor` memanggil fungsi `attemptsTo` yang kurang lebih serupa dengan fungsi `wasAbleTo` di contoh sebelumnya.
+Fungsi `attemptsTo` sama-sama menerima satu atau lebih objek `Performable`.
+Aktor melakukan tiga aksi berikut dengan asumsi saat ini aktor sedang melihat halaman daftar bidang spesialis dokter hewan:
+
+1. Klik tombol dengan teks "Add"
+2. Masukkan input string ke sebuah input field dengan atribut `name` atau `id` bernilai `name`.
+3. Tekan tombol Enter di keyboard untuk submit input.
+
+Hasil akhir dari ketiga aksi di atas akan diverifikasi oleh fungsi _step definition_ yang dipetakan dengan kalimat `Then`.
+Contohnya adalah sebagai berikut:
+
+```java
+@Then("{actor} should see the specialty {string} on the list of specialties")
+public void shouldSeeTheSpecialty(Actor actor, String name) {
+    actor.attemptsTo(
+            Ensure.thatAmongst(InputField.withNameOrId("spec_name"))
+                  .anyMatch(name + " should be on the list",
+                          (field) -> field.getValue()
+                                          .equals(name)
+                  )
+    );
+}
+```
+
+Pada contoh kode di atas, objek `Actor` sama-sama menggunakan fungsi `attemptsTo` seperti pada fungsi _step definition_ untuk kalimat `When`.
+Hanya saja, parameter fungsi tersebut sebaikan diisi dengan objek `PerformablePredicate` yang dapat dibuatkan oleh objek `Ensure`.
+Objek `Ensure` menyediakan fungsi untuk merepresentasikan aksi verifikasi yang dilakukan oleh aktor terhadap kondisi akhir SUR.
+Aksi verifikasi yang dilakukan oleh aktor adalah sebagai berikut:
+
+1. Aktor melihat seluruh input field dengan atribut `name` atau `id` bernilai `spec_name`.
+2. Kemudian cek apakah nama bidang spesialis yang baru dimasukkan di antara seluruh input field yang ditemukan.
+
+Jika aksi verifikasi berhasil dilakukan, maka _test framework_ akan melaporkan skenario test tersebut berhasil/"pass".
+Sebaliknya, jika ternyata verifikasi gagal dilakukan, maka _test framework_ akan melaporka skenario test tersebut gagal/"fail".
+
+Sebagai ringkasan, resep yang umum dipakai dalam membuat _step definition_ adalah sebagai berikut:
+
+- _Step definition_ dari kalimat berawalan `Given` berperan untuk menyiapkan kondisi awal pada SUT sebelum menyimulasikan fitur.
+  Kode Java yang dapat digunakan dalam implementasi _step definition_ ini adalah fungsi `wasAbleTo` milik objek `Actor`.
+  Fungsi tersebut menerima parameter berupa satu atau lebih objek `Performable` yang merepresentasikan aksi aktor.
+  Masing-masing objek `Performable` akan dijalankan sesuai dengan urutan objeknya di dalam parameter fungsi `wasAbleTo`.
+- _Step definition_ dari kalimat berawalan `When` berperan untuk menjalankan aksi-aksi utama yang menyimulasikan fitur.
+  Kode Java yang dapat digunakan dalam implementasi _step definition_ ini adalah fungsi `attemptsTo` milik objek `Actor`.
+  Serupa dengan `wasAbleTo`, fungsi `attemptsTo` menerima parameter berupa satu atau lebih objek `Performable`.
+- _Step definition_ dari kalimat berawalan `Then` berperan untuk melakukan verifikasi terhadap kondisi akhir pada SUT dengan kondisi yang diharapkan oleh skenario test.
+  Kode Java yang dapat digunakan dalam implementasi _step definition_ ini adalah fungsi `attemptsTo` milik objek `Actor`.
+  Hanya saja, objek-objek yang dimasukkan ke parameter fungsi `attemptsTo` sebaiknya adalah objek bertipe `PerformablePredicate` yang dapat dibangun dengan bantuan objek `Ensure`.
+  Objek `Ensure` mengandung instruksi yang melakukan verifikasi terhadap kondisi akhir SUT.
+
+Sebelum memulai latihan mandiri membuat _step definition_ baru,
+ada baiknya kita mempelajari secara singkat bagaimana kita dapat merepresentasikan elemen-elemen _user interface_ ke dalam kode test.
+
+## XPath
+
+Serenity sebagai _test framework_ sudah menyediakan _library_ yang mempermudah pencarian elemen _user interface_ yang umum terlibat dalam interaksi aktor dengan SUT.
+Sebagai contoh, kita dapat mendapatkan referensi terhadap elemen tombol dengan teks "Add" dengan memanggil `Button.withText("Add)`.
+Namun akan ada kasus dimana kita elemen _user interface_ yang diinginkan berada di dalam struktur halaman yang cukup kompleks
+dan belum ada _library_ yang dapat membantu kita menemukan elemen tersebut.
+
+<!-- TODO: Elaborasi dengan menjelaskan class-class yang tersedia di dalam _package_ `net.serenitybdd.screenplay.ui` -->
+
+Mari lihat potongan kode berikut yang diambil dari fungsi _step definition_ untuk mengubah nama bidang spesialis pertama pada daftar:
+
+```java
+Click.on(Button.located(By.xpath("//table[@id='specialties']/tbody/tr[1]/td[2]/button[1]")))
+```
+
+Ekspresi XPath di atas merupakan instruksi untuk mencari sebuah elemen pada dokumen HTML.
+`//table[@id='specialties']` mencari elemen `table` dengan atribut `id` bernilai `specialties`.
+Kemudian dilanjutkan dengan `/tbody` yang akan mencari elemen `tbody` yang berada di dalam `table`.
+Lalu `/tr[1]/td[2]/button[1]` berarti mencari elemen `tr` (baris) pertama di dalam tabel data,
+diikuti dengan mencari elemen `td` (kolom) kedua di baris tersebut,
+dan diakhiri dengan mencari elemen `button` (tombol) pertama di kolom tersebut.
+
+Berikut ini adalah daftar referensi terkait XPath yang direkomendasikan:
+
+- [XPath | Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/XPath)
+- [XPath Cheatsheet](https://devhints.io/xpath)
+-
+### Latihan Singkat: Membuat Step Definition Baru
+
+> TODO: Tambah pengantar
+
+1. [ ] Buat _class_ Java baru bernama `PetTypeStepDefinitions` di dalam _package_ `stepdefinitions`.
+2. [ ] Buat fungsi _step definition_ yang dipetakan ke kalimat `Given` pada skenario test "Add new pet type"
+3. [ ] Buat fungsi _step definition_ yang dipetakan ke kalimat `When` pada skenario test "Add new pet type"
+4. [ ] Buat fungsi _step definition_ yang dipetakan ke kalimat `Then` pada skenario test "Add new pet type"
+5. [ ] Jalankan _test suite_ BDD untuk fitur "Manage pet types"
+   dan pastikan masing-masing fungsi _step definition_ berhasil dicocokkan dengan kalimat terkait.
+
+Tidak ada contoh solusi untuk latihan ini karena hasil akhirnya mirip dengan fungsi-fungsi _step definition_ untuk fitur "Manage specialties".
+Silakan lihat isi _class_ `SpecialtyStepDefinitions` sebagai referensi.
+
+## Refactoring Pada Deskripsi Fitur & Glue Code
+
+> TODO: Tambah paragraf pengantar, angkat isu DRY (Dont Repeat Yourself)
+
+```gherkin
+Feature: Manage specialties
+  The veterinarian wants to be able to add, edit, and delete specialties.
+
+  Scenario: Add new specialty
+    Given a system operator named "John" is looking at the specialties page
+    When he adds a new specialty called "cardiology"
+    Then he should see the specialty "cardiology" on the list of specialties
+
+  Scenario: Edit the first specialty
+    Given a system operator named "John" is looking at the specialties page
+    When he edits the first specialty to "snake oil"
+    Then he should see the specialty "snake oil" on the list of specialties
+```
+
+Kalimat berawalan `Given` muncul berulang kali pada deskripsi fitur di atas
+karena masing-masing skenario test memiliki kondisi awal SUT yang sama,
+yaitu aktor membuka laman daftar bidang spesialis dokter hewan.
+
+Untuk mengurangi duplikasi, kita dapat menggunakan kata kunci `Background`.
+`Background` dapat mengandung kalimat-kalimat yang akan diterapkan ke setiap skenario test di dalam deskripsi fitur.
+
+```gherkin
+Feature: Manage specialties
+  The veterinarian wants to be able to add, edit, and delete specialties.
+
+  Background:
+    Given a system operator named "John" is looking at the specialties page
+
+  Scenario: Add new specialty
+    When he adds a new specialty called "cardiology"
+    Then he should see the specialty "cardiology" on the list of specialties
+
+  Scenario: Edit the first specialty
+    When he edits the first specialty to "snake oil"
+    Then he should see the specialty "snake oil" on the list of specialties
+```
+
+Coba jalankan kembali _test suite_ BDD dan lihat laporan hasil test.
+Masing-masing skenario test akan melaporkan kalimat `Given` yang sama di dalam laporannya.
+
+_Refactoring_ juga dapat diterapkan pada Glue Code.
+Sebagai contoh, mari lihat potongan kode yang menyimulasikan aksi klik menu pada _navigation bar_:
+
+```java
+// Ditemukan pada step definition fitur terkait Specialty
+HoverOverBy.over(Link.withText("Specialties"))
+Click.on(Link.withText("Specialties"))
+// Ditemukan pada step definition fitur terkait Pet Type
+HoverOverBy.over(Link.withText("Pet Types"))
+Click.on(Link.withText("Pet Types"))
+```
+
+Aksi klik menu tersebut dapat digeneralisasi untuk digunakan kembali ketika kita akan menguji fitur lainnya yang dapat diakses melalui _navigation bar_ yang sama.
+
+```java
+package starter.helpers;
+
+import net.serenitybdd.screenplay.Performable;
+import net.serenitybdd.screenplay.Task;
+import net.serenitybdd.screenplay.actions.Click;
+import net.serenitybdd.screenplay.actions.HoverOverBy;
+import net.serenitybdd.screenplay.ui.Link;
+
+public class NavigationBar {
+
+    public static Performable clickMenu(String name) {
+        return Task.where("{0} clicks '" + name + "' on the navigation bar",
+                HoverOverBy.over(Link.withText(name)),
+                Click.on(Link.withText(name)));
+    }
+}
+```
+
+### Latihan Singkat: Refactor Aksi Verifikasi Isi Daftar
+
+Lihat kembali fungsi _step definition_ untuk kalimat `Then` fitur "Manage specialties" berikut:
+
+```java
+@Then("{actor} should see the pet type {string} on the list of pet types")
+public void shouldSeeThePetType(Actor actor, String name) {
+    actor.attemptsTo(
+            Ensure.thatAmongst(InputField.withNameOrId("pettype_name"))
+                  .anyMatch(name + " should be on the list",
+                          (field) -> field.getValue()
+                                          .equals(name)
+                  )
+    );
+}
+```
+
+Prosedur verifikasi _data item_ pada daftar berpotensi untuk digeneralisasi sehingga dapat digunakan untuk prosedur serupa di fitur lain.
+Coba lakukan _refactoring_ terhadap prosedur verifikasi tersebut dengan langkah-langkah berikut:
+
+1. [ ] Buat _class_ Java baru dengan nama bebas di dalam _package_ `starters.helpers`.
+   Berikan nama _class_ Java yang deskriptif.
+2. [ ] Di dalam _class_ baru tersebut, implementasikan sebuah fungsi _static_ baru yang mengembalikan enkapsulasi prosedur verifikasi isi daftar sebagai objek tipe `PerformablePredicate`.
+3. [ ] Ganti prosedur verifikasi di fungsi _step definition_ terkait dengan pemanggilan fungsi yang dibuat pada langkah sebelumnya.
+4. [ ] Jalankan kembali _test suite_ BDD dan pastikan hasilnya masih sama sebelum melakukan _refactoring_.
+
+Contoh solusi dari fungsi yang mengandung hasil enkapsulasi adalah sebagai berikut:
+
+```java
+public static PerformablePredicate verifyDataItem(String inputFieldNameOrId, String dataItem) {
+    return Ensure.thatAmongst(InputField.withNameOrId(inputFieldNameOrId))
+                 .anyMatch(name + " should be on the list",
+                         (field) -> field.getValue()
+                                         .equals(dataItem)
+                 );
+}
+```
+
+## Latihan Mandiri: Membuat Test Suite BDD Untuk Fitur Lainnya
+
+<!-- TODO: Fitur-fitur lainnya ada: Manage Veterinarian, Manage Owner -->
+<!-- Buat deskripsi latihannya untuk dikerjakan di hands-on setelah makan siang -->
+
+<!--
+TODO:
+- Klarifikasi perbedaan `wasAbleTo` dan `attemptsTo`.
+  - Daya: Saya saat ini menduga `wasAbleTo` lebih spesifik digunakan pada step definition @Given karena bertugas untuk menyiapkan preconditions sebelum fitur diujicobakan.
+-->
+
+[1]: https://docs.google.com/presentation/d/1YQtWa0k45xCsa3Rugt-xfZxUSCg8uoMQepMASh1QEeU?usp=drive_fs
+[2]: https://gitlab.cs.ui.ac.id/pmpl/workshops/spring-petclinic-bdd/-/blob/development/docker-compose.yml
+[3]: https://gitlab.cs.ui.ac.id/pmpl/workshops/spring-petclinic-bdd/-/blob/development/src/test/resources/features/veterinarian/manage_specialties.feature
diff --git a/poetry.lock b/poetry.lock
index cd2a96a80bb0a62f5a71b6788d191513710363c1..20b3ed9cdf6e61e264630541c7998680ceeb1dbb 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -2,15 +2,18 @@
 
 [[package]]
 name = "babel"
-version = "2.13.0"
+version = "2.13.1"
 description = "Internationalization utilities"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "Babel-2.13.0-py3-none-any.whl", hash = "sha256:fbfcae1575ff78e26c7449136f1abbefc3c13ce542eeb13d43d50d8b047216ec"},
-    {file = "Babel-2.13.0.tar.gz", hash = "sha256:04c3e2d28d2b7681644508f836be388ae49e0cfe91465095340395b60d00f210"},
+    {file = "Babel-2.13.1-py3-none-any.whl", hash = "sha256:7077a4984b02b6727ac10f1f7294484f737443d7e2e66c5e4380e41a3ae0b4ed"},
+    {file = "Babel-2.13.1.tar.gz", hash = "sha256:33e0952d7dd6374af8dbf6768cc4ddf3ccfefc244f9986d4074704f2fbd18900"},
 ]
 
+[package.dependencies]
+setuptools = {version = "*", markers = "python_version >= \"3.12\""}
+
 [package.extras]
 dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"]
 
@@ -38,101 +41,101 @@ files = [
 
 [[package]]
 name = "charset-normalizer"
-version = "3.3.0"
+version = "3.3.1"
 description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
 optional = false
 python-versions = ">=3.7.0"
 files = [
-    {file = "charset-normalizer-3.3.0.tar.gz", hash = "sha256:63563193aec44bce707e0c5ca64ff69fa72ed7cf34ce6e11d5127555756fd2f6"},
-    {file = "charset_normalizer-3.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:effe5406c9bd748a871dbcaf3ac69167c38d72db8c9baf3ff954c344f31c4cbe"},
-    {file = "charset_normalizer-3.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4162918ef3098851fcd8a628bf9b6a98d10c380725df9e04caf5ca6dd48c847a"},
-    {file = "charset_normalizer-3.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0570d21da019941634a531444364f2482e8db0b3425fcd5ac0c36565a64142c8"},
-    {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5707a746c6083a3a74b46b3a631d78d129edab06195a92a8ece755aac25a3f3d"},
-    {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:278c296c6f96fa686d74eb449ea1697f3c03dc28b75f873b65b5201806346a69"},
-    {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a4b71f4d1765639372a3b32d2638197f5cd5221b19531f9245fcc9ee62d38f56"},
-    {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5969baeaea61c97efa706b9b107dcba02784b1601c74ac84f2a532ea079403e"},
-    {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3f93dab657839dfa61025056606600a11d0b696d79386f974e459a3fbc568ec"},
-    {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:db756e48f9c5c607b5e33dd36b1d5872d0422e960145b08ab0ec7fd420e9d649"},
-    {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:232ac332403e37e4a03d209a3f92ed9071f7d3dbda70e2a5e9cff1c4ba9f0678"},
-    {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e5c1502d4ace69a179305abb3f0bb6141cbe4714bc9b31d427329a95acfc8bdd"},
-    {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:2502dd2a736c879c0f0d3e2161e74d9907231e25d35794584b1ca5284e43f596"},
-    {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23e8565ab7ff33218530bc817922fae827420f143479b753104ab801145b1d5b"},
-    {file = "charset_normalizer-3.3.0-cp310-cp310-win32.whl", hash = "sha256:1872d01ac8c618a8da634e232f24793883d6e456a66593135aeafe3784b0848d"},
-    {file = "charset_normalizer-3.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:557b21a44ceac6c6b9773bc65aa1b4cc3e248a5ad2f5b914b91579a32e22204d"},
-    {file = "charset_normalizer-3.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d7eff0f27edc5afa9e405f7165f85a6d782d308f3b6b9d96016c010597958e63"},
-    {file = "charset_normalizer-3.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a685067d05e46641d5d1623d7c7fdf15a357546cbb2f71b0ebde91b175ffc3e"},
-    {file = "charset_normalizer-3.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0d3d5b7db9ed8a2b11a774db2bbea7ba1884430a205dbd54a32d61d7c2a190fa"},
-    {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2935ffc78db9645cb2086c2f8f4cfd23d9b73cc0dc80334bc30aac6f03f68f8c"},
-    {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fe359b2e3a7729010060fbca442ca225280c16e923b37db0e955ac2a2b72a05"},
-    {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:380c4bde80bce25c6e4f77b19386f5ec9db230df9f2f2ac1e5ad7af2caa70459"},
-    {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0d1e3732768fecb052d90d62b220af62ead5748ac51ef61e7b32c266cac9293"},
-    {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b2919306936ac6efb3aed1fbf81039f7087ddadb3160882a57ee2ff74fd2382"},
-    {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f8888e31e3a85943743f8fc15e71536bda1c81d5aa36d014a3c0c44481d7db6e"},
-    {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:82eb849f085624f6a607538ee7b83a6d8126df6d2f7d3b319cb837b289123078"},
-    {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7b8b8bf1189b3ba9b8de5c8db4d541b406611a71a955bbbd7385bbc45fcb786c"},
-    {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5adf257bd58c1b8632046bbe43ee38c04e1038e9d37de9c57a94d6bd6ce5da34"},
-    {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c350354efb159b8767a6244c166f66e67506e06c8924ed74669b2c70bc8735b1"},
-    {file = "charset_normalizer-3.3.0-cp311-cp311-win32.whl", hash = "sha256:02af06682e3590ab952599fbadac535ede5d60d78848e555aa58d0c0abbde786"},
-    {file = "charset_normalizer-3.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:86d1f65ac145e2c9ed71d8ffb1905e9bba3a91ae29ba55b4c46ae6fc31d7c0d4"},
-    {file = "charset_normalizer-3.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:3b447982ad46348c02cb90d230b75ac34e9886273df3a93eec0539308a6296d7"},
-    {file = "charset_normalizer-3.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:abf0d9f45ea5fb95051c8bfe43cb40cda383772f7e5023a83cc481ca2604d74e"},
-    {file = "charset_normalizer-3.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b09719a17a2301178fac4470d54b1680b18a5048b481cb8890e1ef820cb80455"},
-    {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3d9b48ee6e3967b7901c052b670c7dda6deb812c309439adaffdec55c6d7b78"},
-    {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:edfe077ab09442d4ef3c52cb1f9dab89bff02f4524afc0acf2d46be17dc479f5"},
-    {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3debd1150027933210c2fc321527c2299118aa929c2f5a0a80ab6953e3bd1908"},
-    {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86f63face3a527284f7bb8a9d4f78988e3c06823f7bea2bd6f0e0e9298ca0403"},
-    {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:24817cb02cbef7cd499f7c9a2735286b4782bd47a5b3516a0e84c50eab44b98e"},
-    {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c71f16da1ed8949774ef79f4a0260d28b83b3a50c6576f8f4f0288d109777989"},
-    {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:9cf3126b85822c4e53aa28c7ec9869b924d6fcfb76e77a45c44b83d91afd74f9"},
-    {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:b3b2316b25644b23b54a6f6401074cebcecd1244c0b8e80111c9a3f1c8e83d65"},
-    {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:03680bb39035fbcffe828eae9c3f8afc0428c91d38e7d61aa992ef7a59fb120e"},
-    {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cc152c5dd831641e995764f9f0b6589519f6f5123258ccaca8c6d34572fefa8"},
-    {file = "charset_normalizer-3.3.0-cp312-cp312-win32.whl", hash = "sha256:b8f3307af845803fb0b060ab76cf6dd3a13adc15b6b451f54281d25911eb92df"},
-    {file = "charset_normalizer-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:8eaf82f0eccd1505cf39a45a6bd0a8cf1c70dcfc30dba338207a969d91b965c0"},
-    {file = "charset_normalizer-3.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dc45229747b67ffc441b3de2f3ae5e62877a282ea828a5bdb67883c4ee4a8810"},
-    {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f4a0033ce9a76e391542c182f0d48d084855b5fcba5010f707c8e8c34663d77"},
-    {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ada214c6fa40f8d800e575de6b91a40d0548139e5dc457d2ebb61470abf50186"},
-    {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b1121de0e9d6e6ca08289583d7491e7fcb18a439305b34a30b20d8215922d43c"},
-    {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1063da2c85b95f2d1a430f1c33b55c9c17ffaf5e612e10aeaad641c55a9e2b9d"},
-    {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70f1d09c0d7748b73290b29219e854b3207aea922f839437870d8cc2168e31cc"},
-    {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:250c9eb0f4600361dd80d46112213dff2286231d92d3e52af1e5a6083d10cad9"},
-    {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:750b446b2ffce1739e8578576092179160f6d26bd5e23eb1789c4d64d5af7dc7"},
-    {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:fc52b79d83a3fe3a360902d3f5d79073a993597d48114c29485e9431092905d8"},
-    {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:588245972aca710b5b68802c8cad9edaa98589b1b42ad2b53accd6910dad3545"},
-    {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e39c7eb31e3f5b1f88caff88bcff1b7f8334975b46f6ac6e9fc725d829bc35d4"},
-    {file = "charset_normalizer-3.3.0-cp37-cp37m-win32.whl", hash = "sha256:abecce40dfebbfa6abf8e324e1860092eeca6f7375c8c4e655a8afb61af58f2c"},
-    {file = "charset_normalizer-3.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:24a91a981f185721542a0b7c92e9054b7ab4fea0508a795846bc5b0abf8118d4"},
-    {file = "charset_normalizer-3.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:67b8cc9574bb518ec76dc8e705d4c39ae78bb96237cb533edac149352c1f39fe"},
-    {file = "charset_normalizer-3.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac71b2977fb90c35d41c9453116e283fac47bb9096ad917b8819ca8b943abecd"},
-    {file = "charset_normalizer-3.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3ae38d325b512f63f8da31f826e6cb6c367336f95e418137286ba362925c877e"},
-    {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:542da1178c1c6af8873e143910e2269add130a299c9106eef2594e15dae5e482"},
-    {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30a85aed0b864ac88309b7d94be09f6046c834ef60762a8833b660139cfbad13"},
-    {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aae32c93e0f64469f74ccc730a7cb21c7610af3a775157e50bbd38f816536b38"},
-    {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15b26ddf78d57f1d143bdf32e820fd8935d36abe8a25eb9ec0b5a71c82eb3895"},
-    {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f5d10bae5d78e4551b7be7a9b29643a95aded9d0f602aa2ba584f0388e7a557"},
-    {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:249c6470a2b60935bafd1d1d13cd613f8cd8388d53461c67397ee6a0f5dce741"},
-    {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c5a74c359b2d47d26cdbbc7845e9662d6b08a1e915eb015d044729e92e7050b7"},
-    {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:b5bcf60a228acae568e9911f410f9d9e0d43197d030ae5799e20dca8df588287"},
-    {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:187d18082694a29005ba2944c882344b6748d5be69e3a89bf3cc9d878e548d5a"},
-    {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:81bf654678e575403736b85ba3a7867e31c2c30a69bc57fe88e3ace52fb17b89"},
-    {file = "charset_normalizer-3.3.0-cp38-cp38-win32.whl", hash = "sha256:85a32721ddde63c9df9ebb0d2045b9691d9750cb139c161c80e500d210f5e26e"},
-    {file = "charset_normalizer-3.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:468d2a840567b13a590e67dd276c570f8de00ed767ecc611994c301d0f8c014f"},
-    {file = "charset_normalizer-3.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e0fc42822278451bc13a2e8626cf2218ba570f27856b536e00cfa53099724828"},
-    {file = "charset_normalizer-3.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:09c77f964f351a7369cc343911e0df63e762e42bac24cd7d18525961c81754f4"},
-    {file = "charset_normalizer-3.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:12ebea541c44fdc88ccb794a13fe861cc5e35d64ed689513a5c03d05b53b7c82"},
-    {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:805dfea4ca10411a5296bcc75638017215a93ffb584c9e344731eef0dcfb026a"},
-    {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:96c2b49eb6a72c0e4991d62406e365d87067ca14c1a729a870d22354e6f68115"},
-    {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aaf7b34c5bc56b38c931a54f7952f1ff0ae77a2e82496583b247f7c969eb1479"},
-    {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:619d1c96099be5823db34fe89e2582b336b5b074a7f47f819d6b3a57ff7bdb86"},
-    {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0ac5e7015a5920cfce654c06618ec40c33e12801711da6b4258af59a8eff00a"},
-    {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:93aa7eef6ee71c629b51ef873991d6911b906d7312c6e8e99790c0f33c576f89"},
-    {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7966951325782121e67c81299a031f4c115615e68046f79b85856b86ebffc4cd"},
-    {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:02673e456dc5ab13659f85196c534dc596d4ef260e4d86e856c3b2773ce09843"},
-    {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:c2af80fb58f0f24b3f3adcb9148e6203fa67dd3f61c4af146ecad033024dde43"},
-    {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:153e7b6e724761741e0974fc4dcd406d35ba70b92bfe3fedcb497226c93b9da7"},
-    {file = "charset_normalizer-3.3.0-cp39-cp39-win32.whl", hash = "sha256:d47ecf253780c90ee181d4d871cd655a789da937454045b17b5798da9393901a"},
-    {file = "charset_normalizer-3.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:d97d85fa63f315a8bdaba2af9a6a686e0eceab77b3089af45133252618e70884"},
-    {file = "charset_normalizer-3.3.0-py3-none-any.whl", hash = "sha256:e46cd37076971c1040fc8c41273a8b3e2c624ce4f2be3f5dfcb7a430c1d3acc2"},
+    {file = "charset-normalizer-3.3.1.tar.gz", hash = "sha256:d9137a876020661972ca6eec0766d81aef8a5627df628b664b234b73396e727e"},
+    {file = "charset_normalizer-3.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8aee051c89e13565c6bd366813c386939f8e928af93c29fda4af86d25b73d8f8"},
+    {file = "charset_normalizer-3.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:352a88c3df0d1fa886562384b86f9a9e27563d4704ee0e9d56ec6fcd270ea690"},
+    {file = "charset_normalizer-3.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:223b4d54561c01048f657fa6ce41461d5ad8ff128b9678cfe8b2ecd951e3f8a2"},
+    {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f861d94c2a450b974b86093c6c027888627b8082f1299dfd5a4bae8e2292821"},
+    {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1171ef1fc5ab4693c5d151ae0fdad7f7349920eabbaca6271f95969fa0756c2d"},
+    {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28f512b9a33235545fbbdac6a330a510b63be278a50071a336afc1b78781b147"},
+    {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0e842112fe3f1a4ffcf64b06dc4c61a88441c2f02f373367f7b4c1aa9be2ad5"},
+    {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f9bc2ce123637a60ebe819f9fccc614da1bcc05798bbbaf2dd4ec91f3e08846"},
+    {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f194cce575e59ffe442c10a360182a986535fd90b57f7debfaa5c845c409ecc3"},
+    {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9a74041ba0bfa9bc9b9bb2cd3238a6ab3b7618e759b41bd15b5f6ad958d17605"},
+    {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b578cbe580e3b41ad17b1c428f382c814b32a6ce90f2d8e39e2e635d49e498d1"},
+    {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:6db3cfb9b4fcecb4390db154e75b49578c87a3b9979b40cdf90d7e4b945656e1"},
+    {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:debb633f3f7856f95ad957d9b9c781f8e2c6303ef21724ec94bea2ce2fcbd056"},
+    {file = "charset_normalizer-3.3.1-cp310-cp310-win32.whl", hash = "sha256:87071618d3d8ec8b186d53cb6e66955ef2a0e4fa63ccd3709c0c90ac5a43520f"},
+    {file = "charset_normalizer-3.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:e372d7dfd154009142631de2d316adad3cc1c36c32a38b16a4751ba78da2a397"},
+    {file = "charset_normalizer-3.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ae4070f741f8d809075ef697877fd350ecf0b7c5837ed68738607ee0a2c572cf"},
+    {file = "charset_normalizer-3.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:58e875eb7016fd014c0eea46c6fa92b87b62c0cb31b9feae25cbbe62c919f54d"},
+    {file = "charset_normalizer-3.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dbd95e300367aa0827496fe75a1766d198d34385a58f97683fe6e07f89ca3e3c"},
+    {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de0b4caa1c8a21394e8ce971997614a17648f94e1cd0640fbd6b4d14cab13a72"},
+    {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:985c7965f62f6f32bf432e2681173db41336a9c2611693247069288bcb0c7f8b"},
+    {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a15c1fe6d26e83fd2e5972425a772cca158eae58b05d4a25a4e474c221053e2d"},
+    {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae55d592b02c4349525b6ed8f74c692509e5adffa842e582c0f861751701a673"},
+    {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be4d9c2770044a59715eb57c1144dedea7c5d5ae80c68fb9959515037cde2008"},
+    {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:851cf693fb3aaef71031237cd68699dded198657ec1e76a76eb8be58c03a5d1f"},
+    {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:31bbaba7218904d2eabecf4feec0d07469284e952a27400f23b6628439439fa7"},
+    {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:871d045d6ccc181fd863a3cd66ee8e395523ebfbc57f85f91f035f50cee8e3d4"},
+    {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:501adc5eb6cd5f40a6f77fbd90e5ab915c8fd6e8c614af2db5561e16c600d6f3"},
+    {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f5fb672c396d826ca16a022ac04c9dce74e00a1c344f6ad1a0fdc1ba1f332213"},
+    {file = "charset_normalizer-3.3.1-cp311-cp311-win32.whl", hash = "sha256:bb06098d019766ca16fc915ecaa455c1f1cd594204e7f840cd6258237b5079a8"},
+    {file = "charset_normalizer-3.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:8af5a8917b8af42295e86b64903156b4f110a30dca5f3b5aedea123fbd638bff"},
+    {file = "charset_normalizer-3.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7ae8e5142dcc7a49168f4055255dbcced01dc1714a90a21f87448dc8d90617d1"},
+    {file = "charset_normalizer-3.3.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5b70bab78accbc672f50e878a5b73ca692f45f5b5e25c8066d748c09405e6a55"},
+    {file = "charset_normalizer-3.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5ceca5876032362ae73b83347be8b5dbd2d1faf3358deb38c9c88776779b2e2f"},
+    {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34d95638ff3613849f473afc33f65c401a89f3b9528d0d213c7037c398a51296"},
+    {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9edbe6a5bf8b56a4a84533ba2b2f489d0046e755c29616ef8830f9e7d9cf5728"},
+    {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6a02a3c7950cafaadcd46a226ad9e12fc9744652cc69f9e5534f98b47f3bbcf"},
+    {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10b8dd31e10f32410751b3430996f9807fc4d1587ca69772e2aa940a82ab571a"},
+    {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edc0202099ea1d82844316604e17d2b175044f9bcb6b398aab781eba957224bd"},
+    {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b891a2f68e09c5ef989007fac11476ed33c5c9994449a4e2c3386529d703dc8b"},
+    {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:71ef3b9be10070360f289aea4838c784f8b851be3ba58cf796262b57775c2f14"},
+    {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:55602981b2dbf8184c098bc10287e8c245e351cd4fdcad050bd7199d5a8bf514"},
+    {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:46fb9970aa5eeca547d7aa0de5d4b124a288b42eaefac677bde805013c95725c"},
+    {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:520b7a142d2524f999447b3a0cf95115df81c4f33003c51a6ab637cbda9d0bf4"},
+    {file = "charset_normalizer-3.3.1-cp312-cp312-win32.whl", hash = "sha256:8ec8ef42c6cd5856a7613dcd1eaf21e5573b2185263d87d27c8edcae33b62a61"},
+    {file = "charset_normalizer-3.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:baec8148d6b8bd5cee1ae138ba658c71f5b03e0d69d5907703e3e1df96db5e41"},
+    {file = "charset_normalizer-3.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63a6f59e2d01310f754c270e4a257426fe5a591dc487f1983b3bbe793cf6bac6"},
+    {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d6bfc32a68bc0933819cfdfe45f9abc3cae3877e1d90aac7259d57e6e0f85b1"},
+    {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4f3100d86dcd03c03f7e9c3fdb23d92e32abbca07e7c13ebd7ddfbcb06f5991f"},
+    {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39b70a6f88eebe239fa775190796d55a33cfb6d36b9ffdd37843f7c4c1b5dc67"},
+    {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e12f8ee80aa35e746230a2af83e81bd6b52daa92a8afaef4fea4a2ce9b9f4fa"},
+    {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b6cefa579e1237ce198619b76eaa148b71894fb0d6bcf9024460f9bf30fd228"},
+    {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:61f1e3fb621f5420523abb71f5771a204b33c21d31e7d9d86881b2cffe92c47c"},
+    {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4f6e2a839f83a6a76854d12dbebde50e4b1afa63e27761549d006fa53e9aa80e"},
+    {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:1ec937546cad86d0dce5396748bf392bb7b62a9eeb8c66efac60e947697f0e58"},
+    {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:82ca51ff0fc5b641a2d4e1cc8c5ff108699b7a56d7f3ad6f6da9dbb6f0145b48"},
+    {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:633968254f8d421e70f91c6ebe71ed0ab140220469cf87a9857e21c16687c034"},
+    {file = "charset_normalizer-3.3.1-cp37-cp37m-win32.whl", hash = "sha256:c0c72d34e7de5604df0fde3644cc079feee5e55464967d10b24b1de268deceb9"},
+    {file = "charset_normalizer-3.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:63accd11149c0f9a99e3bc095bbdb5a464862d77a7e309ad5938fbc8721235ae"},
+    {file = "charset_normalizer-3.3.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5a3580a4fdc4ac05f9e53c57f965e3594b2f99796231380adb2baaab96e22761"},
+    {file = "charset_normalizer-3.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2465aa50c9299d615d757c1c888bc6fef384b7c4aec81c05a0172b4400f98557"},
+    {file = "charset_normalizer-3.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cb7cd68814308aade9d0c93c5bd2ade9f9441666f8ba5aa9c2d4b389cb5e2a45"},
+    {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91e43805ccafa0a91831f9cd5443aa34528c0c3f2cc48c4cb3d9a7721053874b"},
+    {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:854cc74367180beb327ab9d00f964f6d91da06450b0855cbbb09187bcdb02de5"},
+    {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c15070ebf11b8b7fd1bfff7217e9324963c82dbdf6182ff7050519e350e7ad9f"},
+    {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c4c99f98fc3a1835af8179dcc9013f93594d0670e2fa80c83aa36346ee763d2"},
+    {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fb765362688821404ad6cf86772fc54993ec11577cd5a92ac44b4c2ba52155b"},
+    {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dced27917823df984fe0c80a5c4ad75cf58df0fbfae890bc08004cd3888922a2"},
+    {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a66bcdf19c1a523e41b8e9d53d0cedbfbac2e93c649a2e9502cb26c014d0980c"},
+    {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ecd26be9f112c4f96718290c10f4caea6cc798459a3a76636b817a0ed7874e42"},
+    {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:3f70fd716855cd3b855316b226a1ac8bdb3caf4f7ea96edcccc6f484217c9597"},
+    {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:17a866d61259c7de1bdadef418a37755050ddb4b922df8b356503234fff7932c"},
+    {file = "charset_normalizer-3.3.1-cp38-cp38-win32.whl", hash = "sha256:548eefad783ed787b38cb6f9a574bd8664468cc76d1538215d510a3cd41406cb"},
+    {file = "charset_normalizer-3.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:45f053a0ece92c734d874861ffe6e3cc92150e32136dd59ab1fb070575189c97"},
+    {file = "charset_normalizer-3.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bc791ec3fd0c4309a753f95bb6c749ef0d8ea3aea91f07ee1cf06b7b02118f2f"},
+    {file = "charset_normalizer-3.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0c8c61fb505c7dad1d251c284e712d4e0372cef3b067f7ddf82a7fa82e1e9a93"},
+    {file = "charset_normalizer-3.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2c092be3885a1b7899cd85ce24acedc1034199d6fca1483fa2c3a35c86e43041"},
+    {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2000c54c395d9e5e44c99dc7c20a64dc371f777faf8bae4919ad3e99ce5253e"},
+    {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4cb50a0335382aac15c31b61d8531bc9bb657cfd848b1d7158009472189f3d62"},
+    {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c30187840d36d0ba2893bc3271a36a517a717f9fd383a98e2697ee890a37c273"},
+    {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe81b35c33772e56f4b6cf62cf4aedc1762ef7162a31e6ac7fe5e40d0149eb67"},
+    {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0bf89afcbcf4d1bb2652f6580e5e55a840fdf87384f6063c4a4f0c95e378656"},
+    {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:06cf46bdff72f58645434d467bf5228080801298fbba19fe268a01b4534467f5"},
+    {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:3c66df3f41abee950d6638adc7eac4730a306b022570f71dd0bd6ba53503ab57"},
+    {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd805513198304026bd379d1d516afbf6c3c13f4382134a2c526b8b854da1c2e"},
+    {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:9505dc359edb6a330efcd2be825fdb73ee3e628d9010597aa1aee5aa63442e97"},
+    {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:31445f38053476a0c4e6d12b047b08ced81e2c7c712e5a1ad97bc913256f91b2"},
+    {file = "charset_normalizer-3.3.1-cp39-cp39-win32.whl", hash = "sha256:bd28b31730f0e982ace8663d108e01199098432a30a4c410d06fe08fdb9e93f4"},
+    {file = "charset_normalizer-3.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:555fe186da0068d3354cdf4bbcbc609b0ecae4d04c921cc13e209eece7720727"},
+    {file = "charset_normalizer-3.3.1-py3-none-any.whl", hash = "sha256:800561453acdecedaac137bf09cd719c7a440b6800ec182f077bb8e7025fb708"},
 ]
 
 [[package]]
@@ -206,13 +209,13 @@ dev = ["flake8", "markdown", "twine", "wheel"]
 
 [[package]]
 name = "gitdb"
-version = "4.0.10"
+version = "4.0.11"
 description = "Git Object Database"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "gitdb-4.0.10-py3-none-any.whl", hash = "sha256:c286cf298426064079ed96a9e4a9d39e7f3e9bf15ba60701e95f5492f28415c7"},
-    {file = "gitdb-4.0.10.tar.gz", hash = "sha256:6eb990b69df4e15bad899ea868dc46572c3f75339735663b81de79b06f17eb9a"},
+    {file = "gitdb-4.0.11-py3-none-any.whl", hash = "sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4"},
+    {file = "gitdb-4.0.11.tar.gz", hash = "sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b"},
 ]
 
 [package.dependencies]
@@ -220,20 +223,20 @@ smmap = ">=3.0.1,<6"
 
 [[package]]
 name = "gitpython"
-version = "3.1.37"
+version = "3.1.40"
 description = "GitPython is a Python library used to interact with Git repositories"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "GitPython-3.1.37-py3-none-any.whl", hash = "sha256:5f4c4187de49616d710a77e98ddf17b4782060a1788df441846bddefbb89ab33"},
-    {file = "GitPython-3.1.37.tar.gz", hash = "sha256:f9b9ddc0761c125d5780eab2d64be4873fc6817c2899cbcb34b02344bdc7bc54"},
+    {file = "GitPython-3.1.40-py3-none-any.whl", hash = "sha256:cf14627d5a8049ffbf49915732e5eddbe8134c3bdb9d476e6182b676fc573f8a"},
+    {file = "GitPython-3.1.40.tar.gz", hash = "sha256:22b126e9ffb671fdd0c129796343a02bf67bf2994b35449ffc9321aa755e18a4"},
 ]
 
 [package.dependencies]
 gitdb = ">=4.0.1,<5"
 
 [package.extras]
-test = ["black", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mypy", "pre-commit", "pytest", "pytest-cov", "pytest-sugar"]
+test = ["black", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest", "pytest-cov", "pytest-instafail", "pytest-subtests", "pytest-sugar"]
 
 [[package]]
 name = "identify"
@@ -450,13 +453,13 @@ recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2.
 
 [[package]]
 name = "mkdocs-material-extensions"
-version = "1.2"
+version = "1.3"
 description = "Extension pack for Python Markdown and MkDocs Material."
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "mkdocs_material_extensions-1.2-py3-none-any.whl", hash = "sha256:c767bd6d6305f6420a50f0b541b0c9966d52068839af97029be14443849fb8a1"},
-    {file = "mkdocs_material_extensions-1.2.tar.gz", hash = "sha256:27e2d1ed2d031426a6e10d5ea06989d67e90bb02acd588bc5673106b5ee5eedf"},
+    {file = "mkdocs_material_extensions-1.3-py3-none-any.whl", hash = "sha256:0297cc48ba68a9fdd1ef3780a3b41b534b0d0df1d1181a44676fda5f464eeadc"},
+    {file = "mkdocs_material_extensions-1.3.tar.gz", hash = "sha256:f0446091503acb110a7cab9349cbc90eeac51b58d1caa92a704a81ca1e24ddbd"},
 ]
 
 [[package]]
@@ -554,13 +557,13 @@ plugins = ["importlib-metadata"]
 
 [[package]]
 name = "pymdown-extensions"
-version = "10.3"
+version = "10.3.1"
 description = "Extension pack for Python Markdown."
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "pymdown_extensions-10.3-py3-none-any.whl", hash = "sha256:77a82c621c58a83efc49a389159181d570e370fff9f810d3a4766a75fc678b66"},
-    {file = "pymdown_extensions-10.3.tar.gz", hash = "sha256:94a0d8a03246712b64698af223848fd80aaf1ae4c4be29c8c61939b0467b5722"},
+    {file = "pymdown_extensions-10.3.1-py3-none-any.whl", hash = "sha256:8cba67beb2a1318cdaf742d09dff7c0fc4cafcc290147ade0f8fb7b71522711a"},
+    {file = "pymdown_extensions-10.3.1.tar.gz", hash = "sha256:f6c79941498a458852853872e379e7bab63888361ba20992fc8b4f8a9b61735e"},
 ]
 
 [package.dependencies]
@@ -837,13 +840,13 @@ files = [
 
 [[package]]
 name = "urllib3"
-version = "2.0.6"
+version = "2.0.7"
 description = "HTTP library with thread-safe connection pooling, file post, and more."
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "urllib3-2.0.6-py3-none-any.whl", hash = "sha256:7a7c7003b000adf9e7ca2a377c9688bbc54ed41b985789ed576570342a375cd2"},
-    {file = "urllib3-2.0.6.tar.gz", hash = "sha256:b19e1a85d206b56d7df1d5e683df4a7725252a964e3993648dd0fb5a1c157564"},
+    {file = "urllib3-2.0.7-py3-none-any.whl", hash = "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"},
+    {file = "urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84"},
 ]
 
 [package.extras]
@@ -854,13 +857,13 @@ zstd = ["zstandard (>=0.18.0)"]
 
 [[package]]
 name = "virtualenv"
-version = "20.24.5"
+version = "20.24.6"
 description = "Virtual Python Environment builder"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "virtualenv-20.24.5-py3-none-any.whl", hash = "sha256:b80039f280f4919c77b30f1c23294ae357c4c8701042086e3fc005963e4e537b"},
-    {file = "virtualenv-20.24.5.tar.gz", hash = "sha256:e8361967f6da6fbdf1426483bfe9fca8287c242ac0bc30429905721cefbff752"},
+    {file = "virtualenv-20.24.6-py3-none-any.whl", hash = "sha256:520d056652454c5098a00c0f073611ccbea4c79089331f60bf9d7ba247bb7381"},
+    {file = "virtualenv-20.24.6.tar.gz", hash = "sha256:02ece4f56fbf939dbbc33c0715159951d6bf14aaf5457b092e4548e1382455af"},
 ]
 
 [package.dependencies]
diff --git a/pyproject.toml b/pyproject.toml
index dc9d0d55b6a6719c703ac95570a8c7579de21579..09bd003e9022d4de22c1ff06698612388d762f4f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,9 +1,12 @@
 [tool.poetry]
 name = "pmpl-course-site"
-version = "2023.1"
+version = "2023.2"
 description = "The website for the Software Quality Assurance (SQA/PMPL) course at Faculty of Computer Science Universitas Indonesia."
 license = "CC-BY-SA-4.0"
-authors = ["Daya Adianto <dayaadianto@cs.ui.ac.id>"]
+authors = [
+    "Ade Azurat <ade@cs.ui.ac.id>",
+    "Daya Adianto <dayaadianto@cs.ui.ac.id>",
+]
 maintainers = [
     "Samuel Tupa Febrian <samuel.tupa@ui.ac.id>",
     "Adrika Novrialdi <adrika.novrialdi@ui.ac.id>",