diff --git a/.gitignore b/.gitignore index cabc789..fba3030 100644 --- a/.gitignore +++ b/.gitignore @@ -161,3 +161,4 @@ cython_debug/ # Miscellaneous .DS_Store +test_db.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9b38853 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "python.testing.pytestArgs": [ + "tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} \ No newline at end of file diff --git a/README.md b/README.md index 798f698..30233b7 100644 --- a/README.md +++ b/README.md @@ -6,39 +6,44 @@ OSR Console is a turn-based dungeon crawler RPG in the Old-School Renaissance (O ## Project status -As of 14 OCT 2023,development has been in progress for about six weeks. +Character creation isn't quite in, and there are no monsters or spells or combat or anything fun like that. This isn't yet a game you can actually play; more like early-stage hackin' stuff together-type stuff. 👉 = In progress - [x] Build partial proof-of-concept in private repo. - [x] Create public repo (this one). - [x] Init `mkdocs` project (sans actual docs). -- [ ] 👉 Move `osrlib`, `osr_ui`, and `tests` projects to public repo. +- [x] Move `osrlib`, `osrgame`, and `tests` projects to this repo. +- [x] Move to [Poetry](https://python-poetry.org/). - [ ] 👉 Character save/load in [TinyDB](https://tinydb.readthedocs.io/). -- [ ] ...and everything else it takes to make a turn-based RPG. +- [ ] Party and character creation workflow in UI. +- [ ] ...and everything else you need for a turn-based dungeon crawler fantasty RPG. ## Prerequisites -- Python 3.10+ -- Poetry +- Python 3.11+ +- Poetry 1.6+ ## Installation -TBD +This is a monorepo housing two projects. The game's library, `osrlib`, and its user interface, `osrgame`. For more information about each, see their respective `README.md` files. + +- [osrgame: Textual TUI for an OSR-style turn-based RPG](osrgame/README.md) +- [osrlib: Python library for OSR-style turn-based RPGs](osrlib/README.md) ## Usage -TBD +TODO ## Contribute -TBD +TODO ## License -[MIT License](LICENSE) +[MIT License](LICENSE) for now. ## Credits -- Project owner: @mmacy -- Game rules and mechanics inspired by the TSR's 1981 versions of the Dungeons & Dragons Basic and Expert sets, or *D&D B/X*. +- Project owner: [@mmacy](https://github.com/mmacy) +- Game rules and mechanics heavily inspired by the TSR's 1981 versions of the Dungeons & Dragons Basic and Expert sets, or *D&D B/X*. diff --git a/osrgame/README.md b/osrgame/README.md new file mode 100644 index 0000000..14a6611 --- /dev/null +++ b/osrgame/README.md @@ -0,0 +1,28 @@ +# osrgame: Textual TUI for an OSR-style turn-based RPG + +The `osrgame` package provides user interface for the OSR Console application--it's the thing you run to play the game. + +## Prerequisites + +- Python 3.11+ +- Poetry 1.6+ + +## Installation + +Install the application's dependencies and its virtual environment by using Poetry. This command will create a Python virtual environment and install `osrlib` and other dependencies for you. + +```sh +# Run from within the /osrgame directory +poetry install +``` + +## Usage + +Launch the OSR Console game application by using Poetry: + +```sh +# Run from within the /osrgame directory (same dir as install command) +poetry run python ./osrgame/osrgame.py +``` + +By starting the game with `poetry run`, you don't have to worry about manually entering a virtual environment because Poetry handles it for you. diff --git a/osrgame/osrgame/__init__.py b/osrgame/osrgame/__init__.py new file mode 100644 index 0000000..749d139 --- /dev/null +++ b/osrgame/osrgame/__init__.py @@ -0,0 +1,3 @@ +from .osrgame import OSRConsole +from .screen import CharacterScreen, WelcomeScreen, ModuleScreen, ExploreScreen +from .widgets import CharacterScreenButtons, CharacterStats, AbilityTable, ItemTable, SavingThrows \ No newline at end of file diff --git a/osrgame/osrgame/osrgame.py b/osrgame/osrgame/osrgame.py new file mode 100644 index 0000000..ca514f7 --- /dev/null +++ b/osrgame/osrgame/osrgame.py @@ -0,0 +1,57 @@ +from textual.app import App, ComposeResult + +from osrlib import CharacterClassType, PlayerCharacter, Armor, Item, Weapon +from screen import CharacterScreen, WelcomeScreen + + +class OSRConsole(App): + CSS_PATH = "screen.tcss" + + BINDINGS = [ + ("c", "character", "Character"), + ("q", "quit", "Quit"), + ] + + player_character = PlayerCharacter( + "Sckricko", CharacterClassType.FIGHTER + ) + armor = Armor("Chain Mail", -5, usable_by_classes = {CharacterClassType.FIGHTER}, max_equipped = 2, gp_value = 40) + shield = Armor("Shield", -1, usable_by_classes = {CharacterClassType.FIGHTER}, max_equipped = 2, gp_value = 10) + sword = Weapon("Sword", "1d8", usable_by_classes = {CharacterClassType.FIGHTER}, max_equipped = 1, gp_value = 10) + player_character.add_item_to_inventory(armor) + player_character.add_item_to_inventory(shield) + player_character.add_item_to_inventory(sword) + player_character.inventory.equip_item(armor) + player_character.inventory.equip_item(shield) + player_character.inventory.equip_item(sword) + + backpack = Item("Backpack", gp_value = 5) + wineskin = Item("Wineskin", gp_value = 1) + iron_rations = Item("Iron Rations", gp_value = 15) + torch = Item("Torch", gp_value = 1) + player_character.add_item_to_inventory(backpack) + player_character.add_item_to_inventory(wineskin) + player_character.add_item_to_inventory(iron_rations) + player_character.add_item_to_inventory(torch) + + SCREENS = {"scr_character": CharacterScreen()} + + def compose(self) -> ComposeResult: + yield WelcomeScreen() + + def on_mount(self) -> None: + self.title = "OSR Console" + self.sub_title = "Adventures in turn-based text" + + def action_character(self) -> None: + """Show the character screen.""" + self.push_screen("scr_character") + + def action_quit(self) -> None: + """Quit the application.""" + self.exit() + + +app = OSRConsole() +if __name__ == "__main__": + app.run() diff --git a/osrgame/osrgame/screen.py b/osrgame/osrgame/screen.py new file mode 100644 index 0000000..f46fb24 --- /dev/null +++ b/osrgame/osrgame/screen.py @@ -0,0 +1,230 @@ +from textual.app import ComposeResult +from textual.containers import Container +from textual.screen import Screen +from textual.widgets import Button, Header, Footer, Log, Placeholder + +from osrlib import CharacterClassType, Armor, Weapon +from widgets import CharacterStats, AbilityTable, ItemTable, SavingThrows, CharacterScreenButtons + + +#################### +# Welcome Screen +class WelcomeScreen(Screen): + BINDINGS = [ + ("escape", "app.pop_screen", "Pop screen"), + ("q", "quit", "Quit"), + ] + + def compose(self) -> ComposeResult: + yield Header(show_clock=True, id="header") + + yield Footer() + + def on_mount(self) -> None: + pass + + def on_button_pressed(self, event: Button.Pressed) -> None: + pass + + def action_quit(self) -> None: + """An action to quit the application.""" + self.exit() + + +#################### +# Main Screen +class MainScreen(Screen): + BINDINGS = [ + ("escape", "app.pop_screen", "Pop screen"), + ("q", "quit", "Quit"), + ] + + def compose(self) -> ComposeResult: + yield Header(show_clock=True, id="header") + yield Placeholder("Main") + yield Footer() + + def on_mount(self) -> None: + pass + + def on_button_pressed(self, event: Button.Pressed) -> None: + pass + + def action_quit(self) -> None: + """An action to quit the application.""" + self.exit() + + +#################### +# Character Screen + + +class CharacterScreen(Screen): + BINDINGS = [ + ("k", "clear_log", "Clear log"), + ("escape", "app.pop_screen", "Pop screen"), + ] + + def compose(self) -> ComposeResult: + yield Header(id="header", show_clock=True, classes="header-footer") + yield CharacterStats(id="stat-block", classes="box") + yield Log(id="log", auto_scroll=True, classes="box") + yield AbilityTable(id="ability-block") + yield SavingThrows(id="saving-throw-block") + yield ItemTable(id="item-block", classes="box") + yield CharacterScreenButtons(id="char-buttons", classes="char-buttons-class") + yield Footer() + + def on_mount(self) -> None: + """Perform actions when the widget is mounted.""" + self.query_one(Log).border_subtitle = "LOG" + self.query_one(CharacterStats).pc_name = self.app.player_character.name + self.query_one(CharacterStats).pc_class = self.app.player_character.character_class + self.query_one(CharacterStats).pc_level = self.app.player_character.character_class.current_level + self.query_one(CharacterStats).pc_hp = self.app.player_character.character_class.hp + self.query_one(CharacterStats).pc_ac = self.app.player_character.armor_class + self.query_one(AbilityTable).update_table() + self.query_one(SavingThrows).update_table() + self.query_one(ItemTable).items = self.app.player_character.inventory.all_items + + def on_button_pressed(self, event: Button.Pressed) -> None: + pc = self.app.player_character + if event.button.id == "btn_roll_abilities": + self.reroll() + self.query_one(CharacterStats).pc_ac = pc.armor_class + + elif event.button.id == "btn_roll_hp": + hp_roll = pc.roll_hp() + pc.character_class.hp = max(hp_roll.total_with_modifier, 1) + roll_string = hp_roll.pretty_print() + self.query_one(Log).write_line(roll_string) + self.query_one(CharacterStats).pc_hp = pc.character_class.hp + + def action_clear_log(self) -> None: + """An action to clear the log.""" + self.query_one(Log).clear() + + def reroll(self): + """Rolls the ability scores of the active character.""" + self.app.player_character.roll_abilities() + self.query_one(AbilityTable).update_table() + + +#################### +# Party Screen +class PartyScreen(Screen): + BINDINGS = [ + ("escape", "app.pop_screen", "Pop screen"), + ("q", "quit", "Quit"), + ] + + def compose(self) -> ComposeResult: + yield Header(show_clock=True, id="header") + yield Placeholder("Party Manager") + yield Footer() + + def on_mount(self) -> None: + pass + + def on_button_pressed(self, event: Button.Pressed) -> None: + pass + + def action_quit(self) -> None: + """An action to quit the application.""" + self.exit() + + +#################### +# Module Screen +class ModuleScreen(Screen): + BINDINGS = [ + ("escape", "app.pop_screen", "Pop screen"), + ("q", "quit", "Quit"), + ] + + def compose(self) -> ComposeResult: + yield Header(show_clock=True, id="header") + yield Placeholder("Module Manager") + yield Footer() + + def on_mount(self) -> None: + pass + + def on_button_pressed(self, event: Button.Pressed) -> None: + pass + + def action_quit(self) -> None: + """An action to quit the application.""" + self.exit() + + +#################### +# Explore Screen +class ExploreScreen(Screen): + BINDINGS = [ + ("escape", "app.pop_screen", "Pop screen"), + ("q", "quit", "Quit"), + ] + + def compose(self) -> ComposeResult: + yield Header(show_clock=True, id="header") + yield Placeholder("Explore") + yield Footer() + + def on_mount(self) -> None: + pass + + def on_button_pressed(self, event: Button.Pressed) -> None: + pass + + def action_quit(self) -> None: + """An action to quit the application.""" + self.exit() + + +#################### +# Combat Screen +class CombatScreen(Screen): + BINDINGS = [ + ("escape", "app.pop_screen", "Pop screen"), + ("q", "quit", "Quit"), + ] + + def compose(self) -> ComposeResult: + yield Header(show_clock=True, id="header") + yield Placeholder("Adventure") + yield Footer() + + def on_mount(self) -> None: + pass + + def on_button_pressed(self, event: Button.Pressed) -> None: + pass + + def action_quit(self) -> None: + """An action to quit the application.""" + self.exit() + + +#################### +# Exit Screen +class ExitScreen(Screen): + BINDINGS = [ + ("escape", "app.pop_screen", "Pop screen"), + ("q", "quit", "Quit"), + ] + + def compose(self) -> ComposeResult: + yield Header(show_clock=True, id="header") + yield Placeholder("Goodbye") + yield Footer() + + def on_mount(self) -> None: + pass + + def on_button_pressed(self, event: Button.Pressed) -> None: + pass + + def action_quit(self) -> None: + """An action to quit the application.""" + self.exit() diff --git a/osrgame/osrgame/screen.tcss b/osrgame/osrgame/screen.tcss new file mode 100644 index 0000000..868342a --- /dev/null +++ b/osrgame/osrgame/screen.tcss @@ -0,0 +1,54 @@ +CharacterScreen { + layout: grid; + grid-size: 4; + grid-columns: 1.5fr 1.25fr 1fr 1fr; + grid-rows: 25% 25% 50%; + background: $surface; +} + +#stat-block { + padding: 1; + column-span: 2; +} +#log { + column-span: 2; +} + +#ability-block { + padding: 1; + column-span: 2; +} + +#saving-throw-block { + padding: 1; + column-span: 2; +} + +#item-block { + column-span: 3; +} + +#char-buttons { + border: solid $surface-lighten-3; + layout: vertical; + align: center middle; +} + +.box { + height: 100%; + border: solid lightgray; +} + +.button { + margin: 1; + background: $surface-lighten-1; +} + +.header-footer { + color: $text; + background: $surface-lighten-1; +} +.table { + color: $text; + background: $surface-lighten-1; +} diff --git a/osrgame/osrgame/widgets.py b/osrgame/osrgame/widgets.py new file mode 100644 index 0000000..7e84992 --- /dev/null +++ b/osrgame/osrgame/widgets.py @@ -0,0 +1,138 @@ +from typing import List +from collections import defaultdict +from rich.text import Text +from textual.app import ComposeResult +from textual.containers import Container +from textual.reactive import reactive +from textual.widgets import Button, DataTable, Static + +from osrlib import Item, ItemType +from osrlib.utils import format_modifiers + + +class CharacterScreenButtons(Container): + def compose(self) -> ComposeResult: + yield Button("Roll abilities", id="btn_roll_abilities", classes="button") + yield Button("Roll HP", id="btn_roll_hp", classes="button") + yield Button("Save character", id="btn_save_character", classes="button") + + +class CharacterStats(Container): + """A container for the character stats like name, class, level, HP, and AC.""" + + pc_name = reactive("") + pc_class = reactive("") + pc_level = reactive(0) + pc_hp = reactive(0) + pc_ac = reactive(0) + + BORDER_TITLE = "CHARACTER RECORD SHEET" + + def compose(self) -> ComposeResult: + yield Static(id="name") + yield Static(id="class") + yield Static(id="level") + yield Static(id="hp") + yield Static(id="ac") + + def watch_pc_name(self, pc_name: str) -> None: + """Update the name label when the PC's name changes.""" + self.query_one("#name", Static).update(f"Name: {pc_name}") + + def watch_pc_class(self, pc_class: str) -> None: + """Update the class label when the PC's class changes.""" + self.query_one("#class", Static).update(f"Class: {pc_class}") + + def watch_pc_level(self, pc_level: int) -> None: + """Update the level label when the PC's level changes.""" + self.query_one("#level", Static).update(f"Level: {pc_level}") + + def watch_pc_hp(self, pc_hp: int) -> None: + """Update the HP label when the PC's hit points change.""" + self.query_one("#hp", Static).update(f"HP: {pc_hp}") + + def watch_pc_ac(self, pc_ac: int) -> None: + """Update the AC label when the PC's armor class changes.""" + self.query_one("#ac", Static).update(f"AC: {pc_ac}") + + +class AbilityTable(Container): + def compose(self) -> ComposeResult: + yield DataTable(id="tbl_abilities", cursor_type=None, classes="table") + + def on_mount(self) -> None: + """Perform actions when the widget is mounted.""" + table = self.query_one(DataTable) + score_column = Text("Score", justify="center") + table.add_columns("Ability", score_column, "Modifiers") + + def update_table(self): + pc = self.app.player_character + table = self.query_one(DataTable) + table.clear() + for k, v in pc.abilities.items(): + row_data = [ + k.value, + Text(str(v.score), justify="center"), + format_modifiers(v.modifiers), + ] + table.add_row(*row_data, key=k.name) + + +class SavingThrows(Container): + def compose(self) -> ComposeResult: + yield DataTable(cursor_type=None, classes="table") + + def on_mount(self) -> None: + """Perform actions when the widget is mounted.""" + table = self.query_one(DataTable) + score_column = Text("Score", justify="center") + table.add_columns("Saving Throw", score_column) + + def update_table(self): + pc = self.app.player_character + table = self.query_one(DataTable) + table.clear() + for k, v in pc.character_class.saving_throws.items(): + row_data = [k.value, Text(str(v), justify="center")] + table.add_row(*row_data, key=k.name) + + +class ItemTable(Container): + items = reactive([], always_update=True) + + BORDER_TITLE = "INVENTORY" + + """A container for widgets that render a character's inventory .""" + + def compose(self) -> ComposeResult: + yield DataTable(cursor_type="row", classes="table") + + def on_mount(self) -> None: + """Perform actions when the widget is mounted.""" + table = self.query_one(DataTable) + table.add_columns( + "Name", + "Type", + "Equipped", + "Value (gp)", + ) + self.update_table(self.items) + + def watch_items(self, items: List[Item]) -> None: + """Update the item table when the PC's list changes.""" + self.update_table(items) + + def update_table(self, items: List[Item]) -> None: + table = self.query_one(DataTable) + table.clear() + if not items: + return + for item in items: + row_data = [ + item.name, + item.item_type.name, + Text(str(item.is_equipped)), + Text(str(item.gp_value), justify="center"), + ] + table.add_row(*row_data, key=item.name) diff --git a/osrgame/poetry.lock b/osrgame/poetry.lock new file mode 100644 index 0000000..58f07d1 --- /dev/null +++ b/osrgame/poetry.lock @@ -0,0 +1,1109 @@ +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. + +[[package]] +name = "babel" +version = "2.13.0" +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"}, +] + +[package.extras] +dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] + +[[package]] +name = "black" +version = "23.9.1" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.8" +files = [ + {file = "black-23.9.1-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:d6bc09188020c9ac2555a498949401ab35bb6bf76d4e0f8ee251694664df6301"}, + {file = "black-23.9.1-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:13ef033794029b85dfea8032c9d3b92b42b526f1ff4bf13b2182ce4e917f5100"}, + {file = "black-23.9.1-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:75a2dc41b183d4872d3a500d2b9c9016e67ed95738a3624f4751a0cb4818fe71"}, + {file = "black-23.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13a2e4a93bb8ca74a749b6974925c27219bb3df4d42fc45e948a5d9feb5122b7"}, + {file = "black-23.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:adc3e4442eef57f99b5590b245a328aad19c99552e0bdc7f0b04db6656debd80"}, + {file = "black-23.9.1-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:8431445bf62d2a914b541da7ab3e2b4f3bc052d2ccbf157ebad18ea126efb91f"}, + {file = "black-23.9.1-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:8fc1ddcf83f996247505db6b715294eba56ea9372e107fd54963c7553f2b6dfe"}, + {file = "black-23.9.1-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:7d30ec46de88091e4316b17ae58bbbfc12b2de05e069030f6b747dfc649ad186"}, + {file = "black-23.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031e8c69f3d3b09e1aa471a926a1eeb0b9071f80b17689a655f7885ac9325a6f"}, + {file = "black-23.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:538efb451cd50f43aba394e9ec7ad55a37598faae3348d723b59ea8e91616300"}, + {file = "black-23.9.1-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:638619a559280de0c2aa4d76f504891c9860bb8fa214267358f0a20f27c12948"}, + {file = "black-23.9.1-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:a732b82747235e0542c03bf352c126052c0fbc458d8a239a94701175b17d4855"}, + {file = "black-23.9.1-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:cf3a4d00e4cdb6734b64bf23cd4341421e8953615cba6b3670453737a72ec204"}, + {file = "black-23.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf99f3de8b3273a8317681d8194ea222f10e0133a24a7548c73ce44ea1679377"}, + {file = "black-23.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:14f04c990259576acd093871e7e9b14918eb28f1866f91968ff5524293f9c573"}, + {file = "black-23.9.1-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:c619f063c2d68f19b2d7270f4cf3192cb81c9ec5bc5ba02df91471d0b88c4c5c"}, + {file = "black-23.9.1-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:6a3b50e4b93f43b34a9d3ef00d9b6728b4a722c997c99ab09102fd5efdb88325"}, + {file = "black-23.9.1-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:c46767e8df1b7beefb0899c4a95fb43058fa8500b6db144f4ff3ca38eb2f6393"}, + {file = "black-23.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50254ebfa56aa46a9fdd5d651f9637485068a1adf42270148cd101cdf56e0ad9"}, + {file = "black-23.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:403397c033adbc45c2bd41747da1f7fc7eaa44efbee256b53842470d4ac5a70f"}, + {file = "black-23.9.1-py3-none-any.whl", hash = "sha256:6ccd59584cc834b6d127628713e4b6b968e5f79572da66284532525a042549f9"}, + {file = "black-23.9.1.tar.gz", hash = "sha256:24b6b3ff5c6d9ea08a8888f6977eae858e1f340d7260cf56d70a49823236b62d"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "certifi" +version = "2023.7.22" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, + {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.0" +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"}, +] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "flake8" +version = "6.1.0" +description = "the modular source code checker: pep8 pyflakes and co" +optional = false +python-versions = ">=3.8.1" +files = [ + {file = "flake8-6.1.0-py2.py3-none-any.whl", hash = "sha256:ffdfce58ea94c6580c77888a86506937f9a1a227dfcd15f245d694ae20a6b6e5"}, + {file = "flake8-6.1.0.tar.gz", hash = "sha256:d5b3857f07c030bdb5bf41c7f53799571d75c4491748a3adcd47de929e34cd23"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.11.0,<2.12.0" +pyflakes = ">=3.1.0,<3.2.0" + +[[package]] +name = "flake8-docstrings" +version = "1.7.0" +description = "Extension for flake8 which uses pydocstyle to check docstrings" +optional = false +python-versions = ">=3.7" +files = [ + {file = "flake8_docstrings-1.7.0-py2.py3-none-any.whl", hash = "sha256:51f2344026da083fc084166a9353f5082b01f72901df422f74b4d953ae88ac75"}, + {file = "flake8_docstrings-1.7.0.tar.gz", hash = "sha256:4c8cc748dc16e6869728699e5d0d685da9a10b0ea718e090b1ba088e67a941af"}, +] + +[package.dependencies] +flake8 = ">=3" +pydocstyle = ">=2.1" + +[[package]] +name = "ghp-import" +version = "2.1.0" +description = "Copy your docs directly to the gh-pages branch." +optional = false +python-versions = "*" +files = [ + {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, + {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, +] + +[package.dependencies] +python-dateutil = ">=2.8.1" + +[package.extras] +dev = ["flake8", "markdown", "twine", "wheel"] + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] + +[[package]] +name = "importlib-metadata" +version = "6.8.0" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_metadata-6.8.0-py3-none-any.whl", hash = "sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb"}, + {file = "importlib_metadata-6.8.0.tar.gz", hash = "sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743"}, +] + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +perf = ["ipython"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "jinja2" +version = "3.1.2" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "linkify-it-py" +version = "2.0.2" +description = "Links recognition library with FULL unicode support." +optional = false +python-versions = ">=3.7" +files = [ + {file = "linkify-it-py-2.0.2.tar.gz", hash = "sha256:19f3060727842c254c808e99d465c80c49d2c7306788140987a1a7a29b0d6ad2"}, + {file = "linkify_it_py-2.0.2-py3-none-any.whl", hash = "sha256:a3a24428f6c96f27370d7fe61d2ac0be09017be5190d68d8658233171f1b6541"}, +] + +[package.dependencies] +uc-micro-py = "*" + +[package.extras] +benchmark = ["pytest", "pytest-benchmark"] +dev = ["black", "flake8", "isort", "pre-commit", "pyproject-flake8"] +doc = ["myst-parser", "sphinx", "sphinx-book-theme"] +test = ["coverage", "pytest", "pytest-cov"] + +[[package]] +name = "markdown" +version = "3.5" +description = "Python implementation of John Gruber's Markdown." +optional = false +python-versions = ">=3.8" +files = [ + {file = "Markdown-3.5-py3-none-any.whl", hash = "sha256:4afb124395ce5fc34e6d9886dab977fd9ae987fc6e85689f08278cf0c69d4bf3"}, + {file = "Markdown-3.5.tar.gz", hash = "sha256:a807eb2e4778d9156c8f07876c6e4d50b5494c5665c4834f67b06459dfd877b3"}, +] + +[package.extras] +docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] +testing = ["coverage", "pyyaml"] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.8" +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[package.dependencies] +linkify-it-py = {version = ">=1,<3", optional = true, markers = "extra == \"linkify\""} +mdit-py-plugins = {version = "*", optional = true, markers = "extra == \"plugins\""} +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "markupsafe" +version = "2.1.3" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, + {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = ">=3.6" +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.4.0" +description = "Collection of plugins for markdown-it-py" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mdit_py_plugins-0.4.0-py3-none-any.whl", hash = "sha256:b51b3bb70691f57f974e257e367107857a93b36f322a9e6d44ca5bf28ec2def9"}, + {file = "mdit_py_plugins-0.4.0.tar.gz", hash = "sha256:d8ab27e9aed6c38aa716819fedfde15ca275715955f8a185a8e1cf90fb1d2c1b"}, +] + +[package.dependencies] +markdown-it-py = ">=1.0.0,<4.0.0" + +[package.extras] +code-style = ["pre-commit"] +rtd = ["myst-parser", "sphinx-book-theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +description = "A deep merge function for 🐍." +optional = false +python-versions = ">=3.6" +files = [ + {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, + {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, +] + +[[package]] +name = "mkdocs" +version = "1.5.3" +description = "Project documentation with Markdown." +optional = false +python-versions = ">=3.7" +files = [ + {file = "mkdocs-1.5.3-py3-none-any.whl", hash = "sha256:3b3a78e736b31158d64dbb2f8ba29bd46a379d0c6e324c2246c3bc3d2189cfc1"}, + {file = "mkdocs-1.5.3.tar.gz", hash = "sha256:eb7c99214dcb945313ba30426c2451b735992c73c2e10838f76d09e39ff4d0e2"}, +] + +[package.dependencies] +click = ">=7.0" +colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} +ghp-import = ">=1.0" +jinja2 = ">=2.11.1" +markdown = ">=3.2.1" +markupsafe = ">=2.0.1" +mergedeep = ">=1.3.4" +packaging = ">=20.5" +pathspec = ">=0.11.1" +platformdirs = ">=2.2.0" +pyyaml = ">=5.1" +pyyaml-env-tag = ">=0.1" +watchdog = ">=2.0" + +[package.extras] +i18n = ["babel (>=2.9.0)"] +min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.3)", "jinja2 (==2.11.1)", "markdown (==3.2.1)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "packaging (==20.5)", "pathspec (==0.11.1)", "platformdirs (==2.2.0)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "typing-extensions (==3.10)", "watchdog (==2.0)"] + +[[package]] +name = "mkdocs-material" +version = "9.4.6" +description = "Documentation that simply works" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_material-9.4.6-py3-none-any.whl", hash = "sha256:78802035d5768a78139c84ad7dce0c6493e8f7dc4861727d36ed91d1520a54da"}, + {file = "mkdocs_material-9.4.6.tar.gz", hash = "sha256:09665e60df7ee9e5ff3a54af173f6d45be718b1ee7dd962bcff3102b81fb0c14"}, +] + +[package.dependencies] +babel = ">=2.10,<3.0" +colorama = ">=0.4,<1.0" +jinja2 = ">=3.0,<4.0" +markdown = ">=3.2,<4.0" +mkdocs = ">=1.5.3,<2.0" +mkdocs-material-extensions = ">=1.2,<2.0" +paginate = ">=0.5,<1.0" +pygments = ">=2.16,<3.0" +pymdown-extensions = ">=10.2,<11.0" +regex = ">=2022.4" +requests = ">=2.26,<3.0" + +[package.extras] +git = ["mkdocs-git-committers-plugin-2 (>=1.1,<2.0)", "mkdocs-git-revision-date-localized-plugin (>=1.2,<2.0)"] +imaging = ["cairosvg (>=2.6,<3.0)", "pillow (>=9.4,<10.0)"] +recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2.0)", "mkdocs-rss-plugin (>=1.6,<2.0)"] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.2" +description = "Extension pack for Python Markdown and MkDocs Material." +optional = false +python-versions = ">=3.7" +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"}, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "osrlib" +version = "0.1.0" +description = "Turn-based dungeon-crawler game engine for OSR-style RPGs." +optional = false +python-versions = "^3.11" +files = [] +develop = false + +[package.dependencies] +tinydb = "^4.8.0" + +[package.source] +type = "directory" +url = "../osrlib" + +[[package]] +name = "packaging" +version = "23.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + +[[package]] +name = "paginate" +version = "0.5.6" +description = "Divides large result sets into pages for easier browsing" +optional = false +python-versions = "*" +files = [ + {file = "paginate-0.5.6.tar.gz", hash = "sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d"}, +] + +[[package]] +name = "pathspec" +version = "0.11.2" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, + {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, +] + +[[package]] +name = "platformdirs" +version = "3.11.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.7" +files = [ + {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"}, + {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"}, +] + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] + +[[package]] +name = "pluggy" +version = "1.3.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, + {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pycodestyle" +version = "2.11.1" +description = "Python style guide checker" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, + {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, +] + +[[package]] +name = "pydocstyle" +version = "6.3.0" +description = "Python docstring style checker" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pydocstyle-6.3.0-py3-none-any.whl", hash = "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019"}, + {file = "pydocstyle-6.3.0.tar.gz", hash = "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1"}, +] + +[package.dependencies] +snowballstemmer = ">=2.2.0" + +[package.extras] +toml = ["tomli (>=1.2.3)"] + +[[package]] +name = "pyflakes" +version = "3.1.0" +description = "passive checker of Python programs" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyflakes-3.1.0-py2.py3-none-any.whl", hash = "sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774"}, + {file = "pyflakes-3.1.0.tar.gz", hash = "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc"}, +] + +[[package]] +name = "pygments" +version = "2.16.1" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"}, + {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"}, +] + +[package.extras] +plugins = ["importlib-metadata"] + +[[package]] +name = "pymdown-extensions" +version = "10.3" +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"}, +] + +[package.dependencies] +markdown = ">=3.2" +pyyaml = "*" + +[package.extras] +extra = ["pygments (>=2.12)"] + +[[package]] +name = "pytest" +version = "7.4.2" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"}, + {file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "pyyaml-env-tag" +version = "0.1" +description = "A custom YAML tag for referencing environment variables in YAML files. " +optional = false +python-versions = ">=3.6" +files = [ + {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, + {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, +] + +[package.dependencies] +pyyaml = "*" + +[[package]] +name = "regex" +version = "2023.10.3" +description = "Alternative regular expression module, to replace re." +optional = false +python-versions = ">=3.7" +files = [ + {file = "regex-2023.10.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4c34d4f73ea738223a094d8e0ffd6d2c1a1b4c175da34d6b0de3d8d69bee6bcc"}, + {file = "regex-2023.10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a8f4e49fc3ce020f65411432183e6775f24e02dff617281094ba6ab079ef0915"}, + {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cd1bccf99d3ef1ab6ba835308ad85be040e6a11b0977ef7ea8c8005f01a3c29"}, + {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:81dce2ddc9f6e8f543d94b05d56e70d03a0774d32f6cca53e978dc01e4fc75b8"}, + {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c6b4d23c04831e3ab61717a707a5d763b300213db49ca680edf8bf13ab5d91b"}, + {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c15ad0aee158a15e17e0495e1e18741573d04eb6da06d8b84af726cfc1ed02ee"}, + {file = "regex-2023.10.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6239d4e2e0b52c8bd38c51b760cd870069f0bdf99700a62cd509d7a031749a55"}, + {file = "regex-2023.10.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4a8bf76e3182797c6b1afa5b822d1d5802ff30284abe4599e1247be4fd6b03be"}, + {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9c727bbcf0065cbb20f39d2b4f932f8fa1631c3e01fcedc979bd4f51fe051c5"}, + {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3ccf2716add72f80714b9a63899b67fa711b654be3fcdd34fa391d2d274ce767"}, + {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:107ac60d1bfdc3edb53be75e2a52aff7481b92817cfdddd9b4519ccf0e54a6ff"}, + {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:00ba3c9818e33f1fa974693fb55d24cdc8ebafcb2e4207680669d8f8d7cca79a"}, + {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f0a47efb1dbef13af9c9a54a94a0b814902e547b7f21acb29434504d18f36e3a"}, + {file = "regex-2023.10.3-cp310-cp310-win32.whl", hash = "sha256:36362386b813fa6c9146da6149a001b7bd063dabc4d49522a1f7aa65b725c7ec"}, + {file = "regex-2023.10.3-cp310-cp310-win_amd64.whl", hash = "sha256:c65a3b5330b54103e7d21cac3f6bf3900d46f6d50138d73343d9e5b2900b2353"}, + {file = "regex-2023.10.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:90a79bce019c442604662d17bf69df99090e24cdc6ad95b18b6725c2988a490e"}, + {file = "regex-2023.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c7964c2183c3e6cce3f497e3a9f49d182e969f2dc3aeeadfa18945ff7bdd7051"}, + {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ef80829117a8061f974b2fda8ec799717242353bff55f8a29411794d635d964"}, + {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5addc9d0209a9afca5fc070f93b726bf7003bd63a427f65ef797a931782e7edc"}, + {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c148bec483cc4b421562b4bcedb8e28a3b84fcc8f0aa4418e10898f3c2c0eb9b"}, + {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d1f21af4c1539051049796a0f50aa342f9a27cde57318f2fc41ed50b0dbc4ac"}, + {file = "regex-2023.10.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b9ac09853b2a3e0d0082104036579809679e7715671cfbf89d83c1cb2a30f58"}, + {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ebedc192abbc7fd13c5ee800e83a6df252bec691eb2c4bedc9f8b2e2903f5e2a"}, + {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d8a993c0a0ffd5f2d3bda23d0cd75e7086736f8f8268de8a82fbc4bd0ac6791e"}, + {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:be6b7b8d42d3090b6c80793524fa66c57ad7ee3fe9722b258aec6d0672543fd0"}, + {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4023e2efc35a30e66e938de5aef42b520c20e7eda7bb5fb12c35e5d09a4c43f6"}, + {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0d47840dc05e0ba04fe2e26f15126de7c755496d5a8aae4a08bda4dd8d646c54"}, + {file = "regex-2023.10.3-cp311-cp311-win32.whl", hash = "sha256:9145f092b5d1977ec8c0ab46e7b3381b2fd069957b9862a43bd383e5c01d18c2"}, + {file = "regex-2023.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:b6104f9a46bd8743e4f738afef69b153c4b8b592d35ae46db07fc28ae3d5fb7c"}, + {file = "regex-2023.10.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:bff507ae210371d4b1fe316d03433ac099f184d570a1a611e541923f78f05037"}, + {file = "regex-2023.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:be5e22bbb67924dea15039c3282fa4cc6cdfbe0cbbd1c0515f9223186fc2ec5f"}, + {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a992f702c9be9c72fa46f01ca6e18d131906a7180950958f766c2aa294d4b41"}, + {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7434a61b158be563c1362d9071358f8ab91b8d928728cd2882af060481244c9e"}, + {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2169b2dcabf4e608416f7f9468737583ce5f0a6e8677c4efbf795ce81109d7c"}, + {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9e908ef5889cda4de038892b9accc36d33d72fb3e12c747e2799a0e806ec841"}, + {file = "regex-2023.10.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12bd4bc2c632742c7ce20db48e0d99afdc05e03f0b4c1af90542e05b809a03d9"}, + {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bc72c231f5449d86d6c7d9cc7cd819b6eb30134bb770b8cfdc0765e48ef9c420"}, + {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bce8814b076f0ce5766dc87d5a056b0e9437b8e0cd351b9a6c4e1134a7dfbda9"}, + {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:ba7cd6dc4d585ea544c1412019921570ebd8a597fabf475acc4528210d7c4a6f"}, + {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b0c7d2f698e83f15228ba41c135501cfe7d5740181d5903e250e47f617eb4292"}, + {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5a8f91c64f390ecee09ff793319f30a0f32492e99f5dc1c72bc361f23ccd0a9a"}, + {file = "regex-2023.10.3-cp312-cp312-win32.whl", hash = "sha256:ad08a69728ff3c79866d729b095872afe1e0557251da4abb2c5faff15a91d19a"}, + {file = "regex-2023.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:39cdf8d141d6d44e8d5a12a8569d5a227f645c87df4f92179bd06e2e2705e76b"}, + {file = "regex-2023.10.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4a3ee019a9befe84fa3e917a2dd378807e423d013377a884c1970a3c2792d293"}, + {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76066d7ff61ba6bf3cb5efe2428fc82aac91802844c022d849a1f0f53820502d"}, + {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe50b61bab1b1ec260fa7cd91106fa9fece57e6beba05630afe27c71259c59b"}, + {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fd88f373cb71e6b59b7fa597e47e518282455c2734fd4306a05ca219a1991b0"}, + {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3ab05a182c7937fb374f7e946f04fb23a0c0699c0450e9fb02ef567412d2fa3"}, + {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dac37cf08fcf2094159922edc7a2784cfcc5c70f8354469f79ed085f0328ebdf"}, + {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e54ddd0bb8fb626aa1f9ba7b36629564544954fff9669b15da3610c22b9a0991"}, + {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3367007ad1951fde612bf65b0dffc8fd681a4ab98ac86957d16491400d661302"}, + {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:16f8740eb6dbacc7113e3097b0a36065a02e37b47c936b551805d40340fb9971"}, + {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:f4f2ca6df64cbdd27f27b34f35adb640b5d2d77264228554e68deda54456eb11"}, + {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:39807cbcbe406efca2a233884e169d056c35aa7e9f343d4e78665246a332f597"}, + {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7eece6fbd3eae4a92d7c748ae825cbc1ee41a89bb1c3db05b5578ed3cfcfd7cb"}, + {file = "regex-2023.10.3-cp37-cp37m-win32.whl", hash = "sha256:ce615c92d90df8373d9e13acddd154152645c0dc060871abf6bd43809673d20a"}, + {file = "regex-2023.10.3-cp37-cp37m-win_amd64.whl", hash = "sha256:0f649fa32fe734c4abdfd4edbb8381c74abf5f34bc0b3271ce687b23729299ed"}, + {file = "regex-2023.10.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9b98b7681a9437262947f41c7fac567c7e1f6eddd94b0483596d320092004533"}, + {file = "regex-2023.10.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:91dc1d531f80c862441d7b66c4505cd6ea9d312f01fb2f4654f40c6fdf5cc37a"}, + {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82fcc1f1cc3ff1ab8a57ba619b149b907072e750815c5ba63e7aa2e1163384a4"}, + {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7979b834ec7a33aafae34a90aad9f914c41fd6eaa8474e66953f3f6f7cbd4368"}, + {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef71561f82a89af6cfcbee47f0fabfdb6e63788a9258e913955d89fdd96902ab"}, + {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd829712de97753367153ed84f2de752b86cd1f7a88b55a3a775eb52eafe8a94"}, + {file = "regex-2023.10.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00e871d83a45eee2f8688d7e6849609c2ca2a04a6d48fba3dff4deef35d14f07"}, + {file = "regex-2023.10.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:706e7b739fdd17cb89e1fbf712d9dc21311fc2333f6d435eac2d4ee81985098c"}, + {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cc3f1c053b73f20c7ad88b0d1d23be7e7b3901229ce89f5000a8399746a6e039"}, + {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6f85739e80d13644b981a88f529d79c5bdf646b460ba190bffcaf6d57b2a9863"}, + {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:741ba2f511cc9626b7561a440f87d658aabb3d6b744a86a3c025f866b4d19e7f"}, + {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e77c90ab5997e85901da85131fd36acd0ed2221368199b65f0d11bca44549711"}, + {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:979c24cbefaf2420c4e377ecd1f165ea08cc3d1fbb44bdc51bccbbf7c66a2cb4"}, + {file = "regex-2023.10.3-cp38-cp38-win32.whl", hash = "sha256:58837f9d221744d4c92d2cf7201c6acd19623b50c643b56992cbd2b745485d3d"}, + {file = "regex-2023.10.3-cp38-cp38-win_amd64.whl", hash = "sha256:c55853684fe08d4897c37dfc5faeff70607a5f1806c8be148f1695be4a63414b"}, + {file = "regex-2023.10.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2c54e23836650bdf2c18222c87f6f840d4943944146ca479858404fedeb9f9af"}, + {file = "regex-2023.10.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:69c0771ca5653c7d4b65203cbfc5e66db9375f1078689459fe196fe08b7b4930"}, + {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ac965a998e1388e6ff2e9781f499ad1eaa41e962a40d11c7823c9952c77123e"}, + {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c0e8fae5b27caa34177bdfa5a960c46ff2f78ee2d45c6db15ae3f64ecadde14"}, + {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6c56c3d47da04f921b73ff9415fbaa939f684d47293f071aa9cbb13c94afc17d"}, + {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ef1e014eed78ab650bef9a6a9cbe50b052c0aebe553fb2881e0453717573f52"}, + {file = "regex-2023.10.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d29338556a59423d9ff7b6eb0cb89ead2b0875e08fe522f3e068b955c3e7b59b"}, + {file = "regex-2023.10.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9c6d0ced3c06d0f183b73d3c5920727268d2201aa0fe6d55c60d68c792ff3588"}, + {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:994645a46c6a740ee8ce8df7911d4aee458d9b1bc5639bc968226763d07f00fa"}, + {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:66e2fe786ef28da2b28e222c89502b2af984858091675044d93cb50e6f46d7af"}, + {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:11175910f62b2b8c055f2b089e0fedd694fe2be3941b3e2633653bc51064c528"}, + {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:06e9abc0e4c9ab4779c74ad99c3fc10d3967d03114449acc2c2762ad4472b8ca"}, + {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fb02e4257376ae25c6dd95a5aec377f9b18c09be6ebdefa7ad209b9137b73d48"}, + {file = "regex-2023.10.3-cp39-cp39-win32.whl", hash = "sha256:3b2c3502603fab52d7619b882c25a6850b766ebd1b18de3df23b2f939360e1bd"}, + {file = "regex-2023.10.3-cp39-cp39-win_amd64.whl", hash = "sha256:adbccd17dcaff65704c856bd29951c58a1bd4b2b0f8ad6b826dbd543fe740988"}, + {file = "regex-2023.10.3.tar.gz", hash = "sha256:3fef4f844d2290ee0ba57addcec17eec9e3df73f10a2748485dfd6a3a188cc0f"}, +] + +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "rich" +version = "13.6.0" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "rich-13.6.0-py3-none-any.whl", hash = "sha256:2b38e2fe9ca72c9a00170a1a2d20c63c790d0e10ef1fe35eba76e1e7b1d7d245"}, + {file = "rich-13.6.0.tar.gz", hash = "sha256:5c14d22737e6d5084ef4771b62d5d4363165b403455a30a1c8ca39dc7b644bef"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "snowballstemmer" +version = "2.2.0" +description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +optional = false +python-versions = "*" +files = [ + {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, + {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, +] + +[[package]] +name = "textual" +version = "0.40.0" +description = "Modern Text User Interface framework" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "textual-0.40.0-py3-none-any.whl", hash = "sha256:3e98f0c9c9a9361d3077c00e3fc5a708f927dd1ce45a1149eb1ba6945ce9d71c"}, + {file = "textual-0.40.0.tar.gz", hash = "sha256:0fd014f9fab7f6d88167c82f90e115b118b3016b8597281d14c9257967f7812e"}, +] + +[package.dependencies] +importlib-metadata = ">=4.11.3" +markdown-it-py = {version = ">=2.1.0", extras = ["linkify", "plugins"]} +rich = ">=13.3.3" +typing-extensions = ">=4.4.0,<5.0.0" + +[package.extras] +syntax = ["tree-sitter (>=0.20.1,<0.21.0)", "tree_sitter_languages (>=1.7.0)"] + +[[package]] +name = "tinydb" +version = "4.8.0" +description = "TinyDB is a tiny, document oriented database optimized for your happiness :)" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "tinydb-4.8.0-py3-none-any.whl", hash = "sha256:30c06d12383d7c332e404ca6a6103fb2b32cbf25712689648c39d9a6bd34bd3d"}, + {file = "tinydb-4.8.0.tar.gz", hash = "sha256:6dd686a9c5a75dfa9280088fd79a419aefe19cd7f4bd85eba203540ef856d564"}, +] + +[[package]] +name = "typing-extensions" +version = "4.8.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, + {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, +] + +[[package]] +name = "uc-micro-py" +version = "1.0.2" +description = "Micro subset of unicode data files for linkify-it-py projects." +optional = false +python-versions = ">=3.7" +files = [ + {file = "uc-micro-py-1.0.2.tar.gz", hash = "sha256:30ae2ac9c49f39ac6dce743bd187fcd2b574b16ca095fa74cd9396795c954c54"}, + {file = "uc_micro_py-1.0.2-py3-none-any.whl", hash = "sha256:8c9110c309db9d9e87302e2f4ad2c3152770930d88ab385cd544e7a7e75f3de0"}, +] + +[package.extras] +test = ["coverage", "pytest", "pytest-cov"] + +[[package]] +name = "urllib3" +version = "2.0.6" +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"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "watchdog" +version = "3.0.0" +description = "Filesystem events monitoring" +optional = false +python-versions = ">=3.7" +files = [ + {file = "watchdog-3.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:336adfc6f5cc4e037d52db31194f7581ff744b67382eb6021c868322e32eef41"}, + {file = "watchdog-3.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a70a8dcde91be523c35b2bf96196edc5730edb347e374c7de7cd20c43ed95397"}, + {file = "watchdog-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:adfdeab2da79ea2f76f87eb42a3ab1966a5313e5a69a0213a3cc06ef692b0e96"}, + {file = "watchdog-3.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2b57a1e730af3156d13b7fdddfc23dea6487fceca29fc75c5a868beed29177ae"}, + {file = "watchdog-3.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7ade88d0d778b1b222adebcc0927428f883db07017618a5e684fd03b83342bd9"}, + {file = "watchdog-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7e447d172af52ad204d19982739aa2346245cc5ba6f579d16dac4bfec226d2e7"}, + {file = "watchdog-3.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9fac43a7466eb73e64a9940ac9ed6369baa39b3bf221ae23493a9ec4d0022674"}, + {file = "watchdog-3.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8ae9cda41fa114e28faf86cb137d751a17ffd0316d1c34ccf2235e8a84365c7f"}, + {file = "watchdog-3.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:25f70b4aa53bd743729c7475d7ec41093a580528b100e9a8c5b5efe8899592fc"}, + {file = "watchdog-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4f94069eb16657d2c6faada4624c39464f65c05606af50bb7902e036e3219be3"}, + {file = "watchdog-3.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7c5f84b5194c24dd573fa6472685b2a27cc5a17fe5f7b6fd40345378ca6812e3"}, + {file = "watchdog-3.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3aa7f6a12e831ddfe78cdd4f8996af9cf334fd6346531b16cec61c3b3c0d8da0"}, + {file = "watchdog-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:233b5817932685d39a7896b1090353fc8efc1ef99c9c054e46c8002561252fb8"}, + {file = "watchdog-3.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:13bbbb462ee42ec3c5723e1205be8ced776f05b100e4737518c67c8325cf6100"}, + {file = "watchdog-3.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8f3ceecd20d71067c7fd4c9e832d4e22584318983cabc013dbf3f70ea95de346"}, + {file = "watchdog-3.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c9d8c8ec7efb887333cf71e328e39cffbf771d8f8f95d308ea4125bf5f90ba64"}, + {file = "watchdog-3.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0e06ab8858a76e1219e68c7573dfeba9dd1c0219476c5a44d5333b01d7e1743a"}, + {file = "watchdog-3.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:d00e6be486affb5781468457b21a6cbe848c33ef43f9ea4a73b4882e5f188a44"}, + {file = "watchdog-3.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:c07253088265c363d1ddf4b3cdb808d59a0468ecd017770ed716991620b8f77a"}, + {file = "watchdog-3.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:5113334cf8cf0ac8cd45e1f8309a603291b614191c9add34d33075727a967709"}, + {file = "watchdog-3.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:51f90f73b4697bac9c9a78394c3acbbd331ccd3655c11be1a15ae6fe289a8c83"}, + {file = "watchdog-3.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:ba07e92756c97e3aca0912b5cbc4e5ad802f4557212788e72a72a47ff376950d"}, + {file = "watchdog-3.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:d429c2430c93b7903914e4db9a966c7f2b068dd2ebdd2fa9b9ce094c7d459f33"}, + {file = "watchdog-3.0.0-py3-none-win32.whl", hash = "sha256:3ed7c71a9dccfe838c2f0b6314ed0d9b22e77d268c67e015450a29036a81f60f"}, + {file = "watchdog-3.0.0-py3-none-win_amd64.whl", hash = "sha256:4c9956d27be0bb08fc5f30d9d0179a855436e655f046d288e2bcc11adfae893c"}, + {file = "watchdog-3.0.0-py3-none-win_ia64.whl", hash = "sha256:5d9f3a10e02d7371cd929b5d8f11e87d4bad890212ed3901f9b4d68767bee759"}, + {file = "watchdog-3.0.0.tar.gz", hash = "sha256:4d98a320595da7a7c5a18fc48cb633c2e73cda78f93cac2ef42d42bf609a33f9"}, +] + +[package.extras] +watchmedo = ["PyYAML (>=3.10)"] + +[[package]] +name = "zipp" +version = "3.17.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, + {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.11" +content-hash = "6a2eed767b66712024291148562ba2b23f5986bc34b2dbc6000668bb43f43cc7" diff --git a/osrgame/pyproject.toml b/osrgame/pyproject.toml new file mode 100644 index 0000000..1aa4f7d --- /dev/null +++ b/osrgame/pyproject.toml @@ -0,0 +1,23 @@ +[tool.poetry] +name = "osrgame" +version = "0.1.0" +description = "Text user interface (TUI) for OSR Console, a turn-based dungeon crawler RPG in the old-school renaissance style." +authors = ["Marsh Macy "] +license = "MIT" +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.11" +textual = "^0.40.0" +osrlib = { path = "../osrlib" } + +[tool.poetry.group.dev.dependencies] +black = "^23.9.1" +flake8 = "^6.1.0" +flake8-docstrings = "^1.7.0" +pytest = "^7.4.2" +mkdocs-material = "^9.4.6" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/osrlib/README.md b/osrlib/README.md new file mode 100644 index 0000000..60fce69 --- /dev/null +++ b/osrlib/README.md @@ -0,0 +1,131 @@ +# osrlib: Python library for OSR-style turn-based RPGs + +The `osrlib` package is the main rules and game engine library for OSR Console. + +If you want to play the game, learn how to install and start the app in [osrgame: Textual TUI for an OSR-style turn-based RPG](../osrgame/README.md). + +If you want contribute code to the `osrlib` package itself, read on to learn how to build and test the library. + +## Prerequisites + +- Python 3.11+ +- Poetry 1.6+ + +## Installation + +Use Poetry to create a Python virtual environment and install `osrlib` and its dependencies: + +```sh +# Create osrlib project venv and install its deps with Poetry +poetry install +``` + +## Build the package + +You can also build the `osrlib` package by using Poetry: + +```sh +# Build the 'osrlib' Python package +poetry build +``` + +## Test the library + +Install the unit and integration test dependencies with Poetry: + +```sh +# Move into the tests directory (assumes you're in /osrlib) +cd ../tests + +# Create tests venv and install test deps with Poetry +poetry install +``` + +### Run all the tests + +To run all unit and integration tests, run `pytest` with `poetry run`, passing only the test directory path as the only argument: + +```sh +# Run ALL unit and integration tests in /tests/*.py +poetry run pytest ../tests +``` + +You should see a bunch of test output ending in a summary of the test results. Here's an abridge example of test output: + +```console +$ poetry run pytest ../tests +========================================================================= test session starts ========================================================================== +platform darwin -- Python 3.11.6, pytest-7.4.2, pluggy-1.3.0 +rootdir: /Users/username/repos/osr-console/tests +configfile: pytest.ini +collecting ... +------------------------------------------------------------------------- live log collection -------------------------------------------------------------------------- +2023-10-15 19:10:19 [INFO] Initializing the GameManager... +2023-10-15 19:10:19 [INFO] GameManager initialized. There are 0 adventures available. +2023-10-15 19:10:19 [INFO] Testing dice_roller.py +collected 196 items + +../tests/test_integration_saveload.py::test_abilities_saveload +---------------------------------------------------------------------------- live log setup ---------------------------------------------------------------------------- +2023-10-15 19:10:19 [INFO] Creating TinyDB @ /Users/username/repos/osr-console/osrlib/test_db.json +2023-10-15 19:10:19 [INFO] Created TinyDB: +PASSED [ 0%] +-------------------------------------------------------------------------- live log teardown --------------------------------------------------------------------------- +2023-10-15 19:10:19 [INFO] Dropping tables from TinyDB: +2023-10-15 19:10:19 [INFO] Closing TinyDB: + +../tests/test_integration_saveload.py::test_item_saveload + +## MANY MORE LINES OF TEST OUTPUT OMITTED ## + +../tests/test_unit_party.py::test_clear_party +---------------------------------------------------------------------------- live log call ----------------------------------------------------------------------------- +2023-10-15 19:10:19 [INFO] Adding 'Sckricko' to party 'The B-Team'... +2023-10-15 19:10:19 [INFO] Setting 'Sckricko' as the active character in party 'The B-Team'... +2023-10-15 19:10:19 [INFO] Set 'Sckricko' as the active character in the party. +2023-10-15 19:10:19 [INFO] Adding 'Mazpar' to party 'The B-Team'... +2023-10-15 19:10:19 [INFO] Setting 'Mazpar' as the active character in party 'The B-Team'... +2023-10-15 19:10:19 [INFO] Set 'Mazpar' as the active character in the party. +2023-10-15 19:10:19 [INFO] Adding 'Slick' to party 'The B-Team'... +2023-10-15 19:10:19 [INFO] Setting 'Slick' as the active character in party 'The B-Team'... +2023-10-15 19:10:19 [INFO] Set 'Slick' as the active character in the party. +PASSED [ 99%] +../tests/test_unit_utils.py::test_format_modifiers +---------------------------------------------------------------------------- live log call ----------------------------------------------------------------------------- +2023-10-15 19:10:19 [INFO] Testing empty modifiers: '' +2023-10-15 19:10:19 [INFO] Testing single modifier with positive value: 'To hit: +1' +2023-10-15 19:10:19 [INFO] Testing single modifier with negative value: 'Damage: -1' +2023-10-15 19:10:19 [INFO] Testing single modifier with no (zero) value: 'Open doors: 0' +2023-10-15 19:10:19 [INFO] Testing multiple modifiers with positive and negative values: 'To hit: +1, Damage: -1, Open doors: 0' +PASSED [100%] + +========================================================================= 196 passed in 0.13s ========================================================================== +``` + +### Run specific tests + +You can pass any argument you normally would to `pytest` when you run it with Poetry. For example, to run only the `test_remove_character_not_in_party` test, pass the `-k` filtering option followed by the test name: + +```console +$ poetry run pytest ../tests -k "test_remove_character" +======================================================= test session starts ======================================================== +platform darwin -- Python 3.11.6, pytest-7.4.2, pluggy-1.3.0 +rootdir: /Users/username/repos/osr-console/tests +configfile: pytest.ini +collecting ... +------------------------------------------------------- live log collection -------------------------------------------------------- +2023-10-15 19:17:14 [INFO] Initializing the GameManager... +2023-10-15 19:17:14 [INFO] GameManager initialized. There are 0 adventures available. +2023-10-15 19:17:14 [INFO] Testing dice_roller.py +collected 196 items / 194 deselected / 2 selected + +../tests/test_unit_party.py::test_remove_character +---------------------------------------------------------- live log call ----------------------------------------------------------- +2023-10-15 19:17:14 [INFO] Adding 'Sckricko' to party 'The B-Team'... +2023-10-15 19:17:14 [INFO] Setting 'Sckricko' as the active character in party 'The B-Team'... +2023-10-15 19:17:14 [INFO] Set 'Sckricko' as the active character in the party. +PASSED [ 50%] +../tests/test_unit_party.py::test_remove_character_not_in_party PASSED [100%] + +================================================ 2 passed, 194 deselected in 0.03s ================================================= +``` diff --git a/osrlib/osrlib/__init__.py b/osrlib/osrlib/__init__.py new file mode 100644 index 0000000..82f88a6 --- /dev/null +++ b/osrlib/osrlib/__init__.py @@ -0,0 +1,73 @@ +from .ability import ( + AbilityType, + Ability, + Strength, + Dexterity, + Constitution, + Intelligence, + Wisdom, + Charisma, +) +from .adventure import ( + AdventureAlreadyStartedError, + Adventure, + Dungeon, + Encounter, + Monster, + Quest, + QuestPiece, +) +from .character_classes import ( + CharacterClass, + CharacterClassType, + ClassLevel, + cleric_levels, + commoner_levels, + dwarf_levels, + elf_levels, + fighter_levels, + halfling_levels, + magic_user_levels, + saving_throws, + thief_levels, + class_levels, + saving_throws, + all_character_classes, +) +from .combat import ( + AttackType, + ModifierType, +) +from .dice_roller import ( + DiceRoll, + roll_dice, +) +from .game_manager import ( + GameManager, + StorageType, +) +from .inventory import Inventory +from .item import ( + Item, + ItemType, + Armor, + Weapon, + Spell, + ItemAlreadyHasOwnerError, + ItemAlreadyInInventoryError, + ItemAlreadyInQuestError, + ItemEquippedError, + ItemNotEquippedError, + ItemNotInInventoryError, + ItemNotUsableError, +) +from .party import ( + Party, + PartyAtCapacityError, + PartyInStartedAdventureError, + CharacterNotInPartyError, + CharacterAlreadyInPartyError, + get_default_party, +) +from .player_character import PlayerCharacter +from .utils import format_modifiers diff --git a/osrlib/osrlib/ability.py b/osrlib/osrlib/ability.py new file mode 100644 index 0000000..54a57b6 --- /dev/null +++ b/osrlib/osrlib/ability.py @@ -0,0 +1,332 @@ +"""Defines player character abilities.""" + +from abc import ABC, abstractmethod +from enum import Enum + +from osrlib.character_classes import CharacterClassType +from osrlib.combat import ModifierType + + +class AbilityType(Enum): + """Enum representing the types of abilities.""" + + STRENGTH = "Strength" + INTELLIGENCE = "Intelligence" + WISDOM = "Wisdom" + DEXTERITY = "Dexterity" + CONSTITUTION = "Constitution" + CHARISMA = "Charisma" + + +class Ability(ABC): + """Abstract base class for character abilities. + + Abilities are inherent traits that every character possesses in varying degrees. + They provide different kinds of modifiers which can affect different game actions. + + Attributes: + score (int): The raw ability score. + modifiers (dict): A mapping of the ability's modifier types to modifier values based on the ability score. + """ + + _MODIFIER_MAP = { + 3: -3, + 4: -2, + 5: -2, + 6: -1, + 7: -1, + 8: -1, + 13: 1, + 14: 1, + 15: 1, + 16: 2, + 17: 2, + 18: 3, + } + + def __init__(self, score: int): + """Initialize Ability with a score. + + Args: + score (int): The raw ability score. + """ + self.ability_type = None + self.score = score + self.modifiers = {} + self.init_modifiers() + self.prime_requisite_classes = [] + + def __str__(self): + """Return string representation of the ability.""" + modifiers_str = ", ".join(f"{mod.value}: {'+' if val > 0 else ''}{val}" for mod, val in self.modifiers.items()) + return f"{self.ability_type.value}: {self.score} - {modifiers_str}" + + def get_earned_xp_adjustment(self): + """Get the experience adjustment for the ability score.""" + if self.score >= 16: + return 0.10 + elif self.score >= 13: + return 0.05 + elif self.score >= 9: + return 0.0 + elif self.score >= 6: + return -0.10 + else: + return -0.20 + + @abstractmethod + def init_modifiers(self): + """Abstract method to initialize ability modifiers.""" + pass + + def _get_modifier(self) -> int: + return self._MODIFIER_MAP.get(self.score, 0) + + def to_dict(self) -> dict: + return { + "ability_type": self.ability_type.name, # Enum converted to string + "score": self.score, + } + + @classmethod + def from_dict(cls, data: dict) -> "Ability": + ability = cls(score=data["score"]) + # Assumes modifiers can be recomputed from score + return ability + + +class Strength(Ability): + """Represents the Strength ability for characters. + + Strength measures muscle power and the ability to use that power. + It primarily influences hand-to-hand combat and opening doors. + + Modifiers: + TO_HIT: Modifier to hand-to-hand attack rolls. + DAMAGE: Modifier to damage in hand-to-hand combat. + OPEN_DOORS: Modifier to chances of opening stuck doors. + """ + + def __init__(self, score: int): + """Initialize a new Strength instance. + + Args: + score (int): The raw ability score. + """ + super().__init__(score) + self.ability_type = AbilityType.STRENGTH + self.prime_requisite_classes = [ + CharacterClassType.FIGHTER, + CharacterClassType.DWARF, + CharacterClassType.ELF, + CharacterClassType.HALFLING, + ] + + def init_modifiers(self): + """Initialize the Strength modifiers. + + Modifiers: + TO_HIT: Modifier to hand-to-hand attack rolls. + DAMAGE: Modifier to damage in hand-to-hand combat. + OPEN_DOORS: Modifier to chances of opening stuck doors. + """ + self.modifiers[ModifierType.TO_HIT] = self._get_modifier() + self.modifiers[ModifierType.DAMAGE] = self._get_modifier() + self.modifiers[ModifierType.OPEN_DOORS] = self._get_modifier() + + +class Intelligence(Ability): + """Represents the Intelligence ability for characters. + + Intelligence is a measure of problem-solving ability, linguistic capability, and magical aptitude. + + Modifiers: + LANGUAGES: Modifier to the number of additional languages the character can learn. + """ + + def __init__(self, score: int): + """Initialize a new Intelligence instance. + + Args: + score (int): The raw ability score. + """ + super().__init__(score) + self.ability_type = AbilityType.INTELLIGENCE + self.prime_requisite_classes = [ + CharacterClassType.MAGIC_USER, + CharacterClassType.ELF, + ] + + # Overriding because we can't use the standard map for languages + _MODIFIER_MAP = { + 13: 1, + 14: 1, + 15: 1, + 16: 2, + 17: 2, + 18: 3, + } + + def init_modifiers(self): + """Initialize the Intelligence modifiers. + + Modifiers: + LANGUAGES: Modifier to the number of additional languages the character can learn. + """ + self.modifiers[ModifierType.LANGUAGES] = self._get_modifier() + + +class Wisdom(Ability): + """Represents the Wisdom ability for characters. + + Wisdom measures a character's common sense, intuition, and willpower. + + Modifiers: + SAVING_THROWS: Modifier to saving throws against spells and magical effects. + """ + + def __init__(self, score: int): + """Initialize a new Wisdom instance. + + Args: + score (int): The raw ability score. + """ + super().__init__(score) + self.ability_type = AbilityType.WISDOM + self.prime_requisite_classes = [CharacterClassType.CLERIC] + + def init_modifiers(self): + """Initialize the Wisdom modifiers. + + Modifiers: + SAVING_THROWS: Modifier to saving throws against spells and magical effects. + """ + self.modifiers[ModifierType.SAVING_THROWS] = self._get_modifier() + + +class Dexterity(Ability): + """Represents the Dexterity ability for characters. + + Dexterity measures agility, reflexes, and coordination. + + Modifiers: + TO_HIT: Modifier to ranged attack rolls. + AC: Modifier to Armor Class (inverts standard modifier since lower AC is better). + INITIATIVE: Modifier to initiative rolls. + """ + + def __init__(self, score: int): + """Initialize a new Intelligence instance. + + Args: + score (int): The raw ability score. + """ + super().__init__(score) + self.ability_type = AbilityType.DEXTERITY + self.prime_requisite_classes = [ + CharacterClassType.THIEF, + CharacterClassType.HALFLING, + ] + + # We need to use both the **standard map** and + # a **custom map** for initiative because its + # modifiers deviate from the standard map. + _INITIATIVE_MODIFIER_MAP = { + 3: -2, + 4: -1, + 5: -1, + 6: -1, + 7: -1, + 8: -1, + 13: 1, + 14: 1, + 15: 1, + 16: 1, + 17: 1, + 18: 2, + } + + def _get_initiative_modifier(self) -> int: + return self._INITIATIVE_MODIFIER_MAP.get(self.score, 0) + + def init_modifiers(self): + """Initialize the Dexterity modifiers. + + Modifiers: + TO_HIT: Modifier to ranged attack rolls. + AC: Modifier to Armor Class (inverts standard modifier since lower AC is better). + INITIATIVE: Modifier to initiative rolls. + """ + self.modifiers[ModifierType.AC] = -self._get_modifier() # Lower-is-better for AC + self.modifiers[ModifierType.TO_HIT] = self._get_modifier() + self.modifiers[ModifierType.INITIATIVE] = self._get_initiative_modifier() + + +class Constitution(Ability): + """Represents the Constitution ability for characters. + + Constitution measures stamina, endurance, and overall health. + + Modifiers: + HP: Modifier to Hit Points gained per level. + """ + + def __init__(self, score: int): + """Initialize a new Constitution instance. + + Args: + score (int): The raw ability score. + """ + super().__init__(score) + self.ability_type = AbilityType.CONSTITUTION + + def init_modifiers(self): + """Initialize the Constitution modifiers. + + Modifiers: + HP: Modifier to Hit Points gained per level. + """ + self.modifiers[ModifierType.HP] = self._get_modifier() + + +class Charisma(Ability): + """Represents the Charisma ability for characters. + + Charisma measures force of personality, leadership ability, and physical attractiveness. + + Modifiers: + REACTION: Modifier to reaction rolls when interacting with NPCs. + """ + + def __init__(self, score: int): + """Initialize a new Charisma instance. + + Args: + score (int): The raw ability score. + """ + super().__init__(score) + self.ability_type = AbilityType.CHARISMA + + # Overriding because we can't use the standard map for reactions + _MODIFIER_MAP = { + 3: -2, + 4: -1, + 5: -1, + 6: -1, + 7: -1, + 8: -1, + 13: 1, + 14: 1, + 15: 1, + 16: 1, + 17: 1, + 18: 2, + } + + def init_modifiers(self): + """Initialize the Charisma modifiers. + + Modifiers: + REACTION: Modifier to reaction rolls when interacting with NPCs. + """ + self.modifiers[ModifierType.REACTION] = self._get_modifier() diff --git a/osrlib/osrlib/adventure.py b/osrlib/osrlib/adventure.py new file mode 100644 index 0000000..c4f3228 --- /dev/null +++ b/osrlib/osrlib/adventure.py @@ -0,0 +1,119 @@ +from osrlib.game_manager import logger + + +class AdventureAlreadyStartedError(Exception): + """Raised when attempting to start an adventure that has already been started.""" + + pass + + +class Adventure: + """An Adventure is a collection of dungeons that can be played through by a party of characters. + + To start an adventure, add a party to the adventure and then call `start_adventure()`. Once an adventure has started, + you can't add or remove the party or its characters until you call `end_adventure()` or the all characters in the + party have been killed. + + Only one party can play through an adventure at a time. + + Attributes: + name (str): The name of the adventure. + short_description (str): A short description of the adventure. + long_description (str): A long description of the adventure. + dungeons (list): A list of the dungeons in the adventure. + quests (list): A list of the quests in the adventure. + party (Party): The party playing through the adventure. + is_started (bool): Whether the adventure has started. + """ + + def __init__( + self, name: str, short_description: str = "", long_description: str = "", dungeons: list = [], quests: list = [] + ): + self.name = name + self.short_description = short_description + self.long_description = long_description + self.dungeons = dungeons + self.quests = quests + self.party = None + self.is_started = False + + def start_adventure(self, party: "Party"): + """Add a party to the adventure and start the adventure. + + Once the adventure has started, you can't add or remove the adventuring party, and the party can't be added to + any other adventures. + + Args: + party (Party): The party of characters who'll play through the adventure. + + Raises: + AdventureAlreadyStartedError: If the adventure has already been started. + """ + logger.info(f"The party '{party.name}' is starting Adventure '{self.name}'...") + if self.is_started: + raise AdventureAlreadyStartedError(f"This adventure ('{self.name}') has already been started.") + + if self.party is not None: + raise AdventureAlreadyStartedError(f"This adventure ('{self.name}') already has a party assigned to it.") + + self.party = party + self.is_started = True + logger.info(f"Party '{party.name}' has started Adventure '{self.name}'.") + + +class Dungeon: + """Contains a collection of encounters. + + The encounters in a dungeon can represent a location like a room or cavern and can contain monsters, traps, treasure, + and quest pieces. + + Attributes: + name (str): The name of the dungeon. + description (str): The description of the dungeon (location, environment, etc.). + encounters (list): A list of the encounters in the dungeon. + """ + + pass + + +class Encounter: + """An encounter represents a location like a room or cavern and can contain monsters, traps, treasure, and quest pieces. + + Attributes: + name (str): The name or ID of the encounter. + description (str): The description of the encounter (location, environment, etc.). + monsters (list): A list of the monsters in the encounter. + traps (list): A list of the traps in the encounter. + treasure (list): A list of the treasure in the encounter. The treasure can be any item like weapons, armor, quest pieces, or gold pieces (or gems or other valuables). + """ + + pass + + +class Monster: + """A Monster is a creature the party can encounter in a dungeon and defeat to obtain experience points and optionally treasure. + + Attributes: + name (str): The name of the monster. + monster_type (MonsterType): The type of monster (DEMON, DRAGON, HUMANOID, MAGICAL, UNDEAD, etc.) + description (str): The monster's description. + hit_dice (str): The hit dice of the monster in NdN format, like "1d8" or "2d6". + hit_points (int): The number of hit points the monster has. + weapon (Weapon): The weapon that the monster uses for attacks. + armor_class (int): The armor class of the monster. + treasure (list): A list of the treasure that the monster is carrying. The treasure can be any item like weapons, armor, quest pieces, or gold pieces (or gems or other valuables). + """ + + pass + + +class Quest: + """Stub class for Quest.""" + + pass + + +class QuestPiece: + """Stub class for QuestPiece.""" + + pass diff --git a/osrlib/osrlib/character_classes.py b/osrlib/osrlib/character_classes.py new file mode 100644 index 0000000..3bf7833 --- /dev/null +++ b/osrlib/osrlib/character_classes.py @@ -0,0 +1,358 @@ +"""Defines character classes for player characters.""" +from enum import Enum +from typing import List, Tuple, Union + +from osrlib.combat import AttackType +from osrlib.dice_roller import DiceRoll, roll_dice + + +class CharacterClassType(Enum): + """Enum representing the types of character classes.""" + + CLERIC = "Cleric" + DWARF = "Dwarf" + ELF = "Elf" + FIGHTER = "Fighter" + HALFLING = "Halfling" + MAGIC_USER = "Magic User" + THIEF = "Thief" + COMMONER = "Commoner" + + +class ClassLevel: + """Represents a single level in a character class. + + Attributes: + level_num (int): The level number. + title (str): The title for the level. + required_xp (int): The XP required to reach this level. + hit_dice (str): The hit dice for this level. + spell_slots (Union[None, str, List[Tuple[int, int]]]): Spell slots available at this level. + """ + + def __init__( + self, + level_num: int, + title: str, + xp_required_for_level: int, + hit_dice: str, + spell_slots: Union[None, str, List[Tuple[int, int]]], + ): + """Initialize a ClassLevel instance.""" + self.level_num = level_num + self.title = title + self.xp_required_for_level = xp_required_for_level + self.hit_dice = hit_dice + self.spell_slots = spell_slots + + def __str__(self) -> str: + """Return a string representation of the ClassLevel instance.""" + return f"{self.level_num} ({self.title})" + + +class CharacterClass: + """Defines a character class for a player character. + + Attributes: + class_type (CharacterClassType): The type of the character class. + saving_throws (List[int]): The saving throw values. + levels (List[ClassLevel]): The class levels. + current_level (ClassLevel): The current level of the character. + hit_die (str): The hit die for the character class. + hp (int): The current hit points. + xp (int): The current experience points. + """ + + def __init__(self, character_class_type: CharacterClassType, level: int = 1, constitution_modifier: int = 0): + """Initialize a CharacterClass instance.""" + self.class_type = character_class_type + self.saving_throws = saving_throws[self.class_type] + self.levels = class_levels[self.class_type] + self.current_level = self.levels[level] + self.hit_die = self.levels[1].hit_dice # hit die is always first-level (1dn) + + # If the character is not first level, roll hit points for the character using the hit dice for that level + if level > 1: + self.hp = max(roll_dice(self.levels[level].hit_dice, constitution_modifier).total_with_modifier, 1) + else: + self.hp = max(self.roll_hp(constitution_modifier).total_with_modifier, 1) + + self.xp = self.current_level.xp_required_for_level + + def __str__(self) -> str: + """Return a string representation of the CharacterClass instance.""" + return self.class_type.value + + def roll_hp(self, hp_modifier: int = 0) -> DiceRoll: + """Roll hit points for the character. + + Args: + hp_modifier (int): Bonus or penalty to apply to the hit point roll. + + Returns: + DiceRoll: The dice roll result. + """ + return roll_dice(self.hit_die, hp_modifier) + + def level_up(self, hp_modifier: int = 0) -> ClassLevel: + """Level up the character if possible. + + If the character's current XP meets the next level's requirement, the character's + current_level is populated with the next level available for the class. Hit points + are rolled using the hit die appropriate for the character's class and are applied + to the character's CharacterClass instance. for additional hit points. + + Example: + + >>> pc = PlayerCharacter(name="Sckricko", character_class=CharacterClassType.WARRIOR) + >>> pc.character_class.xp = 2000 # Manually setting XP to meet level-up criteria + >>> hp_modifier = pc.abilities[AbilityTypes.CONSTITUTION].modifiers[ModifierTypes.HP] + >>> new_level = pc.character_class.level_up(hp_modifier) + >>> new_level.level_num + 2 + + Args: + hp_modifier (int): Hit point bonus or penalty to apply to the HP roll when leveling. + + Returns: + ClassLevel: New level of the character. + + Raises: + ValueError: Raised if leveling up is not possible due to insufficient XP or maximum level reached. + """ + xp_needed_for_next_level = self.levels[self.current_level.level_num + 1].xp_required_for_level + if self.xp >= xp_needed_for_next_level: + if self.current_level.level_num < len(self.levels) - 1: + self.current_level = self.levels[self.current_level.level_num + 1] + self.hp += max(self.roll_hp(hp_modifier).total_with_modifier, 1) + return self.current_level + else: + error_msg = ( + f"Cannot level up. {self.class_type.name} is already at max level {self.current_level.level_num}." + ) + raise ValueError(error_msg) + else: + error_msg = ( + f"Cannot level up. {self.xp} XP is less than {self.current_level.xp_required_for_level} required XP." + ) + raise ValueError(error_msg) + + +cleric_levels = [ + ClassLevel(0, "Cleric (NPC)", 0, "1d4", None), + ClassLevel(1, "Acolyte", 0, "1d6", None), + ClassLevel(2, "Adept", 1500, "2d6", [(1, 1)]), + ClassLevel(3, "Priest/Priestess", 3000, "3d6", [(1, 2)]), + ClassLevel(4, "Vicar", 6000, "4d6", [(1, 2), (2, 1)]), + ClassLevel(5, "Curate", 12000, "5d6", [(1, 2), (2, 2)]), + ClassLevel(6, "Elder", 25000, "6d6", [(1, 2), (2, 2), (3, 1), (4, 1)]), + ClassLevel(7, "Bishop", 50000, "7d6", [(1, 2), (2, 2), (3, 2), (4, 1), (5, 1)]), + ClassLevel(8, "Lama", 100000, "8d6", [(1, 3), (2, 3), (3, 2), (4, 2), (5, 1)]), + ClassLevel(9, "Patriarch/Matriarch", 200000, "9d6", [(1, 3), (2, 3), (3, 3), (4, 2), (5, 2)]), + ClassLevel(10, "Patriarch/Matriarch 10th Level", 300000, "9d6+1", [(1, 4), (2, 4), (3, 3), (4, 2), (5, 2)]), + ClassLevel(11, "Patriarch/Matriarch 11th Level", 400000, "9d6+2", [(1, 4), (2, 4), (3, 4), (4, 3), (5, 4)]), + ClassLevel(12, "Patriarch/Matriarch 12th Level", 500000, "9d6+3", [(1, 5), (2, 5), (3, 4), (4, 4), (5, 3)]), + ClassLevel(13, "Patriarch/Matriarch 13th Level", 600000, "9d6+4", [(1, 5), (2, 5), (3, 5), (4, 4), (5, 4)]), + ClassLevel(14, "Patriarch/Matriarch 14th Level", 700000, "9d6+5", [(1, 6), (2, 5), (3, 5), (4, 5), (5, 4)]), +] + + +thief_levels = [ + ClassLevel(0, "Thief (NPC)", 0, "1d4", None), + ClassLevel(1, "Apprentice", 0, "1d4", None), + ClassLevel(2, "Footpad", 1200, "2d4", None), + ClassLevel(3, "Robber", 2400, "3d4", None), + ClassLevel(4, "Burglar", 4800, "4d4", None), + ClassLevel(5, "Cutpurse", 9600, "5d4", None), + ClassLevel(6, "Sharper", 20000, "6d4", None), + ClassLevel(7, "Pilferer", 40000, "7d4", None), + ClassLevel(8, "Thief", 80000, "8d4", None), + ClassLevel(9, "Master Thief", 160000, "9d4", None), + ClassLevel(10, "Master Thief 10th Level", 280000, "9d4+2", None), + ClassLevel(11, "Master Thief 11th Level", 400000, "9d4+4", None), + ClassLevel(12, "Master Thief 12th Level", 520000, "9d4+6", None), + ClassLevel(13, "Master Thief 13th Level", 640000, "9d4+8", None), + ClassLevel(14, "Master Thief 14th Level", 760000, "9d4+10", None), +] + + +dwarf_levels = [ + ClassLevel(0, "Dwarf (NPC)", 0, "1d4", None), + ClassLevel(1, "Dwarven Veteran", 0, "1d8", None), + ClassLevel(2, "Dwarven Warrior", 2200, "2d8", None), + ClassLevel(3, "Dwarven Swordmaster", 4400, "3d8", None), + ClassLevel(4, "Dwarven Hero", 8800, "4d8", None), + ClassLevel(5, "Dwarven Swashbuckler", 17000, "5d8", None), + ClassLevel(6, "Dwarven Myrmidon", 35000, "6d8", None), + ClassLevel(7, "Dwarven Champion", 70000, "7d8", None), + ClassLevel(8, "Dwarven Superhero", 140000, "8d8", None), + ClassLevel(9, "Dwarven Lord (Lady)", 270000, "9d8", None), + ClassLevel(10, "Dwarven Lord 10th Level", 400000, "9d8+3", None), + ClassLevel(11, "Dwarven Lord 11th Level", 530000, "9d8+6", None), + ClassLevel(12, "Dwarven Lord 12th Level", 660000, "9d8+9", None), +] + + +elf_levels = [ + ClassLevel(0, "Elf (NPC)", 0, "1d4", None), + ClassLevel(1, "Veteran-Medium", 0, "1d6", [(1, 1)]), + ClassLevel(2, "Warrior-Seer", 4000, "2d6", [(1, 2)]), + ClassLevel(3, "Swordmaster-Conjurer", 8000, "3d6", [(1, 2), (2, 1)]), + ClassLevel(4, "Hero-Magician", 16000, "4d6", [(1, 2), (2, 2)]), + ClassLevel(5, "Swashbuckler-Enchanter", 32000, "5d6", [(1, 2), (2, 2), (3, 1)]), + ClassLevel(6, "Myrmidon-Warlock", 64000, "6d6", [(1, 2), (2, 2), (3, 2)]), + ClassLevel(7, "Champion-Sorcerer", 120000, "7d6", [(1, 3), (2, 2), (3, 2), (4, 1)]), + ClassLevel(8, "Superhero-Necromancer", 250000, "8d6", [(1, 3), (2, 3), (3, 2), (4, 2)]), + ClassLevel(9, "Lord/Lady-Wizard", 400000, "9d6", [(1, 3), (2, 3), (3, 3), (4, 2), (5, 1)]), + ClassLevel(10, "Lord/Lady-Wizard 10th Level", 600000, "9d6+1", [(1, 3), (2, 3), (3, 3), (4, 3), (5, 2)]), +] + + +fighter_levels = [ + ClassLevel(0, "Fighter (NPC)", 0, "1d4", None), + ClassLevel(1, "Veteran", 0, "1d8", None), + ClassLevel(2, "Warrior", 2000, "2d8", None), + ClassLevel(3, "Swordmaster", 4000, "3d8", None), + ClassLevel(4, "Hero", 8000, "4d8", None), + ClassLevel(5, "Swashbuckler", 16000, "5d8", None), + ClassLevel(6, "Myrmidon", 32000, "6d8", None), + ClassLevel(7, "Champion", 64000, "7d8", None), + ClassLevel(8, "Superhero", 120000, "8d8", None), + ClassLevel(9, "Lord (Lady)", 240000, "9d8", None), + ClassLevel(10, "10th Level Lord", 360000, "9d8+2", None), + ClassLevel(11, "11th Level Lord", 480000, "9d8+4", None), + ClassLevel(12, "12th Level Lord", 600000, "9d8+6", None), + ClassLevel(13, "13th Level Lord", 720000, "9d8+8", None), + ClassLevel(14, "14th Level Lord", 840000, "9d8+10", None), +] + + +halfling_levels = [ + ClassLevel(0, "Halfling (NPC)", 0, "1d4", None), + ClassLevel(1, "Halfling Veteran", 0, "1d6", None), + ClassLevel(2, "Halfling Warrior", 2000, "2d6", None), + ClassLevel(3, "Halfling Swordmaster", 4000, "3d6", None), + ClassLevel(4, "Halfling Hero", 8000, "4d6", None), + ClassLevel(5, "Halfling Swashbuckler", 16000, "5d6", None), + ClassLevel(6, "Halfling Myrmidon", 32000, "6d6", None), + ClassLevel(7, "Halfling Champion", 64000, "7d6", None), + ClassLevel(8, "Sheriff", 120000, "8d6", None), +] + + +magic_user_levels = [ + ClassLevel(0, "Magic User (NPC)", 0, "1d4-1", None), + ClassLevel(1, "Medium", 0, "1d4", [(1, 1)]), + ClassLevel(2, "Seer", 2500, "2d4", [(1, 2)]), + ClassLevel(3, "Conjurer", 5000, "3d4", [(1, 2), (2, 1)]), + ClassLevel(4, "Magician", 10000, "4d4", [(1, 2), (2, 2)]), + ClassLevel(5, "Enchanter/Enchantress", 20000, "5d4", [(1, 2), (2, 2), (3, 1)]), + ClassLevel(6, "Warlock", 40000, "6d4", [(1, 2), (2, 2), (3, 2)]), + ClassLevel(7, "Sorcerer/Sorceress", 80000, "7d4", [(1, 3), (2, 2), (3, 2), (4, 1)]), + ClassLevel(8, "Necromancer", 150000, "8d4", [(1, 3), (2, 3), (3, 2), (4, 2)]), + ClassLevel(9, "Wizard", 300000, "9d4", [(1, 3), (2, 3), (3, 3), (4, 2), (5, 1)]), + ClassLevel(10, "Wizard 10th Level", 450000, "9d4+1", [(1, 3), (2, 3), (3, 3), (4, 3), (5, 2)]), + ClassLevel(11, "Wizard 11th Level", 600000, "9d4+2", [(1, 4), (2, 3), (3, 3), (4, 3), (5, 2), (6, 1)]), + ClassLevel(12, "Wizard 12th Level", 750000, "9d4+3", [(1, 4), (2, 4), (3, 3), (4, 3), (5, 3), (6, 2)]), + ClassLevel(13, "Wizard 13th Level", 900000, "9d4+4", [(1, 4), (2, 4), (3, 4), (4, 3), (5, 3), (6, 3)]), + ClassLevel(14, "Wizard 14th Level", 1050000, "9d4+5", [(1, 4), (2, 4), (3, 4), (4, 4), (5, 3), (6, 3)]), +] + + +commoner_levels = [ + ClassLevel(0, "Commoner", 0, "1d4-3", None), + ClassLevel(1, "Candlestick Maker", 0, "1d4-2", None), + ClassLevel(2, "Baker", 2000, "1d4-1", None), + ClassLevel(3, "Butcher", 4000, "1d4", None), + ClassLevel(4, "Farmer", 8000, "1d4+1", None), + ClassLevel(5, "Fishmonger", 16000, "1d4+2", None), + ClassLevel(6, "Mason", 32000, "1d4+3", None), + ClassLevel(7, "Blacksmith", 64000, "1d4+4", None), + ClassLevel(8, "Trapper", 120000, "1d4+5", None), + ClassLevel(9, "Hunter", 240000, "1d4+6", None), + ClassLevel(10, "Hunter 10th Level", 360000, "1d4+7", None), +] + + +class_levels = { + CharacterClassType.CLERIC: cleric_levels, + CharacterClassType.DWARF: dwarf_levels, + CharacterClassType.ELF: elf_levels, + CharacterClassType.FIGHTER: fighter_levels, + CharacterClassType.HALFLING: halfling_levels, + CharacterClassType.MAGIC_USER: magic_user_levels, + CharacterClassType.THIEF: thief_levels, + CharacterClassType.COMMONER: commoner_levels, +} + + +saving_throws = { + CharacterClassType.CLERIC: { + AttackType.DEATH_RAY_POISON: 11, + AttackType.MAGIC_WANDS: 12, + AttackType.PARALYSIS_TURN_TO_STONE: 14, + AttackType.DRAGON_BREATH: 16, + AttackType.RODS_STAVES_SPELLS: 15, + }, + CharacterClassType.DWARF: { + AttackType.DEATH_RAY_POISON: 10, + AttackType.MAGIC_WANDS: 11, + AttackType.PARALYSIS_TURN_TO_STONE: 12, + AttackType.DRAGON_BREATH: 13, + AttackType.RODS_STAVES_SPELLS: 14, + }, + CharacterClassType.ELF: { + AttackType.DEATH_RAY_POISON: 12, + AttackType.MAGIC_WANDS: 13, + AttackType.PARALYSIS_TURN_TO_STONE: 13, + AttackType.DRAGON_BREATH: 15, + AttackType.RODS_STAVES_SPELLS: 15, + }, + CharacterClassType.FIGHTER: { + AttackType.DEATH_RAY_POISON: 12, + AttackType.MAGIC_WANDS: 13, + AttackType.PARALYSIS_TURN_TO_STONE: 14, + AttackType.DRAGON_BREATH: 15, + AttackType.RODS_STAVES_SPELLS: 16, + }, + CharacterClassType.HALFLING: { + AttackType.DEATH_RAY_POISON: 10, + AttackType.MAGIC_WANDS: 11, + AttackType.PARALYSIS_TURN_TO_STONE: 12, + AttackType.DRAGON_BREATH: 13, + AttackType.RODS_STAVES_SPELLS: 14, + }, + CharacterClassType.MAGIC_USER: { + AttackType.DEATH_RAY_POISON: 13, + AttackType.MAGIC_WANDS: 14, + AttackType.PARALYSIS_TURN_TO_STONE: 13, + AttackType.DRAGON_BREATH: 16, + AttackType.RODS_STAVES_SPELLS: 15, + }, + CharacterClassType.THIEF: { + AttackType.DEATH_RAY_POISON: 13, + AttackType.MAGIC_WANDS: 14, + AttackType.PARALYSIS_TURN_TO_STONE: 13, + AttackType.DRAGON_BREATH: 16, + AttackType.RODS_STAVES_SPELLS: 15, + }, + CharacterClassType.COMMONER: { + AttackType.DEATH_RAY_POISON: 20, + AttackType.MAGIC_WANDS: 20, + AttackType.PARALYSIS_TURN_TO_STONE: 20, + AttackType.DRAGON_BREATH: 20, + AttackType.RODS_STAVES_SPELLS: 20, + }, +} + + +all_character_classes = [ + CharacterClass(CharacterClassType.CLERIC), + CharacterClass(CharacterClassType.THIEF), + CharacterClass(CharacterClassType.DWARF), + CharacterClass(CharacterClassType.ELF), + CharacterClass(CharacterClassType.FIGHTER), + CharacterClass(CharacterClassType.HALFLING), + CharacterClass(CharacterClassType.MAGIC_USER), + CharacterClass(CharacterClassType.COMMONER), +] diff --git a/osrlib/osrlib/combat.py b/osrlib/osrlib/combat.py new file mode 100644 index 0000000..e3c96bf --- /dev/null +++ b/osrlib/osrlib/combat.py @@ -0,0 +1,24 @@ +from enum import Enum + + +class AttackType(Enum): + MELEE = "Meelee" + RANGED = "Ranged" + DEATH_RAY_POISON = "Death ray or poison" + MAGIC_WANDS = "Magic wands" + PARALYSIS_TURN_TO_STONE = "Paralysis or turn to stone" + DRAGON_BREATH = "Dragon breath" + RODS_STAVES_SPELLS = "Rods, staves, or spells" + + +class ModifierType(Enum): + TO_HIT = "To hit" + DAMAGE = "Damage" + OPEN_DOORS = "Open doors" + LANGUAGES = "Languages" + SAVING_THROWS = "Magic-based saving throws" + AC = "AC" + INITIATIVE = "Initiative" + HP = "HP" + REACTION = "Monster and NPC reactions" + XP = "XP" diff --git a/osrlib/osrlib/dice_roller.py b/osrlib/osrlib/dice_roller.py new file mode 100644 index 0000000..d038df4 --- /dev/null +++ b/osrlib/osrlib/dice_roller.py @@ -0,0 +1,127 @@ +"""Dice roller module for rolling dice based on the nDn or Dn notation, supporting modifiers.""" +import random +import re +from collections import namedtuple + + +class DiceRoll( + namedtuple( + "RollResultBase", + ["num_dice", "num_sides", "total", "modifier", "total_with_modifier", "rolls"], + ) +): + """Roll dice based on the nDn or Dn notation, optionally including a modifier like '3d6+2' or '1d20-1'. + + Args: + namedtuple (RollResult): The named tuple containing the number of dice, number of sides, base roll, modifier, total roll with modifier, and the individual rolls. + """ + def __str__(self): + """ + Returns a string representation of the dice roll based on the ndn notation, including modifiers if applicable. + + Returns: + str: A string in ndn notation (e.g., '3d6', '1d20+5', '2d8-4'). + """ + base = f"{self.num_dice}d{self.num_sides}" + if self.modifier > 0: + return f"{base}+{self.modifier}" + elif self.modifier < 0: + return f"{base}{self.modifier}" + else: + return base + + def pretty_print(self): + """ + Returns a human-readable string representation of the dice roll, including the total roll and any modifiers. + + Returns: + str: A string describing the dice roll and its outcome (e.g., 'Rolled 3d6 and got 11 (11)', 'Rolled 1d20+3 and got 9 (6 + 3)'). + """ + base_str = f"{self.total}" + if self.modifier != 0: + base_str += f" {'+' if self.modifier > 0 else '-'} {abs(self.modifier)}" + return f"Rolled {self.total_with_modifier} on {self} ({base_str})" + + +def roll_dice(notation: str, modifier: int = 0, drop_lowest: bool = False): + """ + Rolls dice based on the nDn or Dn notation, supporting modifiers. + + Args: + notation (str): The dice notation in ndn format with optional modifiers (e.g., '3d6', '1d20+5', '2d8-4'). + modifier (int): An optional additional integer modifier to add to the roll. Defaults to 0. + drop_lowest (bool): Whether to drop the lowest dice roll. Defaults to False. + + Returns: + DiceRoll: A named tuple containing the number of dice, number of sides, base roll, modifier, total roll with modifier, and the individual rolls. + + Raises: + ValueError: If the notation or dice sides are invalid. + + Example usage: + >>> result = roll_dice('3d6') + >>> print(result.pretty_print()) + + >>> result = roll_dice('1d20+5') + >>> print(result.pretty_print()) + + >>> result = roll_dice('4d6', drop_lowest=True) + >>> print(result.pretty_print()) + """ + rand_gen = random.SystemRandom() + + notation = notation.replace(" ", "") + notation = _add_modifier_to_dice_notation(notation, modifier) + + match = re.match(r"(\d*)d(\d+)([+-]\d+)?", notation, re.IGNORECASE) + + num_dice, num_sides, modifier = match.groups() + num_dice = int(num_dice) if num_dice else 1 + num_sides = int(num_sides) + modifier = int(modifier) if modifier else 0 + + if num_sides not in [4, 6, 8, 10, 12, 20, 100]: + raise ValueError("Invalid number of dice sides. Choose from 4, 6, 8, 10, 12, 20, 100.") + + die_rolls = [rand_gen.randint(1, num_sides) for _ in range(num_dice)] + + if drop_lowest and len(die_rolls) > 1: + die_rolls.remove(min(die_rolls)) + + total = sum(die_rolls) + total_with_modifier = total + modifier + + return DiceRoll(num_dice, num_sides, total, modifier, total_with_modifier, die_rolls) + + +def _add_modifier_to_dice_notation(notation: str, modifier: int) -> str: + """ + Adds a modifier to a dice notation string. + + Args: + notation (str): Existing dice notation string, like '1d6' or '1d6+1'. + modifier (int): The integer modifier to add. + + Returns: + str: The modified dice notation string. + + Raises: + ValueError: If the input notation is invalid. + """ + match = re.match(r"(\d*)d(\d+)([+-]\d+)?", notation, re.IGNORECASE) + if not match: + raise ValueError( + "Invalid number of dice and sides. Use dn or ndn format like 'd6', '3d6', '3d6+2', or '3d6-2'." + ) + + num_dice, num_sides, existing_modifier = match.groups() + + existing_modifier = int(existing_modifier) if existing_modifier else 0 + + new_modifier = existing_modifier + modifier + if new_modifier == 0: + return f"{num_dice}d{num_sides}" + elif new_modifier > 0: + return f"{num_dice}d{num_sides}+{new_modifier}" + else: + return f"{num_dice}d{num_sides}{new_modifier}" diff --git a/osrlib/osrlib/game_manager.py b/osrlib/osrlib/game_manager.py new file mode 100644 index 0000000..bd708ae --- /dev/null +++ b/osrlib/osrlib/game_manager.py @@ -0,0 +1,64 @@ +"""The GameManager module provides the main API surface for the game.""" +from enum import Enum +import json +import logging +import warnings + +logger = logging.getLogger(__name__) + + +class StorageType(Enum): + JSON = "json" + YAML = "yaml" + TOML = "toml" + + +class GameManager: + """The GameManager class provides facilities for working with parties and their adventures. It's the main API entry point for the game. + + Access the GameManager singleton via the `game_manager.game_manager` module-level variable. + + Example: + + >>> from osrlib import game_manager, adventure + >>> gm = game_manager.game_manager + >>> gm.parties.append(Party("The B-Team")) + >>> search_for_the_unknown = adventure.Adventure("Search for the Unknown") + >>> gm.start_adventure(search_for_the_unknown, gm.parties[0]) + + + Attributes: + parties (list): A list of the available parties. + adventures (list): A list of the available adventures. + """ + + def __init__( + self, + parties: list = [], + adventures: list = [], + ): + logger.info("Initializing the GameManager...") + self.adventures = adventures + self.parties = parties + logger.info(f"GameManager initialized. There are {len(self.adventures)} adventures available.") + + def save_game(self, storage_type: StorageType = StorageType.JSON): + """Save the game state to persistent storage in the given format. + + Args: + storage_type (StorageType): The format to use for saving the game state. + """ + logger.info(f"Saving the game to persistent storage in {storage_type} format...") + if storage_type == StorageType.JSON: + with open("game_manager.json", "w") as f: + json.dump({"parties": self.parties, "adventures": self.adventures}, f) + else: + warnings.warn(f"Storage type {storage_type} not yet supported.") + + def load_game(self): + """Load the game state from disk.""" + warnings.warn("Game LOAD is not yet implemented.", UserWarning) + + +game_manager = GameManager() +"""This module-level variable is the main API entry point for the game - use it instead of instantiating a new GameManager.""" diff --git a/osrlib/osrlib/inventory.py b/osrlib/osrlib/inventory.py new file mode 100644 index 0000000..c4d3315 --- /dev/null +++ b/osrlib/osrlib/inventory.py @@ -0,0 +1,211 @@ +from collections import defaultdict +from typing import List + +from osrlib.item import ( + Item, + ItemAlreadyHasOwnerError, + ItemAlreadyInInventoryError, + ItemEquippedError, + ItemNotEquippedError, + ItemNotInInventoryError, + ItemNotUsableError, + ItemType, +) + + +class Inventory: + """A container to hold items owned by a player character (PC). + + You should not create an InventoryManager directly. When you initialize a PlayerCharacter, an inventory is created as a + property of the PC. You can then add and remove items to and from the inventory using methods on the + PlayerCharacter. + + Attributes: + item_dict (defaultdict[ItemType, List[Item]]): List of items in the inventory. + owner (PlayerCharacter): Owner of the inventory. + + Example: + >>> pc = PlayerCharacter() + >>> inv = Inventory(pc) + """ + + # TODO: Add a drop_all_items() method to remove all items from the inventory (unequip and set owner to None) + + def __init__(self, player_character_owner: "PlayerCharacter"): + """Initialize a PlayerCharacter's inventory. + + Args: + owner (PlayerCharacter): The player character that owns the inventory. + """ + self.items: defaultdict[ItemType, List[Item]] = defaultdict(list) + self.owner = player_character_owner + + def add_item(self, item: Item): + """Add an item to the inventory and sets its owner. + + Args: + item (Item): Item to add. + """ + if item.owner is not None and item.owner != self.owner: + raise ItemAlreadyHasOwnerError( + f"Can't add item '{item.name}' to inventory because it's already owned by '{item.owner.name}'." + ) + + if item not in self.items[item.item_type]: + item._set_owner(self.owner) + self.items[item.item_type].append(item) + return True # Successfully added + else: + raise ItemAlreadyInInventoryError( + f"Can't add item '{item.name}' to inventory of '{self.owner.name}' because it's already in their inventory." + ) + + def get_item(self, item: Item): + """Gets an item from the inventory. + + Args: + item (Item): Item to get from the inventory. + + Returns: + Item: The item if it exists in the inventory, otherwise an Exception is thrown. + + Raises: + Exception: If the item does not exist in the inventory. + """ + if item in self.items[item.item_type]: + return item + else: + raise ItemNotInInventoryError( + f"Can't get item '{item.name}' from inventory of '{self.owner.name}' because it's not in their inventory." + ) + + def remove_item(self, item: Item): + """Removes an item from the inventory and resets its owner to None. + + Args: + item (Item): Item to remove. + + Returns: + bool: True if the item was successfully removed. + + Raises: + Exception: If the item is currently equipped. + """ + if item.is_equipped: + raise ItemEquippedError( + f"Can't remove item '{item.name}' from inventory of '{self.owner.name}' because it's currently equipped." + ) + + if item in self.items[item.item_type]: + item._set_owner(None) + self.items[item.item_type].remove(item) + return True # Successfully removed + else: + raise ItemNotInInventoryError( + f"Can't remove item '{item.name}' from inventory of '{self.owner.name}' because it's not in their inventory." + ) + + def equip_item(self, item: Item): + """Equips an item if it can be equipped. + + Args: + item (Item): Item to equip. + + Returns: + bool: True if the item was successfully equipped. False if the item could not be equipped. + + Raises: + ItemNotUsableError: If the item is not usable by the owner's character class. + """ + if item.is_equipable: + currently_equipped = sum(1 for i in self.items[item.item_type] if i.is_equipped) + if currently_equipped < item.max_equipped: + item.is_equipped = True + return True # Successfully equipped + else: + return False # Could not equip because max number already equipped + else: + raise ItemNotUsableError(f"Can't equip item '{item.name}' because it is not usable by {self.owner.name}.") + + def unequip_item(self, item: Item): + """Unequips an item if it is currently equipped. + + Args: + item (Item): Item to unequip. + + Returns: + bool: True if the item was successfully unequipped. + + Raises: + Exception: If the item is not currently equipped. + """ + if item.is_equipped: + item.is_equipped = False + return True # Successfully unequipped + else: + raise ItemNotEquippedError(f"Can't unequip item '{item.name}' because it is not currently equipped.") + + @property + def all_items(self): + """Gets all items stored in the items defaultdict inventory property. + + Returns: + list[Item]: List of all items. + """ + return [item for sublist in self.items.values() for item in sublist] + + @property + def armor(self): + """Gets all armor items stored in the items defaultdict inventory property. + + Returns: + list[Item]: List of armor items. Returns an empty list if no armor items are present. + """ + return self.items[ItemType.ARMOR] + + @property + def weapons(self): + """Gets all weapon items stored in the items defaultdict inventory property. + + Returns: + list[Item]: List of weapon items. Returns an empty list if no weapon items are present. + """ + return self.items[ItemType.WEAPON] + + @property + def spells(self): + """Gets all spell items stored in the items defaultdict inventory property. + + Returns: + list[Item]: List of spell items. Returns an empty list if no spell items are present. + """ + return self.items[ItemType.SPELL] + + @property + def equipment(self): + """Gets all equipment items stored in the items defaultdict inventory property. + + Returns: + list[Item]: List of equipment items. Returns an empty list if no equipment items are present. + """ + return self.items[ItemType.EQUIPMENT] + + @property + def magic_items(self): + """Gets all magic items stored in the items defaultdict inventory property. + + Returns: + list[Item]: List of magic items. Returns an empty list if no magic items are present. + """ + return self.items[ItemType.MAGIC_ITEM] + + @property + def misc_items(self): + """Gets all miscellaneous items stored in the items defaultdict inventory property. + + Miscellaneous items include items that are not armor, weapons, spells, equipment, or magic items. + + Returns: + list[Item]: List of miscellaneous items. Returns an empty list if no miscellaneous items are present. + """ + return self.items[ItemType.ITEM] diff --git a/osrlib/osrlib/item.py b/osrlib/osrlib/item.py new file mode 100644 index 0000000..ea63429 --- /dev/null +++ b/osrlib/osrlib/item.py @@ -0,0 +1,414 @@ +"""Classes that represent items in the game and player character (PC) inventory.""" +from enum import Enum +from typing import Optional, Set + +from osrlib import CharacterClassType + + +class ItemType(Enum): + """Enumerates the types of items in the game. + + The item type determines whether the item can be used by a player character (PC) and, if so, whether it can be + equipped by the PC. + """ + + ARMOR = ("Armor", "Armor, helmet, gloves, or boots") + WEAPON = ("Weapon", "Bladed, blunt, or ranged weapon") + SPELL = ("Spell", "Spell or scroll") + EQUIPMENT = ("Equipment", "Piece of adventurers' equipment") + MAGIC_ITEM = ( + "Magic item", + "Potion, ring, or other item imbued with magical properties", + ) + ITEM = ("Normal item", "Normal (non-magical) item") + + +class ItemAlreadyHasOwnerError(Exception): + """Exception raised when an item already has an owner.""" + + pass + + +class ItemAlreadyInInventoryError(Exception): + """Exception raised when trying to add an item to a player character's (PC) inventory already in the inventory.""" + + pass + + +class ItemEquippedError(Exception): + """Exception raised when trying to equip an item the player character (PC) already has equipped.""" + + pass + + +class ItemNotEquippedError(Exception): + """Exception raised when trying to unequip an item the player character (PC) doesn't have equipped.""" + + pass + + +class ItemNotInInventoryError(Exception): + """Exception raised when trying to remove an item from a player character's (PC) inventory that's not in the inventory.""" + + pass + + +class ItemNotUsableError(Exception): + """Exception raised when trying to use an item that the player character (PC) can't use. + + The inability to use an item is typically due to a character class restriction. For example, a magic user can't use + a sword and a thief can't wear plate mail armor.""" + + pass + + +class ItemAlreadyInQuestError(Exception): + """Exception raised when trying to assign an item to a quest that's already been assigned to a quest.""" + + pass + + +class Item: + """An item represents a piece of equipment, a weapon, spell, quest piece, any other item that can be owned by a player character (PC). + + You can specify that an item can be equipped by a PC and that more than one of that item type can be equipped. + + Any item can be marked as a quest piece by setting its quest attribute. Quest pieces are items required to be + obtained as part of a quest. An item can be both a quest piece and usable by a PC, for example, an enchanted (or cursed!) sword or magic ring. + + Attributes: + name (str): Name of the item. + item_type (ItemType): Type of the item. Defaults to ItemType.ITEM. + owner (Optional[PlayerCharacter]): The owner of the item. + usable_by_classes (Set[CharacterClassType]): Classes that can use the item. + max_equipped (int): Max number of this item that can be equipped. + is_equipped (bool): Whether the item is currently equipped. + gp_value (int): The value of the item in gold pieces (gp). + quest (Optional[Quest]): The quest that the item is a part of. + + Example: + >>> usable_by = {CharacterClassType.FIGHTER, CharacterClassType.THIEF} + >>> item = Item("Sword", ItemType.WEAPON, usable_by, max_equipped=1, gp_value=10) + """ + + def __init__( + self, + name: str, + item_type: ItemType = ItemType.ITEM, + usable_by_classes: Optional[Set[CharacterClassType]] = None, + max_equipped: int = 0, + gp_value: int = 0, + quest: Optional["Quest"] = None, + ): + """Initialize an item with the specified properties. + + Don't call the methods on this class directory. Instead, use a PlayerCharacter's InventoryManager (pc.inventory) + to add/remove this item from a PC's inventor or add it to a Quest. + + Args: + name (str): Name of the item. + item_type (ItemType): Type of the item. + usable_by_classes (Set[CharacterClassType]): Classes that can use the item. + max_equipped (int, optional): Max number that can be equipped. Defaults to 0. + gp_value (int, optional): Value of the item in gold pieces (gp). Defaults to 0. + quest (Optional[Quest], optional): The quest that the item is a part of. Defaults to None. + """ + self.name = name + self.item_type = item_type + self.owner = None + self.usable_by_classes = ( + usable_by_classes if usable_by_classes is not None else set() + ) + self.max_equipped = max_equipped + self.gp_value = gp_value + self.is_equipped = False + if quest is not None: + self._set_quest(quest) + + def __str__(self): + return f"{self.name} ({self.item_type.name})" + + def _set_owner(self, player_character: "PlayerCharacter"): + """Sets the owner of the item to the specified player character. + + Args: + pc (PlayerCharacter): The player character to set as the owner. + """ + self.owner = player_character + + def _set_quest(self, quest: "Quest"): + """Sets the quest the item is a part of. + + When this and the other items in the quest are obtained (the item's owner is set), the quest is completed. + + Args: + quest (Quest): The quest that the item is a part of. + """ + if self.quest is not None: + raise ItemAlreadyInQuestError( + f"Item '{self.name}' already been assigned to quest '{quest.name}'." + ) + self.quest = quest + + @property + def is_usable_by_owner(self) -> bool: + """Whether the item is usable by its owner. + + The item is usable if the owner is not None and the owning player character's class is in the set of classes in + the item's usable_by_classes attribute. If the item is usable and also has a max_equipped value greater than 0, + it can be equipped. + + Returns: + bool: True if the CharacterClassType of the owning PlayerCharacter can use the item, otherwise False. + """ + if self.owner is None: + return False + return self.owner.character_class.class_type in self.usable_by_classes + + @property + def is_equipable(self) -> bool: + """Whether the item is equipable by its owner. + + Returns: + bool: True if the item is usable by the owner's character class and at least one of the item can be + equipped, otherwise False. + """ + if self.is_usable_by_owner and self.max_equipped > 0: + return True + return False + + @property + def is_quest_piece(self) -> bool: + """Whether the item is a quest piece. + + This is a convenience property that checks whether the item's quest attribute is not None. + + Returns: + bool: True if the item is associated with a quest, otherwise False. + """ + return self.quest is not None + + def to_dict(self) -> dict: + return { + "name": self.name, + "item_type": self.item_type.value[ + 0 + ], # Store only the first element of the tuple + "usable_by_classes": [ + cls.value for cls in self.usable_by_classes + ], # Assuming CharacterClassType is an Enum + "max_equipped": self.max_equipped, + "is_equipped": self.is_equipped, + "gp_value": self.gp_value, + # TODO: serialize quest attribute (or ID or ...?) + #'quest': self.quest.name if self.quest else None # Assuming Quest has a name attribute + } + + @classmethod + def from_dict(cls, item_dict: dict) -> "Item": + item_type = ItemType( + next(filter(lambda x: x.value[0] == item_dict["item_type"], ItemType)) + ) # Convert back to Enum + usable_by_classes = { + CharacterClassType(value) for value in item_dict["usable_by_classes"] + } # Convert back to Enum Set + + item = cls( + item_dict["name"], + item_type, + usable_by_classes, + item_dict["max_equipped"], + item_dict["gp_value"], + # TODO: deserialize quest attribute + # Quest(item_dict['quest']) if item_dict.get('quest') else None + ) + item.is_equipped = item_dict["is_equipped"] + + return item + + +class Armor(Item): + """An Armor item modifies the armor class (AC) of a player character (PC). + + Inherits all attributes from the Item class and introduces an additional attribute for AC modification. + + The bonus of any non-cursed magic armor or shield is subtracted from the character's AC. For example, a fighter + wearing plate mail armor and using a shield (but with no Dexterity modifier) has an AC of 2. If the fighter uses a + shield + 1, their AC = 1. + + Attributes: + ac_modifier (int): Armor class (AC) bonus or penalty provided by this armor. A positive number reduces AC (good) + and a negative number increases AC (bad). Defaults to 1. + + Example: + >>> armor = Armor("Plate Mail", ac_modifier=7) + """ + + def __init__(self, name: str, ac_modifier: int = -1, **kwargs): + """Initialize an armor item with the specified properties. + + Args: + name (str): Name of the item. + ac_modifier (int, optional): AC modifier. Lower is better. Defaults to -1. + **kwargs: Additional arguments to pass to the parent class. + """ + super().__init__(name, ItemType.ARMOR, **kwargs) + self.ac_modifier = ac_modifier + + def __str__(self): + """Get a string representation of the armor item. + + Returns: + str: A string with the armor name and AC modifier. + """ + return f"{self.name} (AC: {9 + self.ac_modifier})" + + def to_dict(self) -> dict: + armor_dict = super().to_dict() + armor_dict["ac_modifier"] = self.ac_modifier + return armor_dict + + @classmethod + def from_dict(cls, armor_dict: dict) -> "Armor": + base_item = Item.from_dict(armor_dict) + ac_modifier = armor_dict.get("ac_modifier", -1) + return cls( + name=base_item.name, + ac_modifier=ac_modifier, + usable_by_classes=base_item.usable_by_classes, + max_equipped=base_item.max_equipped, + gp_value=base_item.gp_value, + ) + + +class Weapon(Item): + """Represents a weapon item in the game. + + Args: + name (str): The name of the weapon. + to_hit_damage_die (str, optional): The to-hit and damage roll for the weapon. Defaults to '1d4'. + range (Optional[int], optional): The range of the weapon in feet. Defaults to None for melee weapons. + **kwargs: Arbitrary keyword arguments inherited from the Item class. + + Attributes: + to_hit_damage_die (str): The to-hit and damage die for the weapon, formatted like '1d8', '2d4', '1d6+1', etc. + range (Optional[int]): The range of the weapon in feet. + + Note: + The Weapon class extends the Item class to represent weapons in the game. + It specifies damage die and may have a range indicating how far it can attack. + Melee weapons typically have `None` as their range value. + + Example: + >>> sword = Weapon(name="Longsword", to_hit_damage_die="1d8") + >>> enchanted_bow = Weapon(name="Longbow of Accuracy", to_hit_damage_die="1d8+1", range=150) + """ + + def __init__( + self, + name: str, + to_hit_damage_die: str = "1d4", + range: Optional[int] = None, # melee weapons do not have a range + **kwargs, + ): + """Initialize a weapon item with the specified properties.""" + super().__init__(name, ItemType.WEAPON, **kwargs) + self.owner = kwargs.get("owner", None) + self.to_hit_damage_die = ( + to_hit_damage_die # format like "1d8", "1d6+1", "1d4-1" etc. + ) + self.range = range # in feet (None for melee weapons) + + def __str__(self): + return f"{self.name} (Damage: {self.to_hit_damage_die}, Range: {self.range}')" + + def to_dict(self) -> dict: + weapon_dict = super().to_dict() + weapon_dict["to_hit_damage_die"] = self.to_hit_damage_die + weapon_dict["range"] = self.range + return weapon_dict + + @classmethod + def from_dict(cls, weapon_dict: dict) -> "Weapon": + base_item = Item.from_dict(weapon_dict) + to_hit_damage_die = weapon_dict.get("to_hit_damage_die", "1d4") + range = weapon_dict.get("range", None) + return cls( + name=base_item.name, + to_hit_damage_die=to_hit_damage_die, + range=range, + usable_by_classes=base_item.usable_by_classes, + max_equipped=base_item.max_equipped, + gp_value=base_item.gp_value, + ) + + +class Spell(Item): + """Represents a spell item in the game. + + The Spell class extends the Item class to represent spells in the game. Spells have a level and may have a range, + damage die, and duration. Unlike weapons, spells do not have a to-hit roll (they always hit if the spellcaster + successfully cast the spell). + + Args: + name (str): The name of the spell. + spell_level (int): The level of the spell. + damage_die (Optional[str], optional): The damage roll for the spell. Defaults to None. + range (Optional[int], optional): The range of the spell in feet. Defaults to None for touch spells. + duration_turns (Optional[int], optional): The duration of the spell in turns (1 turn = 10 minutes). Defaults to + None for instantaneous spells. + **kwargs: Arbitrary keyword arguments inherited from the Item class. + + Attributes: + spell_level (int): The level of the spell. + damage_die (Optional[str]): The damage die for the spell, formatted like '1d8', '2d6', etc. + range (Optional[int]): The range of the spell in feet. + duration_minutes (Optional[int]): The duration of the spell in minutes. Defaults to None which indicates an + instantaneous spell. + + Example: + >>> fireball = Spell(name="Fireball", spell_level=3, damage_die="8d6", range=150, duration_minutes=None) + >>> heal = Spell(name="Heal", spell_level=6, damage_die=None, range=None, duration_minutes=10) + """ + + def __init__( + self, + name: str, + spell_level: int, + damage_die: Optional[str] = None, + range: Optional[int] = None, # 'touch' spells do not have a range + duration_turns: Optional[int] = None, # 'instantaneous' spells have no duration + **kwargs, + ): + """Initialize a spell item with the specified properties.""" + super().__init__(name, ItemType.SPELL, **kwargs) + self.spell_level = spell_level + self.range = range # None for 'touch' spells + self.damage_die = damage_die # Optional - format like "1d8", "2d6", etc. + self.duration_turns = duration_turns # None for 'instantaneous' spells + + def to_dict(self) -> dict: + spell_dict = super().to_dict() + spell_dict["spell_level"] = self.spell_level + spell_dict["range"] = self.range + spell_dict["damage_die"] = self.damage_die + spell_dict["duration_turns"] = self.duration_turns + return spell_dict + + @classmethod + def from_dict(cls, spell_dict: dict) -> "Spell": + base_item = Item.from_dict(spell_dict) + spell_level = spell_dict.get("spell_level", 1) + range = spell_dict.get("range", None) + damage_die = spell_dict.get("damage_die", None) + duration_turns = spell_dict.get("duration_turns", None) + return cls( + name=base_item.name, + spell_level=spell_level, + range=range, + damage_die=damage_die, + duration_turns=duration_turns, + usable_by_classes=base_item.usable_by_classes, + max_equipped=base_item.max_equipped, + gp_value=base_item.gp_value, + ) diff --git a/osrlib/osrlib/party.py b/osrlib/osrlib/party.py new file mode 100644 index 0000000..7bf5d9a --- /dev/null +++ b/osrlib/osrlib/party.py @@ -0,0 +1,434 @@ +"""The Party module contains the Party class and functions related to managing a party of player characters (collection +of type PlayerCharacter).""" + +from typing import List + +from osrlib import player_character, game_manager as gm +from osrlib.adventure import Adventure, AdventureAlreadyStartedError +from osrlib.character_classes import CharacterClassType + + +class PartyAtCapacityError(Exception): + """Raised when attempting to add a player character to a party that already has the maximum number of members.""" + + pass + + +class CharacterAlreadyInPartyError(Exception): + """Raised when attempting to add a player character to a party that already has that character as a member. + + Example: + Before trying to add a character to a party, check whether the character is already in the party by using the in + operator: + + .. code-block:: python + if not party.is_member(some_new_character): + party.add_character(some_new_character) + """ + + pass + + +class CharacterNotInPartyError(Exception): + """Raised when attempting to remove a player character from a party that does not have the character as a member. + + Example: + Before trying to remove a character from a party, check whether the character is in the party by using the ``in`` + operator: + + .. code-block:: python + if character in party: + party.remove_character(character) + """ + + pass + + +class PartyInStartedAdventureError(Exception): + """Raised when attempting to modify a party's roster when the party is in an adventure that's already been started.""" + + pass + + +class Party: + """Manages a collection of player characters (PCs) that comprise an adventuring party. + + If the party has been added to an adventure and the adventure has been started, you can't modify the party roster by + adding or removing characters. + + Args: + name (str): The name of the party. + max_party_members (int): The maximum number of characters allowed in the party. Defaults to 6. + characters (List[PlayerCharacter]): The characters in the party. Defaults to an empty list. + + Attributes: + name (str): The name of the party. + max_party_members (int): The maximum number of characters allowed in the party. + characters (List[PlayerCharacter]): The characters in the party. + active_character (PlayerCharacter): The currently active, or selected, character in the party. + is_adventuring (bool): Whether the party has been added to an Adventure that has been started. + current_adventure (Adventure): The adventure the party is currently in, or None if the party is not in an + adventure. + """ + + def __init__( + self, + name: str, + max_party_members: int = 6, + characters: List[player_character.PlayerCharacter] = None, + ): + """Initialize a new Party instance.""" + self.name = name + self.max_party_members = max_party_members + self.characters = characters if characters is not None else [] + self.active_character = None + self.current_adventure = None + + def __str__(self): + """Get a string representation of the Party instance. + + Returns: + str: A string representation of the Party instance. + """ + return f"Party: {self.name}" + + @property + def num_characters(self) -> int: + """Get the number of characters in the party. + + Returns: + int: The number of characters in the party. + """ + return len(self.characters) + + @property + def is_adventuring(self) -> bool: + """Returns True if the party has been added to an adventure that has been started. + + Returns: + bool: True if the party has been added to an adventure that has been started. + """ + return self.current_adventure is not None and self.current_adventure.is_started + + @property + def is_alive(self) -> bool: + """Returns True if any character in the party is alive. + + Returns: + bool: True if any character in the party is alive. + """ + return any(character.is_alive for character in self.characters) + + def create_character( + self, name: str, character_class: CharacterClassType, level: int = 1 + ) -> player_character.PlayerCharacter: + """Initialize a new character, add them to the party, set as the active character for the party, and return the + character. + + Example: + Create a new character and add them to the party: + + .. code-block:: python + party.create_character("Sckricko", character_classes.CharacterClassType.FIGHTER, 1) + + Args: + name (str): The name of the character. + character_class (character_classes.CharacterClassType): The character's class. + level (int): The character's level. Defaults to 1. + + Returns: + player_character.PlayerCharacter: The character that was created and added to the party. + + Raises: + PartyInStartedAdventureError: If the party has been added to an adventure that has already been started. + """ + if self.is_adventuring: + raise PartyInStartedAdventureError( + f"Can't create a new character in the party because the party is already adventuring." + ) + + character = player_character.PlayerCharacter(name, character_class, level) + self.add_character(character) + return character + + def add_character( + self, + character: player_character.PlayerCharacter, + set_as_active_character: bool = True, + ): + """Add a character to the party. + + A character can be added to a party only once, and a party has a maximum number of characters. + + Example: + Add a character to a party and allow them to be set as the active character: + + .. code-block:: python + fighter = player_character.PlayerCharacter("Sckricko", character_classes.CharacterClassType.FIGHTER, 1) + thief = player_character.PlayerCharacter("Slick", character_classes.CharacterClassType.THIEF, 1) + party.add_character(fighter) # sets the character as active for the party character by default + party.add_character(thief, set_as_active_character=False) # don't set the character as active for the + # party + if party.active_character == fighter: + print(f"Character '{character.name}' is the active character in the party.") + + Args: + character (PlayerCharacter): The PC to add to the party. + + Raises: + CharacterAlreadyInPartyError: If the character is already in the party. + PartyAtCapacityError: If the party already has the maximum number of characters. + + Returns: + PlayerCharacter: The character that was added to the party. + """ + if self.is_adventuring: + raise PartyInStartedAdventureError( + f"Can't add '{character.name}' to the party because the party is already adventuring." + ) + + if len(self.characters) >= self.max_party_members: + raise PartyAtCapacityError( + f"Party cannot have more than {self.max_party_members} characters." + ) + + if character in self.characters: + raise CharacterAlreadyInPartyError( + f"Character '{character.name}' already in party." + ) + + if character.name in (character.name for character in self.characters): + raise CharacterAlreadyInPartyError( + f"A character with that name ('{character.name}') is already in party." + ) + + gm.logger.info(f"Adding '{character.name}' to party '{self.name}'...") + self.characters.append(character) + + if set_as_active_character: + gm.logger.info( + f"Setting '{character.name}' as the active character in party '{self.name}'..." + ) + self.set_active_character(character) + + return character + + def is_member(self, character: player_character.PlayerCharacter) -> bool: + """Returns True if the character is in the party. + + Example: + + Check whether a character is in the party: + + .. code-block:: python + if party.is_member(some_player_character): + print(f"{some_player_character.name} is in the party.") + else: + print(f"{some_player_character.name} is not in the party.") + + Args: + character (player_character.PlayerCharacter): The character to check for. + + Returns: + bool: True if the character is in the party, False otherwise. + """ + return character in self.characters + + def set_active_character(self, character: player_character.PlayerCharacter): + """Sets the given character as the active, or "selected," character in the party. + + The character must be a member of the party before you can set them as the active character. + + Args: + character (player_character.PlayerCharacter): The party member to set as the active or currently selected character + + Raises: + CharacterNotInPartyError: Raised if the character is not in the party. + """ + if self.is_member(character): + self.active_character = character + gm.logger.info( + f"Set '{character.name}' as the active character in the party." + ) + else: + raise CharacterNotInPartyError( + f"Character '{character.name}' not in party." + ) + + def remove_character(self, character: player_character.PlayerCharacter): + """Removes a character from the party. + + Example: + .. code-block:: python + try: + party.remove_character(character) + except CharacterNotInPartyError: + print(f"Character '{character.name}' wasn't in the party and thus be removed from it.") + + Args: + character (player_character.PlayerCharacter): The PC to remove from the party. + """ + if self.is_member(character): + self.characters.remove(character) + else: + raise CharacterNotInPartyError( + f"Character '{character.name}' not in party." + ) + + def get_character_by_name(self, name: str) -> player_character.PlayerCharacter: + """Get a character from the party by name or None if the character is not in the party. + + Example: + + .. code-block:: python + character = party.get_character_by_name("Sckricko") + if character is not None: + print(f"Character '{character.name}' has {character.hit_points} hit points.") + + Args: + name (str): The name of the character to return. + + Returns: + player_character.PlayerCharacter: The character with the given name or None if the character is not in the party. + """ + return next( + (character for character in self.characters if character.name == name), + None, + ) + + def get_character_by_index(self, index: int) -> player_character.PlayerCharacter: + """Get a character from the party by index, or None if there's no character at that index. + + Example: + + .. code-block:: python + if len(party.characters) > 0: + # Get the first character in the party + first_character = party.get_character_by_index(0) + + # Get the last character in the party + last_character = party.get_character_by_index(-1) + + Args: + index (int): The index of the character to return. + + Returns: + player_character.PlayerCharacter: The character with the given index, or None if the index is out of range. + """ + try: + return self.characters[index] + except IndexError: + return None + + def get_character_index(self, character: player_character.PlayerCharacter) -> int: + """Get the index of a character in the party. + + Example: + + Get the index of a character in the party without checking whether the character is in the party (not + recommended): + + .. code-block:: python + character = party.get_character_by_name("Sckricko") + if party.is_member(character): + index = party.get_character_index(character) + print(f"Character '{character.name}' is number {index + 1} in the party's marching order.") + + Args: + character (player_character.PlayerCharacter): The character to get the index of + + Returns: + int: The index of the character in the party. + + Raises: + CharacterNotInPartyError: If the character is not in the party. + """ + if not self.is_member(character): + raise CharacterNotInPartyError( + f"Character '{character.name}' not in party." + ) + + return self.characters.index(character) + + def move_character_to_index( + self, character: player_character.PlayerCharacter, index: int + ): + """Moves a character to a new slot in the in party's marching order. + + Use this method to adjust the marching order of the party. + + Example: + + .. code-block:: python + # Move a character from fourth in line (index 3) to the front of the party at index 0. + character = party.get_character_by_name("Sckricko") + if party.is_member(character): + party.move_character_to_index(character, 0) + + Args: + character (player_character.PlayerCharacter): The character to move. + index (int): The index to move the character to. + + Raises: + CharacterNotInPartyError: If the character is not in the party. + IndexError: If the index is out of range. + """ + if not self.is_member(character): + raise CharacterNotInPartyError( + f"Can't move '{character.name}' because they're not in the party." + ) + + if index >= len(self.characters): + raise IndexError( + f"Can't move '{character.name}' to index {index} because it's out of range." + ) + + # Don't move the character if they're already at the index. + if index == self.get_character_index(character): + return + + self.characters.remove(character) + self.characters.insert(index, character) + + def clear_party(self): + """Removes all characters from the party.""" + self.characters.clear() + + def start_adventure(self, adventure: Adventure): + """Add the party to an adventure and starts the adventure. + + Once the adventure has started, you can't add or remove the adventuring party, and the party can't be added to + any other adventures. + + Args: + adventure (Adventure): The adventure to start. + + Raises: + AdventureAlreadyStartedError: If the adventure has already been started. + """ + if adventure.is_started: + raise AdventureAlreadyStartedError( + f"Adventure '{adventure.name}' has already been started." + ) + + adventure.start_adventure(self) + self.current_adventure = adventure + gm.logger.info( + f"Started adventure '{adventure.name}' with party '{self.name}'." + ) + + +def get_default_party(): # pragma: no cover + """Get a party of six (6) first-level characters: a Fighter, Elf, Dwarf, Thief, Halfling, and Magic User. + + Returns: + Party: A party with six (6) player characters at first level (zero experience points). + """ + party = Party("Six Player Characters") + party.create_character("Sckricko", CharacterClassType.FIGHTER, 1) + party.create_character("Mazpar", CharacterClassType.ELF, 1) + party.create_character("Blarg The Destructor", CharacterClassType.DWARF, 1) + party.create_character("Slick", CharacterClassType.THIEF, 1) + party.create_character("Dimp Beefeeder", CharacterClassType.HALFLING, 1) + party.create_character("Merlin", CharacterClassType.MAGIC_USER, 1) + return party diff --git a/osrlib/osrlib/player_character.py b/osrlib/osrlib/player_character.py new file mode 100644 index 0000000..d2e31ed --- /dev/null +++ b/osrlib/osrlib/player_character.py @@ -0,0 +1,161 @@ +"""This module contains the PlayerCharacter class.""" +from osrlib.ability import ( + AbilityType, + Charisma, + Constitution, + Dexterity, + Intelligence, + ModifierType, + Strength, + Wisdom, +) +from osrlib import ( + dice_roller, + CharacterClass, + CharacterClassType, + ClassLevel, + Inventory, + Item, +) + + +class PlayerCharacter: + """Represents a player character (PC) in the game. + + Attributes: + name (str): The name of the character. + abilities (dict): A dictionary of the character's abilities. + character_class (CharacterClass): The character's class. + inventory (Inventory): The character's inventory. + """ + + def __init__( + self, + name: str, + character_class_type: CharacterClassType, + level: int = 1, + ): + """Initialize a new PlayerCharacter (PC) instance.""" + self.name = name + self.abilities = {} + self.roll_abilities() + self.character_class = None + self.set_character_class(character_class_type, level) + + self.inventory = Inventory(self) + + def __str__(self): + """Get a string representation of the PlayerCharacter instance. + + Returns: + str: A string representation of the PlayerCharacter instance. + """ + ability_str = ", ".join( + f"{ability.name}: {attr.score:>2}" + for ability, attr in self.abilities.items() + ) + return ( + f"Name: {self.name}, " + f"Class: {self.character_class.class_type.name}, " + f"Level: {self.character_class.current_level.level_num:2}, " + f"HP: {self.character_class.hp:3}, " + f"AC: {self.armor_class:2}, " + f"XP: {self.character_class.xp:7}, " + f"{ability_str}" + ) + + @property + def is_alive(self) -> bool: + """Returns True if the character is alive. + + The character is considered alive if their hit points are greater than 0. + + Returns: + bool: True if the character is alive (hit points > 0), False otherwise. + """ + return self.hit_points > 0 + + @property + def armor_class(self): + """Get the armor class of the character.""" + armor_class = 9 + armor_class += self.abilities[AbilityType.DEXTERITY].modifiers[ModifierType.AC] + armor_class += sum( + armor_item.ac_modifier + for armor_item in self.inventory.armor + if armor_item.is_equipped + ) + return armor_class + + def get_ability_roll(self): + """Rolls a 4d6 and returns the sum of the three highest rolls.""" + roll = dice_roller.roll_dice("4d6", drop_lowest=True) + return roll.total + + def set_character_class( + self, character_class_type: CharacterClassType, level: int = 1 + ): + """Sets the character class of the character.""" + # TODO: Add validation to prevent setting a class if the class' ability score prerequisites aren't met + self.character_class = CharacterClass( + character_class_type, + level, + self.abilities[AbilityType.CONSTITUTION].modifiers[ModifierType.HP], + ) + return self.character_class + + def grant_xp(self, xp: int) -> ClassLevel: + """Grants XP to the character, taking into account their Constitution ability modifier, if any.""" + self.character_class.xp += xp + try: + # Need to pass the character's Constitution modifier all the way down to the roll_hp method + return self.character_class.level_up( + self.abilities[AbilityType.CONSTITUTION].modifiers[ModifierType.HP] + ) + except ValueError as e: + print(e) + + def roll_abilities(self): + """Rolls the ability scores of the character.""" + self.abilities = {} + for ability_class in [ + Strength, + Intelligence, + Wisdom, + Dexterity, + Constitution, + Charisma, + ]: + roll = self.get_ability_roll() + ability_instance = ability_class(roll) + self.abilities[ability_instance.ability_type] = ability_instance + + def roll_hp(self) -> dice_roller.DiceRoll: + """Rolls the character's hit points, taking into account their Constitution modifier, if any. + + The total value of the roll with modifier can be negative after if the roll was low and the character has a + negative Constitution modifier. You should clamp the value to 1 before applying it to the character's HP. + + Returns: + DiceRoll: The result of the HP roll. Value with modifiers can be negative. + """ + hp_modifier = self.abilities.get(AbilityType.CONSTITUTION).modifiers[ + ModifierType.HP + ] + + return self.character_class.roll_hp(hp_modifier) + + def add_item_to_inventory(self, item: Item): + return self.inventory.add_item(item) + + def get_item_from_inventory(self, item: Item): + return self.inventory.get_item(item) + + def remove_item_from_inventory(self, item: Item): + return self.inventory.remove_item(item) + + def equip_item(self, item: Item): + return self.inventory.equip_item(item) + + def unequip_item(self, item: Item): + return self.inventory.unequip_item(item) diff --git a/osrlib/osrlib/utils.py b/osrlib/osrlib/utils.py new file mode 100644 index 0000000..05e260a --- /dev/null +++ b/osrlib/osrlib/utils.py @@ -0,0 +1,21 @@ +from osrlib import ModifierType + + +def format_modifiers(modifiers: dict[ModifierType, int]) -> str: + """Get a string representation of the given modifiers. + + The formatted modifier string returned by this function is a comma-delimited list of the given modifiers. Each + modifier includes its name and value. The value string includes its polarity if it's positive or negative. + + Example: + + >>> # This should return "Strength: +1, Dexterity: -1" + >>> format_modifiers({ModifierType.STRENGTH: 1, ModifierType.DEXTERITY: -1}) + + Args: + modifiers: A dictionary of modifiers to format. + + Returns: + A comma-delimited string representation of the modifiers. + """ + return ", ".join(f"{mod.value}: {'+' if val > 0 else ''}{val}" for mod, val in modifiers.items()) diff --git a/osrlib/poetry.lock b/osrlib/poetry.lock new file mode 100644 index 0000000..b1bfecd --- /dev/null +++ b/osrlib/poetry.lock @@ -0,0 +1,920 @@ +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. + +[[package]] +name = "babel" +version = "2.13.0" +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"}, +] + +[package.extras] +dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] + +[[package]] +name = "black" +version = "23.9.1" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.8" +files = [ + {file = "black-23.9.1-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:d6bc09188020c9ac2555a498949401ab35bb6bf76d4e0f8ee251694664df6301"}, + {file = "black-23.9.1-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:13ef033794029b85dfea8032c9d3b92b42b526f1ff4bf13b2182ce4e917f5100"}, + {file = "black-23.9.1-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:75a2dc41b183d4872d3a500d2b9c9016e67ed95738a3624f4751a0cb4818fe71"}, + {file = "black-23.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13a2e4a93bb8ca74a749b6974925c27219bb3df4d42fc45e948a5d9feb5122b7"}, + {file = "black-23.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:adc3e4442eef57f99b5590b245a328aad19c99552e0bdc7f0b04db6656debd80"}, + {file = "black-23.9.1-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:8431445bf62d2a914b541da7ab3e2b4f3bc052d2ccbf157ebad18ea126efb91f"}, + {file = "black-23.9.1-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:8fc1ddcf83f996247505db6b715294eba56ea9372e107fd54963c7553f2b6dfe"}, + {file = "black-23.9.1-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:7d30ec46de88091e4316b17ae58bbbfc12b2de05e069030f6b747dfc649ad186"}, + {file = "black-23.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031e8c69f3d3b09e1aa471a926a1eeb0b9071f80b17689a655f7885ac9325a6f"}, + {file = "black-23.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:538efb451cd50f43aba394e9ec7ad55a37598faae3348d723b59ea8e91616300"}, + {file = "black-23.9.1-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:638619a559280de0c2aa4d76f504891c9860bb8fa214267358f0a20f27c12948"}, + {file = "black-23.9.1-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:a732b82747235e0542c03bf352c126052c0fbc458d8a239a94701175b17d4855"}, + {file = "black-23.9.1-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:cf3a4d00e4cdb6734b64bf23cd4341421e8953615cba6b3670453737a72ec204"}, + {file = "black-23.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf99f3de8b3273a8317681d8194ea222f10e0133a24a7548c73ce44ea1679377"}, + {file = "black-23.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:14f04c990259576acd093871e7e9b14918eb28f1866f91968ff5524293f9c573"}, + {file = "black-23.9.1-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:c619f063c2d68f19b2d7270f4cf3192cb81c9ec5bc5ba02df91471d0b88c4c5c"}, + {file = "black-23.9.1-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:6a3b50e4b93f43b34a9d3ef00d9b6728b4a722c997c99ab09102fd5efdb88325"}, + {file = "black-23.9.1-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:c46767e8df1b7beefb0899c4a95fb43058fa8500b6db144f4ff3ca38eb2f6393"}, + {file = "black-23.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50254ebfa56aa46a9fdd5d651f9637485068a1adf42270148cd101cdf56e0ad9"}, + {file = "black-23.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:403397c033adbc45c2bd41747da1f7fc7eaa44efbee256b53842470d4ac5a70f"}, + {file = "black-23.9.1-py3-none-any.whl", hash = "sha256:6ccd59584cc834b6d127628713e4b6b968e5f79572da66284532525a042549f9"}, + {file = "black-23.9.1.tar.gz", hash = "sha256:24b6b3ff5c6d9ea08a8888f6977eae858e1f340d7260cf56d70a49823236b62d"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "certifi" +version = "2023.7.22" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, + {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.0" +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"}, +] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "flake8" +version = "6.1.0" +description = "the modular source code checker: pep8 pyflakes and co" +optional = false +python-versions = ">=3.8.1" +files = [ + {file = "flake8-6.1.0-py2.py3-none-any.whl", hash = "sha256:ffdfce58ea94c6580c77888a86506937f9a1a227dfcd15f245d694ae20a6b6e5"}, + {file = "flake8-6.1.0.tar.gz", hash = "sha256:d5b3857f07c030bdb5bf41c7f53799571d75c4491748a3adcd47de929e34cd23"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.11.0,<2.12.0" +pyflakes = ">=3.1.0,<3.2.0" + +[[package]] +name = "flake8-docstrings" +version = "1.7.0" +description = "Extension for flake8 which uses pydocstyle to check docstrings" +optional = false +python-versions = ">=3.7" +files = [ + {file = "flake8_docstrings-1.7.0-py2.py3-none-any.whl", hash = "sha256:51f2344026da083fc084166a9353f5082b01f72901df422f74b4d953ae88ac75"}, + {file = "flake8_docstrings-1.7.0.tar.gz", hash = "sha256:4c8cc748dc16e6869728699e5d0d685da9a10b0ea718e090b1ba088e67a941af"}, +] + +[package.dependencies] +flake8 = ">=3" +pydocstyle = ">=2.1" + +[[package]] +name = "ghp-import" +version = "2.1.0" +description = "Copy your docs directly to the gh-pages branch." +optional = false +python-versions = "*" +files = [ + {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, + {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, +] + +[package.dependencies] +python-dateutil = ">=2.8.1" + +[package.extras] +dev = ["flake8", "markdown", "twine", "wheel"] + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "jinja2" +version = "3.1.2" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "markdown" +version = "3.5" +description = "Python implementation of John Gruber's Markdown." +optional = false +python-versions = ">=3.8" +files = [ + {file = "Markdown-3.5-py3-none-any.whl", hash = "sha256:4afb124395ce5fc34e6d9886dab977fd9ae987fc6e85689f08278cf0c69d4bf3"}, + {file = "Markdown-3.5.tar.gz", hash = "sha256:a807eb2e4778d9156c8f07876c6e4d50b5494c5665c4834f67b06459dfd877b3"}, +] + +[package.extras] +docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] +testing = ["coverage", "pyyaml"] + +[[package]] +name = "markupsafe" +version = "2.1.3" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, + {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = ">=3.6" +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +description = "A deep merge function for 🐍." +optional = false +python-versions = ">=3.6" +files = [ + {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, + {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, +] + +[[package]] +name = "mkdocs" +version = "1.5.3" +description = "Project documentation with Markdown." +optional = false +python-versions = ">=3.7" +files = [ + {file = "mkdocs-1.5.3-py3-none-any.whl", hash = "sha256:3b3a78e736b31158d64dbb2f8ba29bd46a379d0c6e324c2246c3bc3d2189cfc1"}, + {file = "mkdocs-1.5.3.tar.gz", hash = "sha256:eb7c99214dcb945313ba30426c2451b735992c73c2e10838f76d09e39ff4d0e2"}, +] + +[package.dependencies] +click = ">=7.0" +colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} +ghp-import = ">=1.0" +jinja2 = ">=2.11.1" +markdown = ">=3.2.1" +markupsafe = ">=2.0.1" +mergedeep = ">=1.3.4" +packaging = ">=20.5" +pathspec = ">=0.11.1" +platformdirs = ">=2.2.0" +pyyaml = ">=5.1" +pyyaml-env-tag = ">=0.1" +watchdog = ">=2.0" + +[package.extras] +i18n = ["babel (>=2.9.0)"] +min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.3)", "jinja2 (==2.11.1)", "markdown (==3.2.1)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "packaging (==20.5)", "pathspec (==0.11.1)", "platformdirs (==2.2.0)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "typing-extensions (==3.10)", "watchdog (==2.0)"] + +[[package]] +name = "mkdocs-material" +version = "9.4.6" +description = "Documentation that simply works" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_material-9.4.6-py3-none-any.whl", hash = "sha256:78802035d5768a78139c84ad7dce0c6493e8f7dc4861727d36ed91d1520a54da"}, + {file = "mkdocs_material-9.4.6.tar.gz", hash = "sha256:09665e60df7ee9e5ff3a54af173f6d45be718b1ee7dd962bcff3102b81fb0c14"}, +] + +[package.dependencies] +babel = ">=2.10,<3.0" +colorama = ">=0.4,<1.0" +jinja2 = ">=3.0,<4.0" +markdown = ">=3.2,<4.0" +mkdocs = ">=1.5.3,<2.0" +mkdocs-material-extensions = ">=1.2,<2.0" +paginate = ">=0.5,<1.0" +pygments = ">=2.16,<3.0" +pymdown-extensions = ">=10.2,<11.0" +regex = ">=2022.4" +requests = ">=2.26,<3.0" + +[package.extras] +git = ["mkdocs-git-committers-plugin-2 (>=1.1,<2.0)", "mkdocs-git-revision-date-localized-plugin (>=1.2,<2.0)"] +imaging = ["cairosvg (>=2.6,<3.0)", "pillow (>=9.4,<10.0)"] +recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2.0)", "mkdocs-rss-plugin (>=1.6,<2.0)"] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.2" +description = "Extension pack for Python Markdown and MkDocs Material." +optional = false +python-versions = ">=3.7" +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"}, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "packaging" +version = "23.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + +[[package]] +name = "paginate" +version = "0.5.6" +description = "Divides large result sets into pages for easier browsing" +optional = false +python-versions = "*" +files = [ + {file = "paginate-0.5.6.tar.gz", hash = "sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d"}, +] + +[[package]] +name = "pathspec" +version = "0.11.2" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, + {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, +] + +[[package]] +name = "platformdirs" +version = "3.11.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.7" +files = [ + {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"}, + {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"}, +] + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] + +[[package]] +name = "pluggy" +version = "1.3.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, + {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pycodestyle" +version = "2.11.1" +description = "Python style guide checker" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, + {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, +] + +[[package]] +name = "pydocstyle" +version = "6.3.0" +description = "Python docstring style checker" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pydocstyle-6.3.0-py3-none-any.whl", hash = "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019"}, + {file = "pydocstyle-6.3.0.tar.gz", hash = "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1"}, +] + +[package.dependencies] +snowballstemmer = ">=2.2.0" + +[package.extras] +toml = ["tomli (>=1.2.3)"] + +[[package]] +name = "pyflakes" +version = "3.1.0" +description = "passive checker of Python programs" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyflakes-3.1.0-py2.py3-none-any.whl", hash = "sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774"}, + {file = "pyflakes-3.1.0.tar.gz", hash = "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc"}, +] + +[[package]] +name = "pygments" +version = "2.16.1" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"}, + {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"}, +] + +[package.extras] +plugins = ["importlib-metadata"] + +[[package]] +name = "pymdown-extensions" +version = "10.3" +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"}, +] + +[package.dependencies] +markdown = ">=3.2" +pyyaml = "*" + +[package.extras] +extra = ["pygments (>=2.12)"] + +[[package]] +name = "pytest" +version = "7.4.2" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"}, + {file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "pyyaml-env-tag" +version = "0.1" +description = "A custom YAML tag for referencing environment variables in YAML files. " +optional = false +python-versions = ">=3.6" +files = [ + {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, + {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, +] + +[package.dependencies] +pyyaml = "*" + +[[package]] +name = "regex" +version = "2023.10.3" +description = "Alternative regular expression module, to replace re." +optional = false +python-versions = ">=3.7" +files = [ + {file = "regex-2023.10.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4c34d4f73ea738223a094d8e0ffd6d2c1a1b4c175da34d6b0de3d8d69bee6bcc"}, + {file = "regex-2023.10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a8f4e49fc3ce020f65411432183e6775f24e02dff617281094ba6ab079ef0915"}, + {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cd1bccf99d3ef1ab6ba835308ad85be040e6a11b0977ef7ea8c8005f01a3c29"}, + {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:81dce2ddc9f6e8f543d94b05d56e70d03a0774d32f6cca53e978dc01e4fc75b8"}, + {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c6b4d23c04831e3ab61717a707a5d763b300213db49ca680edf8bf13ab5d91b"}, + {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c15ad0aee158a15e17e0495e1e18741573d04eb6da06d8b84af726cfc1ed02ee"}, + {file = "regex-2023.10.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6239d4e2e0b52c8bd38c51b760cd870069f0bdf99700a62cd509d7a031749a55"}, + {file = "regex-2023.10.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4a8bf76e3182797c6b1afa5b822d1d5802ff30284abe4599e1247be4fd6b03be"}, + {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9c727bbcf0065cbb20f39d2b4f932f8fa1631c3e01fcedc979bd4f51fe051c5"}, + {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3ccf2716add72f80714b9a63899b67fa711b654be3fcdd34fa391d2d274ce767"}, + {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:107ac60d1bfdc3edb53be75e2a52aff7481b92817cfdddd9b4519ccf0e54a6ff"}, + {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:00ba3c9818e33f1fa974693fb55d24cdc8ebafcb2e4207680669d8f8d7cca79a"}, + {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f0a47efb1dbef13af9c9a54a94a0b814902e547b7f21acb29434504d18f36e3a"}, + {file = "regex-2023.10.3-cp310-cp310-win32.whl", hash = "sha256:36362386b813fa6c9146da6149a001b7bd063dabc4d49522a1f7aa65b725c7ec"}, + {file = "regex-2023.10.3-cp310-cp310-win_amd64.whl", hash = "sha256:c65a3b5330b54103e7d21cac3f6bf3900d46f6d50138d73343d9e5b2900b2353"}, + {file = "regex-2023.10.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:90a79bce019c442604662d17bf69df99090e24cdc6ad95b18b6725c2988a490e"}, + {file = "regex-2023.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c7964c2183c3e6cce3f497e3a9f49d182e969f2dc3aeeadfa18945ff7bdd7051"}, + {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ef80829117a8061f974b2fda8ec799717242353bff55f8a29411794d635d964"}, + {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5addc9d0209a9afca5fc070f93b726bf7003bd63a427f65ef797a931782e7edc"}, + {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c148bec483cc4b421562b4bcedb8e28a3b84fcc8f0aa4418e10898f3c2c0eb9b"}, + {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d1f21af4c1539051049796a0f50aa342f9a27cde57318f2fc41ed50b0dbc4ac"}, + {file = "regex-2023.10.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b9ac09853b2a3e0d0082104036579809679e7715671cfbf89d83c1cb2a30f58"}, + {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ebedc192abbc7fd13c5ee800e83a6df252bec691eb2c4bedc9f8b2e2903f5e2a"}, + {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d8a993c0a0ffd5f2d3bda23d0cd75e7086736f8f8268de8a82fbc4bd0ac6791e"}, + {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:be6b7b8d42d3090b6c80793524fa66c57ad7ee3fe9722b258aec6d0672543fd0"}, + {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4023e2efc35a30e66e938de5aef42b520c20e7eda7bb5fb12c35e5d09a4c43f6"}, + {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0d47840dc05e0ba04fe2e26f15126de7c755496d5a8aae4a08bda4dd8d646c54"}, + {file = "regex-2023.10.3-cp311-cp311-win32.whl", hash = "sha256:9145f092b5d1977ec8c0ab46e7b3381b2fd069957b9862a43bd383e5c01d18c2"}, + {file = "regex-2023.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:b6104f9a46bd8743e4f738afef69b153c4b8b592d35ae46db07fc28ae3d5fb7c"}, + {file = "regex-2023.10.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:bff507ae210371d4b1fe316d03433ac099f184d570a1a611e541923f78f05037"}, + {file = "regex-2023.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:be5e22bbb67924dea15039c3282fa4cc6cdfbe0cbbd1c0515f9223186fc2ec5f"}, + {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a992f702c9be9c72fa46f01ca6e18d131906a7180950958f766c2aa294d4b41"}, + {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7434a61b158be563c1362d9071358f8ab91b8d928728cd2882af060481244c9e"}, + {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2169b2dcabf4e608416f7f9468737583ce5f0a6e8677c4efbf795ce81109d7c"}, + {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9e908ef5889cda4de038892b9accc36d33d72fb3e12c747e2799a0e806ec841"}, + {file = "regex-2023.10.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12bd4bc2c632742c7ce20db48e0d99afdc05e03f0b4c1af90542e05b809a03d9"}, + {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bc72c231f5449d86d6c7d9cc7cd819b6eb30134bb770b8cfdc0765e48ef9c420"}, + {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bce8814b076f0ce5766dc87d5a056b0e9437b8e0cd351b9a6c4e1134a7dfbda9"}, + {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:ba7cd6dc4d585ea544c1412019921570ebd8a597fabf475acc4528210d7c4a6f"}, + {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b0c7d2f698e83f15228ba41c135501cfe7d5740181d5903e250e47f617eb4292"}, + {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5a8f91c64f390ecee09ff793319f30a0f32492e99f5dc1c72bc361f23ccd0a9a"}, + {file = "regex-2023.10.3-cp312-cp312-win32.whl", hash = "sha256:ad08a69728ff3c79866d729b095872afe1e0557251da4abb2c5faff15a91d19a"}, + {file = "regex-2023.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:39cdf8d141d6d44e8d5a12a8569d5a227f645c87df4f92179bd06e2e2705e76b"}, + {file = "regex-2023.10.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4a3ee019a9befe84fa3e917a2dd378807e423d013377a884c1970a3c2792d293"}, + {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76066d7ff61ba6bf3cb5efe2428fc82aac91802844c022d849a1f0f53820502d"}, + {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe50b61bab1b1ec260fa7cd91106fa9fece57e6beba05630afe27c71259c59b"}, + {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fd88f373cb71e6b59b7fa597e47e518282455c2734fd4306a05ca219a1991b0"}, + {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3ab05a182c7937fb374f7e946f04fb23a0c0699c0450e9fb02ef567412d2fa3"}, + {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dac37cf08fcf2094159922edc7a2784cfcc5c70f8354469f79ed085f0328ebdf"}, + {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e54ddd0bb8fb626aa1f9ba7b36629564544954fff9669b15da3610c22b9a0991"}, + {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3367007ad1951fde612bf65b0dffc8fd681a4ab98ac86957d16491400d661302"}, + {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:16f8740eb6dbacc7113e3097b0a36065a02e37b47c936b551805d40340fb9971"}, + {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:f4f2ca6df64cbdd27f27b34f35adb640b5d2d77264228554e68deda54456eb11"}, + {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:39807cbcbe406efca2a233884e169d056c35aa7e9f343d4e78665246a332f597"}, + {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7eece6fbd3eae4a92d7c748ae825cbc1ee41a89bb1c3db05b5578ed3cfcfd7cb"}, + {file = "regex-2023.10.3-cp37-cp37m-win32.whl", hash = "sha256:ce615c92d90df8373d9e13acddd154152645c0dc060871abf6bd43809673d20a"}, + {file = "regex-2023.10.3-cp37-cp37m-win_amd64.whl", hash = "sha256:0f649fa32fe734c4abdfd4edbb8381c74abf5f34bc0b3271ce687b23729299ed"}, + {file = "regex-2023.10.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9b98b7681a9437262947f41c7fac567c7e1f6eddd94b0483596d320092004533"}, + {file = "regex-2023.10.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:91dc1d531f80c862441d7b66c4505cd6ea9d312f01fb2f4654f40c6fdf5cc37a"}, + {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82fcc1f1cc3ff1ab8a57ba619b149b907072e750815c5ba63e7aa2e1163384a4"}, + {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7979b834ec7a33aafae34a90aad9f914c41fd6eaa8474e66953f3f6f7cbd4368"}, + {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef71561f82a89af6cfcbee47f0fabfdb6e63788a9258e913955d89fdd96902ab"}, + {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd829712de97753367153ed84f2de752b86cd1f7a88b55a3a775eb52eafe8a94"}, + {file = "regex-2023.10.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00e871d83a45eee2f8688d7e6849609c2ca2a04a6d48fba3dff4deef35d14f07"}, + {file = "regex-2023.10.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:706e7b739fdd17cb89e1fbf712d9dc21311fc2333f6d435eac2d4ee81985098c"}, + {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cc3f1c053b73f20c7ad88b0d1d23be7e7b3901229ce89f5000a8399746a6e039"}, + {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6f85739e80d13644b981a88f529d79c5bdf646b460ba190bffcaf6d57b2a9863"}, + {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:741ba2f511cc9626b7561a440f87d658aabb3d6b744a86a3c025f866b4d19e7f"}, + {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e77c90ab5997e85901da85131fd36acd0ed2221368199b65f0d11bca44549711"}, + {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:979c24cbefaf2420c4e377ecd1f165ea08cc3d1fbb44bdc51bccbbf7c66a2cb4"}, + {file = "regex-2023.10.3-cp38-cp38-win32.whl", hash = "sha256:58837f9d221744d4c92d2cf7201c6acd19623b50c643b56992cbd2b745485d3d"}, + {file = "regex-2023.10.3-cp38-cp38-win_amd64.whl", hash = "sha256:c55853684fe08d4897c37dfc5faeff70607a5f1806c8be148f1695be4a63414b"}, + {file = "regex-2023.10.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2c54e23836650bdf2c18222c87f6f840d4943944146ca479858404fedeb9f9af"}, + {file = "regex-2023.10.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:69c0771ca5653c7d4b65203cbfc5e66db9375f1078689459fe196fe08b7b4930"}, + {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ac965a998e1388e6ff2e9781f499ad1eaa41e962a40d11c7823c9952c77123e"}, + {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c0e8fae5b27caa34177bdfa5a960c46ff2f78ee2d45c6db15ae3f64ecadde14"}, + {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6c56c3d47da04f921b73ff9415fbaa939f684d47293f071aa9cbb13c94afc17d"}, + {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ef1e014eed78ab650bef9a6a9cbe50b052c0aebe553fb2881e0453717573f52"}, + {file = "regex-2023.10.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d29338556a59423d9ff7b6eb0cb89ead2b0875e08fe522f3e068b955c3e7b59b"}, + {file = "regex-2023.10.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9c6d0ced3c06d0f183b73d3c5920727268d2201aa0fe6d55c60d68c792ff3588"}, + {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:994645a46c6a740ee8ce8df7911d4aee458d9b1bc5639bc968226763d07f00fa"}, + {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:66e2fe786ef28da2b28e222c89502b2af984858091675044d93cb50e6f46d7af"}, + {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:11175910f62b2b8c055f2b089e0fedd694fe2be3941b3e2633653bc51064c528"}, + {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:06e9abc0e4c9ab4779c74ad99c3fc10d3967d03114449acc2c2762ad4472b8ca"}, + {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fb02e4257376ae25c6dd95a5aec377f9b18c09be6ebdefa7ad209b9137b73d48"}, + {file = "regex-2023.10.3-cp39-cp39-win32.whl", hash = "sha256:3b2c3502603fab52d7619b882c25a6850b766ebd1b18de3df23b2f939360e1bd"}, + {file = "regex-2023.10.3-cp39-cp39-win_amd64.whl", hash = "sha256:adbccd17dcaff65704c856bd29951c58a1bd4b2b0f8ad6b826dbd543fe740988"}, + {file = "regex-2023.10.3.tar.gz", hash = "sha256:3fef4f844d2290ee0ba57addcec17eec9e3df73f10a2748485dfd6a3a188cc0f"}, +] + +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "snowballstemmer" +version = "2.2.0" +description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +optional = false +python-versions = "*" +files = [ + {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, + {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, +] + +[[package]] +name = "tinydb" +version = "4.8.0" +description = "TinyDB is a tiny, document oriented database optimized for your happiness :)" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "tinydb-4.8.0-py3-none-any.whl", hash = "sha256:30c06d12383d7c332e404ca6a6103fb2b32cbf25712689648c39d9a6bd34bd3d"}, + {file = "tinydb-4.8.0.tar.gz", hash = "sha256:6dd686a9c5a75dfa9280088fd79a419aefe19cd7f4bd85eba203540ef856d564"}, +] + +[[package]] +name = "urllib3" +version = "2.0.6" +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"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "watchdog" +version = "3.0.0" +description = "Filesystem events monitoring" +optional = false +python-versions = ">=3.7" +files = [ + {file = "watchdog-3.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:336adfc6f5cc4e037d52db31194f7581ff744b67382eb6021c868322e32eef41"}, + {file = "watchdog-3.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a70a8dcde91be523c35b2bf96196edc5730edb347e374c7de7cd20c43ed95397"}, + {file = "watchdog-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:adfdeab2da79ea2f76f87eb42a3ab1966a5313e5a69a0213a3cc06ef692b0e96"}, + {file = "watchdog-3.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2b57a1e730af3156d13b7fdddfc23dea6487fceca29fc75c5a868beed29177ae"}, + {file = "watchdog-3.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7ade88d0d778b1b222adebcc0927428f883db07017618a5e684fd03b83342bd9"}, + {file = "watchdog-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7e447d172af52ad204d19982739aa2346245cc5ba6f579d16dac4bfec226d2e7"}, + {file = "watchdog-3.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9fac43a7466eb73e64a9940ac9ed6369baa39b3bf221ae23493a9ec4d0022674"}, + {file = "watchdog-3.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8ae9cda41fa114e28faf86cb137d751a17ffd0316d1c34ccf2235e8a84365c7f"}, + {file = "watchdog-3.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:25f70b4aa53bd743729c7475d7ec41093a580528b100e9a8c5b5efe8899592fc"}, + {file = "watchdog-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4f94069eb16657d2c6faada4624c39464f65c05606af50bb7902e036e3219be3"}, + {file = "watchdog-3.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7c5f84b5194c24dd573fa6472685b2a27cc5a17fe5f7b6fd40345378ca6812e3"}, + {file = "watchdog-3.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3aa7f6a12e831ddfe78cdd4f8996af9cf334fd6346531b16cec61c3b3c0d8da0"}, + {file = "watchdog-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:233b5817932685d39a7896b1090353fc8efc1ef99c9c054e46c8002561252fb8"}, + {file = "watchdog-3.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:13bbbb462ee42ec3c5723e1205be8ced776f05b100e4737518c67c8325cf6100"}, + {file = "watchdog-3.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8f3ceecd20d71067c7fd4c9e832d4e22584318983cabc013dbf3f70ea95de346"}, + {file = "watchdog-3.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c9d8c8ec7efb887333cf71e328e39cffbf771d8f8f95d308ea4125bf5f90ba64"}, + {file = "watchdog-3.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0e06ab8858a76e1219e68c7573dfeba9dd1c0219476c5a44d5333b01d7e1743a"}, + {file = "watchdog-3.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:d00e6be486affb5781468457b21a6cbe848c33ef43f9ea4a73b4882e5f188a44"}, + {file = "watchdog-3.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:c07253088265c363d1ddf4b3cdb808d59a0468ecd017770ed716991620b8f77a"}, + {file = "watchdog-3.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:5113334cf8cf0ac8cd45e1f8309a603291b614191c9add34d33075727a967709"}, + {file = "watchdog-3.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:51f90f73b4697bac9c9a78394c3acbbd331ccd3655c11be1a15ae6fe289a8c83"}, + {file = "watchdog-3.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:ba07e92756c97e3aca0912b5cbc4e5ad802f4557212788e72a72a47ff376950d"}, + {file = "watchdog-3.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:d429c2430c93b7903914e4db9a966c7f2b068dd2ebdd2fa9b9ce094c7d459f33"}, + {file = "watchdog-3.0.0-py3-none-win32.whl", hash = "sha256:3ed7c71a9dccfe838c2f0b6314ed0d9b22e77d268c67e015450a29036a81f60f"}, + {file = "watchdog-3.0.0-py3-none-win_amd64.whl", hash = "sha256:4c9956d27be0bb08fc5f30d9d0179a855436e655f046d288e2bcc11adfae893c"}, + {file = "watchdog-3.0.0-py3-none-win_ia64.whl", hash = "sha256:5d9f3a10e02d7371cd929b5d8f11e87d4bad890212ed3901f9b4d68767bee759"}, + {file = "watchdog-3.0.0.tar.gz", hash = "sha256:4d98a320595da7a7c5a18fc48cb633c2e73cda78f93cac2ef42d42bf609a33f9"}, +] + +[package.extras] +watchmedo = ["PyYAML (>=3.10)"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.11" +content-hash = "3dbf16153275ebdf85a3d2449023eec435ad33dad7c62e6ea0a5a9f462c36432" diff --git a/osrlib/pyproject.toml b/osrlib/pyproject.toml new file mode 100644 index 0000000..ff04a1a --- /dev/null +++ b/osrlib/pyproject.toml @@ -0,0 +1,22 @@ +[tool.poetry] +name = "osrlib" +version = "0.1.0" +description = "Turn-based dungeon-crawler game engine for OSR-style RPGs." +authors = ["Marsh Macy "] +license = "MIT" +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.11" +tinydb = "^4.8.0" + +[tool.poetry.group.dev.dependencies] +black = "^23.9.1" +flake8 = "^6.1.0" +flake8-docstrings = "^1.7.0" +pytest = "^7.4.2" +mkdocs-material = "^9.4.6" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/requirements.docs.txt b/requirements.docs.txt deleted file mode 100644 index 898468c..0000000 --- a/requirements.docs.txt +++ /dev/null @@ -1 +0,0 @@ -mkdocs-material \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 0ec2c1a..0000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -textual \ No newline at end of file diff --git a/tests/pytest.ini b/tests/pytest.ini new file mode 100644 index 0000000..edd3ad9 --- /dev/null +++ b/tests/pytest.ini @@ -0,0 +1,6 @@ +# pytest.ini +[pytest] +log_cli = true +log_cli_level = INFO +log_cli_format = %(asctime)s [%(levelname)s] %(message)s +log_cli_date_format = %Y-%m-%d %H:%M:%S diff --git a/tests/test_integration_saveload.py b/tests/test_integration_saveload.py new file mode 100644 index 0000000..e988866 --- /dev/null +++ b/tests/test_integration_saveload.py @@ -0,0 +1,364 @@ +import os +import pytest +from tinydb import Query, TinyDB + +from osrlib import PlayerCharacter, item, character_classes, game_manager as gm +from osrlib.ability import ( + Strength, + Intelligence, + Wisdom, + Dexterity, + Constitution, + Charisma, +) +from osrlib.combat import ModifierType + + +test_fighter = PlayerCharacter("Test Fighter", character_classes.CharacterClassType.FIGHTER) +test_elf = PlayerCharacter("Test Elf", character_classes.CharacterClassType.ELF) +test_magic_user = PlayerCharacter("Test Magic User", character_classes.CharacterClassType.MAGIC_USER) + + +@pytest.fixture +def db(): + db_path = "test_db.json" + full_path = os.path.abspath(db_path) + gm.logger.info(f"Creating TinyDB @ {full_path}") + db = TinyDB(db_path) + gm.logger.info(f"Created TinyDB: {db}") + yield db + gm.logger.info(f"Dropping tables from TinyDB: {db}") + db.drop_tables() + gm.logger.info(f"Closing TinyDB: {db}") + db.close() + + +def test_abilities_saveload(db): + table = db.table("abilities") + + # Create ability instances + str_ability = Strength(16) + int_ability = Intelligence(18) + wis_ability = Wisdom(12) + dex_ability = Dexterity(14) + con_ability = Constitution(5) + cha_ability = Charisma(18) + + # Generate dicts + str_dict = str_ability.to_dict() + int_dict = int_ability.to_dict() + wis_dict = wis_ability.to_dict() + dex_dict = dex_ability.to_dict() + con_dict = con_ability.to_dict() + cha_dict = cha_ability.to_dict() + + # Save to DB + str_id = table.insert(str_dict) + int_id = table.insert(int_dict) + wis_id = table.insert(wis_dict) + dex_id = table.insert(dex_dict) + con_id = table.insert(con_dict) + cha_id = table.insert(cha_dict) + + # Load from DB + str_loaded_dict = table.get(doc_id=str_id) + int_loaded_dict = table.get(doc_id=int_id) + wis_loaded_dict = table.get(doc_id=wis_id) + dex_loaded_dict = table.get(doc_id=dex_id) + con_loaded_dict = table.get(doc_id=con_id) + cha_loaded_dict = table.get(doc_id=cha_id) + + str_loaded = Strength.from_dict(str_loaded_dict) + int_loaded = Intelligence.from_dict(int_loaded_dict) + wis_loaded = Wisdom.from_dict(wis_loaded_dict) + dex_loaded = Dexterity.from_dict(dex_loaded_dict) + con_loaded = Constitution.from_dict(con_loaded_dict) + cha_loaded = Charisma.from_dict(cha_loaded_dict) + + # Assertions - test that the loaded objects are equal to the original objects, specifically that the + # ability scores and modifiers are equal. + # TODO: Add assertions for prime requisite classes once prime requisite classes are implemented. + assert str_loaded.score == 16 + assert str_loaded.modifiers[ModifierType.TO_HIT] == 2 + assert str_loaded.modifiers[ModifierType.DAMAGE] == 2 + assert str_loaded.modifiers[ModifierType.OPEN_DOORS] == 2 + + assert int_loaded.score == 18 + assert int_loaded.modifiers[ModifierType.LANGUAGES] == 3 + + assert wis_loaded.score == 12 + assert wis_loaded.modifiers[ModifierType.SAVING_THROWS] == 0 + + assert dex_loaded.score == 14 + assert dex_loaded.modifiers[ModifierType.AC] == -1 + assert dex_loaded.modifiers[ModifierType.TO_HIT] == 1 + assert dex_loaded.modifiers[ModifierType.INITIATIVE] == 1 + + assert con_loaded.score == 5 + assert con_loaded.modifiers[ModifierType.HP] == -2 + + assert cha_loaded.score == 18 + assert cha_loaded.modifiers[ModifierType.REACTION] == 2 + + +def test_item_saveload(db): + item_table = db.table("items") + + # Create an Item instance + usable_by = { + character_classes.CharacterClassType.FIGHTER, + character_classes.CharacterClassType.THIEF, + } + original_item = item.Item("50' rope", item.ItemType.ITEM, usable_by, max_equipped=0, gp_value=5) + + # Serialize and insert into DB + item_dict = original_item.to_dict() + item_table.insert(item_dict) + + # Retrieve and deserialize + ItemQuery = Query() + retrieved_item_dict = item_table.search(ItemQuery.name == "50' rope")[0] + retrieved_item = item.Item.from_dict(retrieved_item_dict) + + # Assertions to check if deserialization was correct + assert original_item.name == retrieved_item.name + assert original_item.item_type == retrieved_item.item_type + assert original_item.usable_by_classes == retrieved_item.usable_by_classes + assert original_item.max_equipped == retrieved_item.max_equipped + assert original_item.gp_value == retrieved_item.gp_value + + +def test_armor_saveload(db): + item_table = db.table("armor") + + # Create an Item instance + usable_by = { + character_classes.CharacterClassType.FIGHTER, + character_classes.CharacterClassType.ELF, + } + original_item = item.Armor("Chain Mail", -4, usable_by_classes=usable_by, max_equipped=1, gp_value=40) + + # Serialize and insert into DB + item_dict = original_item.to_dict() + item_table.insert(item_dict) + + # Retrieve and deserialize + ItemQuery = Query() + retrieved_item_dict = item_table.search(ItemQuery.name == "Chain Mail")[0] + retrieved_item = item.Armor.from_dict(retrieved_item_dict) + + # Assertions to check if deserialization was correct + assert retrieved_item.item_type == item.ItemType.ARMOR + assert original_item.name == retrieved_item.name + assert original_item.item_type == retrieved_item.item_type + assert original_item.usable_by_classes == retrieved_item.usable_by_classes + assert original_item.max_equipped == retrieved_item.max_equipped + assert original_item.gp_value == retrieved_item.gp_value + assert original_item.ac_modifier == retrieved_item.ac_modifier + + +def test_weapon_saveload(db): + item_table = db.table("weapon") + + # Create an Item instance + usable_by = { + character_classes.CharacterClassType.FIGHTER, + character_classes.CharacterClassType.ELF, + } + original_sword = item.Weapon( + "Sword", + to_hit_damage_die="1d8", + usable_by_classes=usable_by, + max_equipped=1, + gp_value=40, + ) + + # Serialize and insert into DB + item_dict = original_sword.to_dict() + item_table.insert(item_dict) + + # Retrieve and deserialize + ItemQuery = Query() + retrieved_sword_dict = item_table.search(ItemQuery.name == "Sword")[0] + retrieved_sword = item.Weapon.from_dict(retrieved_sword_dict) + + # Assertions to check if deserialization was correct + assert retrieved_sword.item_type == item.ItemType.WEAPON + assert original_sword.name == retrieved_sword.name + assert original_sword.item_type == retrieved_sword.item_type + assert original_sword.usable_by_classes == retrieved_sword.usable_by_classes + assert original_sword.max_equipped == retrieved_sword.max_equipped + assert original_sword.gp_value == retrieved_sword.gp_value + assert original_sword.to_hit_damage_die == retrieved_sword.to_hit_damage_die + assert original_sword.range == None + + # Add weapon to test_fighter's inventory and equip it + test_fighter.inventory.add_item(retrieved_sword) + test_fighter.inventory.equip_item(retrieved_sword) + assert retrieved_sword.is_equipped == True + assert retrieved_sword.owner == test_fighter + test_fighter.inventory.unequip_item(retrieved_sword) + test_fighter.inventory.remove_item(retrieved_sword) + + # Add weapon to test_elf's inventory and equip it + test_elf.inventory.add_item(retrieved_sword) + test_elf.inventory.equip_item(retrieved_sword) + assert retrieved_sword.is_equipped == True + assert retrieved_sword.owner == test_elf + test_elf.inventory.unequip_item(retrieved_sword) + test_elf.inventory.remove_item(retrieved_sword) + + # Add weapon to test_magic_user's inventory and try to equip it + test_magic_user.inventory.add_item(retrieved_sword) + try: + test_magic_user.inventory.equip_item(retrieved_sword) + except item.ItemNotUsableError as e: + assert isinstance(e, item.ItemNotUsableError) + test_magic_user.inventory.remove_item(retrieved_sword) + + +def test_spell_saveload(db): + item_table = db.table("spell") + + # Create a Spell instance + usable_by = {character_classes.CharacterClassType.MAGIC_USER} + original_spell = item.Spell( + "Fireball", + spell_level=3, + damage_die="8d6", + range=150, + duration_turns=None, + usable_by_classes=usable_by, + max_equipped=1, + gp_value=0, + ) + + # Serialize and insert into DB + spell_dict = original_spell.to_dict() + item_table.insert(spell_dict) + + # Retrieve and deserialize + ItemQuery = Query() + retrieved_spell_dict = item_table.search(ItemQuery.name == "Fireball")[0] + retrieved_spell = item.Spell.from_dict(retrieved_spell_dict) + + # Assertions to check if deserialization was correct + assert retrieved_spell.item_type == item.ItemType.SPELL + assert original_spell.name == retrieved_spell.name + assert original_spell.item_type == retrieved_spell.item_type + assert original_spell.usable_by_classes == retrieved_spell.usable_by_classes + assert original_spell.max_equipped == retrieved_spell.max_equipped + assert original_spell.gp_value == retrieved_spell.gp_value + assert original_spell.spell_level == retrieved_spell.spell_level + assert original_spell.range == retrieved_spell.range + assert original_spell.damage_die == retrieved_spell.damage_die + assert original_spell.duration_turns == retrieved_spell.duration_turns + + # Add spell to test_magic_user's inventory and equip it + test_magic_user.inventory.add_item(retrieved_spell) + test_magic_user.inventory.equip_item(retrieved_spell) + assert retrieved_spell.is_equipped == True + assert retrieved_spell.owner == test_magic_user + test_magic_user.inventory.unequip_item(retrieved_spell) + test_magic_user.inventory.remove_item(retrieved_spell) + + # Add spell to test_fighter's inventory and try to equip it + try: + test_fighter.inventory.add_item(retrieved_spell) + test_fighter.inventory.equip_item(retrieved_spell) + except item.ItemNotUsableError as e: + assert isinstance(e, item.ItemNotUsableError) + test_fighter.inventory.remove_item(retrieved_spell) + + +def test_item_autoset_attributes_preserved_on_saveload(db): + """Tests whether a loaded item's dynamically assigned attribute values are preserved through the save/load process. + + For example, the is_usable attribute is determined by the owner's character class, and the is_equipped attribute + is set by calling the owner's inventory.equip_item() method. Thus, we need to ensure those dynamically assigned + attribute values are saved to and loaded from the DB as expected. + + The handling of resetting the attribute values on the reconstituted items is (TODO: will be) handled in the + PlayerCharacter.from_dict() method and won't need to be handled manually by the library user as we're doing here. + """ + item_table = db.table("item") + + # Step 1: Create instances + armor = item.Armor( + "Plate Mail Armor", + gp_value=50, + max_equipped=1, + usable_by_classes={character_classes.CharacterClassType.FIGHTER}, + ) + weapon = item.Weapon( + "Sword", + "1d8", + gp_value=30, + max_equipped=1, + usable_by_classes={character_classes.CharacterClassType.FIGHTER}, + ) + normal_item = item.Item("50' rope", item.ItemType.EQUIPMENT, gp_value=1, max_equipped=0) + + # Step 2: Add to test_fighter's inventory + test_fighter.inventory.add_item(armor) + test_fighter.inventory.add_item(weapon) + test_fighter.inventory.add_item(normal_item) + + # Step 3: Equip armor and weapon + test_fighter.inventory.equip_item(armor) + test_fighter.inventory.equip_item(weapon) + + # Step 4: Serialize and save to DB + armor_dict, weapon_dict, normal_item_dict = ( + armor.to_dict(), + weapon.to_dict(), + normal_item.to_dict(), + ) + item_table.insert_multiple([armor_dict, weapon_dict, normal_item_dict]) + + # Step 4.1: Empty the character's inventory so we can re-add the items that we + # retrieve from the DB. If we don't do this, we won't be able to equip + # the items we retrieve from the DB because the character will already + # have them in the the max number of items equipped for each. + # This step will NOT be needed during a normal deserialization of a + # PlayerCharacter object because the PlayerCharacter's inventory will + # be empty when the PC is deserialized. + test_fighter.inventory.items.clear() + + # Step 5: Load from DB and assert 'is_equipped' attribute + ItemQuery = Query() + retrieved_armor_dict = item_table.search(ItemQuery.name == "Plate Mail Armor")[0] + retrieved_weapon_dict = item_table.search(ItemQuery.name == "Sword")[0] + retrieved_normal_item_dict = item_table.search(ItemQuery.name == "50' rope")[0] + + retrieved_armor = item.Armor.from_dict(retrieved_armor_dict) + retrieved_weapon = item.Weapon.from_dict(retrieved_weapon_dict) + retrieved_normal_item = item.Item.from_dict(retrieved_normal_item_dict) + + # Step 6: Set the item's owner and equip previously equipped items. + # This is necessary because the owner attribute is not serialized and the + # is_usable attribute is determined by the owner's character class. Thus, + # just setting the is_equipped attribute to True is not enough to re-equip + # an item - it needs to be added to their inventory and then equipped. + # TODO: Handle this re-adding and re-equipping of loaded items in the PlayerCharacter.from_dict(). + test_fighter.inventory.add_item(retrieved_armor) + test_fighter.inventory.add_item(retrieved_weapon) + test_fighter.inventory.add_item(retrieved_normal_item) + if retrieved_armor_dict["is_equipped"]: + test_fighter.inventory.equip_item(retrieved_armor) + if retrieved_weapon_dict["is_equipped"]: + test_fighter.inventory.equip_item(retrieved_weapon) + if retrieved_normal_item_dict["is_equipped"]: + test_fighter.inventory.equip_item(retrieved_normal_item) + + assert retrieved_armor.is_equipped == True + assert retrieved_weapon.is_equipped == True + assert retrieved_normal_item.is_equipped == False + + # Step 7: Clean up + # TODO: Add an Inventory.drop_all_items() method to make this easier. + test_fighter.inventory.unequip_item(retrieved_armor) + test_fighter.inventory.unequip_item(retrieved_weapon) + test_fighter.inventory.remove_item(retrieved_armor) + test_fighter.inventory.remove_item(retrieved_weapon) + test_fighter.inventory.remove_item(retrieved_normal_item) diff --git a/tests/test_unit_armor.py b/tests/test_unit_armor.py new file mode 100644 index 0000000..f0f93a1 --- /dev/null +++ b/tests/test_unit_armor.py @@ -0,0 +1,96 @@ +import logging + +import pytest + +from osrlib.ability import AbilityType +from osrlib.character_classes import CharacterClassType +from osrlib.combat import ModifierType +from osrlib.item import Armor +from osrlib.player_character import PlayerCharacter + +logger = logging.getLogger(__name__) + +CLASSES_THAT_CAN_USE_ALL_ARMOR = { + CharacterClassType.FIGHTER, + CharacterClassType.CLERIC, + CharacterClassType.ELF, + CharacterClassType.DWARF, + CharacterClassType.HALFLING, +} + + +def generate_armor(name, ac, classes, max_equipped, gp_value): + return Armor( + name, + ac, + usable_by_classes=classes, + max_equipped=max_equipped, + gp_value=gp_value, + ) + + +def generate_test_params(): + robes = generate_armor( + "Robes", 0, CLASSES_THAT_CAN_USE_ALL_ARMOR | {CharacterClassType.MAGIC_USER} | {CharacterClassType.THIEF}, 1, 0 + ) + shield = generate_armor("Shield", -1, CLASSES_THAT_CAN_USE_ALL_ARMOR, 1, 60) + leather_armor = generate_armor( + "Leather Armor", -3, CLASSES_THAT_CAN_USE_ALL_ARMOR | {CharacterClassType.THIEF}, 1, 20 + ) + chain_mail_armor = generate_armor("Chain Mail Armor", -4, CLASSES_THAT_CAN_USE_ALL_ARMOR, 1, 40) + plate_mail_armor = generate_armor("Plate Mail Armor", -6, CLASSES_THAT_CAN_USE_ALL_ARMOR, 1, 60) + + armors = [robes, shield, leather_armor, chain_mail_armor, plate_mail_armor] + test_params = [] + + for armor in armors: + for char_class in CharacterClassType: + test_params.append((armor, char_class, char_class in armor.usable_by_classes)) + + return test_params + + +@pytest.fixture +def setup_pc(character_class, armor): + # Setup + pc = PlayerCharacter(f"Test {character_class.value}", character_class) + yield pc + + # Teardown + if armor.is_equipped: + pc.unequip_item(armor) + pc.remove_item_from_inventory(armor) + + +# Armor should have been added to the inventory +@pytest.mark.parametrize("armor, character_class, expected_result", generate_test_params()) +def test_armor_added_to_inventory(setup_pc, armor, expected_result): + pc = setup_pc + pc.add_item_to_inventory(armor) + assert armor is pc.get_item_from_inventory(armor) + + +# Armor should "know" whether it's usable by the owning character's class +@pytest.mark.parametrize("armor, character_class, expected_result", generate_test_params()) +def test_armor_usability_by_owner(setup_pc, armor, expected_result): + pc = setup_pc + pc.add_item_to_inventory(armor) + assert armor.is_usable_by_owner == expected_result + + +# Armor should be able to be equipped and should be shown as such if +# usable by the owning character's class and is then equipped. +@pytest.mark.parametrize("armor, character_class, expected_result", generate_test_params()) +def test_armor_equip_status(setup_pc, armor, expected_result): + pc = setup_pc + pc.add_item_to_inventory(armor) + if armor.is_usable_by_owner: + dex = pc.abilities[AbilityType.DEXTERITY].score + ac_mod = pc.abilities[AbilityType.DEXTERITY].modifiers[ModifierType.AC] + logger.info(f"{armor.name} is usable by {pc.name} [DEX: {dex} ({ac_mod}), AC: {pc.armor_class})]") + pc.equip_item(armor) + logger.info(f"{armor.name} equipped by {pc.name} [DEX: {dex} ({ac_mod}), AC: {pc.armor_class})]") + assert armor.is_equipped + else: + logger.info(f"{armor.name} is NOT usable by {pc.name}") + assert not armor.is_equipped diff --git a/tests/test_unit_character_classes.py b/tests/test_unit_character_classes.py new file mode 100644 index 0000000..d21f681 --- /dev/null +++ b/tests/test_unit_character_classes.py @@ -0,0 +1,50 @@ +import logging + +import pytest + +from osrlib.character_classes import CharacterClassType, class_levels +from osrlib.player_character import PlayerCharacter + +logger = logging.getLogger(__name__) + + +def generate_player_character_params(starting_level: int = 1): + params = [] + for character_class_type in CharacterClassType: + pc = PlayerCharacter(f"Test {character_class_type.value}", character_class_type, starting_level) + params.append(pc) + return params + + +@pytest.mark.parametrize("pc", generate_player_character_params(starting_level=1)) +def test_characters_init_sane_level_one(pc: PlayerCharacter): + logger.info(pc) + assert pc.character_class.current_level.level_num == 1 + assert pc.character_class.hp > 0 + assert pc.character_class.xp == 0 + + +@pytest.mark.parametrize("pc", generate_player_character_params(starting_level=0)) +def test_character_can_reach_all_available_levels(pc: PlayerCharacter): + logger.info( + f"Testing whether {pc.name.upper()} can reach all {pc.character_class.class_type.value.upper()} levels ..." + ) + + for level in class_levels[pc.character_class.class_type]: + level_before = pc.character_class.current_level.level_num + next_level = level_before + 1 + + if next_level < len(pc.character_class.levels): + next_level_xp_requirement = pc.character_class.levels[ + pc.character_class.current_level.level_num + 1 + ].xp_required_for_level + + # Grant just enough XP to reach the next level + xp_needed_to_reach_next_level = next_level_xp_requirement - pc.character_class.xp + pc.grant_xp(xp_needed_to_reach_next_level) + + level_after = pc.character_class.current_level.level_num + if level_after > level_before: + logger.info(pc) + + assert pc.character_class.current_level.level_num == len(class_levels[pc.character_class.class_type]) - 1 diff --git a/tests/test_unit_dice_roller.py b/tests/test_unit_dice_roller.py new file mode 100644 index 0000000..fed47f1 --- /dev/null +++ b/tests/test_unit_dice_roller.py @@ -0,0 +1,78 @@ +import logging +from unittest.mock import patch + +import pytest + +from osrlib.dice_roller import DiceRoll, roll_dice + +logger = logging.getLogger(__name__) +logger.info("Testing dice_roller.py") + + +def test_valid_notation(): + """Tests valid dice notation. + + Checks whether the roll_dice function correctly handles a valid dice notation '3d6'. + Mocks randint to always return 3 for consistent testing. + """ + with patch("random.SystemRandom.randint", return_value=3): + result = roll_dice("3d6") + assert result == DiceRoll(3, 6, 9, 0, 9, [3, 3, 3]) + + +def test_invalid_notation(): + """Tests invalid dice notation. + + Checks whether the roll_dice function raises a ValueError for an invalid dice notation '3dd6'. + """ + with pytest.raises(ValueError): + roll_dice("3dd6") + + +def test_invalid_sides(): + """Tests invalid number of sides. + + Checks whether the roll_dice function raises a ValueError when the number of sides is invalid, i.e., '3d7'. + """ + with pytest.raises(ValueError): + roll_dice("3d7") + + +def test_with_modifier(): + """Tests dice notation with a positive modifier. + + Checks whether the roll_dice function correctly adds the positive modifier to the total roll. + Mocks randint to always return 3 for consistent testing. + """ + with patch("random.SystemRandom.randint", return_value=3): + result = roll_dice("3d6+2") + assert result == DiceRoll(3, 6, 9, 2, 11, [3, 3, 3]) + + +def test_drop_lowest(): + """Tests dropping the lowest dice roll. + + Checks whether the roll_dice function correctly drops the lowest dice when the flag is set. + Mocks randint to return specific values [1, 3, 5, 7] for consistent testing. + """ + with patch("random.SystemRandom.randint", side_effect=[1, 3, 5, 7]): + result = roll_dice("4d6", drop_lowest=True) + assert result == DiceRoll(4, 6, 15, 0, 15, [3, 5, 7]) + + +def test_str_representation(): + """Tests the string representation of DiceRoll. + + Checks whether the string representation of the DiceRoll object is as expected. + """ + result = DiceRoll(3, 6, 9, 0, 9, [3, 3, 3]) + assert str(result) == "3d6" + + +def test_pretty_print(): + """Tests the pretty print of DiceRoll. + + Checks whether the pretty print of the DiceRoll object correctly displays the full roll information. + """ + result = DiceRoll(3, 6, 9, 2, 11, [3, 3, 3]) + assert result.pretty_print() == "Rolled 11 on 3d6+2 (9 + 2)" diff --git a/tests/test_unit_item.py b/tests/test_unit_item.py new file mode 100644 index 0000000..590c62e --- /dev/null +++ b/tests/test_unit_item.py @@ -0,0 +1,115 @@ +import logging + +import pytest + +from osrlib.character_classes import CharacterClassType +from osrlib.item import ( + ItemAlreadyHasOwnerError, + ItemAlreadyInInventoryError, + ItemEquippedError, + ItemNotEquippedError, + ItemNotInInventoryError, + ItemNotUsableError, + Weapon, +) +from osrlib.player_character import PlayerCharacter + +logger = logging.getLogger(__name__) + +CLASSES_THAT_CAN_USE_ALL_WEAPONS = { + CharacterClassType.FIGHTER, + CharacterClassType.THIEF, + CharacterClassType.ELF, + CharacterClassType.DWARF, + CharacterClassType.HALFLING, +} + + +def create_weapon(name, damage_die, classes_that_can_use_it, max_equipped, gp_value, range=0): + return Weapon( + name, + damage_die, + usable_by_classes=classes_that_can_use_it, + max_equipped=max_equipped, + gp_value=gp_value, + range=range, + ) + + +def generate_test_params(): + sword = create_weapon("sword", "1d6", CLASSES_THAT_CAN_USE_ALL_WEAPONS, 1, 40) + mace = create_weapon("mace", "1d6", CLASSES_THAT_CAN_USE_ALL_WEAPONS | {CharacterClassType.CLERIC}, 1, 20) + dagger = create_weapon("dagger", "1d4", CLASSES_THAT_CAN_USE_ALL_WEAPONS | {CharacterClassType.MAGIC_USER}, 1, 10) + crossbow = create_weapon("crossbow", "1d6", CLASSES_THAT_CAN_USE_ALL_WEAPONS, 1, 60, 240) + + weapons = [sword, mace, dagger, crossbow] + test_params = [] + + for weapon in weapons: + for char_class in CharacterClassType: + test_params.append((weapon, char_class, char_class in weapon.usable_by_classes)) + + return test_params + + +@pytest.fixture +def setup_pc(character_class): + # Setup + pc = PlayerCharacter(f"Test {character_class.value}", character_class) + yield pc + + +@pytest.mark.parametrize("weapon, character_class, expected_result", generate_test_params()) +def test_weapon_usability_per_character_class(setup_pc, weapon, expected_result): + pc = setup_pc + + # Item should be successfully added to inventory regardless of usability + logger.info(f"Adding {weapon.name} to {pc.name}'s inventory") + assert pc.add_item_to_inventory(weapon), f"{weapon.name} not added to {pc.name}'s inventory" + + logger.info(f"Adding {weapon.name} to {pc.name}'s inventory again") + with pytest.raises(ItemAlreadyInInventoryError): + pc.add_item_to_inventory(weapon) + + # Item should be retrievable from inventory if it was previously added + logger.info(f"Getting {weapon.name} from {pc.name}'s inventory") + weapon_from_inv = pc.get_item_from_inventory(weapon) + assert weapon_from_inv == weapon, f"Could NOT get {weapon.name} from {pc.name}'s inventory" + + # Error should be raised when trying to get an item from inventory that's not in the inventory + with pytest.raises(ItemNotInInventoryError): + some_other_weapon = create_weapon( + "THIS WEAPON NOT IN INVENTORY", "1d6", CLASSES_THAT_CAN_USE_ALL_WEAPONS, 1, 40 + ) + pc.get_item_from_inventory(some_other_weapon) + + # Item should be usable by classes that can use it + logger.info(f"Checking whether {pc.name} can use {weapon.name}") + assert weapon.is_usable_by_owner == expected_result, ( + f"{pc.name} should be able to use {weapon.name}" + if expected_result + else f"{pc.name} should NOT be able to use {weapon.name}" + ) + + # Item should be equipable and unequipable if it's usable by owner + if weapon.is_usable_by_owner: + logger.info(f"Equipping {pc.name} with {weapon.name}") + assert pc.equip_item(weapon), f"{pc.name} could NOT equip {weapon.name}" + + logger.info(f"Unequipping {pc.name}'s {weapon.name}") + assert pc.unequip_item(weapon), f"{pc.name} could NOT unequip {weapon.name}" + + # Error should be raised when trying to unequip an item that's not equipped + if not weapon.is_equipped: + with pytest.raises(ItemNotEquippedError): + pc.unequip_item(weapon) + else: + logger.info(f"{weapon.name} is NOT usable by {pc.name}, skipping equip/unequip test") + + # Item should be removable from inventory + logger.info(f"Removing {weapon.name} from {pc.name}'s inventory") + assert pc.remove_item_from_inventory(weapon), f"{weapon.name} could NOT be removed from {pc.name}'s inventory" + + # Error should be raised on attemping to remove an item not in inventory + with pytest.raises(ItemNotInInventoryError): + pc.remove_item_from_inventory(weapon) diff --git a/tests/test_unit_party.py b/tests/test_unit_party.py new file mode 100644 index 0000000..d6199ff --- /dev/null +++ b/tests/test_unit_party.py @@ -0,0 +1,122 @@ +import pytest +from osrlib import party, player_character, character_classes + + +@pytest.fixture +def setup_party(): + empty_test_party = party.Party("The B-Team", 3) + character1 = player_character.PlayerCharacter("Sckricko", character_classes.CharacterClassType.FIGHTER, 1) + character2 = player_character.PlayerCharacter("Mazpar", character_classes.CharacterClassType.MAGIC_USER, 1) + character3 = player_character.PlayerCharacter("Slick", character_classes.CharacterClassType.THIEF, 1) + return empty_test_party, character1, character2, character3 + + +def test_add_firstcharacter(setup_party): + test_party, character1, _, _ = setup_party + test_party.add_character(character1) + assert character1 in test_party.characters + + +def test_add_character_at_capacity(setup_party): + test_party, character1, character2, character3 = setup_party + max_capacity = test_party.max_party_members # Replace with how you get the max_capacity + + # Fill the party to max_capacity - 1 with uniquely named characters + for i in range(max_capacity - 1): + new_character = player_character.PlayerCharacter( + f"Test Character {i}", character_classes.CharacterClassType.FIGHTER, 1 + ) + test_party.add_character(new_character) + + # Adding one more should work + test_party.add_character(character1) + + # Adding one more beyond that should raise PartyAtCapacityError + with pytest.raises(party.PartyAtCapacityError): + test_party.add_character(character2) + + +def test_remove_character(setup_party): + test_party, character1, _, _ = setup_party + test_party.add_character(character1) + test_party.remove_character(character1) + assert character1 not in test_party.characters + + +def test_remove_character_not_in_party(setup_party): + test_party, character1, _, _ = setup_party + with pytest.raises(party.CharacterNotInPartyError): + test_party.remove_character(character1) + + +def test_get_character_by_name(setup_party): + test_party, character1, character2, character3 = setup_party + test_party.add_character(character1) + test_party.add_character(character2) + test_party.add_character(character3) + assert test_party.get_character_by_name("Mazpar") == character2 + + +def test_get_character_by_name_not_in_party(setup_party): + test_party, _, _, _ = setup_party + assert test_party.get_character_by_name("Mazpar") is None + + +def test_get_character_by_index(setup_party): + test_party, character1, character2, character3 = setup_party + test_party.add_character(character1) + test_party.add_character(character2) + test_party.add_character(character3) + assert test_party.get_character_by_index(1) == character2 + + +def test_get_character_by_index_out_of_range(setup_party): + test_party, _, _, _ = setup_party + assert test_party.get_character_by_index(3) is None + + +def test_get_character_index(setup_party): + test_party, character1, character2, character3 = setup_party + test_party.add_character(character1) + test_party.add_character(character2) + test_party.add_character(character3) + assert test_party.get_character_index(character2) == 1 + + +def test_get_character_index_not_in_party(setup_party): + test_party, character1, _, _ = setup_party + with pytest.raises(party.CharacterNotInPartyError): + test_party.get_character_index(character1) + + +def test_move_character_to_index(setup_party): + test_party, character1, character2, character3 = setup_party + test_party.add_character(character1) + test_party.add_character(character2) + test_party.add_character(character3) + test_party.move_character_to_index(character2, 0) + assert test_party.get_character_by_index(0) == character2 + + +def test_move_character_to_index_not_in_party(setup_party): + test_party, character1, _, _ = setup_party + with pytest.raises(party.CharacterNotInPartyError): + test_party.move_character_to_index(character1, 0) + + +def test_move_character_to_index_out_of_range(setup_party): + test_party, character1, character2, character3 = setup_party + test_party.add_character(character1) + test_party.add_character(character2) + test_party.add_character(character3) + with pytest.raises(IndexError): + test_party.move_character_to_index(character2, 3) + + +def test_clear_party(setup_party): + test_party, character1, character2, character3 = setup_party + test_party.add_character(character1) + test_party.add_character(character2) + test_party.add_character(character3) + test_party.clear_party() + assert len(test_party.characters) == 0 diff --git a/tests/test_unit_utils.py b/tests/test_unit_utils.py new file mode 100644 index 0000000..a54c379 --- /dev/null +++ b/tests/test_unit_utils.py @@ -0,0 +1,48 @@ +import logging + +from osrlib import ModifierType +from osrlib.utils import format_modifiers + +logger = logging.getLogger(__name__) + + +def test_format_modifiers(): + """Test function for the format_modifiers() utility function. + + This function tests the format_modifiers() function with various input values and verifies that the output is as expected. + """ + logger.info("Testing empty modifiers: '" + format_modifiers({}) + "'") + assert format_modifiers({}) == "" + + logger.info("Testing single modifier with positive value: '" + format_modifiers({ModifierType.TO_HIT: 1}) + "'") + assert format_modifiers({ModifierType.TO_HIT: 1}) == "To hit: +1" + + logger.info("Testing single modifier with negative value: '" + format_modifiers({ModifierType.DAMAGE: -1}) + "'") + assert format_modifiers({ModifierType.DAMAGE: -1}) == "Damage: -1" + + logger.info( + "Testing single modifier with no (zero) value: '" + format_modifiers({ModifierType.OPEN_DOORS: 0}) + "'" + ) + assert format_modifiers({ModifierType.OPEN_DOORS: 0}) == "Open doors: 0" + + logger.info( + "Testing multiple modifiers with positive and negative values: '" + + format_modifiers( + { + ModifierType.TO_HIT: 1, + ModifierType.DAMAGE: -1, + ModifierType.OPEN_DOORS: 0, + } + ) + + "'" + ) + assert ( + format_modifiers( + { + ModifierType.TO_HIT: 1, + ModifierType.DAMAGE: -1, + ModifierType.OPEN_DOORS: 0, + } + ) + == "To hit: +1, Damage: -1, Open doors: 0" + )