diff --git a/ardent-andromedas/.github/workflows/lint.yaml b/ardent-andromedas/.github/workflows/lint.yaml new file mode 100644 index 0000000..7f67e80 --- /dev/null +++ b/ardent-andromedas/.github/workflows/lint.yaml @@ -0,0 +1,35 @@ +# GitHub Action workflow enforcing our code style. + +name: Lint + +# Trigger the workflow on both push (to the main repository, on the main branch) +# and pull requests (against the main repository, but from any repo, from any branch). +on: + push: + branches: + - main + pull_request: + +# Brand new concurrency setting! This ensures that not more than one run can be triggered for the same commit. +# It is useful for pull requests coming from the main repository since both triggers will match. +concurrency: lint-${{ github.sha }} + +jobs: + lint: + runs-on: ubuntu-latest + + env: + # The Python version your project uses. Feel free to change this if required. + PYTHON_VERSION: "3.12" + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python ${{ env.PYTHON_VERSION }} + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Run pre-commit hooks + uses: pre-commit/action@v3.0.1 diff --git a/ardent-andromedas/.gitignore b/ardent-andromedas/.gitignore new file mode 100644 index 0000000..5ba5b1c --- /dev/null +++ b/ardent-andromedas/.gitignore @@ -0,0 +1,39 @@ +# Files generated by the interpreter +__pycache__/ +*.py[cod] + +# Environment specific +.venv +venv +.env +env + +# Unittest reports +.coverage* + +# Logs +*.log + +# PyEnv version selector +.python-version + +# Built objects +*.so +dist/ +build/ + +# IDEs +# PyCharm +.idea/ +# VSCode +.vscode/ +# MacOS +.DS_Store + +ecosystem_gifs/ + +# Temporary gitignore for DB testing +src/bot/db/test + +*.sqlite3 +*.sqlite3-journal diff --git a/ardent-andromedas/.pre-commit-config.yaml b/ardent-andromedas/.pre-commit-config.yaml new file mode 100644 index 0000000..4bccb6f --- /dev/null +++ b/ardent-andromedas/.pre-commit-config.yaml @@ -0,0 +1,18 @@ +# Pre-commit configuration. +# See https://github.com/python-discord/code-jam-template/tree/main#pre-commit-run-linting-before-committing + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: check-toml + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace + args: [--markdown-linebreak-ext=md] + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.5.0 + hooks: + - id: ruff + - id: ruff-format diff --git a/ardent-andromedas/LICENSE.txt b/ardent-andromedas/LICENSE.txt new file mode 100644 index 0000000..5a04926 --- /dev/null +++ b/ardent-andromedas/LICENSE.txt @@ -0,0 +1,7 @@ +Copyright 2021 Python Discord + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/ardent-andromedas/README.md b/ardent-andromedas/README.md new file mode 100644 index 0000000..e201b4f --- /dev/null +++ b/ardent-andromedas/README.md @@ -0,0 +1,292 @@ +# EcoCord: A Discord Ecosystem Simulator + +![EcoCord Logo](assets/readme/logo.png) + +Welcome to EcoCord, an innovative Discord bot that transforms your server's activity into a living, breathing ecosystem! Created for the Python Code Jam 2024 with the theme of "Information Overload," EcoCord turns the constant stream of messages, reactions, and user interactions into a vibrant, visual representation of your community's digital life. + +## What is EcoCord? + +EcoCord is a unique Discord bot that creates a dynamic ecosystem based on your server's activity. It visualizes user interactions as various creatures in a simulated environment, bringing your community to life in a whole new way! + +![EcoCord in Action](assets/readme/demo.gif) + +### Key Features: + +- **Real-time Ecosystem Visualization**: Watch as your server's activity is transformed into a lively ecosystem with birds, snakes, and frogs representing active users. +- **Activity-based Interactions**: The more active your server, the more vibrant and diverse the ecosystem becomes! +- **User Avatars**: See your community members represented as cute critters with their own avatars. +- **Word Clouds**: Popular topics and frequently used words appear as dynamic word clouds in the sky. +- **Reaction Emojis**: Watch as reaction emojis float across the screen, adding extra flair to your ecosystem. +- **GIF Generation**: Automatically generate and share GIFs of your server's ecosystem in action. + +## How It Works + +EcoCord listens to various events in your Discord server: + +1. **Messages**: Each message spawns or activates a critter in the ecosystem. +2. **Reactions**: Emojis used in reactions appear and animate in the environment. +3. **Typing**: Users typing are represented by their critters becoming more active. + +As these events occur, the ecosystem evolves: + +- Critters move around, interact, and respond to the overall activity level. +- Word clouds form and dissipate based on message content. + +### Critter Types + +EcoCord features three main types of critters, each with unique behaviors and representations: + +1. **Birds**: + - Fly smoothly across the sky + - Change direction randomly + - Flap their wings as they move + +2. **Snakes**: + - Slither along the ground + - Move in a sinusoidal pattern + - Grow longer as they become more active + +3. **Frogs**: + - Hop around the lower part of the ecosystem + - Have distinct rest and jump states + - Scale in size based on their vertical position + +Each critter type is designed to represent different aspects of user activity and add variety to the ecosystem visualization. + +## Technical Challenges + +EcoCord overcomes several technical challenges to create a seamless and engaging experience: + +1. **GIF Generation and Multithreading**: + - Utilizes a separate process for GIF generation to avoid blocking the main application + - Implements a shared memory approach using `multiprocessing.Array` for efficient frame sharing between processes + - Manages concurrent access to shared resources with locks and queues + - Generates GIFs in under 1s on typical hardware + +2. **SQLite Database Integration**: + - Uses `aiosqlite` for asynchronous database operations + - Stores and retrieves guild configurations and user information + +3. **Efficient Rendering with Pygame**: + - Optimizes drawing operations to handle multiple moving entities + - Implements custom drawing algorithms for each critter type + - Manages transparency and layering for complex visual effects + - Parallax scrolling of background clouds to add depth to the ecosystem + +4. **Discord API Integration**: + - Handles real-time events from Discord using `discord.py` + - Manages rate limits and connection issues gracefully + - Implements command handling and permission checks for bot configuration + +5. **Dynamic Word Cloud Generation**: + - Processes message content in real-time to extract relevant words + - Generates and updates word clouds based on frequently used terms + - Integrates word clouds seamlessly into the ecosystem visualization with masking + +6. **Avatar Integration and Image Processing**: + - Fetches and processes user avatars from Discord + - Applies masks and transformations to integrate avatars with critter designs + - Handles various image formats and sizes efficiently + +These technical solutions work together to create a responsive, visually appealing, and interactive ecosystem that accurately represents the activity in your Discord server. + +## Connection to the Theme: Information Overload + +EcoCord tackles the theme of "Information Overload" by: + +1. **Visualizing Data**: Transforming the overwhelming stream of Discord messages and events into a visually appealing and easily digestible format. +2. **Aggregating Information**: Combining multiple data points (messages, reactions, user activity) into a single, coherent representation. +3. **Dynamic Adaptation**: The ecosystem evolves based on the volume and type of information, providing a real-time view of server activity. +4. **Filtering and Focusing**: By representing users as critters and popular topics as word clouds, EcoCord helps users focus on key information amidst the noise. + +## Innovation Spotlight + +EcoCord stands out in addressing "Information Overload" through its unique approach: + +1. **Dynamic Visualization**: We transform raw data into an engaging, living ecosystem. +2. **Intelligent Aggregation**: Our algorithm combines multiple data points to create meaningful representations. +3. **Scalability**: EcoCord efficiently handles high-volume servers without performance degradation. + +### Key Innovation: Efficient GIF Generation + +Our multithreaded GIF generation process is a standout feature: + +```python +@staticmethod +async def _gif_generation_process( + shared_frames: SharedNumpyArray, + current_frame_index: multiprocessing.Value, + frame_count_queue: multiprocessing.Queue, + gif_info_queue: multiprocessing.Queue, + fps: int, +) -> None: + frames = shared_frames.get_array() + + with ThreadPoolExecutor() as executor: + while True: + frame_count = frame_count_queue.get() + if frame_count is None: + break + + start_index = current_frame_index.value + ordered_frames = np.roll(frames, -start_index, axis=0) + + frames = list(executor.map(Image.fromarray, ordered_frames)) + + duration = int(1000 / fps) + + with io.BytesIO() as gif_buffer: + optimized_frames[0].save( + gif_buffer, + format="GIF", + save_all=True, + append_images=frames[1:], + optimize=False, + duration=[duration] * (len(frames)), + loop=0, + ) + + gif_data = gif_buffer.getvalue() + + gif_info_queue.put((gif_data, time.time())) +``` + +This approach allows us to generate high-quality GIFs efficiently, even for servers with thousands of messages per minute. Key features include: + +1. Asynchronous processing using `asyncio` +2. Shared memory for frame data using `SharedNumpyArray` +3. Multithreading for frame optimization +4. Efficient frame ordering and GIF creation + +## Getting Started + +### Prerequisites + +- Python 3.12 +- Poetry (for dependency management) +- A Discord Bot Token + +### Installation + +1. Clone the repository: + ``` + git clone https://github.com/ardent-andromedas/python-code-jam-2024.git + cd ecocord + ``` + +2. Install dependencies using Poetry: + ``` + poetry install + ``` + +3. Set up your environment variables: + Create a `.env` file in the project root and add your Discord bot token: + ``` + BOT_TOKEN= + ``` + +### Running EcoCord + +1. Start the bot: + ``` + poetry run run + ``` + +2. Invite the bot to your Discord server using the OAuth2 URL generated for your bot in the Discord Developer Portal. + +The bot should be configured for a Guild Install, with the `applications.commands` and `bot` scopes, and `Attach Files`, `Create Public Threads`, `Embed Links`, `Read Message History`, `Send Messages`, `Send Messages in Threads`, `Use Slash Commands`, and `View Channels` permissions. + +3. Use the `/configure` command in your server to set up the channels where EcoCord will operate. + + +## Configuration + +To set up EcoCord in your server, use the `/configure` command. This command allows you to specify which channels the bot should monitor and where it should post ecosystem GIFs. + +![Configure Command](assets/readme/configure.png) + +The configuration options include: + +- **Ecosystem Channel**: The channel where EcoCord will monitor activity and generate the ecosystem. +- **GIF Channel**: The channel where EcoCord will post generated GIFs of the ecosystem. + +After running the `/configure` command, follow the prompts to select the appropriate channels. EcoCord will then start monitoring the specified ecosystem channel and post GIFs in the designated GIF channel. + + +## Usage + +Once EcoCord is running in your server, it will automatically start creating and updating the ecosystem based on server activity. Here are some key commands and features: + +- `/configure`: Set up the channels where EcoCord will operate and where GIFs will be posted. +- Activity Visualization: Watch the ecosystem change in real-time as your server becomes more or less active. +- GIF Generation: Periodically, EcoCord will generate and post GIFs of the ecosystem in threads in the designated channel. + +## Contributing + +We welcome contributions to EcoCord! If you have ideas for new features, improvements, or bug fixes, please feel free to: + +1. Fork the repository +2. Create your feature branch (`git checkout -b feature/AmazingFeature`) +3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) +4. Push to the branch (`git push origin feature/AmazingFeature`) +5. Open a Pull Request + +## Acknowledgments + +- Thanks to the Python Discord community for organizing the Python Code Jam 2024 +- Shoutout to all the contributors who helped bring EcoCord to life +- Special thanks to the creators of the wonderful libraries used in this project, including Discord.py, Pygame, and Pillow + +### Our team + +All team members contributed to the initial project planning and ideation. Special thanks to: + +[Stovoy](https://github.com/Stovoy) +* Project lead and primary developer +* Conceived the ecosystem concept and implemented the core functionality +* Contributed the majority of the codebase + +[Jaavv](https://github.com/Jaavv) +* Contributed to early development stages +* Implemented initial versions of the Discord bot and SQLite integration + +[Walkercito](https://github.com/Walkercito) +* Provided valuable testing support +* Sourced the cloud assets used in the background from [Free Sky with Clouds Background Pixel Art Set](https://free-game-assets.itch.io/free-sky-with-clouds-background-pixel-art-set) + +[ShadowDogger](https://github.com/ShadowDogger) and [Tinoy](https://github.com/tinoy-t) +* Participated in brainstorming sessions +* Offered insights and suggestions during the planning phase + +While some initially planned features like backfilling, timelapses, and snapshotting were ultimately not implemented, the team's collaborative efforts in the early stages helped shape the project's direction and scope. + +## Future Roadmap + +While EcoCord already offers a unique and engaging experience, we have exciting plans for future enhancements: + +1. **Historical Data and Timelapses** + - Implement backfilling of past activity data + - Add a user interaction to request timelapses of past server activity + +2. **Enhanced Ecosystem Visuals** + - Introduce terrain deformation for a more dynamic environment + - Implement advanced procedural generation techniques + - Add more diverse critter types to represent different user behaviors + - Create multiple biomes and environments to reflect server themes or moods + +3. **Increased Customization** + - Allow users to customize their critter appearances and behaviors + - Implement server-wide visual themes and customization options + +4. **Activity-Based Evolution** + - Develop a more sophisticated system for ecosystem growth based on overall server activity levels + - Introduce ecosystem "events" triggered by specific server milestones or activities + +5. **Expanded Event Reactions** + - Increase the range of Discord events that influence the ecosystem + - Add special items or phenomena that appear in response to unique server events + +6. **Performance Optimizations** + - Continually improve rendering and processing efficiency to support larger, more active servers + +We're excited to bring these features to life and further enhance the EcoCord experience. \ No newline at end of file diff --git a/ardent-andromedas/assets/clouds/Clouds 1/1.png b/ardent-andromedas/assets/clouds/Clouds 1/1.png new file mode 100755 index 0000000..9ede45f Binary files /dev/null and b/ardent-andromedas/assets/clouds/Clouds 1/1.png differ diff --git a/ardent-andromedas/assets/clouds/Clouds 1/2.png b/ardent-andromedas/assets/clouds/Clouds 1/2.png new file mode 100755 index 0000000..b58be96 Binary files /dev/null and b/ardent-andromedas/assets/clouds/Clouds 1/2.png differ diff --git a/ardent-andromedas/assets/clouds/Clouds 1/3.png b/ardent-andromedas/assets/clouds/Clouds 1/3.png new file mode 100755 index 0000000..a5818cc Binary files /dev/null and b/ardent-andromedas/assets/clouds/Clouds 1/3.png differ diff --git a/ardent-andromedas/assets/clouds/Clouds 1/4.png b/ardent-andromedas/assets/clouds/Clouds 1/4.png new file mode 100755 index 0000000..c2741c7 Binary files /dev/null and b/ardent-andromedas/assets/clouds/Clouds 1/4.png differ diff --git a/ardent-andromedas/assets/clouds/Clouds 2/1.png b/ardent-andromedas/assets/clouds/Clouds 2/1.png new file mode 100755 index 0000000..bf39b1e Binary files /dev/null and b/ardent-andromedas/assets/clouds/Clouds 2/1.png differ diff --git a/ardent-andromedas/assets/clouds/Clouds 2/2.png b/ardent-andromedas/assets/clouds/Clouds 2/2.png new file mode 100755 index 0000000..e37d51f Binary files /dev/null and b/ardent-andromedas/assets/clouds/Clouds 2/2.png differ diff --git a/ardent-andromedas/assets/clouds/Clouds 2/3.png b/ardent-andromedas/assets/clouds/Clouds 2/3.png new file mode 100755 index 0000000..ad3bcba Binary files /dev/null and b/ardent-andromedas/assets/clouds/Clouds 2/3.png differ diff --git a/ardent-andromedas/assets/clouds/Clouds 2/4.png b/ardent-andromedas/assets/clouds/Clouds 2/4.png new file mode 100755 index 0000000..b9d70f1 Binary files /dev/null and b/ardent-andromedas/assets/clouds/Clouds 2/4.png differ diff --git a/ardent-andromedas/assets/clouds/Clouds 3/1.png b/ardent-andromedas/assets/clouds/Clouds 3/1.png new file mode 100755 index 0000000..273a5a2 Binary files /dev/null and b/ardent-andromedas/assets/clouds/Clouds 3/1.png differ diff --git a/ardent-andromedas/assets/clouds/Clouds 3/2.png b/ardent-andromedas/assets/clouds/Clouds 3/2.png new file mode 100755 index 0000000..a523b4d Binary files /dev/null and b/ardent-andromedas/assets/clouds/Clouds 3/2.png differ diff --git a/ardent-andromedas/assets/clouds/Clouds 3/3.png b/ardent-andromedas/assets/clouds/Clouds 3/3.png new file mode 100755 index 0000000..7a9c555 Binary files /dev/null and b/ardent-andromedas/assets/clouds/Clouds 3/3.png differ diff --git a/ardent-andromedas/assets/clouds/Clouds 3/4.png b/ardent-andromedas/assets/clouds/Clouds 3/4.png new file mode 100755 index 0000000..ca59ca6 Binary files /dev/null and b/ardent-andromedas/assets/clouds/Clouds 3/4.png differ diff --git a/ardent-andromedas/assets/clouds/Clouds 4/1.png b/ardent-andromedas/assets/clouds/Clouds 4/1.png new file mode 100755 index 0000000..5958dc0 Binary files /dev/null and b/ardent-andromedas/assets/clouds/Clouds 4/1.png differ diff --git a/ardent-andromedas/assets/clouds/Clouds 4/2.png b/ardent-andromedas/assets/clouds/Clouds 4/2.png new file mode 100755 index 0000000..bc17a9a Binary files /dev/null and b/ardent-andromedas/assets/clouds/Clouds 4/2.png differ diff --git a/ardent-andromedas/assets/clouds/Clouds 4/3.png b/ardent-andromedas/assets/clouds/Clouds 4/3.png new file mode 100755 index 0000000..7b0b945 Binary files /dev/null and b/ardent-andromedas/assets/clouds/Clouds 4/3.png differ diff --git a/ardent-andromedas/assets/clouds/Clouds 4/4.png b/ardent-andromedas/assets/clouds/Clouds 4/4.png new file mode 100755 index 0000000..f247fe2 Binary files /dev/null and b/ardent-andromedas/assets/clouds/Clouds 4/4.png differ diff --git a/ardent-andromedas/assets/clouds/Clouds 5/1.png b/ardent-andromedas/assets/clouds/Clouds 5/1.png new file mode 100755 index 0000000..8dd4390 Binary files /dev/null and b/ardent-andromedas/assets/clouds/Clouds 5/1.png differ diff --git a/ardent-andromedas/assets/clouds/Clouds 5/2.png b/ardent-andromedas/assets/clouds/Clouds 5/2.png new file mode 100755 index 0000000..b395da8 Binary files /dev/null and b/ardent-andromedas/assets/clouds/Clouds 5/2.png differ diff --git a/ardent-andromedas/assets/clouds/Clouds 5/3.png b/ardent-andromedas/assets/clouds/Clouds 5/3.png new file mode 100755 index 0000000..fd8af88 Binary files /dev/null and b/ardent-andromedas/assets/clouds/Clouds 5/3.png differ diff --git a/ardent-andromedas/assets/clouds/Clouds 5/4.png b/ardent-andromedas/assets/clouds/Clouds 5/4.png new file mode 100755 index 0000000..5b3b5ef Binary files /dev/null and b/ardent-andromedas/assets/clouds/Clouds 5/4.png differ diff --git a/ardent-andromedas/assets/clouds/Clouds 5/5.png b/ardent-andromedas/assets/clouds/Clouds 5/5.png new file mode 100755 index 0000000..6519e85 Binary files /dev/null and b/ardent-andromedas/assets/clouds/Clouds 5/5.png differ diff --git a/ardent-andromedas/assets/clouds/Clouds 6/1.png b/ardent-andromedas/assets/clouds/Clouds 6/1.png new file mode 100755 index 0000000..6061e45 Binary files /dev/null and b/ardent-andromedas/assets/clouds/Clouds 6/1.png differ diff --git a/ardent-andromedas/assets/clouds/Clouds 6/2.png b/ardent-andromedas/assets/clouds/Clouds 6/2.png new file mode 100755 index 0000000..9ccc01b Binary files /dev/null and b/ardent-andromedas/assets/clouds/Clouds 6/2.png differ diff --git a/ardent-andromedas/assets/clouds/Clouds 6/3.png b/ardent-andromedas/assets/clouds/Clouds 6/3.png new file mode 100755 index 0000000..58c2942 Binary files /dev/null and b/ardent-andromedas/assets/clouds/Clouds 6/3.png differ diff --git a/ardent-andromedas/assets/clouds/Clouds 6/4.png b/ardent-andromedas/assets/clouds/Clouds 6/4.png new file mode 100755 index 0000000..8d30b26 Binary files /dev/null and b/ardent-andromedas/assets/clouds/Clouds 6/4.png differ diff --git a/ardent-andromedas/assets/clouds/Clouds 6/5.png b/ardent-andromedas/assets/clouds/Clouds 6/5.png new file mode 100755 index 0000000..f7f13d0 Binary files /dev/null and b/ardent-andromedas/assets/clouds/Clouds 6/5.png differ diff --git a/ardent-andromedas/assets/clouds/Clouds 6/6.png b/ardent-andromedas/assets/clouds/Clouds 6/6.png new file mode 100755 index 0000000..436bda0 Binary files /dev/null and b/ardent-andromedas/assets/clouds/Clouds 6/6.png differ diff --git a/ardent-andromedas/assets/clouds/Clouds 7/1.png b/ardent-andromedas/assets/clouds/Clouds 7/1.png new file mode 100755 index 0000000..be878dc Binary files /dev/null and b/ardent-andromedas/assets/clouds/Clouds 7/1.png differ diff --git a/ardent-andromedas/assets/clouds/Clouds 7/2.png b/ardent-andromedas/assets/clouds/Clouds 7/2.png new file mode 100755 index 0000000..b86ad59 Binary files /dev/null and b/ardent-andromedas/assets/clouds/Clouds 7/2.png differ diff --git a/ardent-andromedas/assets/clouds/Clouds 7/3.png b/ardent-andromedas/assets/clouds/Clouds 7/3.png new file mode 100755 index 0000000..1f3df64 Binary files /dev/null and b/ardent-andromedas/assets/clouds/Clouds 7/3.png differ diff --git a/ardent-andromedas/assets/clouds/Clouds 7/4.png b/ardent-andromedas/assets/clouds/Clouds 7/4.png new file mode 100755 index 0000000..59d9254 Binary files /dev/null and b/ardent-andromedas/assets/clouds/Clouds 7/4.png differ diff --git a/ardent-andromedas/assets/clouds/Clouds 8/1.png b/ardent-andromedas/assets/clouds/Clouds 8/1.png new file mode 100755 index 0000000..fb04977 Binary files /dev/null and b/ardent-andromedas/assets/clouds/Clouds 8/1.png differ diff --git a/ardent-andromedas/assets/clouds/Clouds 8/2.png b/ardent-andromedas/assets/clouds/Clouds 8/2.png new file mode 100755 index 0000000..0708297 Binary files /dev/null and b/ardent-andromedas/assets/clouds/Clouds 8/2.png differ diff --git a/ardent-andromedas/assets/clouds/Clouds 8/3.png b/ardent-andromedas/assets/clouds/Clouds 8/3.png new file mode 100755 index 0000000..693ca85 Binary files /dev/null and b/ardent-andromedas/assets/clouds/Clouds 8/3.png differ diff --git a/ardent-andromedas/assets/clouds/Clouds 8/4.png b/ardent-andromedas/assets/clouds/Clouds 8/4.png new file mode 100755 index 0000000..3e474a1 Binary files /dev/null and b/ardent-andromedas/assets/clouds/Clouds 8/4.png differ diff --git a/ardent-andromedas/assets/clouds/Clouds 8/5.png b/ardent-andromedas/assets/clouds/Clouds 8/5.png new file mode 100755 index 0000000..a93171f Binary files /dev/null and b/ardent-andromedas/assets/clouds/Clouds 8/5.png differ diff --git a/ardent-andromedas/assets/clouds/Clouds 8/6.png b/ardent-andromedas/assets/clouds/Clouds 8/6.png new file mode 100755 index 0000000..e39d0ba Binary files /dev/null and b/ardent-andromedas/assets/clouds/Clouds 8/6.png differ diff --git a/ardent-andromedas/assets/clouds/License.txt b/ardent-andromedas/assets/clouds/License.txt new file mode 100755 index 0000000..6ad19d1 --- /dev/null +++ b/ardent-andromedas/assets/clouds/License.txt @@ -0,0 +1,3 @@ +hhttps://free-game-assets.itch.io/free-sky-with-clouds-background-pixel-art-set + +https://craftpix.net/file-licenses/ diff --git a/ardent-andromedas/assets/readme/configure.png b/ardent-andromedas/assets/readme/configure.png new file mode 100644 index 0000000..a4c01db Binary files /dev/null and b/ardent-andromedas/assets/readme/configure.png differ diff --git a/ardent-andromedas/assets/readme/demo.gif b/ardent-andromedas/assets/readme/demo.gif new file mode 100644 index 0000000..5dbede3 Binary files /dev/null and b/ardent-andromedas/assets/readme/demo.gif differ diff --git a/ardent-andromedas/assets/readme/logo.png b/ardent-andromedas/assets/readme/logo.png new file mode 100644 index 0000000..2336f7f Binary files /dev/null and b/ardent-andromedas/assets/readme/logo.png differ diff --git a/ardent-andromedas/poetry.lock b/ardent-andromedas/poetry.lock new file mode 100644 index 0000000..d5fa0e7 --- /dev/null +++ b/ardent-andromedas/poetry.lock @@ -0,0 +1,1517 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "aiohttp" +version = "3.9.5" +description = "Async http client/server framework (asyncio)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fcde4c397f673fdec23e6b05ebf8d4751314fa7c24f93334bf1f1364c1c69ac7"}, + {file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d6b3f1fabe465e819aed2c421a6743d8debbde79b6a8600739300630a01bf2c"}, + {file = "aiohttp-3.9.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ae79c1bc12c34082d92bf9422764f799aee4746fd7a392db46b7fd357d4a17a"}, + {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d3ebb9e1316ec74277d19c5f482f98cc65a73ccd5430540d6d11682cd857430"}, + {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84dabd95154f43a2ea80deffec9cb44d2e301e38a0c9d331cc4aa0166fe28ae3"}, + {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8a02fbeca6f63cb1f0475c799679057fc9268b77075ab7cf3f1c600e81dd46b"}, + {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c26959ca7b75ff768e2776d8055bf9582a6267e24556bb7f7bd29e677932be72"}, + {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:714d4e5231fed4ba2762ed489b4aec07b2b9953cf4ee31e9871caac895a839c0"}, + {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7a6a8354f1b62e15d48e04350f13e726fa08b62c3d7b8401c0a1314f02e3558"}, + {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c413016880e03e69d166efb5a1a95d40f83d5a3a648d16486592c49ffb76d0db"}, + {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ff84aeb864e0fac81f676be9f4685f0527b660f1efdc40dcede3c251ef1e867f"}, + {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ad7f2919d7dac062f24d6f5fe95d401597fbb015a25771f85e692d043c9d7832"}, + {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:702e2c7c187c1a498a4e2b03155d52658fdd6fda882d3d7fbb891a5cf108bb10"}, + {file = "aiohttp-3.9.5-cp310-cp310-win32.whl", hash = "sha256:67c3119f5ddc7261d47163ed86d760ddf0e625cd6246b4ed852e82159617b5fb"}, + {file = "aiohttp-3.9.5-cp310-cp310-win_amd64.whl", hash = "sha256:471f0ef53ccedec9995287f02caf0c068732f026455f07db3f01a46e49d76bbb"}, + {file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e0ae53e33ee7476dd3d1132f932eeb39bf6125083820049d06edcdca4381f342"}, + {file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c088c4d70d21f8ca5c0b8b5403fe84a7bc8e024161febdd4ef04575ef35d474d"}, + {file = "aiohttp-3.9.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:639d0042b7670222f33b0028de6b4e2fad6451462ce7df2af8aee37dcac55424"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f26383adb94da5e7fb388d441bf09c61e5e35f455a3217bfd790c6b6bc64b2ee"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66331d00fb28dc90aa606d9a54304af76b335ae204d1836f65797d6fe27f1ca2"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ff550491f5492ab5ed3533e76b8567f4b37bd2995e780a1f46bca2024223233"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f22eb3a6c1080d862befa0a89c380b4dafce29dc6cd56083f630073d102eb595"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a81b1143d42b66ffc40a441379387076243ef7b51019204fd3ec36b9f69e77d6"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f64fd07515dad67f24b6ea4a66ae2876c01031de91c93075b8093f07c0a2d93d"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:93e22add827447d2e26d67c9ac0161756007f152fdc5210277d00a85f6c92323"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:55b39c8684a46e56ef8c8d24faf02de4a2b2ac60d26cee93bc595651ff545de9"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4715a9b778f4293b9f8ae7a0a7cef9829f02ff8d6277a39d7f40565c737d3771"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:afc52b8d969eff14e069a710057d15ab9ac17cd4b6753042c407dcea0e40bf75"}, + {file = "aiohttp-3.9.5-cp311-cp311-win32.whl", hash = "sha256:b3df71da99c98534be076196791adca8819761f0bf6e08e07fd7da25127150d6"}, + {file = "aiohttp-3.9.5-cp311-cp311-win_amd64.whl", hash = "sha256:88e311d98cc0bf45b62fc46c66753a83445f5ab20038bcc1b8a1cc05666f428a"}, + {file = "aiohttp-3.9.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:c7a4b7a6cf5b6eb11e109a9755fd4fda7d57395f8c575e166d363b9fc3ec4678"}, + {file = "aiohttp-3.9.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0a158704edf0abcac8ac371fbb54044f3270bdbc93e254a82b6c82be1ef08f3c"}, + {file = "aiohttp-3.9.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d153f652a687a8e95ad367a86a61e8d53d528b0530ef382ec5aaf533140ed00f"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82a6a97d9771cb48ae16979c3a3a9a18b600a8505b1115cfe354dfb2054468b4"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60cdbd56f4cad9f69c35eaac0fbbdf1f77b0ff9456cebd4902f3dd1cf096464c"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8676e8fd73141ded15ea586de0b7cda1542960a7b9ad89b2b06428e97125d4fa"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da00da442a0e31f1c69d26d224e1efd3a1ca5bcbf210978a2ca7426dfcae9f58"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18f634d540dd099c262e9f887c8bbacc959847cfe5da7a0e2e1cf3f14dbf2daf"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:320e8618eda64e19d11bdb3bd04ccc0a816c17eaecb7e4945d01deee2a22f95f"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:2faa61a904b83142747fc6a6d7ad8fccff898c849123030f8e75d5d967fd4a81"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:8c64a6dc3fe5db7b1b4d2b5cb84c4f677768bdc340611eca673afb7cf416ef5a"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:393c7aba2b55559ef7ab791c94b44f7482a07bf7640d17b341b79081f5e5cd1a"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c671dc117c2c21a1ca10c116cfcd6e3e44da7fcde37bf83b2be485ab377b25da"}, + {file = "aiohttp-3.9.5-cp312-cp312-win32.whl", hash = "sha256:5a7ee16aab26e76add4afc45e8f8206c95d1d75540f1039b84a03c3b3800dd59"}, + {file = "aiohttp-3.9.5-cp312-cp312-win_amd64.whl", hash = "sha256:5ca51eadbd67045396bc92a4345d1790b7301c14d1848feaac1d6a6c9289e888"}, + {file = "aiohttp-3.9.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:694d828b5c41255e54bc2dddb51a9f5150b4eefa9886e38b52605a05d96566e8"}, + {file = "aiohttp-3.9.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0605cc2c0088fcaae79f01c913a38611ad09ba68ff482402d3410bf59039bfb8"}, + {file = "aiohttp-3.9.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4558e5012ee03d2638c681e156461d37b7a113fe13970d438d95d10173d25f78"}, + {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dbc053ac75ccc63dc3a3cc547b98c7258ec35a215a92bd9f983e0aac95d3d5b"}, + {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4109adee842b90671f1b689901b948f347325045c15f46b39797ae1bf17019de"}, + {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6ea1a5b409a85477fd8e5ee6ad8f0e40bf2844c270955e09360418cfd09abac"}, + {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3c2890ca8c59ee683fd09adf32321a40fe1cf164e3387799efb2acebf090c11"}, + {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3916c8692dbd9d55c523374a3b8213e628424d19116ac4308e434dbf6d95bbdd"}, + {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8d1964eb7617907c792ca00b341b5ec3e01ae8c280825deadbbd678447b127e1"}, + {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d5ab8e1f6bee051a4bf6195e38a5c13e5e161cb7bad83d8854524798bd9fcd6e"}, + {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:52c27110f3862a1afbcb2af4281fc9fdc40327fa286c4625dfee247c3ba90156"}, + {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:7f64cbd44443e80094309875d4f9c71d0401e966d191c3d469cde4642bc2e031"}, + {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8b4f72fbb66279624bfe83fd5eb6aea0022dad8eec62b71e7bf63ee1caadeafe"}, + {file = "aiohttp-3.9.5-cp38-cp38-win32.whl", hash = "sha256:6380c039ec52866c06d69b5c7aad5478b24ed11696f0e72f6b807cfb261453da"}, + {file = "aiohttp-3.9.5-cp38-cp38-win_amd64.whl", hash = "sha256:da22dab31d7180f8c3ac7c7635f3bcd53808f374f6aa333fe0b0b9e14b01f91a"}, + {file = "aiohttp-3.9.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1732102949ff6087589408d76cd6dea656b93c896b011ecafff418c9661dc4ed"}, + {file = "aiohttp-3.9.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c6021d296318cb6f9414b48e6a439a7f5d1f665464da507e8ff640848ee2a58a"}, + {file = "aiohttp-3.9.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:239f975589a944eeb1bad26b8b140a59a3a320067fb3cd10b75c3092405a1372"}, + {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b7b30258348082826d274504fbc7c849959f1989d86c29bc355107accec6cfb"}, + {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd2adf5c87ff6d8b277814a28a535b59e20bfea40a101db6b3bdca7e9926bc24"}, + {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9a3d838441bebcf5cf442700e3963f58b5c33f015341f9ea86dcd7d503c07e2"}, + {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e3a1ae66e3d0c17cf65c08968a5ee3180c5a95920ec2731f53343fac9bad106"}, + {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c69e77370cce2d6df5d12b4e12bdcca60c47ba13d1cbbc8645dd005a20b738b"}, + {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf56238f4bbf49dab8c2dc2e6b1b68502b1e88d335bea59b3f5b9f4c001475"}, + {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d1469f228cd9ffddd396d9948b8c9cd8022b6d1bf1e40c6f25b0fb90b4f893ed"}, + {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:45731330e754f5811c314901cebdf19dd776a44b31927fa4b4dbecab9e457b0c"}, + {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:3fcb4046d2904378e3aeea1df51f697b0467f2aac55d232c87ba162709478c46"}, + {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8cf142aa6c1a751fcb364158fd710b8a9be874b81889c2bd13aa8893197455e2"}, + {file = "aiohttp-3.9.5-cp39-cp39-win32.whl", hash = "sha256:7b179eea70833c8dee51ec42f3b4097bd6370892fa93f510f76762105568cf09"}, + {file = "aiohttp-3.9.5-cp39-cp39-win_amd64.whl", hash = "sha256:38d80498e2e169bc61418ff36170e0aad0cd268da8b38a17c4cf29d254a8b3f1"}, + {file = "aiohttp-3.9.5.tar.gz", hash = "sha256:edea7d15772ceeb29db4aff55e482d4bcfb6ae160ce144f2682de02f6d693551"}, +] + +[package.dependencies] +aiosignal = ">=1.1.2" +attrs = ">=17.3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +yarl = ">=1.0,<2.0" + +[package.extras] +speedups = ["Brotli", "aiodns", "brotlicffi"] + +[[package]] +name = "aiosignal" +version = "1.3.1" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = false +python-versions = ">=3.7" +files = [ + {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, + {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" + +[[package]] +name = "aiosqlite" +version = "0.20.0" +description = "asyncio bridge to the standard sqlite3 module" +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiosqlite-0.20.0-py3-none-any.whl", hash = "sha256:36a1deaca0cac40ebe32aac9977a6e2bbc7f5189f23f4a54d5908986729e5bd6"}, + {file = "aiosqlite-0.20.0.tar.gz", hash = "sha256:6d35c8c256637f4672f843c31021464090805bf925385ac39473fb16eaaca3d7"}, +] + +[package.dependencies] +typing_extensions = ">=4.0" + +[package.extras] +dev = ["attribution (==1.7.0)", "black (==24.2.0)", "coverage[toml] (==7.4.1)", "flake8 (==7.0.0)", "flake8-bugbear (==24.2.6)", "flit (==3.9.0)", "mypy (==1.8.0)", "ufmt (==2.3.0)", "usort (==1.0.8.post1)"] +docs = ["sphinx (==7.2.6)", "sphinx-mdinclude (==0.5.3)"] + +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[[package]] +name = "attrs" +version = "23.2.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, + {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, +] + +[package.extras] +cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] +dev = ["attrs[tests]", "pre-commit"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] +tests = ["attrs[tests-no-zope]", "zope-interface"] +tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] +tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] + +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + +[[package]] +name = "contourpy" +version = "1.2.1" +description = "Python library for calculating contours of 2D quadrilateral grids" +optional = false +python-versions = ">=3.9" +files = [ + {file = "contourpy-1.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bd7c23df857d488f418439686d3b10ae2fbf9bc256cd045b37a8c16575ea1040"}, + {file = "contourpy-1.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5b9eb0ca724a241683c9685a484da9d35c872fd42756574a7cfbf58af26677fd"}, + {file = "contourpy-1.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c75507d0a55378240f781599c30e7776674dbaf883a46d1c90f37e563453480"}, + {file = "contourpy-1.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11959f0ce4a6f7b76ec578576a0b61a28bdc0696194b6347ba3f1c53827178b9"}, + {file = "contourpy-1.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb3315a8a236ee19b6df481fc5f997436e8ade24a9f03dfdc6bd490fea20c6da"}, + {file = "contourpy-1.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39f3ecaf76cd98e802f094e0d4fbc6dc9c45a8d0c4d185f0f6c2234e14e5f75b"}, + {file = "contourpy-1.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:94b34f32646ca0414237168d68a9157cb3889f06b096612afdd296003fdd32fd"}, + {file = "contourpy-1.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:457499c79fa84593f22454bbd27670227874cd2ff5d6c84e60575c8b50a69619"}, + {file = "contourpy-1.2.1-cp310-cp310-win32.whl", hash = "sha256:ac58bdee53cbeba2ecad824fa8159493f0bf3b8ea4e93feb06c9a465d6c87da8"}, + {file = "contourpy-1.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:9cffe0f850e89d7c0012a1fb8730f75edd4320a0a731ed0c183904fe6ecfc3a9"}, + {file = "contourpy-1.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6022cecf8f44e36af10bd9118ca71f371078b4c168b6e0fab43d4a889985dbb5"}, + {file = "contourpy-1.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef5adb9a3b1d0c645ff694f9bca7702ec2c70f4d734f9922ea34de02294fdf72"}, + {file = "contourpy-1.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6150ffa5c767bc6332df27157d95442c379b7dce3a38dff89c0f39b63275696f"}, + {file = "contourpy-1.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c863140fafc615c14a4bf4efd0f4425c02230eb8ef02784c9a156461e62c965"}, + {file = "contourpy-1.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:00e5388f71c1a0610e6fe56b5c44ab7ba14165cdd6d695429c5cd94021e390b2"}, + {file = "contourpy-1.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4492d82b3bc7fbb7e3610747b159869468079fe149ec5c4d771fa1f614a14df"}, + {file = "contourpy-1.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:49e70d111fee47284d9dd867c9bb9a7058a3c617274900780c43e38d90fe1205"}, + {file = "contourpy-1.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b59c0ffceff8d4d3996a45f2bb6f4c207f94684a96bf3d9728dbb77428dd8cb8"}, + {file = "contourpy-1.2.1-cp311-cp311-win32.whl", hash = "sha256:7b4182299f251060996af5249c286bae9361fa8c6a9cda5efc29fe8bfd6062ec"}, + {file = "contourpy-1.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2855c8b0b55958265e8b5888d6a615ba02883b225f2227461aa9127c578a4922"}, + {file = "contourpy-1.2.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:62828cada4a2b850dbef89c81f5a33741898b305db244904de418cc957ff05dc"}, + {file = "contourpy-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:309be79c0a354afff9ff7da4aaed7c3257e77edf6c1b448a779329431ee79d7e"}, + {file = "contourpy-1.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e785e0f2ef0d567099b9ff92cbfb958d71c2d5b9259981cd9bee81bd194c9a4"}, + {file = "contourpy-1.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1cac0a8f71a041aa587410424ad46dfa6a11f6149ceb219ce7dd48f6b02b87a7"}, + {file = "contourpy-1.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af3f4485884750dddd9c25cb7e3915d83c2db92488b38ccb77dd594eac84c4a0"}, + {file = "contourpy-1.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ce6889abac9a42afd07a562c2d6d4b2b7134f83f18571d859b25624a331c90b"}, + {file = "contourpy-1.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a1eea9aecf761c661d096d39ed9026574de8adb2ae1c5bd7b33558af884fb2ce"}, + {file = "contourpy-1.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:187fa1d4c6acc06adb0fae5544c59898ad781409e61a926ac7e84b8f276dcef4"}, + {file = "contourpy-1.2.1-cp312-cp312-win32.whl", hash = "sha256:c2528d60e398c7c4c799d56f907664673a807635b857df18f7ae64d3e6ce2d9f"}, + {file = "contourpy-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:1a07fc092a4088ee952ddae19a2b2a85757b923217b7eed584fdf25f53a6e7ce"}, + {file = "contourpy-1.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bb6834cbd983b19f06908b45bfc2dad6ac9479ae04abe923a275b5f48f1a186b"}, + {file = "contourpy-1.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1d59e739ab0e3520e62a26c60707cc3ab0365d2f8fecea74bfe4de72dc56388f"}, + {file = "contourpy-1.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd3db01f59fdcbce5b22afad19e390260d6d0222f35a1023d9adc5690a889364"}, + {file = "contourpy-1.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a12a813949e5066148712a0626895c26b2578874e4cc63160bb007e6df3436fe"}, + {file = "contourpy-1.2.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe0ccca550bb8e5abc22f530ec0466136379c01321fd94f30a22231e8a48d985"}, + {file = "contourpy-1.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1d59258c3c67c865435d8fbeb35f8c59b8bef3d6f46c1f29f6123556af28445"}, + {file = "contourpy-1.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f32c38afb74bd98ce26de7cc74a67b40afb7b05aae7b42924ea990d51e4dac02"}, + {file = "contourpy-1.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d31a63bc6e6d87f77d71e1abbd7387ab817a66733734883d1fc0021ed9bfa083"}, + {file = "contourpy-1.2.1-cp39-cp39-win32.whl", hash = "sha256:ddcb8581510311e13421b1f544403c16e901c4e8f09083c881fab2be80ee31ba"}, + {file = "contourpy-1.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:10a37ae557aabf2509c79715cd20b62e4c7c28b8cd62dd7d99e5ed3ce28c3fd9"}, + {file = "contourpy-1.2.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a31f94983fecbac95e58388210427d68cd30fe8a36927980fab9c20062645609"}, + {file = "contourpy-1.2.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef2b055471c0eb466033760a521efb9d8a32b99ab907fc8358481a1dd29e3bd3"}, + {file = "contourpy-1.2.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b33d2bc4f69caedcd0a275329eb2198f560b325605810895627be5d4b876bf7f"}, + {file = "contourpy-1.2.1.tar.gz", hash = "sha256:4d8908b3bee1c889e547867ca4cdc54e5ab6be6d3e078556814a22457f49423c"}, +] + +[package.dependencies] +numpy = ">=1.20" + +[package.extras] +bokeh = ["bokeh", "selenium"] +docs = ["furo", "sphinx (>=7.2)", "sphinx-copybutton"] +mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.8.0)", "types-Pillow"] +test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] +test-no-images = ["pytest", "pytest-cov", "pytest-xdist", "wurlitzer"] + +[[package]] +name = "cycler" +version = "0.12.1" +description = "Composable style cycles" +optional = false +python-versions = ">=3.8" +files = [ + {file = "cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30"}, + {file = "cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"}, +] + +[package.extras] +docs = ["ipython", "matplotlib", "numpydoc", "sphinx"] +tests = ["pytest", "pytest-cov", "pytest-xdist"] + +[[package]] +name = "discord-py" +version = "2.4.0" +description = "A Python wrapper for the Discord API" +optional = false +python-versions = ">=3.8" +files = [ + {file = "discord.py-2.4.0-py3-none-any.whl", hash = "sha256:b8af6711c70f7e62160bfbecb55be699b5cb69d007426759ab8ab06b1bd77d1d"}, + {file = "discord_py-2.4.0.tar.gz", hash = "sha256:d07cb2a223a185873a1d0ee78b9faa9597e45b3f6186df21a95cec1e9bcdc9a5"}, +] + +[package.dependencies] +aiohttp = ">=3.7.4,<4" + +[package.extras] +docs = ["sphinx (==4.4.0)", "sphinx-inline-tabs (==2023.4.21)", "sphinxcontrib-applehelp (==1.0.4)", "sphinxcontrib-devhelp (==1.0.2)", "sphinxcontrib-htmlhelp (==2.0.1)", "sphinxcontrib-jsmath (==1.0.1)", "sphinxcontrib-qthelp (==1.0.3)", "sphinxcontrib-serializinghtml (==1.1.5)", "sphinxcontrib-trio (==1.1.2)", "sphinxcontrib-websupport (==1.2.4)", "typing-extensions (>=4.3,<5)"] +speed = ["Brotli", "aiodns (>=1.1)", "cchardet (==2.1.7)", "orjson (>=3.5.4)"] +test = ["coverage[toml]", "pytest", "pytest-asyncio", "pytest-cov", "pytest-mock", "typing-extensions (>=4.3,<5)", "tzdata"] +voice = ["PyNaCl (>=1.3.0,<1.6)"] + +[[package]] +name = "distlib" +version = "0.3.8" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, +] + +[[package]] +name = "emoji" +version = "2.12.1" +description = "Emoji for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "emoji-2.12.1-py3-none-any.whl", hash = "sha256:a00d62173bdadc2510967a381810101624a2f0986145b8da0cffa42e29430235"}, + {file = "emoji-2.12.1.tar.gz", hash = "sha256:4aa0488817691aa58d83764b6c209f8a27c0b3ab3f89d1b8dceca1a62e4973eb"}, +] + +[package.dependencies] +typing-extensions = ">=4.7.0" + +[package.extras] +dev = ["coverage", "pytest (>=7.4.4)"] + +[[package]] +name = "filelock" +version = "3.15.4" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"}, + {file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"] +typing = ["typing-extensions (>=4.8)"] + +[[package]] +name = "fonttools" +version = "4.53.1" +description = "Tools to manipulate font files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fonttools-4.53.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0679a30b59d74b6242909945429dbddb08496935b82f91ea9bf6ad240ec23397"}, + {file = "fonttools-4.53.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8bf06b94694251861ba7fdeea15c8ec0967f84c3d4143ae9daf42bbc7717fe3"}, + {file = "fonttools-4.53.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b96cd370a61f4d083c9c0053bf634279b094308d52fdc2dd9a22d8372fdd590d"}, + {file = "fonttools-4.53.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1c7c5aa18dd3b17995898b4a9b5929d69ef6ae2af5b96d585ff4005033d82f0"}, + {file = "fonttools-4.53.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e013aae589c1c12505da64a7d8d023e584987e51e62006e1bb30d72f26522c41"}, + {file = "fonttools-4.53.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9efd176f874cb6402e607e4cc9b4a9cd584d82fc34a4b0c811970b32ba62501f"}, + {file = "fonttools-4.53.1-cp310-cp310-win32.whl", hash = "sha256:c8696544c964500aa9439efb6761947393b70b17ef4e82d73277413f291260a4"}, + {file = "fonttools-4.53.1-cp310-cp310-win_amd64.whl", hash = "sha256:8959a59de5af6d2bec27489e98ef25a397cfa1774b375d5787509c06659b3671"}, + {file = "fonttools-4.53.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:da33440b1413bad53a8674393c5d29ce64d8c1a15ef8a77c642ffd900d07bfe1"}, + {file = "fonttools-4.53.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ff7e5e9bad94e3a70c5cd2fa27f20b9bb9385e10cddab567b85ce5d306ea923"}, + {file = "fonttools-4.53.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6e7170d675d12eac12ad1a981d90f118c06cf680b42a2d74c6c931e54b50719"}, + {file = "fonttools-4.53.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bee32ea8765e859670c4447b0817514ca79054463b6b79784b08a8df3a4d78e3"}, + {file = "fonttools-4.53.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6e08f572625a1ee682115223eabebc4c6a2035a6917eac6f60350aba297ccadb"}, + {file = "fonttools-4.53.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b21952c092ffd827504de7e66b62aba26fdb5f9d1e435c52477e6486e9d128b2"}, + {file = "fonttools-4.53.1-cp311-cp311-win32.whl", hash = "sha256:9dfdae43b7996af46ff9da520998a32b105c7f098aeea06b2226b30e74fbba88"}, + {file = "fonttools-4.53.1-cp311-cp311-win_amd64.whl", hash = "sha256:d4d0096cb1ac7a77b3b41cd78c9b6bc4a400550e21dc7a92f2b5ab53ed74eb02"}, + {file = "fonttools-4.53.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d92d3c2a1b39631a6131c2fa25b5406855f97969b068e7e08413325bc0afba58"}, + {file = "fonttools-4.53.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3b3c8ebafbee8d9002bd8f1195d09ed2bd9ff134ddec37ee8f6a6375e6a4f0e8"}, + {file = "fonttools-4.53.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32f029c095ad66c425b0ee85553d0dc326d45d7059dbc227330fc29b43e8ba60"}, + {file = "fonttools-4.53.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10f5e6c3510b79ea27bb1ebfcc67048cde9ec67afa87c7dd7efa5c700491ac7f"}, + {file = "fonttools-4.53.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f677ce218976496a587ab17140da141557beb91d2a5c1a14212c994093f2eae2"}, + {file = "fonttools-4.53.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9e6ceba2a01b448e36754983d376064730690401da1dd104ddb543519470a15f"}, + {file = "fonttools-4.53.1-cp312-cp312-win32.whl", hash = "sha256:791b31ebbc05197d7aa096bbc7bd76d591f05905d2fd908bf103af4488e60670"}, + {file = "fonttools-4.53.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ed170b5e17da0264b9f6fae86073be3db15fa1bd74061c8331022bca6d09bab"}, + {file = "fonttools-4.53.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c818c058404eb2bba05e728d38049438afd649e3c409796723dfc17cd3f08749"}, + {file = "fonttools-4.53.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:651390c3b26b0c7d1f4407cad281ee7a5a85a31a110cbac5269de72a51551ba2"}, + {file = "fonttools-4.53.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e54f1bba2f655924c1138bbc7fa91abd61f45c68bd65ab5ed985942712864bbb"}, + {file = "fonttools-4.53.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9cd19cf4fe0595ebdd1d4915882b9440c3a6d30b008f3cc7587c1da7b95be5f"}, + {file = "fonttools-4.53.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2af40ae9cdcb204fc1d8f26b190aa16534fcd4f0df756268df674a270eab575d"}, + {file = "fonttools-4.53.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:35250099b0cfb32d799fb5d6c651220a642fe2e3c7d2560490e6f1d3f9ae9169"}, + {file = "fonttools-4.53.1-cp38-cp38-win32.whl", hash = "sha256:f08df60fbd8d289152079a65da4e66a447efc1d5d5a4d3f299cdd39e3b2e4a7d"}, + {file = "fonttools-4.53.1-cp38-cp38-win_amd64.whl", hash = "sha256:7b6b35e52ddc8fb0db562133894e6ef5b4e54e1283dff606fda3eed938c36fc8"}, + {file = "fonttools-4.53.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:75a157d8d26c06e64ace9df037ee93a4938a4606a38cb7ffaf6635e60e253b7a"}, + {file = "fonttools-4.53.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4824c198f714ab5559c5be10fd1adf876712aa7989882a4ec887bf1ef3e00e31"}, + {file = "fonttools-4.53.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:becc5d7cb89c7b7afa8321b6bb3dbee0eec2b57855c90b3e9bf5fb816671fa7c"}, + {file = "fonttools-4.53.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84ec3fb43befb54be490147b4a922b5314e16372a643004f182babee9f9c3407"}, + {file = "fonttools-4.53.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:73379d3ffdeecb376640cd8ed03e9d2d0e568c9d1a4e9b16504a834ebadc2dfb"}, + {file = "fonttools-4.53.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:02569e9a810f9d11f4ae82c391ebc6fb5730d95a0657d24d754ed7763fb2d122"}, + {file = "fonttools-4.53.1-cp39-cp39-win32.whl", hash = "sha256:aae7bd54187e8bf7fd69f8ab87b2885253d3575163ad4d669a262fe97f0136cb"}, + {file = "fonttools-4.53.1-cp39-cp39-win_amd64.whl", hash = "sha256:e5b708073ea3d684235648786f5f6153a48dc8762cdfe5563c57e80787c29fbb"}, + {file = "fonttools-4.53.1-py3-none-any.whl", hash = "sha256:f1f8758a2ad110bd6432203a344269f445a2907dc24ef6bccfd0ac4e14e0d71d"}, + {file = "fonttools-4.53.1.tar.gz", hash = "sha256:e128778a8e9bc11159ce5447f76766cefbd876f44bd79aff030287254e4752c4"}, +] + +[package.extras] +all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "pycairo", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.1.0)", "xattr", "zopfli (>=0.1.4)"] +graphite = ["lz4 (>=1.7.4.2)"] +interpolatable = ["munkres", "pycairo", "scipy"] +lxml = ["lxml (>=4.0)"] +pathops = ["skia-pathops (>=0.5.0)"] +plot = ["matplotlib"] +repacker = ["uharfbuzz (>=0.23.0)"] +symfont = ["sympy"] +type1 = ["xattr"] +ufo = ["fs (>=2.2.0,<3)"] +unicode = ["unicodedata2 (>=15.1.0)"] +woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] + +[[package]] +name = "frozenlist" +version = "1.4.1" +description = "A list-like structure which implements collections.abc.MutableSequence" +optional = false +python-versions = ">=3.8" +files = [ + {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"}, + {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"}, + {file = "frozenlist-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc"}, + {file = "frozenlist-1.4.1-cp310-cp310-win32.whl", hash = "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1"}, + {file = "frozenlist-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439"}, + {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0"}, + {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49"}, + {file = "frozenlist-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2"}, + {file = "frozenlist-1.4.1-cp311-cp311-win32.whl", hash = "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17"}, + {file = "frozenlist-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8"}, + {file = "frozenlist-1.4.1-cp312-cp312-win32.whl", hash = "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89"}, + {file = "frozenlist-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5"}, + {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d"}, + {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826"}, + {file = "frozenlist-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7"}, + {file = "frozenlist-1.4.1-cp38-cp38-win32.whl", hash = "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497"}, + {file = "frozenlist-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09"}, + {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e"}, + {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d"}, + {file = "frozenlist-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6"}, + {file = "frozenlist-1.4.1-cp39-cp39-win32.whl", hash = "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932"}, + {file = "frozenlist-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0"}, + {file = "frozenlist-1.4.1-py3-none-any.whl", hash = "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7"}, + {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"}, +] + +[[package]] +name = "identify" +version = "2.6.0" +description = "File identification library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"}, + {file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.7" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, +] + +[[package]] +name = "kiwisolver" +version = "1.4.5" +description = "A fast implementation of the Cassowary constraint solver" +optional = false +python-versions = ">=3.7" +files = [ + {file = "kiwisolver-1.4.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:05703cf211d585109fcd72207a31bb170a0f22144d68298dc5e61b3c946518af"}, + {file = "kiwisolver-1.4.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:146d14bebb7f1dc4d5fbf74f8a6cb15ac42baadee8912eb84ac0b3b2a3dc6ac3"}, + {file = "kiwisolver-1.4.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ef7afcd2d281494c0a9101d5c571970708ad911d028137cd558f02b851c08b4"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9eaa8b117dc8337728e834b9c6e2611f10c79e38f65157c4c38e9400286f5cb1"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ec20916e7b4cbfb1f12380e46486ec4bcbaa91a9c448b97023fde0d5bbf9e4ff"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39b42c68602539407884cf70d6a480a469b93b81b7701378ba5e2328660c847a"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa12042de0171fad672b6c59df69106d20d5596e4f87b5e8f76df757a7c399aa"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a40773c71d7ccdd3798f6489aaac9eee213d566850a9533f8d26332d626b82c"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:19df6e621f6d8b4b9c4d45f40a66839294ff2bb235e64d2178f7522d9170ac5b"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:83d78376d0d4fd884e2c114d0621624b73d2aba4e2788182d286309ebdeed770"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e391b1f0a8a5a10ab3b9bb6afcfd74f2175f24f8975fb87ecae700d1503cdee0"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:852542f9481f4a62dbb5dd99e8ab7aedfeb8fb6342349a181d4036877410f525"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59edc41b24031bc25108e210c0def6f6c2191210492a972d585a06ff246bb79b"}, + {file = "kiwisolver-1.4.5-cp310-cp310-win32.whl", hash = "sha256:a6aa6315319a052b4ee378aa171959c898a6183f15c1e541821c5c59beaa0238"}, + {file = "kiwisolver-1.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:d0ef46024e6a3d79c01ff13801cb19d0cad7fd859b15037aec74315540acc276"}, + {file = "kiwisolver-1.4.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:11863aa14a51fd6ec28688d76f1735f8f69ab1fabf388851a595d0721af042f5"}, + {file = "kiwisolver-1.4.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8ab3919a9997ab7ef2fbbed0cc99bb28d3c13e6d4b1ad36e97e482558a91be90"}, + {file = "kiwisolver-1.4.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fcc700eadbbccbf6bc1bcb9dbe0786b4b1cb91ca0dcda336eef5c2beed37b797"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dfdd7c0b105af050eb3d64997809dc21da247cf44e63dc73ff0fd20b96be55a9"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76c6a5964640638cdeaa0c359382e5703e9293030fe730018ca06bc2010c4437"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbea0db94288e29afcc4c28afbf3a7ccaf2d7e027489c449cf7e8f83c6346eb9"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ceec1a6bc6cab1d6ff5d06592a91a692f90ec7505d6463a88a52cc0eb58545da"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:040c1aebeda72197ef477a906782b5ab0d387642e93bda547336b8957c61022e"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f91de7223d4c7b793867797bacd1ee53bfe7359bd70d27b7b58a04efbb9436c8"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:faae4860798c31530dd184046a900e652c95513796ef51a12bc086710c2eec4d"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:b0157420efcb803e71d1b28e2c287518b8808b7cf1ab8af36718fd0a2c453eb0"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:06f54715b7737c2fecdbf140d1afb11a33d59508a47bf11bb38ecf21dc9ab79f"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fdb7adb641a0d13bdcd4ef48e062363d8a9ad4a182ac7647ec88f695e719ae9f"}, + {file = "kiwisolver-1.4.5-cp311-cp311-win32.whl", hash = "sha256:bb86433b1cfe686da83ce32a9d3a8dd308e85c76b60896d58f082136f10bffac"}, + {file = "kiwisolver-1.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:6c08e1312a9cf1074d17b17728d3dfce2a5125b2d791527f33ffbe805200a355"}, + {file = "kiwisolver-1.4.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:32d5cf40c4f7c7b3ca500f8985eb3fb3a7dfc023215e876f207956b5ea26632a"}, + {file = "kiwisolver-1.4.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f846c260f483d1fd217fe5ed7c173fb109efa6b1fc8381c8b7552c5781756192"}, + {file = "kiwisolver-1.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5ff5cf3571589b6d13bfbfd6bcd7a3f659e42f96b5fd1c4830c4cf21d4f5ef45"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7269d9e5f1084a653d575c7ec012ff57f0c042258bf5db0954bf551c158466e7"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da802a19d6e15dffe4b0c24b38b3af68e6c1a68e6e1d8f30148c83864f3881db"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3aba7311af82e335dd1e36ffff68aaca609ca6290c2cb6d821a39aa075d8e3ff"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:763773d53f07244148ccac5b084da5adb90bfaee39c197554f01b286cf869228"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2270953c0d8cdab5d422bee7d2007f043473f9d2999631c86a223c9db56cbd16"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d099e745a512f7e3bbe7249ca835f4d357c586d78d79ae8f1dcd4d8adeb9bda9"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:74db36e14a7d1ce0986fa104f7d5637aea5c82ca6326ed0ec5694280942d1162"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e5bab140c309cb3a6ce373a9e71eb7e4873c70c2dda01df6820474f9889d6d4"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0f114aa76dc1b8f636d077979c0ac22e7cd8f3493abbab152f20eb8d3cda71f3"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:88a2df29d4724b9237fc0c6eaf2a1adae0cdc0b3e9f4d8e7dc54b16812d2d81a"}, + {file = "kiwisolver-1.4.5-cp312-cp312-win32.whl", hash = "sha256:72d40b33e834371fd330fb1472ca19d9b8327acb79a5821d4008391db8e29f20"}, + {file = "kiwisolver-1.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:2c5674c4e74d939b9d91dda0fae10597ac7521768fec9e399c70a1f27e2ea2d9"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3a2b053a0ab7a3960c98725cfb0bf5b48ba82f64ec95fe06f1d06c99b552e130"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cd32d6c13807e5c66a7cbb79f90b553642f296ae4518a60d8d76243b0ad2898"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59ec7b7c7e1a61061850d53aaf8e93db63dce0c936db1fda2658b70e4a1be709"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da4cfb373035def307905d05041c1d06d8936452fe89d464743ae7fb8371078b"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2400873bccc260b6ae184b2b8a4fec0e4082d30648eadb7c3d9a13405d861e89"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1b04139c4236a0f3aff534479b58f6f849a8b351e1314826c2d230849ed48985"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:4e66e81a5779b65ac21764c295087de82235597a2293d18d943f8e9e32746265"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:7931d8f1f67c4be9ba1dd9c451fb0eeca1a25b89e4d3f89e828fe12a519b782a"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b3f7e75f3015df442238cca659f8baa5f42ce2a8582727981cbfa15fee0ee205"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:bbf1d63eef84b2e8c89011b7f2235b1e0bf7dacc11cac9431fc6468e99ac77fb"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4c380469bd3f970ef677bf2bcba2b6b0b4d5c75e7a020fb863ef75084efad66f"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-win32.whl", hash = "sha256:9408acf3270c4b6baad483865191e3e582b638b1654a007c62e3efe96f09a9a3"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-win_amd64.whl", hash = "sha256:5b94529f9b2591b7af5f3e0e730a4e0a41ea174af35a4fd067775f9bdfeee01a"}, + {file = "kiwisolver-1.4.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:11c7de8f692fc99816e8ac50d1d1aef4f75126eefc33ac79aac02c099fd3db71"}, + {file = "kiwisolver-1.4.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:53abb58632235cd154176ced1ae8f0d29a6657aa1aa9decf50b899b755bc2b93"}, + {file = "kiwisolver-1.4.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:88b9f257ca61b838b6f8094a62418421f87ac2a1069f7e896c36a7d86b5d4c29"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3195782b26fc03aa9c6913d5bad5aeb864bdc372924c093b0f1cebad603dd712"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc579bf0f502e54926519451b920e875f433aceb4624a3646b3252b5caa9e0b6"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a580c91d686376f0f7c295357595c5a026e6cbc3d77b7c36e290201e7c11ecb"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cfe6ab8da05c01ba6fbea630377b5da2cd9bcbc6338510116b01c1bc939a2c18"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d2e5a98f0ec99beb3c10e13b387f8db39106d53993f498b295f0c914328b1333"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a51a263952b1429e429ff236d2f5a21c5125437861baeed77f5e1cc2d2c7c6da"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3edd2fa14e68c9be82c5b16689e8d63d89fe927e56debd6e1dbce7a26a17f81b"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:74d1b44c6cfc897df648cc9fdaa09bc3e7679926e6f96df05775d4fb3946571c"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:76d9289ed3f7501012e05abb8358bbb129149dbd173f1f57a1bf1c22d19ab7cc"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:92dea1ffe3714fa8eb6a314d2b3c773208d865a0e0d35e713ec54eea08a66250"}, + {file = "kiwisolver-1.4.5-cp38-cp38-win32.whl", hash = "sha256:5c90ae8c8d32e472be041e76f9d2f2dbff4d0b0be8bd4041770eddb18cf49a4e"}, + {file = "kiwisolver-1.4.5-cp38-cp38-win_amd64.whl", hash = "sha256:c7940c1dc63eb37a67721b10d703247552416f719c4188c54e04334321351ced"}, + {file = "kiwisolver-1.4.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9407b6a5f0d675e8a827ad8742e1d6b49d9c1a1da5d952a67d50ef5f4170b18d"}, + {file = "kiwisolver-1.4.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15568384086b6df3c65353820a4473575dbad192e35010f622c6ce3eebd57af9"}, + {file = "kiwisolver-1.4.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0dc9db8e79f0036e8173c466d21ef18e1befc02de8bf8aa8dc0813a6dc8a7046"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:cdc8a402aaee9a798b50d8b827d7ecf75edc5fb35ea0f91f213ff927c15f4ff0"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6c3bd3cde54cafb87d74d8db50b909705c62b17c2099b8f2e25b461882e544ff"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:955e8513d07a283056b1396e9a57ceddbd272d9252c14f154d450d227606eb54"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:346f5343b9e3f00b8db8ba359350eb124b98c99efd0b408728ac6ebf38173958"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9098e0049e88c6a24ff64545cdfc50807818ba6c1b739cae221bbbcbc58aad3"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:00bd361b903dc4bbf4eb165f24d1acbee754fce22ded24c3d56eec268658a5cf"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7b8b454bac16428b22560d0a1cf0a09875339cab69df61d7805bf48919415901"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f1d072c2eb0ad60d4c183f3fb44ac6f73fb7a8f16a2694a91f988275cbf352f9"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:31a82d498054cac9f6d0b53d02bb85811185bcb477d4b60144f915f3b3126342"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6512cb89e334e4700febbffaaa52761b65b4f5a3cf33f960213d5656cea36a77"}, + {file = "kiwisolver-1.4.5-cp39-cp39-win32.whl", hash = "sha256:9db8ea4c388fdb0f780fe91346fd438657ea602d58348753d9fb265ce1bca67f"}, + {file = "kiwisolver-1.4.5-cp39-cp39-win_amd64.whl", hash = "sha256:59415f46a37f7f2efeec758353dd2eae1b07640d8ca0f0c42548ec4125492635"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5c7b3b3a728dc6faf3fc372ef24f21d1e3cee2ac3e9596691d746e5a536de920"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:620ced262a86244e2be10a676b646f29c34537d0d9cc8eb26c08f53d98013390"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:378a214a1e3bbf5ac4a8708304318b4f890da88c9e6a07699c4ae7174c09a68d"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf7be1207676ac608a50cd08f102f6742dbfc70e8d60c4db1c6897f62f71523"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ba55dce0a9b8ff59495ddd050a0225d58bd0983d09f87cfe2b6aec4f2c1234e4"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fd32ea360bcbb92d28933fc05ed09bffcb1704ba3fc7942e81db0fd4f81a7892"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5e7139af55d1688f8b960ee9ad5adafc4ac17c1c473fe07133ac092310d76544"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dced8146011d2bc2e883f9bd68618b8247387f4bbec46d7392b3c3b032640126"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9bf3325c47b11b2e51bca0824ea217c7cd84491d8ac4eefd1e409705ef092bd"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5794cf59533bc3f1b1c821f7206a3617999db9fbefc345360aafe2e067514929"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e368f200bbc2e4f905b8e71eb38b3c04333bddaa6a2464a6355487b02bb7fb09"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5d706eba36b4c4d5bc6c6377bb6568098765e990cfc21ee16d13963fab7b3e7"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85267bd1aa8880a9c88a8cb71e18d3d64d2751a790e6ca6c27b8ccc724bcd5ad"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:210ef2c3a1f03272649aff1ef992df2e724748918c4bc2d5a90352849eb40bea"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:11d011a7574eb3b82bcc9c1a1d35c1d7075677fdd15de527d91b46bd35e935ee"}, + {file = "kiwisolver-1.4.5.tar.gz", hash = "sha256:e57e563a57fb22a142da34f38acc2fc1a5c864bc29ca1517a88abc963e60d6ec"}, +] + +[[package]] +name = "matplotlib" +version = "3.9.1" +description = "Python plotting package" +optional = false +python-versions = ">=3.9" +files = [ + {file = "matplotlib-3.9.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:7ccd6270066feb9a9d8e0705aa027f1ff39f354c72a87efe8fa07632f30fc6bb"}, + {file = "matplotlib-3.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:591d3a88903a30a6d23b040c1e44d1afdd0d778758d07110eb7596f811f31842"}, + {file = "matplotlib-3.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd2a59ff4b83d33bca3b5ec58203cc65985367812cb8c257f3e101632be86d92"}, + {file = "matplotlib-3.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc001516ffcf1a221beb51198b194d9230199d6842c540108e4ce109ac05cc0"}, + {file = "matplotlib-3.9.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:83c6a792f1465d174c86d06f3ae85a8fe36e6f5964633ae8106312ec0921fdf5"}, + {file = "matplotlib-3.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:421851f4f57350bcf0811edd754a708d2275533e84f52f6760b740766c6747a7"}, + {file = "matplotlib-3.9.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b3fce58971b465e01b5c538f9d44915640c20ec5ff31346e963c9e1cd66fa812"}, + {file = "matplotlib-3.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a973c53ad0668c53e0ed76b27d2eeeae8799836fd0d0caaa4ecc66bf4e6676c0"}, + {file = "matplotlib-3.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82cd5acf8f3ef43f7532c2f230249720f5dc5dd40ecafaf1c60ac8200d46d7eb"}, + {file = "matplotlib-3.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab38a4f3772523179b2f772103d8030215b318fef6360cb40558f585bf3d017f"}, + {file = "matplotlib-3.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2315837485ca6188a4b632c5199900e28d33b481eb083663f6a44cfc8987ded3"}, + {file = "matplotlib-3.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:a0c977c5c382f6696caf0bd277ef4f936da7e2aa202ff66cad5f0ac1428ee15b"}, + {file = "matplotlib-3.9.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:565d572efea2b94f264dd86ef27919515aa6d629252a169b42ce5f570db7f37b"}, + {file = "matplotlib-3.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d397fd8ccc64af2ec0af1f0efc3bacd745ebfb9d507f3f552e8adb689ed730a"}, + {file = "matplotlib-3.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26040c8f5121cd1ad712abffcd4b5222a8aec3a0fe40bc8542c94331deb8780d"}, + {file = "matplotlib-3.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d12cb1837cffaac087ad6b44399d5e22b78c729de3cdae4629e252067b705e2b"}, + {file = "matplotlib-3.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0e835c6988edc3d2d08794f73c323cc62483e13df0194719ecb0723b564e0b5c"}, + {file = "matplotlib-3.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:44a21d922f78ce40435cb35b43dd7d573cf2a30138d5c4b709d19f00e3907fd7"}, + {file = "matplotlib-3.9.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:0c584210c755ae921283d21d01f03a49ef46d1afa184134dd0f95b0202ee6f03"}, + {file = "matplotlib-3.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:11fed08f34fa682c2b792942f8902e7aefeed400da71f9e5816bea40a7ce28fe"}, + {file = "matplotlib-3.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0000354e32efcfd86bda75729716b92f5c2edd5b947200be9881f0a671565c33"}, + {file = "matplotlib-3.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4db17fea0ae3aceb8e9ac69c7e3051bae0b3d083bfec932240f9bf5d0197a049"}, + {file = "matplotlib-3.9.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:208cbce658b72bf6a8e675058fbbf59f67814057ae78165d8a2f87c45b48d0ff"}, + {file = "matplotlib-3.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:dc23f48ab630474264276be156d0d7710ac6c5a09648ccdf49fef9200d8cbe80"}, + {file = "matplotlib-3.9.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3fda72d4d472e2ccd1be0e9ccb6bf0d2eaf635e7f8f51d737ed7e465ac020cb3"}, + {file = "matplotlib-3.9.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:84b3ba8429935a444f1fdc80ed930babbe06725bcf09fbeb5c8757a2cd74af04"}, + {file = "matplotlib-3.9.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b918770bf3e07845408716e5bbda17eadfc3fcbd9307dc67f37d6cf834bb3d98"}, + {file = "matplotlib-3.9.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f1f2e5d29e9435c97ad4c36fb6668e89aee13d48c75893e25cef064675038ac9"}, + {file = "matplotlib-3.9.1.tar.gz", hash = "sha256:de06b19b8db95dd33d0dc17c926c7c9ebed9f572074b6fac4f65068a6814d010"}, +] + +[package.dependencies] +contourpy = ">=1.0.1" +cycler = ">=0.10" +fonttools = ">=4.22.0" +kiwisolver = ">=1.3.1" +numpy = ">=1.23" +packaging = ">=20.0" +pillow = ">=8" +pyparsing = ">=2.3.1" +python-dateutil = ">=2.7" + +[package.extras] +dev = ["meson-python (>=0.13.1)", "numpy (>=1.25)", "pybind11 (>=2.6)", "setuptools (>=64)", "setuptools_scm (>=7)"] + +[[package]] +name = "multidict" +version = "6.0.5" +description = "multidict implementation" +optional = false +python-versions = ">=3.7" +files = [ + {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9"}, + {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604"}, + {file = "multidict-6.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc"}, + {file = "multidict-6.0.5-cp310-cp310-win32.whl", hash = "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319"}, + {file = "multidict-6.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e"}, + {file = "multidict-6.0.5-cp311-cp311-win32.whl", hash = "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c"}, + {file = "multidict-6.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda"}, + {file = "multidict-6.0.5-cp312-cp312-win32.whl", hash = "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5"}, + {file = "multidict-6.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556"}, + {file = "multidict-6.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc"}, + {file = "multidict-6.0.5-cp37-cp37m-win32.whl", hash = "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee"}, + {file = "multidict-6.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44"}, + {file = "multidict-6.0.5-cp38-cp38-win32.whl", hash = "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241"}, + {file = "multidict-6.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c"}, + {file = "multidict-6.0.5-cp39-cp39-win32.whl", hash = "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b"}, + {file = "multidict-6.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755"}, + {file = "multidict-6.0.5-py3-none-any.whl", hash = "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7"}, + {file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"}, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +description = "Node.js virtual environment builder" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, +] + +[[package]] +name = "numpy" +version = "2.0.0" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "numpy-2.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:04494f6ec467ccb5369d1808570ae55f6ed9b5809d7f035059000a37b8d7e86f"}, + {file = "numpy-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2635dbd200c2d6faf2ef9a0d04f0ecc6b13b3cad54f7c67c61155138835515d2"}, + {file = "numpy-2.0.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:0a43f0974d501842866cc83471bdb0116ba0dffdbaac33ec05e6afed5b615238"}, + {file = "numpy-2.0.0-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:8d83bb187fb647643bd56e1ae43f273c7f4dbcdf94550d7938cfc32566756514"}, + {file = "numpy-2.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79e843d186c8fb1b102bef3e2bc35ef81160ffef3194646a7fdd6a73c6b97196"}, + {file = "numpy-2.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d7696c615765091cc5093f76fd1fa069870304beaccfd58b5dcc69e55ef49c1"}, + {file = "numpy-2.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b4c76e3d4c56f145d41b7b6751255feefae92edbc9a61e1758a98204200f30fc"}, + {file = "numpy-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:acd3a644e4807e73b4e1867b769fbf1ce8c5d80e7caaef0d90dcdc640dfc9787"}, + {file = "numpy-2.0.0-cp310-cp310-win32.whl", hash = "sha256:cee6cc0584f71adefe2c908856ccc98702baf95ff80092e4ca46061538a2ba98"}, + {file = "numpy-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:ed08d2703b5972ec736451b818c2eb9da80d66c3e84aed1deeb0c345fefe461b"}, + {file = "numpy-2.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad0c86f3455fbd0de6c31a3056eb822fc939f81b1618f10ff3406971893b62a5"}, + {file = "numpy-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7f387600d424f91576af20518334df3d97bc76a300a755f9a8d6e4f5cadd289"}, + {file = "numpy-2.0.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:34f003cb88b1ba38cb9a9a4a3161c1604973d7f9d5552c38bc2f04f829536609"}, + {file = "numpy-2.0.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:b6f6a8f45d0313db07d6d1d37bd0b112f887e1369758a5419c0370ba915b3871"}, + {file = "numpy-2.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f64641b42b2429f56ee08b4f427a4d2daf916ec59686061de751a55aafa22e4"}, + {file = "numpy-2.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7039a136017eaa92c1848152827e1424701532ca8e8967fe480fe1569dae581"}, + {file = "numpy-2.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:46e161722e0f619749d1cd892167039015b2c2817296104487cd03ed4a955995"}, + {file = "numpy-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0e50842b2295ba8414c8c1d9d957083d5dfe9e16828b37de883f51fc53c4016f"}, + {file = "numpy-2.0.0-cp311-cp311-win32.whl", hash = "sha256:2ce46fd0b8a0c947ae047d222f7136fc4d55538741373107574271bc00e20e8f"}, + {file = "numpy-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:fbd6acc766814ea6443628f4e6751d0da6593dae29c08c0b2606164db026970c"}, + {file = "numpy-2.0.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:354f373279768fa5a584bac997de6a6c9bc535c482592d7a813bb0c09be6c76f"}, + {file = "numpy-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4d2f62e55a4cd9c58c1d9a1c9edaedcd857a73cb6fda875bf79093f9d9086f85"}, + {file = "numpy-2.0.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:1e72728e7501a450288fc8e1f9ebc73d90cfd4671ebbd631f3e7857c39bd16f2"}, + {file = "numpy-2.0.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:84554fc53daa8f6abf8e8a66e076aff6ece62de68523d9f665f32d2fc50fd66e"}, + {file = "numpy-2.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c73aafd1afca80afecb22718f8700b40ac7cab927b8abab3c3e337d70e10e5a2"}, + {file = "numpy-2.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49d9f7d256fbc804391a7f72d4a617302b1afac1112fac19b6c6cec63fe7fe8a"}, + {file = "numpy-2.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0ec84b9ba0654f3b962802edc91424331f423dcf5d5f926676e0150789cb3d95"}, + {file = "numpy-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:feff59f27338135776f6d4e2ec7aeeac5d5f7a08a83e80869121ef8164b74af9"}, + {file = "numpy-2.0.0-cp312-cp312-win32.whl", hash = "sha256:c5a59996dc61835133b56a32ebe4ef3740ea5bc19b3983ac60cc32be5a665d54"}, + {file = "numpy-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:a356364941fb0593bb899a1076b92dfa2029f6f5b8ba88a14fd0984aaf76d0df"}, + {file = "numpy-2.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e61155fae27570692ad1d327e81c6cf27d535a5d7ef97648a17d922224b216de"}, + {file = "numpy-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4554eb96f0fd263041baf16cf0881b3f5dafae7a59b1049acb9540c4d57bc8cb"}, + {file = "numpy-2.0.0-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:903703372d46bce88b6920a0cd86c3ad82dae2dbef157b5fc01b70ea1cfc430f"}, + {file = "numpy-2.0.0-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:3e8e01233d57639b2e30966c63d36fcea099d17c53bf424d77f088b0f4babd86"}, + {file = "numpy-2.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cde1753efe513705a0c6d28f5884e22bdc30438bf0085c5c486cdaff40cd67a"}, + {file = "numpy-2.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:821eedb7165ead9eebdb569986968b541f9908979c2da8a4967ecac4439bae3d"}, + {file = "numpy-2.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9a1712c015831da583b21c5bfe15e8684137097969c6d22e8316ba66b5baabe4"}, + {file = "numpy-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9c27f0946a3536403efb0e1c28def1ae6730a72cd0d5878db38824855e3afc44"}, + {file = "numpy-2.0.0-cp39-cp39-win32.whl", hash = "sha256:63b92c512d9dbcc37f9d81b123dec99fdb318ba38c8059afc78086fe73820275"}, + {file = "numpy-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:3f6bed7f840d44c08ebdb73b1825282b801799e325bcbdfa6bc5c370e5aecc65"}, + {file = "numpy-2.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9416a5c2e92ace094e9f0082c5fd473502c91651fb896bc17690d6fc475128d6"}, + {file = "numpy-2.0.0-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:17067d097ed036636fa79f6a869ac26df7db1ba22039d962422506640314933a"}, + {file = "numpy-2.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38ecb5b0582cd125f67a629072fed6f83562d9dd04d7e03256c9829bdec027ad"}, + {file = "numpy-2.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cef04d068f5fb0518a77857953193b6bb94809a806bd0a14983a8f12ada060c9"}, + {file = "numpy-2.0.0.tar.gz", hash = "sha256:cf5d1c9e6837f8af9f92b6bd3e86d513cdc11f60fd62185cc49ec7d1aba34864"}, +] + +[[package]] +name = "packaging" +version = "24.1" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, +] + +[[package]] +name = "pillow" +version = "10.4.0" +description = "Python Imaging Library (Fork)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e"}, + {file = "pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc"}, + {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e"}, + {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46"}, + {file = "pillow-10.4.0-cp310-cp310-win32.whl", hash = "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984"}, + {file = "pillow-10.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141"}, + {file = "pillow-10.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1"}, + {file = "pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c"}, + {file = "pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319"}, + {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d"}, + {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696"}, + {file = "pillow-10.4.0-cp311-cp311-win32.whl", hash = "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496"}, + {file = "pillow-10.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91"}, + {file = "pillow-10.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22"}, + {file = "pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94"}, + {file = "pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a"}, + {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b"}, + {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9"}, + {file = "pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42"}, + {file = "pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a"}, + {file = "pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9"}, + {file = "pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3"}, + {file = "pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc"}, + {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a"}, + {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309"}, + {file = "pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060"}, + {file = "pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea"}, + {file = "pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d"}, + {file = "pillow-10.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8d4d5063501b6dd4024b8ac2f04962d661222d120381272deea52e3fc52d3736"}, + {file = "pillow-10.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c1ee6f42250df403c5f103cbd2768a28fe1a0ea1f0f03fe151c8741e1469c8b"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15e02e9bb4c21e39876698abf233c8c579127986f8207200bc8a8f6bb27acf2"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a8d4bade9952ea9a77d0c3e49cbd8b2890a399422258a77f357b9cc9be8d680"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:43efea75eb06b95d1631cb784aa40156177bf9dd5b4b03ff38979e048258bc6b"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:950be4d8ba92aca4b2bb0741285a46bfae3ca699ef913ec8416c1b78eadd64cd"}, + {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d7480af14364494365e89d6fddc510a13e5a2c3584cb19ef65415ca57252fb84"}, + {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:73664fe514b34c8f02452ffb73b7a92c6774e39a647087f83d67f010eb9a0cf0"}, + {file = "pillow-10.4.0-cp38-cp38-win32.whl", hash = "sha256:e88d5e6ad0d026fba7bdab8c3f225a69f063f116462c49892b0149e21b6c0a0e"}, + {file = "pillow-10.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:5161eef006d335e46895297f642341111945e2c1c899eb406882a6c61a4357ab"}, + {file = "pillow-10.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0ae24a547e8b711ccaaf99c9ae3cd975470e1a30caa80a6aaee9a2f19c05701d"}, + {file = "pillow-10.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:298478fe4f77a4408895605f3482b6cc6222c018b2ce565c2b6b9c354ac3229b"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:134ace6dc392116566980ee7436477d844520a26a4b1bd4053f6f47d096997fd"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:930044bb7679ab003b14023138b50181899da3f25de50e9dbee23b61b4de2126"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:c76e5786951e72ed3686e122d14c5d7012f16c8303a674d18cdcd6d89557fc5b"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b2724fdb354a868ddf9a880cb84d102da914e99119211ef7ecbdc613b8c96b3c"}, + {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dbc6ae66518ab3c5847659e9988c3b60dc94ffb48ef9168656e0019a93dbf8a1"}, + {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:06b2f7898047ae93fad74467ec3d28fe84f7831370e3c258afa533f81ef7f3df"}, + {file = "pillow-10.4.0-cp39-cp39-win32.whl", hash = "sha256:7970285ab628a3779aecc35823296a7869f889b8329c16ad5a71e4901a3dc4ef"}, + {file = "pillow-10.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:961a7293b2457b405967af9c77dcaa43cc1a8cd50d23c532e62d48ab6cdd56f5"}, + {file = "pillow-10.4.0-cp39-cp39-win_arm64.whl", hash = "sha256:32cda9e3d601a52baccb2856b8ea1fc213c90b340c542dcef77140dfa3278a9e"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a02364621fe369e06200d4a16558e056fe2805d3468350df3aef21e00d26214b"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1b5dea9831a90e9d0721ec417a80d4cbd7022093ac38a568db2dd78363b00908"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b885f89040bb8c4a1573566bbb2f44f5c505ef6e74cec7ab9068c900047f04b"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87dd88ded2e6d74d31e1e0a99a726a6765cda32d00ba72dc37f0651f306daaa8"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:2db98790afc70118bd0255c2eeb465e9767ecf1f3c25f9a1abb8ffc8cfd1fe0a"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f7baece4ce06bade126fb84b8af1c33439a76d8a6fd818970215e0560ca28c27"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3"}, + {file = "pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=7.3)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] +fpx = ["olefile"] +mic = ["olefile"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] +typing = ["typing-extensions"] +xmp = ["defusedxml"] + +[[package]] +name = "platformdirs" +version = "4.2.2" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, + {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] +type = ["mypy (>=1.8)"] + +[[package]] +name = "pre-commit" +version = "3.7.1" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pre_commit-3.7.1-py2.py3-none-any.whl", hash = "sha256:fae36fd1d7ad7d6a5a1c0b0d5adb2ed1a3bda5a21bf6c3e5372073d7a11cd4c5"}, + {file = "pre_commit-3.7.1.tar.gz", hash = "sha256:8ca3ad567bc78a4972a3f1a477e94a79d4597e8140a6e0b651c5e33899c3654a"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "pydantic" +version = "2.8.2" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"}, + {file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"}, +] + +[package.dependencies] +annotated-types = ">=0.4.0" +pydantic-core = "2.20.1" +typing-extensions = {version = ">=4.6.1", markers = "python_version < \"3.13\""} + +[package.extras] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.20.1" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"}, + {file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840"}, + {file = "pydantic_core-2.20.1-cp310-none-win32.whl", hash = "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250"}, + {file = "pydantic_core-2.20.1-cp310-none-win_amd64.whl", hash = "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c"}, + {file = "pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312"}, + {file = "pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b"}, + {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27"}, + {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b"}, + {file = "pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a"}, + {file = "pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2"}, + {file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"}, + {file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"}, + {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"}, + {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"}, + {file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"}, + {file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"}, + {file = "pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686"}, + {file = "pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83"}, + {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203"}, + {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0"}, + {file = "pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e"}, + {file = "pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20"}, + {file = "pydantic_core-2.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91"}, + {file = "pydantic_core-2.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd"}, + {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa"}, + {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987"}, + {file = "pydantic_core-2.20.1-cp38-none-win32.whl", hash = "sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a"}, + {file = "pydantic_core-2.20.1-cp38-none-win_amd64.whl", hash = "sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434"}, + {file = "pydantic_core-2.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c"}, + {file = "pydantic_core-2.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1"}, + {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09"}, + {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab"}, + {file = "pydantic_core-2.20.1-cp39-none-win32.whl", hash = "sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2"}, + {file = "pydantic_core-2.20.1-cp39-none-win_amd64.whl", hash = "sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"}, + {file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pygame" +version = "2.6.0" +description = "Python Game Development" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pygame-2.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e5707aa9d029752495b3eddc1edff62e0e390a02f699b0f1ce77fe0b8c70ea4f"}, + {file = "pygame-2.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3ed0547368733b854c0d9981c982a3cdfabfa01b477d095c57bf47f2199da44"}, + {file = "pygame-2.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6050f3e95f1f16602153d616b52619c6a2041cee7040eb529f65689e9633fc3e"}, + {file = "pygame-2.6.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89be55b7e9e22e0eea08af9d6cfb97aed5da780f0b3a035803437d481a16d972"}, + {file = "pygame-2.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d65fb222eea1294cfc8206d9e5754d476a1673eb2783c03c4f70e0455320274"}, + {file = "pygame-2.6.0-cp310-cp310-win32.whl", hash = "sha256:71eebb9803cb350298de188fb7cdd3ebf13299f78d59a71c7e81efc649aae348"}, + {file = "pygame-2.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:1551852a2cd5b4139a752888f6cbeeb4a96fc0fe6e6f3f8b9d9784eb8fceab13"}, + {file = "pygame-2.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f6e5e6c010b1bf429388acf4d41d7ab2f7ad8fbf241d0db822102d35c9a2eb84"}, + {file = "pygame-2.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:99902f4a2f6a338057200d99b5120a600c27a9f629ca012a9b0087c045508d08"}, + {file = "pygame-2.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a284664978a1989c1e31a0888b2f70cfbcbafdfa3bb310e750b0d3366416225"}, + {file = "pygame-2.6.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:829623cee298b3dbaa1dd9f52c3051ae82f04cad7708c8c67cb9a1a4b8fd3c0b"}, + {file = "pygame-2.6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6acf7949ed764487d51123f4f3606e8f76b0df167fef12ef73ef423c35fdea39"}, + {file = "pygame-2.6.0-cp311-cp311-win32.whl", hash = "sha256:3f809560c99bd1fb4716610eca0cd36412528f03da1a63841a347b71d0c604ee"}, + {file = "pygame-2.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:6897ab87f9193510a774a3483e00debfe166f340ca159f544ef99807e2a44ec4"}, + {file = "pygame-2.6.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b834711ebc8b9d0c2a5f9bfae4403dd277b2c61bcb689e1aa630d01a1ebcf40a"}, + {file = "pygame-2.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b5ac288655e8a31a303cc286e79cc57979ed2ba19c3a14042d4b6391c1d3bed2"}, + {file = "pygame-2.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d666667b7826b0a7921b8ce0a282ba5281dfa106976c1a3b24e32a0af65ad3b1"}, + {file = "pygame-2.6.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd8848a37a7cee37854c7efb8d451334477c9f8ce7ac339c079e724dc1334a76"}, + {file = "pygame-2.6.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:315e7b3c1c573984f549ac5da9778ac4709b3b4e3a4061050d94eab63fa4fe31"}, + {file = "pygame-2.6.0-cp312-cp312-win32.whl", hash = "sha256:e44bde0840cc21a91c9d368846ac538d106cf0668be1a6030f48df139609d1e8"}, + {file = "pygame-2.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:1c429824b1f881a7a5ce3b5c2014d3d182aa45a22cea33c8347a3971a5446907"}, + {file = "pygame-2.6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b832200bd8b6fc485e087bf3ef7ec1a21437258536413a5386088f5dcd3a9870"}, + {file = "pygame-2.6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:098029d01a46ea4e30620dfb7c28a577070b456c8fc96350dde05f85c0bf51b5"}, + {file = "pygame-2.6.0-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a858bbdeac5ec473ec9e726c55fb8fbdc2f4aad7c55110e899883738071c7c9b"}, + {file = "pygame-2.6.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f908762941fd99e1f66d1211d26383184f6045c45673443138b214bf48a89aa"}, + {file = "pygame-2.6.0-cp36-cp36m-win32.whl", hash = "sha256:4a63daee99d050f47d6ec7fa7dbd1c6597b8f082cdd58b6918d382d2bc31262d"}, + {file = "pygame-2.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:ace471b3849d68968e5427fc01166ef5afaf552a5c442fc2c28d3b7226786f55"}, + {file = "pygame-2.6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fea019713d0c89dfd5909225aa933010100035d1cd30e6c936e8b6f00529fb80"}, + {file = "pygame-2.6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:249dbf2d51d9f0266009a380ccf0532e1a57614a1528bb2f89a802b01d61f93e"}, + {file = "pygame-2.6.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cb51533ee3204e8160600b0de34eaad70eb913a182c94a7777b6051e8fc52f1"}, + {file = "pygame-2.6.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f637636a44712e94e5601ec69160a080214626471983dfb0b5b68aa0c61563d"}, + {file = "pygame-2.6.0-cp37-cp37m-win32.whl", hash = "sha256:e432156b6f346f4cc6cab03ce9657600093390f4c9b10bf458716b25beebfe33"}, + {file = "pygame-2.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:a0194652db7874bdde7dfc69d659ca954544c012e04ae527151325bfb970f423"}, + {file = "pygame-2.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:eae3ee62cc172e268121d5bd9dc406a67094d33517de3a91de3323d6ae23eb02"}, + {file = "pygame-2.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f6a58b0a5a8740a3c2cf6fc5366888bd4514561253437f093c12a9ab4fb3ecae"}, + {file = "pygame-2.6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c71da36997dc7b9b4ee973fa3a5d4a6cfb2149161b5b1c08b712d2f13a63ccfe"}, + {file = "pygame-2.6.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b86771801a7fc10d9a62218f27f1d5c13341c3a27394aa25578443a9cd199830"}, + {file = "pygame-2.6.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4928f3acf5a9ce5fbab384c21f1245304535ffd5fb167ae92a6b4d3cdb55a3b6"}, + {file = "pygame-2.6.0-cp38-cp38-win32.whl", hash = "sha256:4faab2df9926c4d31215986536b112f0d76f711cf02f395805f1ff5df8fd55fc"}, + {file = "pygame-2.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:afbb8d97aed93dfb116fe105603dacb68f8dab05b978a40a9e4ab1b6c1f683fd"}, + {file = "pygame-2.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d11f3646b53819892f4a731e80b8589a9140343d0d4b86b826802191b241228c"}, + {file = "pygame-2.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5ef92ed93c354eabff4b85e457d4d6980115004ec7ff52a19fd38b929c3b80fb"}, + {file = "pygame-2.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bc1795f2e36302882546faacd5a0191463c4f4ae2b90e7c334a7733aa4190d2"}, + {file = "pygame-2.6.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e92294fcc85c4955fe5bc6a0404e4cc870808005dc8f359e881544e3cc214108"}, + {file = "pygame-2.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0cb7bdf3ee0233a3ac02ef777c01dfe315e6d4670f1312c83b91c1ef124359a"}, + {file = "pygame-2.6.0-cp39-cp39-win32.whl", hash = "sha256:ac906478ae489bb837bf6d2ae1eb9261d658aa2c34fa5b283027a04149bda81a"}, + {file = "pygame-2.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:92cf12a9722f6f0bdc5520d8925a8f085cff9c054a2ea462fc409cba3781be27"}, + {file = "pygame-2.6.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:a6636f452fdaddf604a060849feb84c056930b6a3c036214f607741f16aac942"}, + {file = "pygame-2.6.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3dc242dc15d067d10f25c5b12a1da48ca9436d8e2d72353eaf757e83612fba2f"}, + {file = "pygame-2.6.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f82df23598a281c8c342d3c90be213c8fe762a26c15815511f60d0aac6e03a70"}, + {file = "pygame-2.6.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2ed2539bb6bd211fc570b1169dc4a64a74ec5cd95741e62a0ab46bd18fe08e0d"}, + {file = "pygame-2.6.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:904aaf29710c6b03a7e1a65b198f5467ed6525e8e60bdcc5e90ff8584c1d54ea"}, + {file = "pygame-2.6.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcd28f96f0fffd28e71a98773843074597e10d7f55a098e2e5bcb2bef1bdcbf5"}, + {file = "pygame-2.6.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4fad1ab33443ecd4f958dbbb67fc09fcdc7a37e26c34054e3296fb7e26ad641e"}, + {file = "pygame-2.6.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e909186d4d512add39b662904f0f79b73028fbfc4fbfdaf6f9412aed4e500e9c"}, + {file = "pygame-2.6.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79abcbf6d12fce51a955a0652ccd50b6d0a355baa27799535eaf21efb43433dd"}, + {file = "pygame-2.6.0.tar.gz", hash = "sha256:722d33ae676aa8533c1f955eded966411298831346b8d51a77dad22e46ba3e35"}, +] + +[[package]] +name = "pyparsing" +version = "3.1.2" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +optional = false +python-versions = ">=3.6.8" +files = [ + {file = "pyparsing-3.1.2-py3-none-any.whl", hash = "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742"}, + {file = "pyparsing-3.1.2.tar.gz", hash = "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +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.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "python-dotenv" +version = "1.0.1" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.8" +files = [ + {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, + {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[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_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, + {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 = "ruff" +version = "0.5.4" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.5.4-py3-none-linux_armv6l.whl", hash = "sha256:82acef724fc639699b4d3177ed5cc14c2a5aacd92edd578a9e846d5b5ec18ddf"}, + {file = "ruff-0.5.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:da62e87637c8838b325e65beee485f71eb36202ce8e3cdbc24b9fcb8b99a37be"}, + {file = "ruff-0.5.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e98ad088edfe2f3b85a925ee96da652028f093d6b9b56b76fc242d8abb8e2059"}, + {file = "ruff-0.5.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c55efbecc3152d614cfe6c2247a3054cfe358cefbf794f8c79c8575456efe19"}, + {file = "ruff-0.5.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f9b85eaa1f653abd0a70603b8b7008d9e00c9fa1bbd0bf40dad3f0c0bdd06793"}, + {file = "ruff-0.5.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cf497a47751be8c883059c4613ba2f50dd06ec672692de2811f039432875278"}, + {file = "ruff-0.5.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:09c14ed6a72af9ccc8d2e313d7acf7037f0faff43cde4b507e66f14e812e37f7"}, + {file = "ruff-0.5.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:628f6b8f97b8bad2490240aa84f3e68f390e13fabc9af5c0d3b96b485921cd60"}, + {file = "ruff-0.5.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3520a00c0563d7a7a7c324ad7e2cde2355733dafa9592c671fb2e9e3cd8194c1"}, + {file = "ruff-0.5.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93789f14ca2244fb91ed481456f6d0bb8af1f75a330e133b67d08f06ad85b516"}, + {file = "ruff-0.5.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:029454e2824eafa25b9df46882f7f7844d36fd8ce51c1b7f6d97e2615a57bbcc"}, + {file = "ruff-0.5.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:9492320eed573a13a0bc09a2957f17aa733fff9ce5bf00e66e6d4a88ec33813f"}, + {file = "ruff-0.5.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a6e1f62a92c645e2919b65c02e79d1f61e78a58eddaebca6c23659e7c7cb4ac7"}, + {file = "ruff-0.5.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:768fa9208df2bec4b2ce61dbc7c2ddd6b1be9fb48f1f8d3b78b3332c7d71c1ff"}, + {file = "ruff-0.5.4-py3-none-win32.whl", hash = "sha256:e1e7393e9c56128e870b233c82ceb42164966f25b30f68acbb24ed69ce9c3a4e"}, + {file = "ruff-0.5.4-py3-none-win_amd64.whl", hash = "sha256:58b54459221fd3f661a7329f177f091eb35cf7a603f01d9eb3eb11cc348d38c4"}, + {file = "ruff-0.5.4-py3-none-win_arm64.whl", hash = "sha256:bd53da65f1085fb5b307c38fd3c0829e76acf7b2a912d8d79cadcdb4875c1eb7"}, + {file = "ruff-0.5.4.tar.gz", hash = "sha256:2795726d5f71c4f4e70653273d1c23a8182f07dd8e48c12de5d867bfb7557eed"}, +] + +[[package]] +name = "setuptools" +version = "71.0.4" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-71.0.4-py3-none-any.whl", hash = "sha256:ed2feca703be3bdbd94e6bb17365d91c6935c6b2a8d0bb09b66a2c435ba0b1a5"}, + {file = "setuptools-71.0.4.tar.gz", hash = "sha256:48297e5d393a62b7cb2a10b8f76c63a73af933bd809c9e0d0d6352a1a0135dd8"}, +] + +[package.extras] +core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "ordered-set (>=3.1.1)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (<0.4)", "pytest-ruff (>=0.2.1)", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] + +[[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 = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[[package]] +name = "virtualenv" +version = "20.26.3" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"}, + {file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + +[[package]] +name = "wordcloud" +version = "1.9.3" +description = "A little word cloud generator" +optional = false +python-versions = ">=3.7" +files = [ + {file = "wordcloud-1.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5fce423a24e6ca1b89b2770a7c6917d6e26f04bcfefa601cf61819b2fc0770c4"}, + {file = "wordcloud-1.9.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3b6adfc1465b9176b8bc602745dd3ed8ea782b006a81cb59eab3dde92ad9f94c"}, + {file = "wordcloud-1.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad6db37a6f5abeba51a5d503228ea320d4f2fa774864103e7b24acd9dd86fd0e"}, + {file = "wordcloud-1.9.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e74ac99e9582873d7ee28bd03e125dcf73ae46666d55fb4c13e82e90c0e074a"}, + {file = "wordcloud-1.9.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:4001317c0e3b5cb6fd106228ddcd27524d1caf9ae468b3c2c2fc571c6ce56b22"}, + {file = "wordcloud-1.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5f86042e5ce12e2795798033a56f0246906b4d7d9027d554b6cd951ce2fd342a"}, + {file = "wordcloud-1.9.3-cp310-cp310-win32.whl", hash = "sha256:3b90f0390c0a05ba4b4580fb765a3d45d8d21519b50ca5006d6dbdc2a0b86507"}, + {file = "wordcloud-1.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:6f7977285df9254b8704d3f895c06814a6183c6c89e140d6281848c076635e91"}, + {file = "wordcloud-1.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7ced0d5c946d82cfc778febafe3eedeb0bae07dd57ea4f21fe06b9ec8225ab31"}, + {file = "wordcloud-1.9.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6f5499e6360219e61808dc0d2b00cd5104f78a82d2ae8f7986df04731713835f"}, + {file = "wordcloud-1.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb1e8bb7d60f7a90fa8439c7b56dd1df60766115fd57480ac0d83ca5204e0117"}, + {file = "wordcloud-1.9.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e33328044db5c01487f2a3a023b5476947942dacd6a5dc8c217fa039f6c5bd9"}, + {file = "wordcloud-1.9.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:998dc0dc8fcbff88f566f17cb5e0eb3bb21fcafd387b0670be6c14feacaf4cdc"}, + {file = "wordcloud-1.9.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e1a1c3cfa86b605a19711ec58920ccb694dca9d5c9d00b373f4d5952d63793e9"}, + {file = "wordcloud-1.9.3-cp311-cp311-win32.whl", hash = "sha256:f504e3291256c0b6fca044602f8f0e5cb56b7c33724cde9d279c4077fa5b6d27"}, + {file = "wordcloud-1.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:103c9b0465e1cf5b7a38b49ab1c3a0b0301762fa56602ac79287f9d22b46ade3"}, + {file = "wordcloud-1.9.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:dfea303fc1dec4811e4a5671a8021a89724b6fa70639d059ad30c492932be447"}, + {file = "wordcloud-1.9.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:512f3c9a2e8579269a33ac9219d042fd0cc5a3a524ee68079238a3e4efe2b879"}, + {file = "wordcloud-1.9.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d00d916509a17b432032161d492ed7f30b2ebd921303090fe1d2b57011a49cc0"}, + {file = "wordcloud-1.9.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d5e0e7bbd269a62baa63ea2175faea4d74435c0ad828f3d5999fa4c33ebe0629"}, + {file = "wordcloud-1.9.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:483aa4f8d17b9744a3b238269593d1794b962fc757a72a9e7e8468c2665cffb7"}, + {file = "wordcloud-1.9.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:64b342a79553970fa04083761d041067323219ad62b5550a496e42436d23cbb3"}, + {file = "wordcloud-1.9.3-cp312-cp312-win32.whl", hash = "sha256:419acfe0b1d1227b9e3e14ec1bb6c40fd7fa652df4adf81f0ba3e00daca500b5"}, + {file = "wordcloud-1.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:2061a9978a6243107ce1a8a9fa24f421b03a0f7e620769b6f5075857e75aa615"}, + {file = "wordcloud-1.9.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:21f47fabe189f39532378759300a624ae166519dfafbd6a22cfe65b14a7d104d"}, + {file = "wordcloud-1.9.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:524065f8a5a79e00748f45efbeacd25ac1d15850e0d0588753b17a8b2de2a6a7"}, + {file = "wordcloud-1.9.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b2bb53492bc8663ba90a300bbd2da7be5059f9ad192ed1150e9bbbda8016c9a"}, + {file = "wordcloud-1.9.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:643243474faee460e7d08944d3e529c58d0cbf8be11626fbb918ee8ccb913a23"}, + {file = "wordcloud-1.9.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d95f44739a6972abfb97c12656999952dd28ed03700ee8b6efe35d688d489b36"}, + {file = "wordcloud-1.9.3-cp37-cp37m-win32.whl", hash = "sha256:e56364c8829d399397a649501f834c12751ab106cba488ba8d86d532889b528c"}, + {file = "wordcloud-1.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:78f4a3fd3526884e4f526ae070bcb47401766c48c9cb6488933f608f810fadae"}, + {file = "wordcloud-1.9.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0058cf08573c99283fe189e93354d20ca8c9a8aac7207d96e74b93aedd02cdcc"}, + {file = "wordcloud-1.9.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:47d6918381a8a816141bdd391376bff703ec5aa3a6bd88631097a5e2963ebd1a"}, + {file = "wordcloud-1.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05aa3269c5af573cfb11e269de0fe73c2c72aefdd90cdb41368744e7d8bc7507"}, + {file = "wordcloud-1.9.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d74e206f42af172db4d3c0054853523bf46070b12f0626493a56599957dd2196"}, + {file = "wordcloud-1.9.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1932726635c8ed12bb74201d2a6b07f18c2f732aecadb9ae915832485241991f"}, + {file = "wordcloud-1.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:038de1701e7853c41850644453f1c9e69f878e480d42efae154684a47fd59f1a"}, + {file = "wordcloud-1.9.3-cp38-cp38-win32.whl", hash = "sha256:19aa05f60d9261301e4942fd1b1c4b458d903f24c12d2bd1c6ecbb752697a2f3"}, + {file = "wordcloud-1.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:ab5bae12cf27d8de986e4d4518d4778f2b56c660b250b631ff805024038311a1"}, + {file = "wordcloud-1.9.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:888d088f54a897b8597da2fae3954d74b1f7251f7d311bbcc30ec3c6987d3605"}, + {file = "wordcloud-1.9.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:daa6cfa11ce24e7eb4e42dc896dae4f74ae2166cf90ec997996300566e6811d1"}, + {file = "wordcloud-1.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:387dc2bd528ff6bb661451f2a9fd4ccf74b86072d7a2c868285d4c0cf26abeb4"}, + {file = "wordcloud-1.9.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40c32a324319db610b40f387a2a0b42d091817958a5272e0a4c4eb6a158588b5"}, + {file = "wordcloud-1.9.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8078c6c58db4ccb893f120354e7e08bc48a5a5aac3e764f9008bc96a769b208c"}, + {file = "wordcloud-1.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:81f15eb60abc1676808bb85e2edfdbdc0a9011383f2a729c1c2a0cb941516768"}, + {file = "wordcloud-1.9.3-cp39-cp39-win32.whl", hash = "sha256:1d1680bf6c3d1b2f8e3bd02ccfa868fee2655fe13cf5b9e9905251050448fbbd"}, + {file = "wordcloud-1.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:c0f458681e4d49be36064f21bfb1dc8d8c3021fe30e474ee634666b4f84fd851"}, + {file = "wordcloud-1.9.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:baea9ac88ec1ab317461c75834b64ad5dad12a02c4f2384dd546eac3c316dbbb"}, + {file = "wordcloud-1.9.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6956b9f0d0eb14a12f46d41aebb4e7ad2d4c2ec417cc7c586bebd2ddc9c8311"}, + {file = "wordcloud-1.9.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d221b4d0d1d2a1d79286c41d8a4c0ce70065488f153e5d81cc0be7fb494ff10f"}, + {file = "wordcloud-1.9.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:db39dbe91dd31ffb667edcd496f4eeb85ceea397fef4ad51d0766ab934088cc7"}, + {file = "wordcloud-1.9.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a6ae5db43807ca10f5c77dd2d22c78f8f9399758cc5ac6afd7f3c19e58b75d66"}, + {file = "wordcloud-1.9.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a1c431f20ee28a8840f2552a89bd8332c455c318f4de7b6c2ca3159b76df4f0"}, + {file = "wordcloud-1.9.3-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1847ca4466e2b1588478dd8eb87fa7baa28515b37ab7926471595e8ac81e6578"}, + {file = "wordcloud-1.9.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:7b0e14e4dfcff7dee331df7880a2031e352e95a7d30e74ff152f162488b04179"}, + {file = "wordcloud-1.9.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f1c0cff6037a3dc46437537a31925f3895d742fb6d67af71194149763de16a76"}, + {file = "wordcloud-1.9.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a36788c5c79604653327675023cbd97c68813640887b51ce651bb4f5c28c88b"}, + {file = "wordcloud-1.9.3-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3e3907c6496e197a9c4be76770c5ff8a03eddbdfe5a151a55e4eedeaa45ab3ad"}, + {file = "wordcloud-1.9.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:65e6f6b68eecb85c326ae19729dd4151fcdebffc2142c9ee882dc2de955210d0"}, + {file = "wordcloud-1.9.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0c8e18c4afa025819332efffe8008267a83a9c54fe72ae1bc889ddce0eec470d"}, + {file = "wordcloud-1.9.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4df25cb5dd347e43d53e02a009418f5776e7651063aff991865da8f6336bf193"}, + {file = "wordcloud-1.9.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53489ad22d58be3896ec16ed47604832e393224c89f7d7eed040096b07141ac4"}, + {file = "wordcloud-1.9.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:61de4a5f3bfd33e0cb013cce6143bcf71959f3cd8536650b90134d745a553c2c"}, + {file = "wordcloud-1.9.3.tar.gz", hash = "sha256:a9aa738d63ed674a40f0cc31adb83f4ca5fc195f03a6aff6e010d1f5807d1c58"}, +] + +[package.dependencies] +matplotlib = "*" +numpy = ">=1.6.1" +pillow = "*" + +[[package]] +name = "yarl" +version = "1.9.4" +description = "Yet another URL library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e"}, + {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2"}, + {file = "yarl-1.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541"}, + {file = "yarl-1.9.4-cp310-cp310-win32.whl", hash = "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d"}, + {file = "yarl-1.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98"}, + {file = "yarl-1.9.4-cp311-cp311-win32.whl", hash = "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31"}, + {file = "yarl-1.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10"}, + {file = "yarl-1.9.4-cp312-cp312-win32.whl", hash = "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7"}, + {file = "yarl-1.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984"}, + {file = "yarl-1.9.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434"}, + {file = "yarl-1.9.4-cp37-cp37m-win32.whl", hash = "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749"}, + {file = "yarl-1.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2"}, + {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be"}, + {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f"}, + {file = "yarl-1.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3"}, + {file = "yarl-1.9.4-cp38-cp38-win32.whl", hash = "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece"}, + {file = "yarl-1.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b"}, + {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27"}, + {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1"}, + {file = "yarl-1.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0"}, + {file = "yarl-1.9.4-cp39-cp39-win32.whl", hash = "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575"}, + {file = "yarl-1.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15"}, + {file = "yarl-1.9.4-py3-none-any.whl", hash = "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad"}, + {file = "yarl-1.9.4.tar.gz", hash = "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf"}, +] + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" + +[metadata] +lock-version = "2.0" +python-versions = "3.12.*" +content-hash = "2552afdec96ecb59a05dcc9b8a3073608f8bb4328b54419aa4c03093199855a2" diff --git a/ardent-andromedas/pyproject.toml b/ardent-andromedas/pyproject.toml new file mode 100644 index 0000000..2ddf62a --- /dev/null +++ b/ardent-andromedas/pyproject.toml @@ -0,0 +1,101 @@ +[tool.poetry] +name = "python-code-jam-2024" +version = "0.1.0" +description = "" +authors = ["Steve Mostovoy "] +readme = "README.md" +packages = [ + { include = "src" } +] +license = "MIT" + +[tool.poetry.dependencies] +python = "3.12.*" +pygame = "2.6.0" +pillow = "10.4.0" +setuptools = "^71.0.4" +discord-py = "^2.4.0" +python-dotenv = "^1.0.1" +numpy = "^2.0.0" +pydantic = "^2.8.2" +aiosqlite = "^0.20.0" +wordcloud = "^1.9.3" +emoji = "^2.12.1" + +[tool.poetry.dev-dependencies] +ruff = "0.5.4" +pre-commit = "3.7.1" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.ruff] +line-length = 119 +target-version = "py312" +fix = true +src = ["src"] + +[tool.ruff.lint] +select = ["ALL"] +# Ignore some of the most obnoxious linting errors. +ignore = [ + # Missing docstrings. + "D100", + # Builtins. + "A", + # Print statements. + "T20", + # TODOs. + "TD002", + "TD003", + "FIX", + # Random + "S311", + # Docstrings + "D102", + # Magic values + "PLR2004", + "FBT002", + "FBT003", + # Boolean arguments + "FBT001", + # Unused argument + "ARG002", + # Complexity + "C901", + "PLR0912", + "PLR0913", + "PLR0915", + # Ambiguous formatting rules + "D203", + "D211", + "D213", + "W191", + "E111", + "E114", + "E117", + "D206", + "D300", + "Q000", + "Q001", + "Q002", + "Q003", + "COM812", + "COM819", + "ISC001", + "ISC002", + # Subprocess + "S603", +] + +[tool.poetry.scripts] +lint = "src.scripts:lint" +run = "src.scripts:run" +run-interactive = "src.scripts:run_interactive" +run-test = "src.scripts:run_discord_test" +run-gifs = "src.scripts:run_gifs" +dev = "src.scripts:dev" +dev-interactive = "src.scripts:dev_interactive" +dev-test = "src.scripts:dev_discord_test" +dev-gifs = "src.scripts:dev_gifs" diff --git a/ardent-andromedas/src/app.py b/ardent-andromedas/src/app.py new file mode 100644 index 0000000..26d4494 --- /dev/null +++ b/ardent-andromedas/src/app.py @@ -0,0 +1,130 @@ +import asyncio +import sys +import time +from collections.abc import Generator +from datetime import UTC, datetime +from pathlib import Path + +from bot import EcoCordClient, TestEcoCordClient +from ecosystem import EcosystemManager + + +async def run_discord_bot(test_mode: bool = True) -> None: + """Run the Discord bot in either test or production mode. + + Args: + ---- + test_mode (bool): If True, run in test mode with TestEcoCordClient. Default is True. + + Raises: + ------ + Exception: Any unhandled exception during bot execution. + + """ + loop = asyncio.get_event_loop() + client = TestEcoCordClient() if test_mode else EcoCordClient() + + try: + await client.run_bot() + except KeyboardInterrupt: + print("KeyboardInterrupt received. Shutting down...") + except Exception: + raise + finally: + print("Cleaning up...") + await client.stop_all_ecosystems() + await client.close() + await loop.shutdown_asyncgens() + + +def run_gif_generator(duration: float | None = None) -> Generator[tuple[str, float], None, None]: + """Generate GIFs of the ecosystem for a specified duration. + + Args: + ---- + duration (float | None): Duration in seconds to generate GIFs. If None, run indefinitely. + + Yields: + ------ + tuple[str, float]: A tuple containing GIF data (bytes) and timestamp. + + """ + manager = EcosystemManager(generate_gifs=True) + manager.start(show_controls=False) + + start_time = time.time() + try: + while duration is None or time.time() - start_time < duration: + gif_data = manager.get_latest_gif() + if gif_data: + yield gif_data + time.sleep(0.01) + finally: + manager.stop() + + +def run_pygame(show_controls: bool = True, generate_gifs: bool = False) -> EcosystemManager: + """Run the ecosystem simulation using Pygame. + + Args: + ---- + show_controls (bool): If True, display simulation controls. Default is True. + generate_gifs (bool): If True, generate GIFs of the simulation. Default is False. + + Returns: + ------- + EcosystemManager: The initialized and running EcosystemManager instance. + + """ + manager = EcosystemManager(generate_gifs=generate_gifs) + manager.start(show_controls=show_controls) + return manager + + +def main() -> None: + """Run the main Discord bot in production mode.""" + print("Running with Discord integration...") + asyncio.run(run_discord_bot(test_mode=False)) + + +def main_interactive() -> None: + """Run the ecosystem simulation in interactive mode with Pygame controls.""" + print("Running in interactive mode...") + manager = run_pygame(show_controls=True, generate_gifs=False) + try: + while True: + pass + except KeyboardInterrupt: + manager.stop() + + +def main_discord_test() -> None: + """Run the Discord bot in test mode.""" + print("[Test mode] Running with fake Discord integration...") + asyncio.run(run_discord_bot(test_mode=True)) + + +def main_gifs() -> None: + """Generate and save GIFs of the ecosystem simulation for a fixed duration.""" + print("Running in GIF generation mode...") + gif_dir = Path("ecosystem_gifs") + gif_dir.mkdir(exist_ok=True) + + for gif_bytes, timestamp in run_gif_generator(duration=180): + filename = f"ecosystem_{datetime.fromtimestamp(timestamp, tz=UTC).strftime('%Y%m%d_%H%M%S')}.gif" + filepath = gif_dir / filename + + filepath.write_bytes(gif_bytes) + + print(f"New GIF saved: {filepath}") + + +if __name__ == "__main__": + if "--interactive" in sys.argv or "-i" in sys.argv: + main_interactive() + elif "--test" in sys.argv: + main_discord_test() + elif "--gifs" in sys.argv: + main_gifs() + else: + main() diff --git a/ardent-andromedas/src/bot/__init__.py b/ardent-andromedas/src/bot/__init__.py new file mode 100644 index 0000000..424713c --- /dev/null +++ b/ardent-andromedas/src/bot/__init__.py @@ -0,0 +1,11 @@ +"""The Bot module provides the main bot classes for EcoCord. + +Classes: + EcoCordClient: The main bot client for EcoCord. + TestEcoCordClient: A test version of the bot client for development and testing purposes. +""" + +from .bot import EcoCordClient +from .test_bot import TestEcoCordClient + +__all__ = ["EcoCordClient", "TestEcoCordClient"] diff --git a/ardent-andromedas/src/bot/bot.py b/ardent-andromedas/src/bot/bot.py new file mode 100644 index 0000000..c0d3620 --- /dev/null +++ b/ardent-andromedas/src/bot/bot.py @@ -0,0 +1,583 @@ +import asyncio +import io +import logging +from collections import deque +from collections.abc import Coroutine +from datetime import UTC, datetime, timedelta +from typing import Any + +import aiohttp +import discord +from discord import app_commands + +from ecosystem import EcosystemManager +from storage.models import Database, GuildConfig, UserInfo + +from .discord_event import DiscordEvent, EventType, SerializableMember +from .settings import BOT_TOKEN + +logging.basicConfig(level=logging.INFO, format="%(asctime)s:%(levelname)s:%(name)s: %(message)s") + +MAX_MESSAGES = 2 + + +class EcoCordClient(discord.Client): + """A custom Discord client for managing multiple ecosystem simulations and handling Discord events.""" + + def __init__(self) -> None: + """Initialize the EcoCordClient with default intents and set up necessary attributes.""" + intents = discord.Intents.all() + super().__init__(intents=intents) + self.tree = app_commands.CommandTree(self) + self.tree.add_command(configure) + self.guilds_data = {} + self.ready = False + self.database = Database("ecocord") + + async def on_ready(self) -> None: + """Event receiver for when the client is done preparing the data received from Discord.""" + await self.database.initialize() + self.ready = True + print(f"Logged in as {self.user} (ID: {self.user.id})") + + for guild in self.guilds: + await self.tree.sync(guild=guild) + await self.initialize_guild(guild) + + async def initialize_guild(self, guild: discord.Guild) -> None: + """Initialize the guild data and ecosystem managers for a given Discord guild. + + This method sets up the necessary data structures for the guild, retrieves the guild configuration + from the database, and initializes ecosystem managers for the configured channels. It also loads + critters for online members in each ecosystem manager. + + Args: + ---- + guild (discord.Guild): The Discord guild to initialize. + + Returns: + ------- + None + + """ + if guild.id not in self.guilds_data: + self.guilds_data[guild.id] = { + "ecosystem_managers": {}, + } + config = await self.database.get_guild_config(guild.id) + if config: + self.guilds_data[guild.id]["gif_channel_id"] = config.gif_channel + # Call reconfigure_channels here with the channels from the config + channels = [guild.get_channel(channel_id) for channel_id in config.allowed_channels] + await self.reconfigure_channels(guild.id, channels) + + async def on_message(self, message: discord.Message) -> None: + """Event receiver for when a message is sent in a visible channel. + + Creates a DiscordEvent for the message and processes it. + + Args: + ---- + message (discord.Message): The message that was sent. + + """ + # Check if the message is a direct message + is_direct_message = isinstance(message.channel, discord.DMChannel) + + # If it's a direct message, we'll handle it differently + if is_direct_message: + return + + event = DiscordEvent.from_discord_objects( + type=EventType.MESSAGE, + timestamp=message.created_at, + guild=message.guild, + channel=message.channel, + member=message.author, + content=message.content, + ) + await self.process_event(event) + + async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent) -> None: + """Event receiver for when a message reaction is added to a message. + + Creates a DiscordEvent for the reaction and processes it. + + Args: + ---- + payload (discord.RawReactionActionEvent): The raw event payload for the reaction. + + """ + channel = self.get_channel(payload.channel_id) + message = await channel.fetch_message(payload.message_id) + + # Fetch the reaction image + reaction_image_bytes = await self.get_reaction_image(payload.emoji) + + event = DiscordEvent.from_discord_objects( + type=EventType.REACTION, + timestamp=message.created_at, + guild=self.get_guild(payload.guild_id), + channel=channel, + member=payload.member, + content=f"{payload.emoji} added on {message.content}", + reaction_image=reaction_image_bytes, + ) + await self.process_event(event) + + async def get_reaction_image(self, emoji_obj: discord.PartialEmoji | str) -> bytes | None: + """Fetch the image data for a given emoji.""" + if not isinstance(emoji_obj, discord.PartialEmoji): + return None + + if emoji_obj.is_custom_emoji(): + emoji_url = str(emoji_obj.url) + async with aiohttp.ClientSession() as session, session.get(emoji_url) as response: + if response.status == 200: + return await response.read() + + return None + + async def on_raw_typing(self, payload: discord.RawTypingEvent) -> None: + """Event receiver for when a user starts typing in a channel. + + Creates a DiscordEvent for the typing action and processes it. + + Args: + ---- + payload (discord.RawTypingEvent): The raw event payload for the typing action. + + """ + channel = self.get_channel(payload.channel_id) + event = DiscordEvent.from_discord_objects( + type=EventType.TYPING, + timestamp=payload.timestamp, + guild=self.get_guild(payload.guild_id), + channel=channel, + member=payload.user, + content="User is typing", + ) + await self.process_event(event) + + async def get_user_info(self, member: discord.Member | SerializableMember) -> UserInfo: + """Fetch and store user avatar and role color for a specific guild.""" + now = datetime.now(UTC) + + # Check if member is a SerializableMember or discord.Member + if isinstance(member, SerializableMember): + user_id = member.id + guild_id = member.guild_id + else: # discord.Member + user_id = member.id + guild_id = member.guild.id + + user_info = await self.database.get_user_info(user_id, guild_id) + + if user_info is None or (now - user_info.last_updated) > timedelta(hours=1): + if isinstance(member, SerializableMember): + avatar_url = member.avatar + role_color = member.color + else: # discord.Member + avatar_url = str(member.avatar.url) if member.avatar else None + role_color = str(member.color) + + if avatar_url: + async with aiohttp.ClientSession() as session, session.get(avatar_url) as resp: + avatar_data = await resp.read() + else: + avatar_data = None + + user_info = UserInfo( + user_id=user_id, guild_id=guild_id, avatar_data=avatar_data, role_color=role_color, last_updated=now + ) + await self.database.set_user_info(user_info) + + return user_info + + async def process_event(self, event: DiscordEvent) -> None: + """Process a DiscordEvent by logging it and passing it to the corresponding ecosystem manager.""" + if event.member.id == self.user.id: + return + + print( + f"Event: {event.type.name} - {event.member.display_name} in {event.channel}: " + f"{event.content} @ {event.timestamp}" + ) + guild_data = self.guilds_data.get(event.guild.id) + if guild_data: + ecosystem_manager = guild_data["ecosystem_managers"].get(event.channel.id) + if ecosystem_manager: + user_info = await self.get_user_info(event.member) + ecosystem_manager.process_event(event, user_info) + + async def start_ecosystems(self, guild_id: int, channels: list[discord.TextChannel] | None = None) -> None: + """Initialize and start ecosystem managers for specified channels.""" + guild_data = self.guilds_data.get(guild_id) + if not guild_data or guild_data["gif_channel_id"] is None: + return + + gif_channel = await self.fetch_channel(guild_data["gif_channel_id"]) + existing_threads = {thread.name: thread for thread in gif_channel.threads} + + for channel in channels: + if channel.id in guild_data["ecosystem_managers"]: + continue + ecosystem_manager = EcosystemManager(generate_gifs=True, interactive=False) + ecosystem_manager.start(show_controls=False) + guild_data["ecosystem_managers"][channel.id] = ecosystem_manager + + thread_name = f"Ecosystem-{channel.name}" + if thread_name in existing_threads: + thread = existing_threads[thread_name] + print(f"Reusing existing thread for {channel.name}") + else: + thread = await gif_channel.create_thread(name=thread_name, type=discord.ChannelType.public_thread) + print(f"Created new thread for {channel.name}") + + # Start the gif sending task in the background + self.loop.create_task( + self.safe_task( + self.send_gifs(guild_id, channel.id, thread.id), + f"send_gifs for channel {channel.name} in guild {guild_id}", + ) + ) + + async def stop_ecosystems(self, guild_id: int, channel_ids: list[int] | None = None) -> None: + """Stop specified ecosystem managers or all if not specified.""" + guild_data = self.guilds_data.get(guild_id) + if not guild_data: + return + + if channel_ids is None: + channel_ids = list(guild_data["ecosystem_managers"].keys()) + + for channel_id in channel_ids: + ecosystem_manager = guild_data["ecosystem_managers"].pop(channel_id, None) + if ecosystem_manager: + ecosystem_manager.stop() + + async def stop_all_ecosystems(self) -> None: + """Stop all ecosystem managers for all guilds.""" + for guild_data in self.guilds_data.values(): + ecosystem_managers = guild_data.get("ecosystem_managers", {}) + for ecosystem_manager in ecosystem_managers.values(): + ecosystem_manager.stop() + guild_data["ecosystem_managers"] = {} + + async def reconfigure_channels(self, guild_id: int, new_channels: list[discord.TextChannel]) -> None: + """Reconfigure the bot to run in the specified channels.""" + guild_data = self.guilds_data.get(guild_id) + if not guild_data: + return + + new_channel_ids = {channel.id for channel in new_channels} + current_channel_ids = set(guild_data["ecosystem_managers"].keys()) + + channels_to_stop = current_channel_ids - new_channel_ids + channels_to_start = new_channel_ids - current_channel_ids + + await self.stop_ecosystems(guild_id, list(channels_to_stop)) + await self.start_ecosystems(guild_id, [channel for channel in new_channels if channel.id in channels_to_start]) + + # Update the guild configuration in the database + config = GuildConfig( + guild_id=guild_id, allowed_channels=list(new_channel_ids), gif_channel=guild_data["gif_channel_id"] + ) + await self.database.set_guild_config(config) + + async def send_gifs(self, guild_id: int, channel_id: int, thread_id: int) -> None: + """Continuously sends GIFs of the ecosystem to a designated thread.""" + while not self.ready: + await asyncio.sleep(1) + + try: + thread = await self.fetch_channel(thread_id) + except discord.errors.NotFound: + logging.exception("Thread %s not found. Stopping GIF sending for channel %s", thread_id, channel_id) + return + + message_queue = deque(maxlen=MAX_MESSAGES) + + # Find existing messages and populate the queue + try: + existing_messages = await self.find_existing_messages(thread) + # Delete all existing messages except the last max messages + oldest_messages = existing_messages[:-MAX_MESSAGES] + for message in oldest_messages: + await message.delete() + existing_messages = existing_messages[-MAX_MESSAGES:] + message_queue.extend(existing_messages) + except Exception: + logging.exception("Error processing existing messages") + + while True: + try: + guild_data = self.guilds_data.get(guild_id) + if not guild_data: + logging.warning("Guild data not found for guild %s. Stopping GIF sending.", guild_id) + return + + ecosystem_manager = guild_data["ecosystem_managers"].get(channel_id) + if not ecosystem_manager: + logging.warning("Ecosystem manager not found for channel %s. Stopping GIF sending.", channel_id) + return + + gif_data = ecosystem_manager.get_latest_gif() + if not gif_data: + await asyncio.sleep(0.01) + continue + + gif_bytes, timestamp = gif_data + + logging.info("Sending new ecosystem GIF message to thread %s in guild %s", thread_id, guild_id) + + retry_count = 0 + while retry_count < 3: + try: + new_message = await thread.send( + file=discord.File(io.BytesIO(gif_bytes), filename="ecosystem.gif") + ) + break + except discord.errors.HTTPException as e: + retry_count += 1 + logging.warning("Failed to send GIF (attempt %d): %s", retry_count, str(e)) + await asyncio.sleep(5) + else: + logging.error("Failed to send GIF after 3 attempts. Skipping this update.") + continue + + # If the queue is full, the oldest message will be automatically removed + # We need to delete it from Discord as well + if len(message_queue) == MAX_MESSAGES: + old_message = message_queue[0] + try: + await old_message.delete() + except discord.errors.NotFound: + logging.warning("Old message not found, it may have been deleted already.") + + message_queue.append(new_message) + + except Exception: + logging.exception("Unexpected error in send_gifs") + await asyncio.sleep(10) # Wait a bit before retrying the whole loop + + async def find_existing_messages(self, channel: discord.TextChannel) -> list[discord.Message]: + """Find existing ecosystem messages in the given channel. + + Args: + ---- + channel (discord.TextChannel): The channel to search for messages. + + Returns: + ------- + list[discord.Message]: A list of existing ecosystem messages, sorted by creation time. + + """ + existing_messages = [message async for message in channel.history(limit=100) if message.author == self.user] + return sorted(existing_messages, key=lambda m: m.created_at) + + async def run_bot(self) -> None: + """Start the bot and connects to Discord.""" + print("Starting bot...") + if not BOT_TOKEN: + error_message = "BOT_TOKEN is not set" + raise ValueError(error_message) + await self.start(BOT_TOKEN) + + async def on_guild_join(self, guild: discord.Guild) -> None: + """Event receiver for when the bot joins a new guild.""" + await self.initialize_guild(guild) + + embed = discord.Embed( + title="EcoCord Bot Installed!", + description="Bot installed. Admins can use `/configure` to set it up.", + color=discord.Color.blue(), + ) + + # Separate channels into admin-visible and others + admin_channels = [] + other_channels = [] + + for channel in guild.text_channels: + if channel.permissions_for(guild.me).send_messages: + if channel.overwrites_for(guild.default_role).read_messages is not False and any( + role.permissions.administrator + for role in guild.roles + if channel.overwrites_for(role).read_messages is not False + ): + admin_channels.append(channel) + else: + other_channels.append(channel) + + # Try admin channels first, then other channels + for channel in admin_channels + other_channels: + try: + await channel.send(embed=embed) + logging.info( + "Sent welcome message in guild %s (ID: %s) in channel %s", guild.name, guild.id, channel.name + ) + except discord.errors.Forbidden: + continue # Try the next channel if this one didn't work + else: + return # Exit the method after successfully sending the message + # If we've gone through all channels and couldn't send the message + logging.warning( + "Couldn't find any suitable channel to send welcome message in guild %s (ID: %s)", guild.name, guild.id + ) + + async def on_guild_remove(self, guild: discord.Guild) -> None: + """Event receiver for when the bot is removed from a guild.""" + if guild.id in self.guilds_data: + # Stop all ecosystem managers for this guild + await self.stop_ecosystems(guild.id) + # Remove the guild data + del self.guilds_data[guild.id] + logging.info("Removed from guild %s (ID: %s)", guild.name, guild.id) + + async def safe_task(self, coro: Coroutine[Any, Any, Any], task_name: str) -> None: + try: + await coro + except Exception as e: # noqa: BLE001 + print(f"Exception in {task_name}: {e!s}") + import traceback + + traceback.print_exc() + + +class ConfigureView(discord.ui.View): + """View for configuring EcoCord.""" + + def __init__(self, client: discord.Client, guild_id: int, visible_channels: list[discord.TextChannel]) -> None: + """Initialize the ConfigureView. + + Args: + ---- + client: The Discord client. + guild_id: The ID of the guild being configured. + visible_channels: List of visible text channels in the guild. + + """ + super().__init__() + self.client = client + self.guild_id = guild_id + self.visible_channels = visible_channels + + # Get existing configuration + guild_data = self.client.guilds_data.get(self.guild_id, {}) + self.managed_channels = [ + channel for channel in visible_channels if channel.id in guild_data.get("ecosystem_managers", {}) + ] + self.gif_channel = next( + (channel for channel in visible_channels if channel.id == guild_data.get("gif_channel_id")), None + ) + + # Add managed channels select + self.add_item( + discord.ui.ChannelSelect( + custom_id="managed_channels", + placeholder="Select managed channels", + min_values=1, + max_values=len(visible_channels), + channel_types=[discord.ChannelType.text], + default_values=self.managed_channels, + ) + ) + + # Add GIF channel select + self.add_item( + discord.ui.ChannelSelect( + custom_id="gif_channel", + placeholder="Select GIF channel", + min_values=1, + max_values=1, + channel_types=[discord.ChannelType.text], + default_values=[self.gif_channel] if self.gif_channel else None, + ) + ) + + @discord.ui.button(label="Submit", style=discord.ButtonStyle.primary) + async def submit(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: + await self.on_submit(interaction) + + async def on_submit(self, interaction: discord.Interaction) -> None: + client = interaction.client + guild_data = client.guilds_data.get(interaction.guild.id) + + if not self.managed_channels or not self.gif_channel: + await interaction.response.send_message( + "Please select both managed channels and a GIF channel before submitting.", ephemeral=True + ) + return + + if guild_data: + guild_data["gif_channel_id"] = self.gif_channel.id + + # Update the guild configuration in the database + config = GuildConfig( + guild_id=interaction.guild.id, + allowed_channels=[channel.id for channel in self.managed_channels], + gif_channel=self.gif_channel.id, + ) + try: + await client.database.set_guild_config(config) + except Exception: + logging.exception("Failed to update database") + await interaction.followup.send( + "An error occurred while saving the configuration. Please try again.", ephemeral=True + ) + return + + confirmation_message = ( + "Configuration complete!\n\n" + f"Bot configured to run in channels: {', '.join(channel.name for channel in self.managed_channels)}\n" + f"GIF threads will be created in: #{self.gif_channel.name}" + ) + + try: + message = await interaction.original_response() + await message.edit(content=confirmation_message, view=None) + except discord.NotFound: + # If the original message was deleted, send a new message + await interaction.followup.send(content=confirmation_message, ephemeral=True) + + try: + await client.reconfigure_channels(interaction.guild.id, self.managed_channels) + except Exception: + logging.exception("Failed to reconfigure channels") + await interaction.followup.send( + "Configuration saved, but there was an error applying the changes. " + "Please try again or contact support.", + ephemeral=True, + ) + + self.stop() + + async def interaction_check(self, interaction: discord.Interaction) -> bool: + if interaction.data["component_type"] == discord.ComponentType.channel_select.value: + if interaction.data["custom_id"] == "managed_channels": + self.managed_channels = [ + interaction.guild.get_channel(int(channel_id)) for channel_id in interaction.data["values"] + ] + elif interaction.data["custom_id"] == "gif_channel": + self.gif_channel = interaction.guild.get_channel(int(interaction.data["values"][0])) + + # Acknowledge the interaction + await interaction.response.defer() + return True + + +@app_commands.command(name="configure", description="Configure EcoCord") +@app_commands.default_permissions(administrator=True) +async def configure(interaction: discord.Interaction) -> None: + """Configure EcoCord settings for the guild.""" + if not interaction.user.guild_permissions.administrator: + await interaction.response.send_message("You don't have permission to use this command.", ephemeral=True) + return + + visible_channels = [ + channel + for channel in interaction.guild.text_channels + if channel.permissions_for(interaction.guild.me).send_messages + ] + view = ConfigureView(interaction.client, interaction.guild.id, visible_channels) + await interaction.response.send_message("Please configure the bot:", view=view, ephemeral=True) diff --git a/ardent-andromedas/src/bot/discord_event.py b/ardent-andromedas/src/bot/discord_event.py new file mode 100644 index 0000000..e8a3079 --- /dev/null +++ b/ardent-andromedas/src/bot/discord_event.py @@ -0,0 +1,159 @@ +from dataclasses import dataclass +from datetime import datetime +from enum import Flag, auto +from typing import TYPE_CHECKING, Optional + +if TYPE_CHECKING: + import discord + + +@dataclass +class FakeUser: + """A class representing a fake user for testing or placeholder purposes. + + Attributes + ---------- + id (int): The unique identifier for the fake user. + display_name (str): The display name of the fake user. + + """ + + id: int + display_name: str + + +class EventType(Flag): + """EventType is an enumeration that represents different types of events that can occur in a Discord server. + + Attributes + ---------- + ON_LOAD : auto + Represents the event when the ecosystem is loaded. + MESSAGE : auto + Represents the event when a message is sent. + REACTION : auto + Represents the event when a reaction is added to a message. + TYPING : auto + Represents the event when a user starts typing. + + """ + + ON_LOAD = auto() + MESSAGE = auto() + REACTION = auto() + TYPING = auto() + + +@dataclass +class SerializableGuild: + """Serializable representation of a Discord guild. + + Attributes + ---------- + id (int): The unique identifier of the guild. + name (str): The name of the guild. + verification_level (int): The verification level of the guild. + + """ + + id: int + name: str + verification_level: int + + +@dataclass +class SerializableTextChannel: + """Serializable representation of a Discord text channel. + + Attributes + ---------- + id (int): The unique identifier of the text channel. + name (str): The name of the text channel. + + """ + + id: int + name: str + + +@dataclass +class SerializableMember: + """Serializable representation of a Discord member. + + Attributes + ---------- + id (int): The unique identifier of the member. + name (str): The username of the member. + display_name (str): The display name of the member. + roles (list[int]): List of role IDs the member has. + guild_id (int): The ID of the guild the member belongs to. + avatar (str): The URL of the member's avatar. + color (str): The color associated with the member's top role. + + """ + + id: int + name: str + display_name: str + roles: list[int] + guild_id: int + avatar: str + color: str + + +@dataclass +class DiscordEvent: + """A class representing a Discord event with relevant information. + + Attributes + ---------- + type (EventType): The type of the Discord event. + timestamp (datetime): The timestamp when the event occurred. + guild (SerializableGuild): Serializable representation of the Discord guild. + channel (SerializableTextChannel): Serializable representation of the text channel. + member (SerializableMember | FakeUser): Serializable representation of the member or a FakeUser. + content (str): The content or message associated with the event. + reaction_image (Optional[bytes]): The image data of the reaction emoji, if applicable. + + """ + + type: EventType + timestamp: datetime + guild: SerializableGuild + channel: SerializableTextChannel + member: SerializableMember | FakeUser + content: str + reaction_image: bytes | None = None + + @classmethod + def from_discord_objects( + cls, + type: EventType, + timestamp: datetime, + guild: Optional["discord.Guild"], + channel: Optional["discord.TextChannel"], + member: Optional["discord.Member"], + content: str, + reaction_image: bytes | None = None, + ) -> "DiscordEvent": + return cls( + type=type, + timestamp=timestamp, + guild=SerializableGuild(guild.id, guild.name, guild.verification_level.value) if guild else None, + channel=SerializableTextChannel(channel.id, channel.name) + if channel and hasattr(channel, "id") and hasattr(channel, "name") + else None, + member=SerializableMember( + member.id, + member.name, + member.display_name, + [role.id for role in member.roles], + member.guild.id, + str(member.avatar.url) if member.avatar else None, + str(member.color), + ) + if member + else None, + content=content, + reaction_image=reaction_image, + ) diff --git a/ardent-andromedas/src/bot/settings.py b/ardent-andromedas/src/bot/settings.py new file mode 100644 index 0000000..5e6a1cc --- /dev/null +++ b/ardent-andromedas/src/bot/settings.py @@ -0,0 +1,13 @@ +import os +from pathlib import Path + +from dotenv import load_dotenv + +dotenv_path = Path(Path(__file__).parent) / "../../.env" +load_dotenv(dotenv_path) +db_path = Path(__file__).parent / "db/" +test_db_path = Path(__file__).parent / "db/test" + +BOT_TOKEN = os.environ.get("BOT_TOKEN") + +BOT_TOKEN_ARRAY = os.environ.get("TOKEN_ARRAY") diff --git a/ardent-andromedas/src/bot/test_bot.py b/ardent-andromedas/src/bot/test_bot.py new file mode 100644 index 0000000..6997fbc --- /dev/null +++ b/ardent-andromedas/src/bot/test_bot.py @@ -0,0 +1,106 @@ +import asyncio +import random +import traceback +from contextlib import suppress +from datetime import UTC, datetime + +from .bot import EcoCordClient +from .discord_event import DiscordEvent, FakeUser + + +class TestEcoCordClient(EcoCordClient): + """A test client for EcoCordClient that generates fake Discord events.""" + + def __init__(self) -> None: + """Initialize the TestEcoCordClient with fake users, channels, and guild.""" + super().__init__() + self.fake_users = [FakeUser(id=i, display_name=f"User{i}") for i in range(1, 6)] + self.fake_channels = [type("Channel", (), {"id": i, "name": f"Channel{i}"})() for i in range(1, 4)] + self.fake_guild = type("Guild", (), {"id": 1, "name": "TestGuild"})() + self.fake_events_task = None + + async def on_ready(self) -> None: + """Event receiver for when the client is ready. Starts generating fake events.""" + print("Test client ready. Generating fake events...") + self.fake_events_task = asyncio.create_task(self.generate_fake_events()) + + async def generate_fake_events(self) -> None: + """Continuously generate fake events (messages, reactions, or typing).""" + while True: + await asyncio.sleep(random.uniform(0.5, 2.0)) + event_type = random.choice(["message", "reaction", "typing"]) + + if event_type == "message": + await self.generate_fake_message() + elif event_type == "reaction": + await self.generate_fake_reaction() + else: + await self.generate_fake_typing() + + async def generate_fake_message(self) -> None: + """Generate and process a fake message event.""" + user = random.choice(self.fake_users) + channel = random.choice(self.fake_channels) + event = DiscordEvent( + type="message", + timestamp=datetime.now(UTC), + guild=self.fake_guild, + channel=channel, + user=user, + content=f"Fake message {random.randint(1, 1000)}", + ) + await self.process_event(event) + + async def generate_fake_reaction(self) -> None: + """Generate and process a fake reaction event.""" + channel = random.choice(self.fake_channels) + event = DiscordEvent( + type="reaction", + timestamp=datetime.now(UTC), + guild=self.fake_guild, + channel=channel, + user=random.choice(self.fake_users), + content=f"👍 added on Fake message {random.randint(1, 1000)}", + ) + await self.process_event(event) + + async def generate_fake_typing(self) -> None: + """Generate and process a fake typing event.""" + channel = random.choice(self.fake_channels) + event = DiscordEvent( + type="typing", + timestamp=datetime.now(UTC), + guild=self.fake_guild, + channel=channel, + user=random.choice(self.fake_users), + content="User is typing", + ) + await self.process_event(event) + + async def run_bot(self) -> None: + """Run the test bot, generating fake events until stopped or an error occurs.""" + print("Starting test bot...") + self.ready = True + await self.on_ready() + try: + while True: + if self.fake_events_task and self.fake_events_task.done(): + # This will raise the exception if there is one + await self.fake_events_task + # Short sleep to prevent busy-waiting + await asyncio.sleep(0.1) + except asyncio.CancelledError: + print("Bot execution cancelled.") + except Exception: + # Re-raise the exception after printing the stack trace + traceback.print_exc() + raise + finally: + await self.stop_bot() + + async def stop_bot(self) -> None: + """Stop the bot by cancelling the fake events task.""" + if self.fake_events_task: + self.fake_events_task.cancel() + with suppress(asyncio.CancelledError): + await self.fake_events_task diff --git a/ardent-andromedas/src/data_gen/__init__.py b/ardent-andromedas/src/data_gen/__init__.py new file mode 100644 index 0000000..83653a0 --- /dev/null +++ b/ardent-andromedas/src/data_gen/__init__.py @@ -0,0 +1,5 @@ +"""Data generation module for the project. + +This package contains functions and utilities for generating +test data used in the main application. +""" diff --git a/ardent-andromedas/src/data_gen/generator.py b/ardent-andromedas/src/data_gen/generator.py new file mode 100644 index 0000000..a075fac --- /dev/null +++ b/ardent-andromedas/src/data_gen/generator.py @@ -0,0 +1,111 @@ +import asyncio +import random +from datetime import datetime + +import discord +from discord.ext import tasks + +from bot.settings import BOT_TOKEN_ARRAY as TOKENS + +random_messages: list[str] = [ + "Hello everyone!", + "How are u doing?", + "Testing some bots!", + "Anythin new!?", + "Good morning or evening!", + "Who is hungry?", + "teeeeexxttt", +] + + +class EventGenerator(discord.Client): + """A Discord bot client that generates random events and messages.""" + + def __init__(self, token: str, intents: discord.Intents) -> None: + """Initialize the EventGenerator. + + Args: + ---- + token (str): The Discord bot token. + intents (discord.Intents): The Discord intents for the bot. + + """ + super().__init__(intents=intents) + self.token: str = token + + async def on_ready(self) -> None: + """Event receiver for when the bot has successfully connected to Discord. + + Start the random message sending task. + """ + print(f"{self.user} has connected to Discord!") + self.send_random_message.start() + + @tasks.loop(seconds=5) + async def send_random_message(self) -> None: + """Send a random message to a random channel every 5 seconds.""" + print("Attempting to send a random message...") + channels = list(self.get_all_channels()) + if not channels: + print("No channels found.") + return + + channel = random.choice(channels) + print(f"Selected channel: {channel.name} (ID: {channel.id})") + + if isinstance(channel, discord.TextChannel): + async with channel.typing(): + await asyncio.sleep(1) # to simulate "Bot is typing..." + message = random.choice(random_messages) + print(f"Sending message: {message}") + await channel.send(message) + + async def on_typing(self, channel: discord.abc.Messageable, user: discord.User, when: datetime.datetime) -> None: + """Respond to typing events in channels. + + Sends a message if the typing user is a bot (excluding itself). + + Args: + ---- + channel (discord.abc.Messageable): The channel where typing occurred. + user (discord.User): The user who started typing. + when (datetime.datetime): The time when typing started. + + """ + if user.bot and user != self.user: + msg = f"@{user.name} is typing in #{channel.name} at `{when.hour:02}:{when.minute:02}:{when.second:02}`" + await channel.send(msg) + + async def on_message(self, message: discord.Message) -> None: + """React to messages sent in channels the bot can see. + + Adds a thumbs up reaction if the message content is "typing". + + Args: + ---- + message (discord.Message): The message that was sent. + + """ + if message.author == self.user: + return + if message.content.lower() == "typing": + await message.add_reaction("👍") + + async def start_bot(self) -> None: + """Start the bot using the provided token.""" + print(f"Starting bot with token: {self.token[:5]}...") # Log token (partially for security) + try: + await self.start(self.token) + except discord.errors.LoginFailure as e: + print(f"Login failed for token {self.token[:5]}: {e}") + + +async def run_bots() -> None: + """Create and run multiple bot instances using the provided tokens.""" + intents = discord.Intents.all() + bots = [EventGenerator(token, intents) for token in TOKENS] + await asyncio.gather(*[bot.start_bot() for bot in bots]) + + +if __name__ == "__main__": + asyncio.run(run_bots()) diff --git a/ardent-andromedas/src/ecosystem/__init__.py b/ardent-andromedas/src/ecosystem/__init__.py new file mode 100644 index 0000000..bff4b2d --- /dev/null +++ b/ardent-andromedas/src/ecosystem/__init__.py @@ -0,0 +1,9 @@ +"""Ecosystem module for managing and simulating ecological systems. + +This module provides the EcosystemManager class, which is responsible for +handling various aspects of ecosystem simulation and management. +""" + +from .ecosystem import EcosystemManager + +__all__ = ["EcosystemManager"] diff --git a/ardent-andromedas/src/ecosystem/bird.py b/ardent-andromedas/src/ecosystem/bird.py new file mode 100644 index 0000000..de8d24c --- /dev/null +++ b/ardent-andromedas/src/ecosystem/bird.py @@ -0,0 +1,156 @@ +import math +import random + +import pygame +from pygame import Surface, Vector2 + +from ecosystem.critter import Critter + + +class Bird(Critter): + """Represents a bird in the ecosystem simulation. + + This class handles the bird's movement, appearance, and lifecycle. + """ + + def __init__(self, member_id: int, x: int, y: int, width: int, height: int, avatar: bytes | None = None) -> None: + """Initialize a new Bird instance. + + Args: + ---- + member_id (int): The unique identifier for the bird. + x (int): The x-coordinate of the bird's position. + y (int): The y-coordinate of the bird's position. + width (int): The width of the simulation area. + height (int): The height of the simulation area. + avatar (bytes): The avatar data for the bird. + + """ + self.max_height = height * 0.6 + super().__init__( + member_id, random.randint(0, width), random.randint(0, int(self.max_height)), width, height, avatar + ) + self.position = Vector2(self.x, self.y) + self.velocity = Vector2(random.uniform(-1, 1), random.uniform(-1, 1)).normalize() * 2 + self.size = random.uniform(15, 25) + self.color = self.generate_color() + self.wing_angle = 0 + self.wing_speed = random.uniform(10, 15) + self.turn_chance = 0.02 + self.target_angle = 0 + self.turn_speed = random.uniform(0.5, 1.5) + + def generate_color(self) -> tuple[int, int, int]: + """Generate a random color for the bird. + + Returns + ------- + tuple[int, int, int]: A tuple representing an RGB color. + + """ + return ( + random.randint(200, 255), + random.randint(100, 200), + random.randint(100, 200), + ) + + def activate(self) -> None: + self.alive = True + + def deactivate(self) -> None: + self.alive = False + + def update(self, delta: float, activity: float) -> None: + """Update the bird's position and state. + + Args: + ---- + delta (float): The time elapsed since the last update. + activity (float): The current activity level of the simulation. + + """ + self.position += self.velocity * activity * delta * 60 + + if self.position.x < 0 or self.position.x > self.width: + self.velocity.x *= -1 + if self.position.y < 0 or self.position.y > self.max_height: + self.velocity.y *= -1 + + self.position.x = max(0, min(self.position.x, self.width)) + self.position.y = max(0, min(self.position.y, self.max_height)) + + if random.random() < self.turn_chance: + self.target_angle = random.uniform(-math.pi / 4, math.pi / 4) + + if abs(self.target_angle) > 0.01: + turn_amount = self.turn_speed * delta + turn_direction = 1 if self.target_angle > 0 else -1 + actual_turn = min(abs(self.target_angle), turn_amount) * turn_direction + + self.velocity.rotate_ip(math.degrees(actual_turn)) + self.target_angle -= actual_turn + + self.wing_angle = math.sin(pygame.time.get_ticks() * self.wing_speed * 0.001) * 45 + + if random.random() < 0.001 * delta: + self.alive = False + + self.x, self.y = self.position.x, self.position.y + + def draw(self, surface: Surface) -> None: + """Draw the bird on the given surface.""" + # Create a surface for the bird + bird_surface = pygame.Surface((int(self.size * 2), int(self.size * 2)), pygame.SRCALPHA) + + # Create a mask surface for the bird's shape + mask_surface = pygame.Surface((int(self.size * 2), int(self.size * 2)), pygame.SRCALPHA) + pygame.draw.circle(mask_surface, (255, 255, 255), (int(self.size), int(self.size)), int(self.size)) + + # Draw the avatar and apply the mask if available + if self.avatar_surface: + avatar_scaled = pygame.transform.scale(self.avatar_surface, (int(self.size * 2), int(self.size * 2))) + avatar_scaled.blit(mask_surface, (0, 0), special_flags=pygame.BLEND_RGBA_MULT) + bird_surface.blit(avatar_scaled, (0, 0)) + + # Draw the bird body shape with transparency + body_color = (*self.color, 150) # Add alpha value for transparency + body_surface = pygame.Surface((int(self.size * 2), int(self.size * 2)), pygame.SRCALPHA) + pygame.draw.circle(body_surface, body_color, (int(self.size), int(self.size)), int(self.size)) + bird_surface.blit(body_surface, (0, 0), special_flags=pygame.BLEND_RGBA_MAX) + + # Draw the combined bird surface on the main surface + surface.blit(bird_surface, (int(self.position.x - self.size), int(self.position.y - self.size))) + + # Draw wings (slightly transparent) + wing_color = (*self.color, 100) # More transparent wings + left_wing = self.position + Vector2(-self.size, 0).rotate(self.wing_angle) + right_wing = self.position + Vector2(self.size, 0).rotate(-self.wing_angle) + pygame.draw.polygon(surface, wing_color, [self.position, left_wing, right_wing]) + + # Draw eye + eye_position = self.position + self.velocity.normalize() * self.size * 0.5 + pygame.draw.circle(surface, (255, 255, 255), (int(eye_position.x), int(eye_position.y)), int(self.size * 0.2)) + pygame.draw.circle(surface, (0, 0, 0), (int(eye_position.x), int(eye_position.y)), int(self.size * 0.1)) + + # Draw beak + beak_position = self.position + self.velocity.normalize() * self.size + pygame.draw.polygon( + surface, + (255, 200, 0), + [ + beak_position, + beak_position + + Vector2(self.size * 0.3, self.size * 0.1).rotate(self.velocity.angle_to(Vector2(1, 0))), + beak_position + + Vector2(self.size * 0.3, -self.size * 0.1).rotate(self.velocity.angle_to(Vector2(1, 0))), + ], + ) + + def spawn(self) -> None: + """Respawn the bird at a random position in the top portion of the simulation area.""" + self.alive = True + self.position = Vector2(random.randint(0, self.width), random.randint(0, int(self.max_height))) + self.x, self.y = self.position.x, self.position.y + + def despawn(self) -> None: + self.alive = False diff --git a/ardent-andromedas/src/ecosystem/cloud_manager.py b/ardent-andromedas/src/ecosystem/cloud_manager.py new file mode 100644 index 0000000..16fae95 --- /dev/null +++ b/ardent-andromedas/src/ecosystem/cloud_manager.py @@ -0,0 +1,66 @@ +import os +import random +from pathlib import Path + +import pygame + + +class Cloud: + """Represents a cloud object in the game.""" + + def __init__(self, image_path: str, speed: float, target_width: int, target_height: int) -> None: + """Initialize the Cloud object.""" + try: + self.image = pygame.image.load(image_path).convert_alpha() + self.image = pygame.transform.scale(self.image, (target_width, target_height)) + except pygame.error as e: + print(f"Error loading cloud image: {e}") + self.speed = speed + self.x = 0 + self.y = 0 + + def update(self, delta: float) -> None: + """Update the cloud's position.""" + self.x -= self.speed * delta + if self.x <= -self.image.get_width(): + self.x = 0 + + def draw(self, surface: pygame.Surface) -> None: + """Draw the cloud on the given surface.""" + surface.blit(self.image, (self.x, self.y)) + surface.blit(self.image, (self.x + self.image.get_width(), self.y)) + + +class CloudManager: + """Manages multiple cloud objects in the game.""" + + def __init__(self, width: int, height: int) -> None: + """Initialize the CloudManager.""" + self.width = width + self.height = height + self.clouds: list[Cloud] = [] + self._load_clouds() + + def _load_clouds(self) -> None: + """Load cloud images and create Cloud objects.""" + cloud_groups = [group for group in os.listdir("assets/clouds") if Path("assets/clouds", group).is_dir()] + cloud_group = random.choice(cloud_groups) + cloud_path = f"assets/clouds/{cloud_group}" + cloud_images = sorted(os.listdir(cloud_path)) + + for i, image in enumerate(cloud_images): + if not image.endswith(".png"): + continue + speed = 0 if i == 0 else 3 + i * 10 + cloud = Cloud(str(Path(cloud_path) / image), speed, self.width, self.height) + self.clouds.append(cloud) + + def update(self, delta: float) -> None: + """Update all clouds.""" + for cloud in self.clouds: + cloud.update(delta) + + def draw(self, surface: pygame.Surface) -> None: + """Draw all clouds on the given surface.""" + for cloud in self.clouds: + cloud.draw(surface) diff --git a/ardent-andromedas/src/ecosystem/critter.py b/ardent-andromedas/src/ecosystem/critter.py new file mode 100644 index 0000000..bd98a23 --- /dev/null +++ b/ardent-andromedas/src/ecosystem/critter.py @@ -0,0 +1,84 @@ +import io +import logging +from abc import ABC, abstractmethod + +import pygame + + +class Critter(ABC): + """Abstract base class representing a critter in the ecosystem simulation. + + This class defines the interface for critters, including their basic properties + and required methods for updating, drawing, and lifecycle management. + """ + + def __init__( + self, member_id: int, x: float, y: float, width: int, height: int, avatar: bytes | None = None + ) -> None: + """Initialize a new Critter instance. + + Args: + ---- + member_id (int): Unique identifier for the critter. + x (float): Initial x-coordinate of the critter. + y (float): Initial y-coordinate of the critter. + width (int): Width of the ecosystem area. + height (int): Height of the ecosystem area. + avatar (bytes | None): The avatar image data for the critter, if available. + + """ + self.member_id = member_id + self.x = x + self.y = y + self.width = width + self.height = height + self.alive = True + + self.avatar = avatar + self.avatar_surface = None + if self.avatar: + try: + avatar_io = io.BytesIO(self.avatar) + avatar_image = pygame.image.load(avatar_io) + self.avatar_surface = pygame.transform.scale(avatar_image, (64, 64)).convert_alpha() + except pygame.error as e: + print(f"Failed to create avatar surface for critter {member_id}: {e}") + except Exception: + logging.exception("Unexpected error creating avatar surface for snake %s", member_id) + + @abstractmethod + def activate(self) -> None: + """Activates the critter.""" + + @abstractmethod + def deactivate(self) -> None: + """Deactivates the critter.""" + + @abstractmethod + def update(self, delta: float, activity: float) -> None: + """Update the critter's state and position. + + Args: + ---- + delta (float): Time elapsed since the last update. + activity (float): Current activity level in the ecosystem. + + """ + + @abstractmethod + def draw(self, surface: pygame.Surface) -> None: + """Draw the critter on the given surface. + + Args: + ---- + surface (pygame.Surface): The surface to draw the critter on. + + """ + + @abstractmethod + def spawn(self) -> None: + """Spawn the critter in the ecosystem.""" + + @abstractmethod + def despawn(self) -> None: + """Despawn the critter from the ecosystem.""" diff --git a/ardent-andromedas/src/ecosystem/ecosystem.py b/ardent-andromedas/src/ecosystem/ecosystem.py new file mode 100644 index 0000000..7b69bd2 --- /dev/null +++ b/ardent-andromedas/src/ecosystem/ecosystem.py @@ -0,0 +1,786 @@ +import asyncio +import atexit +import contextlib +import io +import multiprocessing +import time +from collections import deque +from collections.abc import Coroutine +from concurrent.futures import ThreadPoolExecutor +from datetime import UTC, datetime, timedelta +from typing import Any + +import numpy as np + +from .shared_numpy_array import SharedNumpyArray + +# Hide pygame welcome message +with contextlib.redirect_stdout(None): + import pygame + +import random + +from PIL import Image + +from bot.discord_event import ( + DiscordEvent, + EventType, + SerializableGuild, + SerializableMember, + SerializableTextChannel, +) +from storage import Database, UserInfo + +from .bird import Bird +from .cloud_manager import CloudManager +from .frog import Frog +from .reaction_emoji import ReactionEmoji +from .snake import Snake +from .speech_bubble import SpeechBubble +from .wordclouds import WordCloudObject + + +class Ecosystem: + """Represents the ecosystem simulation environment. + + This class manages the overall ecosystem, including various entities like plants, frogs, snakes, and birds. + It handles the simulation logic, drawing, and user interface elements. + """ + + def __init__( + self, + width: int, + height: int, + generate_gifs: bool = False, + gif_duration: int = 5, + fps: int = 30, + interactive: bool = True, + ) -> None: + """Initialize the Ecosystem. + + Args: + ---- + width (int): Width of the ecosystem surface. + height (int): Height of the ecosystem surface. + generate_gifs (bool): Whether to generate GIFs of the simulation. + gif_duration (int): Duration of each GIF in seconds. + fps (int): Frames per second for the simulation. + interactive (bool): Whether the ecosystem is interactive or not. + + """ + self.width = width + self.height = height + self.activity = 1 + self.elapsed_time = 0 + self.surface = pygame.Surface((width, height)) + + self.sky_colors = [ + (200, 200, 200), # Barren gray + (135, 206, 235), # Lush blue + ] + self.ground_colors = [ + (210, 180, 140), # Barren tan + (34, 139, 34), # Lush green + ] + + self.critters = [] + self.speech_bubbles = [] + self.reaction_emojis = [] + + self.font = None + self.activity_slider = None + self.reset_button = None + + self.generate_gifs = generate_gifs + self.fps = fps + self.interactive = interactive + + if self.generate_gifs: + self.gif_duration = gif_duration + self.frames_per_gif = self.fps * self.gif_duration + self.frame_count = 0 + self.shared_frames = SharedNumpyArray((self.frames_per_gif, height, width, 3)) + self.current_frame_index = multiprocessing.Value("i", 0) + self.frame_count_queue = multiprocessing.Queue() + self.gif_info_queue = multiprocessing.Queue() + self.gif_process = multiprocessing.Process( + target=self.run_gif_generation_process, + args=( + self.shared_frames, + self.current_frame_index, + self.frame_count_queue, + self.gif_info_queue, + self.fps, + ), + ) + self.gif_process.start() + + self.cloud_manager = CloudManager(self.width, self.height) + self.word_cloud = WordCloudObject( + "", + self.width, + self.height, + 5, + ) + + atexit.register(self.cleanup) + + def setup_ui(self) -> None: + """Set up the user interface elements for the ecosystem.""" + self.font = pygame.font.Font(None, 24) + + button_width = 120 + button_height = 30 + button_spacing = 10 + top_margin = 20 + left_margin = 20 + + self.activity_slider = pygame.Rect(left_margin, top_margin, 200, 20) + + button_y = top_margin + self.activity_slider.height + button_spacing + + self.reset_button = Button( + left_margin, + button_y, + button_width, + button_height, + "Reset", + (255, 0, 0), + (255, 255, 255), + self.font, + ) + + def update(self, delta: float) -> None: + """Update the state of the ecosystem. + + Args: + ---- + delta (float): Time elapsed since the last update. + + """ + self.elapsed_time += delta + + for critter in self.critters: + critter.update(delta, self.activity) + + self._clean_up_entities() + + self.cloud_manager.update(delta) + + self.speech_bubbles = [bubble for bubble in self.speech_bubbles if not bubble.is_expired()] + for bubble in self.speech_bubbles: + bubble.update(delta) + + self.reaction_emojis = [emoji for emoji in self.reaction_emojis if not emoji.is_expired()] + for emoji in self.reaction_emojis: + emoji.update(delta) + + def _clean_up_entities(self) -> None: + """Remove dead entities from the ecosystem.""" + self.critters = [critter for critter in self.critters if critter.alive] + + def draw(self) -> pygame.Surface: + """Draw the current state of the ecosystem. + + Returns + ------- + pygame.Surface: The surface with the drawn ecosystem. + + """ + sky_color = self.interpolate_color(self.sky_colors[0], self.sky_colors[1], self.activity) + self.surface.fill(sky_color) + + self.cloud_manager.draw(self.surface) + + ground_color = self.interpolate_color(self.ground_colors[0], self.ground_colors[1], self.activity) + ground_height = int(self.height * 0.35) + + # Create gradient for ground + gradient_surface = pygame.Surface((self.width, ground_height)) + for y in range(ground_height): + alpha = y / ground_height + color = self.interpolate_color(ground_color, (50, 100, 50), alpha) + pygame.draw.line(gradient_surface, color, (0, y), (self.width, y)) + + self.surface.blit(gradient_surface, (0, self.height - ground_height)) + + for bubble in self.speech_bubbles: + bubble.draw(self.surface) + + for critter in self.critters: + critter.draw(self.surface) + + for emoji in self.reaction_emojis: + emoji.draw(self.surface) + + self.word_cloud.draw(self.surface) + + return self.surface + + def post_update(self) -> None: + """Perform post-update operations, such as frame capturing for GIF generation.""" + if self.generate_gifs: + frame = pygame.surfarray.array3d(self.surface).transpose(1, 0, 2) + frames = self.shared_frames.get_array() + index = self.current_frame_index.value + frames[index] = frame + self.current_frame_index.value = (index + 1) % self.frames_per_gif + self.frame_count += 1 + + if self.frame_count % self.frames_per_gif == 0: + self.frame_count_queue.put(self.frame_count) + + def interpolate_color( + self, color1: tuple[int, int, int], color2: tuple[int, int, int], t: float + ) -> tuple[int, int, int]: + """Interpolate between two colors. + + Args: + ---- + color1 (tuple[int, int, int]): The first color. + color2 (tuple[int, int, int]): The second color. + t (float): Interpolation factor between 0 and 1. + + Returns: + ------- + tuple[int, int, int]: The interpolated color. + + """ + return tuple(int(c1 + (c2 - c1) * t) for c1, c2 in zip(color1, color2, strict=False)) + + def reset(self) -> None: + """Reset the ecosystem to its initial state.""" + self.critters.clear() + self.activity = 1 + self.elapsed_time = 0 + + @staticmethod + async def safe_task(coro: Coroutine[Any, Any, Any], task_name: str) -> None: + try: + await coro + except Exception as e: # noqa: BLE001 + print(f"Exception in {task_name}: {e!s}") + import traceback + + traceback.print_exc() + + @staticmethod + def run_gif_generation_process( + shared_frames: SharedNumpyArray, + current_frame_index: multiprocessing.Value, + frame_count_queue: multiprocessing.Queue, + gif_info_queue: multiprocessing.Queue, + fps: int, + ) -> None: + asyncio.run( + Ecosystem.safe_task( + Ecosystem._gif_generation_process( + shared_frames, current_frame_index, frame_count_queue, gif_info_queue, fps + ), + "_gif_generation_process", + ) + ) + + @staticmethod + async def _gif_generation_process( + shared_frames: SharedNumpyArray, + current_frame_index: multiprocessing.Value, + frame_count_queue: multiprocessing.Queue, + gif_info_queue: multiprocessing.Queue, + fps: int, + ) -> None: + """Process for generating GIFs from captured frames. + + Args: + ---- + shared_frames (SharedNumpyArray): Shared array containing frame data. + current_frame_index (multiprocessing.Value): Current frame index. + frame_count_queue (multiprocessing.Queue): Queue for frame count information. + gif_info_queue (multiprocessing.Queue): Queue for generated GIF information. + fps (int): Frames per second for the GIF. + + """ + frames = shared_frames.get_array() + + def optimize_frame(frame: np.ndarray) -> Image.Image: + return Image.fromarray(frame) + + with ThreadPoolExecutor() as executor: + while True: + frame_count = frame_count_queue.get() + if frame_count is None: + break + + start_index = current_frame_index.value + ordered_frames = np.roll(frames, -start_index, axis=0) + + optimized_frames = list(executor.map(optimize_frame, ordered_frames)) + + duration = int(1000 / fps) + + with io.BytesIO() as gif_buffer: + optimized_frames[0].save( + gif_buffer, + format="GIF", + save_all=True, + append_images=optimized_frames[1:], + optimize=False, + duration=[duration] * (len(optimized_frames)), + loop=0, + ) + + gif_data = gif_buffer.getvalue() + + gif_info_queue.put((gif_data, time.time())) + + def cleanup(self) -> None: + """Clean up resources used by the ecosystem.""" + if pygame.get_init(): + pygame.quit() + if hasattr(self, "gif_process"): + # Signal to stop the gif generation process + self.frame_count_queue.put(None) + + self.gif_process.join() + + +class Button: + """Represents a clickable button in the user interface.""" + + def __init__( + self, + x: int, + y: int, + width: int, + height: int, + text: str, + color: tuple[int, int, int], + text_color: tuple[int, int, int], + font: pygame.font.Font, + ) -> None: + """Initialize a Button. + + Args: + ---- + x (int): X-coordinate of the button. + y (int): Y-coordinate of the button. + width (int): Width of the button. + height (int): Height of the button. + text (str): Text to display on the button. + color (tuple[int, int, int]): Color of the button. + text_color (tuple[int, int, int]): Color of the text. + font (pygame.font.Font): Font for the button text. + + """ + self.rect = pygame.Rect(x, y, width, height) + self.text = text + self.color = color + self.text_color = text_color + self.font = font + + def draw(self, surface: pygame.Surface) -> None: + """Draw the button on the given surface. + + Args: + ---- + surface (pygame.Surface): Surface to draw the button on. + + """ + pygame.draw.rect(surface, self.color, self.rect) + text_surface = self.font.render(self.text, True, self.text_color) + text_rect = text_surface.get_rect(center=self.rect.center) + surface.blit(text_surface, text_rect) + + def is_clicked(self, position: tuple[int, int]) -> bool: + """Check if the button is clicked. + + Args: + ---- + position (tuple[int, int]): Position of the mouse click. + + Returns: + ------- + bool: True if the button is clicked, False otherwise. + + """ + return self.rect.collidepoint(position) + + +class EcosystemManager: + """Manages the ecosystem simulation and handles user interactions.""" + + def __init__( + self, + width: int = 1152, + height: int = 648, + generate_gifs: bool = False, + fps: int = 30, + interactive: bool = True, + ) -> None: + """Initialize the EcosystemManager. + + Args: + ---- + width (int): Width of the ecosystem window. + height (int): Height of the ecosystem window. + generate_gifs (bool): Whether to generate GIFs of the simulation. + fps (int): Frames per second for the simulation. + interactive (bool): Whether the ecosystem is interactive or not. + + """ + self.width = width + self.height = height + self.generate_gifs = generate_gifs + self.fps = fps + self.interactive = interactive + self.process = None + self.command_queue = multiprocessing.Queue() + self.gif_queue = multiprocessing.Queue() + self.running = False + + self.user_critters = {} + self.last_activity = {} + self.fake_user_ids = set() + self.message_history = deque() + + def start(self, show_controls: bool = True) -> None: + """Start the ecosystem simulation. + + Args: + ---- + show_controls (bool): Whether to show UI controls. + + """ + if self.process and self.process.is_alive(): + return + + self.running = True + + self.process = multiprocessing.Process(target=self.run_and_catch_exceptions, args=(show_controls,)) + self.process.start() + + def run_and_catch_exceptions(self, show_controls: bool) -> None: + asyncio.run(self.safe_task(self._run_ecosystem(show_controls), "_run_ecosystem")) + + def stop(self) -> None: + """Stop the ecosystem simulation.""" + self.running = False + self.command_queue.put(("stop", None)) + if self.process: + self.process.join() + + async def safe_task(self, coro: Coroutine[Any, Any, Any], task_name: str) -> None: + try: + await coro + except Exception as e: # noqa: BLE001 + print(f"Exception in {task_name}: {e!s}") + import traceback + + traceback.print_exc() + + async def _run_ecosystem(self, show_controls: bool) -> None: + """Run the ecosystem simulation loop. + + Args: + ---- + show_controls (bool): Whether to show UI controls. + + """ + print("Initializing Pygame") + pygame.init() + print("Pygame initialized") + + if self.interactive: + screen = pygame.display.set_mode((self.width, self.height)) + pygame.display.set_caption(f"Ecosystem Visualization {multiprocessing.current_process().name}") + else: + pygame.display.set_mode((1, 1), pygame.HIDDEN) + screen = pygame.Surface((self.width, self.height)) + + ecosystem = Ecosystem( + self.width, self.height, generate_gifs=self.generate_gifs, fps=self.fps, interactive=self.interactive + ) + ecosystem.setup_ui() + + clock = pygame.time.Clock() + + last_message_time = time.time() + message_interval = 2.0 # Send a message every 2 seconds + + while self.running: + delta = clock.tick(self.fps) / 1000.0 + + if self.interactive: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + self.running = False + elif show_controls and event.type == pygame.MOUSEBUTTONDOWN and event.button == 1: + self._handle_mouse_click(ecosystem, event.pos) + + # Simulate random messages + current_time = time.time() + if current_time - last_message_time >= message_interval: + await self.safe_task(self._simulate_random_message(ecosystem), "_simulate_random_message") + last_message_time = current_time + + ecosystem.update(delta) + screen.blit(ecosystem.draw(), (0, 0)) + ecosystem.post_update() + + if self.interactive and show_controls: + self._draw_controls(ecosystem, screen) + + if self.interactive: + pygame.display.flip() + + if ecosystem.generate_gifs and not ecosystem.gif_info_queue.empty(): + gif_data, timestamp = ecosystem.gif_info_queue.get() + self.gif_queue.put((gif_data, timestamp)) + + while not self.command_queue.empty(): + command, args = self.command_queue.get() + if command == "stop": + self.running = False + elif command == "process_event": + self._process_event(ecosystem, args) + elif command == "set_online_critters": + self._set_online_critters(ecosystem, args) + + pygame.quit() + ecosystem.cleanup() + + def _handle_mouse_click(self, ecosystem: Ecosystem, pos: tuple[int, int]) -> None: + """Handle mouse click events in the ecosystem. + + Args: + ---- + ecosystem (Ecosystem): The ecosystem instance. + pos (tuple[int, int]): Position of the mouse click. + + """ + if ecosystem.activity_slider.collidepoint(pos): + ecosystem.activity = (pos[0] - ecosystem.activity_slider.x) / ecosystem.activity_slider.width + elif ecosystem.reset_button.is_clicked(pos): + ecosystem.reset() + + def _draw_controls(self, ecosystem: Ecosystem, screen: pygame.Surface) -> None: + """Draw UI controls on the screen. + + Args: + ---- + ecosystem (Ecosystem): The ecosystem instance. + screen (pygame.Surface): Surface to draw the controls on. + + """ + pygame.draw.rect(screen, (200, 200, 200), ecosystem.activity_slider) + pygame.draw.rect( + screen, + (0, 255, 0), + ( + ecosystem.activity_slider.x, + ecosystem.activity_slider.y, + ecosystem.activity_slider.width * ecosystem.activity, + ecosystem.activity_slider.height, + ), + ) + + ecosystem.reset_button.draw(screen) + + activity_text = ecosystem.font.render(f"Activity: {ecosystem.activity:.2f}", True, (0, 0, 0)) + screen.blit(activity_text, (ecosystem.activity_slider.x, ecosystem.activity_slider.y - 20)) + + stats_text = ecosystem.font.render( + f"Critters: {len(ecosystem.critters)}", + True, + (0, 0, 0), + ) + screen.blit(stats_text, (25, ecosystem.activity_slider.y + 70)) + + def process_event(self, event: DiscordEvent, user_info: UserInfo) -> None: + """Process a Discord event in the ecosystem. + + Args: + ---- + event (DiscordEvent): The Discord event to process. + user_info (UserInfo): User information including avatar and role color. + + """ + self.command_queue.put(("process_event", (event, user_info))) + + def _process_event(self, ecosystem: Ecosystem, event_data: tuple[DiscordEvent, UserInfo]) -> None: + """Process a Discord event within the ecosystem process. + + Args: + ---- + ecosystem (Ecosystem): The ecosystem instance. + event_data (tuple[DiscordEvent, UserInfo]): The Discord event and user info to process. + + """ + event, user_info = event_data + current_time = datetime.now(UTC) + user_id = event.member.id + + if event.type == EventType.MESSAGE: + self._handle_user_activity(ecosystem, user_id, current_time, False, user_info) + self._add_message_to_history(ecosystem, event.content, current_time) + + critter = self.user_critters.get(user_id) + if critter: + speech_bubble = SpeechBubble( + critter, event.content, self.width, self.height, duration=5, bg_color=user_info.role_color + ) + ecosystem.speech_bubbles.append(speech_bubble) + elif event.type == EventType.TYPING: + self._handle_user_activity(ecosystem, user_id, current_time, True, user_info) + elif event.type == EventType.REACTION: + self._handle_reaction(ecosystem, event) + + self._remove_inactive_users(ecosystem, current_time) + + def _handle_user_activity( + self, ecosystem: Ecosystem, user_id: int, current_time: datetime, is_typing: bool, user_info: UserInfo + ) -> None: + """Handle user activity in the ecosystem. + + Args: + ---- + ecosystem (Ecosystem): The ecosystem instance. + user_id (int): ID of the user. + current_time (datetime): Current timestamp. + is_typing (bool): Whether the user is typing. + user_info (UserInfo): User information including avatar and role color. + + """ + if user_id not in self.user_critters: + self._spawn_new_critter(ecosystem, user_id, user_info) + + self.last_activity[user_id] = current_time + self._remove_inactive_users(ecosystem, current_time) + + def set_online_critters(self, online_members: list[UserInfo]) -> None: + """Set the online critters based on the list of online members' info. + + Args: + ---- + online_members (List[UserInfo]): List of UserInfo objects for online members. + + """ + self.command_queue.put(("set_online_critters", online_members)) + + def _set_online_critters(self, ecosystem: Ecosystem, online_members: list[UserInfo]) -> None: + """Set the online critters within the ecosystem process. + + Args: + ---- + ecosystem (Ecosystem): The ecosystem instance. + online_members (List[UserInfo]): List of UserInfo objects for online members. + + """ + for user_info in online_members: + self._spawn_new_critter(ecosystem, user_info.user_id, user_info) + + def _spawn_new_critter(self, ecosystem: Ecosystem, user_id: int, user_info: UserInfo | None = None) -> None: + if user_id not in self.user_critters: + critter_type = random.choice([Bird, Snake, Frog]) + + avatar_data = user_info.avatar_data if user_info else None + + critter = critter_type( + user_id, + random.randint(0, ecosystem.width), + ecosystem.height - 20, + ecosystem.width, + ecosystem.height, + avatar=avatar_data, + ) + critter.spawn() + self.user_critters[user_id] = critter + ecosystem.critters.append(critter) + + def _remove_inactive_users(self, ecosystem: Ecosystem, current_time: datetime) -> None: + """Remove inactive users from the ecosystem. + + Args: + ---- + ecosystem (Ecosystem): The ecosystem instance. + current_time (datetime): Current timestamp. + + """ + one_minute = timedelta(minutes=1) + inactive_users = [ + user_id for user_id, last_time in self.last_activity.items() if current_time - last_time > one_minute + ] + for user_id in inactive_users: + self._remove_critter(ecosystem, user_id) + + def _remove_critter(self, ecosystem: Ecosystem, user_id: int) -> None: + if user_id in self.user_critters: + critter = self.user_critters.pop(user_id) + ecosystem.critters.discard(critter) + self.last_activity.pop(user_id, None) + + def get_latest_gif(self) -> tuple[bytes, float] | None: + """Get the latest generated GIF. + + Returns + ------- + tuple[bytes, float] | None: Tuple containing GIF data and timestamp, or None if no GIF is available. + + """ + if not self.gif_queue.empty(): + return self.gif_queue.get() + return None + + async def _simulate_random_message(self, ecosystem: Ecosystem) -> None: + guild_id = random.randint(1, 1000000) + channel_id = random.randint(1, 1000000) + + db = Database("ecocord") + user_info = await db.get_random_user_info() + if user_info: + content = "Hello, ecosystem!" + event = DiscordEvent( + type=EventType.MESSAGE, + content=content, + timestamp=datetime.now(UTC), + guild=SerializableGuild(guild_id, "Simulated Guild", 0), + channel=SerializableTextChannel(channel_id, "simulated-channel"), + member=SerializableMember( + id=user_info.user_id, + name=f"SimulatedUser_{user_info.user_id}", + display_name=f"SimulatedUser_{user_info.user_id}", + roles=[], + guild_id=guild_id, + avatar=user_info.avatar_data, + color=user_info.role_color, + ), + ) + self._process_event(ecosystem, (event, user_info)) + self.fake_user_ids.add(user_info.user_id) + else: + print("No user info found in the database.") + + def _add_message_to_history(self, ecosystem: Ecosystem, content: str, timestamp: datetime) -> None: + self.message_history.append((content, timestamp)) + self._clean_old_messages() + self._update_word_cloud(ecosystem) + + def _clean_old_messages(self) -> None: + current_time = datetime.now(UTC) + one_hour_ago = current_time - timedelta(hours=1) + while self.message_history and self.message_history[0][1] < one_hour_ago: + self.message_history.popleft() + + def _update_word_cloud(self, ecosystem: Ecosystem) -> None: + all_text = " ".join(content for content, _ in self.message_history) + ecosystem.word_cloud.change_words(all_text) + + def _handle_reaction(self, ecosystem: Ecosystem, event: DiscordEvent) -> None: + if event.reaction_image: + reaction_emoji = ReactionEmoji( + screen_width=ecosystem.width, + screen_height=ecosystem.height, + image_data=event.reaction_image, + duration=5, + ) + ecosystem.reaction_emojis.append(reaction_emoji) diff --git a/ardent-andromedas/src/ecosystem/frog.py b/ardent-andromedas/src/ecosystem/frog.py new file mode 100644 index 0000000..eae4760 --- /dev/null +++ b/ardent-andromedas/src/ecosystem/frog.py @@ -0,0 +1,158 @@ +import math +import random + +import pygame + +from ecosystem.critter import Critter + + +class Frog(Critter): + """Represents a frog in the ecosystem simulation. + + This class manages the frog's appearance, movement, and lifecycle. + """ + + def __init__( + self, member_id: int, x: float, y: float, width: int, height: int, avatar: bytes | None = None + ) -> None: + """Initialize a new Frog instance. + + Args: + ---- + member_id (int): Unique identifier for the frog. + x (float): Initial x-coordinate of the frog. + y (float): Initial y-coordinate of the frog. + width (int): Width of the ecosystem area. + height (int): Height of the ecosystem area. + avatar (bytes): Avatar data for the frog. + + """ + super().__init__(member_id, x, y, width, height, avatar) + self.size = random.uniform(20, 30) + self.color = (random.randint(100, 150), random.randint(200, 255), random.randint(100, 150)) + self.eye_color = (255, 255, 255) + self.pupil_color = (0, 0, 0) + + self.jump_height = random.uniform(50, 100) + self.jump_duration = random.uniform(0.5, 1.0) + self.rest_duration = random.uniform(1.0, 3.0) + + self.state = "rest" + self.state_time = 0 + self.jump_start_y = self.y + self.jump_target_x = self.x + + self.scale = 1.0 + + def activate(self) -> None: + """Activates the frog.""" + self.alive = True + + def deactivate(self) -> None: + """Deactivates the frog.""" + self.alive = False + + def update(self, delta: float, activity: float) -> None: + """Update the frog's state and position. + + Args: + ---- + delta (float): Time elapsed since the last update. + activity (float): Current activity level in the ecosystem (unused). + + """ + self.state_time += delta + + if self.state == "rest": + if self.state_time >= self.rest_duration: + self.start_jump() + elif self.state == "jump": + progress = self.state_time / self.jump_duration + if progress <= 1: + self.y = self.jump_start_y - self.jump_height * math.sin(progress * math.pi) + self.x += (self.jump_target_x - self.x) * delta / self.jump_duration + else: + self.state = "rest" + self.state_time = 0 + self.y = self.jump_start_y + + if random.random() < 0.01 * delta: + self.deactivate() + + def start_jump(self) -> None: + """Initiate a jump for the frog. + + This method sets up the parameters for the frog's jump, + including the jump target and resetting the state. + """ + self.state = "jump" + self.state_time = 0 + self.jump_start_y = self.y + jump_distance = random.uniform(50, 150) + + new_x = self.x + random.uniform(-jump_distance, jump_distance) + + self.jump_target_x = max(self.size / 2, min(self.width - self.size / 2, new_x)) + new_x = self.x + random.uniform(-jump_distance, jump_distance) + + self.jump_target_x = max(self.size / 2, min(self.width - self.size / 2, new_x)) + + def draw(self, surface: pygame.Surface) -> None: + """Draw the frog on the given surface. + + Args: + ---- + surface (pygame.Surface): The surface to draw the frog on. + + """ + self.scale = 0.5 + (self.y / self.height) * 0.5 + scaled_size = int(self.size * self.scale) + + # Create a surface for the frog body + frog_surface = pygame.Surface((scaled_size, scaled_size), pygame.SRCALPHA) + + # Create a mask surface for the frog's shape + mask_surface = pygame.Surface((scaled_size, scaled_size), pygame.SRCALPHA) + pygame.draw.ellipse(mask_surface, (255, 255, 255), pygame.Rect(0, 0, scaled_size, scaled_size)) + + # Draw the avatar and apply the mask if available + if self.avatar_surface: + avatar_scaled = pygame.transform.scale(self.avatar_surface, (scaled_size, scaled_size)) + avatar_scaled.blit(mask_surface, (0, 0), special_flags=pygame.BLEND_RGBA_MULT) + frog_surface.blit(avatar_scaled, (0, 0)) + + # Draw the frog body shape with transparency + body_color = (*self.color, 150) # Add alpha value for transparency + body_surface = pygame.Surface((scaled_size, scaled_size), pygame.SRCALPHA) + pygame.draw.ellipse(body_surface, body_color, pygame.Rect(0, 0, scaled_size, scaled_size)) + frog_surface.blit(body_surface, (0, 0), special_flags=pygame.BLEND_RGBA_MAX) + + # Draw the combined frog surface on the main surface + surface.blit(frog_surface, (int(self.x - scaled_size // 2), int(self.y - scaled_size // 2))) + + # Draw eyes (white part) + eye_size = scaled_size // 4 + left_eye_pos = (int(self.x - scaled_size // 4), int(self.y - scaled_size // 4)) + right_eye_pos = (int(self.x + scaled_size // 4), int(self.y - scaled_size // 4)) + pygame.draw.circle(surface, self.eye_color, left_eye_pos, eye_size) + pygame.draw.circle(surface, self.eye_color, right_eye_pos, eye_size) + + # Draw pupils + pupil_size = eye_size // 2 + pygame.draw.circle(surface, self.pupil_color, left_eye_pos, pupil_size) + pygame.draw.circle(surface, self.pupil_color, right_eye_pos, pupil_size) + + # Draw mouth + mouth_rect = pygame.Rect(self.x - scaled_size // 4, self.y, scaled_size // 2, scaled_size // 4) + pygame.draw.arc(surface, (50, 50, 50), mouth_rect, math.pi, 2 * math.pi, 2) + + def spawn(self) -> None: + """Spawn the frog in the ecosystem.""" + self.activate() + self.y = self.height - 20 + self.x = random.randint(0, self.width) + self.scale = 0.1 + + def despawn(self) -> None: + """Despawn the frog from the ecosystem.""" + self.deactivate() diff --git a/ardent-andromedas/src/ecosystem/plant.py b/ardent-andromedas/src/ecosystem/plant.py new file mode 100644 index 0000000..7b28153 --- /dev/null +++ b/ardent-andromedas/src/ecosystem/plant.py @@ -0,0 +1,60 @@ +import random + +import pygame + + +class Plant: + """Represents a plant in the ecosystem. + + Attributes + ---------- + x (int): X-coordinate of the plant. + y (int): Y-coordinate of the plant. + size (float): Current size of the plant. + max_size (int): Maximum size the plant can grow to. + growth_rate (float): Rate at which the plant grows. + color (tuple): RGB color of the plant. + alive (bool): Whether the plant is alive or not. + + """ + + def __init__(self, x: int, y: int) -> None: + """Initialize a new Plant instance. + + Args: + ---- + x (int): X-coordinate of the plant. + y (int): Y-coordinate of the plant. + + """ + self.x = x + self.y = y + self.size = 0 + self.max_size = random.randint(20, 50) + self.growth_rate = random.uniform(5, 15) + self.color = (0, random.randint(100, 200), 0) + self.alive = True + + def update(self, dt: float, activity: float) -> None: + """Update the plant's state. + + Args: + ---- + dt (float): Time delta since last update. + activity (float): Environmental activity factor affecting growth. + + """ + if self.size < self.max_size: + self.size += self.growth_rate * activity * dt + elif random.random() < (1 - activity) * 0.5 * dt: + self.alive = False + + def draw(self, surface: pygame.Surface) -> None: + """Draw the plant on the given surface. + + Args: + ---- + surface (pygame.Surface): Surface to draw the plant on. + + """ + pygame.draw.circle(surface, self.color, (self.x, self.y), int(self.size)) diff --git a/ardent-andromedas/src/ecosystem/reaction_emoji.py b/ardent-andromedas/src/ecosystem/reaction_emoji.py new file mode 100644 index 0000000..d4214ca --- /dev/null +++ b/ardent-andromedas/src/ecosystem/reaction_emoji.py @@ -0,0 +1,67 @@ +import io +import random + +import pygame +from PIL import Image + + +class ReactionEmoji: + """Represents an emoji reaction that appears and animates on the screen.""" + + def __init__(self, screen_width: int, screen_height: int, image_data: bytes, duration: float = 5.0) -> None: + """Initialize a ReactionEmoji instance. + + Args: + ---- + screen_width (int): Width of the screen. + screen_height (int): Height of the screen. + image_data (bytes): Raw image data for the emoji. + duration (float, optional): Duration of the emoji animation in seconds. Defaults to 5.0. + + """ + self.x = random.randint(0, screen_width) + self.y = random.randint(0, screen_height) + self.velocity_x = random.uniform(-100, 100) + self.velocity_y = random.uniform(-100, 100) + self.opacity = 255 + self.screen_width = screen_width + self.screen_height = screen_height + self.image = self._create_pygame_image(image_data) + self.duration = duration + self.time_left = duration + self.original_size = self.image.get_size() + self.current_size = self.original_size + + def _create_pygame_image(self, image_data: bytes) -> pygame.Surface: + pil_image = Image.open(io.BytesIO(image_data)) + pil_image = pil_image.convert("RGBA") + return pygame.image.fromstring(pil_image.tobytes(), pil_image.size, pil_image.mode) + + def update(self, delta: float) -> None: + self.time_left -= delta + progress = self.time_left / self.duration + + # Update position + self.x += self.velocity_x * delta + self.y += self.velocity_y * delta + + # Bounce off screen edges + if self.x < 0 or self.x > self.screen_width: + self.velocity_x *= -1 + if self.y < 0 or self.y > self.screen_height: + self.velocity_y *= -1 + + # Update size and opacity + self.current_size = (int(self.original_size[0] * progress), int(self.original_size[1] * progress)) + self.opacity = int(255 * progress) + + def draw(self, surface: pygame.Surface) -> None: + if self.current_size[0] > 0 and self.current_size[1] > 0: + scaled_image = pygame.transform.scale(self.image, self.current_size) + scaled_image.set_alpha(self.opacity) + surface.blit( + scaled_image, (int(self.x) - self.current_size[0] // 2, int(self.y) - self.current_size[1] // 2) + ) + + def is_expired(self) -> bool: + return self.time_left <= 0 diff --git a/ardent-andromedas/src/ecosystem/shared_numpy_array.py b/ardent-andromedas/src/ecosystem/shared_numpy_array.py new file mode 100644 index 0000000..d2cae87 --- /dev/null +++ b/ardent-andromedas/src/ecosystem/shared_numpy_array.py @@ -0,0 +1,35 @@ +import multiprocessing + +import numpy as np + + +class SharedNumpyArray: + """A class for creating and managing shared numpy arrays across multiple processes. + + This class provides a way to create a numpy array that can be shared between + different processes using multiprocessing.RawArray as the underlying storage. + """ + + def __init__(self, shape: tuple[int, ...], dtype: np.dtype = np.uint8) -> None: + """Initialize a SharedNumpyArray. + + Args: + ---- + shape (tuple[int, ...]): The shape of the numpy array. + dtype (np.dtype, optional): The data type of the array. Defaults to np.uint8. + + """ + self.shape = shape + self.dtype = dtype + size = int(np.prod(shape)) + self.shared_array = multiprocessing.RawArray("B", size) + + def get_array(self) -> np.ndarray: + """Get a numpy array view of the shared memory. + + Returns + ------- + np.ndarray: A numpy array that shares memory with the underlying RawArray. + + """ + return np.frombuffer(self.shared_array, dtype=self.dtype).reshape(self.shape) diff --git a/ardent-andromedas/src/ecosystem/snake.py b/ardent-andromedas/src/ecosystem/snake.py new file mode 100644 index 0000000..9001216 --- /dev/null +++ b/ardent-andromedas/src/ecosystem/snake.py @@ -0,0 +1,196 @@ +import math +import random + +import pygame +from pygame import Color, Surface, Vector2 + +from ecosystem.critter import Critter + + +class Snake(Critter): + """Represents a snake in the ecosystem. + + The snake moves towards a target, grows in length, and can spawn or despawn. + """ + + def __init__(self, member_id: int, x: int, y: int, width: int, height: int, avatar: bytes | None = None) -> None: + """Initialize a new Snake instance. + + Args: + ---- + member_id (int): The unique identifier for the snake. + x (int): Initial x-coordinate of the snake's head. + y (int): Initial y-coordinate of the snake's head. + width (int): Width of the game area. + height (int): Height of the game area. + avatar (bytes): The avatar data for the snake. + + """ + super().__init__(member_id, x, y, width, height, avatar) + self.segments = [Vector2(x, y)] + self.x = x + self.y = y + self.direction = Vector2(1, 0) + self.min_y = int(self.height * 0.65) + self.max_y = int(self.height * 0.80) + self.speed = 2 + self.length = 50 + self.color = self.generate_color() + self.target = self.get_new_target() + self.state = "inactive" + self.scale = 0.1 + + def generate_color(self) -> Color: + """Generate a random color for the snake. + + Returns + ------- + Color: A pygame Color object with random RGB values. + + """ + return pygame.Color(random.randint(100, 255), random.randint(100, 255), random.randint(100, 255)) + + def get_new_target(self) -> Vector2: + """Generate a new random target position for the snake. + + Returns + ------- + Vector2: A new target position within the game area. + + """ + return Vector2(random.randint(0, self.width), random.randint(self.min_y, self.max_y)) + + def update(self, delta: float, activity: float) -> None: + """Update the snake's position and state. + + Args: + ---- + delta (float): Time elapsed since the last update. + activity (float): Activity level affecting the snake's speed. + + """ + if self.state == "inactive": + return + + if self.state == "spawn": + self.scale = min(1.0, self.scale + delta) + if self.scale == 1.0: + self.state = "active" + elif self.state == "despawn": + self.scale = max(0.0, self.scale - delta) + if self.scale == 0.0: + self.alive = False + self.state = "inactive" + return + + head = self.segments[0] + to_target = self.target - head + if to_target.length() < 10: + self.target = self.get_new_target() + + to_target = self.target - head + self.direction = to_target.normalize() + new_head = head + self.direction * self.speed * activity * delta * 60 + + new_head.y = max(min(new_head.y, self.max_y), self.min_y) + new_head.x = new_head.x % self.width # Wrap around horizontally + + self.segments.insert(0, new_head) + if len(self.segments) > self.length: + self.segments.pop() + + # Update self.x and self.y to match the new head position + self.x = int(new_head.x) + self.y = int(new_head.y) + + def draw(self, surface: Surface) -> None: + """Draw the snake on the given surface. + + Args: + ---- + surface (Surface): The pygame Surface to draw on. + + """ + # Draw body segments with sinusoidal wave + time = pygame.time.get_ticks() / 1000 + for i, segment in enumerate(self.segments[1:], 1): + radius = int((10 * (1 - i / len(self.segments)) + 5) * self.scale) + alpha = int(255 * (1 - i / len(self.segments))) + color = (*self.color[:3], alpha) + + # Apply sinusoidal wave + wave_amplitude = 5 * self.scale * (1 - i / len(self.segments)) + wave_frequency = 0.2 + wave_speed = 3 + wave_offset = math.sin(time * wave_speed + i * wave_frequency) * wave_amplitude + + wave_vector = self.direction.rotate(90).normalize() * wave_offset + wave_pos = segment + wave_vector + + pygame.draw.circle(surface, color, (int(wave_pos.x), int(wave_pos.y)), radius) + + # Draw head + head = self.segments[0] + head_radius = int(15 * self.scale) + + # Create a surface for the head + head_surface = pygame.Surface((head_radius * 2, head_radius * 2), pygame.SRCALPHA) + + # Draw the base color of the head + pygame.draw.circle(head_surface, self.color, (head_radius, head_radius), head_radius) + + # Apply avatar if available + if self.avatar_surface: + avatar_scaled = pygame.transform.scale(self.avatar_surface, (head_radius * 2, head_radius * 2)) + + # Create a circular mask + mask_surface = pygame.Surface((head_radius * 2, head_radius * 2), pygame.SRCALPHA) + pygame.draw.circle(mask_surface, (255, 255, 255), (head_radius, head_radius), head_radius) + + # Apply mask to avatar + avatar_scaled.blit(mask_surface, (0, 0), special_flags=pygame.BLEND_RGBA_MULT) + + # Blend avatar with head color + head_surface.blit(avatar_scaled, (0, 0), special_flags=pygame.BLEND_RGBA_MULT) + + # Draw the head on the main surface + surface.blit(head_surface, (int(head.x - head_radius), int(head.y - head_radius))) + + # Draw cuter eyes + eye_offset = Vector2(7 * self.scale, 0) + left_eye = head + eye_offset.rotate(0) + right_eye = head + eye_offset.rotate(180) + + eye_radius = int(5 * self.scale) + pupil_radius = int(3 * self.scale) + + # Draw eye whites + pygame.draw.circle(surface, (255, 255, 255), (int(left_eye.x), int(left_eye.y)), eye_radius) + pygame.draw.circle(surface, (255, 255, 255), (int(right_eye.x), int(right_eye.y)), eye_radius) + + # Draw pupils with a slight upward offset + pupil_offset = Vector2(0, -1 * self.scale) + pygame.draw.circle( + surface, (0, 0, 0), (int(left_eye.x + pupil_offset.x), int(left_eye.y + pupil_offset.y)), pupil_radius + ) + pygame.draw.circle( + surface, (0, 0, 0), (int(right_eye.x + pupil_offset.x), int(right_eye.y + pupil_offset.y)), pupil_radius + ) + + def activate(self) -> None: + self.state = "spawn" + self.scale = 0.1 + + def deactivate(self) -> None: + self.state = "despawn" + + def spawn(self) -> None: + self.alive = True + self.activate() + self.segments = [Vector2(random.randint(0, self.width), random.randint(self.min_y, self.max_y))] + self.x = int(self.segments[0].x) + self.y = int(self.segments[0].y) + self.color = self.generate_color() + + def despawn(self) -> None: + self.deactivate() diff --git a/ardent-andromedas/src/ecosystem/speech_bubble.py b/ardent-andromedas/src/ecosystem/speech_bubble.py new file mode 100644 index 0000000..5a019b7 --- /dev/null +++ b/ardent-andromedas/src/ecosystem/speech_bubble.py @@ -0,0 +1,119 @@ +from __future__ import annotations + +import time +from typing import TYPE_CHECKING + +import pygame + +if TYPE_CHECKING: + from .critter import Critter + + +class SpeechBubble: + """A class representing a speech bubble for a critter.""" + + def __init__( + self, + critter: Critter, + content: str, + screen_width: int, + screen_height: int, + duration: float = 5.0, + bg_color: str = "#FFFFFF", + text_color: tuple[int, int, int] = (0, 0, 0), + border_color: tuple[int, int, int] = (0, 0, 0), + font_size: int = 28, + padding: int = 20, + border_radius: int = 20, + max_width: int = 400, + ) -> None: + """Initialize the SpeechBubble.""" + self.critter = critter + self.content = content + self.duration = duration + self.creation_time = time.time() + self.bg_color = self.hex_to_rgb(bg_color) + self.text_color = text_color + self.border_color = border_color + self.font_size = font_size + self.padding = padding + self.border_radius = border_radius + self.max_width = max_width + self.opacity = 51 + self.screen_width = screen_width + self.screen_height = screen_height + + self.font = pygame.font.Font(None, font_size) + self.surface = None + self.position = (self.critter.x, self.critter.y) + self._create_surface() + + def hex_to_rgb(self, hex_color: str) -> tuple[int, int, int]: + """Convert hex color string to RGB tuple.""" + hex_color = hex_color.lstrip("#") + return tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4)) + + def _create_surface(self) -> None: + words = self.content.split() + lines = [] + current_line = [] + for word in words: + test_line = " ".join([*current_line, word]) + if self.font.size(test_line)[0] <= self.max_width - 2 * self.padding: + current_line.append(word) + else: + if current_line: + lines.append(" ".join(current_line)) + current_line = [word] + if current_line: + lines.append(" ".join(current_line)) + + text_surfaces = [self.font.render(line, True, self.text_color) for line in lines] + max_width = max(surface.get_width() for surface in text_surfaces) + total_height = sum(surface.get_height() for surface in text_surfaces) + + self.bubble_body = pygame.Surface( + (max_width + 2 * self.padding, total_height + 2 * self.padding), pygame.SRCALPHA + ) + pygame.draw.rect( + self.bubble_body, + (*self.bg_color, self.opacity), + self.bubble_body.get_rect(), + border_radius=self.border_radius, + ) + + y_offset = self.padding + for surface in text_surfaces: + self.bubble_body.blit(surface, (self.padding, y_offset)) + y_offset += surface.get_height() + + self._draw_bubble() + + def _draw_bubble(self) -> None: + bubble_width, bubble_height = self.bubble_body.get_size() + + self.surface = pygame.Surface((bubble_width, bubble_height), pygame.SRCALPHA) + + bubble_rect = pygame.Rect(0, 0, bubble_width, bubble_height) + pygame.draw.rect(self.surface, (*self.bg_color, self.opacity), bubble_rect, border_radius=self.border_radius) + + pygame.draw.rect(self.surface, self.border_color, bubble_rect, width=2, border_radius=self.border_radius) + + self.surface.blit(self.bubble_body, (0, 0)) + + def update(self, delta: float) -> None: + critter_x, critter_y = self.critter.x, self.critter.y + bubble_width, bubble_height = self.surface.get_size() + x = critter_x - bubble_width // 2 + y = critter_y - bubble_height - 20 + + x = max(0, min(x, self.screen_width - bubble_width)) + y = max(0, min(y, self.screen_height - bubble_height)) + + self.position = (x, y) + + def draw(self, surface: pygame.Surface) -> None: + surface.blit(self.surface, self.position) + + def is_expired(self) -> bool: + return time.time() - self.creation_time > self.duration diff --git a/ardent-andromedas/src/ecosystem/wordclouds.py b/ardent-andromedas/src/ecosystem/wordclouds.py new file mode 100644 index 0000000..74154a9 --- /dev/null +++ b/ardent-andromedas/src/ecosystem/wordclouds.py @@ -0,0 +1,49 @@ +import pygame +from wordcloud import WordCloud + + +class WordCloudObject: + """Represents a full-screen word cloud object in the game.""" + + def __init__(self, words: str, width: int, height: int, speed: float) -> None: + """Initialize the WordCloudObject.""" + self.words = words + self.width = width + self.height = height + self.speed = speed + self.image = None + self.strength = 24 + self.needs_regeneration = True + + def generate_wordcloud(self, surface: pygame.Surface, words: str) -> None: + """Generate a new word cloud with white text.""" + try: + self.wordcloud = WordCloud( + width=self.width, + height=self.height, + background_color=None, + mode="RGBA", + color_func=lambda *_args, **_kwargs: (self.strength, self.strength, self.strength), + ).generate(words) + except ValueError: + self.wordcloud = None + + if self.wordcloud: + wordcloud_image = self.wordcloud.to_image() + self.image = pygame.image.fromstring(wordcloud_image.tobytes(), wordcloud_image.size, wordcloud_image.mode) + self.image = self.image.convert_alpha() + + def change_words(self, new_words: str) -> None: + """Change the words in the word cloud and mark for regeneration.""" + self.words = new_words + self.needs_regeneration = True + + def draw(self, surface: pygame.Surface) -> None: + """Draw the word cloud on the given surface using blend mode.""" + if self.needs_regeneration or self.image is None: + self.generate_wordcloud(surface, self.words) + self.needs_regeneration = False + + if self.image: + # Blend the word cloud with the background + surface.blit(self.image, (0, 0), special_flags=pygame.BLEND_RGBA_SUB) diff --git a/ardent-andromedas/src/scripts.py b/ardent-andromedas/src/scripts.py new file mode 100644 index 0000000..e14de8f --- /dev/null +++ b/ardent-andromedas/src/scripts.py @@ -0,0 +1,90 @@ +"""Utility scripts for running various commands and development tasks. + +This module provides functions to execute different commands and development +workflows using Poetry. It includes functions for formatting, linting, and +running different versions of the application. +""" + +import os +import signal +import subprocess +import sys +from contextlib import suppress +from pathlib import Path + + +def run_command(command: list[str]) -> None: + """Run a command in a subprocess with proper environment setup and signal handling. + + Args: + ---- + command (list[str]): The command to run as a list of strings. + + """ + env = os.environ.copy() + env["PYTHONPATH"] = str(Path(__file__).parent.resolve()) + # Run the process and make sure to terminate it properly + process = subprocess.Popen(command, env=env) + try: + process.communicate() + except KeyboardInterrupt: + if sys.platform == "win32": + process.send_signal(signal.CTRL_C_EVENT) + else: + process.send_signal(signal.SIGINT) + finally: + with suppress(ProcessLookupError): + process.terminate() + process.wait(timeout=5) + if process.poll() is None: + process.kill() + + +def lint() -> None: + """Run the 'ruff check' and 'ruff format' command using Poetry.""" + run_command(["poetry", "run", "ruff", "check"]) + run_command(["poetry", "run", "ruff", "format"]) + + +def run() -> None: + """Run the main application using Poetry.""" + run_command(["poetry", "run", "python", "src/app.py"]) + + +def run_interactive() -> None: + """Run the interactive version of the application using Poetry.""" + run_command(["poetry", "run", "python", "src/app.py", "--interactive"]) + + +def run_discord_test() -> None: + """Run the Discord test version of the application using Poetry.""" + run_command(["poetry", "run", "python", "src/app.py", "--test"]) + + +def run_gifs() -> None: + """Run the GIF generation version of the application using Poetry.""" + run_command(["poetry", "run", "python", "src/app.py", "--gifs"]) + + +def dev() -> None: + """Run lint and the main application in sequence.""" + run_command(["poetry", "run", "lint"]) + run_command(["poetry", "run", "run"]) + + +def dev_interactive() -> None: + """Run lint and the interactive version of the application.""" + run_command(["poetry", "run", "lint"]) + run_command(["poetry", "run", "run-interactive"]) + + +def dev_discord_test() -> None: + """Run lint and the Discord test version of the application.""" + run_command(["poetry", "run", "lint"]) + run_command(["poetry", "run", "run-test"]) + + +def dev_gifs() -> None: + """Run lint and the GIF generation version of the application.""" + run_command(["poetry", "run", "lint"]) + run_command(["poetry", "run", "run-gifs"]) diff --git a/ardent-andromedas/src/storage/__init__.py b/ardent-andromedas/src/storage/__init__.py new file mode 100644 index 0000000..22d47e6 --- /dev/null +++ b/ardent-andromedas/src/storage/__init__.py @@ -0,0 +1,5 @@ +"""Storage module for managing database operations.""" + +from .models import CommandType, Database, GuildConfig, UserInfo + +__all__ = ["Database", "CommandType", "GuildConfig", "UserInfo"] diff --git a/ardent-andromedas/src/storage/models.py b/ardent-andromedas/src/storage/models.py new file mode 100644 index 0000000..f8516da --- /dev/null +++ b/ardent-andromedas/src/storage/models.py @@ -0,0 +1,202 @@ +import logging +from dataclasses import dataclass +from datetime import UTC, datetime +from enum import Enum + +import aiosqlite + + +class CommandType(str, Enum): + """Enum for command types.""" + + ON_LOAD = "on_load" + GET = "get" + INSERT = "insert" + + +@dataclass +class GuildConfig: + """Represents guild configuration.""" + + guild_id: int + allowed_channels: list[int] + gif_channel: int | None + + +@dataclass +class UserInfo: + """Represents user information. + + Attributes + ---------- + user_id : int + The unique identifier for the user. + guild_id : int + The unique identifier for the guild the user belongs to. + avatar_data : bytes | None + The binary data of the user's avatar image, if available. + role_color : int | None + The color associated with the user's role, if any. + last_updated : datetime + The timestamp of when the user information was last updated. + + """ + + user_id: int + guild_id: int + avatar_data: bytes | None + role_color: int | None + last_updated: datetime + + +class Database: + """Handles database operations.""" + + def __init__(self, db_name: str) -> None: + """Initialize the Database. + + Args: + ---- + db_name (str): The name of the database. + + """ + self.db_file_path = f"{db_name}.sqlite3" + self.logger = logging.getLogger(__name__) + + async def initialize(self) -> None: + """Initialize the database and create tables if they don't exist.""" + try: + async with aiosqlite.connect(self.db_file_path) as db: + await db.execute(""" + CREATE TABLE IF NOT EXISTS guild_config ( + guild_id INTEGER PRIMARY KEY, + allowed_channels TEXT NOT NULL, + gif_channel INTEGER + ) + """) + await db.execute(""" + CREATE TABLE IF NOT EXISTS user_info ( + user_id INTEGER NOT NULL, + guild_id INTEGER NOT NULL, + avatar_data BLOB, + role_color TEXT, + last_updated TIMESTAMP NOT NULL, + PRIMARY KEY (user_id, guild_id) + ) + """) + await db.commit() + self.logger.info("Database initialized successfully.") + except aiosqlite.Error: + self.logger.exception("Failed to initialize database") + raise + + async def execute(self, command: CommandType, query: str | None = None, parameters: tuple | None = None) -> None: + """Execute a database command. + + Args: + ---- + command (CommandType): The type of command to execute. + query (Optional[str], optional): The SQL query. Defaults to None. + parameters (Optional[tuple], optional): Query parameters. Defaults to None. + + Raises: + ------ + aiosqlite.Error: If there's an error executing the database command. + + """ + try: + async with aiosqlite.connect(self.db_file_path) as db: + db: aiosqlite.Connection + cursor = await db.cursor() + match command: + case CommandType.ON_LOAD: + await cursor.execute(query) + await db.commit() + self.logger.info("Database loaded successfully.") + case CommandType.INSERT: + await cursor.execute(query, parameters) + await db.commit() + case CommandType.GET: + return await db.execute_fetchall(query, parameters) + except aiosqlite.Error: + self.logger.exception("Database error (%s)", command.value) + raise + + async def set_guild_config(self, config: GuildConfig) -> None: + """Set or update the configuration for a guild.""" + query = """ + INSERT OR REPLACE INTO guild_config (guild_id, allowed_channels, gif_channel) + VALUES (?, ?, ?) + """ + allowed_channels_str = ",".join(map(str, config.allowed_channels)) + data = (config.guild_id, allowed_channels_str, config.gif_channel) + await self.execute(command=CommandType.INSERT, query=query, parameters=data) + + async def get_guild_config(self, guild_id: int) -> GuildConfig | None: + """Retrieve the configuration for a specific guild.""" + query = """ + SELECT guild_id, allowed_channels, gif_channel + FROM guild_config + WHERE guild_id = ? + """ + async with aiosqlite.connect(self.db_file_path) as db, db.execute(query, (guild_id,)) as cursor: + row = await cursor.fetchone() + if row: + guild_id, allowed_channels_str, gif_channel = row + allowed_channels = list(map(int, allowed_channels_str.split(","))) + return GuildConfig(guild_id, allowed_channels, gif_channel) + return None + + async def get_user_info(self, user_id: int, guild_id: int) -> UserInfo | None: + """Retrieve user info for a specific user in a specific guild.""" + query = """ + SELECT user_id, guild_id, avatar_data, role_color, last_updated + FROM user_info + WHERE user_id = ? AND guild_id = ? + """ + async with aiosqlite.connect(self.db_file_path) as db, db.execute(query, (user_id, guild_id)) as cursor: + row = await cursor.fetchone() + if row: + return UserInfo( + user_id=row[0], + guild_id=row[1], + avatar_data=row[2], + role_color=row[3], + last_updated=datetime.fromisoformat(row[4]).replace(tzinfo=UTC), + ) + return None + + async def set_user_info(self, user_info: UserInfo) -> None: + """Set or update the user info for a specific user in a specific guild.""" + query = """ + INSERT OR REPLACE INTO user_info (user_id, guild_id, avatar_data, role_color, last_updated) + VALUES (?, ?, ?, ?, ?) + """ + data = ( + user_info.user_id, + user_info.guild_id, + user_info.avatar_data, + user_info.role_color, + user_info.last_updated.isoformat(), + ) + await self.execute(command=CommandType.INSERT, query=query, parameters=data) + + async def get_random_user_info(self) -> UserInfo | None: + """Retrieve random user info for a specific guild.""" + query = """ + SELECT user_id, guild_id, avatar_data, role_color, last_updated + FROM user_info + ORDER BY RANDOM() + LIMIT 1 + """ + async with aiosqlite.connect(self.db_file_path) as db, db.execute(query) as cursor: + row = await cursor.fetchone() + if row: + return UserInfo( + user_id=row[0], + guild_id=row[1], + avatar_data=row[2], + role_color=row[3], + last_updated=datetime.fromisoformat(row[4]), + ) + return None